diff --git a/docs/changelog.md b/docs/changelog.md index a61a44b2..aabdb6f5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,7 @@ ## Unreleased +- Assume that dataclasses have no dynamic attributes (#456) - Treat Thrift enums as compatible with `int` (#455) - Fix treatment of `TypeVar` with bounds or constraints as callables (#454) diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py index f1c162cd..ec99ce22 100644 --- a/pyanalyze/name_check_visitor.py +++ b/pyanalyze/name_check_visitor.py @@ -85,7 +85,13 @@ from .patma import PatmaVisitor from .shared_options import Paths, ImportPaths, EnforceNoUnused from .reexport import ImplicitReexportTracker -from .safe import safe_getattr, is_hashable, all_of_type, safe_issubclass +from .safe import ( + safe_getattr, + is_hashable, + all_of_type, + safe_issubclass, + is_dataclass_type, +) from .stacked_scopes import ( EMPTY_ORIGIN, AbstractConstraint, @@ -4114,7 +4120,7 @@ def _has_only_known_attributes(self, typ: object) -> bool: return True ts_finder = self.checker.ts_finder if ( - ts_finder.has_stubs(typ) + (ts_finder.has_stubs(typ) or is_dataclass_type(typ)) and not ts_finder.has_attribute(typ, "__getattr__") and not ts_finder.has_attribute(typ, "__getattribute__") and not attributes.may_have_dynamic_attributes(typ) diff --git a/pyanalyze/safe.py b/pyanalyze/safe.py index 14f1d7f3..735f16fc 100644 --- a/pyanalyze/safe.py +++ b/pyanalyze/safe.py @@ -169,3 +169,12 @@ def get_fully_qualified_name(obj: object) -> Optional[str]: if hasattr(obj, "__module__") and hasattr(obj, "__qualname__"): return f"{obj.__module__}.{obj.__qualname__}" return None + + +def is_dataclass_type(cls: type) -> bool: + """Like dataclasses.is_dataclass(), but works correctly for a + non-dataclass subclass of a dataclass.""" + try: + return "__dataclass_fields__" in cls.__dict__ + except Exception: + return False