From f8b5f27e9ca7e218432ff3b648c40b48faa1ee5e Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 28 Aug 2026 19:02:11 +0200 Subject: [PATCH 1/2] Support cached properties --- src/classify/classification.py | 8 ++++++++ src/classify/filters.py | 23 ++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/classify/classification.py b/src/classify/classification.py index cdaeba1..26ed535 100644 --- a/src/classify/classification.py +++ b/src/classify/classification.py @@ -16,6 +16,7 @@ ) from .filters import ( is_attribute, + is_cached_property, is_data_descriptor, is_inner_class, is_method, @@ -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: diff --git a/src/classify/filters.py b/src/classify/filters.py index 01718e7..455bd51 100644 --- a/src/classify/filters.py +++ b/src/classify/filters.py @@ -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: From b30808a4d80ffc1dfb553d22764d655a4ee0c948 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 8 Sep 2026 15:40:48 +0200 Subject: [PATCH 2/2] Make sure we don't double-count cached properties --- tests/test_classification.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_classification.py b/tests/test_classification.py index 46a5713..71dfeda 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -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): ...