diff options
author | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-05-29 19:22:39 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-29 19:22:39 +0300 |
commit | 28fe0d639974c87063abce31ff38134bc4c568a2 (patch) | |
tree | acbdd75ae2ef86018e0254f4dcfc52d402bfc071 /sphinx | |
parent | 638b197d25ad0530763572d8de2c4615643fe150 (diff) | |
parent | 03559f057422da6b6a1d518cb58584b1e834a5fd (diff) |
Merge branch '3.x' into 7701_anonymous_indirect_target
Diffstat (limited to 'sphinx')
-rw-r--r-- | sphinx/application.py | 11 | ||||
-rw-r--r-- | sphinx/builders/latex/__init__.py | 9 | ||||
-rw-r--r-- | sphinx/ext/autodoc/__init__.py | 215 | ||||
-rw-r--r-- | sphinx/ext/napoleon/docstring.py | 2 | ||||
-rw-r--r-- | sphinx/registry.py | 9 | ||||
-rw-r--r-- | sphinx/templates/latex/latex.tex_t | 8 | ||||
-rw-r--r-- | sphinx/util/inspect.py | 2 |
7 files changed, 195 insertions, 61 deletions
diff --git a/sphinx/application.py b/sphinx/application.py index b02fb4d60..bd23c86e7 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -968,12 +968,14 @@ class Sphinx: self.add_css_file(filename, **attributes) - def add_latex_package(self, packagename: str, options: str = None) -> None: + def add_latex_package(self, packagename: str, options: str = None, + after_hyperref: bool = False) -> None: r"""Register a package to include in the LaTeX source code. Add *packagename* to the list of packages that LaTeX source code will include. If you provide *options*, it will be taken to `\usepackage` - declaration. + declaration. If you set *after_hyperref* truthy, the package will be + loaded after ``hyperref`` package. .. code-block:: python @@ -983,8 +985,11 @@ class Sphinx: # => \usepackage[foo,bar]{mypackage} .. versionadded:: 1.3 + .. versionadded:: 3.1 + + *after_hyperref* option. """ - self.registry.add_latex_package(packagename, options) + self.registry.add_latex_package(packagename, options, after_hyperref) def add_lexer(self, alias: str, lexer: Union[Lexer, "Type[Lexer]"]) -> None: """Register a new lexer for source code. diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py index 225975c20..88c471675 100644 --- a/sphinx/builders/latex/__init__.py +++ b/sphinx/builders/latex/__init__.py @@ -129,6 +129,7 @@ class LaTeXBuilder(Builder): self.document_data = [] # type: List[Tuple[str, str, str, str, str, bool]] self.themes = ThemeFactory(self.app) self.usepackages = self.app.registry.latex_packages + self.usepackages_after_hyperref = self.app.registry.latex_packages_after_hyperref texescape.init() self.init_context() @@ -180,6 +181,7 @@ class LaTeXBuilder(Builder): # Apply extension settings to context self.context['packages'] = self.usepackages + self.context['packages_after_hyperref'] = self.usepackages_after_hyperref # Apply user settings to context self.context.update(self.config.latex_elements) @@ -501,6 +503,12 @@ def validate_latex_theme_options(app: Sphinx, config: Config) -> None: config.latex_theme_options.pop(key) +def install_pakcages_for_ja(app: Sphinx) -> None: + """Install packages for Japanese.""" + if app.config.language == 'ja': + app.add_latex_package('pxjahyper', after_hyperref=True) + + def default_latex_engine(config: Config) -> str: """ Better default latex_engine settings for specific languages. """ if config.language == 'ja': @@ -548,6 +556,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_builder(LaTeXBuilder) app.connect('config-inited', validate_config_values, priority=800) app.connect('config-inited', validate_latex_theme_options, priority=800) + app.connect('builder-inited', install_pakcages_for_ja) app.add_config_value('latex_engine', default_latex_engine, None, ENUM('pdflatex', 'xelatex', 'lualatex', 'platex', 'uplatex')) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index f9c775f7d..f8e4be999 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -13,7 +13,7 @@ import importlib import re import warnings -from inspect import Parameter +from inspect import Parameter, Signature from types import ModuleType from typing import ( Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union @@ -392,6 +392,17 @@ class Documenter: # directives of course) return '.'.join(self.objpath) or self.modname + def _call_format_args(self, **kwargs: Any) -> str: + if kwargs: + try: + return self.format_args(**kwargs) + except TypeError: + # avoid chaining exceptions, by putting nothing here + pass + + # retry without arguments for old documenters + return self.format_args() + def format_signature(self, **kwargs: Any) -> str: """Format the signature (arguments and return annotation) of the object. @@ -405,12 +416,7 @@ class Documenter: # try to introspect the signature try: retann = None - try: - args = self.format_args(**kwargs) - except TypeError: - # retry without arguments for old documenters - args = self.format_args() - + args = self._call_format_args(**kwargs) if args: matched = re.match(r'^(\(.*\))\s+->\s+(.*)$', args) if matched: @@ -714,12 +720,26 @@ class Documenter: '.'.join(self.objpath + [mname]) documenter = classes[-1](self.directive, full_mname, self.indent) memberdocumenters.append((documenter, isattr)) - member_order = self.options.member_order or \ - self.env.config.autodoc_member_order - if member_order == 'groupwise': + + member_order = self.options.member_order or self.env.config.autodoc_member_order + memberdocumenters = self.sort_members(memberdocumenters, member_order) + + for documenter, isattr in memberdocumenters: + documenter.generate( + all_members=True, real_modname=self.real_modname, + check_module=members_check_module and not isattr) + + # reset current objects + self.env.temp_data['autodoc:module'] = None + self.env.temp_data['autodoc:class'] = None + + def sort_members(self, documenters: List[Tuple["Documenter", bool]], + order: str) -> List[Tuple["Documenter", bool]]: + """Sort the given member list.""" + if order == 'groupwise': # sort by group; alphabetically within groups - memberdocumenters.sort(key=lambda e: (e[0].member_order, e[0].name)) - elif member_order == 'bysource': + documenters.sort(key=lambda e: (e[0].member_order, e[0].name)) + elif order == 'bysource': if self.analyzer: # sort by source order, by virtue of the module analyzer tagorder = self.analyzer.tagorder @@ -727,25 +747,16 @@ class Documenter: def keyfunc(entry: Tuple[Documenter, bool]) -> int: fullname = entry[0].name.split('::')[1] return tagorder.get(fullname, len(tagorder)) - memberdocumenters.sort(key=keyfunc) + documenters.sort(key=keyfunc) else: # Assume that member discovery order matches source order. # This is a reasonable assumption in Python 3.6 and up, where # module.__dict__ is insertion-ordered. pass - elif member_order == 'alphabetical': - memberdocumenters.sort(key=lambda e: e[0].name) - else: - raise ValueError("Illegal member order {}".format(member_order)) + else: # alphabetical + documenters.sort(key=lambda e: e[0].name) - for documenter, isattr in memberdocumenters: - documenter.generate( - all_members=True, real_modname=self.real_modname, - check_module=members_check_module and not isattr) - - # reset current objects - self.env.temp_data['autodoc:module'] = None - self.env.temp_data['autodoc:class'] = None + return documenters def generate(self, more_content: Any = None, real_modname: str = None, check_module: bool = False, all_members: bool = False) -> None: @@ -852,6 +863,7 @@ class ModuleDocumenter(Documenter): def __init__(self, *args: Any) -> None: super().__init__(*args) merge_special_members_option(self.options) + self.__all__ = None @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any @@ -874,6 +886,30 @@ class ModuleDocumenter(Documenter): type='autodoc') return ret + def import_object(self) -> Any: + 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() + + 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') + + return ret + def add_directive_header(self, sig: str) -> None: Documenter.add_directive_header(self, sig) @@ -889,24 +925,12 @@ class ModuleDocumenter(Documenter): def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, Any]]]: if want_all: - if (self.options.ignore_module_all or not - hasattr(self.object, '__all__')): + if self.__all__: + memberlist = self.__all__ + else: # for implicit module members, check __module__ to avoid # documenting imported objects return True, get_module_members(self.object) - else: - memberlist = self.object.__all__ - # Sometimes __all__ is broken... - if not isinstance(memberlist, (list, tuple)) or not \ - all(isinstance(entry, str) for entry in memberlist): - logger.warning( - __('__all__ should be a list of strings, not %r ' - '(in module %s) -- ignoring __all__') % - (memberlist, self.fullname), - type='autodoc' - ) - # fall back to all members - return True, get_module_members(self.object) else: memberlist = self.options.members or [] ret = [] @@ -922,6 +946,25 @@ class ModuleDocumenter(Documenter): ) return False, ret + def sort_members(self, documenters: List[Tuple["Documenter", bool]], + order: str) -> List[Tuple["Documenter", bool]]: + if order == 'bysource' and self.__all__: + # Sort alphabetically first (for members not listed on the __all__) + documenters.sort(key=lambda e: e[0].name) + + # Sort by __all__ + def keyfunc(entry: Tuple[Documenter, bool]) -> int: + name = entry[0].name.split('::')[1] + if name in self.__all__: + return self.__all__.index(name) + else: + return len(self.__all__) + documenters.sort(key=keyfunc) + + return documenters + else: + return super().sort_members(documenters, order) + class ModuleLevelDocumenter(Documenter): """ @@ -1168,6 +1211,14 @@ class DecoratorDocumenter(FunctionDocumenter): return None +# Types which have confusing metaclass signatures it would be best not to show. +# These are listed by name, rather than storing the objects themselves, to avoid +# needing to import the modules. +_METACLASS_CALL_BLACKLIST = [ + 'enum.EnumMeta.__call__', +] + + class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore """ Specialized Documenter subclass for classes. @@ -1202,27 +1253,83 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: self.doc_as_attr = True return ret + def _get_signature(self) -> Optional[Signature]: + def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: + """ Get the `attr` function or method from `obj`, if it is user-defined. """ + if inspect.is_builtin_class_method(obj, attr): + return None + attr = self.get_attr(obj, attr, None) + if not (inspect.ismethod(attr) or inspect.isfunction(attr)): + return None + return attr + + # This sequence is copied from inspect._signature_from_callable. + # ValueError means that no signature could be found, so we keep going. + + # First, let's see if it has an overloaded __call__ defined + # in its metaclass + call = get_user_defined_function_or_method(type(self.object), '__call__') + + if call is not None: + if "{0.__module__}.{0.__qualname__}".format(call) in _METACLASS_CALL_BLACKLIST: + call = None + + if call is not None: + self.env.app.emit('autodoc-before-process-signature', call, True) + try: + return inspect.signature(call, bound_method=True) + except ValueError: + pass + + # Now we check if the 'obj' class has a '__new__' method + new = get_user_defined_function_or_method(self.object, '__new__') + if new is not None: + self.env.app.emit('autodoc-before-process-signature', new, True) + try: + return inspect.signature(new, bound_method=True) + except ValueError: + pass + + # Finally, we should have at least __init__ implemented + init = get_user_defined_function_or_method(self.object, '__init__') + if init is not None: + self.env.app.emit('autodoc-before-process-signature', init, True) + try: + return inspect.signature(init, bound_method=True) + except ValueError: + pass + + # None of the attributes are user-defined, so fall back to let inspect + # handle it. + # We don't know the exact method that inspect.signature will read + # the signature from, so just pass the object itself to our hook. + self.env.app.emit('autodoc-before-process-signature', self.object, False) + try: + return inspect.signature(self.object, bound_method=False) + except ValueError: + pass + + # Still no signature: happens e.g. for old-style classes + # with __init__ in C and no `__text_signature__`. + return None + def format_args(self, **kwargs: Any) -> str: if self.env.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) - # for classes, the relevant signature is the __init__ method's - initmeth = self.get_attr(self.object, '__init__', None) - # classes without __init__ method, default __init__ or - # __init__ written in C? - if initmeth is None or \ - inspect.is_builtin_class_method(self.object, '__init__') or \ - not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)): - return None try: - self.env.app.emit('autodoc-before-process-signature', initmeth, True) - sig = inspect.signature(initmeth, bound_method=True) - return stringify_signature(sig, show_return_annotation=False, **kwargs) - except TypeError: - # still not possible: happens e.g. for old-style classes - # with __init__ in C + sig = self._get_signature() + except TypeError as exc: + # __signature__ attribute contained junk + logger.warning(__("Failed to get a constructor signature for %s: %s"), + self.fullname, exc) + return None + + if sig is None: return None + return stringify_signature(sig, show_return_annotation=False, **kwargs) + def format_signature(self, **kwargs: Any) -> str: if self.doc_as_attr: return '' diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 11409e6f6..32edd7f8f 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -318,7 +318,7 @@ class GoogleDocstring: return [line[min_indent:] for line in lines] def _escape_args_and_kwargs(self, name: str) -> str: - if name.endswith('_'): + if name.endswith('_') and getattr(self._config, 'strip_signature_backslash', False): name = name[:-1] + r'\_' if name[:2] == '**': diff --git a/sphinx/registry.py b/sphinx/registry.py index 200f59d42..cad74559c 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -98,6 +98,8 @@ class SphinxComponentRegistry: #: LaTeX packages; list of package names and its options self.latex_packages = [] # type: List[Tuple[str, str]] + self.latex_packages_after_hyperref = [] # type: List[Tuple[str, str]] + #: post transforms; list of transforms self.post_transforms = [] # type: List[Type[Transform]] @@ -363,9 +365,12 @@ class SphinxComponentRegistry: logger.debug('[app] adding js_file: %r, %r', filename, attributes) self.js_files.append((filename, attributes)) - def add_latex_package(self, name: str, options: str) -> None: + def add_latex_package(self, name: str, options: str, after_hyperref: bool = False) -> None: logger.debug('[app] adding latex package: %r', name) - self.latex_packages.append((name, options)) + if after_hyperref: + self.latex_packages_after_hyperref.append((name, options)) + else: + self.latex_packages.append((name, options)) def add_enumerable_node(self, node: "Type[Node]", figtype: str, title_getter: TitleGetter = None, override: bool = False) -> None: diff --git a/sphinx/templates/latex/latex.tex_t b/sphinx/templates/latex/latex.tex_t index a0a5a26b1..5082254e7 100644 --- a/sphinx/templates/latex/latex.tex_t +++ b/sphinx/templates/latex/latex.tex_t @@ -46,6 +46,14 @@ <%- endfor %> <%= hyperref %> +<%- for name, option in packages_after_hyperref %> +<%- if option %> +\usepackage[<%= option %>]{<%= name %>} +<%- else %> +\usepackage{<%= name %>} +<%- endif %> +<%- endfor %> + <%= contentsname %> \usepackage{sphinxmessages} <%= tocdepth %> diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 6ba698eed..15f0d66e2 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -726,7 +726,7 @@ def getdoc(obj: Any, attrgetter: Callable = safe_getattr, # This tries to obtain the docstring from super classes. for basecls in getattr(cls, '__mro__', []): meth = safe_getattr(basecls, name, None) - if meth: + if meth is not None: doc = inspect.getdoc(meth) if doc: break |