From dea121ceaea5415bde8a32e1f433e924a71de1f8 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 25 Oct 2024 12:30:57 -0400 Subject: [PATCH 01/38] Introduce ParsedDocstring.with_linker()/.with_tag()/.combine(). The will help to construct one parsed docstring from several parts. --- pydoctor/epydoc/markup/__init__.py | 104 ++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index 3d2d721fc..bf93146dc 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -33,6 +33,8 @@ from __future__ import annotations __docformat__ = 'epytext en' +from functools import cache +from itertools import chain from typing import Callable, ContextManager, List, Optional, Sequence, Iterator, TYPE_CHECKING import abc import sys @@ -147,7 +149,8 @@ def __init__(self, fields: Sequence['Field']): self._stan: Optional[Tag] = None self._summary: Optional['ParsedDocstring'] = None - @abc.abstractproperty + @property + @abc.abstractmethod def has_body(self) -> bool: """ Does this docstring have a non-empty body? @@ -202,6 +205,34 @@ def to_node(self) -> nodes.document: This method might raise L{NotImplementedError} in such cases. (i.e. L{pydoctor.epydoc.markup._types.ParsedTypeDocstring}) """ raise NotImplementedError() + + def with_linker(self, linker: DocstringLinker) -> ParsedDocstring: + """ + Pre-set the linker object for this parsed docstring. + Whatever is passed to L{to_stan()} will be ignored. + """ + return _EnforcedLinkerParsedDocstring(self, linker=linker) + + def with_tag(self, tag: Tag) -> ParsedDocstring: + """ + Wraps the L{to_stan()} result inside the given tag. + + This is useful because some code strips the main tag to keep only it's content. + With this trick, the main tag is preserved. It can also be used to add + a custom CSS class on top of an existing parsed docstring. + """ + # We double wrap it with a transparent tag so the added tags survives ParsedDocstring.combine + # wich combines the content of the main div of the stan, not the div itself. + return _WrappedInTagParsedDocstring( + _WrappedInTagParsedDocstring(self, tag=tag), + tag=tags.transparent) + + @classmethod + def combine(cls, elements: Sequence[ParsedDocstring]) -> ParsedDocstring: + """ + Combine the contents of several parsed docstrings into one. + """ + return _CombinedParsedDocstring(elements) def get_summary(self) -> 'ParsedDocstring': """ @@ -218,11 +249,82 @@ def get_summary(self) -> 'ParsedDocstring': visitor = SummaryExtractor(_document) _document.walk(visitor) except Exception: + # TODO: These could be replaced by parsed_text().with_tag(...) self._summary = epydoc2stan.ParsedStanOnly(tags.span(class_='undocumented')("Broken summary")) else: self._summary = visitor.summary or epydoc2stan.ParsedStanOnly(tags.span(class_='undocumented')("No summary")) return self._summary + +class _CombinedParsedDocstring(ParsedDocstring): + """ + Wraps several parsed docstrings into a single one. + """ + + def __init__(self, elements: Sequence[ParsedDocstring]): + super().__init__(tuple(chain.from_iterable(e.fields for e in elements))) + self._elements = elements + + @property + def has_body(self) -> bool: + return any(e.has_body for e in self._elements) + + @cache + def to_node(self) -> nodes.document: + doc = new_document('composite') + for e in self._elements: + # TODO: Some parsed doctrings simply do not implement to_node() at this time. + # It should be really time to fix this... + subdoc = e.to_node() + # TODO: here all childrens might not have the same document property. + # this should not be a problem, but docutils is likely not meant to be used like that. + doc.children.extend(subdoc.children) + return doc + + @cache + def to_stan(self, linker: DocstringLinker) -> Tag: + stan = tags.transparent() + for e in self._elements: + stan(e.to_stan(linker).children) + return stan + +class _EnforcedLinkerParsedDocstring(ParsedDocstring): + """ + Wraps an existing parsed docstring to be rendered with the + given linker instead of whatever is passed to L{to_stan()}. + """ + def __init__(self, other: ParsedDocstring, *, linker: DocstringLinker): + super().__init__(other.fields) + self.linker = linker + self._parsed = other + + @property + def has_body(self) -> bool: + return self._parsed.has_body + + def to_stan(self, _: object) -> Tag: + return self._parsed.to_stan(self.linker) + + def to_node(self) -> nodes.document: + return self._parsed.to_node() + + +class _WrappedInTagParsedDocstring(ParsedDocstring): + def __init__(self, other: ParsedDocstring, *, tag: Tag) -> None: + super().__init__(other.fields) + self._parsed = other + self._tag = tag + + @property + def has_body(self): + return self._parsed.has_body + + def to_stan(self, linker: DocstringLinker) -> Tag: + return self._tag( + self._parsed.to_stan(linker)) + + def to_node(self) -> nodes.document: + return self._parsed.to_node() ################################################## ## Fields From 95f8f3d21c79a17518b14a85de5ddc382d0d5c1b Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 25 Oct 2024 12:44:05 -0400 Subject: [PATCH 02/38] Colorize the signature ourself. We mimic the Signature.__str__ method for the implementation but instead of returning a str we return a ParsedDocstring, which is far more convenient. This change fixes #801: - Parameters html are divided into .sig-param spans. - When the function is long enought an extra CSS class .expand-signature is added to the parent function-signature. - The first parameter 'cls' or 'self' of (class) methods is marked with the 'undocumented' CSS class, this way it's clearly not part of the API. - Add some CSS to expand the signature of long functions when they have the focus only. --- pydoctor/astbuilder.py | 56 ++---- pydoctor/epydoc2stan.py | 201 ++++++++++++++++++++++ pydoctor/model.py | 4 +- pydoctor/templatewriter/pages/__init__.py | 28 +-- pydoctor/test/test_astbuilder.py | 27 +-- pydoctor/test/test_epydoc2stan.py | 15 +- pydoctor/themes/base/apidocs.css | 10 +- 7 files changed, 268 insertions(+), 73 deletions(-) diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py index aa35626cc..2520a4f1f 100644 --- a/pydoctor/astbuilder.py +++ b/pydoctor/astbuilder.py @@ -19,6 +19,9 @@ 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) +class InvalidSignatureParamName(str): + def isidentifier(self): + return True def parseFile(path: Path) -> ast.Module: """Parse the contents of a Python source file.""" @@ -1032,9 +1035,9 @@ def get_default(index: int) -> Optional[ast.expr]: parameters: List[Parameter] = [] def add_arg(name: str, kind: Any, default: Optional[ast.expr]) -> None: - default_val = Parameter.empty if default is None else _ValueFormatter(default, ctx=func) + default_val = Parameter.empty if default is None else default # this cast() is safe since we're checking if annotations.get(name) is None first - annotation = Parameter.empty if annotations.get(name) is None else _AnnotationValueFormatter(cast(ast.expr, annotations[name]), ctx=func) + annotation = Parameter.empty if annotations.get(name) is None else cast(ast.expr, annotations[name]) parameters.append(Parameter(name, kind, default=default_val, annotation=annotation)) for index, arg in enumerate(posonlyargs): @@ -1056,12 +1059,15 @@ def add_arg(name: str, kind: Any, default: Optional[ast.expr]) -> None: add_arg(kwarg.arg, Parameter.VAR_KEYWORD, None) return_type = annotations.get('return') - return_annotation = Parameter.empty if return_type is None or is_none_literal(return_type) else _AnnotationValueFormatter(return_type, ctx=func) + return_annotation = Parameter.empty if return_type is None or is_none_literal(return_type) else return_type try: signature = Signature(parameters, return_annotation=return_annotation) except ValueError as ex: func.report(f'{func.fullName()} has invalid parameters: {ex}') - signature = Signature() + # Craft an invalid signature that does not look like a function with zero arguments. + signature = Signature( + [Parameter(InvalidSignatureParamName('...'), + kind=Parameter.POSITIONAL_OR_KEYWORD)]) func.annotations = annotations @@ -1120,7 +1126,7 @@ def _annotations_from_function( @param func: The function definition's AST. @return: Mapping from argument name to annotation. The name C{return} is used for the return type. - Unannotated arguments are omitted. + Unannotated arguments are still included with a None value. """ def _get_all_args() -> Iterator[ast.arg]: base_args = func.args @@ -1153,47 +1159,7 @@ def _get_all_ast_annotations() -> Iterator[Tuple[str, Optional[ast.expr]]]: value, self.builder.current), self.builder.current) for name, value in _get_all_ast_annotations() } - -class _ValueFormatter: - """ - Class to encapsulate a python value and translate it to HTML when calling L{repr()} on the L{_ValueFormatter}. - Used for presenting default values of parameters. - """ - - def __init__(self, value: ast.expr, ctx: model.Documentable): - self._colorized = colorize_inline_pyval(value) - """ - The colorized value as L{ParsedDocstring}. - """ - - self._linker = ctx.docstring_linker - """ - Linker. - """ - def __repr__(self) -> str: - """ - Present the python value as HTML. - Without the englobing tags. - """ - # Using node2stan.node2html instead of flatten(to_stan()). - # This avoids calling flatten() twice, - # but potential XML parser errors caused by XMLString needs to be handled later. - return ''.join(node2stan.node2html(self._colorized.to_node(), self._linker)) - -class _AnnotationValueFormatter(_ValueFormatter): - """ - Special L{_ValueFormatter} for function annotations. - """ - def __init__(self, value: ast.expr, ctx: model.Function): - super().__init__(value, ctx) - self._linker = linker._AnnotationLinker(ctx) - - def __repr__(self) -> str: - """ - Present the annotation wrapped inside tags. - """ - return '%s' % super().__repr__() DocumentableT = TypeVar('DocumentableT', bound=model.Documentable) diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index 8b55497d8..c257102d5 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -5,12 +5,14 @@ from collections import defaultdict import enum +import inspect from typing import ( TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict, Dict, Generator, Iterator, List, Mapping, Optional, Sequence, Tuple, Union, ) import ast import re +from functools import cache import attr from docutils import nodes @@ -1172,3 +1174,202 @@ def get_constructors_extra(cls:model.Class) -> ParsedDocstring | None: set_node_attributes(document, children=elements) return ParsedRstDocstring(document, ()) + +@cache +def parsed_text(text: str) -> ParsedDocstring: + """ + Enacpsulate some raw text with no markup inside a L{ParsedDocstring}. + """ + document = new_document('text') + txt_node = set_node_attributes( + nodes.Text(text), + document=document, + lineno=1) + set_node_attributes(document, children=[txt_node]) + return ParsedRstDocstring(document, ()) + + +def _colorize_signature_annotation(annotation: object, + ctx: model.Documentable) -> ParsedDocstring: + """ + Returns L{ParsedDocstring} with extra context to make + sure we resolve tha annotation correctly. + """ + return colorize_inline_pyval(annotation + # Make sure to use the annotation linker in the context of an annotation. + ).with_linker(linker._AnnotationLinker(ctx) + # Make sure the generated tags are not stripped by ParsedDocstring.combine. + ).with_tag(tags.transparent) + +def _is_less_important_param(param: inspect.Parameter, signature:inspect.Signature, ctx: model.Documentable) -> bool: + """ + Whether this parameter is the 'self' param of methods or 'cls' param of class methods. + """ + if param.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY): + return False + if (param.name == 'self' and ctx.kind is model.DocumentableKind.METHOD) or ( + param.name == 'cls' and ctx.kind is model.DocumentableKind.CLASS_METHOD): + if next(iter(signature.parameters.values())) is not param: + return False + # it's not the first param, so don't mark it less important + return param.annotation is inspect._empty and param.default is inspect._empty + return False + +# From inspect.Parameter.__str__() (Python 3.13) +def _colorize_signature_param(param: inspect.Parameter, + signature: inspect.Signature, + ctx: model.Documentable, + has_next: bool) -> ParsedDocstring: + """ + One parameter is converted to a series of ParsedDocstrings. + + - one, the first, for the param name + - two others if the parameter is annotated: one for ': ' and one for the annotation + - two others if the paramter has a default value: one for ' = ' and one for the annotation + """ + kind = param.kind + result: list[ParsedDocstring] = [] + if kind == inspect.Parameter.VAR_POSITIONAL: + result += [parsed_text(f'*{param.name}')] + elif kind == inspect.Parameter.VAR_KEYWORD: + result += [parsed_text(f'**{param.name}')] + else: + if _is_less_important_param(param, signature, ctx): + result += [parsed_text(param.name).with_tag( + tags.span(class_="undocumented"))] + else: + result += [parsed_text(param.name)] + + # Add annotation and default value + if param.annotation is not inspect._empty: + result += [ + parsed_text(': '), + _colorize_signature_annotation(param.annotation, ctx) + ] + + if param.default is not inspect._empty: + if param.annotation is not inspect._empty: + # TODO: should we keep these two different manners ? + result += [parsed_text(' = ')] + else: + result += [parsed_text('=')] + + result += [colorize_inline_pyval(param.default)] + + if has_next: + result.append(parsed_text(', ')) + + # use the same css class as Sphinx + return ParsedDocstring.combine(result).with_tag( + tags.span(class_='sig-param')) + + +# From inspect.Signature.format() (Python 3.13) +def _colorize_signature(sig: inspect.Signature, ctx: model.Documentable) -> ParsedDocstring: + """ + Colorize this signature into a ParsedDocstring. + """ + result: list[ParsedDocstring] = [] + render_pos_only_separator = False + render_kw_only_separator = True + param_number = len(sig.parameters) + for i, param in enumerate(sig.parameters.values()): + kind = param.kind + has_next = (i+1 < param_number) + + if kind == inspect.Parameter.POSITIONAL_ONLY: + render_pos_only_separator = True + elif render_pos_only_separator: + # It's not a positional-only parameter, and the flag + # is set to 'True' (there were pos-only params before.) + if has_next: + result.append(parsed_text('/, ')) + else: + result.append(parsed_text('/')) + render_pos_only_separator = False + + if kind == inspect.Parameter.VAR_POSITIONAL: + # OK, we have an '*args'-like parameter, so we won't need + # a '*' to separate keyword-only arguments + render_kw_only_separator = False + elif kind == inspect.Parameter.KEYWORD_ONLY and render_kw_only_separator: + # We have a keyword-only parameter to render and we haven't + # rendered an '*args'-like parameter before, so add a '*' + # separator to the parameters list ("foo(arg1, *, arg2)" case) + if has_next: + result.append(parsed_text('*, ')) + else: + result.append(parsed_text('*')) + # This condition should be only triggered once, so + # reset the flag + render_kw_only_separator = False + + result.append(_colorize_signature_param(param, sig, ctx, + has_next=has_next or render_pos_only_separator)) + + if render_pos_only_separator: + # There were only positional-only parameters, hence the + # flag was not reset to 'False' + result.append(parsed_text('/')) + + result = [parsed_text('(')] + result + [parsed_text(')')] + + if sig.return_annotation is not inspect._empty: + result += [parsed_text(' -> '), + _colorize_signature_annotation(sig.return_annotation, ctx)] + + return ParsedDocstring.combine(result) + +@cache +def get_parsed_signature(func: Union[model.Function, model.FunctionOverload]) -> ParsedDocstring | None: + signature = func.signature + if signature is None: + # TODO:When the value is None, it should probably not be cached + # just yet because one could have called this function too + # early in the process when the signature property is not set yet. + # Is this possible ? + return None + + ctx = func.primary if isinstance(func, model.FunctionOverload) else func + return _colorize_signature(signature, ctx) + +LONG_FUNCTION_DEF = 80 # this doesn't acount for the 'def ' and the ending ':' +""" +Maximum size of a function definition to be rendered on a single line. +The multiline formatting is only applied at the CSS level to stay customizable. +We add a css class to the signature HTML to signify the signature could possibly +be better formatted on several lines. +""" + +def is_long_function_def(func: model.Function | model.FunctionOverload) -> bool: + """ + Whether this function definition is considered as long. + The lenght of the a function def is defnied by the lenght of it's name plus the lenght of it's signature. + On top of that, a function or method that takes no argument (expect unannotated 'self' for methods, and 'cls' for classmethods) + is never considered as long. + + @see: L{LONG_FUNCTION_DEF} + """ + if func.signature is None: + return False + nargs = len(func.signature.parameters) + if nargs == 0: + # no arguments at all -> never long + return False + ctx = func.primary if isinstance(func, model.FunctionOverload) else func + param1 = next(iter(func.signature.parameters.values())) + if _is_less_important_param(param1, func.signature, ctx): + nargs -= 1 + if nargs == 0: + # method with only unannotated self/cls parameter -> never long + return False + + sig = get_parsed_signature(func) + if sig is None: + # this should never happen since we checked if func.signature is None. + return False + + name_len = len(ctx.name) + signature_len = len(''.join(node2stan.gettext(sig.to_node()))) + return LONG_FUNCTION_DEF - (name_len + signature_len) < 0 + \ No newline at end of file diff --git a/pydoctor/model.py b/pydoctor/model.py index 9c78fc3d0..e29388dff 100644 --- a/pydoctor/model.py +++ b/pydoctor/model.py @@ -869,14 +869,14 @@ def setup(self) -> None: self.signature = None self.overloads = [] -@attr.s(auto_attribs=True) +@attr.s(auto_attribs=True, frozen=True) class FunctionOverload: """ @note: This is not an actual documentable type. """ primary: Function signature: Signature - decorators: Sequence[ast.expr] + decorators: Sequence[ast.expr] = attr.ib(converter=tuple) class Attribute(Inheritable): kind: Optional[DocumentableKind] = DocumentableKind.ATTRIBUTE diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 22dabe5f0..8442e79ac 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -14,7 +14,7 @@ from pydoctor.extensions import zopeinterface from pydoctor.stanutils import html2stan -from pydoctor import epydoc2stan, model, linker, __version__ +from pydoctor import epydoc2stan, model, linker, __version__, node2stan from pydoctor.astbuilder import node2fullname from pydoctor.templatewriter import util, TemplateLookup, TemplateElement from pydoctor.templatewriter.pages.table import ChildTable @@ -57,14 +57,19 @@ def format_signature(func: Union[model.Function, model.FunctionOverload]) -> "Fl Return a stan representation of a nicely-formatted source-like function signature for the given L{Function}. Arguments default values are linked to the appropriate objects when possible. """ - broken = "(...)" - try: - return html2stan(str(func.signature)) if func.signature else broken - except Exception as e: - # We can't use safe_to_stan() here because we're using Signature.__str__ to generate the signature HTML. - epydoc2stan.reportErrors(func.primary if isinstance(func, model.FunctionOverload) else func, - [epydoc2stan.get_to_stan_error(e)], section='signature') - return broken + + parsed_sig = epydoc2stan.get_parsed_signature(func) + if parsed_sig is None: + return "(...)" + ctx = func.primary if isinstance(func, model.FunctionOverload) else func + return epydoc2stan.safe_to_stan( + parsed_sig, + ctx.docstring_linker, + ctx, + fallback=lambda _, doc, ___: tags.transparent( + node2stan.gettext(doc.to_node())), + section='signature' + ) def format_class_signature(cls: model.Class) -> "Flattenable": """ @@ -125,10 +130,13 @@ def format_function_def(func_name: str, is_async: bool, def_stmt = 'async def' if is_async else 'def' if func_name.endswith('.setter') or func_name.endswith('.deleter'): func_name = func_name[:func_name.rindex('.')] + func_class = 'function-signature' + if epydoc2stan.is_long_function_def(func): + func_class += ' expand-signature' r.extend([ tags.span(def_stmt, class_='py-keyword'), ' ', tags.span(func_name, class_='py-defname'), - tags.span(format_signature(func), class_='function-signature'), ':', + tags.span(format_signature(func), class_=func_class), ':', ]) return r diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py index bf45246fb..db3ffe98b 100644 --- a/pydoctor/test/test_astbuilder.py +++ b/pydoctor/test/test_astbuilder.py @@ -1,14 +1,17 @@ +from __future__ import annotations + from typing import Optional, Tuple, Type, List, overload, cast import ast import sys -from pydoctor import astbuilder, astutils, model +from pydoctor import astbuilder, astutils, model, node2stan from pydoctor import epydoc2stan from pydoctor.epydoc.markup import DocstringLinker, ParsedDocstring from pydoctor.options import Options from pydoctor.stanutils import flatten, html2stan, flatten_text from pydoctor.epydoc.markup.epytext import Element, ParsedEpytextDocstring -from pydoctor.epydoc2stan import _get_docformat, format_summary, get_parsed_type +from pydoctor.epydoc2stan import _get_docformat, format_summary, get_parsed_signature, get_parsed_type +from pydoctor.templatewriter.pages import format_signature from pydoctor.test.test_packages import processPackage from pydoctor.utils import partialclass @@ -105,6 +108,13 @@ def to_html( ) -> str: return flatten(parsed_docstring.to_stan(linker)) +def signature2str(func: model.Function | model.FunctionOverload) -> str: + doc = get_parsed_signature(func) + fromhtml = flatten_text(format_signature(func)) + fromdocutils = ''.join(node2stan.gettext(doc.to_node())) + assert fromhtml == fromdocutils + return fromhtml + @overload def type2str(type_expr: None) -> None: ... @@ -225,14 +235,12 @@ def test_function_signature(signature: str, systemcls: Type[model.System]) -> No """ A round trip from source to inspect.Signature and back produces the original text. - - @note: Our inspect.Signature Paramters objects are now tweaked such that they might produce HTML tags, handled by the L{PyvalColorizer}. """ mod = fromText(f'def f{signature}: ...', systemcls=systemcls) docfunc, = mod.contents.values() assert isinstance(docfunc, model.Function) # This little trick makes it possible to back reproduce the original signature from the genrated HTML. - text = flatten_text(html2stan(str(docfunc.signature))) + text = signature2str(docfunc) assert text == signature @posonlyargs @@ -266,7 +274,7 @@ def test_function_badsig(signature: str, systemcls: Type[model.System], capsys: mod = fromText(f'def f{signature}: ...', systemcls=systemcls, modname='mod') docfunc, = mod.contents.values() assert isinstance(docfunc, model.Function) - assert str(docfunc.signature) == '()' + assert signature2str(docfunc) == '(...)' captured = capsys.readouterr().out assert captured.startswith("mod:1: mod.f has invalid parameters: ") @@ -1666,14 +1674,13 @@ def parse(s:str)->bytes: """, systemcls=systemcls) func = mod.contents['parse'] assert isinstance(func, model.Function) - # Work around different space arrangements in Signature.__str__ between python versions - assert flatten_text(html2stan(str(func.signature).replace(' ', ''))) == '(s:Union[str,bytes])->Union[str,bytes]' + assert signature2str(func) == '(s: Union[str, bytes]) -> Union[str, bytes]' assert [astbuilder.node2dottedname(d) for d in (func.decorators or ())] == [] assert len(func.overloads) == 2 assert [astbuilder.node2dottedname(d) for d in func.overloads[0].decorators] == [['dec'], ['overload']] assert [astbuilder.node2dottedname(d) for d in func.overloads[1].decorators] == [['overload']] - assert flatten_text(html2stan(str(func.overloads[0].signature).replace(' ', ''))) == '(s:str)->str' - assert flatten_text(html2stan(str(func.overloads[1].signature).replace(' ', ''))) == '(s:bytes)->bytes' + assert signature2str(func.overloads[0]) == '(s: str) -> str' + assert signature2str(func.overloads[1]) == '(s: bytes) -> bytes' assert capsys.readouterr().out.splitlines() == [ ':11: .parse overload has docstring, unsupported', ':15: .parse overload appeared after primary function', diff --git a/pydoctor/test/test_epydoc2stan.py b/pydoctor/test/test_epydoc2stan.py index 3610c9a48..fe2b48924 100644 --- a/pydoctor/test/test_epydoc2stan.py +++ b/pydoctor/test/test_epydoc2stan.py @@ -1976,8 +1976,10 @@ def f(self, x:typ) -> typ: assert isinstance(f, model.Function) assert f.signature - assert "href" in repr(f.signature.parameters['x'].annotation) - assert "href" in repr(f.signature.return_annotation) + assert "href" in flatten(epydoc2stan._colorize_signature_annotation( + f.signature.parameters['x'].annotation, f).to_stan(None)) + assert "href" in flatten(epydoc2stan._colorize_signature_annotation( + f.signature.return_annotation, f).to_stan(None)) assert isinstance(var, model.Attribute) assert "href" in flatten(epydoc2stan.type2stan(var) or '') @@ -2005,8 +2007,13 @@ def f(self, x:typ) -> typ: assert isinstance(f, model.Function) assert f.signature - assert 'href="index.html#typ"' in repr(f.signature.parameters['x'].annotation) - assert 'href="index.html#typ"' in repr(f.signature.return_annotation) + assert 'href="index.html#typ"' in flatten(epydoc2stan._colorize_signature_annotation( + f.signature.parameters['x'].annotation, f).to_stan(None)) + # the linker can be None here because the annotations uses with_linker() + + assert 'href="index.html#typ"' in flatten(epydoc2stan._colorize_signature_annotation( + f.signature.return_annotation, f).to_stan(None)) + # the linker can be None here because the annotations uses with_linker() assert isinstance(var, model.Attribute) assert 'href="index.html#typ"' in flatten(epydoc2stan.type2stan(var) or '') diff --git a/pydoctor/themes/base/apidocs.css b/pydoctor/themes/base/apidocs.css index 537c3ba5c..304cc6a6d 100644 --- a/pydoctor/themes/base/apidocs.css +++ b/pydoctor/themes/base/apidocs.css @@ -400,8 +400,14 @@ table .private { word-spacing: -5px; } -.function-signature code { - padding: 2px 1px; +/* Force each parameter onto a new line */ +#childList a:target ~ .functionHeader .function-signature.expand-signature .sig-param { + display: block; + margin-left: 20px; +} + +.sig-param .undocumented { + font-size: 93%; } /* From 0b7c76e2d5e000bb14ae3cd24371fbb3eef4cd49 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 25 Oct 2024 15:58:14 -0400 Subject: [PATCH 03/38] Some more adjustments. Make the self param always inline. Try to optimiza a little the _colorize_signature() function. --- pydoctor/epydoc2stan.py | 81 ++++++++++--------- pydoctor/themes/base/apidocs.css | 22 +++-- .../themes/readthedocs/readthedocstheme.css | 8 +- 3 files changed, 67 insertions(+), 44 deletions(-) diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index c257102d5..dca23dcf4 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -1188,6 +1188,15 @@ def parsed_text(text: str) -> ParsedDocstring: set_node_attributes(document, children=[txt_node]) return ParsedRstDocstring(document, ()) +# not using @cache here because it cause too munch trouble since the ParsedDocstring +# are actually not immutable. +def parsed_text_with_css(text:str, css_class: str) -> ParsedDocstring: + parsed_doc = parsed_text(text) + if not css_class: + return parsed_doc + return parsed_doc.with_tag(tags.span(class_=css_class)) + +_empty = inspect.Parameter.empty def _colorize_signature_annotation(annotation: object, ctx: model.Documentable) -> ParsedDocstring: @@ -1201,25 +1210,25 @@ def _colorize_signature_annotation(annotation: object, # Make sure the generated tags are not stripped by ParsedDocstring.combine. ).with_tag(tags.transparent) -def _is_less_important_param(param: inspect.Parameter, signature:inspect.Signature, ctx: model.Documentable) -> bool: +def _is_less_important_param(param: inspect.Parameter, ctx: model.Documentable) -> bool: """ Whether this parameter is the 'self' param of methods or 'cls' param of class methods. + + @Note: this does not check whether the parameter is the first of the signature. + This should be done before calling this function! """ if param.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY): return False if (param.name == 'self' and ctx.kind is model.DocumentableKind.METHOD) or ( param.name == 'cls' and ctx.kind is model.DocumentableKind.CLASS_METHOD): - if next(iter(signature.parameters.values())) is not param: - return False - # it's not the first param, so don't mark it less important - return param.annotation is inspect._empty and param.default is inspect._empty + return param.annotation is _empty and param.default is _empty return False # From inspect.Parameter.__str__() (Python 3.13) def _colorize_signature_param(param: inspect.Parameter, - signature: inspect.Signature, ctx: model.Documentable, - has_next: bool) -> ParsedDocstring: + has_next: bool, + is_first: bool, ) -> ParsedDocstring: """ One parameter is converted to a series of ParsedDocstrings. @@ -1230,31 +1239,28 @@ def _colorize_signature_param(param: inspect.Parameter, kind = param.kind result: list[ParsedDocstring] = [] if kind == inspect.Parameter.VAR_POSITIONAL: - result += [parsed_text(f'*{param.name}')] + result.append(parsed_text(f'*{param.name}')) elif kind == inspect.Parameter.VAR_KEYWORD: - result += [parsed_text(f'**{param.name}')] + result.append(parsed_text(f'**{param.name}')) else: - if _is_less_important_param(param, signature, ctx): - result += [parsed_text(param.name).with_tag( - tags.span(class_="undocumented"))] + if is_first and _is_less_important_param(param, ctx): + result.append(parsed_text_with_css(param.name, css_class='undocumented')) else: - result += [parsed_text(param.name)] + result.append(parsed_text(param.name)) # Add annotation and default value - if param.annotation is not inspect._empty: - result += [ - parsed_text(': '), - _colorize_signature_annotation(param.annotation, ctx) - ] - - if param.default is not inspect._empty: - if param.annotation is not inspect._empty: + if param.annotation is not _empty: + result.append(parsed_text(': ')) + result.append(_colorize_signature_annotation(param.annotation, ctx)) + + if param.default is not _empty: + if param.annotation is not _empty: # TODO: should we keep these two different manners ? - result += [parsed_text(' = ')] + result.append(parsed_text(' = ')) else: - result += [parsed_text('=')] + result.append(parsed_text('=')) - result += [colorize_inline_pyval(param.default)] + result.append(colorize_inline_pyval(param.default)) if has_next: result.append(parsed_text(', ')) @@ -1273,6 +1279,8 @@ def _colorize_signature(sig: inspect.Signature, ctx: model.Documentable) -> Pars render_pos_only_separator = False render_kw_only_separator = True param_number = len(sig.parameters) + result.append(parsed_text('(')) + for i, param in enumerate(sig.parameters.values()): kind = param.kind has_next = (i+1 < param_number) @@ -1283,9 +1291,9 @@ def _colorize_signature(sig: inspect.Signature, ctx: model.Documentable) -> Pars # It's not a positional-only parameter, and the flag # is set to 'True' (there were pos-only params before.) if has_next: - result.append(parsed_text('/, ')) + result.append(parsed_text_with_css('/, ', css_class='sig-symbol')) else: - result.append(parsed_text('/')) + result.append(parsed_text_with_css('/', css_class='sig-symbol')) render_pos_only_separator = False if kind == inspect.Parameter.VAR_POSITIONAL: @@ -1297,26 +1305,27 @@ def _colorize_signature(sig: inspect.Signature, ctx: model.Documentable) -> Pars # rendered an '*args'-like parameter before, so add a '*' # separator to the parameters list ("foo(arg1, *, arg2)" case) if has_next: - result.append(parsed_text('*, ')) + result.append(parsed_text_with_css('*, ', css_class='sig-symbol')) else: - result.append(parsed_text('*')) + result.append(parsed_text_with_css('*', css_class='sig-symbol')) # This condition should be only triggered once, so # reset the flag render_kw_only_separator = False - result.append(_colorize_signature_param(param, sig, ctx, - has_next=has_next or render_pos_only_separator)) + result.append(_colorize_signature_param(param, ctx, + has_next=has_next or render_pos_only_separator, + is_first=i==0)) if render_pos_only_separator: # There were only positional-only parameters, hence the # flag was not reset to 'False' - result.append(parsed_text('/')) + result.append(parsed_text_with_css('/', css_class='sig-symbol')) - result = [parsed_text('(')] + result + [parsed_text(')')] + result.append(parsed_text(')')) - if sig.return_annotation is not inspect._empty: - result += [parsed_text(' -> '), - _colorize_signature_annotation(sig.return_annotation, ctx)] + if sig.return_annotation is not _empty: + result.append(parsed_text(' -> ')) + result.append(_colorize_signature_annotation(sig.return_annotation, ctx)) return ParsedDocstring.combine(result) @@ -1358,7 +1367,7 @@ def is_long_function_def(func: model.Function | model.FunctionOverload) -> bool: return False ctx = func.primary if isinstance(func, model.FunctionOverload) else func param1 = next(iter(func.signature.parameters.values())) - if _is_less_important_param(param1, func.signature, ctx): + if _is_less_important_param(param1, ctx): nargs -= 1 if nargs == 0: # method with only unannotated self/cls parameter -> never long diff --git a/pydoctor/themes/base/apidocs.css b/pydoctor/themes/base/apidocs.css index 304cc6a6d..5620162ed 100644 --- a/pydoctor/themes/base/apidocs.css +++ b/pydoctor/themes/base/apidocs.css @@ -400,16 +400,26 @@ table .private { word-spacing: -5px; } -/* Force each parameter onto a new line */ -#childList a:target ~ .functionHeader .function-signature.expand-signature .sig-param { - display: block; - margin-left: 20px; -} - .sig-param .undocumented { + /* self or cls params */ font-size: 93%; } +/* When focuse, present each parameter onto a new line */ +#childList a:target ~ .functionHeader .function-signature.expand-signature .sig-param, +#childList a:target ~ .functionHeader .function-signature.expand-signature .sig-symbol { + display: block; + margin-left: 1.5em; + padding-left: 1.5em; + text-indent: -1.5em; +} +/* Except the 'self' or 'cls' params, which are rendered on the same line as the function def */ +#childList a:target ~ .functionHeader .function-signature.expand-signature .sig-param:has(.undocumented) { + display: initial; + margin-left: 0; + padding-left: 0; +} + /* - Links to class/function/etc names are nested like this: label diff --git a/pydoctor/themes/readthedocs/readthedocstheme.css b/pydoctor/themes/readthedocs/readthedocstheme.css index b12f3013f..031184940 100644 --- a/pydoctor/themes/readthedocs/readthedocstheme.css +++ b/pydoctor/themes/readthedocs/readthedocstheme.css @@ -86,6 +86,10 @@ code, .literal { padding:1px 2px; } +.function-signature .sig-param { + line-height: 1.3; +} + /* special cases where pydoctor's default theme renders ok with the background, but doesn't render nicely with this theme. */ .class-signature > code, h1 > code, pre code{ @@ -730,9 +734,9 @@ input[type="search"] { background-color: unset; border-left: 3px solid rgb(80, 80, 90); } + #childList a:target ~ .functionBody{ - background-color: transparent; - box-shadow: none; + box-shadow: 7px 1px 0px 3px rgb(253 255 223), -36px 1px 0px 3px rgb(253 255 223); } @font-face { From 188c410671be7cbe923eaa969f84402450f0644c Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 25 Oct 2024 16:43:01 -0400 Subject: [PATCH 04/38] Add comment --- pydoctor/templatewriter/pages/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 8442e79ac..67b958ba6 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -112,6 +112,10 @@ def format_overloads(func: model.Function) -> Iterator["Flattenable"]: """ Format a function overloads definitions as nice HTML signatures. """ + # TODO: Find a manner to wrap long overloads like the ones from temporalio.client.Client.start_workflow + # Maybe when there are more than one long overload, we create a fake overload without any annotations + # expect the one that are the same accros all overloads, then this could be showed when clicking on the function name then all overloads + # could be showed on demand for overload in func.overloads: yield from format_decorators(overload) yield tags.div(format_function_def(func.name, func.is_async, overload)) @@ -130,13 +134,14 @@ def format_function_def(func_name: str, is_async: bool, def_stmt = 'async def' if is_async else 'def' if func_name.endswith('.setter') or func_name.endswith('.deleter'): func_name = func_name[:func_name.rindex('.')] - func_class = 'function-signature' + + func_signature_css_class = 'function-signature' if epydoc2stan.is_long_function_def(func): - func_class += ' expand-signature' + func_signature_css_class += ' expand-signature' r.extend([ tags.span(def_stmt, class_='py-keyword'), ' ', tags.span(func_name, class_='py-defname'), - tags.span(format_signature(func), class_=func_class), ':', + tags.span(format_signature(func), class_=func_signature_css_class), ':', ]) return r From 83d47f71193a976513efad0c097d7d3837e3487d Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 25 Oct 2024 16:46:19 -0400 Subject: [PATCH 05/38] Fix usage of cache --- pydoctor/epydoc2stan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index dca23dcf4..b0ad2f03c 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -12,7 +12,7 @@ ) import ast import re -from functools import cache +from functools import lru_cache import attr from docutils import nodes @@ -1175,7 +1175,7 @@ def get_constructors_extra(cls:model.Class) -> ParsedDocstring | None: set_node_attributes(document, children=elements) return ParsedRstDocstring(document, ()) -@cache +@lru_cache(maxsize=None) def parsed_text(text: str) -> ParsedDocstring: """ Enacpsulate some raw text with no markup inside a L{ParsedDocstring}. @@ -1329,7 +1329,7 @@ def _colorize_signature(sig: inspect.Signature, ctx: model.Documentable) -> Pars return ParsedDocstring.combine(result) -@cache +@lru_cache(maxsize=None) def get_parsed_signature(func: Union[model.Function, model.FunctionOverload]) -> ParsedDocstring | None: signature = func.signature if signature is None: From ab1cdd1aaf351a88fe5c9307c489f4ef88329b3a Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 25 Oct 2024 16:53:45 -0400 Subject: [PATCH 06/38] Fix usages of cache --- pydoctor/epydoc/markup/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index bf93146dc..4a1c21f1e 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -33,7 +33,7 @@ from __future__ import annotations __docformat__ = 'epytext en' -from functools import cache +from functools import lru_cache from itertools import chain from typing import Callable, ContextManager, List, Optional, Sequence, Iterator, TYPE_CHECKING import abc @@ -269,7 +269,7 @@ def __init__(self, elements: Sequence[ParsedDocstring]): def has_body(self) -> bool: return any(e.has_body for e in self._elements) - @cache + @lru_cache(maxsize=None) def to_node(self) -> nodes.document: doc = new_document('composite') for e in self._elements: @@ -281,7 +281,7 @@ def to_node(self) -> nodes.document: doc.children.extend(subdoc.children) return doc - @cache + @lru_cache(maxsize=None) def to_stan(self, linker: DocstringLinker) -> Tag: stan = tags.transparent() for e in self._elements: From eca5cede6c97d7a2a857274669f37e6cf527e331 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 25 Oct 2024 18:04:48 -0400 Subject: [PATCH 07/38] Simplify with_linker() and with_tag(). These do not create new parsed docstrings they only update the local to_stan() method dynamically. --- pydoctor/epydoc/markup/__init__.py | 60 +++++++++--------------------- 1 file changed, 17 insertions(+), 43 deletions(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index 4a1c21f1e..1d2a4f207 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -211,7 +211,12 @@ def with_linker(self, linker: DocstringLinker) -> ParsedDocstring: Pre-set the linker object for this parsed docstring. Whatever is passed to L{to_stan()} will be ignored. """ - return _EnforcedLinkerParsedDocstring(self, linker=linker) + to_stan = self.to_stan + use_linker = linker + def new_to_stan(_: DocstringLinker) -> Tag: + return to_stan(use_linker) + self.to_stan = new_to_stan + return self def with_tag(self, tag: Tag) -> ParsedDocstring: """ @@ -221,11 +226,17 @@ def with_tag(self, tag: Tag) -> ParsedDocstring: With this trick, the main tag is preserved. It can also be used to add a custom CSS class on top of an existing parsed docstring. """ - # We double wrap it with a transparent tag so the added tags survives ParsedDocstring.combine - # wich combines the content of the main div of the stan, not the div itself. - return _WrappedInTagParsedDocstring( - _WrappedInTagParsedDocstring(self, tag=tag), - tag=tags.transparent) + to_stan = self.to_stan + # cloning the tag is really important otherwise since the stan results are cached + # we might end up adding the same to_stan result to the same tag instance which generate bogus output. + tag = tag.clone() + def new_to_stan(linker: DocstringLinker) -> Tag: + # We double wrap it with a transparent tag so the added tags survives ParsedDocstring.combine + # wich combines the content of the main div of the stan, not the div itself. + return tags.transparent(tag(to_stan(linker))) + self.to_stan = new_to_stan + return self + @classmethod def combine(cls, elements: Sequence[ParsedDocstring]) -> ParsedDocstring: @@ -288,44 +299,7 @@ def to_stan(self, linker: DocstringLinker) -> Tag: stan(e.to_stan(linker).children) return stan -class _EnforcedLinkerParsedDocstring(ParsedDocstring): - """ - Wraps an existing parsed docstring to be rendered with the - given linker instead of whatever is passed to L{to_stan()}. - """ - def __init__(self, other: ParsedDocstring, *, linker: DocstringLinker): - super().__init__(other.fields) - self.linker = linker - self._parsed = other - - @property - def has_body(self) -> bool: - return self._parsed.has_body - - def to_stan(self, _: object) -> Tag: - return self._parsed.to_stan(self.linker) - - def to_node(self) -> nodes.document: - return self._parsed.to_node() - -class _WrappedInTagParsedDocstring(ParsedDocstring): - def __init__(self, other: ParsedDocstring, *, tag: Tag) -> None: - super().__init__(other.fields) - self._parsed = other - self._tag = tag - - @property - def has_body(self): - return self._parsed.has_body - - def to_stan(self, linker: DocstringLinker) -> Tag: - return self._tag( - self._parsed.to_stan(linker)) - - def to_node(self) -> nodes.document: - return self._parsed.to_node() - ################################################## ## Fields ################################################## From ff4269f1b96499ec3ea04a401e151b6aaca21510 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 25 Oct 2024 18:33:24 -0400 Subject: [PATCH 08/38] Revert "Simplify with_linker() and with_tag(). These do not create new parsed docstrings they only update the local to_stan() method dynamically." This reverts commit eca5cede6c97d7a2a857274669f37e6cf527e331. --- pydoctor/epydoc/markup/__init__.py | 60 +++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index 1d2a4f207..4a1c21f1e 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -211,12 +211,7 @@ def with_linker(self, linker: DocstringLinker) -> ParsedDocstring: Pre-set the linker object for this parsed docstring. Whatever is passed to L{to_stan()} will be ignored. """ - to_stan = self.to_stan - use_linker = linker - def new_to_stan(_: DocstringLinker) -> Tag: - return to_stan(use_linker) - self.to_stan = new_to_stan - return self + return _EnforcedLinkerParsedDocstring(self, linker=linker) def with_tag(self, tag: Tag) -> ParsedDocstring: """ @@ -226,17 +221,11 @@ def with_tag(self, tag: Tag) -> ParsedDocstring: With this trick, the main tag is preserved. It can also be used to add a custom CSS class on top of an existing parsed docstring. """ - to_stan = self.to_stan - # cloning the tag is really important otherwise since the stan results are cached - # we might end up adding the same to_stan result to the same tag instance which generate bogus output. - tag = tag.clone() - def new_to_stan(linker: DocstringLinker) -> Tag: - # We double wrap it with a transparent tag so the added tags survives ParsedDocstring.combine - # wich combines the content of the main div of the stan, not the div itself. - return tags.transparent(tag(to_stan(linker))) - self.to_stan = new_to_stan - return self - + # We double wrap it with a transparent tag so the added tags survives ParsedDocstring.combine + # wich combines the content of the main div of the stan, not the div itself. + return _WrappedInTagParsedDocstring( + _WrappedInTagParsedDocstring(self, tag=tag), + tag=tags.transparent) @classmethod def combine(cls, elements: Sequence[ParsedDocstring]) -> ParsedDocstring: @@ -299,7 +288,44 @@ def to_stan(self, linker: DocstringLinker) -> Tag: stan(e.to_stan(linker).children) return stan +class _EnforcedLinkerParsedDocstring(ParsedDocstring): + """ + Wraps an existing parsed docstring to be rendered with the + given linker instead of whatever is passed to L{to_stan()}. + """ + def __init__(self, other: ParsedDocstring, *, linker: DocstringLinker): + super().__init__(other.fields) + self.linker = linker + self._parsed = other + + @property + def has_body(self) -> bool: + return self._parsed.has_body + + def to_stan(self, _: object) -> Tag: + return self._parsed.to_stan(self.linker) + + def to_node(self) -> nodes.document: + return self._parsed.to_node() + +class _WrappedInTagParsedDocstring(ParsedDocstring): + def __init__(self, other: ParsedDocstring, *, tag: Tag) -> None: + super().__init__(other.fields) + self._parsed = other + self._tag = tag + + @property + def has_body(self): + return self._parsed.has_body + + def to_stan(self, linker: DocstringLinker) -> Tag: + return self._tag( + self._parsed.to_stan(linker)) + + def to_node(self) -> nodes.document: + return self._parsed.to_node() + ################################################## ## Fields ################################################## From 914e01c59b7c7e902c55a3fd6aee2eb23e2f9a2b Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 25 Oct 2024 18:56:07 -0400 Subject: [PATCH 09/38] Minor changes not to use lru_cache too much --- pydoctor/epydoc/markup/__init__.py | 19 ++++++++++--------- pydoctor/epydoc2stan.py | 21 ++++++++++----------- pydoctor/model.py | 13 ++++++++----- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index 4a1c21f1e..b2f2285e0 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -33,9 +33,8 @@ from __future__ import annotations __docformat__ = 'epytext en' -from functools import lru_cache from itertools import chain -from typing import Callable, ContextManager, List, Optional, Sequence, Iterator, TYPE_CHECKING +from typing import Callable, ContextManager, Iterable, List, Optional, Sequence, Iterator, TYPE_CHECKING import abc import sys import re @@ -264,16 +263,17 @@ class _CombinedParsedDocstring(ParsedDocstring): def __init__(self, elements: Sequence[ParsedDocstring]): super().__init__(tuple(chain.from_iterable(e.fields for e in elements))) self._elements = elements + self._doc = self._document(elements) @property def has_body(self) -> bool: return any(e.has_body for e in self._elements) - @lru_cache(maxsize=None) - def to_node(self) -> nodes.document: + @classmethod + def _document(cls, elements: Iterable[ParsedDocstring]) -> nodes.document: doc = new_document('composite') - for e in self._elements: - # TODO: Some parsed doctrings simply do not implement to_node() at this time. + for e in elements: + # TODO: Some parsed doctrings simply do not implement to_node(). # It should be really time to fix this... subdoc = e.to_node() # TODO: here all childrens might not have the same document property. @@ -281,7 +281,9 @@ def to_node(self) -> nodes.document: doc.children.extend(subdoc.children) return doc - @lru_cache(maxsize=None) + def to_node(self) -> nodes.document: + return self._doc + def to_stan(self, linker: DocstringLinker) -> Tag: stan = tags.transparent() for e in self._elements: @@ -320,8 +322,7 @@ def has_body(self): return self._parsed.has_body def to_stan(self, linker: DocstringLinker) -> Tag: - return self._tag( - self._parsed.to_stan(linker)) + return self._tag(self._parsed.to_stan(linker)) def to_node(self) -> nodes.document: return self._parsed.to_node() diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index b0ad2f03c..c790b8932 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -1175,7 +1175,7 @@ def get_constructors_extra(cls:model.Class) -> ParsedDocstring | None: set_node_attributes(document, children=elements) return ParsedRstDocstring(document, ()) -@lru_cache(maxsize=None) +@lru_cache() def parsed_text(text: str) -> ParsedDocstring: """ Enacpsulate some raw text with no markup inside a L{ParsedDocstring}. @@ -1188,8 +1188,8 @@ def parsed_text(text: str) -> ParsedDocstring: set_node_attributes(document, children=[txt_node]) return ParsedRstDocstring(document, ()) -# not using @cache here because it cause too munch trouble since the ParsedDocstring -# are actually not immutable. +# not using @cache here because we need new span tag for every call +# othewise it messes-up everything. def parsed_text_with_css(text:str, css_class: str) -> ParsedDocstring: parsed_doc = parsed_text(text) if not css_class: @@ -1329,18 +1329,17 @@ def _colorize_signature(sig: inspect.Signature, ctx: model.Documentable) -> Pars return ParsedDocstring.combine(result) -@lru_cache(maxsize=None) def get_parsed_signature(func: Union[model.Function, model.FunctionOverload]) -> ParsedDocstring | None: - signature = func.signature - if signature is None: - # TODO:When the value is None, it should probably not be cached - # just yet because one could have called this function too - # early in the process when the signature property is not set yet. - # Is this possible ? + if (psig:=func.parsed_signature) is not None: + return psig + + if (signature:=func.signature) is None: return None ctx = func.primary if isinstance(func, model.FunctionOverload) else func - return _colorize_signature(signature, ctx) + psig = _colorize_signature(signature, ctx) + func.parsed_signature = psig + return psig LONG_FUNCTION_DEF = 80 # this doesn't acount for the 'def ' and the ending ':' """ diff --git a/pydoctor/model.py b/pydoctor/model.py index e29388dff..f6171459e 100644 --- a/pydoctor/model.py +++ b/pydoctor/model.py @@ -857,10 +857,12 @@ def isNameDefined(self, name: str) -> bool: class Function(Inheritable): kind = DocumentableKind.FUNCTION is_async: bool - annotations: Mapping[str, Optional[ast.expr]] - decorators: Optional[Sequence[ast.expr]] - signature: Optional[Signature] - overloads: List['FunctionOverload'] + annotations: Mapping[str, ast.expr | None] + decorators: Sequence[ast.expr] | None + signature: Signature | None + overloads: List[FunctionOverload] + + parsed_signature: ParsedDocstring | None = None # set in get_parsed_signature() def setup(self) -> None: super().setup() @@ -869,7 +871,7 @@ def setup(self) -> None: self.signature = None self.overloads = [] -@attr.s(auto_attribs=True, frozen=True) +@attr.s(auto_attribs=True) class FunctionOverload: """ @note: This is not an actual documentable type. @@ -877,6 +879,7 @@ class FunctionOverload: primary: Function signature: Signature decorators: Sequence[ast.expr] = attr.ib(converter=tuple) + parsed_signature: ParsedDocstring | None = None # set in get_parsed_signature() class Attribute(Inheritable): kind: Optional[DocumentableKind] = DocumentableKind.ATTRIBUTE From cdef9654336eb845da15618a8b5ba6ac11ce5593 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 25 Oct 2024 19:14:56 -0400 Subject: [PATCH 10/38] Try to optimize what I can --- pydoctor/epydoc2stan.py | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index c790b8932..4211e09d8 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -1106,7 +1106,7 @@ def format_constructor_short_text(constructor: model.Function, forclass: model.C # Special casing __new__ because it's actually a static method if index==0 and (constructor.name in ('__new__', '__init__') or - constructor.kind is model.DocumentableKind.CLASS_METHOD): + constructor.kind is _CLASS_METHOD): # Omit first argument (self/cls) from simplified signature. continue star = '' @@ -1197,6 +1197,11 @@ def parsed_text_with_css(text:str, css_class: str) -> ParsedDocstring: return parsed_doc.with_tag(tags.span(class_=css_class)) _empty = inspect.Parameter.empty +_POSITIONAL_ONLY = inspect.Parameter.POSITIONAL_ONLY +_POSITIONAL_OR_KEYWORD = inspect.Parameter.POSITIONAL_OR_KEYWORD +_VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD +_VAR_POSITIONAL = inspect.Parameter.VAR_POSITIONAL +_KEYWORD_ONLY = inspect.Parameter.KEYWORD_ONLY def _colorize_signature_annotation(annotation: object, ctx: model.Documentable) -> ParsedDocstring: @@ -1210,6 +1215,8 @@ def _colorize_signature_annotation(annotation: object, # Make sure the generated tags are not stripped by ParsedDocstring.combine. ).with_tag(tags.transparent) +_METHOD = model.DocumentableKind.METHOD +_CLASS_METHOD = model.DocumentableKind.CLASS_METHOD def _is_less_important_param(param: inspect.Parameter, ctx: model.Documentable) -> bool: """ Whether this parameter is the 'self' param of methods or 'cls' param of class methods. @@ -1217,10 +1224,10 @@ def _is_less_important_param(param: inspect.Parameter, ctx: model.Documentable) @Note: this does not check whether the parameter is the first of the signature. This should be done before calling this function! """ - if param.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY): + if param.kind not in (_POSITIONAL_OR_KEYWORD, _POSITIONAL_ONLY): return False - if (param.name == 'self' and ctx.kind is model.DocumentableKind.METHOD) or ( - param.name == 'cls' and ctx.kind is model.DocumentableKind.CLASS_METHOD): + if (param.name == 'self' and ctx.kind is _METHOD) or ( + param.name == 'cls' and ctx.kind is _CLASS_METHOD): return param.annotation is _empty and param.default is _empty return False @@ -1238,9 +1245,9 @@ def _colorize_signature_param(param: inspect.Parameter, """ kind = param.kind result: list[ParsedDocstring] = [] - if kind == inspect.Parameter.VAR_POSITIONAL: + if kind == _VAR_POSITIONAL: result.append(parsed_text(f'*{param.name}')) - elif kind == inspect.Parameter.VAR_KEYWORD: + elif kind == _VAR_KEYWORD: result.append(parsed_text(f'**{param.name}')) else: if is_first and _is_less_important_param(param, ctx): @@ -1285,7 +1292,7 @@ def _colorize_signature(sig: inspect.Signature, ctx: model.Documentable) -> Pars kind = param.kind has_next = (i+1 < param_number) - if kind == inspect.Parameter.POSITIONAL_ONLY: + if kind == _POSITIONAL_ONLY: render_pos_only_separator = True elif render_pos_only_separator: # It's not a positional-only parameter, and the flag @@ -1296,11 +1303,11 @@ def _colorize_signature(sig: inspect.Signature, ctx: model.Documentable) -> Pars result.append(parsed_text_with_css('/', css_class='sig-symbol')) render_pos_only_separator = False - if kind == inspect.Parameter.VAR_POSITIONAL: + if kind == _VAR_POSITIONAL: # OK, we have an '*args'-like parameter, so we won't need # a '*' to separate keyword-only arguments render_kw_only_separator = False - elif kind == inspect.Parameter.KEYWORD_ONLY and render_kw_only_separator: + elif kind == _KEYWORD_ONLY and render_kw_only_separator: # We have a keyword-only parameter to render and we haven't # rendered an '*args'-like parameter before, so add a '*' # separator to the parameters list ("foo(arg1, *, arg2)" case) @@ -1358,26 +1365,23 @@ def is_long_function_def(func: model.Function | model.FunctionOverload) -> bool: @see: L{LONG_FUNCTION_DEF} """ - if func.signature is None: + if (sig:=func.signature) is None or ( + psig:=get_parsed_signature(func)) is None: return False - nargs = len(func.signature.parameters) + + nargs = len(sig.parameters) if nargs == 0: # no arguments at all -> never long return False ctx = func.primary if isinstance(func, model.FunctionOverload) else func - param1 = next(iter(func.signature.parameters.values())) + param1 = next(iter(sig.parameters.values())) if _is_less_important_param(param1, ctx): nargs -= 1 if nargs == 0: # method with only unannotated self/cls parameter -> never long return False - sig = get_parsed_signature(func) - if sig is None: - # this should never happen since we checked if func.signature is None. - return False - name_len = len(ctx.name) - signature_len = len(''.join(node2stan.gettext(sig.to_node()))) + signature_len = len(''.join(node2stan.gettext(psig.to_node()))) return LONG_FUNCTION_DEF - (name_len + signature_len) < 0 \ No newline at end of file From 2033d65a6cf12f71044e70b7764c2f33292bafe6 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sat, 26 Oct 2024 00:32:30 -0400 Subject: [PATCH 11/38] Fix mypy --- pydoctor/astbuilder.py | 2 +- pydoctor/epydoc/markup/__init__.py | 2 +- pydoctor/model.py | 2 +- pydoctor/test/test_astbuilder.py | 1 + pydoctor/test/test_epydoc2stan.py | 12 ++++++------ 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py index 2520a4f1f..e6cadb5b1 100644 --- a/pydoctor/astbuilder.py +++ b/pydoctor/astbuilder.py @@ -20,7 +20,7 @@ get_docstring_node, get_assign_docstring_node, unparse, NodeVisitor, Parentage, Str) class InvalidSignatureParamName(str): - def isidentifier(self): + def isidentifier(self) -> bool: return True def parseFile(path: Path) -> ast.Module: diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index b2f2285e0..10c0c39b7 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -318,7 +318,7 @@ def __init__(self, other: ParsedDocstring, *, tag: Tag) -> None: self._tag = tag @property - def has_body(self): + def has_body(self) -> bool: return self._parsed.has_body def to_stan(self, linker: DocstringLinker) -> Tag: diff --git a/pydoctor/model.py b/pydoctor/model.py index f6171459e..3cabc8096 100644 --- a/pydoctor/model.py +++ b/pydoctor/model.py @@ -878,7 +878,7 @@ class FunctionOverload: """ primary: Function signature: Signature - decorators: Sequence[ast.expr] = attr.ib(converter=tuple) + decorators: Sequence[ast.expr] parsed_signature: ParsedDocstring | None = None # set in get_parsed_signature() class Attribute(Inheritable): diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py index db3ffe98b..d1619f296 100644 --- a/pydoctor/test/test_astbuilder.py +++ b/pydoctor/test/test_astbuilder.py @@ -110,6 +110,7 @@ def to_html( def signature2str(func: model.Function | model.FunctionOverload) -> str: doc = get_parsed_signature(func) + assert doc fromhtml = flatten_text(format_signature(func)) fromdocutils = ''.join(node2stan.gettext(doc.to_node())) assert fromhtml == fromdocutils diff --git a/pydoctor/test/test_epydoc2stan.py b/pydoctor/test/test_epydoc2stan.py index fe2b48924..c9b51857c 100644 --- a/pydoctor/test/test_epydoc2stan.py +++ b/pydoctor/test/test_epydoc2stan.py @@ -1977,9 +1977,9 @@ def f(self, x:typ) -> typ: assert isinstance(f, model.Function) assert f.signature assert "href" in flatten(epydoc2stan._colorize_signature_annotation( - f.signature.parameters['x'].annotation, f).to_stan(None)) + f.signature.parameters['x'].annotation, f).to_stan(NotFoundLinker())) assert "href" in flatten(epydoc2stan._colorize_signature_annotation( - f.signature.return_annotation, f).to_stan(None)) + f.signature.return_annotation, f).to_stan(NotFoundLinker())) assert isinstance(var, model.Attribute) assert "href" in flatten(epydoc2stan.type2stan(var) or '') @@ -2008,12 +2008,12 @@ def f(self, x:typ) -> typ: assert isinstance(f, model.Function) assert f.signature assert 'href="index.html#typ"' in flatten(epydoc2stan._colorize_signature_annotation( - f.signature.parameters['x'].annotation, f).to_stan(None)) - # the linker can be None here because the annotations uses with_linker() + f.signature.parameters['x'].annotation, f).to_stan(NotFoundLinker())) + # the linker can be NotFoundLinker() here because the annotations uses with_linker() assert 'href="index.html#typ"' in flatten(epydoc2stan._colorize_signature_annotation( - f.signature.return_annotation, f).to_stan(None)) - # the linker can be None here because the annotations uses with_linker() + f.signature.return_annotation, f).to_stan(NotFoundLinker())) + # the linker can be NotFoundLinker() here because the annotations uses with_linker() assert isinstance(var, model.Attribute) assert 'href="index.html#typ"' in flatten(epydoc2stan.type2stan(var) or '') From 282250be75212c2d68da1b4150ebdf559ae9595c Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sat, 26 Oct 2024 10:18:35 -0400 Subject: [PATCH 12/38] Remove unused imports --- pydoctor/astbuilder.py | 3 +-- pydoctor/templatewriter/pages/__init__.py | 1 - pydoctor/test/test_astbuilder.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py index e6cadb5b1..f11d344d5 100644 --- a/pydoctor/astbuilder.py +++ b/pydoctor/astbuilder.py @@ -13,8 +13,7 @@ Type, TypeVar, Union, Set, cast ) -from pydoctor import epydoc2stan, model, node2stan, extensions, linker -from pydoctor.epydoc.markup._pyval_repr import colorize_inline_pyval +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) diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 67b958ba6..7d5c68745 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -13,7 +13,6 @@ from twisted.web.template import Element, Tag, renderer, tags from pydoctor.extensions import zopeinterface -from pydoctor.stanutils import html2stan from pydoctor import epydoc2stan, model, linker, __version__, node2stan from pydoctor.astbuilder import node2fullname from pydoctor.templatewriter import util, TemplateLookup, TemplateElement diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py index d1619f296..7895a51e1 100644 --- a/pydoctor/test/test_astbuilder.py +++ b/pydoctor/test/test_astbuilder.py @@ -8,7 +8,7 @@ from pydoctor import epydoc2stan from pydoctor.epydoc.markup import DocstringLinker, ParsedDocstring from pydoctor.options import Options -from pydoctor.stanutils import flatten, html2stan, flatten_text +from pydoctor.stanutils import flatten, flatten_text from pydoctor.epydoc.markup.epytext import Element, ParsedEpytextDocstring from pydoctor.epydoc2stan import _get_docformat, format_summary, get_parsed_signature, get_parsed_type from pydoctor.templatewriter.pages import format_signature From 6a4de9f99a66c5fa3cd5813f67bfc233f7141f76 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Tue, 29 Oct 2024 15:12:04 -0400 Subject: [PATCH 13/38] Better implementation of with_linker and with_tag inside a single subclass. Introduce ParsedDocstring.to_text(). --- pydoctor/epydoc/markup/__init__.py | 75 +++++++++++++++--------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index 10c0c39b7..2a0966e0f 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -130,7 +130,7 @@ class ParsedDocstring(abc.ABC): markup parsers such as L{pydoctor.epydoc.markup.epytext.parse_docstring()} or L{pydoctor.epydoc.markup.restructuredtext.parse_docstring()}. - Subclasses must implement L{has_body()} and L{to_node()}. + Subclasses must at least implement L{has_body()} and L{to_node()}. A default implementation for L{to_stan()} method, relying on L{to_node()} is provided. But some subclasses override this behaviour. @@ -204,13 +204,22 @@ def to_node(self) -> nodes.document: This method might raise L{NotImplementedError} in such cases. (i.e. L{pydoctor.epydoc.markup._types.ParsedTypeDocstring}) """ raise NotImplementedError() + + def to_text(self) -> str: + """ + Translate this docstring to a string. + The default implementation depends on L{to_node}. + """ + return ''.join(node2stan.gettext(self.to_node())) def with_linker(self, linker: DocstringLinker) -> ParsedDocstring: """ Pre-set the linker object for this parsed docstring. Whatever is passed to L{to_stan()} will be ignored. """ - return _EnforcedLinkerParsedDocstring(self, linker=linker) + l = linker + return WrappedParsedDocstring(self, + to_stan=lambda this, _: this.to_stan(l)) def with_tag(self, tag: Tag) -> ParsedDocstring: """ @@ -222,16 +231,16 @@ def with_tag(self, tag: Tag) -> ParsedDocstring: """ # We double wrap it with a transparent tag so the added tags survives ParsedDocstring.combine # wich combines the content of the main div of the stan, not the div itself. - return _WrappedInTagParsedDocstring( - _WrappedInTagParsedDocstring(self, tag=tag), - tag=tags.transparent) + t = tag + return WrappedParsedDocstring(self, + lambda this, linker: tags.transparent(t(this.to_stan(linker)))) @classmethod def combine(cls, elements: Sequence[ParsedDocstring]) -> ParsedDocstring: """ Combine the contents of several parsed docstrings into one. """ - return _CombinedParsedDocstring(elements) + return _ParsedDocstringTree(elements) def get_summary(self) -> 'ParsedDocstring': """ @@ -255,22 +264,22 @@ def get_summary(self) -> 'ParsedDocstring': return self._summary -class _CombinedParsedDocstring(ParsedDocstring): +class _ParsedDocstringTree(ParsedDocstring): """ - Wraps several parsed docstrings into a single one. + Several parsed docstrings into a single one. """ def __init__(self, elements: Sequence[ParsedDocstring]): super().__init__(tuple(chain.from_iterable(e.fields for e in elements))) self._elements = elements - self._doc = self._document(elements) + self._doc: nodes.document | None = None @property def has_body(self) -> bool: return any(e.has_body for e in self._elements) @classmethod - def _document(cls, elements: Iterable[ParsedDocstring]) -> nodes.document: + def _generate_document(cls, elements: Iterable[ParsedDocstring]) -> nodes.document: doc = new_document('composite') for e in elements: # TODO: Some parsed doctrings simply do not implement to_node(). @@ -282,6 +291,8 @@ def _document(cls, elements: Iterable[ParsedDocstring]) -> nodes.document: return doc def to_node(self) -> nodes.document: + if not self._doc: + self._doc = self._generate_document(self._elements) return self._doc def to_stan(self, linker: DocstringLinker) -> Tag: @@ -290,42 +301,32 @@ def to_stan(self, linker: DocstringLinker) -> Tag: stan(e.to_stan(linker).children) return stan -class _EnforcedLinkerParsedDocstring(ParsedDocstring): +class WrappedParsedDocstring(ParsedDocstring): """ - Wraps an existing parsed docstring to be rendered with the - given linker instead of whatever is passed to L{to_stan()}. + Wraps a parsed docstring to suppplement the to_stan() method. """ - def __init__(self, other: ParsedDocstring, *, linker: DocstringLinker): + def __init__(self, + other: ParsedDocstring, + to_stan: Callable[[ParsedDocstring, DocstringLinker], Tag]): super().__init__(other.fields) - self.linker = linker - self._parsed = other - - @property - def has_body(self) -> bool: - return self._parsed.has_body + self.wrapped = other + """ + The wrapped parsed docstring. + """ + self._to_stan = to_stan - def to_stan(self, _: object) -> Tag: - return self._parsed.to_stan(self.linker) + def to_stan(self, docstring_linker): + return self._to_stan(self.wrapped, docstring_linker) - def to_node(self) -> nodes.document: - return self._parsed.to_node() + # Boring - -class _WrappedInTagParsedDocstring(ParsedDocstring): - def __init__(self, other: ParsedDocstring, *, tag: Tag) -> None: - super().__init__(other.fields) - self._parsed = other - self._tag = tag + def to_node(self) -> nodes.document: + return self.wrapped.to_node() @property def has_body(self) -> bool: - return self._parsed.has_body - - def to_stan(self, linker: DocstringLinker) -> Tag: - return self._tag(self._parsed.to_stan(linker)) - - def to_node(self) -> nodes.document: - return self._parsed.to_node() + return self.wrapped.has_body + ################################################## ## Fields From c0f93dc5fcf8781d92a6bd5cdc97ec65dc9249e6 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Tue, 29 Oct 2024 15:17:24 -0400 Subject: [PATCH 14/38] First attempt to implement relatively smart Expand/Collapse signatures when overloads are overwhelming... This probably requires some more tweaks but it's still better than showing everything at once. --- pydoctor/epydoc2stan.py | 36 +++---- pydoctor/templatewriter/pages/__init__.py | 98 ++++++++++++++++--- .../templatewriter/pages/attributechild.py | 2 +- .../templatewriter/pages/functionchild.py | 2 +- pydoctor/test/test_templatewriter.py | 10 +- pydoctor/themes/base/apidocs.css | 41 ++++++++ 6 files changed, 149 insertions(+), 40 deletions(-) diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index 4211e09d8..9b111d1f6 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -1348,40 +1348,32 @@ def get_parsed_signature(func: Union[model.Function, model.FunctionOverload]) -> func.parsed_signature = psig return psig -LONG_FUNCTION_DEF = 80 # this doesn't acount for the 'def ' and the ending ':' -""" -Maximum size of a function definition to be rendered on a single line. -The multiline formatting is only applied at the CSS level to stay customizable. -We add a css class to the signature HTML to signify the signature could possibly -be better formatted on several lines. -""" - -def is_long_function_def(func: model.Function | model.FunctionOverload) -> bool: +def function_signature_len(func: model.Function | model.FunctionOverload) -> int: """ - Whether this function definition is considered as long. The lenght of the a function def is defnied by the lenght of it's name plus the lenght of it's signature. On top of that, a function or method that takes no argument (expect unannotated 'self' for methods, and 'cls' for classmethods) - is never considered as long. - - @see: L{LONG_FUNCTION_DEF} + will always have a lenght equals to the function name len plus two for 'function()'. """ + ctx = func.primary if isinstance(func, model.FunctionOverload) else func + name_len = len(ctx.name) + if (sig:=func.signature) is None or ( psig:=get_parsed_signature(func)) is None: - return False - + return name_len + 2 # bogus function def + nargs = len(sig.parameters) if nargs == 0: - # no arguments at all -> never long - return False - ctx = func.primary if isinstance(func, model.FunctionOverload) else func + # no arguments at all + return name_len + 2 + param1 = next(iter(sig.parameters.values())) if _is_less_important_param(param1, ctx): nargs -= 1 if nargs == 0: - # method with only unannotated self/cls parameter -> never long - return False + # method with only unannotated self/cls parameter + return name_len + 2 name_len = len(ctx.name) - signature_len = len(''.join(node2stan.gettext(psig.to_node()))) - return LONG_FUNCTION_DEF - (name_len + signature_len) < 0 + signature_len = len(psig.to_text()) + return name_len + signature_len \ No newline at end of file diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 7d5c68745..49d4c3b97 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -27,7 +27,7 @@ from pydoctor.templatewriter.pages.functionchild import FunctionChild -def format_decorators(obj: Union[model.Function, model.Attribute, model.FunctionOverload]) -> Iterator["Flattenable"]: +def _format_decorators(obj: Union[model.Function, model.Attribute, model.FunctionOverload]) -> Iterator["Flattenable"]: # Since we use this function to colorize the FunctionOverload decorators and it's not an actual Documentable subclass, we use the overload's # primary function for parts that requires an interface to Documentable methods or attributes documentable_obj = obj if not isinstance(obj, model.FunctionOverload) else obj.primary @@ -49,7 +49,11 @@ def format_decorators(obj: Union[model.Function, model.Attribute, model.Function # Report eventual warnings. It warns when we can't colorize the expression for some reason. epydoc2stan.reportWarnings(documentable_obj, doc.warnings, section='colorize decorator') - yield '@', stan.children, tags.br() + + yield tags.span('@', stan.children, tags.br(), class_='function-decorator') + +def format_decorators(obj: Union[model.Function, model.Attribute, model.FunctionOverload]) -> Tag: + return tags.span(*_format_decorators(obj), class_='function-decorators') def format_signature(func: Union[model.Function, model.FunctionOverload]) -> "Flattenable": """ @@ -107,27 +111,96 @@ def format_class_signature(cls: model.Class) -> "Flattenable": r.append(')') return r +LONG_SIGNATURE = 80 # this doesn't acount for the 'def ' and the ending ':' +""" +Maximum size of a function definition to be rendered on a single line. +The multiline formatting is only applied at the CSS level to stay customizable. +We add a css class to the signature HTML to signify the signature could possibly +be better formatted on several lines. +""" + +PRETTY_LONG_SIGNATURE = LONG_SIGNATURE * 2 +""" +From that number of characters, a signature is considered pretty long. +""" + +VERY_LONG_SIGNATURE = PRETTY_LONG_SIGNATURE * 3 +""" +From that number of characters, a signature is considered very long. +""" + +def _are_overloads_overwhelming(func: model.Function) -> bool: + # a manner to wrap long overloads like the ones from temporalio.client.Client.start_workflow + # Maybe when there are more than one long overload, we create a fake overload without any annotations + # expect the one that are the same accros all overloads, then this could be showed when clicking on the function name then all overloads + # could be showed on demand + + # The goal here is to hide overwhelming informations and only display it on demand. + # The following code tries hard to determine if the overloads are overwhelming... + # First what is overwhelming overloads ? + # - If there are at least 2 very long signatures, it's overwhelming. + # - If there are at least 6 pretty long signatures, it's overwhelming. + # - If there are at least 10 long signatures, it's overwhelming. + # - If there are 16 or more signatures, it's overwhelming. + + if len(func.overloads) >= 16: + return True + + n_long, n_pretty_long, n_very_long = 0, 0, 0 + for o in func.overloads: + siglen = epydoc2stan.function_signature_len(o) + if siglen > LONG_SIGNATURE: + n_long += 1 + if siglen > PRETTY_LONG_SIGNATURE: + n_pretty_long += 1 + if siglen > VERY_LONG_SIGNATURE: + n_very_long += 1 + if n_very_long >= 3: + return True + elif n_pretty_long >= 6: + return True + elif n_long >= 10: + return True + + return False + +def _expand_overloads_link(ctx: model.Documentable) -> list[Tag]: + _id = f'{ctx.fullName()}-overload-expand-link' + return [ + tags.input(type='checkbox', id=_id, style="display: none;", class_="overload-expand-checkbox"), + tags.label(for_=_id, class_="overload-expand-link btn btn-link"), + ] + def format_overloads(func: model.Function) -> Iterator["Flattenable"]: """ Format a function overloads definitions as nice HTML signatures. """ - # TODO: Find a manner to wrap long overloads like the ones from temporalio.client.Client.start_workflow - # Maybe when there are more than one long overload, we create a fake overload without any annotations - # expect the one that are the same accros all overloads, then this could be showed when clicking on the function name then all overloads - # could be showed on demand + # When the overloads are overwhelming, we only show the first and the last overloads. + # the overloads in between are only showed with def x(...) and no decorators. + + are_overwhelming = _are_overloads_overwhelming(func) + overload_class = 'function-overload' + + if are_overwhelming: + yield from _expand_overloads_link(func) + overload_class += ' collapse-overload' + for overload in func.overloads: - yield from format_decorators(overload) - yield tags.div(format_function_def(func.name, func.is_async, overload)) + yield tags.div(format_decorators(overload), + tags.div(format_function_def(func.name, func.is_async, overload)), + class_=overload_class) def format_function_def(func_name: str, is_async: bool, func: Union[model.Function, model.FunctionOverload]) -> List["Flattenable"]: """ Format a function definition as nice HTML signature. - If the function is overloaded, it will return an empty list. We use L{format_overloads} for these. + If the function is overloaded, it will return an empty list. + We use L{format_overloads} for these. """ r:List["Flattenable"] = [] - # If this is a function with overloads, we do not render the principal signature because the overloaded signatures will be shown instead. + # If this is a function with overloads, we do not render the principal + # signature because the overloaded signatures will be shown instead. if isinstance(func, model.Function) and func.overloads: return r def_stmt = 'async def' if is_async else 'def' @@ -135,12 +208,13 @@ def format_function_def(func_name: str, is_async: bool, func_name = func_name[:func_name.rindex('.')] func_signature_css_class = 'function-signature' - if epydoc2stan.is_long_function_def(func): + if epydoc2stan.function_signature_len(func) > LONG_SIGNATURE: func_signature_css_class += ' expand-signature' r.extend([ tags.span(def_stmt, class_='py-keyword'), ' ', tags.span(func_name, class_='py-defname'), - tags.span(format_signature(func), class_=func_signature_css_class), ':', + tags.span(format_signature(func), ':', + class_=func_signature_css_class), ]) return r diff --git a/pydoctor/templatewriter/pages/attributechild.py b/pydoctor/templatewriter/pages/attributechild.py index e22e84fc2..470873854 100644 --- a/pydoctor/templatewriter/pages/attributechild.py +++ b/pydoctor/templatewriter/pages/attributechild.py @@ -51,7 +51,7 @@ def anchorHref(self, request: object, tag: Tag) -> str: @renderer def decorator(self, request: object, tag: Tag) -> "Flattenable": - return list(format_decorators(self.ob)) + return format_decorators(self.ob) @renderer def attribute(self, request: object, tag: Tag) -> "Flattenable": diff --git a/pydoctor/templatewriter/pages/functionchild.py b/pydoctor/templatewriter/pages/functionchild.py index 0ddbff371..2efe3ee0f 100644 --- a/pydoctor/templatewriter/pages/functionchild.py +++ b/pydoctor/templatewriter/pages/functionchild.py @@ -54,7 +54,7 @@ def overloads(self, request: object, tag: Tag) -> "Flattenable": @renderer def decorator(self, request: object, tag: Tag) -> "Flattenable": - return list(format_decorators(self.ob)) + return format_decorators(self.ob) @renderer def functionDef(self, request: object, tag: Tag) -> "Flattenable": diff --git a/pydoctor/test/test_templatewriter.py b/pydoctor/test/test_templatewriter.py index 2e899c411..03a066729 100644 --- a/pydoctor/test/test_templatewriter.py +++ b/pydoctor/test/test_templatewriter.py @@ -576,11 +576,13 @@ def test_format_decorators() -> None: def func(): ... ''') - stan = stanutils.flatten(list(pages.format_decorators(cast(model.Function, mod.contents['func'])))) - assert stan == ("""@string_decorator(set('""" + stan = stanutils.flatten(pages.format_decorators(cast(model.Function, mod.contents['func']))) + assert stan == ("""""" + """@string_decorator(set('""" r"""\\/:*?"<>|\f\v\t\r\n""" - """'))
@simple_decorator""" - """(max_examples=700, deadline=None, option=range(10))
""") + """'))
@simple_decorator""" + """(max_examples=700, deadline=None, option=range(10))
""" + """
""") def test_compact_module_summary() -> None: diff --git a/pydoctor/themes/base/apidocs.css b/pydoctor/themes/base/apidocs.css index 177572562..cd2356001 100644 --- a/pydoctor/themes/base/apidocs.css +++ b/pydoctor/themes/base/apidocs.css @@ -434,6 +434,47 @@ table .private { padding-left: 0; } +/* Style for the "Expand/Collapse signtures" link */ + +input[type=checkbox].overload-expand-checkbox:not(:checked) ~ label.overload-expand-link::after { + content: "Expand signatures"; +} + +input[type=checkbox].overload-expand-checkbox:checked ~ label.overload-expand-link::after { + content: "Collapse signatures"; +} + +input[type=checkbox].overload-expand-checkbox:not(:checked) ~ .collapse-overload .function-signature * +{ + display: inline-flex !important; + gap: 1px; + margin-left: 0 !important; + padding-left: 0 !important; + text-indent: 0 !important; + height: 1.2em; +} + +input[type=checkbox].overload-expand-checkbox:not(:checked) ~ .collapse-overload .function-signature{ + text-overflow: ellipsis; + word-wrap: break-word; + white-space: nowrap; + overflow: hidden; + display: inline-block; + width: -webkit-fill-available; + height: 1.3em; +} + +.overload-expand-link { + float: right; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +input[type=checkbox].overload-expand-checkbox:not(:checked) ~ .collapse-overload div:has(.function-signature) { + display: flex; + text-wrap-mode: nowrap; + gap: 8px; +} + /* - Links to class/function/etc names are nested like this: label From da89d7c1954798db5a6baee75fdab97f4faff8d6 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 14 Nov 2024 13:28:03 -0500 Subject: [PATCH 15/38] Simplify things: don't try to wrap overload signatures. Sphinx doesn't try to do such things either... --- pydoctor/templatewriter/pages/__init__.py | 74 +++-------------------- pydoctor/themes/base/apidocs.css | 46 +------------- 2 files changed, 10 insertions(+), 110 deletions(-) diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 49d4c3b97..3bc7d4412 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -37,6 +37,7 @@ def _format_decorators(obj: Union[model.Function, model.Attribute, model.Functio fn = node2fullname(dec.func, documentable_obj) # We don't want to show the deprecated decorator; # it shows up as an infobox. + # TODO: move this somewhere it can be customized. if fn in ("twisted.python.deprecate.deprecated", "twisted.python.deprecate.deprecatedProperty"): break @@ -50,10 +51,10 @@ def _format_decorators(obj: Union[model.Function, model.Attribute, model.Functio # Report eventual warnings. It warns when we can't colorize the expression for some reason. epydoc2stan.reportWarnings(documentable_obj, doc.warnings, section='colorize decorator') - yield tags.span('@', stan.children, tags.br(), class_='function-decorator') + yield tags.span('@', stan.children, tags.br()) def format_decorators(obj: Union[model.Function, model.Attribute, model.FunctionOverload]) -> Tag: - return tags.span(*_format_decorators(obj), class_='function-decorators') + return tags.span(*_format_decorators(obj), class_='func-decorators') def format_signature(func: Union[model.Function, model.FunctionOverload]) -> "Flattenable": """ @@ -111,84 +112,22 @@ def format_class_signature(cls: model.Class) -> "Flattenable": r.append(')') return r -LONG_SIGNATURE = 80 # this doesn't acount for the 'def ' and the ending ':' +LONG_SIGNATURE = 90 # this doesn't acount for the 'def ' and the ending ':' """ Maximum size of a function definition to be rendered on a single line. The multiline formatting is only applied at the CSS level to stay customizable. We add a css class to the signature HTML to signify the signature could possibly be better formatted on several lines. """ - -PRETTY_LONG_SIGNATURE = LONG_SIGNATURE * 2 -""" -From that number of characters, a signature is considered pretty long. -""" - -VERY_LONG_SIGNATURE = PRETTY_LONG_SIGNATURE * 3 -""" -From that number of characters, a signature is considered very long. -""" - -def _are_overloads_overwhelming(func: model.Function) -> bool: - # a manner to wrap long overloads like the ones from temporalio.client.Client.start_workflow - # Maybe when there are more than one long overload, we create a fake overload without any annotations - # expect the one that are the same accros all overloads, then this could be showed when clicking on the function name then all overloads - # could be showed on demand - - # The goal here is to hide overwhelming informations and only display it on demand. - # The following code tries hard to determine if the overloads are overwhelming... - # First what is overwhelming overloads ? - # - If there are at least 2 very long signatures, it's overwhelming. - # - If there are at least 6 pretty long signatures, it's overwhelming. - # - If there are at least 10 long signatures, it's overwhelming. - # - If there are 16 or more signatures, it's overwhelming. - - if len(func.overloads) >= 16: - return True - - n_long, n_pretty_long, n_very_long = 0, 0, 0 - for o in func.overloads: - siglen = epydoc2stan.function_signature_len(o) - if siglen > LONG_SIGNATURE: - n_long += 1 - if siglen > PRETTY_LONG_SIGNATURE: - n_pretty_long += 1 - if siglen > VERY_LONG_SIGNATURE: - n_very_long += 1 - if n_very_long >= 3: - return True - elif n_pretty_long >= 6: - return True - elif n_long >= 10: - return True - - return False - -def _expand_overloads_link(ctx: model.Documentable) -> list[Tag]: - _id = f'{ctx.fullName()}-overload-expand-link' - return [ - tags.input(type='checkbox', id=_id, style="display: none;", class_="overload-expand-checkbox"), - tags.label(for_=_id, class_="overload-expand-link btn btn-link"), - ] - def format_overloads(func: model.Function) -> Iterator["Flattenable"]: """ Format a function overloads definitions as nice HTML signatures. """ - # When the overloads are overwhelming, we only show the first and the last overloads. - # the overloads in between are only showed with def x(...) and no decorators. - - are_overwhelming = _are_overloads_overwhelming(func) - overload_class = 'function-overload' - - if are_overwhelming: - yield from _expand_overloads_link(func) - overload_class += ' collapse-overload' for overload in func.overloads: yield tags.div(format_decorators(overload), tags.div(format_function_def(func.name, func.is_async, overload)), - class_=overload_class) + class_='function-overload') def format_function_def(func_name: str, is_async: bool, func: Union[model.Function, model.FunctionOverload]) -> List["Flattenable"]: @@ -209,7 +148,8 @@ def format_function_def(func_name: str, is_async: bool, func_signature_css_class = 'function-signature' if epydoc2stan.function_signature_len(func) > LONG_SIGNATURE: - func_signature_css_class += ' expand-signature' + func_signature_css_class += ' long-signature' + r.extend([ tags.span(def_stmt, class_='py-keyword'), ' ', tags.span(func_name, class_='py-defname'), diff --git a/pydoctor/themes/base/apidocs.css b/pydoctor/themes/base/apidocs.css index cd2356001..8e1e07d20 100644 --- a/pydoctor/themes/base/apidocs.css +++ b/pydoctor/themes/base/apidocs.css @@ -420,60 +420,20 @@ table .private { } /* When focuse, present each parameter onto a new line */ -#childList a:target ~ .functionHeader .function-signature.expand-signature .sig-param, -#childList a:target ~ .functionHeader .function-signature.expand-signature .sig-symbol { +#childList a:target ~ .functionHeader .function-signature.long-signature .sig-param, +#childList a:target ~ .functionHeader .function-signature.long-signature .sig-symbol { display: block; margin-left: 1.5em; padding-left: 1.5em; text-indent: -1.5em; } /* Except the 'self' or 'cls' params, which are rendered on the same line as the function def */ -#childList a:target ~ .functionHeader .function-signature.expand-signature .sig-param:has(.undocumented) { +#childList a:target ~ .functionHeader .function-signature.long-signature .sig-param:has(.undocumented) { display: initial; margin-left: 0; padding-left: 0; } -/* Style for the "Expand/Collapse signtures" link */ - -input[type=checkbox].overload-expand-checkbox:not(:checked) ~ label.overload-expand-link::after { - content: "Expand signatures"; -} - -input[type=checkbox].overload-expand-checkbox:checked ~ label.overload-expand-link::after { - content: "Collapse signatures"; -} - -input[type=checkbox].overload-expand-checkbox:not(:checked) ~ .collapse-overload .function-signature * -{ - display: inline-flex !important; - gap: 1px; - margin-left: 0 !important; - padding-left: 0 !important; - text-indent: 0 !important; - height: 1.2em; -} - -input[type=checkbox].overload-expand-checkbox:not(:checked) ~ .collapse-overload .function-signature{ - text-overflow: ellipsis; - word-wrap: break-word; - white-space: nowrap; - overflow: hidden; - display: inline-block; - width: -webkit-fill-available; - height: 1.3em; -} - -.overload-expand-link { - float: right; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; -} - -input[type=checkbox].overload-expand-checkbox:not(:checked) ~ .collapse-overload div:has(.function-signature) { - display: flex; - text-wrap-mode: nowrap; - gap: 8px; -} /* - Links to class/function/etc names are nested like this: From 141b21167e0327c8890330836dba3eff7774f34e Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 14 Nov 2024 14:18:10 -0500 Subject: [PATCH 16/38] Get rid of the ParsedStanOnly by using parsed_text_with_css instead. Fix some cyclic imports issue as a drive-by change: model.Documentable was uncessarly runtime imported inside restructuredtext and epydoc parsers. --- pydoctor/epydoc/markup/__init__.py | 12 ++-- pydoctor/epydoc/markup/_napoleon.py | 8 ++- pydoctor/epydoc/markup/epytext.py | 6 +- pydoctor/epydoc/markup/google.py | 5 +- pydoctor/epydoc/markup/numpy.py | 5 +- pydoctor/epydoc/markup/plaintext.py | 9 ++- pydoctor/epydoc/markup/restructuredtext.py | 25 +++++++- pydoctor/epydoc2stan.py | 66 ++++------------------ 8 files changed, 62 insertions(+), 74 deletions(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index 2a0966e0f..e1c17537e 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -170,7 +170,6 @@ def get_toc(self, depth: int) -> Optional['ParsedDocstring']: docstring_toc = new_document('toc') if contents: docstring_toc.extend(contents) - from pydoctor.epydoc.markup.restructuredtext import ParsedRstDocstring return ParsedRstDocstring(docstring_toc, ()) else: return None @@ -248,8 +247,6 @@ def get_summary(self) -> 'ParsedDocstring': @note: The summary is cached. """ - # Avoid rare cyclic import error, see https://github.com/twisted/pydoctor/pull/538#discussion_r845668735 - from pydoctor import epydoc2stan if self._summary is not None: return self._summary try: @@ -257,10 +254,9 @@ def get_summary(self) -> 'ParsedDocstring': visitor = SummaryExtractor(_document) _document.walk(visitor) except Exception: - # TODO: These could be replaced by parsed_text().with_tag(...) - self._summary = epydoc2stan.ParsedStanOnly(tags.span(class_='undocumented')("Broken summary")) + self._summary = parsed_text_with_css('Broken summary', 'undocumented') else: - self._summary = visitor.summary or epydoc2stan.ParsedStanOnly(tags.span(class_='undocumented')("No summary")) + self._summary = visitor.summary or parsed_text_with_css('No summary', 'undocumented') return self._summary @@ -579,7 +575,6 @@ def visit_paragraph(self, node: nodes.paragraph) -> None: set_node_attributes(nodes.paragraph('', ''), document=summary_doc, lineno=1, children=summary_pieces)]) - from pydoctor.epydoc.markup.restructuredtext import ParsedRstDocstring self.summary = ParsedRstDocstring(summary_doc, fields=[]) def visit_field(self, node: nodes.Node) -> None: @@ -587,3 +582,6 @@ def visit_field(self, node: nodes.Node) -> None: def unknown_visit(self, node: nodes.Node) -> None: '''Ignore all unknown nodes''' + + +from pydoctor.epydoc.markup.restructuredtext import ParsedRstDocstring, parsed_text_with_css diff --git a/pydoctor/epydoc/markup/_napoleon.py b/pydoctor/epydoc/markup/_napoleon.py index e31dd2c92..7586ee8bf 100644 --- a/pydoctor/epydoc/markup/_napoleon.py +++ b/pydoctor/epydoc/markup/_napoleon.py @@ -4,12 +4,14 @@ """ from __future__ import annotations -from typing import List, Optional, Type +from typing import List, Optional, Type, TYPE_CHECKING from pydoctor.epydoc.markup import ParsedDocstring, ParseError, processtypes from pydoctor.epydoc.markup import restructuredtext from pydoctor.napoleon.docstring import GoogleDocstring, NumpyDocstring -from pydoctor.model import Attribute, Documentable + +if TYPE_CHECKING: + from pydoctor.model import Documentable class NapoelonDocstringParser: @@ -64,6 +66,8 @@ def _parse_docstring( errors: List[ParseError], docstring_cls: Type[GoogleDocstring], ) -> ParsedDocstring: + # TODO: would be best to avoid this import + from pydoctor.model import Attribute docstring_obj = docstring_cls( docstring, is_attribute=isinstance(self.obj, Attribute) diff --git a/pydoctor/epydoc/markup/epytext.py b/pydoctor/epydoc/markup/epytext.py index 5a35ab732..cc9ae51f4 100644 --- a/pydoctor/epydoc/markup/epytext.py +++ b/pydoctor/epydoc/markup/epytext.py @@ -132,7 +132,7 @@ # 4. helpers # 5. testing -from typing import Any, Iterable, List, Optional, Sequence, Set, Union, cast +from typing import Any, Iterable, List, Optional, Sequence, Set, Union, cast, TYPE_CHECKING import re import unicodedata @@ -141,7 +141,9 @@ from pydoctor.epydoc.markup import Field, ParseError, ParsedDocstring, ParserFunction from pydoctor.epydoc.docutils import set_node_attributes, new_document -from pydoctor.model import Documentable + +if TYPE_CHECKING: + from pydoctor.model import Documentable ################################################## ## Helper functions diff --git a/pydoctor/epydoc/markup/google.py b/pydoctor/epydoc/markup/google.py index ea2203407..490c58d2d 100644 --- a/pydoctor/epydoc/markup/google.py +++ b/pydoctor/epydoc/markup/google.py @@ -6,11 +6,12 @@ """ from __future__ import annotations -from typing import Optional +from typing import Optional, TYPE_CHECKING from pydoctor.epydoc.markup import ParserFunction from pydoctor.epydoc.markup._napoleon import NapoelonDocstringParser -from pydoctor.model import Documentable +if TYPE_CHECKING: + from pydoctor.model import Documentable def get_parser(obj: Optional[Documentable]) -> ParserFunction: diff --git a/pydoctor/epydoc/markup/numpy.py b/pydoctor/epydoc/markup/numpy.py index f44756bc9..e2e6f87e3 100644 --- a/pydoctor/epydoc/markup/numpy.py +++ b/pydoctor/epydoc/markup/numpy.py @@ -6,11 +6,12 @@ """ from __future__ import annotations -from typing import Optional +from typing import Optional, TYPE_CHECKING from pydoctor.epydoc.markup import ParserFunction from pydoctor.epydoc.markup._napoleon import NapoelonDocstringParser -from pydoctor.model import Documentable +if TYPE_CHECKING: + from pydoctor.model import Documentable def get_parser(obj: Optional[Documentable]) -> ParserFunction: diff --git a/pydoctor/epydoc/markup/plaintext.py b/pydoctor/epydoc/markup/plaintext.py index 1c7b1fd71..027d042b3 100644 --- a/pydoctor/epydoc/markup/plaintext.py +++ b/pydoctor/epydoc/markup/plaintext.py @@ -9,17 +9,20 @@ verbatim output, preserving all whitespace. """ from __future__ import annotations + __docformat__ = 'epytext en' -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from docutils import nodes from twisted.web.template import Tag, tags from pydoctor.epydoc.markup import DocstringLinker, ParsedDocstring, ParseError, ParserFunction -from pydoctor.model import Documentable from pydoctor.epydoc.docutils import set_node_attributes, new_document +if TYPE_CHECKING: + from pydoctor.model import Documentable + def parse_docstring(docstring: str, errors: List[ParseError]) -> ParsedDocstring: """ Parse the given docstring, which is formatted as plain text; and @@ -31,7 +34,7 @@ def parse_docstring(docstring: str, errors: List[ParseError]) -> ParsedDocstring """ return ParsedPlaintextDocstring(docstring) -def get_parser(obj: Optional[Documentable]) -> ParserFunction: +def get_parser(obj: Documentable | None) -> ParserFunction: """ Just return the L{parse_docstring} function. """ diff --git a/pydoctor/epydoc/markup/restructuredtext.py b/pydoctor/epydoc/markup/restructuredtext.py index 8c11806d7..86964d5a3 100644 --- a/pydoctor/epydoc/markup/restructuredtext.py +++ b/pydoctor/epydoc/markup/restructuredtext.py @@ -41,9 +41,11 @@ from __future__ import annotations __docformat__ = 'epytext en' +from functools import lru_cache from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Sequence, Set, cast if TYPE_CHECKING: from typing import TypeAlias + from pydoctor.model import Documentable import re from docutils import nodes @@ -58,8 +60,9 @@ from pydoctor.epydoc.markup import Field, ParseError, ParsedDocstring, ParserFunction from pydoctor.epydoc.markup.plaintext import ParsedPlaintextDocstring -from pydoctor.epydoc.docutils import new_document -from pydoctor.model import Documentable +from pydoctor.epydoc.docutils import new_document, set_node_attributes + +from twisted.web.template import tags #: A dictionary whose keys are the "consolidated fields" that are #: recognized by epydoc; and whose values are the corresponding epydoc @@ -120,6 +123,24 @@ def get_parser(obj:Documentable) -> ParserFunction: """ return parse_docstring +@lru_cache() +def parsed_text(text: str) -> ParsedDocstring: + """ + Enacpsulate some raw text with no markup inside a L{ParsedDocstring}. + """ + document = new_document('text') + txt_node = set_node_attributes( + nodes.Text(text), + document=document, + lineno=1) + set_node_attributes(document, children=[txt_node]) + return ParsedRstDocstring(document, ()) + +# not using cache here because we need new span tag for every call +# othewise it messes-up everything. +def parsed_text_with_css(text:str, css_class: str) -> ParsedDocstring: + return parsed_text(text).with_tag(tags.span(class_=css_class)) + class OptimizedReporter(Reporter): """A reporter that ignores all debug messages. This is used to shave a couple seconds off of epydoc's run time, since docutils diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index 9b111d1f6..0eedf9857 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -24,7 +24,7 @@ from twisted.web.template import Tag, tags from pydoctor.epydoc.markup import ParsedDocstring, DocstringLinker import pydoctor.epydoc.markup.plaintext -from pydoctor.epydoc.markup.restructuredtext import ParsedRstDocstring +from pydoctor.epydoc.markup.restructuredtext import ParsedRstDocstring, parsed_text, parsed_text_with_css from pydoctor.epydoc.markup._pyval_repr import colorize_pyval, colorize_inline_pyval if TYPE_CHECKING: @@ -35,7 +35,8 @@ Alias to L{pydoctor.linker.taglink()}. """ -BROKEN = tags.p(class_="undocumented")('Broken description') +BROKEN_TEXT = 'Broken description' +BROKEN = tags.p(class_="undocumented")(BROKEN_TEXT) def _get_docformat(obj: model.Documentable) -> str: """ @@ -672,26 +673,6 @@ def ensure_parsed_docstring(obj: model.Documentable) -> Optional[model.Documenta return None -class ParsedStanOnly(ParsedDocstring): - """ - A L{ParsedDocstring} directly constructed from stan, for caching purposes. - - L{to_stan} method simply returns back what's given to L{ParsedStanOnly.__init__}. - """ - def __init__(self, stan: Tag): - super().__init__(fields=[]) - self._fromstan = stan - - @property - def has_body(self) -> bool: - return True - - def to_stan(self, docstring_linker: Any) -> Tag: - return self._fromstan - - def to_node(self) -> Any: - raise NotImplementedError() - def _get_parsed_summary(obj: model.Documentable) -> Tuple[Optional[model.Documentable], ParsedDocstring]: """ Ensures that the L{model.Documentable.parsed_summary} attribute of a documentable is set to it's final value. @@ -705,7 +686,8 @@ def _get_parsed_summary(obj: model.Documentable) -> Tuple[Optional[model.Documen return (source, obj.parsed_summary) if source is None: - summary_parsed_doc: ParsedDocstring = ParsedStanOnly(format_undocumented(obj)) + summary_parsed_doc: ParsedDocstring = parsed_text_with_css( + format_undocumented_text(obj), 'undocumented') else: # Tell mypy that if we found a docstring, we also have its source. assert obj.parsed_docstring is not None @@ -811,10 +793,9 @@ def format_docstring(obj: model.Documentable) -> Tag: return ret def format_summary_fallback(errs: List[ParseError], parsed_doc:ParsedDocstring, ctx:model.Documentable) -> Tag: - stan = BROKEN - # override parsed_summary instance variable to remeber this one is broken. - ctx.parsed_summary = ParsedStanOnly(stan) - return stan + # override parsed_summary instance variable to remember this one is broken. + ctx.parsed_summary = parsed_text_with_css(BROKEN_TEXT, 'undocumented') + return BROKEN def format_summary(obj: model.Documentable) -> Tag: """Generate an shortened HTML representation of a docstring.""" @@ -834,8 +815,8 @@ def format_summary(obj: model.Documentable) -> Tag: return stan -def format_undocumented(obj: model.Documentable) -> Tag: - """Generate an HTML representation for an object lacking a docstring.""" +def format_undocumented_text(obj: model.Documentable) -> str: + """Generate a string representation for an object lacking a docstring.""" sub_objects_with_docstring_count: DefaultDict[model.DocumentableKind, int] = defaultdict(int) sub_objects_total_count: DefaultDict[model.DocumentableKind, int] = defaultdict(int) @@ -846,12 +827,11 @@ def format_undocumented(obj: model.Documentable) -> Tag: if sub_ob.docstring is not None: sub_objects_with_docstring_count[kind] += 1 - tag: Tag = tags.span(class_='undocumented') if sub_objects_with_docstring_count: kind = obj.kind assert kind is not None # if kind is None, object is invisible - tag( + return ( "No ", format_kind(kind).lower(), " docstring; ", ', '.join( f"{sub_objects_with_docstring_count[kind]}/{sub_objects_total_count[kind]} " @@ -862,8 +842,7 @@ def format_undocumented(obj: model.Documentable) -> Tag: " documented" ) else: - tag("Undocumented") - return tag + return "Undocumented" def type2stan(obj: model.Documentable) -> Optional[Tag]: @@ -1175,27 +1154,6 @@ def get_constructors_extra(cls:model.Class) -> ParsedDocstring | None: set_node_attributes(document, children=elements) return ParsedRstDocstring(document, ()) -@lru_cache() -def parsed_text(text: str) -> ParsedDocstring: - """ - Enacpsulate some raw text with no markup inside a L{ParsedDocstring}. - """ - document = new_document('text') - txt_node = set_node_attributes( - nodes.Text(text), - document=document, - lineno=1) - set_node_attributes(document, children=[txt_node]) - return ParsedRstDocstring(document, ()) - -# not using @cache here because we need new span tag for every call -# othewise it messes-up everything. -def parsed_text_with_css(text:str, css_class: str) -> ParsedDocstring: - parsed_doc = parsed_text(text) - if not css_class: - return parsed_doc - return parsed_doc.with_tag(tags.span(class_=css_class)) - _empty = inspect.Parameter.empty _POSITIONAL_ONLY = inspect.Parameter.POSITIONAL_ONLY _POSITIONAL_OR_KEYWORD = inspect.Parameter.POSITIONAL_OR_KEYWORD From a46a3a38df11914fa26e2635fd5664b0784dbc59 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 14 Nov 2024 14:56:24 -0500 Subject: [PATCH 17/38] Few simplifications here and there. --- pydoctor/epydoc/markup/__init__.py | 18 +++++++++++++++- pydoctor/epydoc/markup/_types.py | 3 ++- pydoctor/epydoc2stan.py | 2 +- pydoctor/templatewriter/pages/__init__.py | 3 +-- pydoctor/templatewriter/search.py | 7 +------ pydoctor/test/__init__.py | 25 ++++------------------- pydoctor/test/epydoc/test_pyval_repr.py | 7 +++---- pydoctor/test/test_astbuilder.py | 2 +- 8 files changed, 30 insertions(+), 37 deletions(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index e1c17537e..c226c9611 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -33,6 +33,7 @@ from __future__ import annotations __docformat__ = 'epytext en' +import contextlib from itertools import chain from typing import Callable, ContextManager, Iterable, List, Optional, Sequence, Iterator, TYPE_CHECKING import abc @@ -209,7 +210,8 @@ def to_text(self) -> str: Translate this docstring to a string. The default implementation depends on L{to_node}. """ - return ''.join(node2stan.gettext(self.to_node())) + doc = self.to_node() + return ''.join(node2stan.gettext(doc)) def with_linker(self, linker: DocstringLinker) -> ParsedDocstring: """ @@ -425,6 +427,20 @@ def switch_context(self, ob:Optional['Documentable']) -> ContextManager[None]: in this case error will NOT be reported at all. """ +class NotFoundLinker(DocstringLinker): + """A DocstringLinker implementation that cannot find any links.""" + + def link_to(self, target: str, label: "Flattenable") -> Tag: + return tags.transparent(label) + + def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: + return tags.code(label) + + @contextlib.contextmanager + def switch_context(self, ob: Documentable | None) -> Iterator[None]: + yield + + ################################################## ## ParseError exceptions ################################################## diff --git a/pydoctor/epydoc/markup/_types.py b/pydoctor/epydoc/markup/_types.py index 8e94243d6..6f356de02 100644 --- a/pydoctor/epydoc/markup/_types.py +++ b/pydoctor/epydoc/markup/_types.py @@ -48,8 +48,9 @@ def has_body(self) -> bool: def to_node(self) -> nodes.document: """ - Not implemented. + Not implemented at this time :/ """ + #TODO: Fix this soon raise NotImplementedError() def to_stan(self, docstring_linker: DocstringLinker) -> Tag: diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index 0eedf9857..dd9319796 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -966,7 +966,7 @@ def colorized_pyval_fallback(_: List[ParseError], doc:ParsedDocstring, __:model. """ This fallback function uses L{ParsedDocstring.to_node()}, so it must be used only with L{ParsedDocstring} subclasses that implements C{to_node()}. """ - return Tag('code')(node2stan.gettext(doc.to_node())) + return tags.code(doc.to_text()) def _format_constant_value(obj: model.Attribute) -> Iterator["Flattenable"]: diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 3bc7d4412..72d76158e 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -70,8 +70,7 @@ def format_signature(func: Union[model.Function, model.FunctionOverload]) -> "Fl parsed_sig, ctx.docstring_linker, ctx, - fallback=lambda _, doc, ___: tags.transparent( - node2stan.gettext(doc.to_node())), + fallback=lambda _, doc, ___: tags.transparent(doc.to_text()), section='signature' ) diff --git a/pydoctor/templatewriter/search.py b/pydoctor/templatewriter/search.py index 3faf88c69..bebcb64b8 100644 --- a/pydoctor/templatewriter/search.py +++ b/pydoctor/templatewriter/search.py @@ -104,12 +104,7 @@ def format_docstring(self, ob: model.Documentable) -> Optional[str]: source = epydoc2stan.ensure_parsed_docstring(ob) if source is not None: assert ob.parsed_docstring is not None - try: - doc = ' '.join(node2stan.gettext(ob.parsed_docstring.to_node())) - except NotImplementedError: - # some ParsedDocstring subclass raises NotImplementedError on calling to_node() - # Like ParsedPlaintextDocstring. - doc = source.docstring + doc = ob.parsed_docstring.to_text() return doc def format_kind(self, ob:model.Documentable) -> str: diff --git a/pydoctor/test/__init__.py b/pydoctor/test/__init__.py index 45e8cf406..186518e24 100644 --- a/pydoctor/test/__init__.py +++ b/pydoctor/test/__init__.py @@ -1,20 +1,18 @@ """PyDoctor's test suite.""" -import contextlib from logging import LogRecord -from typing import Iterable, TYPE_CHECKING, Iterator, Optional, Sequence +from typing import Iterable, TYPE_CHECKING, Sequence import sys import pytest from pathlib import Path -from twisted.web.template import Tag, tags from pydoctor import epydoc2stan, model from pydoctor.templatewriter import IWriter, TemplateLookup -from pydoctor.epydoc.markup import DocstringLinker +from pydoctor.epydoc.markup import NotFoundLinker -if TYPE_CHECKING: - from twisted.web.template import Flattenable + +__all__ = ['InMemoryWriter', 'NotFoundLinker', 'posonlyargs', 'typecomment', 'CapSys'] posonlyargs = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python 3.8") typecomment = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python 3.8") @@ -87,18 +85,3 @@ def _writeDocsFor(self, ob: model.Documentable) -> None: for o in ob.contents.values(): self._writeDocsFor(o) - - -class NotFoundLinker(DocstringLinker): - """A DocstringLinker implementation that cannot find any links.""" - - def link_to(self, target: str, label: "Flattenable") -> Tag: - return tags.transparent(label) - - def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: - return tags.code(label) - - @contextlib.contextmanager - def switch_context(self, ob: Optional[model.Documentable]) -> Iterator[None]: - yield - \ No newline at end of file diff --git a/pydoctor/test/epydoc/test_pyval_repr.py b/pydoctor/test/epydoc/test_pyval_repr.py index 50e88adf6..d1a819fec 100644 --- a/pydoctor/test/epydoc/test_pyval_repr.py +++ b/pydoctor/test/epydoc/test_pyval_repr.py @@ -10,7 +10,6 @@ from pydoctor.epydoc.markup._pyval_repr import PyvalColorizer, colorize_inline_pyval from pydoctor.test import NotFoundLinker from pydoctor.stanutils import flatten, flatten_text, html2stan -from pydoctor.node2stan import gettext def color(v: Any, linebreakok:bool=True, maxlines:int=5, linelen:int=40) -> str: colorizer = PyvalColorizer(linelen=linelen, linebreakok=linebreakok, maxlines=maxlines) @@ -1160,7 +1159,7 @@ def color_re(s: Union[bytes, str], val = colorizer.colorize(extract_expr(ast.parse(f"re.compile({repr(s)})"))) if check_roundtrip: - raw_text = ''.join(gettext(val.to_node())) + raw_text = val.to_text() re_begin = 13 raw_string = True @@ -1428,7 +1427,7 @@ def color2(v: Any, linelen:int=50) -> str: """ colorizer = PyvalColorizer(linelen=linelen, maxlines=5) colorized = colorizer.colorize(v) - text1 = ''.join(gettext(colorized.to_node())) + text1 = colorized.to_text() text2 = flatten_text(html2stan(flatten(colorized.to_stan(NotFoundLinker())))) assert text1 == text2 return text2 @@ -1473,7 +1472,7 @@ def test_summary() -> None: """ summarizer = PyvalColorizer(linelen=60, maxlines=1, linebreakok=False) def summarize(v:Any) -> str: - return(''.join(gettext(summarizer.colorize(v).to_node()))) + return summarizer.colorize(v).to_text() assert summarize(list(range(100))) == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16..." assert summarize('hello\nworld') == r"'hello\nworld'" diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py index 7895a51e1..8956f8871 100644 --- a/pydoctor/test/test_astbuilder.py +++ b/pydoctor/test/test_astbuilder.py @@ -112,7 +112,7 @@ def signature2str(func: model.Function | model.FunctionOverload) -> str: doc = get_parsed_signature(func) assert doc fromhtml = flatten_text(format_signature(func)) - fromdocutils = ''.join(node2stan.gettext(doc.to_node())) + fromdocutils = doc.to_text() assert fromhtml == fromdocutils return fromhtml From 40ac0a60932be39596d2252d929945fdb1453c13 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 14 Nov 2024 14:58:59 -0500 Subject: [PATCH 18/38] Use the CSS class 'decorator' for all decorators. --- pydoctor/templatewriter/pages/__init__.py | 6 +++--- pydoctor/test/test_templatewriter.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 72d76158e..baf621feb 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -13,7 +13,7 @@ from twisted.web.template import Element, Tag, renderer, tags from pydoctor.extensions import zopeinterface -from pydoctor import epydoc2stan, model, linker, __version__, node2stan +from pydoctor import epydoc2stan, model, linker, __version__ from pydoctor.astbuilder import node2fullname from pydoctor.templatewriter import util, TemplateLookup, TemplateElement from pydoctor.templatewriter.pages.table import ChildTable @@ -51,10 +51,10 @@ def _format_decorators(obj: Union[model.Function, model.Attribute, model.Functio # Report eventual warnings. It warns when we can't colorize the expression for some reason. epydoc2stan.reportWarnings(documentable_obj, doc.warnings, section='colorize decorator') - yield tags.span('@', stan.children, tags.br()) + yield tags.span('@', stan.children, tags.br(), class_='decorator') def format_decorators(obj: Union[model.Function, model.Attribute, model.FunctionOverload]) -> Tag: - return tags.span(*_format_decorators(obj), class_='func-decorators') + return tags.div(*_format_decorators(obj)) def format_signature(func: Union[model.Function, model.FunctionOverload]) -> "Flattenable": """ diff --git a/pydoctor/test/test_templatewriter.py b/pydoctor/test/test_templatewriter.py index 03a066729..9019802fc 100644 --- a/pydoctor/test/test_templatewriter.py +++ b/pydoctor/test/test_templatewriter.py @@ -577,12 +577,12 @@ def func(): ... ''') stan = stanutils.flatten(pages.format_decorators(cast(model.Function, mod.contents['func']))) - assert stan == ("""""" + assert stan == ("""
""" """@string_decorator(set('""" r"""\\/:*?"<>|\f\v\t\r\n""" - """'))
@simple_decorator""" + """'))
@simple_decorator""" """(max_examples=700, deadline=None, option=range(10))
""" - """
""") + """
""") def test_compact_module_summary() -> None: From 4172485fa212557df7c10de874e5b52873a991ed Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 14 Nov 2024 16:44:27 -0500 Subject: [PATCH 19/38] Fix various bugs in the implementation. also remove the summary caching on the parsed docstring side, this is unnecessary because it's already cahed in the documentable. --- pydoctor/epydoc/markup/__init__.py | 28 +++++++++---------- pydoctor/epydoc/markup/restructuredtext.py | 1 + pydoctor/epydoc2stan.py | 14 +++------- pydoctor/test/epydoc/test_restructuredtext.py | 1 - pydoctor/test/test_astbuilder.py | 6 ++++ 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index c226c9611..90db67f5e 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -145,9 +145,7 @@ def __init__(self, fields: Sequence['Field']): A list of L{Field}s, each of which encodes a single field. The field's bodies are encoded as C{ParsedDocstring}s. """ - self._stan: Optional[Tag] = None - self._summary: Optional['ParsedDocstring'] = None @property @abc.abstractmethod @@ -232,9 +230,16 @@ def with_tag(self, tag: Tag) -> ParsedDocstring: """ # We double wrap it with a transparent tag so the added tags survives ParsedDocstring.combine # wich combines the content of the main div of the stan, not the div itself. - t = tag - return WrappedParsedDocstring(self, - lambda this, linker: tags.transparent(t(this.to_stan(linker)))) + def to_stan(this: ParsedDocstring, linker: DocstringLinker) -> Tag: + # Since the stan is cached inside _stan attribute we can't simply use + # "lambda this, linker: tags.transparent(t(this.to_stan(linker)))" as the new to_stan method. + # this would not behave correctly because each time to_stan will be called, the content would be duplicated. + if this._stan is not None: + return this._stan + this._stan = Tag('')(tag(this.to_stan(linker))) + return this._stan + + return WrappedParsedDocstring(self, to_stan) @classmethod def combine(cls, elements: Sequence[ParsedDocstring]) -> ParsedDocstring: @@ -246,20 +251,15 @@ def combine(cls, elements: Sequence[ParsedDocstring]) -> ParsedDocstring: def get_summary(self) -> 'ParsedDocstring': """ Returns the summary of this docstring. - - @note: The summary is cached. """ - if self._summary is not None: - return self._summary try: _document = self.to_node() visitor = SummaryExtractor(_document) _document.walk(visitor) except Exception: - self._summary = parsed_text_with_css('Broken summary', 'undocumented') - else: - self._summary = visitor.summary or parsed_text_with_css('No summary', 'undocumented') - return self._summary + return parsed_text_with_css('Broken summary', 'undocumented') + + return visitor.summary or parsed_text_with_css('No summary', 'undocumented') class _ParsedDocstringTree(ParsedDocstring): @@ -313,7 +313,7 @@ def __init__(self, """ self._to_stan = to_stan - def to_stan(self, docstring_linker): + def to_stan(self, docstring_linker) -> Tag: return self._to_stan(self.wrapped, docstring_linker) # Boring diff --git a/pydoctor/epydoc/markup/restructuredtext.py b/pydoctor/epydoc/markup/restructuredtext.py index 86964d5a3..d6a6afc75 100644 --- a/pydoctor/epydoc/markup/restructuredtext.py +++ b/pydoctor/epydoc/markup/restructuredtext.py @@ -138,6 +138,7 @@ def parsed_text(text: str) -> ParsedDocstring: # not using cache here because we need new span tag for every call # othewise it messes-up everything. +@lru_cache() def parsed_text_with_css(text:str, css_class: str) -> ParsedDocstring: return parsed_text(text).with_tag(tags.span(class_=css_class)) diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index dd9319796..91eeb3b66 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -687,7 +687,7 @@ def _get_parsed_summary(obj: model.Documentable) -> Tuple[Optional[model.Documen if source is None: summary_parsed_doc: ParsedDocstring = parsed_text_with_css( - format_undocumented_text(obj), 'undocumented') + format_undocumented_summary(obj), 'undocumented') else: # Tell mypy that if we found a docstring, we also have its source. assert obj.parsed_docstring is not None @@ -815,7 +815,7 @@ def format_summary(obj: model.Documentable) -> Tag: return stan -def format_undocumented_text(obj: model.Documentable) -> str: +def format_undocumented_summary(obj: model.Documentable) -> str: """Generate a string representation for an object lacking a docstring.""" sub_objects_with_docstring_count: DefaultDict[model.DocumentableKind, int] = defaultdict(int) @@ -1255,10 +1255,7 @@ def _colorize_signature(sig: inspect.Signature, ctx: model.Documentable) -> Pars elif render_pos_only_separator: # It's not a positional-only parameter, and the flag # is set to 'True' (there were pos-only params before.) - if has_next: - result.append(parsed_text_with_css('/, ', css_class='sig-symbol')) - else: - result.append(parsed_text_with_css('/', css_class='sig-symbol')) + result.append(parsed_text_with_css('/, ', css_class='sig-symbol')) render_pos_only_separator = False if kind == _VAR_POSITIONAL: @@ -1269,10 +1266,7 @@ def _colorize_signature(sig: inspect.Signature, ctx: model.Documentable) -> Pars # We have a keyword-only parameter to render and we haven't # rendered an '*args'-like parameter before, so add a '*' # separator to the parameters list ("foo(arg1, *, arg2)" case) - if has_next: - result.append(parsed_text_with_css('*, ', css_class='sig-symbol')) - else: - result.append(parsed_text_with_css('*', css_class='sig-symbol')) + result.append(parsed_text_with_css('*, ', css_class='sig-symbol')) # This condition should be only triggered once, so # reset the flag render_kw_only_separator = False diff --git a/pydoctor/test/epydoc/test_restructuredtext.py b/pydoctor/test/epydoc/test_restructuredtext.py index 2234c8880..fb30e167a 100644 --- a/pydoctor/test/epydoc/test_restructuredtext.py +++ b/pydoctor/test/epydoc/test_restructuredtext.py @@ -249,7 +249,6 @@ def test_summary(markup:str) -> None: errors: List[ParseError] = [] pdoc = get_parser_by_name(markup)(dedent(src), errors) assert not errors - assert pdoc.get_summary() == pdoc.get_summary() # summary is cached inside ParsedDocstring as well. assert flatten_text(pdoc.get_summary().to_stan(NotFoundLinker())) == summary_text diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py index 8956f8871..8afb41c3a 100644 --- a/pydoctor/test/test_astbuilder.py +++ b/pydoctor/test/test_astbuilder.py @@ -229,6 +229,9 @@ async def a(): '(a, b=3, *c, **kw)', '(f=True)', '(x=0.1, y=-2)', + '(x, *v)', + '(x, *, v)', + '(x, *, v=1)', r"(s='theory', t='con\'text')", )) @systemcls_param @@ -252,6 +255,9 @@ def test_function_signature(signature: str, systemcls: Type[model.System]) -> No '(x, y, /, z, w=42)', '(x, y, /, z=0, w=0)', '(x, y=3, /, z=5, w=7)', + '(x, /, *v)', + '(x, /, *, v)', + '(x, /, *, v=1)', '(x, /, *v, a=1, b=2)', '(x, /, *, a=1, b=2, **kwargs)', )) From 7103ce5f5ac6a4d4b290711d118868b2e7bbaa44 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 14 Nov 2024 16:45:23 -0500 Subject: [PATCH 20/38] Fix pyflakes --- pydoctor/epydoc2stan.py | 3 +-- pydoctor/templatewriter/search.py | 2 +- pydoctor/test/test_astbuilder.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index 91eeb3b66..a99493a29 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -12,12 +12,11 @@ ) import ast import re -from functools import lru_cache import attr from docutils import nodes -from pydoctor import model, linker, node2stan +from pydoctor import model, linker from pydoctor.astutils import is_none_literal from pydoctor.epydoc.docutils import new_document, set_node_attributes from pydoctor.epydoc.markup import Field as EpydocField, ParseError, get_parser_by_name, processtypes diff --git a/pydoctor/templatewriter/search.py b/pydoctor/templatewriter/search.py index bebcb64b8..fe48a00ee 100644 --- a/pydoctor/templatewriter/search.py +++ b/pydoctor/templatewriter/search.py @@ -10,7 +10,7 @@ import attr from pydoctor.templatewriter.pages import Page -from pydoctor import model, epydoc2stan, node2stan +from pydoctor import model, epydoc2stan from twisted.web.template import Tag, renderer from lunr import lunr, get_default_builder diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py index 8afb41c3a..dbe4022ed 100644 --- a/pydoctor/test/test_astbuilder.py +++ b/pydoctor/test/test_astbuilder.py @@ -4,7 +4,7 @@ import ast import sys -from pydoctor import astbuilder, astutils, model, node2stan +from pydoctor import astbuilder, astutils, model from pydoctor import epydoc2stan from pydoctor.epydoc.markup import DocstringLinker, ParsedDocstring from pydoctor.options import Options From eae961a376889a4f6def87dc8b6ca31d59b3d3fc Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 14 Nov 2024 16:53:49 -0500 Subject: [PATCH 21/38] Fix format_undocumented_summary returning a tuple of strings instead of a string. A some annotation. --- pydoctor/epydoc/markup/__init__.py | 2 +- pydoctor/epydoc2stan.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index 90db67f5e..c29199e4d 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -313,7 +313,7 @@ def __init__(self, """ self._to_stan = to_stan - def to_stan(self, docstring_linker) -> Tag: + def to_stan(self, docstring_linker: DocstringLinker) -> Tag: return self._to_stan(self.wrapped, docstring_linker) # Boring diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index a99493a29..d95e426ae 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -831,14 +831,14 @@ def format_undocumented_summary(obj: model.Documentable) -> str: kind = obj.kind assert kind is not None # if kind is None, object is invisible return ( - "No ", format_kind(kind).lower(), " docstring; ", - ', '.join( + f"No {format_kind(kind).lower()} docstring; " + + ', '.join( f"{sub_objects_with_docstring_count[kind]}/{sub_objects_total_count[kind]} " f"{format_kind(kind, plural=sub_objects_with_docstring_count[kind]>=2).lower()}" for kind in sorted(sub_objects_total_count, key=(lambda x:x.value)) - ), - " documented" + ) + + " documented" ) else: return "Undocumented" From 7c6c6eb093dbff4ae91326a29643fd074e34268d Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 14 Nov 2024 16:58:24 -0500 Subject: [PATCH 22/38] increase the threshold for a function to be rendered in several lines. --- pydoctor/templatewriter/pages/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index baf621feb..11358a4fb 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -111,7 +111,7 @@ def format_class_signature(cls: model.Class) -> "Flattenable": r.append(')') return r -LONG_SIGNATURE = 90 # this doesn't acount for the 'def ' and the ending ':' +LONG_SIGNATURE = 120 # this doesn't acount for the 'def ' and the ending ':' """ Maximum size of a function definition to be rendered on a single line. The multiline formatting is only applied at the CSS level to stay customizable. From 19400ffd72e51d29928bd5d6090afdf4073277a6 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 14 Nov 2024 17:16:41 -0500 Subject: [PATCH 23/38] Avoid an empty div for decorators when there are no decorators. --- pydoctor/templatewriter/pages/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 11358a4fb..6b2428352 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -54,7 +54,9 @@ def _format_decorators(obj: Union[model.Function, model.Attribute, model.Functio yield tags.span('@', stan.children, tags.br(), class_='decorator') def format_decorators(obj: Union[model.Function, model.Attribute, model.FunctionOverload]) -> Tag: - return tags.div(*_format_decorators(obj)) + if decs:=list(_format_decorators(obj)): + return tags.div(decs) + return tags.transparent def format_signature(func: Union[model.Function, model.FunctionOverload]) -> "Flattenable": """ From a3ebbdf6561fd29c680af7d8a2f142d486f928f1 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 14 Nov 2024 18:03:51 -0500 Subject: [PATCH 24/38] Use non breaking spaces in sugnature defs. --- pydoctor/templatewriter/pages/__init__.py | 7 ++++--- pydoctor/themes/base/apidocs.css | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 6b2428352..f31ca9024 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -10,7 +10,7 @@ from urllib.parse import urljoin from twisted.web.iweb import IRenderable, ITemplateLoader, IRequest -from twisted.web.template import Element, Tag, renderer, tags +from twisted.web.template import Element, Tag, renderer, tags, CharRef from pydoctor.extensions import zopeinterface from pydoctor import epydoc2stan, model, linker, __version__ @@ -130,6 +130,7 @@ def format_overloads(func: model.Function) -> Iterator["Flattenable"]: tags.div(format_function_def(func.name, func.is_async, overload)), class_='function-overload') +_nbsp = CharRef(160) # non-breaking space. def format_function_def(func_name: str, is_async: bool, func: Union[model.Function, model.FunctionOverload]) -> List["Flattenable"]: """ @@ -143,7 +144,7 @@ def format_function_def(func_name: str, is_async: bool, # signature because the overloaded signatures will be shown instead. if isinstance(func, model.Function) and func.overloads: return r - def_stmt = 'async def' if is_async else 'def' + def_stmt = ['async', _nbsp, 'def'] if is_async else ['def'] if func_name.endswith('.setter') or func_name.endswith('.deleter'): func_name = func_name[:func_name.rindex('.')] @@ -152,7 +153,7 @@ def format_function_def(func_name: str, is_async: bool, func_signature_css_class += ' long-signature' r.extend([ - tags.span(def_stmt, class_='py-keyword'), ' ', + tags.span(def_stmt, class_='py-keyword'), _nbsp, tags.span(func_name, class_='py-defname'), tags.span(format_signature(func), ':', class_=func_signature_css_class), diff --git a/pydoctor/themes/base/apidocs.css b/pydoctor/themes/base/apidocs.css index 8e1e07d20..7f881ef00 100644 --- a/pydoctor/themes/base/apidocs.css +++ b/pydoctor/themes/base/apidocs.css @@ -434,6 +434,9 @@ table .private { padding-left: 0; } +#childList > div .functionHeader { + word-break: break-word; +} /* - Links to class/function/etc names are nested like this: From cd257eb0e7eb6afe739b2b0b031a038ec089bc0c Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 15 Nov 2024 11:25:40 -0500 Subject: [PATCH 25/38] Improve a little bit the rendering of parameter tables that uses very long annotations. --- pydoctor/themes/base/apidocs.css | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/pydoctor/themes/base/apidocs.css b/pydoctor/themes/base/apidocs.css index 7f881ef00..4c07e91a1 100644 --- a/pydoctor/themes/base/apidocs.css +++ b/pydoctor/themes/base/apidocs.css @@ -254,8 +254,15 @@ ul ul ul ul ul ul ul { /* Argument name + type column table */ .fieldTable tr td.fieldArgContainer { - width: 325px; word-break: break-word; + /* It not seems to work with percentage values + so I used px values, these are just indications for the + CSS auto layour table algo not to create tables + with rather unbalanced columns width. */ + max-width: 400px; +} +.fieldTable tr td.fieldArgDesc { + max-width: 600px; } /* parameters names in parameters table */ @@ -306,7 +313,13 @@ ul ul ul ul ul ul ul { #splitTables > table tr td:nth-child(3) { width: auto; } - + .fieldTable{ + table-layout: fixed; + } + /* Argument name + type column table */ + .fieldTable tr td.fieldArgContainer { + width: 34%; + } } @media only screen and (max-width: 820px) { @@ -321,13 +334,6 @@ ul ul ul ul ul ul ul { #splitTables > table tr td:nth-child(2) { width: 160px; } - /* Argument name + type column table */ - .fieldTable tr td.fieldArgContainer { - width: 170px; - } - .fieldTable { - table-layout: fixed; - } } @media only screen and (max-width: 450px) { @@ -335,10 +341,6 @@ ul ul ul ul ul ul ul { #splitTables > table tr td:nth-child(2) { width: 100px; } - /* Argument name + type column table */ - .fieldTable tr td.fieldArgContainer { - width: 125px; - } } table .package { From 907792afcc07d1dcf99784d7b61818f48930660e Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sat, 16 Nov 2024 09:27:08 -0500 Subject: [PATCH 26/38] Get rid of the AnnotationLinker - drop the verbose messages when an annotation is ambiguous (pydoctor is not a checker). Get rid of ParsedDocstring.with_linker() this API did not make sens and was rather an anti-pattern. Replace that with a new parameter of colorize_pyval(is_annotation: bool). obj_reference nodes can now be created with the attribute 'is_annotation' in which case they will be resolved a little bit differently. --- pydoctor/epydoc/markup/__init__.py | 54 +++++++++-------------- pydoctor/epydoc/markup/_pyval_repr.py | 31 ++++++++----- pydoctor/epydoc2stan.py | 13 +++--- pydoctor/linker.py | 45 +------------------ pydoctor/node2stan.py | 10 +++-- pydoctor/templatewriter/pages/__init__.py | 7 +-- pydoctor/test/epydoc/test_pyval_repr.py | 25 ++++++++++- pydoctor/test/test_epydoc2stan.py | 41 +++-------------- 8 files changed, 89 insertions(+), 137 deletions(-) diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index c29199e4d..3737e30d5 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -211,15 +211,6 @@ def to_text(self) -> str: doc = self.to_node() return ''.join(node2stan.gettext(doc)) - def with_linker(self, linker: DocstringLinker) -> ParsedDocstring: - """ - Pre-set the linker object for this parsed docstring. - Whatever is passed to L{to_stan()} will be ignored. - """ - l = linker - return WrappedParsedDocstring(self, - to_stan=lambda this, _: this.to_stan(l)) - def with_tag(self, tag: Tag) -> ParsedDocstring: """ Wraps the L{to_stan()} result inside the given tag. @@ -228,18 +219,7 @@ def with_tag(self, tag: Tag) -> ParsedDocstring: With this trick, the main tag is preserved. It can also be used to add a custom CSS class on top of an existing parsed docstring. """ - # We double wrap it with a transparent tag so the added tags survives ParsedDocstring.combine - # wich combines the content of the main div of the stan, not the div itself. - def to_stan(this: ParsedDocstring, linker: DocstringLinker) -> Tag: - # Since the stan is cached inside _stan attribute we can't simply use - # "lambda this, linker: tags.transparent(t(this.to_stan(linker)))" as the new to_stan method. - # this would not behave correctly because each time to_stan will be called, the content would be duplicated. - if this._stan is not None: - return this._stan - this._stan = Tag('')(tag(this.to_stan(linker))) - return this._stan - - return WrappedParsedDocstring(self, to_stan) + return _ParsedDocstringWithTag(self, tag) @classmethod def combine(cls, elements: Sequence[ParsedDocstring]) -> ParsedDocstring: @@ -299,28 +279,36 @@ def to_stan(self, linker: DocstringLinker) -> Tag: stan(e.to_stan(linker).children) return stan -class WrappedParsedDocstring(ParsedDocstring): +class _ParsedDocstringWithTag(ParsedDocstring): """ - Wraps a parsed docstring to suppplement the to_stan() method. + Wraps a parsed docstring to wrap the result of the + the to_stan() method inside a custom Tag. """ def __init__(self, other: ParsedDocstring, - to_stan: Callable[[ParsedDocstring, DocstringLinker], Tag]): + tag: Tag): super().__init__(other.fields) self.wrapped = other """ The wrapped parsed docstring. """ - self._to_stan = to_stan - - def to_stan(self, docstring_linker: DocstringLinker) -> Tag: - return self._to_stan(self.wrapped, docstring_linker) + self._tag = tag + self._stan: Tag | None = None + + # We double wrap it with a transparent tag so the added tags survives ParsedDocstring.combine + # wich combines the content of the main div of the stan, not the div itself. + def to_stan(self, linker: DocstringLinker) -> Tag: + # Since the stan is cached inside _stan attribute we can't simply use + # "lambda this, linker: tags.transparent(self._tag(this.to_stan(linker)))" as the new to_stan method. + # this would not behave correctly because each time to_stan will be called, the content would be duplicated. + if (stan:=self._stan) is not None: + return stan + self._stan = stan = Tag('')(self._tag(self.wrapped.to_stan(linker))) + return stan # Boring - def to_node(self) -> nodes.document: return self.wrapped.to_node() - @property def has_body(self) -> bool: return self.wrapped.has_body @@ -388,7 +376,7 @@ class DocstringLinker(Protocol): target URL for crossreference links. """ - def link_to(self, target: str, label: "Flattenable") -> Tag: + def link_to(self, target: str, label: "Flattenable", *, is_annotation: bool = False) -> Tag: """ Format a link to a Python identifier. This will resolve the identifier like Python itself would. @@ -396,6 +384,8 @@ def link_to(self, target: str, label: "Flattenable") -> Tag: @param target: The name of the Python identifier that should be linked to. @param label: The label to show for the link. + @param is_annotation: Generated links will give precedence to the module + defined varaible rather the nested definitions when there are name colisions. @return: The link, or just the label if the target was not found. """ @@ -430,7 +420,7 @@ def switch_context(self, ob:Optional['Documentable']) -> ContextManager[None]: class NotFoundLinker(DocstringLinker): """A DocstringLinker implementation that cannot find any links.""" - def link_to(self, target: str, label: "Flattenable") -> Tag: + def link_to(self, target: str, label: "Flattenable", *, is_annotation: bool = False) -> Tag: return tags.transparent(label) def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: diff --git a/pydoctor/epydoc/markup/_pyval_repr.py b/pydoctor/epydoc/markup/_pyval_repr.py index 7691b79a1..7eca65ac4 100644 --- a/pydoctor/epydoc/markup/_pyval_repr.py +++ b/pydoctor/epydoc/markup/_pyval_repr.py @@ -199,7 +199,9 @@ def __init__(self, document: nodes.document, is_complete: bool, warnings: List[s def to_stan(self, docstring_linker: DocstringLinker) -> Tag: return Tag('code')(super().to_stan(docstring_linker)) -def colorize_pyval(pyval: Any, linelen:Optional[int], maxlines:int, linebreakok:bool=True, refmap:Optional[Dict[str, str]]=None) -> ColorizedPyvalRepr: +def colorize_pyval(pyval: Any, linelen:Optional[int], maxlines:int, + linebreakok:bool=True, refmap:Optional[Dict[str, str]]=None, + is_annotation: bool = False) -> ColorizedPyvalRepr: """ Get a L{ColorizedPyvalRepr} instance for this piece of ast. @@ -209,14 +211,15 @@ def colorize_pyval(pyval: Any, linelen:Optional[int], maxlines:int, linebreakok: This can be used for cases the where the linker might be wrong, obviously this is just a workaround. @return: A L{ColorizedPyvalRepr} describing the given pyval. """ - return PyvalColorizer(linelen=linelen, maxlines=maxlines, linebreakok=linebreakok, refmap=refmap).colorize(pyval) + return PyvalColorizer(linelen=linelen, maxlines=maxlines, linebreakok=linebreakok, + refmap=refmap, is_annotation=is_annotation).colorize(pyval) -def colorize_inline_pyval(pyval: Any, refmap:Optional[Dict[str, str]]=None) -> ColorizedPyvalRepr: +def colorize_inline_pyval(pyval: Any, refmap:Optional[Dict[str, str]]=None, is_annotation: bool = False) -> ColorizedPyvalRepr: """ Used to colorize type annotations and parameters default values. @returns: C{L{colorize_pyval}(pyval, linelen=None, linebreakok=False)} """ - return colorize_pyval(pyval, linelen=None, maxlines=1, linebreakok=False, refmap=refmap) + return colorize_pyval(pyval, linelen=None, maxlines=1, linebreakok=False, refmap=refmap, is_annotation=is_annotation) def _get_str_func(pyval: AnyStr) -> Callable[[str], AnyStr]: func = cast(Callable[[str], AnyStr], str if isinstance(pyval, str) else \ @@ -261,14 +264,17 @@ def _bytes_escape(b: bytes) -> str: class PyvalColorizer: """ - Syntax highlighter for Python values. + Syntax highlighter for Python AST (and some builtins types). """ - def __init__(self, linelen:Optional[int], maxlines:int, linebreakok:bool=True, refmap:Optional[Dict[str, str]]=None): + def __init__(self, linelen:Optional[int], maxlines:int, linebreakok:bool=True, + refmap:Optional[Dict[str, str]]=None, is_annotation: bool = False): self.linelen: Optional[int] = linelen if linelen!=0 else None self.maxlines: Union[int, float] = maxlines if maxlines!=0 else float('inf') self.linebreakok = linebreakok self.refmap = refmap if refmap is not None else {} + self.is_annotation = is_annotation + # some edge cases require to compute the precedence ahead of time and can't be # easily done with access only to the parent node of some operators. self.explicit_precedence:Dict[ast.AST, int] = {} @@ -284,7 +290,7 @@ def __init__(self, linelen:Optional[int], maxlines:int, linebreakok:bool=True, r NUMBER_TAG = None # ints, floats, etc QUOTE_TAG = 'variable-quote' # Quotes around strings. STRING_TAG = 'variable-string' # Body of string literals - LINK_TAG = 'variable-link' # Links to other documentables, extracted from AST names and attributes. + LINK_TAG = None # Links, we don't use an explicit class here, but in node2stan. ELLIPSIS_TAG = 'variable-ellipsis' LINEWRAP_TAG = 'variable-linewrap' UNKNOWN_TAG = 'variable-unknown' @@ -1017,16 +1023,21 @@ def _output(self, s: AnyStr, css_class: Optional[str], if (self.linelen is None or state.charpos + segment_len <= self.linelen or link is True - or css_class in ('variable-quote',)): + or css_class in (self.QUOTE_TAG,)): state.charpos += segment_len if link is True: # Here, we bypass the linker if refmap contains the segment we're linking to. - # The linker can be problematic because it has some design blind spots when the same name is declared in the imports and in the module body. + # The linker can be problematic because it has some design blind spots when + # the same name is declared in the imports and in the module body. # Note that the argument name is 'refuri', not 'refuid. - element = obj_reference('', segment, refuri=self.refmap.get(segment, segment)) + element = obj_reference('', segment, + refuri=self.refmap.get(segment, segment)) + if self.is_annotation: + # Don't set the attribute if it's not True. + element.attributes['is_annotation'] = True elif css_class is not None: element = nodes.inline('', segment, classes=[css_class]) else: diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index d95e426ae..255ac11e2 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -274,10 +274,10 @@ def __init__(self, obj: model.Documentable): def set_param_types_from_annotations( self, annotations: Mapping[str, Optional[ast.expr]] ) -> None: - _linker = linker._AnnotationLinker(self.obj) + _linker = self.obj.docstring_linker formatted_annotations = { name: None if value is None - else ParamType(safe_to_stan(colorize_inline_pyval(value), _linker, + else ParamType(safe_to_stan(colorize_inline_pyval(value, is_annotation=True), _linker, self.obj, fallback=colorized_pyval_fallback, section='annotation', report=False), # don't spam the log, invalid annotation are going to be reported when the signature gets colorized origin=FieldOrigin.FROM_AST) @@ -853,8 +853,7 @@ def type2stan(obj: model.Documentable) -> Optional[Tag]: if parsed_type is None: return None else: - _linker = linker._AnnotationLinker(obj) - return safe_to_stan(parsed_type, _linker, obj, + return safe_to_stan(parsed_type, obj.docstring_linker, obj, fallback=colorized_pyval_fallback, section='annotation') def get_parsed_type(obj: model.Documentable) -> Optional[ParsedDocstring]: @@ -868,7 +867,7 @@ def get_parsed_type(obj: model.Documentable) -> Optional[ParsedDocstring]: # Only Attribute instances have the 'annotation' attribute. annotation: Optional[ast.expr] = getattr(obj, 'annotation', None) if annotation is not None: - return colorize_inline_pyval(annotation) + return colorize_inline_pyval(annotation, is_annotation=True) return None @@ -1166,9 +1165,7 @@ def _colorize_signature_annotation(annotation: object, Returns L{ParsedDocstring} with extra context to make sure we resolve tha annotation correctly. """ - return colorize_inline_pyval(annotation - # Make sure to use the annotation linker in the context of an annotation. - ).with_linker(linker._AnnotationLinker(ctx) + return colorize_inline_pyval(annotation, is_annotation=True, # Make sure the generated tags are not stripped by ParsedDocstring.combine. ).with_tag(tags.transparent) diff --git a/pydoctor/linker.py b/pydoctor/linker.py index a36949339..f2bc1b7bf 100644 --- a/pydoctor/linker.py +++ b/pydoctor/linker.py @@ -133,7 +133,7 @@ def look_for_intersphinx(self, name: str) -> Optional[str]: def link_to(self, identifier: str, label: "Flattenable", *, is_annotation: bool = False) -> Tag: if is_annotation: - fullID = self.obj.expandAnnotationName(identifier) + fullID = (self.obj.parent or self.obj).expandAnnotationName(identifier) else: fullID = self.obj.expandName(identifier) @@ -241,46 +241,3 @@ def _resolve_identifier_xref(self, if self.reporting_obj: self.reporting_obj.report(message, 'resolve_identifier_xref', lineno) raise LookupError(identifier) - -class _AnnotationLinker(DocstringLinker): - """ - Specialized linker to resolve annotations attached to the given L{Documentable}. - - Links will be created in the context of C{obj} but - generated with the C{obj.module}'s linker when possible. - """ - def __init__(self, obj:'model.Documentable') -> None: - self._obj = obj - self._module = obj.module - self._scope = obj.parent or obj - self._scope_linker = _EpydocLinker(self._scope) - - @property - def obj(self) -> 'model.Documentable': - return self._obj - - def warn_ambiguous_annotation(self, target:str) -> None: - # report a low-level message about ambiguous annotation - mod_ann = self._module.expandName(target) - obj_ann = self._scope.expandName(target) - if mod_ann != obj_ann and '.' in obj_ann and '.' in mod_ann: - self.obj.report( - f'ambiguous annotation {target!r}, could be interpreted as ' - f'{obj_ann!r} instead of {mod_ann!r}', section='annotation', - thresh=1 - ) - - def link_to(self, target: str, label: "Flattenable") -> Tag: - with self.switch_context(self._obj): - if self._module.isNameDefined(target): - self.warn_ambiguous_annotation(target) - return self._scope_linker.link_to(target, label, is_annotation=True) - - def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: - with self.switch_context(self._obj): - return self.obj.docstring_linker.link_xref(target, label, lineno) - - @contextlib.contextmanager - def switch_context(self, ob:Optional['model.Documentable']) -> Iterator[None]: - with self._scope_linker.switch_context(ob): - yield diff --git a/pydoctor/node2stan.py b/pydoctor/node2stan.py index 8b2f00665..f18649714 100644 --- a/pydoctor/node2stan.py +++ b/pydoctor/node2stan.py @@ -3,6 +3,7 @@ """ from __future__ import annotations +from functools import partial from itertools import chain import re import optparse @@ -99,17 +100,20 @@ def __init__(self, super().__init__(document) # don't allow

