--- /dev/null
+.. change::
+ :tags: bug, sql
+ :tickets: 13140
+
+ Improved the ability for :class:`.TypeDecorator` to produce a correct
+ ``repr()`` for "schema" types such as :class:`.Enum` and :class:`.Boolean`.
+ This is mostly to support the Alembic autogenerate use case so that custom
+ types render with relevant arguments present. Improved the architecture
+ used by :class:`.TypeEngine` to produce ``repr()`` strings to be more
+ modular for compound types like :class:`.TypeDecorator`.
else:
return super()._object_value_for_elem(elem)
- def __repr__(self) -> str:
- return util.generic_repr(
+ def repr_struct(self) -> util.GenericRepr:
+ return util.GenericRepr(
self, to_inspect=[ENUM, _StringType, sqltypes.Enum]
)
kw["retrieve_as_bitwise"] = self.retrieve_as_bitwise
return util.constructor_copy(self, cls, *self.values, **kw)
- def __repr__(self) -> str:
- return util.generic_repr(
+ def repr_struct(self) -> util.GenericRepr:
+ return util.GenericRepr(
self,
to_inspect=[SET, _StringType],
additional_kw=[
_NumericCommonType, sqltypes.Numeric[Union[decimal.Decimal, float]]
):
- def __repr__(self) -> str:
- return util.generic_repr(
+ def repr_struct(self) -> util.GenericRepr:
+ return util.GenericRepr(
self,
to_inspect=[_NumericType, _NumericCommonType, sqltypes.Numeric],
)
super().__init__(precision=precision, asdecimal=asdecimal, **kw)
self.scale = scale
- def __repr__(self) -> str:
- return util.generic_repr(
+ def repr_struct(self) -> util.GenericRepr:
+ return util.GenericRepr(
self, to_inspect=[_FloatType, _NumericCommonType, sqltypes.Float]
)
self.display_width = display_width
super().__init__(**kw)
- def __repr__(self) -> str:
- return util.generic_repr(
+ def repr_struct(self) -> util.GenericRepr:
+ return util.GenericRepr(
self,
to_inspect=[_IntegerType, _NumericCommonType, sqltypes.Integer],
)
self.national = national
super().__init__(**kw)
- def __repr__(self) -> str:
- return util.generic_repr(
+ def repr_struct(self) -> util.GenericRepr:
+ return util.GenericRepr(
self, to_inspect=[_StringType, sqltypes.String]
)
)
) from err
- def __repr__(self):
- return util.generic_repr(
+ def repr_struct(self):
+ return util.GenericRepr(
self,
additional_kw=[
("native_enum", True),
def __str__(self) -> str:
return str(self.compile())
+ def repr_struct(self) -> util.GenericRepr:
+ """Return a :class:`.GenericRepr` object representing this type.
+
+ This method is used to generate the repr string for the type.
+ Subclasses can override this to customize the repr structure.
+
+ .. versionadded:: 2.1
+
+ """
+ return util.GenericRepr(self)
+
def __repr__(self) -> str:
- return util.generic_repr(self)
+ return str(self.repr_struct())
class TypeEngineMixin:
def sort_key_function(self) -> Optional[Callable[[Any], Any]]: # type: ignore # noqa: E501
return self.impl_instance.sort_key_function
- def __repr__(self) -> str:
- return util.generic_repr(self, to_inspect=self.impl_instance)
+ def repr_struct(self) -> util.GenericRepr:
+ """Return a :class:`.GenericRepr` object representing this type.
+
+ For TypeDecorator, this returns a repr structure based on the
+ impl instance but with the TypeDecorator's class name.
+
+ .. versionadded:: 2.1
+
+ """
+ return self.impl_instance.repr_struct().set_class_name(
+ self.__class__.__name__
+ )
class Variant(TypeDecorator[_T]):
from .langhelpers import format_argspec_plus as format_argspec_plus
from .langhelpers import generic_fn_descriptor as generic_fn_descriptor
from .langhelpers import generic_repr as generic_repr
+from .langhelpers import GenericRepr as GenericRepr
from .langhelpers import get_callable_argspec as get_callable_argspec
from .langhelpers import get_cls_kwargs as get_cls_kwargs
from .langhelpers import get_func_kwargs as get_func_kwargs
return func_or_cls
-def generic_repr(
- obj: Any,
- additional_kw: Sequence[Tuple[str, Any]] = (),
- to_inspect: Optional[Union[object, List[object]]] = None,
- omit_kwarg: Sequence[str] = (),
-) -> str:
- """Produce a __repr__() based on direct association of the __init__()
- specification vs. same-named attributes present.
+class GenericRepr:
+ """Encapsulates the logic for creating a generic __repr__() string.
+ This class allows for the repr structure to be created, then modified
+ (e.g., changing the class name), before being rendered as a string.
+
+ .. versionadded:: 2.1
"""
- if to_inspect is None:
- to_inspect = [obj]
- else:
- to_inspect = _collections.to_list(to_inspect)
- missing = object()
+ __slots__ = (
+ "_obj",
+ "_additional_kw",
+ "_to_inspect",
+ "_omit_kwarg",
+ "_class_name",
+ )
- pos_args = []
- kw_args: _collections.OrderedDict[str, Any] = _collections.OrderedDict()
- vargs = None
- for i, insp in enumerate(to_inspect):
- try:
- spec = compat.inspect_getfullargspec(insp.__init__)
- except TypeError:
- continue
- else:
- default_len = len(spec.defaults) if spec.defaults else 0
- if i == 0:
- if spec.varargs:
- vargs = spec.varargs
- if default_len:
- pos_args.extend(spec.args[1:-default_len])
- else:
- pos_args.extend(spec.args[1:])
- else:
- kw_args.update(
- [(arg, missing) for arg in spec.args[1:-default_len]]
- )
+ _obj: Any
+ _additional_kw: Sequence[Tuple[str, Any]]
+ _to_inspect: List[object]
+ _omit_kwarg: Sequence[str]
+ _class_name: Optional[str]
- if default_len:
- assert spec.defaults
- kw_args.update(
- [
- (arg, default)
- for arg, default in zip(
- spec.args[-default_len:], spec.defaults
- )
- ]
- )
- output: List[str] = []
+ def __init__(
+ self,
+ obj: Any,
+ additional_kw: Sequence[Tuple[str, Any]] = (),
+ to_inspect: Optional[Union[object, List[object]]] = None,
+ omit_kwarg: Sequence[str] = (),
+ ):
+ """Create a GenericRepr object.
+
+ :param obj: The object being repr'd
+ :param additional_kw: Additional keyword arguments to check for in
+ the repr, as a sequence of 2-tuples of (name, default_value)
+ :param to_inspect: One or more objects whose __init__ signature
+ should be inspected. If not provided, defaults to [obj].
+ :param omit_kwarg: Sequence of keyword argument names to omit from
+ the repr output
+ """
+ self._obj = obj
+ self._additional_kw = additional_kw
+ self._to_inspect = (
+ [obj] if to_inspect is None else _collections.to_list(to_inspect)
+ )
+ self._omit_kwarg = omit_kwarg
+ self._class_name = None
- output.extend(repr(getattr(obj, arg, None)) for arg in pos_args)
+ def set_class_name(self, class_name: str) -> GenericRepr:
+ """Set the class name to be used in the repr.
- if vargs is not None and hasattr(obj, vargs):
- output.extend([repr(val) for val in getattr(obj, vargs)])
+ By default, the class name is taken from obj.__class__.__name__.
+ This method allows it to be overridden.
- for arg, defval in kw_args.items():
- if arg in omit_kwarg:
- continue
- try:
- val = getattr(obj, arg, missing)
- if val is not missing and val != defval:
- output.append("%s=%r" % (arg, val))
- except Exception:
- pass
+ :param class_name: The class name to use
+ :return: self, for method chaining
+ """
+ self._class_name = class_name
+ return self
- if additional_kw:
- for arg, defval in additional_kw:
+ def __str__(self) -> str:
+ """Produce the __repr__() string based on the configured parameters."""
+ obj = self._obj
+ to_inspect = self._to_inspect
+ additional_kw = self._additional_kw
+ omit_kwarg = self._omit_kwarg
+
+ missing = object()
+
+ pos_args = []
+ kw_args: _collections.OrderedDict[str, Any] = (
+ _collections.OrderedDict()
+ )
+ vargs = None
+ for i, insp in enumerate(to_inspect):
+ try:
+ spec = compat.inspect_getfullargspec(insp.__init__) # type: ignore[misc] # noqa: E501
+ except TypeError:
+ continue
+ else:
+ default_len = len(spec.defaults) if spec.defaults else 0
+ if i == 0:
+ if spec.varargs:
+ vargs = spec.varargs
+ if default_len:
+ pos_args.extend(spec.args[1:-default_len])
+ else:
+ pos_args.extend(spec.args[1:])
+ else:
+ kw_args.update(
+ [(arg, missing) for arg in spec.args[1:-default_len]]
+ )
+
+ if default_len:
+ assert spec.defaults
+ kw_args.update(
+ [
+ (arg, default)
+ for arg, default in zip(
+ spec.args[-default_len:], spec.defaults
+ )
+ ]
+ )
+ output: List[str] = []
+
+ output.extend(repr(getattr(obj, arg, None)) for arg in pos_args)
+
+ if vargs is not None and hasattr(obj, vargs):
+ output.extend([repr(val) for val in getattr(obj, vargs)])
+
+ for arg, defval in kw_args.items():
+ if arg in omit_kwarg:
+ continue
try:
val = getattr(obj, arg, missing)
if val is not missing and val != defval:
except Exception:
pass
- return "%s(%s)" % (obj.__class__.__name__, ", ".join(output))
+ if additional_kw:
+ for arg, defval in additional_kw:
+ try:
+ val = getattr(obj, arg, missing)
+ if val is not missing and val != defval:
+ output.append("%s=%r" % (arg, val))
+ except Exception:
+ pass
+
+ class_name = (
+ self._class_name
+ if self._class_name is not None
+ else obj.__class__.__name__
+ )
+ return "%s(%s)" % (class_name, ", ".join(output))
+
+
+def generic_repr(
+ obj: Any,
+ additional_kw: Sequence[Tuple[str, Any]] = (),
+ to_inspect: Optional[Union[object, List[object]]] = None,
+ omit_kwarg: Sequence[str] = (),
+) -> str:
+ """Produce a __repr__() based on direct association of the __init__()
+ specification vs. same-named attributes present.
+
+ """
+ return str(
+ GenericRepr(
+ obj,
+ additional_kw=additional_kw,
+ to_inspect=to_inspect,
+ omit_kwarg=omit_kwarg,
+ )
+ )
def class_hierarchy(cls):
from sqlalchemy.testing.schema import Table
from sqlalchemy.testing.util import picklers
from sqlalchemy.types import UserDefinedType
+from sqlalchemy.util import GenericRepr
def _all_dialect_modules():
)
def test_resolve(self, value, expected):
is_(literal(value).type, expected)
+
+
+class ReprTest(fixtures.TestBase):
+ """test suite for TypeEngine repr_struct() and GenericRepr"""
+
+ def test_generic_repr_basic(self):
+ """Test GenericRepr basic functionality."""
+ t = String(50)
+ gr = GenericRepr(t)
+ eq_(str(gr), "String(length=50)")
+
+ def test_generic_repr_set_class_name(self):
+ """Test GenericRepr.set_class_name() method."""
+ t = String(50)
+ gr = GenericRepr(t)
+ gr.set_class_name("CustomString")
+ eq_(str(gr), "CustomString(length=50)")
+
+ def test_type_engine_repr_struct(self):
+ """Test TypeEngine.repr_struct() returns GenericRepr."""
+ t = String(50)
+ gr = t.repr_struct()
+ assert isinstance(gr, GenericRepr)
+ eq_(str(gr), "String(length=50)")
+
+ @testing.combinations(
+ (Integer(), "Integer()"),
+ (String(50), "String(length=50)"),
+ (VARCHAR(100), "VARCHAR(length=100)"),
+ (NUMERIC(10, 2), "NUMERIC(precision=10, scale=2)"),
+ (
+ Enum("a", "b", "c", name="myenum"),
+ "Enum('a', 'b', 'c', name='myenum')",
+ ),
+ (
+ mysql.NUMERIC(10, 2, unsigned=True),
+ "NUMERIC(unsigned=True, precision=10, scale=2)",
+ ),
+ (
+ mysql.VARCHAR(50, charset="utf8"),
+ "VARCHAR(charset='utf8', length=50)",
+ ),
+ (mysql.ENUM("a", "b", "c"), "ENUM('a', 'b', 'c')"),
+ (mysql.SET("a", "b", "c"), "SET('a', 'b', 'c')"),
+ argnames="type_,expected",
+ )
+ def test_type_repr(self, type_, expected):
+ """Test repr for various type objects."""
+ eq_(repr(type_), expected)
+
+ @testing.variation("impl_type", ["enum", "boolean", "string"])
+ @testing.variation("has_name", [True, False])
+ def test_type_decorator_repr(self, impl_type, has_name):
+ """Test TypeDecorator wrapping various SchemaType objects."""
+
+ if impl_type.enum:
+
+ class MyType(TypeDecorator):
+ impl = Enum
+ cache_ok = True
+
+ if has_name:
+ t = MyType("a", "b", "c", name="myenum")
+ eq_(repr(t), "MyType('a', 'b', 'c', name='myenum')")
+ else:
+ t = MyType("x", "y", "z")
+ eq_(repr(t), "MyType('x', 'y', 'z')")
+
+ elif impl_type.boolean:
+
+ class MyType(TypeDecorator):
+ impl = Boolean
+ cache_ok = True
+
+ if has_name:
+ t = MyType(create_constraint=True, name="mybool")
+ eq_(
+ repr(t),
+ "MyType(create_constraint=True, name='mybool')",
+ )
+ else:
+ t = MyType()
+ eq_(repr(t), "MyType()")
+
+ elif impl_type.string:
+
+ class MyType(TypeDecorator):
+ impl = String
+ cache_ok = True
+
+ if has_name:
+ # String doesn't have a name parameter, use length
+ t = MyType(100)
+ eq_(repr(t), "MyType(length=100)")
+ else:
+ t = MyType()
+ eq_(repr(t), "MyType()")
+ else:
+ impl_type.fail()