Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
bf5716a
Add test
tristanlatr Sep 17, 2022
f7d0d1a
Add src
tristanlatr Sep 17, 2022
d87e745
Structure tests
tristanlatr Sep 18, 2022
f022cdf
wip Class.constructors attribute
tristanlatr Sep 18, 2022
d578d4c
Add extra information with constructor signature that links to the de…
tristanlatr Sep 19, 2022
6f8e806
Introduce the SignatureBuilder class and refactor the astbuilder to u…
tristanlatr Sep 19, 2022
24d5ad6
wip
tristanlatr Sep 27, 2022
379c63e
Fix issues
tristanlatr Oct 31, 2022
a527d97
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Oct 31, 2022
c5fc9b9
try to fix mypy
tristanlatr Oct 31, 2022
c4be1a9
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Jun 5, 2023
a2389dc
Abstract out some of the core visiting code for dataclass like classes
tristanlatr Jun 11, 2023
36e3683
Factor-out callable analysis inside attrs.py into functions in astuti…
tristanlatr Jun 11, 2023
d974015
Fix docstrings
tristanlatr Jun 11, 2023
d1281cf
Update pydoctor/astutils.py
tristanlatr Jun 11, 2023
41d8ddb
Refactors
tristanlatr Jun 11, 2023
bb7c62c
Merge branch '718-dataclass-like-abstraction' of github.com:twisted/p…
tristanlatr Jun 11, 2023
ba47bab
Fix docstring
tristanlatr Jun 11, 2023
1f74226
Merge branch '718-dataclass-like-abstraction' into 305-handling-of-co…
tristanlatr Jun 11, 2023
9b3860b
remove commented code
tristanlatr Jun 11, 2023
a3f0cd3
Remove unused imports
tristanlatr Jun 11, 2023
4822ea9
Fix annotation of signature_from_functiondef()
tristanlatr Jun 11, 2023
3f8c7f6
Improve support for attrs generated classes, still WIP...
tristanlatr Jun 14, 2023
31ef268
Better understand the factory parameter.
tristanlatr Jun 16, 2023
c06de7e
Fix detected regression in overload handling
tristanlatr Jul 6, 2023
2155fec
Improve annotation_from_attrib()
tristanlatr Jul 7, 2023
4b9142e
Better handle multiple dataclass like extensions
tristanlatr Jul 7, 2023
cf8ba4e
Merge branch '718-dataclass-like-abstraction' into 305-handling-of-co…
tristanlatr Jul 7, 2023
f7220e9
Adjust postProcess
tristanlatr Jul 7, 2023
def0ee0
refarctors and add test
tristanlatr Jul 7, 2023
5a1939c
Add support for auto_detect parameter
tristanlatr Jul 7, 2023
411a584
Support inherited constructor params
tristanlatr Jul 7, 2023
5c72350
Fix presentation of constructors of attrs class
tristanlatr Jul 7, 2023
3720604
Fix order of arguments in constructor short text
tristanlatr Jul 7, 2023
299cd58
Fix keyword only feature
tristanlatr Jul 8, 2023
7497b7a
Add support for the new APIs of attrs, fixes #718
tristanlatr Jul 8, 2023
6a33634
Fix/silent mypy warnings and other refactors
tristanlatr Jul 9, 2023
4ce9b97
Add a docstring to attrs generated __init__ methods
tristanlatr Jul 9, 2023
4016b4c
Fix bugs regarding the rendering of constructors
tristanlatr Jul 9, 2023
eb83341
docs
tristanlatr Jul 9, 2023
06e06e1
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Jul 9, 2023
63edbed
Add documentation section on improved attrs support
tristanlatr Jul 9, 2023
1e82784
Add tests
tristanlatr Jul 9, 2023
435ac99
Merge branch '305-handling-of-constructors' of github.com:twisted/pyd…
tristanlatr Jul 10, 2023
bcce4f3
Fix little issue of priorization of presented annotations
tristanlatr Jul 10, 2023
ad9bfef
Fix mypy
tristanlatr Jul 14, 2023
607a89a
add changelog entry
tristanlatr Jul 14, 2023
3e512f4
make it pass the tests on older versions of python as well.
tristanlatr Jul 14, 2023
11ba98c
normal indentation
tristanlatr Jul 14, 2023
83adf80
skip type comment test on python < 3.8
tristanlatr Jul 14, 2023
ce1e05f
minor changes
tristanlatr Jul 14, 2023
34d7bb3
remove unused imports
tristanlatr Jul 18, 2023
fe08559
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Sep 28, 2023
89a58ef
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Sep 28, 2023
d4deb77
Fix merge error
tristanlatr Nov 3, 2023
d81bf62
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Nov 3, 2023
45a5d3a
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Dec 7, 2023
4926d47
Add comments
tristanlatr Jan 3, 2024
2ac31e7
Merge branch '305-handling-of-constructors' of github.com:twisted/pyd…
tristanlatr Jan 3, 2024
b21ad7e
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Jan 17, 2024
417e995
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Feb 10, 2024
a1fff97
WIP- merging dataclass, attrs and other utilities into a single module.
tristanlatr Mar 24, 2024
60c319e
Merge branch '305-handling-of-constructors' of github.com:twisted/pyd…
tristanlatr Mar 24, 2024
b59a6c0
.source_code is not an attribute
tristanlatr Jun 17, 2025
a62e2f2
wip...
tristanlatr Jun 17, 2025
bd658dc
Still work in progress... 12 dataclass related tests failed, all test…
tristanlatr Jun 28, 2025
da74a74
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Jun 28, 2025
537ecf7
Fix all tests
tristanlatr Jun 29, 2025
7ad8099
Use different link for attrs and dataclasses
tristanlatr Jun 29, 2025
b79a7d7
Fix mypy
tristanlatr Jun 29, 2025
88ff3ed
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Jul 10, 2025
dcc5e07
Fix mypy and remove commented code
tristanlatr Jul 11, 2025
049b417
Move the changelog entries to the development section
tristanlatr Jul 11, 2025
6a3c870
Remove unused imports
tristanlatr Jul 11, 2025
c15fb73
Cleanup import
tristanlatr Jul 11, 2025
faed763
Merge branch 'master' into 305-handling-of-constructors
tristanlatr Jul 11, 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
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ What's New?
in development
^^^^^^^^^^^^^^

