Skip to content

Commit

Permalink
fix: Fix deepcopy crashing because of __getattr__
Browse files Browse the repository at this point in the history
Issue #73: #73
PR #119: #119
  • Loading branch information
pawamoy authored Nov 25, 2022
1 parent b2f6e3f commit 11b023b
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 24 deletions.
28 changes: 20 additions & 8 deletions src/griffe/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from functools import lru_cache
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, ItemsView, KeysView, ValuesView

from griffe.mixins import GetMembersMixin, SetMembersMixin

Expand All @@ -31,17 +31,29 @@ def __setitem__(self, key: Path, value: list[str]) -> None:
def __bool__(self) -> bool:
return True

def __getattr__(self, name: str, default: Any = None) -> Any:
"""Lookup attributes into underlying dict.
def keys(self) -> KeysView:
"""Return the collection keys.
Parameters:
name: The attribute name.
default: A default value.
Returns:
The collection keys.
"""
return self._data.keys()

def values(self) -> ValuesView:
"""Return the collection values.
Returns:
The collection values.
"""
return self._data.values()

def items(self) -> ItemsView:
"""Return the collection items.
Returns:
The attribute of the underlying dict.
The collection items.
"""
return getattr(self._data, name, default)
return self._data.items()

# TODO: remove once Python 3.7 support is dropped
@lru_cache(maxsize=None) # noqa: B019
Expand Down
171 changes: 155 additions & 16 deletions src/griffe/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from contextlib import suppress
from pathlib import Path
from textwrap import dedent
from typing import Any, Callable, cast
from typing import Any, Callable, Union, cast

from griffe.collections import LinesCollection, ModulesCollection
from griffe.docstrings.dataclasses import DocstringSection
Expand Down Expand Up @@ -318,7 +318,7 @@ class Object(GetMembersMixin, SetMembersMixin, ObjectAliasMixin, SerializationMi

kind: Kind
is_alias: bool = False
is_collection = False
is_collection: bool = False

def __init__(
self,
Expand Down Expand Up @@ -754,7 +754,7 @@ def _endlineno(self) -> int | None:
return blockfinder.last


class Alias(ObjectAliasMixin):
class Alias(ObjectAliasMixin): # noqa: WPS338
"""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,
Expand All @@ -777,6 +777,7 @@ class Alias(ObjectAliasMixin):
"""

is_alias: bool = True
is_collection: bool = False

def __init__(
self,
Expand Down Expand Up @@ -818,19 +819,6 @@ def __init__(
def __repr__(self) -> str:
return f"<Alias({self.name!r}, {self.target_path!r})>"

def __getattr__(self, name: str) -> Any:
# forward everything to the target
if self._passed_through:
raise CyclicAliasError([self.target_path])
self._passed_through = True
try:
attr = getattr(self.target, name)
except CyclicAliasError as error:
raise CyclicAliasError([self.target_path] + error.chain)
finally:
self._passed_through = False
return attr

def __getitem__(self, key):
# not handled by __getattr__
return self.target[key]
Expand All @@ -842,6 +830,8 @@ def __setitem__(self, key, value):
def __len__(self) -> int:
return 1

# SPECIAL PROXIES -------------------------------

@property
def kind(self) -> Kind:
"""Return the target's kind, or Kind.ALIAS if the target cannot be resolved.
Expand Down Expand Up @@ -922,6 +912,155 @@ def modules_collection(self) -> ModulesCollection:
# no need to forward to the target
return self.parent.modules_collection # type: ignore[union-attr] # we assume there's always a parent

# GENERIC OBJECT PROXIES --------------------------------

@property
def docstring(self): # noqa: D102
return self.target.docstring

@property
def members(self): # noqa: D102
return self.target.members

@property
def labels(self): # noqa: D102
return self.target.labels

@property
def imports(self): # noqa: D102
return self.target.imports

@property
def exports(self): # noqa: D102
return self.target.exports

@property
def aliases(self): # noqa: D102
return self.target.aliases

