From 7320640d42ebb4546f787fe458d5032a67ea20b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 17 Oct 2023 13:39:10 +0200 Subject: [PATCH] fix: Add missing proxies (methods/properties) to aliases --- src/griffe/dataclasses.py | 51 +++++++++++++++++++++++++++++++++++---- tests/test_dataclasses.py | 20 +++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/griffe/dataclasses.py b/src/griffe/dataclasses.py index 4ab1f5ef..c450e208 100644 --- a/src/griffe/dataclasses.py +++ b/src/griffe/dataclasses.py @@ -372,6 +372,7 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({self.name!r}, {self.lineno!r}, {self.endlineno!r})" def __bool__(self) -> bool: + # Prevent using `__len__`. return True def __len__(self) -> int: @@ -723,7 +724,7 @@ def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]: return base -class Alias(ObjectAliasMixin): +class Alias(ObjectAliasMixin, SerializationMixin): """This class represents an alias, or indirection, to an object declared in another module. Aliases represent objects that are in the scope of a module or class, @@ -804,6 +805,10 @@ def __delitem__(self, key: str | tuple[str, ...]): # not handled by __getattr__ del self.target[key] + def __bool__(self) -> bool: + # Prevent using `__len__`. + return True + def __len__(self) -> int: return 1 @@ -883,19 +888,27 @@ def inherited_members(self) -> dict[str, Alias]: # noqa: D102 for name, member in final_target.inherited_members.items() } + def as_json(self, *, full: bool = False, **kwargs: Any) -> str: # noqa: D102 + try: + return self.final_target.as_json(full=full, **kwargs) + except (AliasResolutionError, CyclicAliasError): + return super().as_json(full=full, **kwargs) + # GENERIC OBJECT PROXIES -------------------------------- # The following methods and properties exist on the target(s). # We first try to reach the final target, trigerring alias resolution errors # and cyclic aliases errors early. We avoid recursing in the alias chain. @property - def lineno(self) -> int | None: - """The target lineno or the alias lineno.""" + def extra(self) -> dict: # noqa: D102 + return self.final_target.extra + + @property + def lineno(self) -> int | None: # noqa: D102 return self.final_target.lineno @property - def endlineno(self) -> int | None: - """The target endlineno or the alias endlineno.""" + def endlineno(self) -> int | None: # noqa: D102 return self.final_target.endlineno @property @@ -966,6 +979,10 @@ def filepath(self) -> Path | list[Path]: # noqa: D102 def relative_filepath(self) -> Path: # noqa: D102 return self.final_target.relative_filepath + @property + def relative_package_filepath(self) -> Path: # noqa: D102 + return self.final_target.relative_package_filepath + @property def canonical_path(self) -> str: # noqa: D102 return self.final_target.canonical_path @@ -1011,6 +1028,30 @@ def bases(self) -> list[Expr | str]: # noqa: D102 def decorators(self) -> list[Decorator]: # noqa: D102 return cast(Union[Class, Function], self.target).decorators + @property + def imports_future_annotations(self) -> bool: # noqa: D102 + return cast(Module, self.final_target).imports_future_annotations + + @property + def is_init_module(self) -> bool: # noqa: D102 + return cast(Module, self.final_target).is_init_module + + @property + def is_package(self) -> bool: # noqa: D102 + return cast(Module, self.final_target).is_package + + @property + def is_subpackage(self) -> bool: # noqa: D102 + return cast(Module, self.final_target).is_subpackage + + @property + def is_namespace_package(self) -> bool: # noqa: D102 + return cast(Module, self.final_target).is_namespace_package + + @property + def is_namespace_subpackage(self) -> bool: # noqa: D102 + return cast(Module, self.final_target).is_namespace_subpackage + @property def overloads(self) -> dict[str, list[Function]] | list[Function] | None: # noqa: D102 return cast(Union[Module, Class, Function], self.target).overloads diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py index ebf245a3..ef452de2 100644 --- a/tests/test_dataclasses.py +++ b/tests/test_dataclasses.py @@ -4,6 +4,7 @@ from copy import deepcopy +import griffe from griffe.dataclasses import Docstring, Module from griffe.loader import GriffeLoader from griffe.tests import module_vtree, temporary_pypackage @@ -70,3 +71,22 @@ def test_deepcopy() -> None: deepcopy(mod) deepcopy(mod.as_dict()) + + +def test_alias_proxies() -> None: + """Assert that the Alias class has all the necessary methods and properties. + + Parameters: + cls: The class to check. + """ + api = griffe.load("griffe") + alias_members = set(api["dataclasses.Alias"].all_members.keys()) + for cls in ( + api["dataclasses.Module"], + api["dataclasses.Class"], + api["dataclasses.Function"], + api["dataclasses.Attribute"], + ): + for name in cls.all_members: + if not name.startswith("_") or name.startswith("__"): + assert name in alias_members