summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakeshi KOMIYA <i.tkomiya@gmail.com>2020-01-08 01:37:53 +0900
committerTakeshi KOMIYA <i.tkomiya@gmail.com>2020-01-08 01:37:53 +0900
commitaf2a3c0ddeb2f81c8f685e4d266adefc749a2642 (patch)
treef4b5f634fcc3e2315bf5eedaceed11a16ba0ff14
parent92a204284b98510b3bfdd2a6391e9855c564a6a0 (diff)
parent8e1cbd24c61934df7eb426aad0dc48830789b096 (diff)
downloadsphinx-git-af2a3c0ddeb2f81c8f685e4d266adefc749a2642.tar.gz
Merge branch '2.0'
-rw-r--r--CHANGES8
-rw-r--r--doc/extdev/deprecated.rst20
-rw-r--r--sphinx/builders/latex/__init__.py2
-rw-r--r--sphinx/builders/manpage.py2
-rw-r--r--sphinx/builders/texinfo.py2
-rw-r--r--sphinx/domains/cpp.py2
-rw-r--r--sphinx/domains/std.py45
-rw-r--r--sphinx/ext/apidoc.py2
-rw-r--r--sphinx/transforms/i18n.py11
-rw-r--r--sphinx/transforms/post_transforms/__init__.py2
-rw-r--r--sphinx/util/inspect.py174
-rw-r--r--sphinx/util/nodes.py23
-rw-r--r--sphinx/util/typing.py154
-rw-r--r--sphinx/writers/html.py35
-rw-r--r--sphinx/writers/html5.py35
-rw-r--r--tests/test_domain_std.py10
-rw-r--r--tests/test_util_nodes.py19
-rw-r--r--tests/test_util_typing.py98
18 files changed, 429 insertions, 215 deletions
diff --git a/CHANGES b/CHANGES
index ef1ccf6f0..95a2e1906 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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