Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/classify/classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from .filters import (
is_attribute,
is_cached_property,
is_data_descriptor,
is_inner_class,
is_method,
Expand Down Expand Up @@ -67,6 +68,13 @@ def classify[C](obj: type[C]) -> Class:
prop = Method.from_func(member.obj.fget, member.cls)
properties[member.name].append(prop)

## CACHED PROPERTIES
cached_props = [m for m in members if is_cached_property(m)]
for member in cached_props:
logger.debug("extracting cached property", member=member)
prop = Method.from_func(member.obj, member.cls)
properties[member.name].append(prop)

## DATA DESCRIPTORS
descriptors = [m for m in members if is_data_descriptor(m)]
for member in descriptors:
Expand Down
23 changes: 22 additions & 1 deletion src/classify/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,28 @@


def is_attribute(member: Member) -> bool:
return member.kind == "data" and not is_inner_class(member)
return (
member.kind == "data"
and not is_inner_class(member)
and not is_cached_property(member)
)


def is_cached_property(member: Member) -> bool:
# covers functools.cached_property and variations like
# django.utils.functional.cached_property.
# These are method descriptors wrapping a function, so they get
# reported as "method", but the descriptor is not callable.
cls = type(member.obj)
# getattr_static: getattr() would call arbitrary __getattr__
# implementations on member values
func = inspect.getattr_static(member.obj, "func", None)
return (
hasattr(cls, "__get__")
and not hasattr(cls, "__set__")
and not callable(member.obj)
and callable(func)
)


def is_data_descriptor(member: Member) -> bool:
Expand Down
8 changes: 8 additions & 0 deletions tests/test_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ def test_classify_includes_wrapped_methods(name):
assert name in structure.methods


@pytest.mark.parametrize("name", ["my_cached_prop", "my_dj_cached_prop"])
def test_classify_cached_properties_are_properties(name):
structure = classify(DummyClass)

assert name in structure.properties
assert name not in structure.methods


def test_classify_excludes_c_implemented_methods():
class MyDict(dict):
def mine(self): ...
Expand Down
Loading