* Better ``attrs`` support: generate precise ``__init__`` method from analyzed fields, supports
principal ``attrs`` idioms:
- ``attr.s(auto_attribs, kw_only, auto_detect, init)``/``attrs.define(...)``
- ``attr.ib(init, default, factory, converter, type, kw_only)``/``attrs.field(...)``
- ``attr.Factory(list)``
It does not support the decorators based syntax for setting the validator/factory/default or converter.
* Better ``dataclasses``support: generate precise ``__init__`` method from analyzed fields.
* Fix bug, ``ivar`` field and fiends are not ignored anymore in introspected C-modules.
* Fix bug that would result in duplicated "Cannot find link target" warnings when the
types under a docstring *Attributes* section failed to resolved.
Expand Down
42 changes: 40 additions & 2 deletions docs/source/codedoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ Pydoctor also supports *attribute docstrings*::
"""This docstring describes a class variable."""

def __init__(self):
self.ivar = []
"""This docstring describes an instance variable."""
self.ivar = [];"It can also be used inline."

Attribute docstrings are not part of the Python language itself (`PEP 224 <https://www.python.org/dev/peps/pep-0224/>`_ was rejected), so these docstrings are not available at runtime.

Expand Down Expand Up @@ -284,6 +283,45 @@ If you are using explicit ``attr.ib`` definitions instead of ``auto_attribs``, p
list_of_numbers = attr.ib(factory=list) # type: List[int]
"""Multiple numbers."""

Pydoctor look for ``attrs`` fields declarations and analyze the
arguments passed to ``attr.s`` and ``attr.ib`` in order to
precisely infer what's the signature of the constructor method::

from typing import List
import pathlib
import attr

def convert_paths(p:List[str]) -> List[pathlib.Path]:
return [pathlib.Path(s) for s in p]

@attr.s(auto_attribs=True)
class Base:
a: int

@attr.s(auto_attribs=True, kw_only=True)
class SomeClass(Base):
a_number:int=42; "docstring of number A."
list_of_numbers:List[int] = attr.ib(factory=list); "List of ints"
converted_paths:List[pathlib.Path] = attr.ib(converter=convert_paths, factory=list); "Uses a converter"

The constrcutor method will be documented as if it was explicitly defined,
with a docstring including documentation of each parameters and a note
saying the method is generated by attrs::

