summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakeshi KOMIYA <i.tkomiya@gmail.com>2020-11-21 01:01:36 +0900
committerTakeshi KOMIYA <i.tkomiya@gmail.com>2020-11-21 01:01:36 +0900
commit3a4ae2092a6f236e2d71fdedb4b66f594e30d4e7 (patch)
treeafa4f774e0f8660d5ce247f01cace9deaa848d58
parentf3a6004f822b05e352372a77f449332ad230d21e (diff)
parent18b2707b2ac621f23f8ee6a7ca19bf1590be7826 (diff)
downloadsphinx-git-3a4ae2092a6f236e2d71fdedb4b66f594e30d4e7.tar.gz
Merge branch '3.x'
-rw-r--r--.github/workflows/main.yml2
-rw-r--r--CHANGES27
-rw-r--r--doc/extdev/deprecated.rst10
-rw-r--r--doc/usage/extensions/autodoc.rst12
-rw-r--r--setup.cfg4
-rw-r--r--sphinx/application.py6
-rw-r--r--sphinx/builders/html/__init__.py1
-rw-r--r--sphinx/builders/linkcheck.py15
-rw-r--r--sphinx/ext/autodoc/__init__.py144
-rw-r--r--sphinx/ext/autodoc/importer.py19
-rw-r--r--sphinx/ext/autosummary/generate.py6
-rw-r--r--sphinx/themes/basic/search.html1
-rw-r--r--sphinx/util/inspect.py45
-rw-r--r--sphinx/util/requests.py5
-rw-r--r--sphinx/util/typing.py23
-rw-r--r--tests/certs/cert.pem50
-rw-r--r--tests/roots/test-ext-autodoc/target/annotations.py10
-rw-r--r--tests/roots/test-linkcheck-localserver-anchor/conf.py2
-rw-r--r--tests/roots/test-linkcheck-localserver-anchor/index.rst1
-rw-r--r--tests/roots/test-linkcheck-localserver-https/conf.py1
-rw-r--r--tests/roots/test-linkcheck-localserver-https/index.rst1
-rw-r--r--tests/roots/test-linkcheck-localserver/conf.py1
-rw-r--r--tests/roots/test-linkcheck-localserver/index.rst2
-rw-r--r--tests/test_build_latex.py4
-rw-r--r--tests/test_build_linkcheck.py287
-rw-r--r--tests/test_domain_c.py17
-rw-r--r--tests/test_domain_cpp.py21
-rw-r--r--tests/test_domain_std.py2
-rw-r--r--tests/test_environment_indexentries.py10
-rw-r--r--tests/test_ext_autodoc.py4
-rw-r--r--tests/test_ext_autodoc_autoattribute.py71
-rw-r--r--tests/test_ext_autodoc_autodata.py74
-rw-r--r--tests/test_ext_autodoc_configs.py40
-rw-r--r--tests/test_ext_intersphinx.py1
-rw-r--r--tests/test_ext_napoleon_docstring.py4
-rw-r--r--tests/test_intl.py35
-rw-r--r--tests/test_markup.py1
-rw-r--r--tests/test_project.py1
-rw-r--r--tests/test_pycode.py1
-rw-r--r--tests/test_util.py3
-rw-r--r--tests/test_util_inspect.py24
-rw-r--r--tests/test_util_typing.py9
-rw-r--r--tests/typing_test_data.py2
-rw-r--r--tests/utils.py48
44 files changed, 825 insertions, 222 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index a63a76a7f..bf58415a6 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -41,6 +41,8 @@ jobs:
if: endsWith(matrix.python, '-dev')
with:
python-version: ${{ matrix.python }}
+ env:
+ ACTIONS_ALLOW_UNSECURE_COMMANDS: true
- name: Check Python version
run: python --version
- name: Install graphviz
diff --git a/CHANGES b/CHANGES
index fe5818615..b7d9c4de3 100644
--- a/CHANGES
+++ b/CHANGES
@@ -68,6 +68,8 @@ Deprecated
----------
* The ``follow_wrapped`` argument of ``sphinx.util.inspect.signature()``
+* ``sphinx.ext.autodoc.DataDeclarationDocumenter``
+* ``sphinx.util.requests.is_ssl_error()``
Features added
--------------
@@ -80,6 +82,8 @@ Features added
* autodoc: Add ``Documenter.config`` as a shortcut to access the config object
* autodoc: Add Optional[t] to annotation of function and method if a default
value equal to None is set.
+* #8209: autodoc: Add ``:no-value:`` option to :rst:dir:`autoattribute` and
+ :rst:dir:`autodata` directive to suppress the default value of the variable
* #6914: Add a new event :event:`warn-missing-reference` to custom warning
messages when failed to resolve a cross-reference
* #6914: Emit a detailed warning when failed to resolve a ``:ref:`` reference
@@ -91,11 +95,18 @@ Bugs fixed
* #4606: autodoc: the location of the warning is incorrect for inherited method
* #8105: autodoc: the signature of class constructor is incorrect if the class
is decorated
+* #8434: autodoc: :confval:`autodoc_type_aliases` does not effect to variables
+ and attributes
+* #8443: autodoc: autodata directive can't create document for PEP-526 based
+ type annotated variables
+* #8443: autodoc: autoattribute directive can't create document for PEP-526
+ based uninitalized variables
+* #8419: html search: Do not load ``language_data.js`` in non-search pages
Testing
--------
-Release 3.3.1 (in development)
+Release 3.3.2 (in development)
==============================
Dependencies
@@ -113,13 +124,21 @@ Features added
Bugs fixed
----------
+Testing
+--------
+
+Release 3.3.1 (released Nov 12, 2020)
+=====================================
+
+Bugs fixed
+----------
+
* #8372: autodoc: autoclass directive became slower than Sphinx-3.2
* #7727: autosummary: raise PycodeError when documenting python package
without __init__.py
+* #8350: autosummary: autosummary_mock_imports causes slow down builds
* #8364: C, properly initialize attributes in empty symbols.
-
-Testing
---------
+* #8399: i18n: Put system locale path after the paths specified by configuration
Release 3.3.0 (released Nov 02, 2020)
=====================================
diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst
index a206bc09b..da086d050 100644
--- a/doc/extdev/deprecated.rst
+++ b/doc/extdev/deprecated.rst
@@ -61,6 +61,16 @@ The following is a list of deprecated interfaces.
- 5.0
- N/A
+ * - ``sphinx.ext.autodoc.DataDeclarationDocumenter``
+ - 3.4
+ - 5.0
+ - ``sphinx.ext.autodoc.DataDocumenter``
+
+ * - ``sphinx.util.requests.is_ssl_error()``
+ - 3.4
+ - 5.0
+ - N/A
+
* - ``sphinx.builders.latex.LaTeXBuilder.usepackages``
- 3.3
- 5.0
diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst
index 86df8a79f..2f27e6574 100644
--- a/doc/usage/extensions/autodoc.rst
+++ b/doc/usage/extensions/autodoc.rst
@@ -326,6 +326,15 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,
By default, without ``annotation`` option, Sphinx tries to obtain the value of
the variable and print it after the name.
+ The ``no-value`` option can be used instead of a blank ``annotation`` to show the
+ type hint but not the value::
+
+ .. autodata:: CD_DRIVE
+ :no-value:
+
+ If both the ``annotation`` and ``no-value`` options are used, ``no-value`` has no
+ effect.
+
For module data members and class attributes, documentation can either be put
into a comment with special formatting (using a ``#:`` to start the comment
instead of just ``#``), or in a docstring *after* the definition. Comments
@@ -365,6 +374,9 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,
option.
.. versionchanged:: 2.0
:rst:dir:`autodecorator` added.
+ .. versionchanged:: 3.4
+ :rst:dir:`autodata` and :rst:dir:`autoattribute` now have a ``no-value``
+ option.
.. note::
diff --git a/setup.cfg b/setup.cfg
index f59829d88..37a6d41f3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -29,9 +29,11 @@ directory = sphinx/locale/
[flake8]
max-line-length = 95
ignore = E116,E241,E251,E741,W504,I101
-exclude = .git,.tox,.venv,tests/*,build/*,doc/_build/*,sphinx/search/*,doc/usage/extensions/example*.py
+exclude = .git,.tox,.venv,tests/roots/*,build/*,doc/_build/*,sphinx/search/*,doc/usage/extensions/example*.py
application-import-names = sphinx
import-order-style = smarkets
+per-file-ignores =
+ tests/*: E501
[flake8:local-plugins]
extension =
diff --git a/sphinx/application.py b/sphinx/application.py
index 7661b82d6..b0834fef7 100644
--- a/sphinx/application.py
+++ b/sphinx/application.py
@@ -130,7 +130,7 @@ class Sphinx:
:ivar outdir: Directory for storing build documents.
"""
- def __init__(self, srcdir: str, confdir: str, outdir: str, doctreedir: str,
+ def __init__(self, srcdir: str, confdir: Optional[str], outdir: str, doctreedir: str,
buildername: str, confoverrides: Dict = None,
status: IO = sys.stdout, warning: IO = sys.stderr,
freshenv: bool = False, warningiserror: bool = False, tags: List[str] = None,
@@ -289,8 +289,8 @@ class Sphinx:
if catalog.domain == 'sphinx' and catalog.is_outdated():
catalog.write_mo(self.config.language)
- locale_dirs = [None] # type: List[Optional[str]]
- locale_dirs += list(repo.locale_dirs)
+ locale_dirs = list(repo.locale_dirs) # type: List[Optional[str]]
+ locale_dirs += [None]
locale_dirs += [path.join(package_dir, 'locale')]
self.translator, has_translation = locale.init(locale_dirs, self.config.language)
diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py
index 0af5dde28..49dac78d5 100644
--- a/sphinx/builders/html/__init__.py
+++ b/sphinx/builders/html/__init__.py
@@ -295,7 +295,6 @@ class StandaloneHTMLBuilder(Builder):
self.add_js_file('jquery.js')
self.add_js_file('underscore.js')
self.add_js_file('doctools.js')
- self.add_js_file('language_data.js')
for filename, attrs in self.app.registry.js_files:
self.add_js_file(filename, **attrs)
diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py
index 40e54a8d7..1dc0337c3 100644
--- a/sphinx/builders/linkcheck.py
+++ b/sphinx/builders/linkcheck.py
@@ -28,7 +28,6 @@ from sphinx.locale import __
from sphinx.util import encode_uri, logging, requests
from sphinx.util.console import darkgray, darkgreen, purple, red, turquoise # type: ignore
from sphinx.util.nodes import get_node_line
-from sphinx.util.requests import is_ssl_error
logger = logging.getLogger(__name__)
@@ -108,9 +107,7 @@ class CheckExternalLinksBuilder(Builder):
self.workers.append(thread)
def check_thread(self) -> None:
- kwargs = {
- 'allow_redirects': True,
- } # type: Dict
+ kwargs = {}
if self.app.config.linkcheck_timeout:
kwargs['timeout'] = self.app.config.linkcheck_timeout
@@ -171,8 +168,9 @@ class CheckExternalLinksBuilder(Builder):
try:
# try a HEAD request first, which should be easier on
# the server and the network
- response = requests.head(req_url, config=self.app.config,
- auth=auth_info, **kwargs)
+ response = requests.head(req_url, allow_redirects=True,
+ config=self.app.config, auth=auth_info,
+ **kwargs)
response.raise_for_status()
except HTTPError:
# retry with GET request if that fails, some servers
@@ -190,10 +188,7 @@ class CheckExternalLinksBuilder(Builder):
else:
return 'broken', str(err), 0
except Exception as err:
- if is_ssl_error(err):
- return 'ignored', str(err), 0
- else:
- return 'broken', str(err), 0
+ return 'broken', str(err), 0
if response.url.rstrip('/') == req_url.rstrip('/'):
return 'working', '', 0
else:
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 73d3c75ef..a6cd850dc 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -16,7 +16,7 @@ import warnings
from inspect import Parameter, Signature
from types import ModuleType
from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Sequence,
- Set, Tuple, Type, TypeVar, Union, get_type_hints)
+ Set, Tuple, Type, TypeVar, Union)
from docutils.statemachine import StringList
@@ -33,7 +33,7 @@ from sphinx.util import inspect, logging
from sphinx.util.docstrings import extract_metadata, prepare_docstring
from sphinx.util.inspect import (evaluate_signature, getdoc, object_description, safe_getattr,
stringify_signature)
-from sphinx.util.typing import restify
+from sphinx.util.typing import get_type_hints, restify
from sphinx.util.typing import stringify as stringify_typehint
if TYPE_CHECKING:
@@ -954,7 +954,7 @@ class ModuleDocumenter(Documenter):
def __init__(self, *args: Any) -> None:
super().__init__(*args)
merge_members_option(self.options)
- self.__all__ = None
+ self.__all__ = None # type: Optional[Sequence[str]]
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
@@ -978,26 +978,16 @@ class ModuleDocumenter(Documenter):
return ret
def import_object(self, raiseerror: bool = False) -> bool:
- def is_valid_module_all(__all__: Any) -> bool:
- """Check the given *__all__* is valid for a module."""
- if (isinstance(__all__, (list, tuple)) and
- all(isinstance(e, str) for e in __all__)):
- return True
- else:
- return False
-
ret = super().import_object(raiseerror)
- if not self.options.ignore_module_all:
- __all__ = getattr(self.object, '__all__', None)
- if is_valid_module_all(__all__):
- # valid __all__ found. copy it to self.__all__
- self.__all__ = __all__
- elif __all__:
- # invalid __all__ found.
- logger.warning(__('__all__ should be a list of strings, not %r '
- '(in module %s) -- ignoring __all__') %
- (__all__, self.fullname), type='autodoc')
+ try:
+ if not self.options.ignore_module_all:
+ self.__all__ = inspect.getall(self.object)
+ except ValueError as exc:
+ # invalid __all__ found.
+ logger.warning(__('__all__ should be a list of strings, not %r '
+ '(in module %s) -- ignoring __all__') %
+ (exc.args[0], self.fullname), type='autodoc')
return ret
@@ -1670,28 +1660,41 @@ class DataDocumenter(ModuleLevelDocumenter):
priority = -10
option_spec = dict(ModuleLevelDocumenter.option_spec)
option_spec["annotation"] = annotation_option
+ option_spec["no-value"] = bool_option
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return isinstance(parent, ModuleDocumenter) and isattr
+ def import_object(self, raiseerror: bool = False) -> bool:
+ try:
+ return super().import_object(raiseerror=True)
+ except ImportError as exc:
+ # annotation only instance variable (PEP-526)
+ try:
+ self.parent = importlib.import_module(self.modname)
+ annotations = get_type_hints(self.parent, None,
+ self.config.autodoc_type_aliases)
+ if self.objpath[-1] in annotations:
+ self.object = UNINITIALIZED_ATTR
+ return True
+ except ImportError:
+ pass
+
+ if raiseerror:
+ raise
+ else:
+ logger.warning(exc.args[0], type='autodoc', subtype='import_object')
+ self.env.note_reread()
+ return False
+
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
sourcename = self.get_sourcename()
if not self.options.annotation:
# obtain annotation for this data
- try:
- annotations = get_type_hints(self.parent)
- except NameError:
- # Failed to evaluate ForwardRef (maybe TYPE_CHECKING)
- annotations = safe_getattr(self.parent, '__annotations__', {})
- except TypeError:
- annotations = {}
- except KeyError:
- # a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084)
- annotations = {}
-
+ annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases)
if self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
@@ -1702,7 +1705,7 @@ class DataDocumenter(ModuleLevelDocumenter):
sourcename)
try:
- if self.object is UNINITIALIZED_ATTR:
+ if self.object is UNINITIALIZED_ATTR or self.options.no_value:
pass
else:
objrepr = object_description(self.object)
@@ -1722,6 +1725,13 @@ class DataDocumenter(ModuleLevelDocumenter):
return self.get_attr(self.parent or self.object, '__module__', None) \
or self.modname
+ def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
+ if self.object is UNINITIALIZED_ATTR:
+ # suppress docstring of the value
+ super().add_content(more_content, no_docstring=True)
+ else:
+ super().add_content(more_content, no_docstring=no_docstring)
+
class DataDeclarationDocumenter(DataDocumenter):
"""
@@ -1735,30 +1745,10 @@ class DataDeclarationDocumenter(DataDocumenter):
# must be higher than AttributeDocumenter
priority = 11
- @classmethod
- def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
- ) -> bool:
- """This documents only INSTANCEATTR members."""
- return (isinstance(parent, ModuleDocumenter) and
- isattr and
- member is INSTANCEATTR)
-
- def import_object(self, raiseerror: bool = False) -> bool:
- """Never import anything."""
- # disguise as a data
- self.objtype = 'data'
- self.object = UNINITIALIZED_ATTR
- try:
- # import module to obtain type annotation
- self.parent = importlib.import_module(self.modname)
- except ImportError:
- pass
-
- return True
-
- def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
- """Never try to get a docstring from the object."""
- super().add_content(more_content, no_docstring=True)
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ warnings.warn("%s is deprecated." % self.__class__.__name__,
+ RemovedInSphinx50Warning, stacklevel=2)
+ super().__init__(*args, **kwargs)
class GenericAliasDocumenter(DataDocumenter):
@@ -1998,6 +1988,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
member_order = 60
option_spec = dict(ModuleLevelDocumenter.option_spec)
option_spec["annotation"] = annotation_option
+ option_spec["no-value"] = bool_option
# must be higher than the MethodDocumenter, else it will recognize
# some non-data descriptors as methods
@@ -2024,6 +2015,22 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
def isinstanceattribute(self) -> bool:
"""Check the subject is an instance attribute."""
+ # uninitialized instance variable (PEP-526)
+ with mock(self.config.autodoc_mock_imports):
+ try:
+ ret = import_object(self.modname, self.objpath[:-1], 'class',
+ attrgetter=self.get_attr,
+ warningiserror=self.config.autodoc_warningiserror)
+ self.parent = ret[3]
+ annotations = get_type_hints(self.parent, None,
+ self.config.autodoc_type_aliases)
+ if self.objpath[-1] in annotations:
+ self.object = UNINITIALIZED_ATTR
+ return True
+ except ImportError:
+ pass
+
+ # An instance variable defined inside __init__().
try:
analyzer = ModuleAnalyzer.for_module(self.modname)
attr_docs = analyzer.find_attr_docs()
@@ -2034,7 +2041,9 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
return False
except PycodeError:
- return False
+ pass
+
+ return False
def import_object(self, raiseerror: bool = False) -> bool:
try:
@@ -2069,17 +2078,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
sourcename = self.get_sourcename()
if not self.options.annotation:
# obtain type annotation for this attribute
- try:
- annotations = get_type_hints(self.parent)
- except NameError:
- # Failed to evaluate ForwardRef (maybe TYPE_CHECKING)
- annotations = safe_getattr(self.parent, '__annotations__', {})
- except TypeError:
- annotations = {}
- except KeyError:
- # a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084)
- annotations = {}
-
+ annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases)
if self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
@@ -2092,7 +2091,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
# data descriptors do not have useful values
if not self._datadescriptor:
try:
- if self.object is INSTANCEATTR:
+ if self.object is INSTANCEATTR or self.options.no_value:
pass
else:
objrepr = object_description(self.object)
@@ -2244,8 +2243,8 @@ class SlotsAttributeDocumenter(AttributeDocumenter):
% self.__class__.__name__,
RemovedInSphinx50Warning, stacklevel=2)
name = self.objpath[-1]
- __slots__ = safe_getattr(self.parent, '__slots__', [])
- if isinstance(__slots__, dict) and isinstance(__slots__.get(name), str):
+ __slots__ = inspect.getslots(self.parent)
+ if __slots__ and isinstance(__slots__.get(name, None), str):
docstring = prepare_docstring(__slots__[name])
return [docstring]
else:
@@ -2280,7 +2279,6 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_autodocumenter(ClassDocumenter)
app.add_autodocumenter(ExceptionDocumenter)
app.add_autodocumenter(DataDocumenter)
- app.add_autodocumenter(DataDeclarationDocumenter)
app.add_autodocumenter(GenericAliasDocumenter)
app.add_autodocumenter(TypeVarDocumenter)
app.add_autodocumenter(FunctionDocumenter)
diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py
index df5614f99..e61874c47 100644
--- a/sphinx/ext/autodoc/importer.py
+++ b/sphinx/ext/autodoc/importer.py
@@ -15,7 +15,7 @@ from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Optional, Tup
from sphinx.pycode import ModuleAnalyzer
from sphinx.util import logging
-from sphinx.util.inspect import isclass, isenumclass, safe_getattr
+from sphinx.util.inspect import getslots, isclass, isenumclass, safe_getattr
if False:
# For type annotation
@@ -203,14 +203,15 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
members[name] = Attribute(name, True, value)
# members in __slots__
- if isclass(subject) and getattr(subject, '__slots__', None) is not None:
- from sphinx.ext.autodoc import SLOTSATTR
-
- slots = subject.__slots__
- if isinstance(slots, str):
- slots = [slots]
- for name in slots:
- members[name] = Attribute(name, True, SLOTSATTR)
+ try:
+ __slots__ = getslots(subject)
+ if __slots__:
+ from sphinx.ext.autodoc import SLOTSATTR
+
+ for name in __slots__:
+ members[name] = Attribute(name, True, SLOTSATTR)
+ except (TypeError, ValueError):
+ pass
# other members
for name in dir(subject):
diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py
index a098177a4..a66aa350a 100644
--- a/sphinx/ext/autosummary/generate.py
+++ b/sphinx/ext/autosummary/generate.py
@@ -81,8 +81,7 @@ class AutosummaryEntry(NamedTuple):
def setup_documenters(app: Any) -> None:
- from sphinx.ext.autodoc import (AttributeDocumenter, ClassDocumenter,
- DataDeclarationDocumenter, DataDocumenter,
+ from sphinx.ext.autodoc import (AttributeDocumenter, ClassDocumenter, DataDocumenter,
DecoratorDocumenter, ExceptionDocumenter,
FunctionDocumenter, GenericAliasDocumenter,
InstanceAttributeDocumenter, MethodDocumenter,
@@ -92,8 +91,7 @@ def setup_documenters(app: Any) -> None:
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
FunctionDocumenter, MethodDocumenter, AttributeDocumenter,
InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
- SlotsAttributeDocumenter, DataDeclarationDocumenter, GenericAliasDocumenter,
- SingledispatchFunctionDocumenter,
+ SlotsAttributeDocumenter, GenericAliasDocumenter, SingledispatchFunctionDocumenter,
] # type: List[Type[Documenter]]
for documenter in documenters:
app.registry.add_documenter(documenter.objtype, documenter)
diff --git a/sphinx/themes/basic/search.html b/sphinx/themes/basic/search.html
index 2673369f2..cf574f8d5 100644
--- a/sphinx/themes/basic/search.html
+++ b/sphinx/themes/basic/search.html
@@ -12,6 +12,7 @@
{%- block scripts %}
{{ super() }}
<script src="{{ pathto('_static/searchtools.js', 1) }}"></script>
+ <script src="{{ pathto('_static/language_data.js', 1) }}"></script>
{%- endblock %}
{% block extrahead %}
<script src="{{ pathto('searchindex.js', 1) }}" defer></script>
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
index d1e9c4435..beaed6099 100644
--- a/sphinx/util/inspect.py
+++ b/sphinx/util/inspect.py
@@ -20,7 +20,7 @@ import warnings
from functools import partial, partialmethod
from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule # NOQA
from io import StringIO
-from typing import Any, Callable, Dict, cast
+from typing import Any, Callable, Dict, Optional, Sequence, cast
from sphinx.deprecation import RemovedInSphinx50Warning
from sphinx.pycode.ast import ast # for py36-37
@@ -107,7 +107,11 @@ def getargspec(func: Callable) -> Any:
def unwrap(obj: Any) -> Any:
"""Get an original object from wrapped object (wrapped functions)."""
try:
- return inspect.unwrap(obj)
+ if hasattr(obj, '__sphinx_mock__'):
+ # Skip unwrapping mock object to avoid RecursionError
+ return obj
+ else:
+ return inspect.unwrap(obj)
except ValueError:
# might be a mock object
return obj
@@ -133,6 +137,43 @@ def unwrap_all(obj: Any, *, stop: Callable = None) -> Any:
return obj
+def getall(obj: Any) -> Optional[Sequence[str]]:
+ """Get __all__ attribute of the module as dict.
+
+ Return None if given *obj* does not have __all__.
+ Raises ValueError if given *obj* have invalid __all__.
+ """
+ __all__ = safe_getattr(obj, '__all__', None)
+ if __all__ is None:
+ return None
+ else:
+ if (isinstance(__all__, (list, tuple)) and all(isinstance(e, str) for e in __all__)):
+ return __all__
+ else:
+ raise ValueError(__all__)
+
+
+def getslots(obj: Any) -> Optional[Dict]:
+ """Get __slots__ attribute of the class as dict.
+
+ Return None if gienv *obj* does not have __slots__.
+ """
+ if not inspect.isclass(obj):
+ raise TypeError
+
+ __slots__ = safe_getattr(obj, '__slots__', None)
+ if __slots__ is None:
+ return None
+ elif isinstance(__slots__, dict):
+ return __slots__
+ elif isinstance(__slots__, str):
+ return {__slots__: None}
+ elif isinstance(__slots__, (list, tuple)):
+ return {e: None for e in __slots__}
+ else:
+ raise ValueError
+
+
def isenumclass(x: Any) -> bool:
"""Check if the object is subclass of enum."""
return inspect.isclass(x) and issubclass(x, enum.Enum)
diff --git a/sphinx/util/requests.py b/sphinx/util/requests.py
index b3fc8bc35..ca570249a 100644
--- a/sphinx/util/requests.py
+++ b/sphinx/util/requests.py
@@ -18,6 +18,7 @@ import requests
import sphinx
from sphinx.config import Config
+from sphinx.deprecation import RemovedInSphinx50Warning
try:
from requests.packages.urllib3.exceptions import SSLError
@@ -43,6 +44,10 @@ useragent_header = [('User-Agent',
def is_ssl_error(exc: Exception) -> bool:
"""Check an exception is SSLError."""
+ warnings.warn(
+ "is_ssl_error() is outdated and likely returns incorrect results "
+ "for modern versions of Requests.",
+ RemovedInSphinx50Warning)
if isinstance(exc, SSLError):
return True
else:
diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py
index 55af67829..9daa4b28a 100644
--- a/sphinx/util/typing.py
+++ b/sphinx/util/typing.py
@@ -57,6 +57,29 @@ TitleGetter = Callable[[nodes.Node], str]
Inventory = Dict[str, Dict[str, Tuple[str, str, str, str]]]
+def get_type_hints(obj: Any, globalns: Dict = None, localns: Dict = None) -> Dict[str, Any]:
+ """Return a dictionary containing type hints for a function, method, module or class object.
+
+ This is a simple wrapper of `typing.get_type_hints()` that does not raise an error on
+ runtime.
+ """
+ from sphinx.util.inspect import safe_getattr # lazy loading
+
+ try:
+ return typing.get_type_hints(obj, None, localns)
+ except NameError:
+ # Failed to evaluate ForwardRef (maybe TYPE_CHECKING)
+ return safe_getattr(obj, '__annotations__', {})
+ except TypeError:
+ return {}
+ except KeyError:
+ # a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084)
+ return {}
+ except AttributeError:
+ # AttributeError is raised on 3.5.2 (fixed by 3.5.3)
+ return {}
+
+
def is_system_TypeVar(typ: Any) -> bool:
"""Check *typ* is system defined TypeVar."""
modname = getattr(typ, '__module__', '')
diff --git a/tests/certs/cert.pem b/tests/certs/cert.pem
new file mode 100644
index 000000000..6f8c35c6b
--- /dev/null
+++ b/tests/certs/cert.pem
@@ -0,0 +1,50 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9fzHGBPNaZNcN
+nL/1nvO2xJR/E64vFua3QfPQQ5HpigjrK/HRUlRGztRKJ+CEjCXNYNfQ4dUcV45o
+k5uPH3U1CkAw2d/We+kZnAHkNuw4mRC0ohdzpUByyDOA5WtUWPn9SwhXCVz6fM7e
+I52auvzpUE6soVDM3nucnqZDJ3Ua9KgB02FrqX13S76Uq+uf8Q2hpTruO/nBzB4p
+6xFwJJ1taXEEWi8swg6HO8/+0x0AeripV6JieNUptEFuV9kLvRz9qGg0CO2f7AdI
+jNeFDGrgO7qJ+VxXV9Gnbi6ph4vsUwtJZB3phRGGomdgiRd6PSma81nvTe1z69x/
+g+8P091pAgMBAAECggEAIrTABfd0JpMffAPAeJjjJA8+70NIfKFiIiA3Kmalu7Mn
+TQMgZ+j/PHS3FtnU2hHc/o+FF2G1KVqz311heUYWrl8xQIE26M6K88DJ6+VPQFJw
+Z9TkHK8gbaVTIYFjNfCR4J00atRxLgNb0/2L6QHkPksSDbYB2XPKCfZYlyYL4aKq
+dePghFu9ePXhUXooPCqke+kP0b8OmHzPlmJpxbeb8ujiox2+4wYjN8lWPz8xHv8i
+IM7V5hAbPIaQfu/joKrRKk+Kk8UqGurkKQ75KLLL+1oaJO/GLTQ4bk5tpRgfWPda
+aEBzSPrnqame2CKUWtBughuRWSxdTIMvdXIC/ym1gQKBgQDx6Nyio/L6I5CdlXwC
+HAzBCy1mnj70Kj97tQc+A/0z8dD7fCSE/oo8IiEKixcjnaSxHk8VjINF/w17n63W
+8neE7pVsuDwxfhiQ9ZRI1WpV0LsFEoTrEWG7Ax8UzbHXCQbNJ9SI0HJRo9UN0f/Z
+t+ZT+HNUzdcpCwTvdRVDisbXcQKBgQDIiMz58GFEwdGPXJKEhSyQ3kSQBjeqo0Vl
+wMDuDvFEckHl/p1RnDo0lzaq6FivOX84ymvGNdQW14TnQp3A/mkQ5o6k/e1pfAA6
+X0Y6tBH/QppVo5sFvOufyn02k48k5pFAjLHH9L9i0dyWqq4V6PgA2uk4qilFxEg/
+CJEVfq4ZeQKBgQCZPHKWq9f8T48J42kcRPxnRFdMC63BKQnxqOifhhNcVi+VPjw7
+6qlSEiRv80+DBhcPAy4BbnKxYjD+QFX0NL80+5S3u7SVfVS+bnGx+U5UcdYmDmcY
+KHiJ6B5GJU4j8tnWFwbwa2ofAPKywHWbSnyicF1OON20aACGVtpTYJM4YQKBgBW4
+09NDGZY0FHoeAfT+4/vxR6X+NmtyciL6hSuETNgoNEEwmmPrs1ZdBtvufSTF6qUB
+MDlxPT8YK1pNmf78z+63ur3ej6f8eZ3ZEidruANZeJRMO4+cjj1p1rRhuYC6xQMj
++mH5ff27U9SyOlc/PBYDoH212PCouVaym9yjM0KpAoGBALr583slY55ESOthLrfX
+1ecoET5xxRm431XbZMnxu0uUvHWNfqoojtmD7laclb9HwkpShPB6PT1egBIvDWWM
+bVUuXzJ8gP0tIG3dHgiiUlld3ahOiaMYSU77uLFBRWv5sQqfewLuFvlzHn/2ZSt7
+TcipT4f67b18W8iuLJELEs57
+-----END PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDuTCCAqGgAwIBAgIUUNvkPwe0W8C2I0+KnLpMaQ+S+vowDQYJKoZIhvcNAQEL
+BQAwYTELMAkGA1UEBhMCRlIxETAPBgNVBAgMCEJyZXRhZ25lMQ8wDQYDVQQHDAZS
+ZW5uZXMxGjAYBgNVBAoMEVNwaGlueCB0ZXN0IHN1aXRlMRIwEAYDVQQDDAlsb2Nh
+bGhvc3QwHhcNMjAxMTE1MTcyNDExWhcNMzAxMTEzMTcyNDExWjBhMQswCQYDVQQG
+EwJGUjERMA8GA1UECAwIQnJldGFnbmUxDzANBgNVBAcMBlJlbm5lczEaMBgGA1UE
+CgwRU3BoaW54IHRlc3Qgc3VpdGUxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAL1/McYE81pk1w2cv/We87bElH8Tri8W
+5rdB89BDkemKCOsr8dFSVEbO1Eon4ISMJc1g19Dh1RxXjmiTm48fdTUKQDDZ39Z7
+6RmcAeQ27DiZELSiF3OlQHLIM4Dla1RY+f1LCFcJXPp8zt4jnZq6/OlQTqyhUMze
+e5yepkMndRr0qAHTYWupfXdLvpSr65/xDaGlOu47+cHMHinrEXAknW1pcQRaLyzC
+Doc7z/7THQB6uKlXomJ41Sm0QW5X2Qu9HP2oaDQI7Z/sB0iM14UMauA7uon5XFdX
+0aduLqmHi+xTC0lkHemFEYaiZ2CJF3o9KZrzWe9N7XPr3H+D7w/T3WkCAwEAAaNp
+MGcwHQYDVR0OBBYEFN1iHZj88N6eI2FlRzza52xzOU5EMB8GA1UdIwQYMBaAFN1i
+HZj88N6eI2FlRzza52xzOU5EMA8GA1UdEwEB/wQFMAMBAf8wFAYDVR0RBA0wC4IJ
+bG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQBVUZm1iw7N7uZu/SF3hailxS+1
+3KChItWu3ZOIjlmDIkaJ9kWqP2ficUg3tBUx6/UOjHQAwRC4rj87BoSV2mEy+0OX
+fyy+ER/BeHYly5v+hpjVojVKeqysk5CKttZM+cOibT2SzLLYf0InNqZRQRJco+nL
+QNR0hVo/Lz6Mf1gF2ywf9bXSF3+XECU4K6sVm4QpFbJNm+fHqJBuh1LXHRrcTAsP
+LM6PBnd3P5QTcr/G0s/tYMPmero9YHZUO8FMvMVoI2K8k6/duG/EbBaNzriRI1OM
+PpZGCWxbJfyApnzc5lGAG4zJnV/wpOyNhKJuW9N1fr2oEwPpJlS3VzrgeKcY
+-----END CERTIFICATE-----
diff --git a/tests/roots/test-ext-autodoc/target/annotations.py b/tests/roots/test-ext-autodoc/target/annotations.py
index 691176b08..e9ff2f604 100644
--- a/tests/roots/test-ext-autodoc/target/annotations.py
+++ b/tests/roots/test-ext-autodoc/target/annotations.py
@@ -4,6 +4,9 @@ from typing import overload
myint = int
+#: docstring
+variable: myint
+
def sum(x: myint, y: myint) -> myint:
"""docstring"""
@@ -23,3 +26,10 @@ def mult(x: float, y: float) -> float:
def mult(x, y):
"""docstring"""
return x, y
+
+
+class Foo:
+ """docstring"""
+
+ #: docstring
+ attr: myint
diff --git a/tests/roots/test-linkcheck-localserver-anchor/conf.py b/tests/roots/test-linkcheck-localserver-anchor/conf.py
new file mode 100644
index 000000000..2ba1f85e8
--- /dev/null
+++ b/tests/roots/test-linkcheck-localserver-anchor/conf.py
@@ -0,0 +1,2 @@
+exclude_patterns = ['_build']
+linkcheck_anchors = True
diff --git a/tests/roots/test-linkcheck-localserver-anchor/index.rst b/tests/roots/test-linkcheck-localserver-anchor/index.rst
new file mode 100644
index 000000000..807fe964b
--- /dev/null
+++ b/tests/roots/test-linkcheck-localserver-anchor/index.rst
@@ -0,0 +1 @@
+`local server <http://localhost:7777/#anchor>`_
diff --git a/tests/roots/test-linkcheck-localserver-https/conf.py b/tests/roots/test-linkcheck-localserver-https/conf.py
new file mode 100644
index 000000000..a45d22e28
--- /dev/null
+++ b/tests/roots/test-linkcheck-localserver-https/conf.py
@@ -0,0 +1 @@
+exclude_patterns = ['_build']
diff --git a/tests/roots/test-linkcheck-localserver-https/index.rst b/tests/roots/test-linkcheck-localserver-https/index.rst
new file mode 100644
index 000000000..fea598359
--- /dev/null
+++ b/tests/roots/test-linkcheck-localserver-https/index.rst
@@ -0,0 +1 @@
+`HTTPS server <https://localhost:7777/>`_
diff --git a/tests/roots/test-linkcheck-localserver/conf.py b/tests/roots/test-linkcheck-localserver/conf.py
index 2ba1f85e8..a45d22e28 100644
--- a/tests/roots/test-linkcheck-localserver/conf.py
+++ b/tests/roots/test-linkcheck-localserver/conf.py
@@ -1,2 +1 @@
exclude_patterns = ['_build']
-linkcheck_anchors = True
diff --git a/tests/roots/test-linkcheck-localserver/index.rst b/tests/roots/test-linkcheck-localserver/index.rst
index 807fe964b..c617e942f 100644
--- a/tests/roots/test-linkcheck-localserver/index.rst
+++ b/tests/roots/test-linkcheck-localserver/index.rst
@@ -1 +1 @@
-`local server <http://localhost:7777/#anchor>`_
+`local server <http://localhost:7777/>`_
diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py
index 86466e4a9..3d5a9554d 100644
--- a/tests/test_build_latex.py
+++ b/tests/test_build_latex.py
@@ -20,7 +20,7 @@ from test_build_html import ENV_WARNINGS
from sphinx.builders.latex import default_latex_documents
from sphinx.config import Config
-from sphinx.errors import SphinxError, ThemeError
+from sphinx.errors import SphinxError
from sphinx.testing.util import strip_escseq
from sphinx.util.osutil import cd, ensuredir
from sphinx.writers.latex import LaTeXTranslator
@@ -762,7 +762,7 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning):
assert ('\\caption{This is the figure caption with a footnote to '
'\\sphinxfootnotemark[7].}\\label{\\detokenize{index:id29}}\\end{figure}\n'
'%\n\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n'
- 'Footnote in caption\n%\n\\end{footnotetext}')in result
+ 'Footnote in caption\n%\n\\end{footnotetext}') in result
assert ('\\sphinxcaption{footnote \\sphinxfootnotemark[8] in '
'caption of normal table}\\label{\\detokenize{index:id30}}') in result
assert ('\\caption{footnote \\sphinxfootnotemark[9] '
diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py
index 88cf2aee1..965a8576d 100644
--- a/tests/test_build_linkcheck.py
+++ b/tests/test_build_linkcheck.py
@@ -10,12 +10,12 @@
import http.server
import json
-import re
-from unittest import mock
+import textwrap
import pytest
+import requests
-from utils import http_server
+from utils import CERT_FILE, http_server, https_server, modify_env
@pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True)
@@ -57,7 +57,7 @@ def test_defaults_json(app):
assert len(rows) == 10
# the output order of the rows is not stable
# due to possible variance in network latency
- rowsby = {row["uri"]:row for row in rows}
+ rowsby = {row["uri"]: row for row in rows}
assert rowsby["https://www.google.com#!bar"] == {
'filename': 'links.txt',
'lineno': 10,
@@ -110,7 +110,8 @@ def test_anchors_ignored(app):
# expect all ok when excluding #top
assert not content
-@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-anchor', freshenv=True)
def test_raises_for_invalid_status(app):
class InternalServerErrorHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
@@ -126,56 +127,258 @@ def test_raises_for_invalid_status(app):
)
+class HeadersDumperHandler(http.server.BaseHTTPRequestHandler):
+ def do_HEAD(self):
+ self.do_GET()
+
+ def do_GET(self):
+ self.send_response(200, "OK")
+ self.end_headers()
+ print(self.headers.as_string())
+
+
@pytest.mark.sphinx(
- 'linkcheck', testroot='linkcheck', freshenv=True,
+ 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
confoverrides={'linkcheck_auth': [
- (r'.+google\.com/image.+', 'authinfo1'),
- (r'.+google\.com.+', 'authinfo2'),
- ]
- })
-def test_auth(app):
- mock_req = mock.MagicMock()
- mock_req.return_value = 'fake-response'
-
- with mock.patch.multiple('requests', get=mock_req, head=mock_req):
+ (r'^$', ('no', 'match')),
+ (r'^http://localhost:7777/$', ('user1', 'password')),
+ (r'.*local.*', ('user2', 'hunter2')),
+ ]})
+def test_auth_header_uses_first_match(app, capsys):
+ with http_server(HeadersDumperHandler):
app.builder.build_all()
- for c_args, c_kwargs in mock_req.call_args_list:
- if 'google.com/image' in c_args[0]:
- assert c_kwargs['auth'] == 'authinfo1'
- elif 'google.com' in c_args[0]:
- assert c_kwargs['auth'] == 'authinfo2'
- else:
- assert not c_kwargs['auth']
+ stdout, stderr = capsys.readouterr()
+ auth = requests.auth._basic_auth_str('user1', 'password')
+ assert "Authorization: %s\n" % auth in stdout
@pytest.mark.sphinx(
- 'linkcheck', testroot='linkcheck', freshenv=True,
+ 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
+ confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]})
+def test_auth_header_no_match(app, capsys):
+ with http_server(HeadersDumperHandler):
+ app.builder.build_all()
+ stdout, stderr = capsys.readouterr()
+ assert "Authorization" not in stdout
+
+
+@pytest.mark.sphinx(
+ 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
confoverrides={'linkcheck_request_headers': {
- "https://localhost:7777/": {
+ "http://localhost:7777/": {
"Accept": "text/html",
},
- "http://www.sphinx-doc.org": { # no slash at the end
- "Accept": "application/json",
- },
"*": {
"X-Secret": "open sesami",
}
}})
-def test_linkcheck_request_headers(app):
- mock_req = mock.MagicMock()
- mock_req.return_value = 'fake-response'
+def test_linkcheck_request_headers(app, capsys):
+ with http_server(HeadersDumperHandler):
+ app.builder.build_all()
+
+ stdout, _stderr = capsys.readouterr()
+ assert "Accept: text/html\n" in stdout
+ assert "X-Secret" not in stdout
+ assert "sesami" not in stdout
+
- with mock.patch.multiple('requests', get=mock_req, head=mock_req):
+@pytest.mark.sphinx(
+ 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
+ confoverrides={'linkcheck_request_headers': {
+ "http://localhost:7777": {"Accept": "application/json"},
+ "*": {"X-Secret": "open sesami"}
+ }})
+def test_linkcheck_request_headers_no_slash(app, capsys):
+ with http_server(HeadersDumperHandler):
app.builder.build_all()
- for args, kwargs in mock_req.call_args_list:
- url = args[0]
- headers = kwargs.get('headers', {})
- if "https://localhost:7777" in url:
- assert headers["Accept"] == "text/html"
- elif 'http://www.sphinx-doc.org' in url:
- assert headers["Accept"] == "application/json"
- elif 'https://www.google.com' in url:
- assert headers["Accept"] == "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8"
- assert headers["X-Secret"] == "open sesami"
+
+ stdout, _stderr = capsys.readouterr()
+ assert "Accept: application/json\n" in stdout
+ assert "X-Secret" not in stdout
+ assert "sesami" not in stdout
+
+
+@pytest.mark.sphinx(
+ 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
+ confoverrides={'linkcheck_request_headers': {
+ "http://do.not.match.org": {"Accept": "application/json"},
+ "*": {"X-Secret": "open sesami"}
+ }})
+def test_linkcheck_request_headers_default(app, capsys):
+ with http_server(HeadersDumperHandler):
+ app.builder.build_all()
+
+ stdout, _stderr = capsys.readouterr()
+ assert "Accepts: application/json\n" not in stdout
+ assert "X-Secret: open sesami\n" in stdout
+
+
+def make_redirect_handler(*, support_head):
+ class RedirectOnceHandler(http.server.BaseHTTPRequestHandler):
+ def do_HEAD(self):
+ if support_head:
+ self.do_GET()
+ else:
+ self.send_response(405, "Method Not Allowed")
+ self.end_headers()
+
+ def do_GET(self):
+ if self.path == "/?redirected=1":
+ self.send_response(204, "No content")
else:
- assert headers["Accept"] == "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8"
+ self.send_response(302, "Found")
+ self.send_header("Location", "http://localhost:7777/?redirected=1")
+ self.end_headers()
+
+ def log_date_time_string(self):
+ """Strip date and time from logged messages for assertions."""
+ return ""
+
+ return RedirectOnceHandler
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
+def test_follows_redirects_on_HEAD(app, capsys):
+ with http_server(make_redirect_handler(support_head=True)):
+ app.builder.build_all()
+ stdout, stderr = capsys.readouterr()
+ content = (app.outdir / 'output.txt').read_text()
+ assert content == (
+ "index.rst:1: [redirected with Found] "
+ "http://localhost:7777/ to http://localhost:7777/?redirected=1\n"
+ )
+ assert stderr == textwrap.dedent(
+ """\
+ 127.0.0.1 - - [] "HEAD / HTTP/1.1" 302 -
+ 127.0.0.1 - - [] "HEAD /?redirected=1 HTTP/1.1" 204 -
+ """
+ )
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
+def test_follows_redirects_on_GET(app, capsys):
+ with http_server(make_redirect_handler(support_head=False)):
+ app.builder.build_all()
+ stdout, stderr = capsys.readouterr()
+ content = (app.outdir / 'output.txt').read_text()
+ assert content == (
+ "index.rst:1: [redirected with Found] "
+ "http://localhost:7777/ to http://localhost:7777/?redirected=1\n"
+ )
+ assert stderr == textwrap.dedent(
+ """\
+ 127.0.0.1 - - [] "HEAD / HTTP/1.1" 405 -
+ 127.0.0.1 - - [] "GET / HTTP/1.1" 302 -
+ 127.0.0.1 - - [] "GET /?redirected=1 HTTP/1.1" 204 -
+ """
+ )
+
+
+class OKHandler(http.server.BaseHTTPRequestHandler):
+ def do_HEAD(self):
+ self.send_response(200, "OK")
+ self.end_headers()
+
+ def do_GET(self):
+ self.do_HEAD()
+ self.wfile.write(b"ok\n")
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
+def test_invalid_ssl(app):
+ # Link indicates SSL should be used (https) but the server does not handle it.
+ with http_server(OKHandler):
+ app.builder.build_all()
+
+ with open(app.outdir / 'output.json') as fp:
+ content = json.load(fp)
+ assert content["status"] == "broken"
+ assert content["filename"] == "index.rst"
+ assert content["lineno"] == 1
+ assert content["uri"] == "https://localhost:7777/"
+ assert "SSLError" in content["info"]
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
+def test_connect_to_selfsigned_fails(app):
+ with https_server(OKHandler):
+ app.builder.build_all()
+
+ with open(app.outdir / 'output.json') as fp:
+ content = json.load(fp)
+ assert content["status"] == "broken"
+ assert content["filename"] == "index.rst"
+ assert content["lineno"] == 1
+ assert content["uri"] == "https://localhost:7777/"
+ assert "[SSL: CERTIFICATE_VERIFY_FAILED]" in content["info"]
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
+def test_connect_to_selfsigned_with_tls_verify_false(app):
+ app.config.tls_verify = False
+ with https_server(OKHandler):
+ app.builder.build_all()
+
+ with open(app.outdir / 'output.json') as fp:
+ content = json.load(fp)
+ assert content == {
+ "code": 0,
+ "status": "working",
+ "filename": "index.rst",
+ "lineno": 1,
+ "uri": "https://localhost:7777/",
+ "info": "",
+ }
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
+def test_connect_to_selfsigned_with_tls_cacerts(app):
+ app.config.tls_cacerts = CERT_FILE
+ with https_server(OKHandler):
+ app.builder.build_all()
+
+ with open(app.outdir / 'output.json') as fp:
+ content = json.load(fp)
+ assert content == {
+ "code": 0,
+ "status": "working",
+ "filename": "index.rst",
+ "lineno": 1,
+ "uri": "https://localhost:7777/",
+ "info": "",
+ }
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
+def test_connect_to_selfsigned_with_requests_env_var(app):
+ with modify_env(REQUESTS_CA_BUNDLE=CERT_FILE), https_server(OKHandler):
+ app.builder.build_all()
+
+ with open(app.outdir / 'output.json') as fp:
+ content = json.load(fp)
+ assert content == {
+ "code": 0,
+ "status": "working",
+ "filename": "index.rst",
+ "lineno": 1,
+ "uri": "https://localhost:7777/",
+ "info": "",
+ }
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
+def test_connect_to_selfsigned_nonexistent_cert_file(app):
+ app.config.tls_cacerts = "does/not/exist"
+ with https_server(OKHandler):
+ app.builder.build_all()
+
+ with open(app.outdir / 'output.json') as fp:
+ content = json.load(fp)
+ assert content == {
+ "code": 0,
+ "status": "broken",
+ "filename": "index.rst",
+ "lineno": 1,
+ "uri": "https://localhost:7777/",
+ "info": "Could not find a suitable TLS CA certificate bundle, invalid path: does/not/exist",
+ }
diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py
index 57a7c49e6..28e74e7bf 100644
--- a/tests/test_domain_c.py
+++ b/tests/test_domain_c.py
@@ -75,12 +75,12 @@ def _check(name, input, idDict, output, key, asTextOutput):
idExpected.append(idExpected[i - 1])
idActual = [None]
for i in range(1, _max_id + 1):
- #try:
+ # try:
id = ast.get_id(version=i)
assert id is not None
idActual.append(id[len(_id_prefix[i]):])
- #except NoOldIdError:
- # idActual.append(None)
+ # except NoOldIdError:
+ # idActual.append(None)
res = [True]
for i in range(1, _max_id + 1):
@@ -94,7 +94,7 @@ def _check(name, input, idDict, output, key, asTextOutput):
print("Error in id version %d." % i)
print("result: %s" % idActual[i])
print("expected: %s" % idExpected[i])
- #print(rootSymbol.dump(0))
+ # print(rootSymbol.dump(0))
raise DefinitionError("")
@@ -106,7 +106,7 @@ def check(name, input, idDict, output=None, key=None, asTextOutput=None):
if name != 'macro':
# Second, check with semicolon
_check(name, input + ' ;', idDict, output + ';', key,
- asTextOutput + ';' if asTextOutput is not None else None)
+ asTextOutput + ';' if asTextOutput is not None else None)
def test_expressions():
@@ -422,7 +422,7 @@ def test_nested_name():
check('function', 'void f(.A.B a)', {1: "f"})
-def test_union_definitions():
+def test_struct_definitions():
check('struct', '{key}A', {1: 'A'})
@@ -482,7 +482,7 @@ def test_attributes():
# style: user-defined paren
check('member', 'paren_attr() int f', {1: 'f'})
check('member', 'paren_attr(a) int f', {1: 'f'})
- check('member', 'paren_attr("") int f',{1: 'f'})
+ check('member', 'paren_attr("") int f', {1: 'f'})
check('member', 'paren_attr(()[{}][]{}) int f', {1: 'f'})
with pytest.raises(DefinitionError):
parse('member', 'paren_attr(() int f')
@@ -521,7 +521,7 @@ def test_attributes():
def filter_warnings(warning, file):
- lines = warning.getvalue().split("\n");
+ lines = warning.getvalue().split("\n")
res = [l for l in lines if "domain-c" in l and "{}.rst".format(file) in l and
"WARNING: document isn't included in any toctree" not in l]
print("Filtered warnings for file '{}':".format(file))
@@ -602,6 +602,7 @@ def _get_obj(app, queryName):
return (docname, anchor, objectType)
return (queryName, "not", "found")
+
def test_cfunction(app):
text = (".. c:function:: PyObject* "
"PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)")
diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py
index e9962f358..0b9e31d2a 100644
--- a/tests/test_domain_cpp.py
+++ b/tests/test_domain_cpp.py
@@ -181,9 +181,9 @@ def test_expressions():
expr = i + l + u
exprCheck(expr, 'L' + expr + 'E')
decimalFloats = ['5e42', '5e+42', '5e-42',
- '5.', '5.e42', '5.e+42', '5.e-42',
- '.5', '.5e42', '.5e+42', '.5e-42',
- '5.0', '5.0e42', '5.0e+42', '5.0e-42']
+ '5.', '5.e42', '5.e+42', '5.e-42',
+ '.5', '.5e42', '.5e+42', '.5e-42',
+ '5.0', '5.0e42', '5.0e+42', '5.0e-42']
hexFloats = ['ApF', 'Ap+F', 'Ap-F',
'A.', 'A.pF', 'A.p+F', 'A.p-F',
'.A', '.ApF', '.Ap+F', '.Ap-F',
@@ -424,9 +424,9 @@ def test_member_definitions():
check('member', 'int b : 8 = 42', {1: 'b__i', 2: '1b'})
check('member', 'int b : 8{42}', {1: 'b__i', 2: '1b'})
# TODO: enable once the ternary operator is supported
- #check('member', 'int b : true ? 8 : a = 42', {1: 'b__i', 2: '1b'})
+ # check('member', 'int b : true ? 8 : a = 42', {1: 'b__i', 2: '1b'})
# TODO: enable once the ternary operator is supported
- #check('member', 'int b : (true ? 8 : a) = 42', {1: 'b__i', 2: '1b'})
+ # check('member', 'int b : (true ? 8 : a) = 42', {1: 'b__i', 2: '1b'})
check('member', 'int b : 1 || new int{0}', {1: 'b__i', 2: '1b'})
@@ -536,8 +536,8 @@ def test_function_definitions():
check('function', 'int foo(const A*...)', {1: "foo__ACPDp", 2: "3fooDpPK1A"})
check('function', 'int foo(const int A::*... a)', {2: "3fooDpM1AKi"})
check('function', 'int foo(const int A::*...)', {2: "3fooDpM1AKi"})
- #check('function', 'int foo(int (*a)(A)...)', {1: "foo__ACRDp", 2: "3fooDpPK1A"})
- #check('function', 'int foo(int (*)(A)...)', {1: "foo__ACRDp", 2: "3fooDpPK1A"})
+ # check('function', 'int foo(int (*a)(A)...)', {1: "foo__ACRDp", 2: "3fooDpPK1A"})
+ # check('function', 'int foo(int (*)(A)...)', {1: "foo__ACRDp", 2: "3fooDpPK1A"})
check('function', 'virtual void f()', {1: "f", 2: "1fv"})
# test for ::nestedName, from issue 1738
check("function", "result(int val, ::std::error_category const &cat)",
@@ -706,7 +706,6 @@ def test_class_definitions():
check('class', 'template<class T> {key}has_var<T, std::void_t<decltype(&T::var)>>',
{2: 'I0E7has_varI1TNSt6void_tIDTadN1T3varEEEEE'})
-
check('class', 'template<typename ...Ts> {key}T<int (*)(Ts)...>',
{2: 'IDpE1TIJPFi2TsEEE'})
check('class', 'template<int... Is> {key}T<(Is)...>',
@@ -1000,7 +999,7 @@ def test_build_domain_cpp_warn_template_param_qualified_name(app, status, warnin
@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True})
-def test_build_domain_cpp_backslash_ok(app, status, warning):
+def test_build_domain_cpp_backslash_ok_true(app, status, warning):
app.builder.build_all()
ws = filter_warnings(warning, "backslash")
assert len(ws) == 0
@@ -1015,7 +1014,7 @@ def test_build_domain_cpp_semicolon(app, status, warning):
@pytest.mark.sphinx(testroot='domain-cpp',
confoverrides={'nitpicky': True, 'strip_signature_backslash': True})
-def test_build_domain_cpp_backslash_ok(app, status, warning):
+def test_build_domain_cpp_backslash_ok_false(app, status, warning):
app.builder.build_all()
ws = filter_warnings(warning, "backslash")
assert len(ws) == 1
@@ -1245,4 +1244,4 @@ def test_mix_decl_duplicate(app, warning):
assert "Declaration is '.. cpp:function:: void A()'." in ws[1]
assert "index.rst:3: WARNING: Duplicate C++ declaration, also defined at index:1." in ws[2]
assert "Declaration is '.. cpp:struct:: A'." in ws[3]
- assert ws[4] == "" \ No newline at end of file
+ assert ws[4] == ""
diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py
index fd6c40df6..8c8004a73 100644
--- a/tests/test_domain_std.py
+++ b/tests/test_domain_std.py
@@ -330,7 +330,7 @@ def test_multiple_cmdoptions(app):
def test_productionlist(app, status, warning):
app.builder.build_all()
- warnings = warning.getvalue().split("\n");
+ warnings = warning.getvalue().split("\n")
assert len(warnings) == 2
assert warnings[-1] == ''
assert "Dup2.rst:4: WARNING: duplicate token description of Dup, other instance in Dup1" in warnings[0]
diff --git a/tests/test_environment_indexentries.py b/tests/test_environment_indexentries.py
index c15226d85..075022be5 100644
--- a/tests/test_environment_indexentries.py
+++ b/tests/test_environment_indexentries.py
@@ -38,8 +38,9 @@ def test_create_single_index(app):
('upgrade', [('', '#index-3')])], None]),
('Python', [[('', '#index-1')], [], None])])
assert index[3] == ('S', [('Sphinx', [[('', '#index-4')], [], None])])
- assert index[4] == ('Е', [('ёлка', [[('', '#index-6')], [], None]),
- ('Ель', [[('', '#index-5')], [], None])])
+ assert index[4] == ('Е',
+ [('ёлка', [[('', '#index-6')], [], None]),
+ ('Ель', [[('', '#index-5')], [], None])])
assert index[5] == ('ת', [('‏תירבע‎', [[('', '#index-7')], [], None])])
@@ -69,8 +70,9 @@ def test_create_pair_index(app):
('ёлка', [('', '#index-5')]),
('Ель', [('', '#index-4')])],
None])])
- assert index[6] == ('Е', [('ёлка', [[], [('Sphinx', [('', '#index-5')])], None]),
- ('Ель', [[], [('Sphinx', [('', '#index-4')])], None])])
+ assert index[6] == ('Е',
+ [('ёлка', [[], [('Sphinx', [('', '#index-5')])], None]),
+ ('Ель', [[], [('Sphinx', [('', '#index-4')])], None])])
@pytest.mark.sphinx('dummy', freshenv=True)
diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py
index 041aefc9f..20eda86af 100644
--- a/tests/test_ext_autodoc.py
+++ b/tests/test_ext_autodoc.py
@@ -177,7 +177,6 @@ def test_format_signature(app):
for C in (D, E):
assert formatsig('class', 'D', C, None, None) == '()'
-
class SomeMeta(type):
def __call__(cls, a, b=None):
return type.__call__(cls, a, b)
@@ -209,7 +208,6 @@ def test_format_signature(app):
assert formatsig('class', 'C', C, None, None) == '(a, b=None)'
assert formatsig('class', 'C', D, 'a, b', 'X') == '(a, b) -> X'
-
class ListSubclass(list):
pass
@@ -219,7 +217,6 @@ def test_format_signature(app):
else:
assert formatsig('class', 'C', ListSubclass, None, None) == ''
-
class ExceptionSubclass(Exception):
pass
@@ -227,7 +224,6 @@ def test_format_signature(app):
if getattr(Exception, '__text_signature__', None) is None:
assert formatsig('class', 'C', ExceptionSubclass, None, None) == ''
-
# __init__ have signature at first line of docstring
directive.env.config.autoclass_content = 'both'
diff --git a/tests/test_ext_autodoc_autoattribute.py b/tests/test_ext_autodoc_autoattribute.py
new file mode 100644
index 000000000..31dccbd03
--- /dev/null
+++ b/tests/test_ext_autodoc_autoattribute.py
@@ -0,0 +1,71 @@
+"""
+ test_ext_autodoc_autoattribute
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Test the autodoc extension. This tests mainly the Documenters; the auto
+ directives are tested in a test source file translated by test_build.
+
+ :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import sys
+
+import pytest
+from test_ext_autodoc import do_autodoc
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_autoattribute(app):
+ actual = do_autodoc(app, 'attribute', 'target.Class.attr')
+ assert list(actual) == [
+ '',
+ '.. py:attribute:: Class.attr',
+ ' :module: target',
+ " :value: 'bar'",
+ '',
+ ' should be documented -- süß',
+ '',
+ ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_autoattribute_novalue(app):
+ options = {'no-value': True}
+ actual = do_autodoc(app, 'attribute', 'target.Class.attr', options)
+ assert list(actual) == [
+ '',
+ '.. py:attribute:: Class.attr',
+ ' :module: target',
+ '',
+ ' should be documented -- süß',
+ '',
+ ]
+
+
+@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_autoattribute_typed_variable(app):
+ actual = do_autodoc(app, 'attribute', 'target.typed_vars.Class.attr2')
+ assert list(actual) == [
+ '',
+ '.. py:attribute:: Class.attr2',
+ ' :module: target.typed_vars',
+ ' :type: int',
+ '',
+ ]
+
+
+@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_autoattribute_instance_variable(app):
+ actual = do_autodoc(app, 'attribute', 'target.typed_vars.Class.attr4')
+ assert list(actual) == [
+ '',
+ '.. py:attribute:: Class.attr4',
+ ' :module: target.typed_vars',
+ ' :type: int',
+ '',
+ ' attr4',
+ '',
+ ]
diff --git a/tests/test_ext_autodoc_autodata.py b/tests/test_ext_autodoc_autodata.py
new file mode 100644
index 000000000..72665cdba
--- /dev/null
+++ b/tests/test_ext_autodoc_autodata.py
@@ -0,0 +1,74 @@
+"""
+ test_ext_autodoc_autodata
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Test the autodoc extension. This tests mainly the Documenters; the auto
+ directives are tested in a test source file translated by test_build.
+
+ :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import sys
+
+import pytest
+from test_ext_autodoc import do_autodoc
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_autodata(app):
+ actual = do_autodoc(app, 'data', 'target.integer')
+ assert list(actual) == [
+ '',
+ '.. py:data:: integer',
+ ' :module: target',
+ ' :value: 1',
+ '',
+ ' documentation for the integer',
+ '',
+ ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_autodata_novalue(app):
+ options = {'no-value': True}
+ actual = do_autodoc(app, 'data', 'target.integer', options)
+ assert list(actual) == [
+ '',
+ '.. py:data:: integer',
+ ' :module: target',
+ '',
+ ' documentation for the integer',
+ '',
+ ]
+
+
+@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_autodata_typed_variable(app):
+ actual = do_autodoc(app, 'data', 'target.typed_vars.attr2')
+ assert list(actual) == [
+ '',
+ '.. py:data:: attr2',
+ ' :module: target.typed_vars',
+ ' :type: str',
+ '',
+ ' attr2',
+ '',
+ ]
+
+
+@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_autodata_type_comment(app):
+ actual = do_autodoc(app, 'data', 'target.typed_vars.attr3')
+ assert list(actual) == [
+ '',
+ '.. py:data:: attr3',
+ ' :module: target.typed_vars',
+ ' :type: str',
+ " :value: ''",
+ '',
+ ' attr3',
+ '',
+ ]
diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py
index 3d1005ac0..4dff7c3db 100644
--- a/tests/test_ext_autodoc_configs.py
+++ b/tests/test_ext_autodoc_configs.py
@@ -700,6 +700,19 @@ def test_autodoc_type_aliases(app):
'.. py:module:: target.annotations',
'',
'',
+ '.. py:class:: Foo()',
+ ' :module: target.annotations',
+ '',
+ ' docstring',
+ '',
+ '',
+ ' .. py:attribute:: Foo.attr',
+ ' :module: target.annotations',
+ ' :type: int',
+ '',
+ ' docstring',
+ '',
+ '',
'.. py:function:: mult(x: int, y: int) -> int',
' mult(x: float, y: float) -> float',
' :module: target.annotations',
@@ -712,6 +725,13 @@ def test_autodoc_type_aliases(app):
'',
' docstring',
'',
+ '',
+ '.. py:data:: variable',
+ ' :module: target.annotations',
+ ' :type: int',
+ '',
+ ' docstring',
+ '',
]
# define aliases
@@ -722,6 +742,19 @@ def test_autodoc_type_aliases(app):
'.. py:module:: target.annotations',
'',
'',
+ '.. py:class:: Foo()',
+ ' :module: target.annotations',
+ '',
+ ' docstring',
+ '',
+ '',
+ ' .. py:attribute:: Foo.attr',
+ ' :module: target.annotations',
+ ' :type: myint',
+ '',
+ ' docstring',
+ '',
+ '',
'.. py:function:: mult(x: myint, y: myint) -> myint',
' mult(x: float, y: float) -> float',
' :module: target.annotations',
@@ -734,6 +767,13 @@ def test_autodoc_type_aliases(app):
'',
' docstring',
'',
+ '',
+ '.. py:data:: variable',
+ ' :module: target.annotations',
+ ' :type: myint',
+ '',
+ ' docstring',
+ '',
]
diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py
index ef621e2b6..63a62aeab 100644
--- a/tests/test_ext_intersphinx.py
+++ b/tests/test_ext_intersphinx.py
@@ -11,7 +11,6 @@
import http.server
import os
import unittest
-from io import BytesIO
from unittest import mock
import pytest
diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py
index 0d2f8727e..220a394d4 100644
--- a/tests/test_ext_napoleon_docstring.py
+++ b/tests/test_ext_napoleon_docstring.py
@@ -1070,7 +1070,7 @@ Methods:
description
-"""
+""" # NOQA
config = Config()
actual = str(GoogleDocstring(docstring, config=config, app=None, what='module',
options={'noindex': True}))
@@ -2222,7 +2222,7 @@ definition_after_normal_text : int
["{", "'F'", ", ", "'C'", ", ", "'N or C'", "}", ", ", "default", " ", "'F'"],
["str", ", ", "default", ": ", "'F or C'"],
["int", ", ", "default", ": ", "None"],
- ["int", ", " , "default", " ", "None"],
+ ["int", ", ", "default", " ", "None"],
["int", ", ", "default", " ", ":obj:`None`"],
['"ma{icious"'],
[r"'with \'quotes\''"],
diff --git a/tests/test_intl.py b/tests/test_intl.py
index 4ffbf506d..775ffcd80 100644
--- a/tests/test_intl.py
+++ b/tests/test_intl.py
@@ -92,15 +92,6 @@ def assert_count(expected_expr, result, count):
@sphinx_intl
@pytest.mark.sphinx('text')
@pytest.mark.test_params(shared_result='test_intl_basic')
-def test_text_toctree(app):
- app.build()
- result = (app.outdir / 'index.txt').read_text()
- assert_startswith(result, "CONTENTS\n********\n\nTABLE OF CONTENTS\n")
-
-
-@sphinx_intl
-@pytest.mark.sphinx('text')
-@pytest.mark.test_params(shared_result='test_intl_basic')
def test_text_emit_warnings(app, warning):
app.build()
# test warnings in translation
@@ -436,11 +427,16 @@ def test_text_admonitions(app):
@pytest.mark.test_params(shared_result='test_intl_gettext')
def test_gettext_toctree(app):
app.build()
- # --- toctree
+ # --- toctree (index.rst)
expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'index.po')
actual = read_po(app.outdir / 'index.pot')
for expect_msg in [m for m in expect if m.id]:
assert expect_msg.id in [m.id for m in actual if m.id]
+ # --- toctree (toctree.rst)
+ expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po')
+ actual = read_po(app.outdir / 'toctree.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.id in [m.id for m in actual if m.id]
@sphinx_intl
@@ -468,23 +464,16 @@ def test_text_table(app):
@sphinx_intl
-@pytest.mark.sphinx('gettext')
-@pytest.mark.test_params(shared_result='test_intl_gettext')
-def test_gettext_toctree(app):
- app.build()
- # --- toctree
- expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po')
- actual = read_po(app.outdir / 'toctree.pot')
- for expect_msg in [m for m in expect if m.id]:
- assert expect_msg.id in [m.id for m in actual if m.id]
-
-
-@sphinx_intl
@pytest.mark.sphinx('text')
@pytest.mark.test_params(shared_result='test_intl_basic')
def test_text_toctree(app):
app.build()
- # --- toctree
+ # --- toctree (index.rst)
+ # Note: index.rst contains contents that is not shown in text.
+ result = (app.outdir / 'index.txt').read_text()
+ assert 'CONTENTS' in result
+ assert 'TABLE OF CONTENTS' in result
+ # --- toctree (toctree.rst)
result = (app.outdir / 'toctree.txt').read_text()
expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po')
for expect_msg in [m for m in expect if m.id]:
diff --git a/tests/test_markup.py b/tests/test_markup.py
index e7d855c36..a2bcb2dc1 100644
--- a/tests/test_markup.py
+++ b/tests/test_markup.py
@@ -17,7 +17,6 @@ from docutils.parsers.rst import Parser as RstParser
from sphinx import addnodes
from sphinx.builders.html.transforms import KeyboardTransform
from sphinx.builders.latex import LaTeXBuilder
-from sphinx.builders.latex.theming import ThemeFactory
from sphinx.roles import XRefRole
from sphinx.testing.util import Struct, assert_node
from sphinx.transforms import SphinxSmartQuotes
diff --git a/tests/test_project.py b/tests/test_project.py
index 50b06f7b8..906a52bfe 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -13,7 +13,6 @@ from collections import OrderedDict
import pytest
from sphinx.project import Project
-from sphinx.testing.comparer import PathComparer
def test_project_discover(rootdir):
diff --git a/tests/test_pycode.py b/tests/test_pycode.py
index ac3b34c9f..3d69e53cb 100644
--- a/tests/test_pycode.py
+++ b/tests/test_pycode.py
@@ -19,6 +19,7 @@ from sphinx.pycode import ModuleAnalyzer
SPHINX_MODULE_PATH = os.path.splitext(sphinx.__file__)[0] + '.py'
+
def test_ModuleAnalyzer_get_module_source():
assert ModuleAnalyzer.get_module_source('sphinx') == (sphinx.__file__, sphinx.__loader__.get_source('sphinx'))
diff --git a/tests/test_util.py b/tests/test_util.py
index c58931bb4..2d03ed89a 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -14,8 +14,7 @@ from unittest.mock import patch
import pytest
-import sphinx
-from sphinx.errors import ExtensionError, PycodeError
+from sphinx.errors import ExtensionError
from sphinx.testing.util import strip_escseq
from sphinx.util import (SkipProgressMessage, display_chunk, encode_uri, ensuredir,
import_object, logging, parselinenos, progress_message,
diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py
index e87e94f1d..72c985f34 100644
--- a/tests/test_util_inspect.py
+++ b/tests/test_util_inspect.py
@@ -19,7 +19,7 @@ import _testcapi
import pytest
from sphinx.util import inspect
-from sphinx.util.inspect import is_builtin_class_method, stringify_signature
+from sphinx.util.inspect import stringify_signature
def test_signature():
@@ -490,6 +490,28 @@ def test_dict_customtype():
assert "<CustomType(2)>: 2" in description
+def test_getslots():
+ class Foo:
+ pass
+
+ class Bar:
+ __slots__ = ['attr']
+
+ class Baz:
+ __slots__ = {'attr': 'docstring'}
+
+ class Qux:
+ __slots__ = 'attr'
+
+ assert inspect.getslots(Foo) is None
+ assert inspect.getslots(Bar) == {'attr': None}
+ assert inspect.getslots(Baz) == {'attr': 'docstring'}
+ assert inspect.getslots(Qux) == {'attr': None}
+
+ with pytest.raises(TypeError):
+ inspect.getslots(Bar())
+
+
@pytest.mark.sphinx(testroot='ext-autodoc')
def test_isclassmethod(app):
from target.methods import Base, Inherited
diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py
index 354db1567..73b4aca53 100644
--- a/tests/test_util_typing.py
+++ b/tests/test_util_typing.py
@@ -10,8 +10,7 @@
import sys
from numbers import Integral
-from typing import (Any, Callable, Dict, Generator, Generic, List, Optional, Tuple, TypeVar,
- Union)
+from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, TypeVar, Union
import pytest
@@ -25,8 +24,10 @@ class MyClass1:
class MyClass2(MyClass1):
__qualname__ = '<MyClass2>'
+
T = TypeVar('T')
+
class MyList(List[T]):
pass
@@ -132,8 +133,8 @@ def test_stringify_type_hints_containers():
@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')
def test_stringify_Annotated():
- from typing import Annotated
- assert stringify(Annotated[str, "foo", "bar"]) == "str"
+ from typing import Annotated # type: ignore
+ assert stringify(Annotated[str, "foo", "bar"]) == "str" # NOQA
def test_stringify_type_hints_string():
diff --git a/tests/typing_test_data.py b/tests/typing_test_data.py
index 8b30c843f..c2db7d95b 100644
--- a/tests/typing_test_data.py
+++ b/tests/typing_test_data.py
@@ -77,7 +77,7 @@ def f14() -> Any:
pass
-def f15(x: "Unknown", y: "int") -> Any:
+def f15(x: "Unknown", y: "int") -> Any: # type: ignore # NOQA
pass
diff --git a/tests/utils.py b/tests/utils.py
index 182dc1df0..eb2c40c52 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,7 +1,15 @@
import contextlib
import http.server
+import os
+import pathlib
+import ssl
import threading
+# Generated with:
+# $ openssl req -new -x509 -days 3650 -nodes -out cert.pem \
+# -keyout cert.pem -addext "subjectAltName = DNS:localhost"
+CERT_FILE = str(pathlib.Path(__file__).parent / "certs" / "cert.pem")
+
class HttpServerThread(threading.Thread):
def __init__(self, handler, *args, **kwargs):
@@ -17,11 +25,41 @@ class HttpServerThread(threading.Thread):
self.join()
+class HttpsServerThread(HttpServerThread):
+ def __init__(self, handler, *args, **kwargs):
+ super().__init__(handler, *args, **kwargs)
+ self.server.socket = ssl.wrap_socket(
+ self.server.socket,
+ certfile=CERT_FILE,
+ server_side=True,
+ )
+
+
+def create_server(thread_class):
+ def server(handler):
+ server_thread = thread_class(handler, daemon=True)
+ server_thread.start()
+ try:
+ yield server_thread
+ finally:
+ server_thread.terminate()
+ return contextlib.contextmanager(server)
+
+
+http_server = create_server(HttpServerThread)
+https_server = create_server(HttpsServerThread)
+
+
@contextlib.contextmanager
-def http_server(handler):
- server_thread = HttpServerThread(handler, daemon=True)
- server_thread.start()
+def modify_env(**env):
+ original_env = os.environ.copy()
+ for k, v in env.items():
+ os.environ[k] = v
try:
- yield server_thread
+ yield
finally:
- server_thread.terminate()
+ for k in env:
+ try:
+ os.environ[k] = original_env[k]
+ except KeyError:
+ os.unsetenv(k)