Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for namespace packages #871

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
644e888
Bare bone support for implicit namespace packages.
tristanlatr Jan 23, 2025
f594862
Fix a bug/typo and refactors
tristanlatr Jan 29, 2025
b972e2d
Add some tests
tristanlatr Jan 29, 2025
6617b54
Stop processing a package tree if it's found to be not added.
tristanlatr Jan 29, 2025
b42e700
Simplify building a little bit
tristanlatr Jan 29, 2025
90b3895
Remove some typing
tristanlatr Jan 29, 2025
c364a8e
Add issue link to comment
tristanlatr Jan 29, 2025
de63830
Merge branch '343-support-for-namespace-packages' of github.com:twist…
tristanlatr Jan 29, 2025
246dd89
Add this file since git doesn't track empty directories
tristanlatr Jan 29, 2025
4e47211
Simplify error
tristanlatr Jan 29, 2025
cdf9f2a
Fix some typing and rename
tristanlatr Jan 30, 2025
0d197db
Cleanup dead code and implement rendering of multiple source links fo…
tristanlatr Jan 30, 2025
dd89621
Fix the Overloads of _addPackageOrModule
tristanlatr Jan 30, 2025
9a427c0
Fix typing
tristanlatr Jan 30, 2025
a1db223
fix mypy
tristanlatr Jan 30, 2025
9964643
Fix mypy
tristanlatr Jan 30, 2025
580af87
Actually test rendering of several source links. Fixes a bug
tristanlatr Jan 30, 2025
296586d
Use tresh=0 for duplicated modules messages
tristanlatr Jan 30, 2025
66e35da
Add support for old school namespace package with extend_path() and d…
tristanlatr Jan 30, 2025
db3bc6e
Fix typing of _OldSchoolNamespacePackageVis and support __path__: lis…
tristanlatr Jan 30, 2025
e383f5a
Add type ignore
tristanlatr Jan 30, 2025
9c28a31
Log a message when people do dynamic things with extend_path() for in…
tristanlatr Jan 30, 2025
da42e28
Add readme entry
tristanlatr Jan 30, 2025
49ee866
Add required exception
tristanlatr Jan 30, 2025
9a99103
Simplify and test is_old_school_namespace_package
tristanlatr Jan 30, 2025
7b2b57b
Show namespace packages just list packages
tristanlatr Jan 30, 2025
6818f69
Generate a descriptive message for namespace packages
tristanlatr Jan 30, 2025
b497a93
Improve changelog
tristanlatr Jan 30, 2025
a0a8777
Add test cases for better branch coverage
tristanlatr Jan 30, 2025
a297e5f
Skip unecessary lower() call
tristanlatr Jan 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ in development
^^^^^^^^^^^^^^

* Drop support for Python 3.8.
* Add support for `Namespace Packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`_:
- Support implicit native namespace packages (PEP 420).
- Support legacy namespace packages as well (with `declare_namespace()` or `extend_path()`).

pydoctor 24.11.2
^^^^^^^^^^^^^^^^
Expand Down
53 changes: 39 additions & 14 deletions pydoctor/astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,7 +1206,21 @@ def __init__(self, system: model.System):
self.currentMod: Optional[model.Module] = None # current module, set when visiting ast.Module.

self._stack: List[model.Documentable] = []
self.ast_cache: Dict[Path, Optional[ast.Module]] = {}


def parseFile(self, path: Path, ctx: model.Module) -> Optional[ast.Module]:
try:
return self.system._ast_parser.parseFile(path)
except Exception as e:
ctx.report(f"cannot parse file, {e}")
return None

def parseString(self, string:str, ctx: model.Module) -> Optional[ast.Module]:
try:
return self.system._ast_parser.parseString(string)
except Exception:
ctx.report("cannot parse string")
return None

