diff --git a/README.rst b/README.rst
index 9fe3562ae..a8674f6d2 100644
--- a/README.rst
+++ b/README.rst
@@ -73,6 +73,16 @@ 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.
* Improve the search box UX:
- There is now a keyboard shortcut ('Ctrl+K' or 'Cmd+K' on Mac or '/',
but the later is overriden by ReadTheDocs) to focus the search box.
diff --git a/docs/source/codedoc.rst b/docs/source/codedoc.rst
index 8c35193f6..8331a2fbc 100644
--- a/docs/source/codedoc.rst
+++ b/docs/source/codedoc.rst
@@ -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 `_ was rejected), so these docstrings are not available at runtime.
@@ -306,6 +305,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
+
+ @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``).
Private API
-----------
diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py
index 18229faeb..499fa36c1 100644
--- a/pydoctor/astbuilder.py
+++ b/pydoctor/astbuilder.py
@@ -12,7 +12,6 @@
Any, Callable, Collection, Dict, Iterable, Iterator, List, Mapping, Optional, Sequence, Tuple,
Type, TypeVar, Union, Set, cast
)
-
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,
diff --git a/pydoctor/astutils.py b/pydoctor/astutils.py
index 6a21deb95..4247914b7 100644
--- a/pydoctor/astutils.py
+++ b/pydoctor/astutils.py
@@ -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
@@ -16,6 +18,9 @@
if TYPE_CHECKING:
from pydoctor import model
+ from typing import Protocol, Literal
+else:
+ Protocol = Literal = object
# AST visitors
@@ -88,6 +93,19 @@ def iterassign(node:_AssingT) -> Iterator[Optional[List[str]]]:
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.
@@ -103,6 +121,17 @@ def node2dottedname(node: Optional[ast.AST]) -> Optional[List[str]]:
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())
+
def node2fullname(expr: Optional[ast.AST],
ctx: model.Documentable | None = None,
*,
@@ -542,6 +571,113 @@ def extract_docstring(node: Str) -> Tuple[int, str]:
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 ctx: Context of 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.
diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py
index e1032ff0c..95d54530f 100644
--- a/pydoctor/epydoc/markup/__init__.py
+++ b/pydoctor/epydoc/markup/__init__.py
@@ -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
diff --git a/pydoctor/epydoc/markup/plaintext.py b/pydoctor/epydoc/markup/plaintext.py
index 1c8d3a35d..5d57a519a 100644
--- a/pydoctor/epydoc/markup/plaintext.py
+++ b/pydoctor/epydoc/markup/plaintext.py
@@ -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,
diff --git a/pydoctor/extensions/attrs.py b/pydoctor/extensions/attrs.py
index 212910f80..a25f26b4b 100644
--- a/pydoctor/extensions/attrs.py
+++ b/pydoctor/extensions/attrs.py
@@ -1,174 +1,644 @@
"""
-Support for L{attrs}.
+Support for L{attrs}, includes L{dataclasses} as well.
"""
+# Implementation of these utilities have been regouped in a single module in
+# order to minimize code duplication; as a side effect the code has a greater complexity.
+
from __future__ import annotations
import ast
+import enum
import inspect
+import copy
+
+from itertools import chain
+from typing import Dict, List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
-from typing import Optional, Union
+from pydoctor.epydoc.docutils import new_document, set_node_attributes
+from pydoctor.epydoc.markup.restructuredtext import ParsedRstDocstring
+if TYPE_CHECKING:
+ from typing import NotRequired
+ from typing_extensions import TypedDict
+else:
+ TypedDict = dict
-from pydoctor import astbuilder, model, astutils, extensions
+import attr as _attr, attrs as _attrs
-import attr
+from pydoctor import model, astutils, extensions, epydoc2stan
+from pydoctor.epydoc.markup import ParsedDocstring, Field
+from pydoctor.epydoc.markup.plaintext import ParsedPlaintextDocstring
+from pydoctor.epydoc.markup._pyval_repr import colorize_inline_pyval
+from pydoctor.extensions import ModuleVisitorExt, ClassMixin
-attrs_decorator_signature = inspect.signature(attr.s)
-"""Signature of the L{attr.s} class decorator."""
+from pydoctor.epydoc2stan import parse_docstring
-attrib_signature = inspect.signature(attr.ib)
-"""Signature of the L{attr.ib} function for defining class attributes."""
+# This list is rather incomplete :/
+builtin_types = frozenset(('frozenset', 'int', 'bytes',
+ 'complex', 'list', 'tuple',
+ 'set', 'dict', 'range'))
-def uses_auto_attribs(call: ast.AST, module: model.Module) -> bool:
- """Does the given L{attr.s()} decoration contain C{auto_attribs=True}?
- @param call: AST of the call to L{attr.s()}.
- This function will assume that L{attr.s()} is called without
- verifying that.
- @param module: Module that contains the call, used for error reporting.
- @return: L{True} if L{True} is passed for C{auto_attribs},
- L{False} in all other cases: if C{auto_attribs} is not passed,
- if an explicit L{False} is passed or if an error was reported.
+# The common process
+
+class ClassOptions(TypedDict):
"""
- if not isinstance(call, ast.Call):
- return False
- if not astutils.node2fullname(call.func, module) in ('attr.s', 'attr.attrs', 'attr.attributes'):
- return False
- try:
- args = astutils.bind_args(attrs_decorator_signature, call)
- except TypeError as ex:
- message = str(ex).replace("'", '"')
- module.report(
- f"Invalid arguments for attr.s(): {message}",
- lineno_offset=call.lineno
- )
- return False
+ Dictionary that may contain the following keys:
- auto_attribs_expr = args.arguments.get('auto_attribs')
- if auto_attribs_expr is None:
- return False
+ - auto_attribs: bool|None
- try:
- value = ast.literal_eval(auto_attribs_expr)
- except ValueError:
- module.report(
- 'Unable to figure out value for "auto_attribs" argument '
- 'to attr.s(), maybe too complex',
- lineno_offset=call.lineno
- )
- return False
+ L{True} if this class uses the C{auto_attribs} feature of the L{attrs}
+ library to automatically convert annotated fields into attributes. This is
+ always True for dataclasses.
- if not isinstance(value, bool):
- module.report(
- f'Value for "auto_attribs" argument to attr.s() '
- f'has type "{type(value).__name__}", expected "bool"',
- lineno_offset=call.lineno
- )
- return False
+ - kw_only: bool
+
+ C{True} is this class uses C{kw_only} feature.
+
+ - init: bool|None
+
+ False if L{attrs } is not generating an __init__ method for this class.
+
+ - auto_detect:bool
+
+ For attrs only.
+ """
+ auto_attribs: NotRequired[bool|None]
+ kw_only: NotRequired[bool]
+ init: NotRequired[bool|None]
+ auto_detect: NotRequired[bool]
+
+class AttrsLikeClass(ClassMixin, model.Class):
+ def setup(self) -> None:
+ super().setup()
+ self._cls_type: ClassType = ClassType.REGULAR
+ self._cls_options: ClassOptions = {}
+
+ # these two attributes helps us infer the signature of the __init__ function
+ self._cls_constructor_parameters: List[inspect.Parameter] = []
+ self._cls_constructor_annotations: Dict[str, Optional[ast.expr]] = {}
+
+class ClassType(enum.Enum):
+
+ REGULAR = 0
+ """
+ This class is just a regular class.
+ """
+
+ ATTRS_CLASSIC = 1
+ """
+ L{attr.s} like::
+ @attr.s(auto_attribs=True)
+ class S:
+ c: int
+ """
+
+ ATTRS_NEW = 2
+ """
+ L{attrs.define} like::
+ @attr.frozen
+ class S:
+ c: int
+ """
- return value
+ DATACLASS = 3
+ """
+ L{dataclasses.dataclass} like::
+ @dataclass
+ class S:
+ c: int
+ """
+
+def _attrs_class_sig_helper( # type: ignore
+ maybe_cls=None, these=None, repr_ns=None, repr=None, cmp=None,
+ hash=None, init=None, slots=False, frozen=False, weakref_slot=True,
+ str=False, auto_attribs=False, kw_only=False, cache_hash=False,
+ auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False,
+ getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True,
+ unsafe_hash=None,
+): ...
+
+def _define_class_sig_helper( # type: ignore
+ maybe_cls=None, *, these=None, repr=None, unsafe_hash=None, hash=None,
+ init=None, slots=True, frozen=False, weakref_slot=True, str=False, auto_attribs=None,
+ kw_only=False, cache_hash=False, auto_exc=True, eq=None, order=False, auto_detect=True,
+ getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True,
+): ...
+
+def _dataclass_class_sig_helper( # type: ignore
+ cls=None, /, *, init=True, repr=True, eq=True, order=False,
+ unsafe_hash=False, frozen=False, match_args=True,
+ kw_only=False, slots=False, weakref_slot=False): ...
+
+def _dataclass_field_sig_helperd( # type: ignore
+ *, default=..., default_factory=..., init=True, repr=True,
+ hash=None, compare=True, metadata=None, kw_only=...):...
+
+_class_type_2_decorator_signature = {
+ ClassType.ATTRS_CLASSIC: inspect.signature(_attrs_class_sig_helper),
+ ClassType.ATTRS_NEW: inspect.signature(_define_class_sig_helper),
+ ClassType.DATACLASS: inspect.signature(_dataclass_class_sig_helper),
+}
+
+_class_type_2_field_signature = {
+ ClassType.ATTRS_CLASSIC: inspect.signature(_attr.ib),
+ ClassType.ATTRS_NEW: inspect.signature(_attrs.field),
+ ClassType.DATACLASS: inspect.signature(_dataclass_field_sig_helperd),
+}
+
+_class_type_2_link = {
+ ClassType.DATACLASS: 'U{dataclass }',
+ ClassType.ATTRS_CLASSIC: 'U{attrs }',
+ ClassType.ATTRS_NEW: 'U{attrs }',
+}
+
+_fallback_call = ast.Call(func=ast.Name(id='define', ctx=ast.Load()),
+ args=[], keywords=[], lineno=0,)
+
+def get_cls_type_decorator(decorators: list[ast.expr], ctx: model.Documentable
+ ) -> tuple[ClassType, ast.expr | None]:
+ types: list[tuple[ClassType, ast.expr]] = []
+
+ for dottedname, decnode in astutils.iter_decorators(decorators, ctx):
+ if dottedname in (
+ 'attr.s', 'attr.attrs', 'attr.attributes'):
+ types.append((ClassType.ATTRS_CLASSIC, decnode))
+ elif dottedname in (
+ 'attr.mutable', 'attr.frozen', 'attr.define',
+ 'attrs.mutable', 'attrs.frozen', 'attrs.define',
+ ):
+ types.append((ClassType.ATTRS_NEW, decnode))
+ elif dottedname in ('dataclasses.dataclass',):
+ types.append((ClassType.DATACLASS, decnode))
+
+ # for basenode in klass.mro(include_external=True, include_self=False):
+ # # base_fullname = astutils.node2fullname(basenode, klass.parent)
+ # if basenode == klass.system.allobjects.get(_d:='pydantic.BaseModel', _d):
+ # types.append(ClassType.PYDANTIC_MODEL)
+ # elif basenode in (klass.system.allobjects.get(_d:='typing.NamedTuple', _d),
+ # klass.system.allobjects.get(_d:='typing_extensions.NamedTuple', _d)):
+ # types.append(ClassType.NAMEDTUPLE)
+
+ if not types:
+ return ClassType.REGULAR, None
+ return types[0] # if many are provided, first listed wins.
+
+def _get_decorator_param_spec(cls_type: ClassType
+ ) -> dict[str, tuple[object, type | tuple[type, ...]]]:
+ # init attrs options based on arguments and whether the newer version of the APIs are in-use.
+ decorator_param_spec: dict[str, tuple[object, type | tuple[type, ...]]] = {
+ # there two options are also compatble with dataclasses.
+ 'init': (None, (bool, type(None))),
+ 'kw_only': (False, bool),
+ }
+ if cls_type == ClassType.ATTRS_CLASSIC:
+ decorator_param_spec['auto_attribs'] = (False, bool)
+ decorator_param_spec['auto_detect'] = (False, bool)
+ elif cls_type == ClassType.ATTRS_NEW:
+ decorator_param_spec['auto_attribs'] = (None, (bool, type(None)))
+ decorator_param_spec['auto_detect'] = (True, bool)
+ elif cls_type == ClassType.DATACLASS:
+ # Dataclasses are a subset of attrs with the following options:
+ decorator_param_spec['auto_attribs'] = (True, bool)
+ decorator_param_spec['auto_detect'] = (True, bool)
+ else: assert False
+ return decorator_param_spec
-def is_attrib(expr: Optional[ast.expr], ctx: model.Documentable) -> bool:
+def get_cls_decorator_options(cls_type: ClassType,
+ deco: ast.expr,
+ classdef:ast.ClassDef,
+ ctx: model.Documentable) -> ClassOptions:
+
+ sig = _class_type_2_decorator_signature[cls_type]
+ attrs_args = astutils.safe_bind_args(sig, deco, ctx)
+ if not attrs_args:
+ attrs_args = astutils.bind_args(sig, _fallback_call)
+
+ options: ClassOptions = {name: astutils.get_literal_arg(attrs_args, # type: ignore[assignment]
+ name,
+ default,
+ typecheck,
+ deco.lineno,
+ ctx)
+ for name, (default, typecheck) in
+ _get_decorator_param_spec(cls_type).items()}
+
+ if cls_type == ClassType.ATTRS_NEW and options['auto_attribs'] is None:
+ fields = collect_fields(classdef, ctx)
+ # auto detect auto_attrib value for newer APIs of attrs.
+ options['auto_attribs'] = len(fields) > 0 and \
+ not any(isinstance(a, ast.Assign) for a in fields)
+
+ return options
+
+def is_attrs_field(expr: Optional[ast.expr], ctx: model.Documentable) -> bool:
"""Does this expression return an C{attr.ib}?"""
- return isinstance(expr, ast.Call) and astutils.node2fullname(expr.func, ctx) in (
- 'attr.ib', 'attr.attrib', 'attr.attr'
+ return isinstance(expr, ast.Call) and \
+ astutils.node2fullname(expr.func, ctx) in (
+ 'attr.ib', 'attr.attrib', 'attr.attr', 'attrs.field', 'attr.field'
)
-def attrib_args(expr: ast.expr, ctx: model.Documentable) -> Optional[inspect.BoundArguments]:
- """Get the arguments passed to an C{attr.ib} definition.
- @return: The arguments, or L{None} if C{expr} does not look like
- an C{attr.ib} definition or the arguments passed to it are invalid.
+def is_dataclass_field(expr: Optional[ast.expr], ctx: model.Documentable) -> bool:
+ return isinstance(expr, ast.Call) and \
+ astutils.node2fullname(expr.func, ctx) == 'dataclasses.field'
+
+def is_field(expr: Optional[ast.expr], ctx: model.Documentable) -> bool:
+ return is_attrs_field(expr, ctx) or is_dataclass_field(expr, ctx)
+
+def get_factory(expr: Optional[ast.expr], ctx: model.Documentable) -> Optional[ast.expr]:
+ """
+ If this AST represent a call to L{attrs.Factory}, returns the expression inside the factory call
"""
- if isinstance(expr, ast.Call) and astutils.node2fullname(expr.func, ctx) in (
- 'attr.ib', 'attr.attrib', 'attr.attr'
- ):
+ if isinstance(expr, ast.Call) and \
+ astutils.node2fullname(expr.func, ctx) in ('attrs.Factory', 'attr.Factory'):
try:
- return astutils.bind_args(attrib_signature, expr)
- except TypeError as ex:
- message = str(ex).replace("'", '"')
- ctx.module.report(
- f"Invalid arguments for attr.ib(): {message}",
- lineno_offset=expr.lineno
- )
+ factory, = expr.args
+ except Exception:
+ return None
+ else:
+ return factory
+ else:
+ return None
+
+def _callable_return_type(dname:List[str], ctx:model.Documentable) -> Optional[ast.expr]:
+ """
+ Given a callable dotted name in a certain context,
+ get it's return type as ast expression.
+
+ Note that the expression might not be fully
+ resolvable in the new context since it can come from other modules.
+
+ This is not type inference, we're simply looking up the name and.
+ """
+ r = ctx.resolveName('.'.join(dname))
+ if isinstance(r, model.Class):
+ return astutils.dottedname2node(dname)
+ elif isinstance(r, model.Function):
+ rtype = r.annotations.get('return')
+ if rtype:
+ # TODO: Here the returned ast might not be in the same module
+ # as the attrs class, so the names might not be resolvable.
+ # So the right to do would be check whether it's defined in the same module
+ # and if not: use the fully qualified name instead so the linker will link to the
+ # object successfuly.
+
+ # TODO: We should make sure not to return unresolved type variables, this might
+ # need us to restrain the results to a something without brackets all together.
+ return rtype
+ elif r is None and len(dname)==1 and dname[0] in builtin_types:
+ return astutils.dottedname2node(dname)
+ # TODO: we might be able to use the shpinx inventory to check if the
+ # provided callable is a class, in which case the class could be linked.
return None
+def _annotation_from_factory(
+ factory:ast.expr,
+ ctx: model.Documentable,
+ ) -> Optional[ast.expr]:
+ dname = astutils.node2dottedname(factory)
+ if dname:
+ return _callable_return_type(dname, ctx)
+ else:
+ return None
+
+def _annotation_from_converter(
+ converter:ast.expr,
+ ctx: model.Documentable,
+ ) -> Optional[ast.expr]:
+ dname = astutils.node2dottedname(converter)
+ if dname:
+ r = ctx.resolveName('.'.join(dname))
+ if isinstance(r, model.Class):
+ args = dict(r.constructor_params)
+ elif isinstance(r, model.Function):
+ args = dict(r.annotations)
+ else:
+ return astutils.dottedname2node(['object'])
+ args.pop('return', None)
+ if len(args)==1:
+ return args.popitem()[1]
+ return None
+
def annotation_from_attrib(
- self: astbuilder.ModuleVistor,
- expr: ast.expr,
- ctx: model.Documentable
+ args:inspect.BoundArguments,
+ ctx: model.Documentable,
+ for_constructor:bool=False
) -> Optional[ast.expr]:
"""Get the type of an C{attr.ib} definition.
- @param expr: The L{ast.Call} expression's AST.
+ @param args: The L{inspect.BoundArguments} of the C{attr.ib()} call.
@param ctx: The context in which this expression is evaluated.
+ @param for_constructor: Whether we're trying to figure out the __init__ parameter annotations
+ instead of the attribute annotations.
@return: A type annotation, or None if the expression is not
an C{attr.ib} definition or contains no type information.
"""
- args = attrib_args(expr, ctx)
- if args is not None:
- typ = args.arguments.get('type')
- if typ is not None:
- return astutils.unstring_annotation(typ, ctx)
+ if for_constructor:
+ # If a converter is defined...
+ converter = args.arguments.get('converter')
+ if converter is not None:
+ return _annotation_from_converter(converter, ctx)
+
+ typ = args.arguments.get('type')
+ if typ is not None:
+ return astutils.unstring_annotation(typ, ctx)
+
+ if not for_constructor:
+ factory = args.arguments.get('factory', args.arguments.get('default_factory'))
+ if factory is not None:
+ return _annotation_from_factory(factory, ctx)
+
default = args.arguments.get('default')
if default is not None:
- return astutils.infer_type(default)
+ factory = get_factory(default, ctx)
+ if factory is not None:
+ return _annotation_from_factory(factory, ctx)
+ else:
+ return astutils.infer_type(default)
return None
-class ModuleVisitor(extensions.ModuleVisitorExt):
+def default_from_attrib(args:inspect.BoundArguments, ctx: model.Documentable) -> Optional[ast.expr]:
+ d = args.arguments.get('default')
+ f = args.arguments.get('factory', args.arguments.get('default_factory'))
+ if isinstance(d, ast.expr):
+ factory = get_factory(d, ctx)
+ if factory:
+ if astutils.node2dottedname(factory):
+ return ast.Call(func=factory, args=[], keywords=[], lineno=d.lineno)
+ else:
+ return ast.Constant(value=..., lineno=d.lineno)
+ return d
+ elif isinstance(f, ast.expr):
+ if astutils.node2dottedname(f):
+ # If a simple factory is defined, the default value is a call to this function
+ return ast.Call(func=f, args=[], keywords=[], lineno=f.lineno)
+ else:
+ # Else we can't figure it out
+ return ast.Constant(value=..., lineno=f.lineno)
+ else:
+ return None
+
+def collect_fields(node: ast.ClassDef, ctx: model.Documentable) -> Sequence[Union[ast.Assign, ast.AnnAssign]]:
+ # CAN only find attrs fields, not dataclass.
+ # used for the auto detection of auto_attribs value in newer APIs.
+ def _f(assign:Union[ast.Assign, ast.AnnAssign]) -> bool:
+ if isinstance(assign, ast.AnnAssign) and \
+ not astutils.is_using_typing_classvar(assign.annotation, ctx):
+ return True
+ if is_attrs_field(assign.value, ctx):
+ return True
+ return False
+ return list(filter(_f, astutils.collect_assigns(node)))
+
+
+_nothing = object()
+
+class ModuleVisitor(ModuleVisitorExt):
+
+ def visit_Assign(self, node: Union[ast.Assign, ast.AnnAssign]) -> None:
+ current = self.visitor.builder.current
+ if not isinstance(current, AttrsLikeClass):
+ return
+ if current._cls_type == ClassType.REGULAR:
+ return
+ for dottedname in astutils.iterassign(node):
+ if dottedname and len(dottedname)==1:
+ # We consider single name assignment only
+ target, = dottedname
+ attr: Optional[model.Documentable] = current.contents.get(target)
+ if not isinstance(attr, model.Attribute) or \
+ astutils.is_using_typing_classvar(attr.annotation, current):
+ continue
+ annotation = node.annotation if isinstance(node, ast.AnnAssign) else None
+ self.handle_field(current, attr, annotation, node.value)
+
+ visit_AnnAssign = visit_Assign
def visit_ClassDef(self, node:ast.ClassDef) -> None:
"""
Called when a class definition is visited.
"""
- cls = self.visitor.builder.current
- if not isinstance(cls, model.Class) or cls.name!=node.name:
+ cls = self.visitor.builder._stack[-1].contents.get(node.name)
+ if not isinstance(cls, AttrsLikeClass):
+ return
+
+ cls_type, deco = get_cls_type_decorator(node.decorator_list, ctx=cls.parent)
+ if cls_type == ClassType.REGULAR:
+ # not an attrs like class
return
+ assert deco is not None
+ cls._cls_type = cls_type
+ cls._cls_options = get_cls_decorator_options(cls_type, deco,
+ classdef=node,
+ ctx=cls.parent)
+ # init the self argument
+ cls._cls_constructor_parameters.append(
+ inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD))
+ cls._cls_constructor_annotations['self'] = None
+
+ def handle_field(self, cls: AttrsLikeClass,
+ attr: model.Attribute,
+ annotation:Optional[ast.expr],
+ value:Optional[ast.expr]) -> None:
+ # MUST only be called for non-REGULAR classes.
+ is_field_call = is_field(value, ctx=cls)
+ is_implicit_field = not is_field_call and (
+ cls._cls_options.get('auto_attribs') and annotation is not None)
+
+ if not (is_field_call or is_implicit_field):
+ return
+
+ attrib_args = None
+ attrib_args_value = {}
- assert isinstance(cls, AttrsClass)
- cls.auto_attribs = any(uses_auto_attribs(decnode, cls.module) for decnode in node.decorator_list)
+ attr.kind = model.DocumentableKind.INSTANCE_VARIABLE
+ if is_field_call:
+ assert value is not None
+ attrib_args = astutils.safe_bind_args(
+ _class_type_2_field_signature[cls._cls_type],
+ value, cls.module)
+ if attrib_args:
+ if annotation is None and attr.annotation is None:
+ attr.annotation = annotation_from_attrib(attrib_args, cls)
+
+ attrib_args_value = {name: astutils.get_literal_arg(attrib_args, name, default,
+ typecheck, attr.linenumber, cls.module
+ ) for name, default, typecheck in
+ (('init', True, bool),
+ ('kw_only', False, bool),
+ ('alias', None, (str, type(None))))}
+
+ # Handle the auto-creation of the __init__ method.
+ if cls._cls_options.get('init', _nothing) in (True, None) and \
+ is_implicit_field or attrib_args_value.get('init', True):
- def _handleAttrsAssignmentInClass(self, target:str, node: Union[ast.Assign, ast.AnnAssign]) -> None:
- cls = self.visitor.builder.current
- assert isinstance(cls, AttrsClass)
+ kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD
+ if cls._cls_options.get('kw_only') or attrib_args_value.get('kw_only'):
+ kind = inspect.Parameter.KEYWORD_ONLY
- attr: Optional[model.Documentable] = cls.contents.get(target)
- if attr is None:
- return
- if not isinstance(attr, model.Attribute):
- return
+ attrs_default: ast.expr | None = ast.Constant(value=..., lineno=attr.linenumber)
+
+ if is_implicit_field:
+ factory = get_factory(value, cls)
+ if factory:
+ if astutils.node2dottedname(factory):
+ attrs_default = ast.Call(func=factory, args=[], keywords=[],
+ lineno=factory.lineno)
+
+ # else, the factory is not a simple function/class name,
+ # so we give up on trying to figure it out.
+ else:
+ attrs_default = value
+
+ elif attrib_args:
+ attrs_default = default_from_attrib(attrib_args, cls)
+
+ # attrs strips the leading underscores from the parameter names,
+ # since there is not such thing as a private parameter.
+ # This is not true for dataclasses and others!
+ if attr.name == '_':
+ # A dataclass flag
+ if astutils.node2fullname(annotation, cls) == 'dataclasses.KW_ONLY':
+ cls._cls_options['kw_only'] = True
+ return
- annotation = node.annotation if isinstance(node, ast.AnnAssign) else None
-
- if is_attrib(node.value, cls) or (
- cls.auto_attribs and \
- annotation is not None and \
- not astutils.is_using_typing_classvar(annotation, cls)):
+ init_param_name: str
+ if cls._cls_type!= ClassType.DATACLASS:
+ if not (init_param_name:=attrib_args_value.get('alias')): # type:ignore[assignment]
+ init_param_name = attr.name.lstrip('_')
+ else:
+ init_param_name = attr.name
+
+ if attrib_args:
+ constructor_annotation = annotation_from_attrib(
+ attrib_args, cls, for_constructor=True) or \
+ attr.annotation or annotation_from_attrib(
+ attrib_args, cls)
+ else:
+ constructor_annotation = attr.annotation
- attr.kind = model.DocumentableKind.INSTANCE_VARIABLE
- if annotation is None and node.value is not None:
- attr.annotation = annotation_from_attrib(self.visitor, node.value, cls)
+ cls._cls_constructor_annotations[init_param_name] = constructor_annotation
+ cls._cls_constructor_parameters.append(
+ inspect.Parameter(
+ init_param_name, kind=kind,
+ default=attrs_default
+ if attrs_default else inspect.Parameter.empty,
+ annotation=constructor_annotation
+ if constructor_annotation else inspect.Parameter.empty))
+
- def _handleAttrsAssignment(self, node: Union[ast.Assign, ast.AnnAssign]) -> None:
- for dottedname in astutils.iterassign(node):
- if dottedname and len(dottedname)==1:
- # Here, we consider single name assignment only
- current = self.visitor.builder.current
- if isinstance(current, model.Class):
- self._handleAttrsAssignmentInClass(
- dottedname[0], node
- )
-
- def visit_Assign(self, node: Union[ast.Assign, ast.AnnAssign]) -> None:
- self._handleAttrsAssignment(node)
- visit_AnnAssign = visit_Assign
+def collect_inherited_constructor_params(cls:AttrsLikeClass) -> Tuple[List[inspect.Parameter],
+ Dict[str, Optional[ast.expr]]]:
+ # see https://github.com/python-attrs/attrs/pull/635/files
+
+ base_attrs:List[inspect.Parameter] = []
+ base_annotations:Dict[str, Optional[ast.expr]] = {}
+ own_param_names = cls._cls_constructor_annotations
+
+ # Traverse the MRO and collect attributes.
+ for base_cls in reversed(cls.mro(include_external=False, include_self=False)):
+ assert isinstance(base_cls, AttrsLikeClass)
+ for p in base_cls._cls_constructor_parameters[1:]:
+ if p.name in own_param_names:
+ continue
+
+ base_attrs.append(p)
+ base_annotations[p.name] = base_cls._cls_constructor_annotations[p.name]
+
+ # For each name, only keep the freshest definition i.e. the furthest at the
+ # back. base_annotations is fine because it gets overwritten with every new
+ # instance.
+ filtered:List[inspect.Parameter] = []
+ seen = set()
+ for a in reversed(base_attrs):
+ if a.name in seen:
+ continue
+ filtered.insert(0, copy.copy(a))
+ seen.add(a.name)
-class AttrsClass(extensions.ClassMixin, model.Class):
+ return filtered, base_annotations
+
+def generated_constructor_docstring(cls:AttrsLikeClass, constructor_signature:inspect.Signature) -> ParsedDocstring:
+ """
+ Get a docstring for the attrs or dataclass generated constructor method.
+ """
+ fields = []
+ for param in constructor_signature.parameters.values():
+ if param.name=='self':
+ continue
+ attr = cls.find(param.name)
+ if isinstance(attr, model.Attribute):
+ if is_field(attr.value, cls):
+ field_doc: ParsedDocstring = colorize_inline_pyval(attr.value)
+ else:
+ field_doc = ParsedPlaintextDocstring('')
+ epydoc2stan.ensure_parsed_docstring(attr)
+ if attr.parsed_docstring:
+ field_doc = ParsedRstDocstring(set_node_attributes(
+ new_document('docstring'),
+ # concatenate two parsed docstrings.
+ children=chain(field_doc.to_node().children,
+ attr.parsed_docstring.to_node().children)), ())
+
+ if field_doc.has_body:
+ fields.append(Field('param', param.name, field_doc, lineno=cls.linenumber))
- def setup(self) -> None:
- super().setup()
- self.auto_attribs: bool = False
- """
- L{True} if this class uses the C{auto_attribs} feature of the L{attrs}
- library to automatically convert annotated fields into attributes.
- """
+ doc = parse_docstring(cls, f'{_class_type_2_link[cls._cls_type]} generated method',
+ cls, markup='epytext', section='attrs')
+ doc.fields = fields
+ return doc
+def postProcess(system:model.System) -> None:
+
+ for cls in list(system.objectsOfType(AttrsLikeClass)):
+ # by default attr.s() overrides any defined __init__ mehtod, whereas dataclasses.
+ if cls._cls_type != ClassType.REGULAR:
+
+ if cls._cls_options.get('init') is False or \
+ cls._cls_options.get('init', _nothing) is None and \
+ cls._cls_options.get('auto_detect') is True and \
+ cls.contents.get('__init__'):
+ continue
+
+ func = system.Function(system, '__init__', cls)
+ # init Function attributes that otherwise would be undefined :/
+ func.parentMod = cls.parentMod
+ func.decorators = None
+ func.is_async = False
+ func.parentMod = cls.parentMod
+ func.setLineNumber(cls.linenumber)
+ system.addObject(func)
+
+ # collect arguments from super classes attributes definitions.
+ inherited_params, inherited_annotations = collect_inherited_constructor_params(cls)
+ # don't forget to set the KEYWORD_ONLY flag on inherited parameters
+ if cls._cls_options.get('kw_only') is True:
+ for p in inherited_params:
+ p._kind = inspect.Parameter.KEYWORD_ONLY # type:ignore[attr-defined]
+ # make sure that self is kept first.
+ parameters = [cls._cls_constructor_parameters[0],
+ *inherited_params, *cls._cls_constructor_parameters[1:]]
+ annotations: Dict[str, Optional[ast.expr]] = {'self': None,
+ **inherited_annotations,
+ **cls._cls_constructor_annotations}
+
+ # Re-ordering kw_only arguments at the end of the list
+ for param in tuple(parameters):
+ if param.kind is inspect.Parameter.KEYWORD_ONLY:
+ parameters.remove(param)
+ parameters.append(param)
+ ann = annotations[param.name]
+ del annotations[param.name]
+ annotations[param.name] = ann
+
+ func.annotations = annotations
+ try:
+ func.signature = inspect.Signature(parameters)
+ except Exception as e:
+ func.report(f'could not deduce class __init__ signature: {e}')
+ func.signature = inspect.Signature()
+ func.annotations = {}
+ else:
+ func.parsed_docstring = generated_constructor_docstring(cls, func.signature)
+
def setup_pydoctor_extension(r:extensions.ExtRegistrar) -> None:
r.register_astbuilder_visitor(ModuleVisitor)
- r.register_mixin(AttrsClass)
+ r.register_mixin(AttrsLikeClass)
+ r.register_post_processor(postProcess)
diff --git a/pydoctor/model.py b/pydoctor/model.py
index cc6cf1fb9..bf52a7d97 100644
--- a/pydoctor/model.py
+++ b/pydoctor/model.py
@@ -767,7 +767,7 @@ def get_constructors(cls:Class) -> Iterator[Function]:
dunder_constructor = _find_dunder_constructor(cls)
if dunder_constructor:
yield dunder_constructor
-
+
# Then look for staticmethod/classmethod constructors,
# This only happens at the local scope level (i.e not looking in super-classes).
for fun in cls.contents.values():
@@ -789,6 +789,7 @@ def get_constructors(cls:Class) -> Iterator[Function]:
return_ann in ('typing.Self', 'typing_extensions.Self'):
yield fun
+
class Class(CanContainImportsDocumentable):
kind = DocumentableKind.CLASS
parent: CanContainImportsDocumentable
diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py
index e3cf95955..edac992fb 100644
--- a/pydoctor/test/test_astbuilder.py
+++ b/pydoctor/test/test_astbuilder.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+from textwrap import dedent
from typing import Optional, Tuple, Type, List, overload, cast
import ast
@@ -2775,6 +2776,49 @@ def __init__(self):
mod = fromText(src, systemcls=systemcls)
assert getConstructorsText(mod.contents['Animal']) == "Animal()"
+@systemcls_param
+def test_docstring_attribute_inline(systemcls:Type[model.System]) -> None:
+ src='''\
+ class SomeClass(Base):
+ a_number:int=42; "docstring of number A."
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert mod.contents['SomeClass'].contents['a_number'].docstring=="docstring of number A."
+
+# test for astutils.collect_assigns
+def test_astutils_collect_assigns() -> None:
+ mod = ast.parse(dedent('''\
+ class C:
+ def __init__(self):...
+ var:int
+ foo = True
+ class F:
+ [a for a in []]
+ {n:stuff for (n,stuff) in ()}
+ second = 1
+ l = lambda x:True
+ '''
+ ))
+
+ C = mod.body[0]
+ F = C.body[-1] # type:ignore
+ # no assignment in module
+ assert [n for n in astutils.collect_assigns(mod)] == []
+ # found one class in module
+ assert [n.name for n in astutils._collect_nodes(mod, ast.ClassDef)] == ['C'] # type:ignore
+ # two attribute assignment in C
+ assert [n.lineno for n in astutils.collect_assigns(C)] == [3,4]
+ # one function
+ assert [n.name for n in astutils._collect_nodes(C, ast.FunctionDef)] == ['__init__'] # type:ignore
+ # one class
+ assert [n.name for n in astutils._collect_nodes(C, ast.ClassDef)] == ['F'] # type:ignore
+ # two assignments in F
+ assert [n.lineno for n in astutils.collect_assigns(F)] == [8,9]
+ # two names in F (it does not recurse on nested scopes)
+ assert [n.lineno for n in astutils._collect_nodes(F, ast.Name)] == [8,9] # type: ignore
+ # two comprehensions
+ assert [n.lineno for n in astutils._collect_nodes(F, (ast.ListComp, ast.DictComp))] == [6,7] # type: ignore
+
@systemcls_param
def test_class_var_override(systemcls: Type[model.System]) -> None:
diff --git a/pydoctor/test/test_attrs.py b/pydoctor/test/test_attrs.py
index a13f24654..2efa6cb33 100644
--- a/pydoctor/test/test_attrs.py
+++ b/pydoctor/test/test_attrs.py
@@ -1,7 +1,12 @@
-from typing import Type
+import re
+import sys
+from typing import Optional, Type
-from pydoctor import model
+from pydoctor import epydoc2stan, model
from pydoctor.extensions import attrs
+from pydoctor.stanutils import flatten_text
+from pydoctor.node2stan import gettext
+from pydoctor.templatewriter import pages
from pydoctor.test import CapSys
from pydoctor.test.test_astbuilder import fromText, AttrsSystem, type2str
@@ -13,6 +18,17 @@
AttrsSystem, # system with attrs extension only
))
+def assert_constructor(cls:model.Documentable, sig:str,
+ shortsig:Optional[str]=None) -> None:
+ assert isinstance(cls, attrs.AttrsLikeClass)
+ assert cls._cls_type != attrs.ClassType.REGULAR
+ constructor = cls.contents['__init__']
+ assert isinstance(constructor, model.Function)
+ assert flatten_text(pages.format_signature(constructor)).replace(' ','') == sig.replace(' ','')
+ if shortsig:
+ assert epydoc2stan.format_constructor_short_text(constructor, forclass=cls) == shortsig
+
+
@attrs_systemcls_param
def test_attrs_attrib_type(systemcls: Type[model.System]) -> None:
"""An attr.ib's "type" or "default" argument is used as an alternative
@@ -89,8 +105,8 @@ class C:
d = 123 # ignored by auto_attribs because no annotation
''', modname='test', systemcls=systemcls)
C = mod.contents['C']
- assert isinstance(C, attrs.AttrsClass)
- assert C.auto_attribs == True
+ assert isinstance(C, attrs.AttrsLikeClass)
+ assert C._cls_options['auto_attribs'] == True
assert C.contents['a'].kind is model.DocumentableKind.INSTANCE_VARIABLE
assert C.contents['b'].kind is model.DocumentableKind.INSTANCE_VARIABLE
assert C.contents['c'].kind is model.DocumentableKind.CLASS_VARIABLE
@@ -122,10 +138,742 @@ class C4: ...
captured = capsys.readouterr().out
assert captured == (
'test:10: Invalid arguments for attr.s(): got an unexpected keyword argument "auto_attribzzz"\n'
- 'test:13: Unable to figure out value for "auto_attribs" argument to attr.s(), maybe too complex\n'
- 'test:16: Value for "auto_attribs" argument to attr.s() has type "int", expected "bool"\n'
+ 'test:13: Unable to figure out value for "auto_attribs" argument, maybe too complex\n'
+ 'test:16: Value for "auto_attribs" argument has type "int", expected "bool"\n'
)
+@attrs_systemcls_param
+def test_attrs_constructor_method_infer_arg_types(systemcls: Type[model.System], capsys: CapSys) -> None:
+ src = '''\
+ @attr.s
+ class C(object):
+ c = attr.ib(default=100)
+ x = attr.ib(default=1)
+ b = attr.ib(default=23)
+
+ @attr.s(init=False)
+ class D(C):
+ a = attr.ib(default=42)
+ x = attr.ib(default=2)
+ d = attr.ib(default=3.14)
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert capsys.readouterr().out == ''
+ C = mod.contents['C']
+ assert isinstance(C, attrs.AttrsLikeClass)
+ assert C._cls_options['init'] is None
+ D = mod.contents['D']
+ assert isinstance(D, attrs.AttrsLikeClass)
+ assert D._cls_options['init'] is False
+
+ assert_constructor(C, '(self, c: int = 100, x: int = 1, b: int = 23)', 'C(c, x, b)')
+
+# Test case for auto_attribs with defaults
+@attrs_systemcls_param
+def test_attrs_constructor_auto_attribs(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s(auto_attribs=True)
+ class C:
+ a: int
+ b: str = "default"
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['C'], '(self, a: int, b: str = \'default\')')
+
+# Test case for kw_only
+@attrs_systemcls_param
+def test_attrs_constructor_kw_only(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s(kw_only=True)
+ class C:
+ a = attr.ib()
+ b: str = attr.ib()
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ C = mod.contents['C']
+ assert isinstance(C, attrs.AttrsLikeClass)
+ assert C._cls_options['kw_only'] is True
+ assert C._cls_options['init'] is None
+ assert_constructor(C, '(self, *, a, b: str)')
+
+# Test case for default factory
+@attrs_systemcls_param
+def test_attrs_constructor_factory(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s(auto_attribs=True)
+ class C:
+ a: int = attr.ib(factory=lambda:False)
+ b: str = attr.Factory(str)
+ c: list = attr.ib(default=attr.Factory(list))
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['C'], '(self, a: int = ..., b: str = str(), c: list = list())')
+
+@attrs_systemcls_param
+def test_attrs_constructor_factory_no_annotations(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s
+ class C:
+ a = attr.ib(factory=list)
+ b = attr.ib(default=attr.Factory(list))
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['C'], '(self, a: list = list(), b: list = list())')
+
+# Test case for init=False:
+@attrs_systemcls_param
+def test_attrs_no_constructor(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s(init=False)
+ class C:
+ a: int = attr.ib()
+ b: str = attr.ib()
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ C = mod.contents['C']
+ assert C.contents.get('__init__') is None
+
+# Test case for single inheritance:
+@attrs_systemcls_param
+def test_attrs_constructor_single_inheritance(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s(auto_attribs=True)
+ class Base:
+ a: int
+
+ @attr.s(auto_attribs=True)
+ class Derived(Base):
+ b: str
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['Derived'], '(self, a: int, b: str)', 'Derived(a, b)')
+
+# Test case for multiple inheritance:
+@attrs_systemcls_param
+def test_attrs_constructor_multiple_inheritance(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s(auto_attribs=True)
+ class Base1:
+ a: int
+
+ @attr.s(auto_attribs=True)
+ class Base2:
+ b: str
+
+ @attr.s(auto_attribs=True)
+ class Derived(Base1, Base2):
+ c: float
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['Derived'], '(self, b: str, a: int, c: float)', 'Derived(b, a, c)')
+
+# Test case for inheritance with overridden attributes:
+@attrs_systemcls_param
+def test_attrs_constructor_single_inheritance_overridden_attribute(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s(auto_attribs=True)
+ class Base:
+ a: int
+ b: str = "default"
+
+ @attr.s(auto_attribs=True)
+ class Derived(Base):
+ b: str = "overridden"
+ c: float = 3.14
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['Derived'], '(self, a: int, b: str = \'overridden\', c: float = 3.14)', 'Derived(a, b, c)')
+
+@attrs_systemcls_param
+def test_attrs_constructor_single_inheritance_traverse_subclasses(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s(auto_attribs=True)
+ class FieldDesc:
+ name: Optional[str] = None
+ type: Optional[Tag] = None
+ body: Optional[Tag] = None
+
+ @attr.s(auto_attribs=True)
+ class _SignatureDesc(FieldDesc):
+ type_origin: Optional[object] = None
+
+ @attr.s(auto_attribs=True)
+ class ReturnDesc(_SignatureDesc):...
+ '''
+
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['ReturnDesc'],
+ '(self, name: Optional[str] = None, type: Optional[Tag] = None, body: Optional[Tag] = None, type_origin: Optional[object] = None)',
+ 'ReturnDesc(name, type, body, type_origin)')
+
+# Test case with attr.ib(init=False):
+@attrs_systemcls_param
+def test_attrs_constructor_attribute_init_False(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s(auto_attribs=True)
+ class MyClass:
+ a: int
+ b: str = attr.ib(init=False)
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int)')
+
+# Test case with attr.ib(kw_only=True):
+@attrs_systemcls_param
+def test_attrs_constructor_attribute_kw_only_reorder(systemcls: Type[model.System], capsys:CapSys) -> None:
+ src = '''\
+ import attr
+ @attr.s(auto_attribs=True)
+ class MyClass:
+ a: int
+ b: str = attr.ib(kw_only=True)
+ c: float
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert not capsys.readouterr().out
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, c: float, *, b: str)')
+
+@attrs_systemcls_param
+def test_converter_init_annotation(systemcls:Type[model.System]) -> None:
+ src = '''\
+ import attr
+
+ class Stuff:
+ ...
+
+ def convert_to_upper(value: object) -> str:
+ return str(value).upper()
+
+ @attr.s
+ class MyClass:
+ name: str = attr.ib(converter=convert_to_upper)
+ st:Stuff = attr.ib(converter=Stuff)
+ age:int = attr.ib(converter=int)
+ '''
+
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, name: object, st: Stuff, age: object)')
+
+@attrs_systemcls_param
+def test_auto_detect_init(systemcls:Type[model.System]) -> None:
+ src = '''\
+ import attr
+
+ @attr.s(auto_detect=True, auto_attribs=True)
+ class MyClass:
+ a: int
+ b: str
+
+ def __init__(self):
+ self.a = 1
+ self.b = 0
+
+ '''
+
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self)')
+
+@attrs_systemcls_param
+def test_auto_detect_init_new_APIs(systemcls:Type[model.System]) -> None:
+ src = '''\
+ import attr
+
+ @attr.define
+ class MyClass:
+ a: int
+ b: str
+
+ def __init__(self):
+ self.a = 1
+ self.b = 0
+
+ '''
+
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self)')
+
+@attrs_systemcls_param
+def test_auto_detect_is_False_init_overriden(systemcls:Type[model.System]) -> None:
+ src = '''\
+ import attr
+
+ @attr.s(auto_detect=False, auto_attribs=True)
+ class MyClass:
+ a: int
+ b: str
+
+ def __init__(self):
+ self.a = 1
+ self.b = 0
+
+ '''
+
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str)')
+
+@attrs_systemcls_param
+def test_auto_detect_is_True_init_is_True(systemcls:Type[model.System]) -> None:
+ # Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*,
+ # *cmp*, or *hash* overrides whatever *auto_detect* would determine.
+
+ src = '''\
+ import attr
+
+ @attr.s(auto_detect=True, auto_attribs=True, init=True)
+ class MyClass:
+ a: int
+ b: str
+
+ def __init__(self):
+ self.a = 1
+ self.b = 0
+
+ '''
+
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str)')
+
+@attrs_systemcls_param
+def test_field_keyword_only_inherited_parameters(systemcls:Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s
+ class A:
+ a = attr.ib(default=0)
+ @attr.s
+ class B(A):
+ b = attr.ib(kw_only=True)
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['B'], '(self, a: int = 0, *, b)')
+
+
+
+@attrs_systemcls_param
+def test_class_keyword_only_inherited_parameters(systemcls:Type[model.System]) -> None:
+ # see https://github.com/python-attrs/attrs/commit/123df6704176d1981cf0d8f15a5021f4e2ce01ed
+ src = '''\
+ import attr
+ @attr.s
+ class A:
+ a = attr.ib(default=0)
+ @attr.s(kw_only=True)
+ class B(A):
+ b = attr.ib()
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['A'], '(self, a: int = 0)')
+ assert_constructor(mod.contents['B'], '(self, *, a: int = 0, b)', 'B(a, b)')
+
+ src = '''\
+ import attr
+ @attr.s(auto_attribs=True)
+ class A:
+ a:int
+ @attr.s(auto_attribs=True, kw_only=True)
+ class B(A):
+ b:int
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['A'], '(self, a: int)')
+ assert_constructor(mod.contents['B'], '(self, *, a: int, b: int)', 'B(a, b)')
+
+@attrs_systemcls_param
+def test_attrs_new_APIs_autodetect_auto_attribs_is_True(systemcls:Type[model.System]) -> None:
+ src = '''\
+ import attrs as attr
+
+ @attr.define(auto_attribs=None)
+ class MyClass:
+ a: int
+ b: str = attr.field(default=attr.Factory(str))
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert mod.contents['MyClass']._cls_options['auto_attribs']==True #type:ignore
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ mod = fromText(src.replace('@attr.define(auto_attribs=None)', '@attr.define'), systemcls=systemcls)
+ assert mod.contents['MyClass']._cls_options['auto_attribs']==True #type:ignore
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ mod = fromText(src.replace('@attr.define(auto_attribs=None)', '@attr.mutable'), systemcls=systemcls)
+ assert mod.contents['MyClass']._cls_options['auto_attribs']==True #type:ignore
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ mod = fromText(src.replace('@attr.define', '@attr.mutable'), systemcls=systemcls)
+ assert mod.contents['MyClass']._cls_options['auto_attribs']==True #type:ignore
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ mod = fromText(src.replace('@attr.define(auto_attribs=None)', '@attr.frozen'), systemcls=systemcls)
+ assert mod.contents['MyClass']._cls_options['auto_attribs']==True #type:ignore
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ mod = fromText(src.replace('@attr.define', '@attr.frozen'), systemcls=systemcls)
+ assert mod.contents['MyClass']._cls_options['auto_attribs']==True #type:ignore
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ # older namespace
+
+ mod = fromText(src.replace('import attrs as attr', 'import attr'), systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ mod = fromText(src.replace('@attr.define(auto_attribs=None)', '@attr.define').replace('import attrs as attr', 'import attr'), systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ mod = fromText(src.replace('@attr.define(auto_attribs=None)', '@attr.mutable').replace('import attrs as attr', 'import attr'), systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ mod = fromText(src.replace('@attr.define', '@attr.mutable').replace('import attrs as attr', 'import attr'), systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ mod = fromText(src.replace('@attr.define(auto_attribs=None)', '@attr.frozen').replace('import attrs as attr', 'import attr'), systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+ mod = fromText(src.replace('@attr.define', '@attr.frozen').replace('import attrs as attr', 'import attr'), systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, b: str = str())')
+
+@attrs_systemcls_param
+def test_attrs_new_APIs_autodetect_auto_attribs_is_False(systemcls:Type[model.System]) -> None:
+ src = '''\
+ import attrs as attr
+
+ @attr.define
+ class MyClass:
+ a: int
+ b = attr.field(factory=set)
+ c = 42
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert mod.contents['MyClass']._cls_options['auto_attribs']==False #type:ignore
+ assert_constructor(mod.contents['MyClass'], '(self, b: set = set())')
+
+@attrs_systemcls_param
+def test_attrs_duplicate_param(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import attr
+ @attr.s(auto_attribs=True)
+ class MyClass:
+ a: int
+
+ if int('36'):
+ @attr.s(auto_attribs=True)
+ class MyClass:
+ a: int
+
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int)')
+
+@pytest.mark.skipif(sys.version_info < (3, 8), reason="type comment requires python 3.8 or later")
+@attrs_systemcls_param
+def test_type_comment_wins_over_factory_annotation(systemcls: Type[model.System]) -> None:
+ src = '''\
+ from typing import List
+ import attr
+
+ @attr.s
+ class SomeClass:
+
+ a_number = attr.ib(default=42)
+ """One number."""
+
+ list_of_numbers = attr.ib(factory=list) # type: list[int]
+ """Multiple numbers."""
+ '''
+
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['SomeClass'], '(self, a_number: int = 42, list_of_numbers: list[int] = list())')
+
+@attrs_systemcls_param
+def test_attrs_alias_param(systemcls: type[model.System]) -> None:
+ src = '''\
+ from attrs import define, field
+ @define
+ class C:
+ _x: int = field(alias="_x")
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['C'], '(self, _x: int)')
+
+@attrs_systemcls_param
+def test_docstring_generated(systemcls: Type[model.System]) -> None:
+ src = '''\
+ 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"
+ '''
+
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['SomeClass'], '(self, *, a: int, a_number: int = 42, list_of_numbers: list[int] = list(), converted_paths: list[str] = list())')
+
+ __init__ = mod.contents['SomeClass'].contents['__init__']
+ assert re.match(
+ r'''attrs generated method''',
+ ''.join(gettext(__init__.parsed_docstring.to_node()))) # type:ignore
+ assert len(__init__.parsed_docstring.fields)==3 # type:ignore
+ assert re.match(
+ r'''docstring of number A\sattr.ib\(factory=list\)List of ints\sattr.ib\(converter=convert_paths, factory=list\)Uses a converter''',
+ ' '.join(text for text in (''.join(gettext(f.body().to_node())) for f in __init__.parsed_docstring.fields)) # type:ignore
+ )
+
+@attrs_systemcls_param
+def test_docstring_generated_dataclass(systemcls: Type[model.System]) -> None:
+ src = '''\
+ from dataclasses import dataclass
+
+ @dataclass
+ class SomeClass:
+ a: int
+ b: str
+ '''
+
+ mod = fromText(src, systemcls=systemcls)
+
+ __init__ = mod.contents['SomeClass'].contents['__init__']
+ assert re.match(
+ r'''dataclass generated method''',
+ ''.join(gettext(__init__.parsed_docstring.to_node()))) # type:ignore
+
+@attrs_systemcls_param
+def test_define_type_comment_not_auto_attribs(systemcls: Type[model.System]) -> None:
+ # this should be interpreted as using auto_attribs=False
+ src='''\
+ import attr
+ @attr.define
+ class A:
+ a = 0 #type:int'''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['A'], '(self)')
+
+@attrs_systemcls_param
+def test_dataclass_basic(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import dataclasses
+
+ @dataclasses.dataclass
+ class C:
+ x: int
+ y: int
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['C'], '(self, x: int, y: int)')
+
+@attrs_systemcls_param
+def test_dataclass_defaults(systemcls: Type[model.System]) -> None:
+ src = '''\
+ from dataclasses import dataclass
+
+ @dataclass
+ class C:
+ name: str
+ age: int = 30
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['C'], '(self, name: str, age: int = 30)')
+
+@attrs_systemcls_param
+def test_dataclass_kw_only(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import dataclasses
+
+ @dataclasses.dataclass(kw_only=True)
+ class C:
+ title: str
+ author: str
+ year: int
+ price: float
+ total_pages: int = 0
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ klass = mod.contents['C']
+ assert_constructor(klass, '(self, *, title: str, author: str, year: int, price: float, total_pages: int = 0)')
+
+@attrs_systemcls_param
+def test_dataclass_kw_only_flag(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import dataclasses
+
+ @dataclasses.dataclass
+ class C:
+ _: dataclasses.KW_ONLY
+ title: str
+ author: str
+ year: int
+ price: float
+ total_pages: int = 0
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['C'], '(self, *, title: str, author: str, year: int, price: float, total_pages: int = 0)')
+
+@attrs_systemcls_param
+def test_dataclass_factory(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import dataclasses
+
+ @dataclasses.dataclass
+ class C:
+ width: int
+ height: int
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['C'], '(self, width: int, height: int)')
+
+@attrs_systemcls_param
+def test_dataclass_no_constructor(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import dataclasses
+ @dataclasses.dataclass(init=False)
+ class C:
+ a: int
+ b: str
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ C = mod.contents['C']
+ assert '__init__' not in C.contents
+
+# Test case for dataclass with single inheritance:
+@attrs_systemcls_param
+def test_dataclass_constructor_single_inheritance(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import dataclasses
+ @dataclasses.dataclass
+ class Base:
+ a: int
+ @dataclasses.dataclass
+ class Derived(Base):
+ b: str
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['Derived'], '(self, a: int, b: str)', 'Derived(a, b)')
+
+# Test case for dataclass with multiple inheritance:
+@attrs_systemcls_param
+def test_dataclass_constructor_multiple_inheritance(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import dataclasses
+ @dataclasses.dataclass
+ class Base1:
+ a: int
+ @dataclasses.dataclass
+ class Base2:
+ b: str
+ @dataclasses.dataclass
+ class Derived(Base1, Base2):
+ c: float
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['Derived'], '(self, b: str, a: int, c: float)', 'Derived(b, a, c)')
+
+# Test case for dataclass with overridden attributes:
+@attrs_systemcls_param
+def test_dataclass_constructor_single_inheritance_overridden_attribute(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import dataclasses
+ @dataclasses.dataclass
+ class Base:
+ a: int
+ b: str = "default"
+ @dataclasses.dataclass
+ class Derived(Base):
+ b: str = "overridden"
+ c: float = 3.14
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['Derived'], '(self, a: int, b: str = \'overridden\', c: float = 3.14)', 'Derived(a, b, c)')
+
+@attrs_systemcls_param
+def test_dataclass_constructor_single_inheritance_traverse_subclasses(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import dataclasses
+ @dataclasses.dataclass
+ class FieldDesc:
+ name: Optional[str] = None
+ type: Optional[Tag] = None
+ body: Optional[Tag] = None
+ @dataclasses.dataclass
+ class _SignatureDesc(FieldDesc):
+ type_origin: Optional[object] = None
+ @dataclasses.dataclass
+ class ReturnDesc(_SignatureDesc):...
+ '''
+
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['ReturnDesc'],
+ '(self, name: Optional[str] = None, type: Optional[Tag] = None, body: Optional[Tag] = None, type_origin: Optional[object] = None)',
+ 'ReturnDesc(name, type, body, type_origin)')
+
+# Test case with dataclass field having init=False:
+@attrs_systemcls_param
+def test_dataclass_constructor_attribute_init_False(systemcls: Type[model.System]) -> None:
+ src = '''\
+ import dataclasses
+ @dataclasses.dataclass
+ class MyClass:
+ a: int
+ b: str = dataclasses.field(init=False)
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert_constructor(mod.contents['MyClass'], '(self, a: int)')
+
+# Test case with dataclass field having kw_only=True:
+@attrs_systemcls_param
+def test_dataclass_constructor_attribute_kw_only_reorder(systemcls: Type[model.System], capsys:CapSys) -> None:
+ src = '''\
+ import dataclasses
+ @dataclasses.dataclass
+ class MyClass:
+ a: int
+ b: str = dataclasses.field(kw_only=True)
+ c: float
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert not capsys.readouterr().out
+ assert_constructor(mod.contents['MyClass'], '(self, a: int, c: float, *, b: str)')
+
+@attrs_systemcls_param
+def test_dataclass_constructor_kw_only_reordering_with_inheritence(systemcls: Type[model.System], capsys:CapSys) -> None:
+ # see https://docs.python.org/3/library/dataclasses.html#re-ordering-of-keyword-only-parameters-in-init
+ src = '''\
+ import dataclasses
+
+ @dataclasses.dataclass
+ class Base:
+ x: Any = 15.0
+ y: int = dataclasses.field(kw_only=True, default=0)
+ w: int = dataclasses.field(kw_only=True, default=1)
+
+ @dataclasses.dataclass
+ class MyClass(Base):
+ z: int = 10
+ t: int = dataclasses.field(kw_only=True, default=0)
+ '''
+ mod = fromText(src, systemcls=systemcls)
+ assert not capsys.readouterr().out
+ assert_constructor(mod.contents['MyClass'], '(self, x: Any = 15.0, z: int = 10, *, y: int = 0, w: int = 1, t: int = 0)')
+
@attrs_systemcls_param
def test_attrs_class_else_branch(systemcls: Type[model.System]) -> None:
@@ -145,4 +893,4 @@ class C:
''', systemcls=systemcls)
var = mod.contents['C'].contents['var']
- assert var.kind is model.DocumentableKind.INSTANCE_VARIABLE
\ No newline at end of file
+ assert var.kind is model.DocumentableKind.INSTANCE_VARIABLE
diff --git a/pydoctor/test/test_templatewriter.py b/pydoctor/test/test_templatewriter.py
index 3c9f6e9d3..93bc14d87 100644
--- a/pydoctor/test/test_templatewriter.py
+++ b/pydoctor/test/test_templatewriter.py
@@ -838,8 +838,8 @@ class Foo:
_sorted = sorted(Foo.contents.values(), key=system.membersOrder(Foo))
names = [s.name for s in _sorted]
-
- assert names == ['b', 'a'] # should be 'b', 'a'.
+ # auto-generated __init__
+ assert names == ['__init__', 'b', 'a'] # should be 'b', 'a'.
src_crash_xml_entities = '''\
diff --git a/tox.ini b/tox.ini
index d8c76688a..7de83ddab 100644
--- a/tox.ini
+++ b/tox.ini
@@ -123,6 +123,7 @@ commands =
git clone --depth 1 https://github.com/temporalio/sdk-python.git {toxworkdir}/temporalio; \
fi"
sh -c "cd {toxworkdir}/temporalio && git pull"
+ sh -c "cd {toxworkdir}/temporalio && sed -i 's/^warnings-as-errors.*/warnings-as-errors=false/' pyproject.toml"
rm -rf {toxworkdir}/temporalio-output
sh -c "pydoctor --config={toxworkdir}/temporalio/pyproject.toml \
--html-output={toxworkdir}/temporalio-output --theme=readthedocs \