def __init__(self, *, a: int, a_number: int = 42,
list_of_numbers: List[int] = list(),
converted_paths: List[str] = list()):
"""
attrs generated method
Copy link
Member

Choose a reason for hiding this comment

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

Could this be a note like @note:, with a title line "Initialize a SomeClass"? In the summary, I think seeing "attrs generated method" on every __init__ is even less helpful than that.


@param a_number: docstring of number A.
@param list_of_numbers: C{attr.ib(factory=list)}
List of ints
@param converted_paths: C{attr.ib(converter=convert_paths, factory=list)}
Uses a converter
"""

Pydoctor also supports the newer APIs (``attrs.define``/``attrs.field``).
Copy link
Contributor Author

@tristanlatr tristanlatr Jun 29, 2025

Choose a reason for hiding this comment

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

Suggested change
Pydoctor also supports the newer APIs (``attrs.define``/``attrs.field``).
Pydoctor also supports the newer APIs (``attrs.define``/``attrs.field``); as well as ``dataclasses.dataclass`` generated classes.


Private API
-----------
Expand Down
9 changes: 7 additions & 2 deletions pydoctor/astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@
from pathlib import Path
from typing import (
Any, Callable, Collection, Dict, Iterable, Iterator, List, Mapping, Optional, Sequence, Tuple,
Type, TypeVar, Union, Set, cast
Type, TypeVar, Union, Set, cast, TYPE_CHECKING
)

from pydoctor import epydoc2stan, model, extensions
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, upgrade_annotation, iterassign, extract_docstring_linenum, infer_type, get_parents,
get_docstring_node, get_assign_docstring_node, unparse, NodeVisitor, Parentage, Str)


if TYPE_CHECKING:
from typing import Protocol
else:
Protocol = object

def parseFile(path: Path) -> ast.Module:
"""Parse the contents of a Python source file."""
with open(path, 'rb') as f:
Expand Down
138 changes: 137 additions & 1 deletion pydoctor/astutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
"""
from __future__ import annotations

import enum
import inspect
import sys
from numbers import Number
from typing import Any, Callable, Collection, Iterator, Optional, List, Iterable, Sequence, TYPE_CHECKING, Tuple, Union, cast
from typing import (Any, Callable, Collection, Iterator, Optional, List, Iterable,
Sequence, TYPE_CHECKING, Tuple, Union, Type, TypeVar, cast)
from inspect import BoundArguments, Signature
import ast

Expand All @@ -16,6 +18,9 @@

if TYPE_CHECKING:
from pydoctor import model
from typing import Protocol, Literal
else:
Protocol = Literal = object

# AST visitors

Expand Down Expand Up @@ -88,6 +93,19 @@
dottedname = node2dottedname(target)
yield dottedname

def iter_decorators(decorator_list:List[ast.expr],
ctx: model.Documentable) -> Iterator[tuple[str|None, ast.expr]]:
"""
Utility function to iterate decorators.
"""

for decnode in decorator_list:
namenode = decnode
if isinstance(namenode, ast.Call):
namenode = namenode.func
dottedname = node2fullname(namenode, ctx)
yield dottedname, decnode

def node2dottedname(node: Optional[ast.AST]) -> Optional[List[str]]:
"""
Resove expression composed by L{ast.Attribute} and L{ast.Name} nodes to a list of names.
Expand All @@ -103,6 +121,17 @@
parts.reverse()
return parts

def dottedname2node(parts:List[str]) -> Union[ast.Name, ast.Attribute]:
"""
Reverse operation of L{node2dottedname}.
"""
assert parts, "must not be empty"

if len(parts)==1:
return ast.Name(parts[0], ast.Load())
else:
return ast.Attribute(dottedname2node(parts[:-1]), parts[-1], ast.Load())

Check warning on line 133 in pydoctor/astutils.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/astutils.py#L133

Added line #L133 was not covered by tests

