From ed3f2c617239668d74ad3d86aeda0ca2030a5933 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 21 Apr 2021 10:39:09 -0400 Subject: [PATCH] Limit dc field logic to only fields that are definitely dc Fixed regression where recent changes to support Python dataclasses had the inadvertent effect that an ORM mapped class could not successfully override the ``__new__()`` method. In this case the "__new__" method comes out as staticmethod in cls.__dict__ vs. a function in the metaclass dict_, so comparing using identity fails. I was hoping not to have too much "dataclass" hardcoded, the logic here if it were generalized to other attribute declaration systems there would still have a flag that indicates an attribute is part of the "special declaration system". Fixes: #6331 Change-Id: Ia28a44fb57c668fa2fc5cd1ff38fd511f2c747e6 --- doc/build/changelog/unreleased_14/6331.rst | 7 +++++++ lib/sqlalchemy/orm/decl_base.py | 16 +++++++++------- test/orm/declarative/test_basic.py | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 doc/build/changelog/unreleased_14/6331.rst diff --git a/doc/build/changelog/unreleased_14/6331.rst b/doc/build/changelog/unreleased_14/6331.rst new file mode 100644 index 0000000000..0bcecaec90 --- /dev/null +++ b/doc/build/changelog/unreleased_14/6331.rst @@ -0,0 +1,7 @@ +.. change:: + :tags: bug, orm, regression, declarative + :tickets: 6331 + + Fixed regression where recent changes to support Python dataclasses had the + inadvertent effect that an ORM mapped class could not successfully override + the ``__new__()`` method. diff --git a/lib/sqlalchemy/orm/decl_base.py b/lib/sqlalchemy/orm/decl_base.py index 5a5d98a958..b3444f26f4 100644 --- a/lib/sqlalchemy/orm/decl_base.py +++ b/lib/sqlalchemy/orm/decl_base.py @@ -410,7 +410,7 @@ class _ClassScanMapperConfig(_MapperConfig): def local_attributes_for_class(): for name, obj in vars(cls).items(): - yield name, obj + yield name, obj, False else: field_names = set() @@ -421,10 +421,10 @@ class _ClassScanMapperConfig(_MapperConfig): field_names.add(field.name) yield field.name, _as_dc_declaredattr( field.metadata, sa_dataclass_metadata_key - ) + ), True for name, obj in vars(cls).items(): if name not in field_names: - yield name, obj + yield name, obj, False return local_attributes_for_class @@ -455,7 +455,7 @@ class _ClassScanMapperConfig(_MapperConfig): local_attributes_for_class, attribute_is_overridden ) - for name, obj in local_attributes_for_class(): + for name, obj, is_dataclass in local_attributes_for_class(): if name == "__mapper_args__": check_decl = _check_declared_props_nocascade( obj, name, cls @@ -567,15 +567,17 @@ class _ClassScanMapperConfig(_MapperConfig): # however, check for some more common mistakes else: self._warn_for_decl_attributes(base, name, obj) - elif name not in dict_ or dict_[name] is not obj: + elif is_dataclass and ( + name not in dict_ or dict_[name] is not obj + ): # here, we are definitely looking at the target class # and not a superclass. this is currently a # dataclass-only path. if the name is only # a dataclass field and isn't in local cls.__dict__, # put the object there. - # assert that the dataclass-enabled resolver agrees # with what we are seeing + assert not attribute_is_overridden(name, obj) if _is_declarative_props(obj): @@ -607,7 +609,7 @@ class _ClassScanMapperConfig(_MapperConfig): column_copies = self.column_copies # copy mixin columns to the mapped class - for name, obj in attributes_for_class(): + for name, obj, is_dataclass in attributes_for_class(): if isinstance(obj, Column): if attribute_is_overridden(name, obj): # if column has been overridden diff --git a/test/orm/declarative/test_basic.py b/test/orm/declarative/test_basic.py index e7c8f188d9..ecc824391d 100644 --- a/test/orm/declarative/test_basic.py +++ b/test/orm/declarative/test_basic.py @@ -2282,6 +2282,24 @@ class DeclarativeTest(DeclarativeTestBase): assert not hasattr(Foo, "data_hybrid") + def test_classes_can_override_new(self): + class MyTable(Base): + __tablename__ = "my_table" + id = Column(Integer, primary_key=True) + + def __new__(cls, *args, **kwargs): + return object.__new__(cls) + + def some_method(self): + pass + + @staticmethod + def some_static_method(self): + pass + + mt = MyTable(id=5) + eq_(mt.id, 5) + @testing.requires.python36 def test_kw_support_in_declarative_meta_init(self): # This will not fail if DeclarativeMeta __init__ supports **kw -- 2.47.3