diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-11-25 14:29:30 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-11-29 19:25:59 -0500 |
| commit | 3e3e3ab0d46b8912649afc7c3eb63b76c19d93fe (patch) | |
| tree | f2c5b6fde3c6679138b255056d1b38db2ac67fc6 /lib/sqlalchemy/orm/decl_base.py | |
| parent | 78833af4e650d37e6257cfbb541e4db56e2a285f (diff) | |
| download | sqlalchemy-3e3e3ab0d46b8912649afc7c3eb63b76c19d93fe.tar.gz | |
annotated / DC forms for association proxy
Added support for the :func:`.association_proxy` extension function to
take part within Python ``dataclasses`` configuration, when using
the native dataclasses feature described at
:ref:`orm_declarative_native_dataclasses`. Included are attribute-level
arguments including :paramref:`.association_proxy.init` and
:paramref:`.association_proxy.default_factory`.
Documentation for association proxy has also been updated to use
"Annotated Declarative Table" forms within examples, including type
annotations used for :class:`.AssocationProxy` itself.
Also modernized documentation examples in sqlalchemy.ext.mutable,
which was not up to date even for 1.4 style code.
Corrected typing for relationship(secondary) where "secondary"
accepts a callable (i.e. lambda) as well
Fixes: #8878
Fixes: #8876
Fixes: #8880
Change-Id: Ibd4f3591155a89f915713393e103e61cc072ed57
Diffstat (limited to 'lib/sqlalchemy/orm/decl_base.py')
| -rw-r--r-- | lib/sqlalchemy/orm/decl_base.py | 102 |
1 files changed, 66 insertions, 36 deletions
diff --git a/lib/sqlalchemy/orm/decl_base.py b/lib/sqlalchemy/orm/decl_base.py index 21e3c3344..1e716e687 100644 --- a/lib/sqlalchemy/orm/decl_base.py +++ b/lib/sqlalchemy/orm/decl_base.py @@ -44,6 +44,7 @@ from .base import InspectionAttr from .descriptor_props import CompositeProperty from .descriptor_props import SynonymProperty from .interfaces import _AttributeOptions +from .interfaces import _DCAttributeOptions from .interfaces import _IntrospectsAnnotations from .interfaces import _MappedAttribute from .interfaces import _MapsColumns @@ -1262,6 +1263,8 @@ class _ClassScanMapperConfig(_MapperConfig): or self.is_dataclass_prior_to_mapping ) + look_for_dataclass_things = bool(self.dataclass_setup_arguments) + for k in list(collected_attributes): if k in _include_dunders: @@ -1304,15 +1307,21 @@ class _ClassScanMapperConfig(_MapperConfig): "accidentally placed at the end of the line?" % k ) continue - elif not isinstance(value, (Column, MapperProperty, _MapsColumns)): + elif look_for_dataclass_things and isinstance( + value, dataclasses.Field + ): + # we collected a dataclass Field; dataclasses would have + # set up the correct state on the class + continue + elif not isinstance(value, (Column, _DCAttributeOptions)): # using @declared_attr for some object that - # isn't Column/MapperProperty; remove from the clsdict_view + # isn't Column/MapperProperty/_DCAttributeOptions; remove + # from the clsdict_view # and place the evaluated value onto the class. - if not k.startswith("__"): - collected_attributes.pop(k) - self._warn_for_decl_attributes(cls, k, value) - if not late_mapped: - setattr(cls, k, value) + collected_attributes.pop(k) + self._warn_for_decl_attributes(cls, k, value) + if not late_mapped: + setattr(cls, k, value) continue # we expect to see the name 'metadata' in some valid cases; # however at this point we see it's assigned to something trying @@ -1372,38 +1381,59 @@ class _ClassScanMapperConfig(_MapperConfig): # by util._extract_mapped_subtype before we got here. assert expect_annotations_wo_mapped - if ( - isinstance(value, (MapperProperty, _MapsColumns)) - and value._has_dataclass_arguments - and not self.dataclass_setup_arguments - ): - if isinstance(value, MapperProperty): - argnames = [ - "init", - "default_factory", - "repr", - "default", - ] - else: - argnames = ["init", "default_factory", "repr"] + if isinstance(value, _DCAttributeOptions): + + if ( + value._has_dataclass_arguments + and not look_for_dataclass_things + ): + if isinstance(value, MapperProperty): + argnames = [ + "init", + "default_factory", + "repr", + "default", + ] + else: + argnames = ["init", "default_factory", "repr"] + + args = { + a + for a in argnames + if getattr( + value._attribute_options, f"dataclasses_{a}" + ) + is not _NoArg.NO_ARG + } - args = { - a - for a in argnames - if getattr( - value._attribute_options, f"dataclasses_{a}" + raise exc.ArgumentError( + f"Attribute '{k}' on class {cls} includes " + f"dataclasses argument(s): " + f"{', '.join(sorted(repr(a) for a in args))} but " + f"class does not specify " + "SQLAlchemy native dataclass configuration." ) - is not _NoArg.NO_ARG - } - raise exc.ArgumentError( - f"Attribute '{k}' on class {cls} includes dataclasses " - f"argument(s): " - f"{', '.join(sorted(repr(a) for a in args))} but " - f"class does not specify " - "SQLAlchemy native dataclass configuration." - ) - our_stuff[k] = value + if not isinstance(value, (MapperProperty, _MapsColumns)): + # filter for _DCAttributeOptions objects that aren't + # MapperProperty / mapped_column(). Currently this + # includes AssociationProxy. pop it from the things + # we're going to map and set it up as a descriptor + # on the class. + collected_attributes.pop(k) + + # Assoc Prox (or other descriptor object that may + # use _DCAttributeOptions) is usually here, except if + # 1. we're a + # dataclass, dataclasses would have removed the + # attr here or 2. assoc proxy is coming from a + # superclass, we want it to be direct here so it + # tracks state or 3. assoc prox comes from + # declared_attr, uncommon case + setattr(cls, k, value) + continue + + our_stuff[k] = value # type: ignore def _extract_declared_columns(self) -> None: our_stuff = self.properties |
