diff options
author | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-11-20 19:01:36 +0300 |
---|---|---|
committer | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-11-20 19:01:36 +0300 |
commit | 3a4ae2092a6f236e2d71fdedb4b66f594e30d4e7 (patch) | |
tree | afa4f774e0f8660d5ce247f01cace9deaa848d58 /sphinx | |
parent | f3a6004f822b05e352372a77f449332ad230d21e (diff) | |
parent | 18b2707b2ac621f23f8ee6a7ca19bf1590be7826 (diff) |
Merge branch '3.x'
Diffstat (limited to 'sphinx')
-rw-r--r-- | sphinx/application.py | 6 | ||||
-rw-r--r-- | sphinx/builders/html/__init__.py | 1 | ||||
-rw-r--r-- | sphinx/builders/linkcheck.py | 15 | ||||
-rw-r--r-- | sphinx/ext/autodoc/__init__.py | 144 | ||||
-rw-r--r-- | sphinx/ext/autodoc/importer.py | 19 | ||||
-rw-r--r-- | sphinx/ext/autosummary/generate.py | 6 | ||||
-rw-r--r-- | sphinx/themes/basic/search.html | 1 | ||||
-rw-r--r-- | sphinx/util/inspect.py | 45 | ||||
-rw-r--r-- | sphinx/util/requests.py | 5 | ||||
-rw-r--r-- | sphinx/util/typing.py | 23 |
10 files changed, 163 insertions, 102 deletions
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__', '') |