From f806491fca4b08623d7fcffc375bd5cbe3790e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20Klus=C3=A1k?= Date: Mon, 17 Aug 2020 11:58:56 -0400 Subject: Add support for classical mapping of dataclasses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added support for direct mapping of Python classes that are defined using the Python ``dataclasses`` decorator. See the section :ref:`mapping_dataclasses` for background. Pull request courtesy Václav Klusák. Fixes: #5027 Closes: #5516 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/5516 Pull-request-sha: bb48c63d1561ca48c954ad9f84a3eb2646571115 Change-Id: Ie33db2aae4adeeb5d99633fe926b9c30bab0b885 --- lib/sqlalchemy/orm/descriptor_props.py | 2 +- lib/sqlalchemy/orm/mapper.py | 24 ++++++++++++++++++++---- lib/sqlalchemy/testing/requirements.py | 4 ++++ 3 files changed, 25 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index 39cf86e34..c2efa24a1 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -56,7 +56,7 @@ class DescriptorProperty(MapperProperty): if self.descriptor is None: desc = getattr(mapper.class_, self.key, None) - if mapper._is_userland_descriptor(desc): + if mapper._is_userland_descriptor(self.key, desc): self.descriptor = desc if self.descriptor is None: diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 446f6790e..755d4afc7 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -56,6 +56,12 @@ from ..sql import util as sql_util from ..sql import visitors from ..util import HasMemoized +try: + import dataclasses +except ImportError: + # The dataclasses module was added in Python 3.7 + dataclasses = None + _mapper_registry = weakref.WeakKeyDictionary() _already_compiling = False @@ -2632,7 +2638,7 @@ class Mapper( return result - def _is_userland_descriptor(self, obj): + def _is_userland_descriptor(self, assigned_name, obj): if isinstance( obj, ( @@ -2643,7 +2649,14 @@ class Mapper( ): return False else: - return True + return assigned_name not in self._dataclass_fields + + @HasMemoized.memoized_attribute + def _dataclass_fields(self): + if dataclasses is None or not dataclasses.is_dataclass(self.class_): + return frozenset() + + return {field.name for field in dataclasses.fields(self.class_)} def _should_exclude(self, name, assigned_name, local, column): """determine whether a particular property should be implicitly @@ -2656,16 +2669,19 @@ class Mapper( # check for class-bound attributes and/or descriptors, # either local or from an inherited class + # ignore dataclass field default values if local: if self.class_.__dict__.get( assigned_name, None ) is not None and self._is_userland_descriptor( - self.class_.__dict__[assigned_name] + assigned_name, self.class_.__dict__[assigned_name] ): return True else: attr = self.class_manager._get_class_attr_mro(assigned_name, None) - if attr is not None and self._is_userland_descriptor(attr): + if attr is not None and self._is_userland_descriptor( + assigned_name, attr + ): return True if ( diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index 9b8caac2e..4114137d4 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -1142,6 +1142,10 @@ class SuiteRequirements(Requirements): "Python version 3.7 or greater is required.", ) + @property + def dataclasses(self): + return self.python37 + @property def cpython(self): return exclusions.only_if( -- cgit v1.2.1