Skip to content

Commit

Permalink
Merge branch 'master' into 587-better-property-support
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanlatr authored Sep 12, 2023
2 parents 4e174e2 + 4e05370 commit 917947a
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 78 deletions.
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ 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
Expand All @@ -95,6 +96,7 @@ in development
* 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
100 changes: 31 additions & 69 deletions pydoctor/astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
from pydoctor import epydoc2stan, model, node2stan, extensions, astutils, linker
from pydoctor.epydoc.markup._pyval_repr import colorize_inline_pyval
from pydoctor.astutils import (is_none_literal, is_typing_annotation, is_using_annotations, is_using_typing_final, node2dottedname, node2fullname,
is__name__equals__main__, unstring_annotation, iterassign, extract_docstring_linenum, get_parents,
is__name__equals__main__, unstring_annotation, iterassign, extract_docstring_linenum, infer_type, get_parents,
NodeVisitor, Parentage)


def parseFile(path: Path) -> ast.Module:
"""Parse the contents of a Python source file."""
with open(path, 'rb') as f:
Expand Down Expand Up @@ -202,6 +203,17 @@ def __init__(self, builder: 'ASTBuilder', module: model.Module):
self.system = builder.system
self.module = module

def _infer_attr_annotations(self, scope: model.Documentable) -> None:
# Infer annotation when leaving scope so explicit
# annotations take precedence.
for attrib in scope.contents.values():
if not isinstance(attrib, model.Attribute):
continue
# If this attribute has not explicit annotation,
# infer its type from it's ast expression.
if attrib.annotation is None and attrib.value is not None:
# do not override explicit annotation
attrib.annotation = infer_type(attrib.value)

def visit_If(self, node: ast.If) -> None:
if isinstance(node.test, ast.Compare):
Expand All @@ -221,6 +233,7 @@ def visit_Module(self, node: ast.Module) -> None:
epydoc2stan.extract_fields(self.module)

def depart_Module(self, node: ast.Module) -> None:
self._infer_attr_annotations(self.builder.current)
self.builder.pop(self.module)

def visit_ClassDef(self, node: ast.ClassDef) -> None:
Expand Down Expand Up @@ -304,6 +317,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None:


def depart_ClassDef(self, node: ast.ClassDef) -> None:
self._infer_attr_annotations(self.builder.current)
self.builder.popClass()


Expand Down Expand Up @@ -584,14 +598,22 @@ def _tweakConstantAnnotation(obj: model.Attribute, annotation:Optional[ast.expr]
annotation = extract_final_subscript(annotation)
except ValueError as e:
obj.report(str(e), section='ast', lineno_offset=lineno-obj.linenumber)
obj.annotation = _infer_type(value) if value else None
obj.annotation = infer_type(value) if value else None
else:
# Will not display as "Final[str]" but rather only "str"
obj.annotation = annotation
else:
# Just plain "Final" annotation.
# Simply ignore it because it's duplication of information.
obj.annotation = _infer_type(value) if value else None
obj.annotation = infer_type(value) if value else None

@staticmethod
def _setAttributeAnnotation(obj: model.Attribute,
annotation: Optional[ast.expr],) -> None:
if annotation is not None:
# TODO: What to do when an attribute has several explicit annotations?
# (mypy reports a warning in these kind of cases)
obj.annotation = annotation

@staticmethod
def _storeAttrValue(obj:model.Attribute, new_value:Optional[ast.expr],
Expand Down Expand Up @@ -647,11 +669,9 @@ def _handleModuleVar(self,

if not isinstance(obj, model.Attribute):
return

if annotation is None and expr is not None:
annotation = _infer_type(expr)

obj.annotation = annotation
self._setAttributeAnnotation(obj, annotation)

obj.setLineNumber(lineno)

self._handleConstant(obj, annotation, expr, lineno,
Expand Down Expand Up @@ -694,11 +714,8 @@ def _handleClassVar(self,
if obj.kind is None:
obj.kind = model.DocumentableKind.CLASS_VARIABLE

if expr is not None:
if annotation is None:
annotation = _infer_type(expr)

obj.annotation = annotation
self._setAttributeAnnotation(obj, annotation)

obj.setLineNumber(lineno)

self._handleConstant(obj, annotation, expr, lineno,
Expand Down Expand Up @@ -726,10 +743,8 @@ def _handleInstanceVar(self,
if obj is None:
obj = self.builder.addAttribute(name=name, kind=None, parent=cls)

if annotation is None and expr is not None:
annotation = _infer_type(expr)

obj.annotation = annotation
self._setAttributeAnnotation(obj, annotation)

obj.setLineNumber(lineno)
# undonditionnaly set the kind to ivar
obj.kind = model.DocumentableKind.INSTANCE_VARIABLE
Expand Down Expand Up @@ -1153,59 +1168,6 @@ def __repr__(self) -> str:
"""
return '<code>%s</code>' % super().__repr__()


def _infer_type(expr: ast.expr) -> Optional[ast.expr]:
"""Infer an 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


DocumentableT = TypeVar('DocumentableT', bound=model.Documentable)

class ASTBuilder:
Expand Down
56 changes: 55 additions & 1 deletion pydoctor/astutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,59 @@ def extract_docstring(node: ast.Str) -> Tuple[int, str]:
lineno = extract_docstring_linenum(node)
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.
Expand All @@ -437,4 +490,5 @@ def _yield_parents(n:Optional[ast.AST]) -> Iterator[ast.AST]:
yield n
p = cast(ast.AST, getattr(n, 'parent', None))
yield from _yield_parents(p)
yield from _yield_parents(getattr(node, 'parent', None))
yield from _yield_parents(getattr(node, 'parent', None))

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
27 changes: 21 additions & 6 deletions pydoctor/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1552,26 +1552,27 @@ def postProcess(self) -> None:
without the risk of drawing incorrect conclusions because modules
were not fully processed yet.
"""

# default post-processing includes:
# - Processing of subclasses
# - MRO computing.
# - Lookup of constructors
# - Checking whether the class is an exception
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

init_properties(self)

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

for post_processor in self._post_processors:
post_processor(self)

Expand All @@ -1583,6 +1584,20 @@ def fetchIntersphinxInventories(self, cache: CacheT) -> None:
for url in self.options.intersphinx:
self.intersphinx.update(cache, url)

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
) -> Tuple[Optional[str], Optional[Documentable]]:
Expand Down
Loading

0 comments on commit 917947a

Please sign in to comment.