Skip to content

Commit

Permalink
Merge commit 'b5c5fedaf50c520e3847f98d35869bd5fe6967b1' into 846-typi…
Browse files Browse the repository at this point in the history
…ng-Generic-in-MRO
  • Loading branch information
tristanlatr committed Dec 12, 2024
2 parents 8d46b2b + b5c5fed commit 76841e2
Show file tree
Hide file tree
Showing 6 changed files with 774 additions and 60 deletions.
5 changes: 1 addition & 4 deletions pydoctor/astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ def parseFile(path: Path) -> ast.Module:
src = f.read() + b'\n'
return _parse(src, filename=str(path))

if sys.version_info >= (3,8):
_parse = partial(ast.parse, type_comments=True)
else:
_parse = ast.parse
_parse = partial(ast.parse, type_comments=True)

def _maybeAttribute(cls: model.Class, name: str) -> bool:
"""Check whether a name is a potential attribute of the given class.
Expand Down
53 changes: 9 additions & 44 deletions pydoctor/astutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from __future__ import annotations

import inspect
import platform
import sys
from numbers import Number
from typing import Any, Callable, Collection, Iterator, Optional, List, Iterable, Sequence, TYPE_CHECKING, Tuple, Union, cast
Expand Down Expand Up @@ -232,11 +231,7 @@ def get_assign_docstring_node(assign:ast.Assign | ast.AnnAssign) -> Str | None:

def is_none_literal(node: ast.expr) -> bool:
"""Does this AST node represent the literal constant None?"""
if sys.version_info >= (3,8):
return isinstance(node, ast.Constant) and node.value is None
else:
# TODO: remove me when python3.7 is not supported anymore
return isinstance(node, (ast.Constant, ast.NameConstant)) and node.value is None
return isinstance(node, ast.Constant) and node.value is None

def unstring_annotation(node: ast.expr, ctx:'model.Documentable', section:str='annotation') -> ast.expr:
"""Replace all strings in the given expression by parsed versions.
Expand Down Expand Up @@ -489,23 +484,11 @@ def get_docstring_node(node: ast.AST) -> Str | None:
return node.value
return None

_string_lineno_is_end = sys.version_info < (3,8) \
and platform.python_implementation() != 'PyPy'
"""True iff the 'lineno' attribute of an AST string node points to the last
line in the string, rather than the first line.
"""


class _StrMeta(type):
if sys.version_info >= (3,8):
def __instancecheck__(self, instance: object) -> bool:
if isinstance(instance, ast.expr):
return get_str_value(instance) is not None
return False
else:
# TODO: remove me when python3.7 is not supported
def __instancecheck__(self, instance: object) -> bool:
return isinstance(instance, ast.Str)
def __instancecheck__(self, instance: object) -> bool:
if isinstance(instance, ast.expr):
return get_str_value(instance) is not None
return False

class Str(ast.expr, metaclass=_StrMeta):
"""
Expand All @@ -514,15 +497,11 @@ class Str(ast.expr, metaclass=_StrMeta):
Do not try to instanciate this class.
"""

value: str

def __init__(self, *args: Any, **kwargs: Any) -> None:
raise TypeError(f'{Str.__qualname__} cannot be instanciated')

if sys.version_info >= (3,8):
value: str
else:
# TODO: remove me when python3.7 is not supported
s: str

def extract_docstring_linenum(node: Str) -> int:
r"""
In older CPython versions, the AST only tells us the end line
Expand All @@ -533,18 +512,8 @@ def extract_docstring_linenum(node: Str) -> int:
Leading blank lines are stripped by cleandoc(), so we must
return the line number of the first non-blank line.
"""
if sys.version_info >= (3,8):
doc = node.value
else:
# TODO: remove me when python3.7 is not supported
doc = node.s
doc = node.value
lineno = node.lineno
if _string_lineno_is_end:
# In older CPython versions, the AST only tells us the end line
# number and we must approximate the start line number.
# This approximation is correct if the docstring does not contain
# explicit newlines ('\n') or joined lines ('\' at end of line).
lineno -= doc.count('\n')

# Leading blank lines are stripped by cleandoc(), so we must
# return the line number of the first non-blank line.
Expand All @@ -564,11 +533,7 @@ def extract_docstring(node: Str) -> Tuple[int, str]:
- The line number of the first non-blank line of the docsring. See L{extract_docstring_linenum}.
- The docstring to be parsed, cleaned by L{inspect.cleandoc}.
"""
if sys.version_info >= (3,8):
value = node.value
else:
# TODO: remove me when python3.7 is not supported
value = node.s
value = node.value
lineno = extract_docstring_linenum(node)
return lineno, inspect.cleandoc(value)

Expand Down
9 changes: 1 addition & 8 deletions pydoctor/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from collections import defaultdict
import datetime
import importlib
import platform
import sys
import textwrap
import types
Expand Down Expand Up @@ -57,12 +56,6 @@
# Functions can't contain anything.


_string_lineno_is_end = sys.version_info < (3,8) \
and platform.python_implementation() != 'PyPy'
"""True iff the 'lineno' attribute of an AST string node points to the last
line in the string, rather than the first line.
"""

class LineFromAst(int):
"Simple L{int} wrapper for linenumbers coming from ast analysis."

Expand Down Expand Up @@ -707,7 +700,7 @@ def _compute_mro(self, cls: Class) -> list[ClassOrStr]:
# support documenting typing.py module by using allobject.get.
generic = cls.system.allobjects.get(_d:='typing.Generic', _d)
if generic in bases and any(generic in _mro for _mro in bases_mros):
# this is cafe since we checked 'generic in bases'.
# this is safe since we checked 'generic in bases'.
bases.remove(generic) # type: ignore[arg-type]

try:
Expand Down
10 changes: 6 additions & 4 deletions pydoctor/test/test_configparser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from io import StringIO
from typing import Any, Dict, List
import requests
from pathlib import Path

from pydoctor._configparser import parse_toml_section_name, is_quoted, unquote_str, IniConfigParser, TomlConfigParser

Expand Down Expand Up @@ -32,9 +32,11 @@ def test_unquote_str() -> None:
assert unquote_str('""""value""""') == '""""value""""'

def test_unquote_naughty_quoted_strings() -> None:
# See https://github.com/minimaxir/big-list-of-naughty-strings/blob/master/blns.txt
res = requests.get('https://raw.githubusercontent.com/minimaxir/big-list-of-naughty-strings/master/blns.txt')
text = res.text
# See https://github.com/minimaxir/big-list-of-naughty-strings

text = Path(__file__).parent.joinpath('unquote_test_strings.txt'
).read_text(encoding='utf-8', errors='replace')

for i, string in enumerate(text.split('\n')):
if string.strip().startswith('#'):
continue
Expand Down
15 changes: 15 additions & 0 deletions pydoctor/test/test_mro.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,21 @@ class C(A, Generic[T], B[T]): ...
assert_mro_equals(mod.contents['C'],
["t.C", "t.A", "t.B", "typing.Generic"])

def test_mro_generic_in_system(capsys:CapSys) -> None:
src = '''
class TypeVar:...
class Generic: ...
T = TypeVar('T')
class A: ...
class B(Generic[T]): ...
class C(A, Generic[T], B[T]): ...
'''
mod = fromText(src, modname='typing')
assert not capsys.readouterr().out
assert_mro_equals(mod.contents['C'],
["typing.C", "typing.A", "typing.B", "typing.Generic"])


def test_mro_generic_4(capsys:CapSys) -> None:
src = '''
from typing import Generic, TypeVar
Expand Down
Loading

0 comments on commit 76841e2

Please sign in to comment.