tags, start at

- # h1 is reserved for the page nodes.title. + # h1 is reserved for the page title. self.section_level += 1 # Handle interpreted text (crossreferences) def visit_title_reference(self, node: nodes.title_reference) -> None: lineno = get_lineno(node) - self._handle_reference(node, link_func=lambda target, label: self._linker.link_xref(target, label, lineno)) + self._handle_reference(node, link_func=partial(self._linker.link_xref, lineno=lineno)) # Handle internal references def visit_obj_reference(self, node: obj_reference) -> None: - self._handle_reference(node, link_func=self._linker.link_to) + if node.attributes.get('is_annotation'): + self._handle_reference(node, link_func=partial(self._linker.link_to, is_annotation=True)) + else: + self._handle_reference(node, link_func=self._linker.link_to) def _handle_reference(self, node: nodes.title_reference, link_func: Callable[[str, "Flattenable"], "Flattenable"]) -> None: label: "Flattenable" diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index f31ca9024..b158cac03 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -13,7 +13,7 @@ from twisted.web.template import Element, Tag, renderer, tags, CharRef from pydoctor.extensions import zopeinterface -from pydoctor import epydoc2stan, model, linker, __version__ +from pydoctor import epydoc2stan, model, __version__ from pydoctor.astbuilder import node2fullname from pydoctor.templatewriter import util, TemplateLookup, TemplateElement from pydoctor.templatewriter.pages.table import ChildTable @@ -85,7 +85,7 @@ def format_class_signature(cls: model.Class) -> "Flattenable": # the linker will only be used to resolve the generic arguments of the base classes, # it won't actually resolve the base classes (see comment few lines below). # this is why we're using the annotation linker. - _linker = linker._AnnotationLinker(cls) + _linker = cls.docstring_linker if cls.rawbases: r.append('(') @@ -105,7 +105,8 @@ def format_class_signature(cls: model.Class) -> "Flattenable": # link to external class, using the colorizer here # to link to classes with generics (subscripts and other AST expr). - stan = epydoc2stan.safe_to_stan(colorize_inline_pyval(base_node, refmap=refmap), _linker, cls, + # we use is_annotation=True because bases are unstringed, they can contain annotations. + stan = epydoc2stan.safe_to_stan(colorize_inline_pyval(base_node, refmap=refmap, is_annotation=True), _linker, cls, fallback=epydoc2stan.colorized_pyval_fallback, section='rendering of class signature') r.extend(stan.children) diff --git a/pydoctor/test/epydoc/test_pyval_repr.py b/pydoctor/test/epydoc/test_pyval_repr.py index d1a819fec..5dc150447 100644 --- a/pydoctor/test/epydoc/test_pyval_repr.py +++ b/pydoctor/test/epydoc/test_pyval_repr.py @@ -11,8 +11,8 @@ from pydoctor.test import NotFoundLinker from pydoctor.stanutils import flatten, flatten_text, html2stan -def color(v: Any, linebreakok:bool=True, maxlines:int=5, linelen:int=40) -> str: - colorizer = PyvalColorizer(linelen=linelen, linebreakok=linebreakok, maxlines=maxlines) +def color(v: Any, linebreakok:bool=True, maxlines:int=5, linelen:int=40, is_annotation: bool = False) -> str: + colorizer = PyvalColorizer(linelen=linelen, linebreakok=linebreakok, maxlines=maxlines, is_annotation=is_annotation) parsed_doc = colorizer.colorize(v) return parsed_doc.to_node().pformat() @@ -1576,3 +1576,24 @@ def test_expressions_parens(subtests:Any) -> None: check_src("{**({} == {})}") check_src("{**{'y': 2}, 'x': 1, None: True}") check_src("{**{'y': 2}, **{'x': 1}}") + + +def test_is_annotation_flag() -> None: + + # If a line goes beyond linelen, it is wrapped using the ``↵`` element. + # Check that the last line gets a ``↵`` when maxlines is exceeded: + + assert color(extract_expr(ast.parse('list[dict] + set()')), is_annotation=True) == ''' + + list + [ + + + dict + ] + + + + set + ( + ) +''' diff --git a/pydoctor/test/test_epydoc2stan.py b/pydoctor/test/test_epydoc2stan.py index c9b51857c..079588f6f 100644 --- a/pydoctor/test/test_epydoc2stan.py +++ b/pydoctor/test/test_epydoc2stan.py @@ -1303,28 +1303,6 @@ def test_EpydocLinker_warnings(capsys: CapSys) -> None: # No warnings are logged when generating the summary. assert captured == '' -def test_AnnotationLinker_xref(capsys: CapSys) -> None: - """ - Even if the annotation linker is not designed to resolve xref, - it will still do the right thing by forwarding any xref requests to - the initial object's linker. - """ - - mod = fromText(''' - class C: - var="don't use annotation linker for xref!" - ''') - mod.system.intersphinx = cast(SphinxInventory, InMemoryInventory()) - _linker = linker._AnnotationLinker(mod.contents['C']) - - url = flatten(_linker.link_xref('socket.socket', 'socket', 0)) - assert 'https://docs.python.org/3/library/socket.html#socket.socket' in url - assert not capsys.readouterr().out - - url = flatten(_linker.link_xref('var', 'var', 0)) - assert 'href="#var"' in url - assert not capsys.readouterr().out - def test_xref_not_found_epytext(capsys: CapSys) -> None: """ When a link in an epytext docstring cannot be resolved, the reference @@ -1414,7 +1392,7 @@ class RecordingAnnotationLinker(NotFoundLinker): def __init__(self) -> None: self.requests: List[str] = [] - def link_to(self, target: str, label: "Flattenable") -> Tag: + def link_to(self, target: str, label: "Flattenable", *, is_annotation: bool = False) -> Tag: self.requests.append(target) return tags.transparent(label) @@ -1977,9 +1955,9 @@ def f(self, x:typ) -> typ: assert isinstance(f, model.Function) assert f.signature assert "href" in flatten(epydoc2stan._colorize_signature_annotation( - f.signature.parameters['x'].annotation, f).to_stan(NotFoundLinker())) + f.signature.parameters['x'].annotation, f).to_stan(f.docstring_linker)) assert "href" in flatten(epydoc2stan._colorize_signature_annotation( - f.signature.return_annotation, f).to_stan(NotFoundLinker())) + f.signature.return_annotation, f).to_stan(f.docstring_linker)) assert isinstance(var, model.Attribute) assert "href" in flatten(epydoc2stan.type2stan(var) or '') @@ -2000,7 +1978,6 @@ def f(self, x:typ) -> typ: var: typ ''' system = model.System() - system.options.verbosity = 1 mod = fromText(src, modname='m', system=system) f = mod.system.allobjects['m.C.f'] var = mod.system.allobjects['m.C.var'] @@ -2008,21 +1985,15 @@ def f(self, x:typ) -> typ: assert isinstance(f, model.Function) assert f.signature assert 'href="index.html#typ"' in flatten(epydoc2stan._colorize_signature_annotation( - f.signature.parameters['x'].annotation, f).to_stan(NotFoundLinker())) - # the linker can be NotFoundLinker() here because the annotations uses with_linker() + f.signature.parameters['x'].annotation, f).to_stan(f.docstring_linker)) assert 'href="index.html#typ"' in flatten(epydoc2stan._colorize_signature_annotation( - f.signature.return_annotation, f).to_stan(NotFoundLinker())) - # the linker can be NotFoundLinker() here because the annotations uses with_linker() + f.signature.return_annotation, f).to_stan(f.docstring_linker)) assert isinstance(var, model.Attribute) assert 'href="index.html#typ"' in flatten(epydoc2stan.type2stan(var) or '') - assert capsys.readouterr().out == """\ -m:5: ambiguous annotation 'typ', could be interpreted as 'm.C.typ' instead of 'm.typ' -m:5: ambiguous annotation 'typ', could be interpreted as 'm.C.typ' instead of 'm.typ' -m:7: ambiguous annotation 'typ', could be interpreted as 'm.C.typ' instead of 'm.typ' -""" + # Pydoctor is not a checker so no warning is beeing reported. def test_not_found_annotation_does_not_create_link() -> None: """ From 91edc51df627efb7dcfdd1ad6dc3041a710e77f7 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Mon, 18 Nov 2024 09:18:07 -0500 Subject: [PATCH 27/38] Change comment --- pydoctor/test/epydoc/test_pyval_repr.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pydoctor/test/epydoc/test_pyval_repr.py b/pydoctor/test/epydoc/test_pyval_repr.py index 5dc150447..6f09a1312 100644 --- a/pydoctor/test/epydoc/test_pyval_repr.py +++ b/pydoctor/test/epydoc/test_pyval_repr.py @@ -1579,10 +1579,7 @@ def test_expressions_parens(subtests:Any) -> None: def test_is_annotation_flag() -> None: - - # If a line goes beyond linelen, it is wrapped using the ``↵`` element. - # Check that the last line gets a ``↵`` when maxlines is exceeded: - + # the is_annotation attribut is added to all links when is_annotation=True is passed. assert color(extract_expr(ast.parse('list[dict] + set()')), is_annotation=True) == ''' list From 25b5e622404ffd693b17af071b1d2045673ba555 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 21 Nov 2024 11:56:49 -0500 Subject: [PATCH 28/38] Add an environment to build temporalio docs --- .github/workflows/system.yaml | 2 +- tox.ini | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/system.yaml b/.github/workflows/system.yaml index 897d84dab..11f2e22a1 100644 --- a/.github/workflows/system.yaml +++ b/.github/workflows/system.yaml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - tox_target: [twisted-apidoc, cpython-summary, python-igraph-apidocs, cpython-apidocs, numpy-apidocs, git-buildpackage-apidocs, pytype-apidocs] + tox_target: [twisted-apidoc, cpython-summary, python-igraph-apidocs, cpython-apidocs, numpy-apidocs, git-buildpackage-apidocs, pytype-apidocs, temporalio-apidocs] steps: - uses: actions/checkout@v4 diff --git a/tox.ini b/tox.ini index 76a58483f..9722f3dc0 100644 --- a/tox.ini +++ b/tox.ini @@ -105,6 +105,18 @@ commands = # Code 2 error means bad docstrings, which is OK for this test. assert code==2, 'pydoctor exited with code %s, expected code 2.'%code" +[testenv:temporalio-apidocs] +description = Build temporalio/sdk-python API documentation. +commands = + sh -c "if [ ! -d {toxworkdir}/temporalio ]; then \ + git clone --depth 1 https://github.com/temporalio/sdk-python.git {toxworkdir}/temporalio; \ + fi" + sh -c "cd {toxworkdir}/temporalio && git pull" + rm -rf {toxworkdir}/temporalio-output + sh -c "pydoctor --config={toxworkdir}/temporalio/pyproject.toml \ + --html-output={toxworkdir}/temporalio-output --theme=readthedocs \ + --quiet --add-package={toxworkdir}/temporalio/temporalio" + # Requires cmake [testenv:python-igraph-apidocs] description = Build python-igraph API documentation From bd2de92084c651055f409c7bcecb6c1b2bf12009 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Thu, 21 Nov 2024 11:57:07 -0500 Subject: [PATCH 29/38] Add a bug overload in the google demo --- docs/google_demo/__init__.py | 212 ++++++++++++++++++++++++++++++++++- 1 file changed, 211 insertions(+), 1 deletion(-) diff --git a/docs/google_demo/__init__.py b/docs/google_demo/__init__.py index c0c0908d9..853069434 100644 --- a/docs/google_demo/__init__.py +++ b/docs/google_demo/__init__.py @@ -34,7 +34,8 @@ https://google.github.io/styleguide/pyguide.html """ -from typing import List, Union # NOQA +from datetime import timedelta +from typing import Any, Awaitable, Callable, Concatenate, List, Mapping, Optional, Sequence, Union, overload # NOQA module_level_variable1 = 12345 @@ -298,3 +299,212 @@ class ExamplePEP526Class: attr1: str attr2: int + + +_not_in_the_demo = object() + +@overload +async def overwhelming_overload( + workflow: tuple[Any, Any], + *, + id: str, + task_queue: str, + execution_timeout: Optional[timedelta] = None, + run_timeout: Optional[timedelta] = None, + task_timeout: Optional[timedelta] = None, + id_reuse_policy: _not_in_the_demo = '_not_in_the_demo.WorkflowIDReusePolicy.ALLOW_DUPLICATE', + id_conflict_policy: _not_in_the_demo = '_not_in_the_demo.WorkflowIDConflictPolicy.UNSPECIFIED', + retry_policy: Optional[_not_in_the_demo.RetryPolicy] = None, + cron_schedule: str = "", + memo: Optional[Mapping[str, Any]] = None, + search_attributes: Optional[ + Union[ + _not_in_the_demo.TypedSearchAttributes, + _not_in_the_demo.SearchAttributes, + ] + ] = None, + start_delay: Optional[timedelta] = None, + start_signal: Optional[str] = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + request_eager_start: bool = False, +) -> tuple[Any, Any]: ... + +# Overload for single-param workflow +@overload +async def overwhelming_overload( + workflow: tuple[Any, Any, Any], + arg: Any, + *, + id: str, + task_queue: str, + execution_timeout: Optional[timedelta] = None, + run_timeout: Optional[timedelta] = None, + task_timeout: Optional[timedelta] = None, + id_reuse_policy: _not_in_the_demo.WorkflowIDReusePolicy = '_not_in_the_demo.WorkflowIDReusePolicy.ALLOW_DUPLICATE', + id_conflict_policy: _not_in_the_demo.WorkflowIDConflictPolicy = '_not_in_the_demo.WorkflowIDConflictPolicy.UNSPECIFIED', + retry_policy: Optional[_not_in_the_demo.RetryPolicy] = None, + cron_schedule: str = "", + memo: Optional[Mapping[str, Any]] = None, + search_attributes: Optional[ + Union[ + _not_in_the_demo.TypedSearchAttributes, + _not_in_the_demo.SearchAttributes, + ] + ] = None, + start_delay: Optional[timedelta] = None, + start_signal: Optional[str] = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + request_eager_start: bool = False, +) -> tuple[Any, Any]: ... + +# Overload for multi-param workflow +@overload +async def overwhelming_overload( + workflow: Callable[ + Concatenate[Any, Any], Awaitable[Any] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + execution_timeout: Optional[timedelta] = None, + run_timeout: Optional[timedelta] = None, + task_timeout: Optional[timedelta] = None, + id_reuse_policy: _not_in_the_demo.WorkflowIDReusePolicy = '_not_in_the_demo.WorkflowIDReusePolicy.ALLOW_DUPLICATE', + id_conflict_policy: _not_in_the_demo.WorkflowIDConflictPolicy = '_not_in_the_demo.WorkflowIDConflictPolicy.UNSPECIFIED', + retry_policy: Optional[_not_in_the_demo.RetryPolicy] = None, + cron_schedule: str = "", + memo: Optional[Mapping[str, Any]] = None, + search_attributes: Optional[ + Union[ + _not_in_the_demo.TypedSearchAttributes, + _not_in_the_demo.SearchAttributes, + ] + ] = None, + start_delay: Optional[timedelta] = None, + start_signal: Optional[str] = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + request_eager_start: bool = False, +) -> tuple[Any, Any]: ... + +# Overload for string-name workflow +@overload +async def overwhelming_overload( + workflow: str, + arg: Any = _not_in_the_demo._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: Optional[type] = None, + execution_timeout: Optional[timedelta] = None, + run_timeout: Optional[timedelta] = None, + task_timeout: Optional[timedelta] = None, + id_reuse_policy: _not_in_the_demo.WorkflowIDReusePolicy = '_not_in_the_demo.WorkflowIDReusePolicy.ALLOW_DUPLICATE', + id_conflict_policy: _not_in_the_demo.WorkflowIDConflictPolicy = '_not_in_the_demo.WorkflowIDConflictPolicy.UNSPECIFIED', + retry_policy: Optional[_not_in_the_demo.RetryPolicy] = None, + cron_schedule: str = "", + memo: Optional[Mapping[str, Any]] = None, + search_attributes: Optional[ + Union[ + _not_in_the_demo.TypedSearchAttributes, + _not_in_the_demo.SearchAttributes, + ] + ] = None, + start_delay: Optional[timedelta] = None, + start_signal: Optional[str] = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + request_eager_start: bool = False, +) -> tuple[Any, Any]: ... + +async def overwhelming_overload( + workflow: Union[str, Callable[..., Awaitable[Any]]], + arg: Any = _not_in_the_demo, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: Optional[type] = None, + execution_timeout: Optional[timedelta] = None, + run_timeout: Optional[timedelta] = None, + task_timeout: Optional[timedelta] = None, + id_reuse_policy: _not_in_the_demo.WorkflowIDReusePolicy = '_not_in_the_demo.WorkflowIDReusePolicy.ALLOW_DUPLICATE', + id_conflict_policy: _not_in_the_demo.WorkflowIDConflictPolicy = '_not_in_the_demo.WorkflowIDConflictPolicy.UNSPECIFIED', + retry_policy: Optional[_not_in_the_demo.RetryPolicy] = None, + cron_schedule: str = "", + memo: Optional[Mapping[str, Any]] = None, + search_attributes: Optional[ + Union[ + _not_in_the_demo.TypedSearchAttributes, + _not_in_the_demo.SearchAttributes, + ] + ] = None, + start_delay: Optional[timedelta] = None, + start_signal: Optional[str] = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + request_eager_start: bool = False, + stack_level: int = 2, +) -> tuple[Any, Any]: + """ + This is a big overload taken from the source code of temporalio sdk for Python. + The types don't make sens: it's only to showcase bigger overload. + + Start a workflow and return its handle. + + Args: + workflow: String name or class method decorated with + ``@workflow.run`` for the workflow to start. + arg: Single argument to the workflow. + args: Multiple arguments to the workflow. Cannot be set if arg is. + id: Unique identifier for the workflow execution. + task_queue: Task queue to run the workflow on. + result_type: For string workflows, this can set the specific result + type hint to deserialize into. + execution_timeout: Total workflow execution timeout including + retries and continue as new. + run_timeout: Timeout of a single workflow run. + task_timeout: Timeout of a single workflow task. + id_reuse_policy: How already-existing IDs are treated. + id_conflict_policy: How already-running workflows of the same ID are + treated. Default is unspecified which effectively means fail the + start attempt. This cannot be set if ``id_reuse_policy`` is set + to terminate if running. + retry_policy: Retry policy for the workflow. + cron_schedule: See https://docs.temporal.io/docs/content/what-is-a-temporal-cron-job/ + memo: Memo for the workflow. + search_attributes: Search attributes for the workflow. The + dictionary form of this is deprecated, use + :py:class:`_not_in_the_demo.TypedSearchAttributes`. + start_delay: Amount of time to wait before starting the workflow. + This does not work with ``cron_schedule``. + start_signal: If present, this signal is sent as signal-with-start + instead of traditional workflow start. + start_signal_args: Arguments for start_signal if start_signal + present. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + request_eager_start: Potentially reduce the latency to start this workflow by + encouraging the server to start it on a local worker running with + this same client. + This is currently experimental. + + Returns: + A workflow handle to the started workflow. + + Raises: + temporalio.exceptions.WorkflowAlreadyStartedError: Workflow has + already been started. + RPCError: Workflow could not be started for some other reason. + """ + ... \ No newline at end of file From cc82f10e9aaf1b9b8f2504b7c78c077bd926ed56 Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:20:41 -0500 Subject: [PATCH 30/38] Apply suggestions from code review --- docs/google_demo/__init__.py | 214 +-------------------- pydoctor/epydoc/markup/__init__.py | 2 +- pydoctor/epydoc/markup/restructuredtext.py | 2 - pydoctor/epydoc2stan.py | 13 +- pydoctor/themes/base/apidocs.css | 2 +- 5 files changed, 8 insertions(+), 225 deletions(-) diff --git a/docs/google_demo/__init__.py b/docs/google_demo/__init__.py index 853069434..b53c42e19 100644 --- a/docs/google_demo/__init__.py +++ b/docs/google_demo/__init__.py @@ -34,8 +34,7 @@ https://google.github.io/styleguide/pyguide.html """ -from datetime import timedelta -from typing import Any, Awaitable, Callable, Concatenate, List, Mapping, Optional, Sequence, Union, overload # NOQA +from typing import List, Union # NOQA module_level_variable1 = 12345 @@ -298,213 +297,4 @@ class ExamplePEP526Class: """ attr1: str - attr2: int - - -_not_in_the_demo = object() - -@overload -async def overwhelming_overload( - workflow: tuple[Any, Any], - *, - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: _not_in_the_demo = '_not_in_the_demo.WorkflowIDReusePolicy.ALLOW_DUPLICATE', - id_conflict_policy: _not_in_the_demo = '_not_in_the_demo.WorkflowIDConflictPolicy.UNSPECIFIED', - retry_policy: Optional[_not_in_the_demo.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - _not_in_the_demo.TypedSearchAttributes, - _not_in_the_demo.SearchAttributes, - ] - ] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, -) -> tuple[Any, Any]: ... - -# Overload for single-param workflow -@overload -async def overwhelming_overload( - workflow: tuple[Any, Any, Any], - arg: Any, - *, - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: _not_in_the_demo.WorkflowIDReusePolicy = '_not_in_the_demo.WorkflowIDReusePolicy.ALLOW_DUPLICATE', - id_conflict_policy: _not_in_the_demo.WorkflowIDConflictPolicy = '_not_in_the_demo.WorkflowIDConflictPolicy.UNSPECIFIED', - retry_policy: Optional[_not_in_the_demo.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - _not_in_the_demo.TypedSearchAttributes, - _not_in_the_demo.SearchAttributes, - ] - ] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, -) -> tuple[Any, Any]: ... - -# Overload for multi-param workflow -@overload -async def overwhelming_overload( - workflow: Callable[ - Concatenate[Any, Any], Awaitable[Any] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: _not_in_the_demo.WorkflowIDReusePolicy = '_not_in_the_demo.WorkflowIDReusePolicy.ALLOW_DUPLICATE', - id_conflict_policy: _not_in_the_demo.WorkflowIDConflictPolicy = '_not_in_the_demo.WorkflowIDConflictPolicy.UNSPECIFIED', - retry_policy: Optional[_not_in_the_demo.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - _not_in_the_demo.TypedSearchAttributes, - _not_in_the_demo.SearchAttributes, - ] - ] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, -) -> tuple[Any, Any]: ... - -# Overload for string-name workflow -@overload -async def overwhelming_overload( - workflow: str, - arg: Any = _not_in_the_demo._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: Optional[type] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: _not_in_the_demo.WorkflowIDReusePolicy = '_not_in_the_demo.WorkflowIDReusePolicy.ALLOW_DUPLICATE', - id_conflict_policy: _not_in_the_demo.WorkflowIDConflictPolicy = '_not_in_the_demo.WorkflowIDConflictPolicy.UNSPECIFIED', - retry_policy: Optional[_not_in_the_demo.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - _not_in_the_demo.TypedSearchAttributes, - _not_in_the_demo.SearchAttributes, - ] - ] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, -) -> tuple[Any, Any]: ... - -async def overwhelming_overload( - workflow: Union[str, Callable[..., Awaitable[Any]]], - arg: Any = _not_in_the_demo, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: Optional[type] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: _not_in_the_demo.WorkflowIDReusePolicy = '_not_in_the_demo.WorkflowIDReusePolicy.ALLOW_DUPLICATE', - id_conflict_policy: _not_in_the_demo.WorkflowIDConflictPolicy = '_not_in_the_demo.WorkflowIDConflictPolicy.UNSPECIFIED', - retry_policy: Optional[_not_in_the_demo.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - _not_in_the_demo.TypedSearchAttributes, - _not_in_the_demo.SearchAttributes, - ] - ] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - stack_level: int = 2, -) -> tuple[Any, Any]: - """ - This is a big overload taken from the source code of temporalio sdk for Python. - The types don't make sens: it's only to showcase bigger overload. - - Start a workflow and return its handle. - - Args: - workflow: String name or class method decorated with - ``@workflow.run`` for the workflow to start. - arg: Single argument to the workflow. - args: Multiple arguments to the workflow. Cannot be set if arg is. - id: Unique identifier for the workflow execution. - task_queue: Task queue to run the workflow on. - result_type: For string workflows, this can set the specific result - type hint to deserialize into. - execution_timeout: Total workflow execution timeout including - retries and continue as new. - run_timeout: Timeout of a single workflow run. - task_timeout: Timeout of a single workflow task. - id_reuse_policy: How already-existing IDs are treated. - id_conflict_policy: How already-running workflows of the same ID are - treated. Default is unspecified which effectively means fail the - start attempt. This cannot be set if ``id_reuse_policy`` is set - to terminate if running. - retry_policy: Retry policy for the workflow. - cron_schedule: See https://docs.temporal.io/docs/content/what-is-a-temporal-cron-job/ - memo: Memo for the workflow. - search_attributes: Search attributes for the workflow. The - dictionary form of this is deprecated, use - :py:class:`_not_in_the_demo.TypedSearchAttributes`. - start_delay: Amount of time to wait before starting the workflow. - This does not work with ``cron_schedule``. - start_signal: If present, this signal is sent as signal-with-start - instead of traditional workflow start. - start_signal_args: Arguments for start_signal if start_signal - present. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - request_eager_start: Potentially reduce the latency to start this workflow by - encouraging the server to start it on a local worker running with - this same client. - This is currently experimental. - - Returns: - A workflow handle to the started workflow. - - Raises: - temporalio.exceptions.WorkflowAlreadyStartedError: Workflow has - already been started. - RPCError: Workflow could not be started for some other reason. - """ - ... \ No newline at end of file + attr2: int \ No newline at end of file diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index 3737e30d5..224377443 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -385,7 +385,7 @@ def link_to(self, target: str, label: "Flattenable", *, is_annotation: bool = Fa should be linked to. @param label: The label to show for the link. @param is_annotation: Generated links will give precedence to the module - defined varaible rather the nested definitions when there are name colisions. + defined variables rather the nested definitions when there are name collisions. @return: The link, or just the label if the target was not found. """ diff --git a/pydoctor/epydoc/markup/restructuredtext.py b/pydoctor/epydoc/markup/restructuredtext.py index d6a6afc75..2fce5a0d9 100644 --- a/pydoctor/epydoc/markup/restructuredtext.py +++ b/pydoctor/epydoc/markup/restructuredtext.py @@ -136,8 +136,6 @@ def parsed_text(text: str) -> ParsedDocstring: set_node_attributes(document, children=[txt_node]) return ParsedRstDocstring(document, ()) -# not using cache here because we need new span tag for every call -# othewise it messes-up everything. @lru_cache() def parsed_text_with_css(text:str, css_class: str) -> ParsedDocstring: return parsed_text(text).with_tag(tags.span(class_=css_class)) diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index 255ac11e2..711f12e1d 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -1191,11 +1191,7 @@ def _colorize_signature_param(param: inspect.Parameter, has_next: bool, is_first: bool, ) -> ParsedDocstring: """ - One parameter is converted to a series of ParsedDocstrings. - - - one, the first, for the param name - - two others if the parameter is annotated: one for ': ' and one for the annotation - - two others if the paramter has a default value: one for ' = ' and one for the annotation + Convert a single parameter to a parsed docstring representation. """ kind = param.kind result: list[ParsedDocstring] = [] @@ -1203,11 +1199,10 @@ def _colorize_signature_param(param: inspect.Parameter, result.append(parsed_text(f'*{param.name}')) elif kind == _VAR_KEYWORD: result.append(parsed_text(f'**{param.name}')) + elif is_first and _is_less_important_param(param, ctx): + result.append(parsed_text_with_css(param.name, css_class='undocumented')) else: - if is_first and _is_less_important_param(param, ctx): - result.append(parsed_text_with_css(param.name, css_class='undocumented')) - else: - result.append(parsed_text(param.name)) + result.append(parsed_text(param.name)) # Add annotation and default value if param.annotation is not _empty: diff --git a/pydoctor/themes/base/apidocs.css b/pydoctor/themes/base/apidocs.css index 4c07e91a1..6387e6c0c 100644 --- a/pydoctor/themes/base/apidocs.css +++ b/pydoctor/themes/base/apidocs.css @@ -257,7 +257,7 @@ ul ul ul ul ul ul ul { word-break: break-word; /* It not seems to work with percentage values so I used px values, these are just indications for the - CSS auto layour table algo not to create tables + CSS auto layout table algo not to create tables with rather unbalanced columns width. */ max-width: 400px; } From 668f4d0c0b0f094b9bb544b6fd31722a0e511d62 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 13 Dec 2024 13:29:39 -0500 Subject: [PATCH 31/38] Fix the NotFoundLinker --- pydoctor/linker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydoctor/linker.py b/pydoctor/linker.py index a3773e81c..a68f4f69c 100644 --- a/pydoctor/linker.py +++ b/pydoctor/linker.py @@ -245,7 +245,7 @@ def _resolve_identifier_xref(self, class NotFoundLinker(DocstringLinker): """A DocstringLinker implementation that cannot find any links.""" - def link_to(self, target: str, label: "Flattenable") -> Tag: + def link_to(self, target: str, label: "Flattenable", *, is_annotation: bool = False) -> Tag: return tags.transparent(label) def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: From 7fc2b10f562f27bbc73674ac9aacd5f9eef0a709 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 13 Dec 2024 13:34:34 -0500 Subject: [PATCH 32/38] Do not mark overloaded functions with css class .long-signature --- pydoctor/templatewriter/pages/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index b158cac03..2568a261a 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -150,7 +150,12 @@ def format_function_def(func_name: str, is_async: bool, func_name = func_name[:func_name.rindex('.')] func_signature_css_class = 'function-signature' - if epydoc2stan.function_signature_len(func) > LONG_SIGNATURE: + + # We never mark the overloaded functions as long since this could make the output of pydoctor + # worst that before when there are many overloads to be wrapped. It allows to + # to scroll less to get to the actual main documentation of the function. + if not isinstance(func, model.FunctionOverload) and \ + epydoc2stan.function_signature_len(func) > LONG_SIGNATURE: func_signature_css_class += ' long-signature' r.extend([ From 80de0437974a0665e57ca84eb90a4cd87c9d1a58 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 13 Dec 2024 13:36:00 -0500 Subject: [PATCH 33/38] Remove unused imports --- pydoctor/epydoc/markup/_napoleon.py | 3 --- pydoctor/epydoc/markup/epytext.py | 2 +- pydoctor/epydoc/markup/plaintext.py | 5 +---- pydoctor/epydoc/markup/restructuredtext.py | 1 - 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/pydoctor/epydoc/markup/_napoleon.py b/pydoctor/epydoc/markup/_napoleon.py index 89d9638ed..d30758aaf 100644 --- a/pydoctor/epydoc/markup/_napoleon.py +++ b/pydoctor/epydoc/markup/_napoleon.py @@ -62,9 +62,6 @@ def _parse_docstring( errors: list[ParseError], docstring_cls: type[GoogleDocstring], ) -> ParsedDocstring: - # TODO: would be best to avoid this import - from pydoctor.model import Attribute - docstring_obj = docstring_cls( docstring, what=self.objclass, diff --git a/pydoctor/epydoc/markup/epytext.py b/pydoctor/epydoc/markup/epytext.py index 5bf37bcf3..414d8c75d 100644 --- a/pydoctor/epydoc/markup/epytext.py +++ b/pydoctor/epydoc/markup/epytext.py @@ -132,7 +132,7 @@ # 4. helpers # 5. testing -from typing import Any, Iterable, List, Optional, Sequence, Set, Union, cast, TYPE_CHECKING +from typing import Any, Iterable, List, Optional, Sequence, Set, Union, cast import re import unicodedata diff --git a/pydoctor/epydoc/markup/plaintext.py b/pydoctor/epydoc/markup/plaintext.py index 88067a1a9..8edef3135 100644 --- a/pydoctor/epydoc/markup/plaintext.py +++ b/pydoctor/epydoc/markup/plaintext.py @@ -12,7 +12,7 @@ __docformat__ = 'epytext en' -from typing import List, Optional, TYPE_CHECKING +from typing import List, Optional from docutils import nodes from twisted.web.template import Tag, tags @@ -21,9 +21,6 @@ from pydoctor.epydoc.docutils import set_node_attributes, new_document -if TYPE_CHECKING: - from pydoctor.model import Documentable - def parse_docstring(docstring: str, errors: List[ParseError]) -> ParsedDocstring: """ Parse the given docstring, which is formatted as plain text; and diff --git a/pydoctor/epydoc/markup/restructuredtext.py b/pydoctor/epydoc/markup/restructuredtext.py index c5b16b627..8e42a1924 100644 --- a/pydoctor/epydoc/markup/restructuredtext.py +++ b/pydoctor/epydoc/markup/restructuredtext.py @@ -45,7 +45,6 @@ from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Sequence, Set, cast if TYPE_CHECKING: from typing import TypeAlias - from pydoctor.model import Documentable import re from docutils import nodes From a9c5bf27c1e903e6689f76aaba6ff2ef4d679b70 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 13 Dec 2024 13:41:14 -0500 Subject: [PATCH 34/38] Add readme entries --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index c8a99a9d2..2d795ec23 100644 --- a/README.rst +++ b/README.rst @@ -74,6 +74,8 @@ in development ^^^^^^^^^^^^^^ * Drop support for Python 3.8. +* Signatures of function definitions are now wrapped onto several lines when the function has the focus. +* The first parameter of classmethods and methods (``cls`` or ``self``) is colored in gray so it's clear that these are not part of the API. pydoctor 24.11.1 ^^^^^^^^^^^^^^^^ From 6784e4cb88ae3c39363be8672883b5aa12ac51d2 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 13 Dec 2024 13:49:42 -0500 Subject: [PATCH 35/38] Upadate docs tests --- docs/tests/test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tests/test.py b/docs/tests/test.py index 0b957c1e0..d15bd4fe8 100644 --- a/docs/tests/test.py +++ b/docs/tests/test.py @@ -197,7 +197,8 @@ def test_search(query:str, expected:List[str], order_is_important:bool=True) -> 'pydoctor.epydoc.markup.plaintext.ParsedPlaintextDocstring.to_stan', 'pydoctor.epydoc.markup._types.ParsedTypeDocstring.to_stan', 'pydoctor.epydoc.markup._pyval_repr.ColorizedPyvalRepr.to_stan', - 'pydoctor.epydoc2stan.ParsedStanOnly.to_stan', + 'pydoctor.epydoc.markup._ParsedDocstringTree.to_stan', + 'pydoctor.epydoc.markup._ParsedDocstringWithTag.to_stan', ] test_search('to_stan*', to_stan_results, order_is_important=False) test_search('to_stan', to_stan_results, order_is_important=False) @@ -250,8 +251,7 @@ def test_missing_subclasses(): infos = ('pydoctor.epydoc.markup._types.ParsedTypeDocstring', 'pydoctor.epydoc.markup.epytext.ParsedEpytextDocstring', 'pydoctor.epydoc.markup.plaintext.ParsedPlaintextDocstring', - 'pydoctor.epydoc.markup.restructuredtext.ParsedRstDocstring', - 'pydoctor.epydoc2stan.ParsedStanOnly', ) + 'pydoctor.epydoc.markup.restructuredtext.ParsedRstDocstring', ) with open(BASE_DIR / 'api' / 'pydoctor.epydoc.markup.ParsedDocstring.html', 'r', encoding='utf-8') as stream: page = stream.read() From 78f73b950cc48b0560096d1e91967a15c635d44f Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 13 Dec 2024 13:53:36 -0500 Subject: [PATCH 36/38] Like back, consider a function long from 88 chars. --- pydoctor/templatewriter/pages/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 2568a261a..80394e6da 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -114,7 +114,7 @@ def format_class_signature(cls: model.Class) -> "Flattenable": r.append(')') return r -LONG_SIGNATURE = 120 # this doesn't acount for the 'def ' and the ending ':' +LONG_SIGNATURE = 88 # this doesn't acount for the 'def ' and the ending ':' """ Maximum size of a function definition to be rendered on a single line. The multiline formatting is only applied at the CSS level to stay customizable. From bf7045f32bdcd72503460645577aa791d8408f73 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Fri, 13 Dec 2024 13:56:44 -0500 Subject: [PATCH 37/38] Adjust test again --- docs/tests/test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tests/test.py b/docs/tests/test.py index d15bd4fe8..b9f9eef53 100644 --- a/docs/tests/test.py +++ b/docs/tests/test.py @@ -209,7 +209,8 @@ def test_search(query:str, expected:List[str], order_is_important:bool=True) -> 'pydoctor.epydoc.markup._types.ParsedTypeDocstring.to_node', 'pydoctor.epydoc.markup.restructuredtext.ParsedRstDocstring.to_node', 'pydoctor.epydoc.markup.epytext.ParsedEpytextDocstring.to_node', - 'pydoctor.epydoc2stan.ParsedStanOnly.to_node', + 'pydoctor.epydoc.markup._ParsedDocstringTree.to_node', + 'pydoctor.epydoc.markup._ParsedDocstringWithTag.to_node', ] test_search('to_node*', to_node_results, order_is_important=False) test_search('to_node', to_node_results, order_is_important=False) From a37b028b3b5d1efbb2383e1fd08a407477866ed0 Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:40:14 -0500 Subject: [PATCH 38/38] Update README.rst --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 2d795ec23..1ed61ab0a 100644 --- a/README.rst +++ b/README.rst @@ -76,6 +76,7 @@ in development * Drop support for Python 3.8. * Signatures of function definitions are now wrapped onto several lines when the function has the focus. * The first parameter of classmethods and methods (``cls`` or ``self``) is colored in gray so it's clear that these are not part of the API. +* When pydoctor encounters an invalid signature, it shows (…) as the signature instead of the misleading zero argument signature. pydoctor 24.11.1 ^^^^^^^^^^^^^^^^