Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap signatures onto several lines when function len is over a treshold #831

Open
wants to merge 42 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
dea121c
Introduce ParsedDocstring.with_linker()/.with_tag()/.combine(). The w…
tristanlatr Oct 25, 2024
95f8f3d
Colorize the signature ourself.
tristanlatr Oct 25, 2024
0b7c76e
Some more adjustments. Make the self param always inline. Try to opti…
tristanlatr Oct 25, 2024
188c410
Add comment
tristanlatr Oct 25, 2024
83d47f7
Fix usage of cache
tristanlatr Oct 25, 2024
ab1cdd1
Fix usages of cache
tristanlatr Oct 25, 2024
eca5ced
Simplify with_linker() and with_tag(). These do not create new parsed…
tristanlatr Oct 25, 2024
ff4269f
Revert "Simplify with_linker() and with_tag(). These do not create ne…
tristanlatr Oct 25, 2024
914e01c
Minor changes not to use lru_cache too much
tristanlatr Oct 25, 2024
cdef965
Try to optimize what I can
tristanlatr Oct 25, 2024
2033d65
Fix mypy
tristanlatr Oct 26, 2024
5396396
Merge branch 'master' into 801-signature-spans
tristanlatr Oct 26, 2024
282250b
Remove unused imports
tristanlatr Oct 26, 2024
6a4de9f
Better implementation of with_linker and with_tag inside a single sub…
tristanlatr Oct 29, 2024
c0f93dc
First attempt to implement relatively smart Expand/Collapse signature…
tristanlatr Oct 29, 2024
da89d7c
Simplify things: don't try to wrap overload signatures. Sphinx doesn'…
tristanlatr Nov 14, 2024
141b211
Get rid of the ParsedStanOnly by using parsed_text_with_css instead.
tristanlatr Nov 14, 2024
a46a3a3
Few simplifications here and there.
tristanlatr Nov 14, 2024
40ac0a6
Use the CSS class 'decorator' for all decorators.
tristanlatr Nov 14, 2024
4172485
Fix various bugs in the implementation.
tristanlatr Nov 14, 2024
7103ce5
Fix pyflakes
tristanlatr Nov 14, 2024
eae961a
Fix format_undocumented_summary returning a tuple of strings instead …
tristanlatr Nov 14, 2024
7c6c6eb
increase the threshold for a function to be rendered in several lines.
tristanlatr Nov 14, 2024
19400ff
Avoid an empty div for decorators when there are no decorators.
tristanlatr Nov 14, 2024
a3ebbdf
Use non breaking spaces in sugnature defs.
tristanlatr Nov 14, 2024
cd257eb
Improve a little bit the rendering of parameter tables that uses very…
tristanlatr Nov 15, 2024
907792a
Get rid of the AnnotationLinker - drop the verbose messages when an a…
tristanlatr Nov 16, 2024
977e5b5
Merge branch 'master' into 801-signature-spans
tristanlatr Nov 16, 2024
91edc51
Change comment
tristanlatr Nov 18, 2024
b504c21
Merge branch '801-signature-spans' of github.com:twisted/pydoctor int…
tristanlatr Nov 18, 2024
25b5e62
Add an environment to build temporalio docs
tristanlatr Nov 21, 2024
bd2de92
Add a bug overload in the google demo
tristanlatr Nov 21, 2024
cc82f10
Apply suggestions from code review
tristanlatr Dec 13, 2024
07fc41d
Merge branch 'master' into 801-signature-spans
tristanlatr Dec 13, 2024
668f4d0
Fix the NotFoundLinker
tristanlatr Dec 13, 2024
7fc2b10
Do not mark overloaded functions with css class .long-signature
tristanlatr Dec 13, 2024
80de043
Remove unused imports
tristanlatr Dec 13, 2024
a9c5bf2
Add readme entries
tristanlatr Dec 13, 2024
6784e4c
Upadate docs tests
tristanlatr Dec 13, 2024
78f73b9
Like back, consider a function long from 88 chars.
tristanlatr Dec 13, 2024
bf7045f
Adjust test again
tristanlatr Dec 13, 2024
a37b028
Update README.rst
tristanlatr Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 12 additions & 47 deletions pydoctor/astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
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)