def member_is_exported(self, member: Object | Alias, explicitely: bool = True) -> bool: # noqa: D102
return self.target.member_is_exported(member, explicitely)

def is_kind(self, kind: str | Kind | set[str | Kind]) -> bool: # noqa: D102
return self.target.is_kind(kind)

@property
def is_module(self): # noqa: D102
return self.target.is_module

@property
def is_class(self): # noqa: D102
return self.target.is_class

@property
def is_function(self): # noqa: D102
return self.target.is_function

@property
def is_attribute(self): # noqa: D102
return self.target.is_attribute

def has_labels(self, labels: set[str]) -> bool: # noqa: D102
return self.target.has_labels(labels)

def filter_members(self, *predicates: Callable[[Object | Alias], bool]) -> dict[str, Object | Alias]: # noqa: D102
return self.target.filter_members(*predicates)

@property
def modules(self): # noqa: D102
return self.target.modules

@property
def classes(self): # noqa: D102
return self.target.classes

@property
def functions(self): # noqa: D102
return self.target.functions

@property
def attributes(self): # noqa: D102
return self.target.attributes

@property
def module(self): # noqa: D102
return self.target.module

@property
def package(self): # noqa: D102
return self.target.package

@property
def filepath(self): # noqa: D102
return self.target.filepath

@property
def relative_filepath(self): # noqa: D102
return self.target.relative_filepath

@property
def canonical_path(self): # noqa: D102
return self.target.canonical_path

@property
def lines_collection(self): # noqa: D102
return self.target.lines_collection

@property
def lines(self): # noqa: D102
return self.target.lines

@property
def source(self): # noqa: D102
return self.target.source

def resolve(self, name: str) -> str: # noqa: D102
return self.target.resolve(name)

# SPECIFIC MODULE/CLASS/FUNCTION/ATTRIBUTE PROXIES ---------------

@property
def _filepath(self) -> Path | list[Path] | None: # noqa: D102
return cast(Module, self.target)._filepath # noqa: WPS437

@property
def bases(self) -> list[Name | Expression | str]: # noqa: D102
return cast(Class, self.target).bases

@property
def decorators(self) -> list[Decorator]: # noqa: D102
return cast(Union[Class, Function], self.target).decorators

@property
def overloads(self) -> dict[str, list[Function]] | list[Function] | None: # noqa: D102
return cast(Union[Module, Class, Function], self.target).overloads

@property
def parameters(self) -> Parameters: # noqa: D102
return cast(Function, self.target).parameters

@property
def returns(self) -> str | Name | Expression | None: # noqa: D102
return cast(Function, self.target).returns

@property
def setter(self) -> Function | None: # noqa: D102
return cast(Function, self.target).setter

@property
def deleter(self) -> Function | None: # noqa: D102
return cast(Function, self.target).deleter

@property
def value(self) -> str | None: # noqa: D102
return cast(Attribute, self.target).value

@property
def annotation(self) -> str | Name | Expression | None: # noqa: D102
return cast(Attribute, self.target).annotation

# SPECIFIC ALIAS METHOD AND PROPERTIES -----------------

@property
def target(self) -> Object | Alias:
"""Resolve and return the target, if possible.
Expand Down
11 changes: 11 additions & 0 deletions tests/test_dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests for the `dataclasses` module."""

from copy import deepcopy

from griffe.dataclasses import Docstring, Module
from griffe.loader import GriffeLoader
from tests.helpers import module_vtree, temporary_pypackage
Expand Down Expand Up @@ -57,3 +59,12 @@ def test_has_docstrings_does_not_trigger_alias_resolution():
package = loader.load_module(tmp_package.name)
assert not package.has_docstrings
assert not package["mod_a.someobj"].resolved


def test_deepcopy():
"""Assert we can deep-copy object trees."""
loader = GriffeLoader()
mod = loader.load_module("griffe")

deepcopy(mod)
deepcopy(mod.as_dict())

0 comments on commit 11b023b

Please sign in to comment.