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
  • Loading branch information
pawamoy committed Nov 25, 2022
1 parent d16c641 commit c64f145
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 60 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
103 changes: 51 additions & 52 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 @@ -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 Down Expand Up @@ -928,149 +928,148 @@ def modules_collection(self) -> ModulesCollection:
# GENERIC OBJECT PROXIES --------------------------------

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

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

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

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

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

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

# @property
# def runtime(self):
# return self.target.runtime

def member_is_exported(self, member: Object | Alias, explicitely: bool = True) -> bool:
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:
def is_kind(self, kind: str | Kind | set[str | Kind]) -> bool: # noqa: D102
return self.target.is_kind(kind)

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

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

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

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

def has_labels(self, labels: set[str]) -> bool:
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]:
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):
def modules(self): # noqa: D102
return self.target.modules

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

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

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

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

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

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

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

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

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

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

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

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

# SPECIFIC MODULE/CLASS/FUNCTION/ATTRIBUTE PROXIES --------------------------------
@property
def bases(self) -> list[Name | Expression | str]:
return self.target.bases
def _filepath(self) -> Path | list[Path] | None: # noqa: D102
return cast(Module, self.target)._filepath # noqa: WPS437

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

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

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

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

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

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

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

@property
def annotation(self) -> str | Name | Expression | None:
return self.target.annotation
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 -----------------

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 c64f145

Please sign in to comment.