def node2fullname(expr: Optional[ast.AST],
ctx: model.Documentable | None = None,
*,
Expand Down Expand Up @@ -537,6 +566,113 @@
lineno = extract_docstring_linenum(node)
return lineno, inspect.cleandoc(value)

def safe_bind_args(sig:Signature, call: ast.AST, ctx: model.Documentable) -> Optional[inspect.BoundArguments]:
"""
Binds the arguments of a function call to that function's signature.

When L{bind_args} raises a L{TypeError}, it reports a warning and returns C{None}.
"""
if not isinstance(call, ast.Call):
return None
try:
return bind_args(sig, call)
except TypeError as ex:
message = str(ex).replace("'", '"')
call_dottedname = node2dottedname(call.func)
callable_name = f"{'.'.join(call_dottedname)}()" if call_dottedname else 'callable'
ctx.module.report(
f"Invalid arguments for {callable_name}: {message}",
lineno_offset=call.lineno
)
return None

class _V(enum.Enum):
NoValue = enum.auto()
_T = TypeVar('_T', bound=object)
def _get_literal_arg(args:BoundArguments, name:str,
typecheck:Union[Type[_T], Tuple[Type[_T],...]]) -> Union['Literal[_V.NoValue]', _T]:
"""
Helper function for L{get_literal_arg}.

If the value is not present in the arguments, returns L{_V.NoValue}.
@raises ValueError: If the passed value is not a literal or if it's not the right type.
"""
expr = args.arguments.get(name)
if expr is None:
return _V.NoValue

try:
value = ast.literal_eval(expr)
except ValueError:
message = (
f'Unable to figure out value for {name!r} argument, maybe too complex'
).replace("'", '"')
raise ValueError(message)

if not isinstance(value, typecheck):
expected_type = " or ".join(repr(t.__name__) for t in (typecheck if isinstance(typecheck, tuple) else (typecheck,)))
message = (f'Value for {name!r} argument '
f'has type "{type(value).__name__}", expected {expected_type}'
).replace("'", '"')
raise ValueError(message)

return value #type:ignore

def get_literal_arg(args:BoundArguments, name:str, default:_T,
typecheck: Union[Type[_T], Tuple[Type[_T],...]],
lineno:int, ctx: model.Documentable) -> _T:
"""
Retreive the literal value of an argument from the L{BoundArguments}.
Only works with purely literal values (no C{Name} or C{Attribute}).

@param args: The L{BoundArguments} instance.
@param name: The name of the argument
@param default: The default value of the argument, this value is returned
if the argument is not found.
@param typecheck: The type of the literal value this argument is expected to have.
@param lineno: The lineumber of the callsite, used for error reporting.
@param module: Module that contains the call, used for error reporting.
@return: The value of the argument if we can infer it, otherwise returns
the default value.
"""
try:
value = _get_literal_arg(args, name, typecheck)
except ValueError as e:
ctx.module.report(str(e), lineno_offset=lineno)
return default
if value is _V.NoValue:
# default value
return default
else:
return value

_SCOPE_TYPES = (ast.SetComp, ast.DictComp, ast.ListComp, ast.GeneratorExp,
ast.Lambda, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
_ClassInfo = Union[Type[Any], Tuple[Type[Any],...]]

def _collect_nodes(node:ast.AST, typecheck:_ClassInfo,
stop_typecheck:_ClassInfo=_SCOPE_TYPES) -> Sequence[ast.AST]:
class _Collector(ast.NodeVisitor):
def __init__(self) -> None:
self.collected:List[ast.AST] = []
def _collect(self, node:ast.AST) -> None:
if isinstance(node, typecheck):
self.collected.append(node)
def generic_visit(self, node: ast.AST) -> None:
self._collect(node)
if not isinstance(node, stop_typecheck):
super().generic_visit(node)

visitor = _Collector()
ast.NodeVisitor.generic_visit(visitor, node)
return visitor.collected

def collect_assigns(node:ast.AST) -> Sequence[Union[ast.Assign, ast.AnnAssign]]:
"""
Returns a list of L{ast.Assign} or L{ast.AnnAssign} declared in the given scope.
It does not include assignments in nested scopes.
"""
return _collect_nodes(node, (ast.Assign, ast.AnnAssign)) #type:ignore

def infer_type(expr: ast.expr) -> Optional[ast.expr]:
"""Infer a literal expression's type.
Expand Down
1 change: 0 additions & 1 deletion pydoctor/epydoc/markup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,6 @@ def parsed_text(text: str,
return ParsedRstDocstring(set_node_attributes(new_document(source),
children=[text_node(text, klass)
if klass else nodes.Text(text)]), ())


##################################################
## Fields
Expand Down
2 changes: 1 addition & 1 deletion pydoctor/epydoc/markup/plaintext.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def to_node(self) -> nodes.document:
paragraphs = [set_node_attributes(nodes.paragraph('',''), children=[
set_node_attributes(nodes.Text(p.strip('\n')), document=_document, lineno=0)],
document=_document, lineno=0)
for p in self._text.split('\n\n')]
for p in self._text.split('\n\n') if p not in ('', '\n')]

# assemble document
_document = set_node_attributes(_document,
Expand Down
Loading
Loading