diff options
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 @@ -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:: @@ -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) |