diff options
author | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-01-08 01:37:53 +0900 |
---|---|---|
committer | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-01-08 01:37:53 +0900 |
commit | af2a3c0ddeb2f81c8f685e4d266adefc749a2642 (patch) | |
tree | f4b5f634fcc3e2315bf5eedaceed11a16ba0ff14 | |
parent | 92a204284b98510b3bfdd2a6391e9855c564a6a0 (diff) | |
parent | 8e1cbd24c61934df7eb426aad0dc48830789b096 (diff) | |
download | sphinx-git-af2a3c0ddeb2f81c8f685e4d266adefc749a2642.tar.gz |
Merge branch '2.0'
-rw-r--r-- | CHANGES | 8 | ||||
-rw-r--r-- | doc/extdev/deprecated.rst | 20 | ||||
-rw-r--r-- | sphinx/builders/latex/__init__.py | 2 | ||||
-rw-r--r-- | sphinx/builders/manpage.py | 2 | ||||
-rw-r--r-- | sphinx/builders/texinfo.py | 2 | ||||
-rw-r--r-- | sphinx/domains/cpp.py | 2 | ||||
-rw-r--r-- | sphinx/domains/std.py | 45 | ||||
-rw-r--r-- | sphinx/ext/apidoc.py | 2 | ||||
-rw-r--r-- | sphinx/transforms/i18n.py | 11 | ||||
-rw-r--r-- | sphinx/transforms/post_transforms/__init__.py | 2 | ||||
-rw-r--r-- | sphinx/util/inspect.py | 174 | ||||
-rw-r--r-- | sphinx/util/nodes.py | 23 | ||||
-rw-r--r-- | sphinx/util/typing.py | 154 | ||||
-rw-r--r-- | sphinx/writers/html.py | 35 | ||||
-rw-r--r-- | sphinx/writers/html5.py | 35 | ||||
-rw-r--r-- | tests/test_domain_std.py | 10 | ||||
-rw-r--r-- | tests/test_util_nodes.py | 19 | ||||
-rw-r--r-- | tests/test_util_typing.py | 98 |
18 files changed, 429 insertions, 215 deletions
@@ -52,6 +52,7 @@ Deprecated * The ``decode`` argument of ``sphinx.pycode.ModuleAnalyzer()`` * ``sphinx.directives.other.Index`` +* ``sphinx.environment.temp_data['gloss_entries']`` * ``sphinx.environment.BuildEnvironment.indexentries`` * ``sphinx.environment.collectors.indexentries.IndexEntriesCollector`` * ``sphinx.io.FiletypeNotFoundError`` @@ -60,6 +61,9 @@ Deprecated * ``sphinx.roles.Index`` * ``sphinx.util.detect_encoding()`` * ``sphinx.util.get_module_source()`` +* ``sphinx.util.inspect.Signature.format_annotation()`` +* ``sphinx.util.inspect.Signature.format_annotation_new()`` +* ``sphinx.util.inspect.Signature.format_annotation_old()`` Features added -------------- @@ -69,6 +73,8 @@ Features added down the build * #6837: LaTeX: Support a nested table * #6966: graphviz: Support ``:class:`` option +* #6696: html: ``:scale:`` option of image/figure directive not working for SVG + images (imagesize-1.2.0 or above is required) Bugs fixed ---------- @@ -76,6 +82,8 @@ Bugs fixed * #6925: html: Remove redundant type="text/javascript" from <script> elements * #6906, #6907: autodoc: failed to read the source codes encoeded in cp1251 * #6961: latex: warning for babel shown twice +* #6559: Wrong node-ids are generated in glossary directive +* #6986: apidoc: misdetects module name for .so file inside module Testing -------- diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 3deac657d..d91ea9308 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -41,6 +41,11 @@ The following is a list of deprecated interfaces. - 4.0 - ``sphinx.domains.index.IndexDirective`` + * - ``sphinx.environment.temp_data['gloss_entries']`` + - 2.4 + - 4.0 + - ``documents.nameids`` + * - ``sphinx.environment.BuildEnvironment.indexentries`` - 2.4 - 4.0 @@ -81,6 +86,21 @@ The following is a list of deprecated interfaces. - 4.0 - N/A + * - ``sphinx.util.inspect.Signature.format_annotation()`` + - 2.4 + - 4.0 + - ``sphinx.util.typing.stringify()`` + + * - ``sphinx.util.inspect.Signature.format_annotation_new()`` + - 2.4 + - 4.0 + - ``sphinx.util.typing.stringify()`` + + * - ``sphinx.util.inspect.Signature.format_annotation_old()`` + - 2.4 + - 4.0 + - ``sphinx.util.typing.stringify()`` + * - ``sphinx.builders.gettext.POHEADER`` - 2.3 - 4.0 diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py index 23ef8e76b..7d4d38c42 100644 --- a/sphinx/builders/latex/__init__.py +++ b/sphinx/builders/latex/__init__.py @@ -138,7 +138,7 @@ class LaTeXBuilder(Builder): def get_target_uri(self, docname: str, typ: str = None) -> str: if docname not in self.docnames: - raise NoUri + raise NoUri(docname, typ) else: return '%' + docname diff --git a/sphinx/builders/manpage.py b/sphinx/builders/manpage.py index d4c7feb05..4166dece9 100644 --- a/sphinx/builders/manpage.py +++ b/sphinx/builders/manpage.py @@ -53,7 +53,7 @@ class ManualPageBuilder(Builder): def get_target_uri(self, docname: str, typ: str = None) -> str: if typ == 'token': return '' - raise NoUri + raise NoUri(docname, typ) @progress_message(__('writing')) def write(self, *ignored: Any) -> None: diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py index ed663be39..5e2e6e240 100644 --- a/sphinx/builders/texinfo.py +++ b/sphinx/builders/texinfo.py @@ -63,7 +63,7 @@ class TexinfoBuilder(Builder): def get_target_uri(self, docname: str, typ: str = None) -> str: if docname not in self.docnames: - raise NoUri + raise NoUri(docname, typ) else: return '%' + docname diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index eda04b9f4..d8eec6330 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -6857,7 +6857,7 @@ class CPPDomain(Domain): if s is None or s.declaration is None: txtName = str(name) if txtName.startswith('std::') or txtName == 'std': - raise NoUri() + raise NoUri(txtName, typ) return None, None if typ.startswith('cpp:'): diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index eafec90e1..9e7dd2353 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -29,7 +29,7 @@ from sphinx.locale import _, __ from sphinx.roles import XRefRole from sphinx.util import ws_re, logging, docname_join from sphinx.util.docutils import SphinxDirective -from sphinx.util.nodes import clean_astext, make_refnode +from sphinx.util.nodes import clean_astext, make_id, make_refnode from sphinx.util.typing import RoleFunction if False: @@ -243,34 +243,44 @@ def split_term_classifiers(line: str) -> List[Optional[str]]: def make_glossary_term(env: "BuildEnvironment", textnodes: Iterable[Node], index_key: str, - source: str, lineno: int, new_id: str = None) -> nodes.term: + source: str, lineno: int, node_id: str = None, + document: nodes.document = None) -> nodes.term: # get a text-only representation of the term and register it # as a cross-reference target term = nodes.term('', '', *textnodes) term.source = source term.line = lineno - - gloss_entries = env.temp_data.setdefault('gloss_entries', set()) termtext = term.astext() - if new_id is None: - new_id = nodes.make_id('term-' + termtext) - if new_id == 'term': - # the term is not good for node_id. Generate it by sequence number instead. - new_id = 'term-%d' % env.new_serialno('glossary') - while new_id in gloss_entries: - new_id = 'term-%d' % env.new_serialno('glossary') - gloss_entries.add(new_id) + + if node_id: + # node_id is given from outside (mainly i18n module), use it forcedly + pass + elif document: + node_id = make_id(env, document, 'term', termtext) + term['ids'].append(node_id) + document.note_explicit_target(term) + else: + warnings.warn('make_glossary_term() expects document is passed as an argument.', + RemovedInSphinx40Warning) + gloss_entries = env.temp_data.setdefault('gloss_entries', set()) + node_id = nodes.make_id('term-' + termtext) + if node_id == 'term': + # "term" is not good for node_id. Generate it by sequence number instead. + node_id = 'term-%d' % env.new_serialno('glossary') + + while node_id in gloss_entries: + node_id = 'term-%d' % env.new_serialno('glossary') + gloss_entries.add(node_id) + term['ids'].append(node_id) std = cast(StandardDomain, env.get_domain('std')) - std.note_object('term', termtext.lower(), new_id, location=(env.docname, lineno)) + std.note_object('term', termtext.lower(), node_id, location=(env.docname, lineno)) # add an index entry too indexnode = addnodes.index() - indexnode['entries'] = [('single', termtext, new_id, 'main', index_key)] + indexnode['entries'] = [('single', termtext, node_id, 'main', index_key)] indexnode.source, indexnode.line = term.source, term.line term.append(indexnode) - term['ids'].append(new_id) - term['names'].append(new_id) return term @@ -368,7 +378,8 @@ class Glossary(SphinxDirective): textnodes, sysmsg = self.state.inline_text(parts[0], lineno) # use first classifier as a index key - term = make_glossary_term(self.env, textnodes, parts[1], source, lineno) + term = make_glossary_term(self.env, textnodes, parts[1], source, lineno, + document=self.state.document) term.rawsource = line system_messages.extend(sysmsg) termtexts.append(term.astext()) diff --git a/sphinx/ext/apidoc.py b/sphinx/ext/apidoc.py index 42605aeae..1d12ac6a6 100644 --- a/sphinx/ext/apidoc.py +++ b/sphinx/ext/apidoc.py @@ -128,7 +128,7 @@ def create_package_file(root: str, master_package: str, subroot: str, py_files: subpackages = [module_join(master_package, subroot, pkgname) for pkgname in subpackages] # build a list of sub modules - submodules = [path.splitext(sub)[0] for sub in py_files + submodules = [sub.split('.')[0] for sub in py_files if not is_skipped_module(path.join(root, sub), opts, excludes) and sub != INITPY] submodules = [module_join(master_package, subroot, modname) diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index e61905830..34d5b1368 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -202,18 +202,13 @@ class Locale(SphinxTransform): # glossary terms update refid if isinstance(node, nodes.term): - gloss_entries = self.env.temp_data.setdefault('gloss_entries', set()) - for _id in node['names']: - if _id in gloss_entries: - gloss_entries.remove(_id) - + for _id in node['ids']: parts = split_term_classifiers(msgstr) patch = publish_msgstr(self.app, parts[0], source, node.line, self.config, settings) patch = make_glossary_term(self.env, patch, parts[1], - source, node.line, _id) - node['ids'] = patch['ids'] - node['names'] = patch['names'] + source, node.line, _id, + self.document) processed = True # update leaves with processed nodes diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index a16427dad..ee459cc56 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -82,7 +82,7 @@ class ReferencesResolver(SphinxPostTransform): try: domain = self.env.domains[node['refdomain']] except KeyError: - raise NoUri + raise NoUri(target, typ) newnode = domain.resolve_xref(self.env, refdoc, self.app.builder, typ, target, node, contnode) # really hardwired reference types diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 1ce5bb900..2d665c1a6 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -21,8 +21,9 @@ from inspect import ( # NOQA from io import StringIO from typing import Any, Callable, Mapping, List, Tuple +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.util import logging -from sphinx.util.typing import NoneType +from sphinx.util.typing import stringify as stringify_annotation if sys.version_info > (3, 7): from types import ( @@ -381,11 +382,11 @@ class Signature: return None def format_args(self, show_annotation: bool = True) -> str: - def format_param_annotation(param: inspect.Parameter) -> str: + def get_annotation(param: inspect.Parameter) -> Any: if isinstance(param.annotation, str) and param.name in self.annotations: - return self.format_annotation(self.annotations[param.name]) + return self.annotations[param.name] else: - return self.format_annotation(param.annotation) + return param.annotation args = [] last_kind = None @@ -409,7 +410,7 @@ class Signature: arg.write(param.name) if show_annotation and param.annotation is not param.empty: arg.write(': ') - arg.write(format_param_annotation(param)) + arg.write(stringify_annotation(get_annotation(param))) if param.default is not param.empty: if param.annotation is param.empty or show_annotation is False: arg.write('=') @@ -422,13 +423,13 @@ class Signature: arg.write(param.name) if show_annotation and param.annotation is not param.empty: arg.write(': ') - arg.write(format_param_annotation(param)) + arg.write(stringify_annotation(get_annotation(param))) elif param.kind == param.VAR_KEYWORD: arg.write('**') arg.write(param.name) if show_annotation and param.annotation is not param.empty: arg.write(': ') - arg.write(format_param_annotation(param)) + arg.write(stringify_annotation(get_annotation(param))) args.append(arg.getvalue()) last_kind = param.kind @@ -437,164 +438,29 @@ class Signature: return '(%s)' % ', '.join(args) else: if 'return' in self.annotations: - annotation = self.format_annotation(self.annotations['return']) + annotation = stringify_annotation(self.annotations['return']) else: - annotation = self.format_annotation(self.return_annotation) + annotation = stringify_annotation(self.return_annotation) return '(%s) -> %s' % (', '.join(args), annotation) def format_annotation(self, annotation: Any) -> str: - """Return formatted representation of a type annotation. - - Show qualified names for types and additional details for types from - the ``typing`` module. - - Displaying complex types from ``typing`` relies on its private API. - """ - if isinstance(annotation, str): - return annotation - elif isinstance(annotation, typing.TypeVar): # type: ignore - return annotation.__name__ - elif not annotation: - return repr(annotation) - elif annotation is NoneType: # type: ignore - return 'None' - elif getattr(annotation, '__module__', None) == 'builtins': - return annotation.__qualname__ - elif annotation is Ellipsis: - return '...' - - if sys.version_info >= (3, 7): # py37+ - return self.format_annotation_new(annotation) - else: - return self.format_annotation_old(annotation) + """Return formatted representation of a type annotation.""" + warnings.warn('format_annotation() is deprecated', + RemovedInSphinx40Warning) + return stringify_annotation(annotation) def format_annotation_new(self, annotation: Any) -> str: """format_annotation() for py37+""" - module = getattr(annotation, '__module__', None) - if module == 'typing': - if getattr(annotation, '_name', None): - qualname = annotation._name - elif getattr(annotation, '__qualname__', None): - qualname = annotation.__qualname__ - elif getattr(annotation, '__forward_arg__', None): - qualname = annotation.__forward_arg__ - else: - qualname = self.format_annotation(annotation.__origin__) # ex. Union - elif hasattr(annotation, '__qualname__'): - qualname = '%s.%s' % (module, annotation.__qualname__) - else: - qualname = repr(annotation) - - if getattr(annotation, '__args__', None): - if qualname == 'Union': - if len(annotation.__args__) == 2 and annotation.__args__[1] is NoneType: # type: ignore # NOQA - return 'Optional[%s]' % self.format_annotation(annotation.__args__[0]) - else: - args = ', '.join(self.format_annotation(a) for a in annotation.__args__) - return '%s[%s]' % (qualname, args) - elif qualname == 'Callable': - args = ', '.join(self.format_annotation(a) for a in annotation.__args__[:-1]) - returns = self.format_annotation(annotation.__args__[-1]) - return '%s[[%s], %s]' % (qualname, args, returns) - elif annotation._special: - return qualname - else: - args = ', '.join(self.format_annotation(a) for a in annotation.__args__) - return '%s[%s]' % (qualname, args) - - return qualname + warnings.warn('format_annotation_new() is deprecated', + RemovedInSphinx40Warning) + return stringify_annotation(annotation) def format_annotation_old(self, annotation: Any) -> str: """format_annotation() for py36 or below""" - module = getattr(annotation, '__module__', None) - if module == 'typing': - if getattr(annotation, '_name', None): - qualname = annotation._name - elif getattr(annotation, '__qualname__', None): - qualname = annotation.__qualname__ - elif getattr(annotation, '__forward_arg__', None): - qualname = annotation.__forward_arg__ - elif getattr(annotation, '__origin__', None): - qualname = self.format_annotation(annotation.__origin__) # ex. Union - else: - qualname = repr(annotation).replace('typing.', '') - elif hasattr(annotation, '__qualname__'): - qualname = '%s.%s' % (module, annotation.__qualname__) - else: - qualname = repr(annotation) - - if (isinstance(annotation, typing.TupleMeta) and # type: ignore - not hasattr(annotation, '__tuple_params__')): # for Python 3.6 - params = annotation.__args__ - if params: - param_str = ', '.join(self.format_annotation(p) for p in params) - return '%s[%s]' % (qualname, param_str) - else: - return qualname - elif isinstance(annotation, typing.GenericMeta): - params = None - if hasattr(annotation, '__args__'): - # for Python 3.5.2+ - if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA - params = annotation.__args__ # type: ignore - else: # typing.Callable - args = ', '.join(self.format_annotation(arg) for arg - in annotation.__args__[:-1]) # type: ignore - result = self.format_annotation(annotation.__args__[-1]) # type: ignore - return '%s[[%s], %s]' % (qualname, args, result) - elif hasattr(annotation, '__parameters__'): - # for Python 3.5.0 and 3.5.1 - params = annotation.__parameters__ # type: ignore - if params is not None: - param_str = ', '.join(self.format_annotation(p) for p in params) - return '%s[%s]' % (qualname, param_str) - elif (hasattr(typing, 'UnionMeta') and - isinstance(annotation, typing.UnionMeta) and # type: ignore - hasattr(annotation, '__union_params__')): # for Python 3.5 - params = annotation.__union_params__ - if params is not None: - if len(params) == 2 and params[1] is NoneType: # type: ignore - return 'Optional[%s]' % self.format_annotation(params[0]) - else: - param_str = ', '.join(self.format_annotation(p) for p in params) - return '%s[%s]' % (qualname, param_str) - elif (hasattr(annotation, '__origin__') and - annotation.__origin__ is typing.Union): # for Python 3.5.2+ - params = annotation.__args__ - if params is not None: - if len(params) == 2 and params[1] is NoneType: # type: ignore - return 'Optional[%s]' % self.format_annotation(params[0]) - else: - param_str = ', '.join(self.format_annotation(p) for p in params) - return 'Union[%s]' % param_str - elif (isinstance(annotation, typing.CallableMeta) and # type: ignore - getattr(annotation, '__args__', None) is not None and - hasattr(annotation, '__result__')): # for Python 3.5 - # Skipped in the case of plain typing.Callable - args = annotation.__args__ - if args is None: - return qualname - elif args is Ellipsis: - args_str = '...' - else: - formatted_args = (self.format_annotation(a) for a in args) - args_str = '[%s]' % ', '.join(formatted_args) - return '%s[%s, %s]' % (qualname, - args_str, - self.format_annotation(annotation.__result__)) - elif (isinstance(annotation, typing.TupleMeta) and # type: ignore - hasattr(annotation, '__tuple_params__') and - hasattr(annotation, '__tuple_use_ellipsis__')): # for Python 3.5 - params = annotation.__tuple_params__ - if params is not None: - param_strings = [self.format_annotation(p) for p in params] - if annotation.__tuple_use_ellipsis__: - param_strings.append('...') - return '%s[%s]' % (qualname, - ', '.join(param_strings)) - - return qualname + warnings.warn('format_annotation_old() is deprecated', + RemovedInSphinx40Warning) + return stringify_annotation(annotation) def getdoc(obj: Any, attrgetter: Callable = safe_getattr, diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index f33a6a001..7c7300c60 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -28,6 +28,7 @@ if False: # For type annotation from typing import Type # for python3.5.1 from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment from sphinx.utils.tags import Tags logger = logging.getLogger(__name__) @@ -435,6 +436,28 @@ def inline_all_toctrees(builder: "Builder", docnameset: Set[str], docname: str, return tree +def make_id(env: "BuildEnvironment", document: nodes.document, + prefix: str = '', term: str = None) -> str: + """Generate an appropriate node_id for given *prefix* and *term*.""" + node_id = None + if prefix: + idformat = prefix + "-%s" + else: + idformat = document.settings.id_prefix + "%s" + + # try to generate node_id by *term* + if prefix and term: + node_id = nodes.make_id(idformat % term) + if node_id == prefix: + # *term* is not good to generate a node_id. + node_id = None + + while node_id is None or node_id in document.ids: + node_id = idformat % env.new_serialno(prefix) + + return node_id + + def make_refnode(builder: "Builder", fromdocname: str, todocname: str, targetid: str, child: Node, title: str = None) -> nodes.reference: """Shortcut to create a reference node.""" diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 1b2ec3f60..ccceefed6 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -8,7 +8,9 @@ :license: BSD, see LICENSE for details. """ -from typing import Any, Callable, Dict, List, Tuple, Union +import sys +import typing +from typing import Any, Callable, Dict, List, Tuple, TypeVar, Union from docutils import nodes from docutils.parsers.rst.states import Inliner @@ -35,3 +37,153 @@ TitleGetter = Callable[[nodes.Node], str] # inventory data on memory Inventory = Dict[str, Dict[str, Tuple[str, str, str, str]]] + + +def stringify(annotation: Any) -> str: + """Stringify type annotation object.""" + if isinstance(annotation, str): + return annotation + elif isinstance(annotation, TypeVar): # type: ignore + return annotation.__name__ + elif not annotation: + return repr(annotation) + elif annotation is NoneType: # type: ignore + return 'None' + elif getattr(annotation, '__module__', None) == 'builtins': + return annotation.__qualname__ + elif annotation is Ellipsis: + return '...' + + if sys.version_info >= (3, 7): # py37+ + return _stringify_py37(annotation) + else: + return _stringify_py36(annotation) + + +def _stringify_py37(annotation: Any) -> str: + """stringify() for py37+.""" + module = getattr(annotation, '__module__', None) + if module == 'typing': + if getattr(annotation, '_name', None): + qualname = annotation._name + elif getattr(annotation, '__qualname__', None): + qualname = annotation.__qualname__ + elif getattr(annotation, '__forward_arg__', None): + qualname = annotation.__forward_arg__ + else: + qualname = stringify(annotation.__origin__) # ex. Union + elif hasattr(annotation, '__qualname__'): + qualname = '%s.%s' % (module, annotation.__qualname__) + else: + qualname = repr(annotation) + + if getattr(annotation, '__args__', None): + if qualname == 'Union': + if len(annotation.__args__) == 2 and annotation.__args__[1] is NoneType: # type: ignore # NOQA + return 'Optional[%s]' % stringify(annotation.__args__[0]) + else: + args = ', '.join(stringify(a) for a in annotation.__args__) + return '%s[%s]' % (qualname, args) + elif qualname == 'Callable': + args = ', '.join(stringify(a) for a in annotation.__args__[:-1]) + returns = stringify(annotation.__args__[-1]) + return '%s[[%s], %s]' % (qualname, args, returns) + elif annotation._special: + return qualname + else: + args = ', '.join(stringify(a) for a in annotation.__args__) + return '%s[%s]' % (qualname, args) + + return qualname + + +def _stringify_py36(annotation: Any) -> str: + """stringify() for py35 and py36.""" + module = getattr(annotation, '__module__', None) + if module == 'typing': + if getattr(annotation, '_name', None): + qualname = annotation._name + elif getattr(annotation, '__qualname__', None): + qualname = annotation.__qualname__ + elif getattr(annotation, '__forward_arg__', None): + qualname = annotation.__forward_arg__ + elif getattr(annotation, '__origin__', None): + qualname = stringify(annotation.__origin__) # ex. Union + else: + qualname = repr(annotation).replace('typing.', '') + elif hasattr(annotation, '__qualname__'): + qualname = '%s.%s' % (module, annotation.__qualname__) + else: + qualname = repr(annotation) + + if (isinstance(annotation, typing.TupleMeta) and # type: ignore + not hasattr(annotation, '__tuple_params__')): # for Python 3.6 + params = annotation.__args__ + if params: + param_str = ', '.join(stringify(p) for p in params) + return '%s[%s]' % (qualname, param_str) + else: + return qualname + elif isinstance(annotation, typing.GenericMeta): + params = None + if hasattr(annotation, '__args__'): + # for Python 3.5.2+ + if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA + params = annotation.__args__ # type: ignore + else: # typing.Callable + args = ', '.join(stringify(arg) for arg + in annotation.__args__[:-1]) # type: ignore + result = stringify(annotation.__args__[-1]) # type: ignore + return '%s[[%s], %s]' % (qualname, args, result) + elif hasattr(annotation, '__parameters__'): + # for Python 3.5.0 and 3.5.1 + params = annotation.__parameters__ # type: ignore + if params is not None: + param_str = ', '.join(stringify(p) for p in params) + return '%s[%s]' % (qualname, param_str) + elif (hasattr(typing, 'UnionMeta') and + isinstance(annotation, typing.UnionMeta) and # type: ignore + hasattr(annotation, '__union_params__')): # for Python 3.5 + params = annotation.__union_params__ + if params is not None: + if len(params) == 2 and params[1] is NoneType: # type: ignore + return 'Optional[%s]' % stringify(params[0]) + else: + param_str = ', '.join(stringify(p) for p in params) + return '%s[%s]' % (qualname, param_str) + elif (hasattr(annotation, '__origin__') and + annotation.__origin__ is typing.Union): # for Python 3.5.2+ + params = annotation.__args__ + if params is not None: + if len(params) == 2 and params[1] is NoneType: # type: ignore + return 'Optional[%s]' % stringify(params[0]) + else: + param_str = ', '.join(stringify(p) for p in params) + return 'Union[%s]' % param_str + elif (isinstance(annotation, typing.CallableMeta) and # type: ignore + getattr(annotation, '__args__', None) is not None and + hasattr(annotation, '__result__')): # for Python 3.5 + # Skipped in the case of plain typing.Callable + args = annotation.__args__ + if args is None: + return qualname + elif args is Ellipsis: + args_str = '...' + else: + formatted_args = (stringify(a) for a in args) + args_str = '[%s]' % ', '.join(formatted_args) + return '%s[%s, %s]' % (qualname, + args_str, + stringify(annotation.__result__)) + elif (isinstance(annotation, typing.TupleMeta) and # type: ignore + hasattr(annotation, '__tuple_params__') and + hasattr(annotation, '__tuple_use_ellipsis__')): # for Python 3.5 + params = annotation.__tuple_params__ + if params is not None: + param_strings = [stringify(p) for p in params] + if annotation.__tuple_use_ellipsis__: + param_strings.append('...') + return '%s[%s]' % (qualname, + ', '.join(param_strings)) + + return qualname diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index 5530dd8f5..aa7afd4a1 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -578,6 +578,21 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): node['uri'] = posixpath.join(self.builder.imgpath, self.builder.images[olduri]) + if 'scale' in node: + # Try to figure out image height and width. Docutils does that too, + # but it tries the final file name, which does not necessarily exist + # yet at the time the HTML file is written. + if not ('width' in node and 'height' in node): + size = get_image_size(os.path.join(self.builder.srcdir, olduri)) + if size is None: + logger.warning(__('Could not obtain image size. :scale: option is ignored.'), # NOQA + location=node) + else: + if 'width' not in node: + node['width'] = str(size[0]) + if 'height' not in node: + node['height'] = str(size[1]) + uri = node['uri'] if uri.lower().endswith(('svg', 'svgz')): atts = {'src': uri} @@ -585,6 +600,12 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): atts['width'] = node['width'] if 'height' in node: atts['height'] = node['height'] + if 'scale' in node: + scale = node['scale'] / 100.0 + if 'width' in atts: + atts['width'] = int(atts['width']) * scale + if 'height' in atts: + atts['height'] = int(atts['height']) * scale atts['alt'] = node.get('alt', uri) if 'align' in node: self.body.append('<div align="%s" class="align-%s">' % @@ -595,20 +616,6 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): self.body.append(self.emptytag(node, 'img', '', **atts)) return - if 'scale' in node: - # Try to figure out image height and width. Docutils does that too, - # but it tries the final file name, which does not necessarily exist - # yet at the time the HTML file is written. - if not ('width' in node and 'height' in node): - size = get_image_size(os.path.join(self.builder.srcdir, olduri)) - if size is None: - logger.warning(__('Could not obtain image size. :scale: option is ignored.'), # NOQA - location=node) - else: - if 'width' not in node: - node['width'] = str(size[0]) - if 'height' not in node: - node['height'] = str(size[1]) super().visit_image(node) # overwritten diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index 4d6089718..b8adb7ea3 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -519,6 +519,21 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): node['uri'] = posixpath.join(self.builder.imgpath, self.builder.images[olduri]) + if 'scale' in node: + # Try to figure out image height and width. Docutils does that too, + # but it tries the final file name, which does not necessarily exist + # yet at the time the HTML file is written. + if not ('width' in node and 'height' in node): + size = get_image_size(os.path.join(self.builder.srcdir, olduri)) + if size is None: + logger.warning(__('Could not obtain image size. :scale: option is ignored.'), # NOQA + location=node) + else: + if 'width' not in node: + node['width'] = str(size[0]) + if 'height' not in node: + node['height'] = str(size[1]) + uri = node['uri'] if uri.lower().endswith(('svg', 'svgz')): atts = {'src': uri} @@ -526,6 +541,12 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): atts['width'] = node['width'] if 'height' in node: atts['height'] = node['height'] + if 'scale' in node: + scale = node['scale'] / 100.0 + if 'width' in atts: + atts['width'] = int(atts['width']) * scale + if 'height' in atts: + atts['height'] = int(atts['height']) * scale atts['alt'] = node.get('alt', uri) if 'align' in node: self.body.append('<div align="%s" class="align-%s">' % @@ -536,20 +557,6 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): self.body.append(self.emptytag(node, 'img', '', **atts)) return - if 'scale' in node: - # Try to figure out image height and width. Docutils does that too, - # but it tries the final file name, which does not necessarily exist - # yet at the time the HTML file is written. - if not ('width' in node and 'height' in node): - size = get_image_size(os.path.join(self.builder.srcdir, olduri)) - if size is None: - logger.warning(__('Could not obtain image size. :scale: option is ignored.'), # NOQA - location=node) - else: - if 'width' not in node: - node['width'] = str(size[0]) - if 'height' not in node: - node['height'] = str(size[1]) super().visit_image(node) # overwritten diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py index 93504c482..adde491c4 100644 --- a/tests/test_domain_std.py +++ b/tests/test_domain_std.py @@ -264,6 +264,16 @@ def test_glossary_alphanumeric(app): assert ("/", "/", "term", "index", "term-0", -1) in objects +def test_glossary_conflicted_labels(app): + text = (".. _term-foo:\n" + ".. glossary::\n" + "\n" + " foo\n") + restructuredtext.parse(app, text) + objects = list(app.env.get_domain("std").get_objects()) + assert ("foo", "foo", "term", "index", "term-0", -1) in objects + + def test_cmdoption(app): text = (".. program:: ls\n" "\n" diff --git a/tests/test_util_nodes.py b/tests/test_util_nodes.py index b1a6b7d1d..76ba51800 100644 --- a/tests/test_util_nodes.py +++ b/tests/test_util_nodes.py @@ -17,7 +17,9 @@ from docutils.parsers import rst from docutils.utils import new_document from sphinx.transforms import ApplySourceWorkaround -from sphinx.util.nodes import NodeMatcher, extract_messages, clean_astext, split_explicit_title +from sphinx.util.nodes import ( + NodeMatcher, extract_messages, clean_astext, make_id, split_explicit_title +) def _transform(doctree): @@ -27,6 +29,7 @@ def _transform(doctree): def create_new_document(): settings = frontend.OptionParser( components=(rst.Parser,)).get_default_values() + settings.id_prefix = 'id' document = new_document('dummy.txt', settings) return document @@ -180,6 +183,20 @@ def test_clean_astext(): assert 'hello world' == clean_astext(node) +def test_make_id(app): + document = create_new_document() + assert make_id(app.env, document) == 'id0' + assert make_id(app.env, document, 'term') == 'term-0' + assert make_id(app.env, document, 'term', 'Sphinx') == 'term-sphinx' + + # when same ID is already registered + document.ids['term-sphinx'] = True + assert make_id(app.env, document, 'term', 'Sphinx') == 'term-1' + + document.ids['term-2'] = True + assert make_id(app.env, document, 'term') == 'term-3' + + @pytest.mark.parametrize( 'title, expected', [ diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py new file mode 100644 index 000000000..9a225f0f1 --- /dev/null +++ b/tests/test_util_typing.py @@ -0,0 +1,98 @@ +""" + test_util_typing + ~~~~~~~~~~~~~~~~ + + Tests util.typing functions. + + :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import sys +from numbers import Integral +from typing import Any, Dict, List, TypeVar, Union, Callable, Tuple, Optional + +from sphinx.util.typing import stringify + + +class MyClass1: + pass + + +class MyClass2(MyClass1): + __qualname__ = '<MyClass2>' + + +def test_stringify(): + assert stringify(int) == "int" + assert stringify(str) == "str" + assert stringify(None) == "None" + assert stringify(Integral) == "numbers.Integral" + assert stringify(Any) == "Any" + + +def test_stringify_type_hints_containers(): + assert stringify(List) == "List" + assert stringify(Dict) == "Dict" + assert stringify(List[int]) == "List[int]" + assert stringify(List[str]) == "List[str]" + assert stringify(Dict[str, float]) == "Dict[str, float]" + assert stringify(Tuple[str, str, str]) == "Tuple[str, str, str]" + assert stringify(Tuple[str, ...]) == "Tuple[str, ...]" + assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]" + + +def test_stringify_type_hints_string(): + assert stringify("int") == "int" + assert stringify("str") == "str" + assert stringify(List["int"]) == "List[int]" + assert stringify("Tuple[str]") == "Tuple[str]" + assert stringify("unknown") == "unknown" + + +def test_stringify_type_hints_Callable(): + assert stringify(Callable) == "Callable" + + if sys.version_info >= (3, 7): + assert stringify(Callable[[str], int]) == "Callable[[str], int]" + assert stringify(Callable[..., int]) == "Callable[[...], int]" + else: + assert stringify(Callable[[str], int]) == "Callable[str, int]" + assert stringify(Callable[..., int]) == "Callable[..., int]" + + +def test_stringify_type_hints_Union(): + assert stringify(Optional[int]) == "Optional[int]" + assert stringify(Union[str, None]) == "Optional[str]" + assert stringify(Union[int, str]) == "Union[int, str]" + + if sys.version_info >= (3, 7): + assert stringify(Union[int, Integral]) == "Union[int, numbers.Integral]" + assert (stringify(Union[MyClass1, MyClass2]) == + "Union[test_util_typing.MyClass1, test_util_typing.<MyClass2>]") + else: + assert stringify(Union[int, Integral]) == "numbers.Integral" + assert stringify(Union[MyClass1, MyClass2]) == "test_util_typing.MyClass1" + + +def test_stringify_type_hints_typevars(): + T = TypeVar('T') + T_co = TypeVar('T_co', covariant=True) + T_contra = TypeVar('T_contra', contravariant=True) + + assert stringify(T) == "T" + assert stringify(T_co) == "T_co" + assert stringify(T_contra) == "T_contra" + assert stringify(List[T]) == "List[T]" + + +def test_stringify_type_hints_custom_class(): + assert stringify(MyClass1) == "test_util_typing.MyClass1" + assert stringify(MyClass2) == "test_util_typing.<MyClass2>" + + +def test_stringify_type_hints_alias(): + MyStr = str + MyTuple = Tuple[str, str] + assert stringify(MyStr) == "str" + assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore |