Skip to content

Commit

Permalink
Merge branch 'master' into post-processors-priority
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanlatr authored Sep 16, 2023
2 parents 7453586 + 4e05370 commit df6ca45
Show file tree
Hide file tree
Showing 7 changed files with 595 additions and 203 deletions.
8 changes: 8 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,21 @@ in development
scope when possible, when impossible, the theoretical runtime scopes are used. A warning can
be reported when an annotation name is ambiguous (can be resolved to different names
depending on the scope context) with option ``-v``.
* Ensure that explicit annotation are honored when there are multiple declarations of the same name.
* Use stricter verification before marking an attribute as constant:
- instance variables are never marked as constant
- a variable that has several definitions will not be marked as constant
- a variable declaration under any kind of control flow block will not be marked as constant
* Do not trigger warnings when pydoctor cannot make sense of a potential constant attribute
(pydoctor is not a static checker).
* Fix presentation of type aliases in string form.
* Improve the AST colorizer to output less parenthesis when it's not required.
* Fix colorization of dictionary unpacking.
* Improve the class hierarchy such that it links top level names with intersphinx when possible.
* Add highlighting when clicking on "View In Hierarchy" link from class page.
* Recognize variadic generics type variables (PEP 646).
* Fix support for introspection of cython3 generated modules.
* Instance variables are marked as such across subclasses.

pydoctor 23.4.1
^^^^^^^^^^^^^^^
Expand Down
285 changes: 138 additions & 147 deletions pydoctor/astbuilder.py

Large diffs are not rendered by default.

85 changes: 83 additions & 2 deletions pydoctor/astutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import platform
import sys
from numbers import Number
from typing import Iterator, Optional, List, Iterable, Sequence, TYPE_CHECKING, Tuple, Union
from typing import Iterator, Optional, List, Iterable, Sequence, TYPE_CHECKING, Tuple, Union, cast
from inspect import BoundArguments, Signature
import ast

Expand Down Expand Up @@ -402,4 +402,85 @@ def extract_docstring(node: ast.Str) -> Tuple[int, str]:
- The docstring to be parsed, cleaned by L{inspect.cleandoc}.
"""
lineno = extract_docstring_linenum(node)
return lineno, inspect.cleandoc(node.s)
return lineno, inspect.cleandoc(node.s)


def infer_type(expr: ast.expr) -> Optional[ast.expr]:
"""Infer a literal expression's type.
@param expr: The expression's AST.
@return: A type annotation, or None if the expression has no obvious type.
"""
try:
value: object = ast.literal_eval(expr)
except (ValueError, TypeError):
return None
else:
ann = _annotation_for_value(value)
if ann is None:
return None
else:
return ast.fix_missing_locations(ast.copy_location(ann, expr))

def _annotation_for_value(value: object) -> Optional[ast.expr]:
if value is None:
return None
name = type(value).__name__
if isinstance(value, (dict, list, set, tuple)):
ann_elem = _annotation_for_elements(value)
if isinstance(value, dict):
ann_value = _annotation_for_elements(value.values())
if ann_value is None:
ann_elem = None
elif ann_elem is not None:
ann_elem = ast.Tuple(elts=[ann_elem, ann_value])
if ann_elem is not None:
if name == 'tuple':
ann_elem = ast.Tuple(elts=[ann_elem, ast.Ellipsis()])
return ast.Subscript(value=ast.Name(id=name),
slice=ast.Index(value=ann_elem))
return ast.Name(id=name)

def _annotation_for_elements(sequence: Iterable[object]) -> Optional[ast.expr]:
names = set()
for elem in sequence:
ann = _annotation_for_value(elem)
if isinstance(ann, ast.Name):
names.add(ann.id)
else:
# Nested sequences are too complex.
return None
if len(names) == 1:
name = names.pop()
return ast.Name(id=name)
else:
# Empty sequence or no uniform type.
return None


class Parentage(ast.NodeTransformer):
"""
Add C{parent} attribute to ast nodes instances.
"""
# stolen from https://stackoverflow.com/a/68845448
parent: Optional[ast.AST] = None

def visit(self, node: ast.AST) -> ast.AST:
setattr(node, 'parent', self.parent)
self.parent = node
node = super().visit(node)
if isinstance(node, ast.AST):
self.parent = getattr(node, 'parent')
return node

def get_parents(node:ast.AST) -> Iterator[ast.AST]:
"""
Once nodes have the C{.parent} attribute with {Parentage}, use this function
to get a iterator on all parents of the given node up to the root module.
"""
def _yield_parents(n:Optional[ast.AST]) -> Iterator[ast.AST]:
if n:
yield n
p = cast(ast.AST, getattr(n, 'parent', None))
yield from _yield_parents(p)
yield from _yield_parents(getattr(node, 'parent', None))

35 changes: 13 additions & 22 deletions pydoctor/epydoc/markup/_pyval_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
from pydoctor.epydoc.markup import DocstringLinker
from pydoctor.epydoc.markup.restructuredtext import ParsedRstDocstring
from pydoctor.epydoc.docutils import set_node_attributes, wbr, obj_reference, new_document
from pydoctor.astutils import node2dottedname, bind_args
from pydoctor.astutils import node2dottedname, bind_args, Parentage, get_parents

def decode_with_backslashreplace(s: bytes) -> str:
r"""
Expand Down Expand Up @@ -111,21 +111,6 @@ def restore(self, mark: _MarkedColorizerState) -> List[nodes.Node]:
del self.result[mark.length:]
return trimmed

class _Parentage(ast.NodeTransformer):
"""
Add C{parent} attribute to ast nodes instances.
"""
# stolen from https://stackoverflow.com/a/68845448
parent: Optional[ast.AST] = None

def visit(self, node: ast.AST) -> ast.AST:
setattr(node, 'parent', self.parent)
self.parent = node
node = super().visit(node)
if isinstance(node, ast.AST):
self.parent = getattr(node, 'parent')
return node

# TODO: add support for comparators when needed.
# _OperatorDelimitier is needed for:
# - IfExp
Expand All @@ -152,10 +137,14 @@ def __init__(self, colorizer: 'PyvalColorizer', state: _ColorizerState,
self.marked = state.mark()

# We use a hack to populate a "parent" attribute on AST nodes.
# See _Parentage class, applied in PyvalColorizer._colorize_ast()
parent_node: Optional[ast.AST] = getattr(node, 'parent', None)

if parent_node:
# See astutils.Parentage class, applied in PyvalColorizer._colorize_ast()
try:
parent_node: ast.AST = next(get_parents(node))
except StopIteration:
return

# avoid needless parenthesis, since we now collect parents for every nodes
if isinstance(parent_node, (ast.expr, ast.keyword, ast.comprehension)):
precedence = astor.op_util.get_op_precedence(node.op)
if isinstance(parent_node, (ast.UnaryOp, ast.BinOp, ast.BoolOp)):
parent_precedence = astor.op_util.get_op_precedence(parent_node.op)
Expand Down Expand Up @@ -547,8 +536,10 @@ def _colorize_ast_constant(self, pyval: ast.AST, state: _ColorizerState) -> None

def _colorize_ast(self, pyval: ast.AST, state: _ColorizerState) -> None:
# Set nodes parent in order to check theirs precedences and add delimiters when needed.
if not getattr(pyval, 'parent', None):
_Parentage().visit(pyval)
try:
next(get_parents(pyval))
except StopIteration:
Parentage().visit(pyval)

if self._is_ast_constant(pyval):
self._colorize_ast_constant(pyval, state)
Expand Down
2 changes: 1 addition & 1 deletion pydoctor/extensions/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def annotation_from_attrib(
return astutils.unstring_annotation(typ, ctx)
default = args.arguments.get('default')
if default is not None:
return astbuilder._infer_type(default)
return astutils.infer_type(default)
return None

class ModuleVisitor(extensions.ModuleVisitorExt):
Expand Down
35 changes: 25 additions & 10 deletions pydoctor/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@ class FunctionOverload:

class Attribute(Inheritable):
kind: Optional[DocumentableKind] = DocumentableKind.ATTRIBUTE
annotation: Optional[ast.expr]
annotation: Optional[ast.expr] = None
decorators: Optional[Sequence[ast.expr]] = None
value: Optional[ast.expr] = None
"""
Expand Down Expand Up @@ -1448,22 +1448,37 @@ def fetchIntersphinxInventories(self, cache: CacheT) -> None:
self.intersphinx.update(cache, url)

def defaultPostProcess(system:'System') -> None:
# default post-processing includes:
# - Processing of subclasses
# - MRO computing.
# - Lookup of constructors
# - Checking whether the class is an exception
for cls in system.objectsOfType(Class):

for cls in self.objectsOfType(Class):
# Initiate the MROs
cls._init_mro()
# Lookup of constructors
cls._init_constructors()


# Compute subclasses
for b in cls.baseobjects:
if b is not None:
b.subclasses.append(cls)


# Checking whether the class is an exception
if is_exception(cls):
cls.kind = DocumentableKind.EXCEPTION

for attrib in self.objectsOfType(Attribute):
_inherits_instance_variable_kind(attrib)

def _inherits_instance_variable_kind(attr: Attribute) -> None:
"""
If any of the inherited members of a class variable is an instance variable,
then the subclass' class variable become an instance variable as well.
"""
if attr.kind is not DocumentableKind.CLASS_VARIABLE:
return
docsources = attr.docsources()
next(docsources)
for inherited in docsources:
if inherited.kind is DocumentableKind.INSTANCE_VARIABLE:
attr.kind = DocumentableKind.INSTANCE_VARIABLE
break

def get_docstring(
obj: Documentable
Expand Down
Loading

0 comments on commit df6ca45

Please sign in to comment.