Merge branch '3.4.x' into 3.x

This commit is contained in:
Takeshi KOMIYA
2020-12-23 23:18:06 +09:00
10 changed files with 82 additions and 8 deletions

View File

@@ -37,6 +37,13 @@ Features added
Bugs fixed
----------
* #8559: autodoc: AttributeError is raised when using forward-reference type
annotations
* #8568: autodoc: TypeError is raised on checking slots attribute
* #8567: autodoc: Instance attributes are incorrectly added to Parent class
* #8565: linkcheck: Fix PriorityQueue crash when link tuples are not
comparable
Testing
--------

View File

@@ -22,7 +22,7 @@ from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple
from urllib.parse import unquote, urlparse
from docutils import nodes
from docutils.nodes import Node
from docutils.nodes import Element, Node
from requests import Response
from requests.exceptions import HTTPError, TooManyRedirects
@@ -47,6 +47,14 @@ QUEUE_POLL_SECS = 1
DEFAULT_DELAY = 60.0
def node_line_or_0(node: Element) -> int:
"""
PriorityQueue items must be comparable. The line number is part of the
tuple used by the PriorityQueue, keep an homogeneous type for comparison.
"""
return get_node_line(node) or 0
class AnchorCheckParser(HTMLParser):
"""Specialized HTML parser that looks for a specific anchor."""
@@ -406,7 +414,7 @@ class CheckExternalLinksBuilder(Builder):
if 'refuri' not in refnode:
continue
uri = refnode['refuri']
lineno = get_node_line(refnode)
lineno = node_line_or_0(refnode)
uri_info = (CHECK_IMMEDIATELY, uri, docname, lineno)
self.wqueue.put(uri_info, False)
n += 1
@@ -415,7 +423,7 @@ class CheckExternalLinksBuilder(Builder):
for imgnode in doctree.traverse(nodes.image):
uri = imgnode['candidates'].get('?')
if uri and '://' in uri:
lineno = get_node_line(imgnode)
lineno = node_line_or_0(imgnode)
uri_info = (CHECK_IMMEDIATELY, uri, docname, lineno)
self.wqueue.put(uri_info, False)
n += 1

View File

@@ -1857,13 +1857,14 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
def update_annotations(self, parent: Any) -> None:
"""Update __annotations__ to support type_comment and so on."""
try:
annotations = inspect.getannotations(parent)
annotations = dict(inspect.getannotations(parent))
parent.__annotations__ = annotations
analyzer = ModuleAnalyzer.for_module(self.modname)
analyzer.analyze()
for (classname, attrname), annotation in analyzer.annotations.items():
if classname == '' and attrname not in annotations:
annotations[attrname] = annotation # type: ignore
annotations[attrname] = annotation
except AttributeError:
pass
@@ -2115,7 +2116,7 @@ class SlotsMixin(DataDocumenterMixinBase):
return True
else:
return False
except (AttributeError, ValueError):
except (AttributeError, ValueError, TypeError):
return False
def import_object(self, raiseerror: bool = False) -> bool:
@@ -2295,7 +2296,8 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
def update_annotations(self, parent: Any) -> None:
"""Update __annotations__ to support type_comment and so on."""
try:
annotations = inspect.getannotations(parent)
annotations = dict(inspect.getannotations(parent))
parent.__annotations__ = annotations
for cls in inspect.getmro(parent):
try:
@@ -2306,11 +2308,14 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
analyzer.analyze()
for (classname, attrname), annotation in analyzer.annotations.items():
if classname == qualname and attrname not in annotations:
annotations[attrname] = annotation # type: ignore
annotations[attrname] = annotation
except (AttributeError, PycodeError):
pass
except AttributeError:
pass
except TypeError:
# Failed to set __annotations__ (built-in, extensions, etc.)
pass
def import_object(self, raiseerror: bool = False) -> bool:
try:

View File

@@ -187,6 +187,7 @@ def getslots(obj: Any) -> Optional[Dict]:
Return None if gienv *obj* does not have __slots__.
Raises AttributeError if given *obj* raises an error on accessing __slots__.
Raises TypeError if given *obj* is not a class.
Raises ValueError if given *obj* have invalid __slots__.
"""
if not inspect.isclass(obj):

View File

@@ -153,6 +153,8 @@ def _restify_py37(cls: Optional["Type"]) -> str:
return ':obj:`%s`' % cls._name
else:
return ':obj:`%s.%s`' % (cls.__module__, cls._name)
elif isinstance(cls, ForwardRef):
return ':class:`%s`' % cls.__forward_arg__
else:
# not a class (ex. TypeVar)
return ':obj:`%s.%s`' % (cls.__module__, cls.__name__)

View File

@@ -0,0 +1 @@
exclude_patterns = ['_build']

View File

@@ -0,0 +1,6 @@
.. image:: http://localhost:7777/
:target: http://localhost:7777/
`weblate.org`_
.. _weblate.org: http://localhost:7777/

View File

@@ -573,3 +573,40 @@ def test_limit_rate_bails_out_after_waiting_max_time(app):
checker.rate_limits = {"localhost": RateLimit(90.0, 0.0)}
next_check = checker.limit_rate(FakeResponse())
assert next_check is None
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-localserver-two-links', freshenv=True,
)
def test_priorityqueue_items_are_comparable(app):
with http_server(OKHandler):
app.builder.build_all()
content = (app.outdir / 'output.json').read_text()
rows = [json.loads(x) for x in sorted(content.splitlines())]
assert rows == [
{
'filename': 'index.rst',
# Should not be None.
'lineno': 0,
'status': 'working',
'code': 0,
'uri': 'http://localhost:7777/',
'info': '',
},
{
'filename': 'index.rst',
'lineno': 0,
'status': 'working',
'code': 0,
'uri': 'http://localhost:7777/',
'info': '',
},
{
'filename': 'index.rst',
'lineno': 4,
'status': 'working',
'code': 0,
'uri': 'http://localhost:7777/',
'info': '',
}
]

View File

@@ -690,6 +690,7 @@ def test_autodoc_special_members(app):
actual = do_autodoc(app, 'class', 'target.Class', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)',
' .. py:attribute:: Class.__annotations__',
' .. py:attribute:: Class.__dict__',
' .. py:method:: Class.__init__(arg)',
' .. py:attribute:: Class.__module__',

View File

@@ -109,6 +109,12 @@ def test_restify_type_hints_alias():
assert restify(MyTuple) == ":class:`Tuple`\\ [:class:`str`, :class:`str`]" # type: ignore
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
def test_restify_type_ForwardRef():
from typing import ForwardRef # type: ignore
assert restify(ForwardRef("myint")) == ":class:`myint`"
def test_restify_broken_type_hints():
assert restify(BrokenType) == ':class:`tests.test_util_typing.BrokenType`'