Skip to content

Fix "ignored exception in hasattr" in dmypy #19428

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 18, 2025

Conversation

sterliakov
Copy link
Collaborator

Fixes #19425. That property has no setter so it should safe to exclude. It can raise in inconsistent state that arises when it is not copied last?

@sterliakov sterliakov marked this pull request as ready for review July 11, 2025 17:09
Copy link
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Copy link
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix! Raising an exception in a property is a bit questionable, so an alternative way to do this would be to make setter a method. But let's merge this since this should fix still the underlying bug.

@JukkaL JukkaL merged commit a0665e1 into python:master Jul 18, 2025
20 checks passed
@ilevkivskyi
Copy link
Member

@JukkaL why do we even try to copy properties in replace_object_state()? We have a lot of properties on expressions/statements so it means that there are thousand of AttributeErrors thrown on the setattr() calls because properties are not writeable.

Well there is a TODO item there, maybe we should try addressing it?

@sterliakov
Copy link
Collaborator Author

@ilevkivskyi Compiled and non-compiled versions of properties are vastly different, and we need to support both modes. In particular, properties accessed on class in compiled code aren't instances of builtins.property...

cat <<EOF >c.py
class A:
    @property
    def foo(self) -> int:
        return 0
EOF

$ mypyc c.py
running build_ext
building 'c' extension
creating build/temp.linux-x86_64-cpython-313/build
cc -pthread -fno-strict-overflow -Wsign-compare -Wunreachable-code -DNDEBUG -g -O3 -Wall -fPIC -fPIC -I/home/stas/Documents/Work/mypy/mypyc/lib-rt -I/tmp/tmp.Y6rtvxbGHL/.venv/include -I/home/stas/.local/share/uv/python/cpython-3.13.3-linux-x86_64-gnu/include/python3.13 -c build/__native.c -o build/temp.linux-x86_64-cpython-313/build/__native.o -O3 -g1 -Werror -Wno-unused-function -Wno-unused-label -Wno-unreachable-code -Wno-unused-variable -Wno-unused-command-line-argument -Wno-unknown-warning-option -Wno-unused-but-set-variable -Wno-ignored-optimization-argument -Wno-cpp
creating build/lib.linux-x86_64-cpython-313
cc -pthread -shared -Wl,--exclude-libs,ALL -LModules/_hacl build/temp.linux-x86_64-cpython-313/build/__native.o -L/home/stas/.local/share/uv/python/cpython-3.13.3-linux-x86_64-gnu/lib -o build/lib.linux-x86_64-cpython-313/c.cpython-313-x86_64-linux-gnu.so
copying build/lib.linux-x86_64-cpython-313/c.cpython-313-x86_64-linux-gnu.so -> 
$ python -c 'from c import A; print(isinstance(A.foo, property))'
False
$ rm *.so
$ python -c 'from c import A; print(isinstance(A.foo, property))'
True

Unless mypyc sets some extra attribute on the generated property, I don't know how to detect them without brute force.

@brianschubert
Copy link
Member

Hmm, what about inspect.isgetsetdescriptor(A.foo)?

@sterliakov
Copy link
Collaborator Author

@brianschubert They are intentionally included for some reason!

mypy/mypy/util.py

Lines 367 to 409 in f209888

def get_class_descriptors(cls: type[object]) -> Sequence[str]:
import inspect # Lazy import for minor startup speed win
# Maintain a cache of type -> attributes defined by descriptors in the class
# (that is, attributes from __slots__ and C extension classes)
if cls not in fields_cache:
members = inspect.getmembers(
cls, lambda o: inspect.isgetsetdescriptor(o) or inspect.ismemberdescriptor(o)
)
fields_cache[cls] = [x for x, y in members if x != "__weakref__" and x != "__dict__"]
return fields_cache[cls]
def replace_object_state(
new: object, old: object, copy_dict: bool = False, skip_slots: tuple[str, ...] = ()
) -> None:
"""Copy state of old node to the new node.
This handles cases where there is __dict__ and/or attribute descriptors
(either from slots or because the type is defined in a C extension module).
Assume that both objects have the same __class__.
"""
if hasattr(old, "__dict__"):
if copy_dict:
new.__dict__ = dict(old.__dict__)
else:
new.__dict__ = old.__dict__
for attr in get_class_descriptors(old.__class__):
if attr in skip_slots:
continue
try:
if hasattr(old, attr):
setattr(new, attr, getattr(old, attr))
elif hasattr(new, attr):
delattr(new, attr)
# There is no way to distinguish getsetdescriptors that allow
# writes from ones that don't (I think?), so we just ignore
# AttributeErrors if we need to.
# TODO: What about getsetdescriptors that act like properties???
except AttributeError:
pass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

AssertionError raised after #19248
4 participants