Skip to content

Commit

Permalink
refactor: Load properties as attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Nov 30, 2022
1 parent 727456d commit 5c97a45
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 15 deletions.
41 changes: 30 additions & 11 deletions src/griffe/agents/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
from griffe.expressions import Expression, Name
from griffe.importer import dynamic_import

if sys.version_info < (3, 8):
from cached_property import cached_property
else:
from functools import cached_property # noqa: WPS440

empty = Signature.empty


Expand Down Expand Up @@ -109,7 +114,11 @@ def _should_create_alias(parent: ObjectNode, child: ObjectNode, current_module_p
# like ast -> _ast -> ast (here we inspect _ast)
# or os -> posix/nt -> os (here we inspect posix/nt)

child_module = getmodule(child.obj)
child_obj = child.obj
if isinstance(child_obj, cached_property):
child_obj = child_obj.func

child_module = getmodule(child_obj)

if not child_module:
return None
Expand Down Expand Up @@ -344,7 +353,8 @@ def inspect_cached_property(self, node: ObjectNode) -> None:
Parameters:
node: The node to inspect.
"""
self.handle_function(node, {"cached property"})
node.obj = node.obj.func
self.handle_function(node, {"cached", "property"})

def inspect_property(self, node: ObjectNode) -> None:
"""Inspect a property.
Expand Down Expand Up @@ -376,15 +386,24 @@ def handle_function(self, node: ObjectNode, labels: set | None = None): # noqa:
else:
returns = _convert_object_to_annotation(return_annotation, parent=self.current)

function = Function(
name=node.name,
parameters=parameters,
returns=returns,
docstring=self._get_docstring(node),
)
if labels:
function.labels |= labels
self.current[node.name] = function
obj: Attribute | Function
labels = labels or set()
if "property" in labels:
obj = Attribute(
name=node.name,
value=None,
annotation=returns,
docstring=self._get_docstring(node),
)
else:
obj = Function(
name=node.name,
parameters=parameters,
returns=returns,
docstring=self._get_docstring(node),
)
obj.labels |= labels
self.current[node.name] = obj

def inspect_attribute(self, node: ObjectNode) -> None:
"""Inspect an attribute.
Expand Down
4 changes: 2 additions & 2 deletions src/griffe/agents/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,6 @@ def kind(self) -> ObjectKind:
return ObjectKind.STATICMETHOD
if self.is_classmethod:
return ObjectKind.CLASSMETHOD
if self.is_method_descriptor:
return ObjectKind.METHOD_DESCRIPTOR
if self.is_method:
return ObjectKind.METHOD
if self.is_builtin_method:
Expand All @@ -343,6 +341,8 @@ def kind(self) -> ObjectKind:
return ObjectKind.CACHED_PROPERTY
if self.is_property:
return ObjectKind.PROPERTY
if self.is_method_descriptor:
return ObjectKind.METHOD_DESCRIPTOR
return ObjectKind.ATTRIBUTE

@cached_property
Expand Down
16 changes: 16 additions & 0 deletions src/griffe/agents/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"abc.abstractmethod": {"abstractmethod"},
"functools.cache": {"cached"},
"functools.cached_property": {"cached", "property"},
"cached_property.cached_property": {"cached", "property"},
"functools.lru_cache": {"cached"},
"dataclasses.dataclass": {"dataclass"},
}
Expand Down Expand Up @@ -335,6 +336,21 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
lineno = node.lineno

labels |= self.decorators_to_labels(decorators)

if "property" in labels:
attribute = Attribute(
name=node.name,
value=None,
annotation=safe_get_annotation(node.returns, parent=self.current),
lineno=node.lineno,
endlineno=node.end_lineno, # type: ignore[union-attr]
docstring=self._get_docstring(node),
runtime=not self.type_guarded,
)
attribute.labels |= labels
self.current[node.name] = attribute
return

base_property, property_function = self.get_base_property(decorators)

# handle parameters
Expand Down
7 changes: 6 additions & 1 deletion src/griffe/docstrings/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,12 @@ def _read_returns_section( # noqa: WPS231
else:
# try to retrieve the annotation from the docstring parent
with suppress(AttributeError, KeyError, ValueError):
annotation = docstring.parent.returns # type: ignore[union-attr]
if docstring.parent.is_function: # type: ignore[union-attr]
annotation = docstring.parent.returns # type: ignore[union-attr]
elif docstring.parent.is_attribute: # type: ignore[union-attr]
annotation = docstring.parent.annotation # type: ignore[union-attr]
else:
raise ValueError
if len(block) > 1:
if annotation.is_tuple:
annotation = annotation.tuple_item(index)
Expand Down
7 changes: 6 additions & 1 deletion src/griffe/docstrings/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,12 @@ def _read_returns_section( # noqa: WPS231
if annotation is None:
# try to retrieve the annotation from the docstring parent
with suppress(AttributeError, KeyError, ValueError):
annotation = docstring.parent.returns # type: ignore[union-attr]
if docstring.parent.is_function: # type: ignore[union-attr]
annotation = docstring.parent.returns # type: ignore[union-attr]
elif docstring.parent.is_attribute: # type: ignore[union-attr]
annotation = docstring.parent.annotation # type: ignore[union-attr]
else:
raise ValueError
if len(items) > 1:
if annotation.is_tuple:
annotation = annotation.tuple_item(index)
Expand Down
24 changes: 24 additions & 0 deletions tests/test_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,27 @@ def test_missing_dependency():
with pytest.raises(ImportError): # noqa: PT012
with suppress(ModuleNotFoundError):
inspect("package.module", filepath=filepath, import_paths=[tmp_package.tmpdir])


def test_inspect_properties_as_attributes():
"""Assert properties are created as attributes and not functions."""
with temporary_inspected_module(
"""
try:
from functools import cached_property
except ImportError:
from cached_property import cached_property
class C:
@property
def prop(self) -> bool:
return True
@cached_property
def cached_prop(self) -> int:
return 0
"""
) as module:
assert module["C.prop"].is_attribute
assert "property" in module["C.prop"].labels
assert module["C.cached_prop"].is_attribute
assert "cached" in module["C.cached_prop"].labels
21 changes: 21 additions & 0 deletions tests/test_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,24 @@ def method(self):
assert module.docstring.lineno == 2
assert module["C"].docstring.lineno == 6
assert module["C.method"].docstring.lineno == 10


def test_visit_properties_as_attributes():
"""Assert properties are created as attributes and not functions."""
with temporary_visited_module(
"""
from functools import cached_property
class C:
@property
def prop(self) -> bool:
return True
@cached_property
def cached_prop(self) -> int:
return 0
"""
) as module:
assert module["C.prop"].is_attribute
assert "property" in module["C.prop"].labels
assert module["C.cached_prop"].is_attribute
assert "cached" in module["C.cached_prop"].labels

0 comments on commit 5c97a45

Please sign in to comment.