mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch '3.4.x' into 3.x
This commit is contained in:
7
CHANGES
7
CHANGES
@@ -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
|
||||
--------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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__)
|
||||
|
||||
1
tests/roots/test-linkcheck-localserver-two-links/conf.py
Normal file
1
tests/roots/test-linkcheck-localserver-two-links/conf.py
Normal file
@@ -0,0 +1 @@
|
||||
exclude_patterns = ['_build']
|
||||
@@ -0,0 +1,6 @@
|
||||
.. image:: http://localhost:7777/
|
||||
:target: http://localhost:7777/
|
||||
|
||||
`weblate.org`_
|
||||
|
||||
.. _weblate.org: http://localhost:7777/
|
||||
@@ -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': '',
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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__',
|
||||
|
||||
@@ -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`'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user