Skip to content

Commit

Permalink
Improved robustness of determining whether a class is pure Python (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
ariebovenberg committed Feb 26, 2022
1 parent 16e3dc6 commit ff33f50
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 34 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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)
-------------------

Expand Down
2 changes: 1 addition & 1 deletion docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]
license = "MIT"
Expand Down
49 changes: 22 additions & 27 deletions src/slotscheck/checks.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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))
)


Expand Down Expand Up @@ -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__")
4 changes: 2 additions & 2 deletions src/slotscheck/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
has_duplicate_slots,
has_slotless_base,
has_slots,
is_purepython_class,
is_pure_python,
slots,
slots_overlap,
)
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions tests/src/test_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit ff33f50

Please sign in to comment.