Skip to content

Commit 3319410

Browse files
committed
fix: Always consider special objects ("dunder" attributes/methods/etc.) 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
1 parent ea90952 commit 3319410

File tree

2 files changed

+32
-2
lines changed

2 files changed

+32
-2
lines changed

src/griffe/mixins.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,11 @@ def has_private_name(self) -> bool:
299299
"""Whether this object/alias has a private name."""
300300
return self.name.startswith("_") # type: ignore[attr-defined]
301301

302+
@property
303+
def has_special_name(self) -> bool:
304+
"""Whether this object/alias has a special name."""
305+
return self.name.startswith("__") and self.name.endswith("__") # type: ignore[attr-defined]
306+
302307
@property
303308
def is_exported(self) -> bool:
304309
"""Whether this object/alias is exported (listed in `__all__`)."""
@@ -365,18 +370,30 @@ def is_public(self) -> bool:
365370
- Otherwise, the object is public.
366371
"""
367372
# TODO: Return regular True/False values in next version.
373+
374+
# Give priority to the `public` attribute if it is set.
368375
if self.public is not None: # type: ignore[attr-defined]
369376
return _True if self.public else _False # type: ignore[return-value,attr-defined]
377+
378+
# If the object is defined at the module-level and is listed in `__all__`, it is public.
379+
# If the parent module defines `__all__` but does not list the object, it is private.
370380
if self.parent and self.parent.is_module and bool(self.parent.exports): # type: ignore[attr-defined]
371381
return _True if self.name in self.parent.exports else _False # type: ignore[attr-defined,return-value]
372-
if self.has_private_name:
382+
383+
# Special objects are always considered public.
384+
# Even if we don't access them directly, they are used through different *public* means
385+
# like instantiating classes (`__init__`), using operators (`__eq__`), etc..
386+
if self.has_private_name and not self.has_special_name:
373387
return _False # type: ignore[return-value]
374-
# The following condition effectively filters out imported objects.
388+
375389
# TODO: In a future version, we will support two conventions regarding imports:
376390
# - `from a import x as x` marks `x` as public.
377391
# - `from a import *` marks all wildcard imported objects as public.
392+
# The following condition effectively filters out imported objects.
378393
if self.is_alias and not (self.inherited or (self.parent and self.parent.is_alias)): # type: ignore[attr-defined]
379394
return _False # type: ignore[return-value]
395+
396+
# If we reached this point, the object is public.
380397
return _True # type: ignore[return-value]
381398

382399

tests/test_public_api.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,16 @@ def test_not_detecting_imported_objects_as_public() -> None:
88
with temporary_visited_module("from abc import ABC\ndef func(): ...") as module:
99
assert not module["ABC"].is_public
1010
assert module["func"].is_public # control case
11+
12+
13+
def test_detecting_dunder_attributes_as_public() -> None:
14+
"""Dunder attributes (methods, etc.) must be considered public."""
15+
with temporary_visited_module(
16+
"""
17+
def __getattr__(name): ...
18+
class A:
19+
def __init__(self): ...
20+
""",
21+
) as module:
22+
assert module["__getattr__"].is_public
23+
assert module["A.__init__"].is_public

0 commit comments

Comments
 (0)