def _push(self,
cls: Type[DocumentableT],
Expand Down Expand Up @@ -1312,28 +1326,39 @@ def processModuleAST(self, mod_ast: ast.Module, mod: model.Module) -> None:
vis.extensions.attach_visitor(vis)
vis.walkabout(mod_ast)

def parseFile(self, path: Path, ctx: model.Module) -> Optional[ast.Module]:
class SyntaxTreeParser:

def __init__(self) -> None:
self.ast_cache: Dict[Path, ast.Module | Exception] = {}

def parseFile(self, path: Path) -> ast.Module:
try:
return self.ast_cache[path]
r = self.ast_cache[path]
except KeyError:
mod: Optional[ast.Module] = None
tree: ast.Module | Exception
try:
mod = parseFile(path)
except (SyntaxError, ValueError) as e:
ctx.report(f"cannot parse file, {e}")

self.ast_cache[path] = mod
return mod
tree = parseFile(path)
return tree
except Exception as e:
tree = e
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caching the exception instance is not a good idea because if cycles with locals.
Instead we should cache the exception type and args and re-raise another instance of it when asked again.

raise
finally:
self.ast_cache[path] = tree
else:
if isinstance(r, Exception):
raise r
return r

def parseString(self, py_string:str, ctx: model.Module) -> Optional[ast.Module]:
def parseString(self, string:str) -> ast.Module:
mod = None
try:
mod = _parse(py_string)
except (SyntaxError, ValueError):
ctx.report("cannot parse string")
mod = _parse(string)
except (SyntaxError, ValueError) as e:
raise SyntaxError("cannot parse string") from e
return mod

model.System.defaultBuilder = ASTBuilder
model.System.syntaxTreeParser = SyntaxTreeParser

def findModuleLevelAssign(mod_ast: ast.Module) -> Iterator[Tuple[str, ast.Assign]]:
"""
Expand Down
102 changes: 102 additions & 0 deletions pydoctor/astutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,3 +737,105 @@

del _op_data, _index, _precedence_data, _symbol_data, _deprecated
# This was part of the astor library for Python AST manipulation.


class _OldSchoolNamespacePackageVis(ast.NodeVisitor):

is_namespace_package: bool = False

def visit_Module(self, node: ast.Module) -> None:
try:
self.generic_visit(node)
except StopIteration:
pass

def visit_skip(self, node: ast.AST) -> None:
pass

visit_FunctionDef = visit_AsyncFunctionDef = visit_ClassDef = visit_skip
visit_AugAssign = visit_skip
visit_Return = visit_Raise = visit_Assert = visit_skip
visit_Pass = visit_Break = visit_Continue = visit_Delete = visit_skip
visit_Global = visit_Nonlocal = visit_skip
visit_Import = visit_ImportFrom = visit_skip

def visit_Expr(self, node: ast.Expr) -> None:
# Search for ast.Expr nodes that contains a call to a name or attribute
# access of "declare_namespace" and a single argument: __name__
if not isinstance(val:=node.value, ast.Call):
return
if not isinstance(func:=val.func, (ast.Name, ast.Attribute)):
return

Check warning on line 768 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L768

Added line #L768 was not covered by tests
if isinstance(func, ast.Name) and func.id == 'declare_namespace' or \
isinstance(func, ast.Attribute) and func.attr == 'declare_namespace':
# checks the arguments are the basic one, not custom
try:
arg1, = (*val.args, *(k.value for k in val.keywords))
except ValueError:
raise StopIteration

Check warning on line 775 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L774-L775

Added lines #L774 - L775 were not covered by tests
if not isinstance(arg1, ast.Name) or arg1.id != '__name__':
raise NamespacePackageUnsupported

Check warning on line 777 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L777

Added line #L777 was not covered by tests

self.is_namespace_package = True
raise StopIteration

def visit_Assign(self, node: ast.Assign) -> None:
# search for assignments nodes that contains a call in the
# rhs to name or attribute acess of "extend_path" and two arguments:
# __path__ and __name__.

if not any(isinstance(t, ast.Name) and t.id == '__path__' for t in node.targets):
return
if not isinstance(val:=node.value, ast.Call):
return

Check warning on line 790 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L790

Added line #L790 was not covered by tests
if not isinstance(func:=val.func, (ast.Name, ast.Attribute)):
return

Check warning on line 792 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L792

Added line #L792 was not covered by tests
if isinstance(func, ast.Name) and func.id == 'extend_path' or \
isinstance(func, ast.Attribute) and func.attr == 'extend_path':
# checks the arguments are the basic one, not custom
try:
arg1, arg2 = (*val.args, *(k.value for k in val.keywords))
except ValueError:
raise StopIteration

Check warning on line 799 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L798-L799

Added lines #L798 - L799 were not covered by tests
if not isinstance(arg1, ast.Name) or arg1.id != '__path__':
raise NamespacePackageUnsupported

Check warning on line 801 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L801

Added line #L801 was not covered by tests
if not isinstance(arg2, ast.Name) or arg1.id != '__name__':
raise NamespacePackageUnsupported

self.is_namespace_package = True
raise StopIteration

Check warning on line 806 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L805-L806

Added lines #L805 - L806 were not covered by tests

def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
setattr(node, 'targets', [node.target])
try:
self.visit_Assign(node) # type:ignore[arg-type]

Check warning on line 811 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L809-L811

Added lines #L809 - L811 were not covered by tests
finally:
delattr(node, 'targets')

Check warning on line 813 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L813

Added line #L813 was not covered by tests

class NamespacePackageUnsupported(Exception):
...

def is_old_school_namespace_package(tree: ast.Module) -> bool:
"""
Returns True if the module is a pre PEP 420 namespace package::

from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
# OR
import pkg_resources
pkg_resources.declare_namespace(__name__)
# OR
__import__('pkg_resources').declare_namespace(__name__)
# OR
import pkg_resources
pkg_resources.declare_namespace(name=__name__)

The following code will raise an NamespacePackageUnsupported::

from pkgutil import extend_path
__path__ = extend_path(__path__, __name__ + '.impl')

"""
v =_OldSchoolNamespacePackageVis()
v.visit(tree)
return v.is_namespace_package
1 change: 1 addition & 0 deletions pydoctor/epydoc2stan.py
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,7 @@ def format_kind(kind: model.DocumentableKind, plural: bool = False) -> str:
Transform a `model.DocumentableKind` Enum value to string.
"""
names = {
model.DocumentableKind.NAMESPACE_PACKAGE : 'Namespace Package',
model.DocumentableKind.PACKAGE : 'Package',
model.DocumentableKind.MODULE : 'Module',
model.DocumentableKind.INTERFACE : 'Interface',
Expand Down
Loading
Loading