summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/decl_base.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2022-11-25 14:29:30 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2022-11-29 19:25:59 -0500
commit3e3e3ab0d46b8912649afc7c3eb63b76c19d93fe (patch)
treef2c5b6fde3c6679138b255056d1b38db2ac67fc6 /lib/sqlalchemy/orm/decl_base.py
parent78833af4e650d37e6257cfbb541e4db56e2a285f (diff)
downloadsqlalchemy-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.py102
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