class InvalidSignatureParamName(str):
def isidentifier(self) -> bool:
return True

def parseFile(path: Path) -> ast.Module:
"""Parse the contents of a Python source file."""
Expand Down Expand Up @@ -1032,9 +1034,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):
Expand All @@ -1056,12 +1058,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

Expand Down Expand Up @@ -1120,7 +1125,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
Expand Down Expand Up @@ -1153,47 +1158,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 <code> 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 <code> tags.
"""
return '<code>%s</code>' % super().__repr__()

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

Expand Down
144 changes: 126 additions & 18 deletions pydoctor/epydoc/markup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
from __future__ import annotations
__docformat__ = 'epytext en'

from typing import Callable, ContextManager, List, Optional, Sequence, Iterator, TYPE_CHECKING
import contextlib
from itertools import chain
from typing import Callable, ContextManager, Iterable, List, Optional, Sequence, Iterator, TYPE_CHECKING
import abc
import sys
import re
Expand Down Expand Up @@ -129,7 +131,7 @@
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.
Expand All @@ -143,11 +145,10 @@
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

@abc.abstractproperty
@property
@abc.abstractmethod
def has_body(self) -> bool:
"""
Does this docstring have a non-empty body?
Expand All @@ -168,7 +169,6 @@
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
Expand Down Expand Up @@ -203,25 +203,115 @@
"""
raise NotImplementedError()

def to_text(self) -> str:
"""
Translate this docstring to a string.
The default implementation depends on L{to_node}.
"""
doc = self.to_node()
return ''.join(node2stan.gettext(doc))

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.
"""
return _ParsedDocstringWithTag(self, tag)

@classmethod
def combine(cls, elements: Sequence[ParsedDocstring]) -> ParsedDocstring:
"""
Combine the contents of several parsed docstrings into one.
"""
return _ParsedDocstringTree(elements)

def get_summary(self) -> 'ParsedDocstring':
"""
Returns the summary of this docstring.

@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:
_document = self.to_node()
visitor = SummaryExtractor(_document)
_document.walk(visitor)
except Exception:
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
return parsed_text_with_css('Broken summary', 'undocumented')

Check warning on line 240 in pydoctor/epydoc/markup/__init__.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/epydoc/markup/__init__.py#L240

Added line #L240 was not covered by tests

return visitor.summary or parsed_text_with_css('No summary', 'undocumented')


class _ParsedDocstringTree(ParsedDocstring):
"""
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: nodes.document | None = None

@property
def has_body(self) -> bool:
return any(e.has_body for e in self._elements)

@classmethod
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().
# 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

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:
tristanlatr marked this conversation as resolved.
Show resolved Hide resolved
stan = tags.transparent()
for e in self._elements:
stan(e.to_stan(linker).children)
return stan

class _ParsedDocstringWithTag(ParsedDocstring):
"""
Wraps a parsed docstring to wrap the result of the
the to_stan() method inside a custom Tag.
"""
def __init__(self,
other: ParsedDocstring,
tag: Tag):
super().__init__(other.fields)
self.wrapped = other
"""
The wrapped parsed docstring.
"""
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

Check warning on line 314 in pydoctor/epydoc/markup/__init__.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/epydoc/markup/__init__.py#L314

Added line #L314 was not covered by tests


##################################################
Expand Down Expand Up @@ -286,14 +376,16 @@
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.

@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.
tristanlatr marked this conversation as resolved.
Show resolved Hide resolved
@return: The link, or just the label if the target was not found.
"""

Expand Down Expand Up @@ -325,6 +417,20 @@
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", *, is_annotation: bool = False) -> 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

Check warning on line 431 in pydoctor/epydoc/markup/__init__.py

View check run for this annotation

Codecov / codecov/patch

pydoctor/epydoc/markup/__init__.py#L431

Added line #L431 was not covered by tests


##################################################
## ParseError exceptions
##################################################
Expand Down Expand Up @@ -475,11 +581,13 @@
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:
raise nodes.SkipNode()

def unknown_visit(self, node: nodes.Node) -> None:
'''Ignore all unknown nodes'''


from pydoctor.epydoc.markup.restructuredtext import ParsedRstDocstring, parsed_text_with_css
8 changes: 6 additions & 2 deletions pydoctor/epydoc/markup/_napoleon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading