diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c30e87a..008258d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog ========= +0.13.0 (2022-02-26) +------------------- + +- Improved robustness of determining whether a class is pure Python (#65) + 0.12.1 (2022-02-12) ------------------- diff --git a/docs/advanced.rst b/docs/advanced.rst index 77668f5..4af1e8a 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -18,7 +18,7 @@ Use the following configuration: repos: - repo: https://github.com/ariebovenberg/slotscheck - rev: v0.12.1 + rev: v0.13.0 hooks: - id: slotscheck # If your Python files are not importable from the project root, diff --git a/pyproject.toml b/pyproject.toml index 3501694..dcdb6c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "slotscheck" -version = "0.12.1" +version = "0.13.0" description = "Ensure your __slots__ are working properly." authors = ["Arie Bovenberg "] license = "MIT" diff --git a/src/slotscheck/checks.py b/src/slotscheck/checks.py index 348f97e..b8dfd4b 100644 --- a/src/slotscheck/checks.py +++ b/src/slotscheck/checks.py @@ -1,7 +1,6 @@ "Slots-related checks and inspection tools" import builtins -import sys -from functools import lru_cache +import platform from typing import Collection, Iterator, Optional @@ -22,10 +21,7 @@ def has_slots(c: type) -> bool: return ( "__slots__" in c.__dict__ or c in _SLOTTED_BUILTINS - or ( - not issubclass(c, BaseException) - and not is_purepython_class(c) # type: ignore - ) + or (not issubclass(c, BaseException) and not is_pure_python(c)) ) @@ -56,26 +52,25 @@ def has_duplicate_slots(c: type) -> bool: } -_UNSETTABLE_ATTRITUBE_MSG = ( - "cannot set '_SLOTSCHECK_POKE' attribute of immutable type" - if sys.version_info > (3, 10) - else "can't set attributes of built-in/extension type" -) +# The 'is a pure python class' logic below is adapted +# from https://stackoverflow.com/a/41012823/ +# If the active Python interpreter is the official CPython interpreter, +# prefer a more reliable CPython-specific solution guaranteed to succeed. +if platform.python_implementation() == "CPython": + # Magic number defined by the Python codebase at "Include/object.h". + Py_TPFLAGS_HEAPTYPE = 1 << 9 -@lru_cache(maxsize=None) -def is_purepython_class(t: type) -> bool: - "Whether a class is defined in Python, or an extension/C module" - # AFIAK there is no _easy_ way to check if a class is pure Python. - # One symptom of a non-native class is that it is not possible to - # set attributes on it. - # Let's use that as an easy proxy for now. - try: - t._SLOTSCHECK_POKE = 1 # type: ignore - except TypeError as e: - if e.args[0].startswith(_UNSETTABLE_ATTRITUBE_MSG): - return False - raise # some other error we may want to know about - else: - del t._SLOTSCHECK_POKE # type: ignore - return True + def is_pure_python(cls: type) -> bool: + "Whether the class is pure-Python or C-based" + return bool(cls.__flags__ & Py_TPFLAGS_HEAPTYPE) + + +# Else, fallback to a CPython-agnostic solution typically but *NOT* +# necessarily succeeding. For all real-world objects of interest, this is +# effectively successful. Edge cases exist but are suitably rare. +else: + + def is_pure_python(cls: type) -> bool: + "Whether the class is pure-Python or C-based" + return "__dict__" in dir(cls) or hasattr(cls, "__slots__") diff --git a/src/slotscheck/cli.py b/src/slotscheck/cli.py index 58cacc8..8649692 100644 --- a/src/slotscheck/cli.py +++ b/src/slotscheck/cli.py @@ -25,7 +25,7 @@ has_duplicate_slots, has_slotless_base, has_slots, - is_purepython_class, + is_pure_python, slots, slots_overlap, ) @@ -448,7 +448,7 @@ def _print_report( classes_by_status = groupby( classes, key=lambda c: None - if not is_purepython_class(c) + if not is_pure_python(c) else True if has_slots(c) else False, diff --git a/tests/src/test_checks.py b/tests/src/test_checks.py index edeabf3..70db860 100644 --- a/tests/src/test_checks.py +++ b/tests/src/test_checks.py @@ -87,9 +87,8 @@ def test_slots(self, klass): def test_no_slots(self, klass): assert not has_slots(klass) - def test_opaque_class(self): - with pytest.raises(TypeError, match="BOOM!"): - assert not has_slots(_UnsettableClass) + def test_immutable_class(self): + assert not has_slots(_UnsettableClass) class TestSlotsOverlap: