Skip to content

Commit

Permalink
fix: Always consider special objects ("dunder" attributes/methods/etc…
Browse files Browse the repository at this point in the history
….) to be public

The reasoning is that is a special object, even though it is not accessed directly, provides public functionality: comparing objects, multiplying them, printing their Python representation or stringified version, etc..

Issue-294: #294
Issue-295: #295
  • Loading branch information
pawamoy committed Jun 17, 2024
1 parent ea90952 commit 3319410
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 2 deletions.
21 changes: 19 additions & 2 deletions src/griffe/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ def has_private_name(self) -> bool:
"""Whether this object/alias has a private name."""
return self.name.startswith("_") # type: ignore[attr-defined]

@property
def has_special_name(self) -> bool:
"""Whether this object/alias has a special name."""
return self.name.startswith("__") and self.name.endswith("__") # type: ignore[attr-defined]

@property
def is_exported(self) -> bool:
"""Whether this object/alias is exported (listed in `__all__`)."""
Expand Down Expand Up @@ -365,18 +370,30 @@ def is_public(self) -> bool:
- Otherwise, the object is public.
"""
# TODO: Return regular True/False values in next version.

# Give priority to the `public` attribute if it is set.
if self.public is not None: # type: ignore[attr-defined]
return _True if self.public else _False # type: ignore[return-value,attr-defined]

# If the object is defined at the module-level and is listed in `__all__`, it is public.
# If the parent module defines `__all__` but does not list the object, it is private.
if self.parent and self.parent.is_module and bool(self.parent.exports): # type: ignore[attr-defined]
return _True if self.name in self.parent.exports else _False # type: ignore[attr-defined,return-value]
if self.has_private_name:

# Special objects are always considered public.
# Even if we don't access them directly, they are used through different *public* means
# like instantiating classes (`__init__`), using operators (`__eq__`), etc..
if self.has_private_name and not self.has_special_name:
return _False # type: ignore[return-value]
# The following condition effectively filters out imported objects.

# TODO: In a future version, we will support two conventions regarding imports:
# - `from a import x as x` marks `x` as public.
# - `from a import *` marks all wildcard imported objects as public.
# The following condition effectively filters out imported objects.
if self.is_alias and not (self.inherited or (self.parent and self.parent.is_alias)): # type: ignore[attr-defined]
return _False # type: ignore[return-value]

# If we reached this point, the object is public.
return _True # type: ignore[return-value]


Expand Down
13 changes: 13 additions & 0 deletions tests/test_public_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,16 @@ def test_not_detecting_imported_objects_as_public() -> None:
with temporary_visited_module("from abc import ABC\ndef func(): ...") as module:
assert not module["ABC"].is_public
assert module["func"].is_public # control case


def test_detecting_dunder_attributes_as_public() -> None:
"""Dunder attributes (methods, etc.) must be considered public."""
with temporary_visited_module(
"""
def __getattr__(name): ...
class A:
def __init__(self): ...
""",
) as module:
assert module["__getattr__"].is_public
assert module["A.__init__"].is_public

0 comments on commit 3319410

Please sign in to comment.