diff --git a/CHANGES b/CHANGES index 7ab1d9f1d..1ea5acf32 100644 --- a/CHANGES +++ b/CHANGES @@ -41,6 +41,9 @@ Deprecated Features added -------------- +* #9075: autodoc: Add a config variable :confval:`autodoc_unqualified_typehints` + to suppress the leading module names of typehints of function signatures (ex. + ``io.StringIO`` -> ``StringIO``) * #9831: Autosummary now documents only the members specified in a module's ``__all__`` attribute if :confval:`autosummary_ignore_module_all` is set to ``False``. The default behaviour is unchanged. Autogen also now supports @@ -53,6 +56,11 @@ Features added ``~``) as ``:type:`` option * #9894: linkcheck: add option ``linkcheck_exclude_documents`` to disable link checking in matched documents. +* #9793: sphinx-build: Allow to use the parallel build feature in macOS on macOS + and Python3.8+ +* #9391: texinfo: improve variable in ``samp`` role +* #9578: texinfo: Add :confval:`texinfo_cross_references` to disable cross + references for readability with standalone readers Bugs fixed ---------- @@ -61,9 +69,14 @@ Bugs fixed * #9883: autodoc: doccomment for the alias to mocked object was ignored * #9908: autodoc: debug message is shown on building document using NewTypes with Python 3.10 +* #9947: i18n: topic directive having a bullet list can't be translatable * #9878: mathjax: MathJax configuration is placed after loading MathJax itself * #9857: Generated RFC links use outdated base url * #9909: HTML, prevent line-wrapping in literal text. +* #9925: LaTeX: prohibit also with ``'xelatex'`` line splitting at dashes of + inline and parsed literals +* #9944: LaTeX: extra vertical whitespace for some nested declarations +* #9390: texinfo: Do not emit labels inside footnotes Testing -------- diff --git a/doc/_static/conf.py.txt b/doc/_static/conf.py.txt index 9078199b3..3077d1b93 100644 --- a/doc/_static/conf.py.txt +++ b/doc/_static/conf.py.txt @@ -319,6 +319,10 @@ texinfo_documents = [ # # texinfo_no_detailmenu = False +# If false, do not generate in manual @ref nodes. +# +# texinfo_cross_references = False + # -- A random example ----------------------------------------------------- import sys, os diff --git a/doc/_themes/sphinx13/theme.conf b/doc/_themes/sphinx13/theme.conf index 876b19803..19a480a6b 100644 --- a/doc/_themes/sphinx13/theme.conf +++ b/doc/_themes/sphinx13/theme.conf @@ -1,4 +1,4 @@ [theme] inherit = basic stylesheet = sphinx13.css -pygments_style = trac +pygments_style = default diff --git a/doc/faq.rst b/doc/faq.rst index 4b273023d..2e1081439 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -299,6 +299,10 @@ appear in the source. Emacs, on the other-hand, will by default replace :ref:`texinfo-links` +One can disable generation of the inline references in a document +with :confval:`texinfo_cross_references`. That makes +an info file more readable with stand-alone reader (``info``). + The exact behavior of how Emacs displays references is dependent on the variable ``Info-hide-note-references``. If set to the value of ``hide``, Emacs will hide both the ``*note:`` part and the ``target-id``. This is generally the best way diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 2a923de51..b5bd02e4f 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -2499,6 +2499,13 @@ These options influence Texinfo output. .. versionadded:: 1.1 +.. confval:: texinfo_cross_references + + If false, do not generate inline references in a document. That makes + an info file more readable with stand-alone reader (``info``). + Default is ``True``. + + .. versionadded:: 4.4 .. _qthelp-options: diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index c22b8b1ae..19051e3e0 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -662,6 +662,13 @@ There are also config values that you can set: .. __: https://mypy.readthedocs.io/en/latest/kinds_of_types.html#type-aliases .. versionadded:: 3.3 +.. confval:: autodoc_unqualified_typehints + + If True, the leading module names of typehints of function signatures (ex. + ``io.StringIO`` -> ``StringIO``). Defaults to False. + + .. versionadded:: 4.4 + .. confval:: autodoc_preserve_defaults If True, the default argument values of functions will be not evaluated on diff --git a/sphinx/application.py b/sphinx/application.py index ec0234a4e..475f08853 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -12,7 +12,6 @@ import os import pickle -import platform import sys import warnings from collections import deque @@ -195,12 +194,6 @@ class Sphinx: # say hello to the world logger.info(bold(__('Running Sphinx v%s') % sphinx.__display_version__)) - # notice for parallel build on macOS and py38+ - if sys.version_info > (3, 8) and platform.system() == 'Darwin' and parallel > 1: - logger.info(bold(__("For security reasons, parallel mode is disabled on macOS and " - "python3.8 and above. For more details, please read " - "https://github.com/sphinx-doc/sphinx/issues/6803"))) - # status code for command-line application self.statuscode = 0 diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py index ee10d58c3..2b28ce400 100644 --- a/sphinx/builders/texinfo.py +++ b/sphinx/builders/texinfo.py @@ -211,6 +211,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('texinfo_domain_indices', True, None, [list]) app.add_config_value('texinfo_show_urls', 'footnote', None) app.add_config_value('texinfo_no_detailmenu', False, None) + app.add_config_value('texinfo_cross_references', True, None) return { 'version': 'builtin', diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index bcf312d7e..5402ce37a 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -80,7 +80,8 @@ class ModuleEntry(NamedTuple): deprecated: bool -def type_to_xref(target: str, env: BuildEnvironment = None) -> addnodes.pending_xref: +def type_to_xref(target: str, env: BuildEnvironment = None, suppress_prefix: bool = False + ) -> addnodes.pending_xref: """Convert a type string to a cross reference node.""" if target == 'None': reftype = 'obj' @@ -101,6 +102,8 @@ def type_to_xref(target: str, env: BuildEnvironment = None) -> addnodes.pending_ elif target.startswith('~'): target = target[1:] text = target.split('.')[-1] + elif suppress_prefix: + text = target.split('.')[-1] else: text = target @@ -150,6 +153,8 @@ def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Nod return unparse(node.value) elif isinstance(node, ast.Index): return unparse(node.value) + elif isinstance(node, ast.Invert): + return [addnodes.desc_sig_punctuation('', '~')] elif isinstance(node, ast.List): result = [addnodes.desc_sig_punctuation('', '[')] if node.elts: @@ -180,6 +185,8 @@ def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Nod if isinstance(subnode, nodes.Text): result[i] = nodes.literal('', '', subnode) return result + elif isinstance(node, ast.UnaryOp): + return unparse(node.op) + unparse(node.operand) elif isinstance(node, ast.Tuple): if node.elts: result = [] @@ -209,12 +216,19 @@ def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Nod try: tree = ast_parse(annotation) - result = unparse(tree) - for i, node in enumerate(result): + result: List[Node] = [] + for node in unparse(tree): if isinstance(node, nodes.literal): - result[i] = node[0] + result.append(node[0]) elif isinstance(node, nodes.Text) and node.strip(): - result[i] = type_to_xref(str(node), env) + if (result and isinstance(result[-1], addnodes.desc_sig_punctuation) and + result[-1].astext() == '~'): + result.pop() + result.append(type_to_xref(str(node), env, suppress_prefix=True)) + else: + result.append(type_to_xref(str(node), env)) + else: + result.append(node) return result except SyntaxError: return [type_to_xref(annotation, env)] diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index a4d5884e8..5ada06a6a 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1295,6 +1295,8 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ def format_args(self, **kwargs: Any) -> str: if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) + if self.config.autodoc_unqualified_typehints: + kwargs.setdefault('unqualified_typehints', True) try: self.env.app.emit('autodoc-before-process-signature', self.object, False) @@ -1323,6 +1325,9 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ self.add_line(' :async:', sourcename) def format_signature(self, **kwargs: Any) -> str: + if self.config.autodoc_unqualified_typehints: + kwargs.setdefault('unqualified_typehints', True) + sigs = [] if (self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads and @@ -1561,6 +1566,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: def format_args(self, **kwargs: Any) -> str: if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) + if self.config.autodoc_unqualified_typehints: + kwargs.setdefault('unqualified_typehints', True) try: self._signature_class, self._signature_method_name, sig = self._get_signature() @@ -1582,6 +1589,9 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # do not show signatures return '' + if self.config.autodoc_unqualified_typehints: + kwargs.setdefault('unqualified_typehints', True) + sig = super().format_signature() sigs = [] @@ -2110,6 +2120,8 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: def format_args(self, **kwargs: Any) -> str: if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) + if self.config.autodoc_unqualified_typehints: + kwargs.setdefault('unqualified_typehints', True) try: if self.object == object.__init__ and self.parent != object: @@ -2160,6 +2172,9 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: pass def format_signature(self, **kwargs: Any) -> str: + if self.config.autodoc_unqualified_typehints: + kwargs.setdefault('unqualified_typehints', True) + sigs = [] if (self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads and @@ -2833,6 +2848,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('autodoc_typehints_description_target', 'all', True, ENUM('all', 'documented')) app.add_config_value('autodoc_type_aliases', {}, True) + app.add_config_value('autodoc_unqualified_typehints', False, 'env') app.add_config_value('autodoc_warningiserror', True, True) app.add_config_value('autodoc_inherit_docstrings', True, True) app.add_event('autodoc-before-process-signature') diff --git a/sphinx/texinputs/sphinxlatexliterals.sty b/sphinx/texinputs/sphinxlatexliterals.sty index d2ba89ea7..cc768c25b 100644 --- a/sphinx/texinputs/sphinxlatexliterals.sty +++ b/sphinx/texinputs/sphinxlatexliterals.sty @@ -1,7 +1,7 @@ %% LITERAL BLOCKS % % change this info string if making any custom modification -\ProvidesFile{sphinxlatexliterals.sty}[2021/01/27 code-blocks and parsed literals] +\ProvidesFile{sphinxlatexliterals.sty}[2021/12/06 code-blocks and parsed literals] % Provides support for this output mark-up from Sphinx latex writer: % @@ -704,6 +704,10 @@ % the \catcode13=5\relax (deactivate end of input lines) is left to callers \newcommand*{\sphinxunactivateextrasandspace}{\catcode32=10\relax \sphinxunactivateextras}% +% alltt uses a monospace font and linebreaks at dashes (which are escaped +% to \sphinxhyphen{} which expands to -\kern\z@) are inhibited with pdflatex. +% Not with xelatex (cf \defaultfontfeatures in latex writer), so: +\newcommand*{\sphinxhypheninparsedliteral}{\sphinxhyphennobreak} % now for the modified alltt environment \newenvironment{sphinxalltt} {% at start of next line to workaround Emacs/AUCTeX issue with this file @@ -711,6 +715,7 @@ \ifspx@opt@parsedliteralwraps \sbox\sphinxcontinuationbox {\spx@opt@verbatimcontinued}% \sbox\sphinxvisiblespacebox {\spx@opt@verbatimvisiblespace}% + \let\sphinxhyphen\sphinxhypheninparsedliteral \sphinxbreaksattexescapedchars \sphinxbreaksviaactiveinparsedliteral \sphinxbreaksatspaceinparsedliteral @@ -757,10 +762,14 @@ \protected\def\sphinxtextbackslashbreakafter {\discretionary{\sphinx@textbackslash}{\sphinxafterbreak}{\sphinx@textbackslash}} \let\sphinxtextbackslash\sphinxtextbackslashbreakafter +% - is escaped to \sphinxhyphen{} and this default ensures no linebreak +% behaviour (also with a non monospace font, or with xelatex) +\newcommand*{\sphinxhyphenininlineliteral}{\sphinxhyphennobreak} % the macro must be protected if it ends up used in moving arguments, % in 'alltt' \@noligs is done already, and the \scantokens must be avoided. \protected\def\sphinxupquote#1{{\def\@tempa{alltt}% \ifx\@tempa\@currenvir\else + \let\sphinxhyphen\sphinxhyphenininlineliteral \ifspx@opt@inlineliteralwraps % break at . , ; ? ! / \sphinxbreaksviaactive diff --git a/sphinx/texinputs/sphinxlatexstyletext.sty b/sphinx/texinputs/sphinxlatexstyletext.sty index ab50aed56..539ee0de3 100644 --- a/sphinx/texinputs/sphinxlatexstyletext.sty +++ b/sphinx/texinputs/sphinxlatexstyletext.sty @@ -1,7 +1,7 @@ %% TEXT STYLING % % change this info string if making any custom modification -\ProvidesFile{sphinxlatexstyletext.sty}[2021/01/27 text styling] +\ProvidesFile{sphinxlatexstyletext.sty}[2021/12/06 text styling] % Basically everything here consists of macros which are part of the latex % markup produced by the Sphinx latex writer @@ -72,12 +72,20 @@ % Special characters % -% This definition prevents en-dash and em-dash TeX ligatures. +% The \kern\z@ is to prevent en-dash and em-dash TeX ligatures. +% A linebreak can occur after the dash in regular text (this is +% normal behaviour of "-" in TeX, it is not related to \kern\z@). % -% It inserts a potential breakpoint after the hyphen. This is to keep in sync -% with behavior in code-blocks, parsed and inline literals. For a breakpoint -% before the hyphen use \leavevmode\kern\z@- (within \makeatletter/\makeatother) +% Parsed-literals and inline literals also use the \sphinxhyphen +% but linebreaks there are prevented due to monospace font family. +% (xelatex needs a special addition, cf. sphinxlatexliterals.sty) +% +% Inside code-blocks, dashes are escaped via another macro, from +% Pygments latex output (search for \PYGZhy in sphinxlatexliterals.sty), +% and are configured to allow linebreaks despite the monospace font. +% (the #1 swallows the {} from \sphinxhyphen{} mark-up) \protected\def\sphinxhyphen#1{-\kern\z@} +\protected\def\sphinxhyphennobreak#1{\mbox{-}} % The {} from texescape mark-up is kept, else -- gives en-dash in PDF bookmark \def\sphinxhyphenforbookmarks{-} diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 134740929..663e6da68 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -208,7 +208,7 @@ class ApplySourceWorkaround(SphinxTransform): def apply(self, **kwargs: Any) -> None: for node in self.document.traverse(): # type: Node - if isinstance(node, (nodes.TextElement, nodes.image)): + if isinstance(node, (nodes.TextElement, nodes.image, nodes.topic)): apply_source_workaround(node) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 24ea49ae0..3a282ad7c 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -744,10 +744,13 @@ def evaluate_signature(sig: inspect.Signature, globalns: Dict = None, localns: D def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, - show_return_annotation: bool = True) -> str: + show_return_annotation: bool = True, + unqualified_typehints: bool = False) -> str: """Stringify a Signature object. :param show_annotation: Show annotation in result + :param unqualified_typehints: Show annotations as unqualified + (ex. io.StringIO -> StringIO) """ args = [] last_kind = None @@ -771,7 +774,7 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, if show_annotation and param.annotation is not param.empty: arg.write(': ') - arg.write(stringify_annotation(param.annotation)) + arg.write(stringify_annotation(param.annotation, unqualified_typehints)) if param.default is not param.empty: if show_annotation and param.annotation is not param.empty: arg.write(' = ') @@ -791,7 +794,7 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, show_return_annotation is False): return '(%s)' % ', '.join(args) else: - annotation = stringify_annotation(sig.return_annotation) + annotation = stringify_annotation(sig.return_annotation, unqualified_typehints) return '(%s) -> %s' % (', '.join(args), annotation) diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index bc16e44c1..c0700f3bb 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -150,6 +150,11 @@ def apply_source_workaround(node: Element) -> None: for classifier in reversed(list(node.parent.traverse(nodes.classifier))): node.rawsource = re.sub(r'\s*:\s*%s' % re.escape(classifier.astext()), '', node.rawsource) + if isinstance(node, nodes.topic) and node.source is None: + # docutils-0.18 does not fill the source attribute of topic + logger.debug('[i18n] PATCH: %r to have source, line: %s', + get_full_module_name(node), repr_domxml(node)) + node.source, node.line = node.parent.source, node.parent.line # workaround: literal_block under bullet list (#4913) if isinstance(node, nodes.literal_block) and node.source is None: diff --git a/sphinx/util/parallel.py b/sphinx/util/parallel.py index 2a83d6297..d7abc81df 100644 --- a/sphinx/util/parallel.py +++ b/sphinx/util/parallel.py @@ -9,8 +9,6 @@ """ import os -import platform -import sys import time import traceback from math import sqrt @@ -28,12 +26,7 @@ logger = logging.getLogger(__name__) # our parallel functionality only works for the forking Process -# -# Note: "fork" is not recommended on macOS and py38+. -# see https://bugs.python.org/issue33725 -parallel_available = (multiprocessing and - (os.name == 'posix') and - not (sys.version_info > (3, 8) and platform.system() == 'Darwin')) +parallel_available = multiprocessing and os.name == 'posix' class SerialTasks: @@ -64,7 +57,7 @@ class ParallelTasks: # task arguments self._args: Dict[int, Optional[List[Any]]] = {} # list of subprocesses (both started and waiting) - self._procs: Dict[int, multiprocessing.Process] = {} + self._procs: Dict[int, multiprocessing.context.ForkProcess] = {} # list of receiving pipe connections of running subprocesses self._precvs: Dict[int, Any] = {} # list of receiving pipe connections of waiting subprocesses @@ -96,8 +89,8 @@ class ParallelTasks: self._result_funcs[tid] = result_func or (lambda arg, result: None) self._args[tid] = arg precv, psend = multiprocessing.Pipe(False) - proc = multiprocessing.Process(target=self._process, - args=(psend, task_func, arg)) + context = multiprocessing.get_context('fork') + proc = context.Process(target=self._process, args=(psend, task_func, arg)) self._procs[tid] = proc self._precvsWaiting[tid] = precv self._join_one() diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index e1972d86d..259384ec7 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -299,10 +299,19 @@ def _restify_py36(cls: Optional[Type]) -> str: return ':py:obj:`%s.%s`' % (cls.__module__, qualname) -def stringify(annotation: Any) -> str: - """Stringify type annotation object.""" +def stringify(annotation: Any, smartref: bool = False) -> str: + """Stringify type annotation object. + + :param smartref: If true, add "~" prefix to the result to remove the leading + module and class names from the reference text + """ from sphinx.util import inspect # lazy loading + if smartref: + prefix = '~' + else: + prefix = '' + if isinstance(annotation, str): if annotation.startswith("'") and annotation.endswith("'"): # might be a double Forward-ref'ed type. Go unquoting. @@ -313,11 +322,11 @@ def stringify(annotation: Any) -> str: if annotation.__module__ == 'typing': return annotation.__name__ else: - return '.'.join([annotation.__module__, annotation.__name__]) + return prefix + '.'.join([annotation.__module__, annotation.__name__]) elif inspect.isNewType(annotation): if sys.version_info > (3, 10): # newtypes have correct module info since Python 3.10+ - return '%s.%s' % (annotation.__module__, annotation.__name__) + return prefix + '%s.%s' % (annotation.__module__, annotation.__name__) else: return annotation.__name__ elif not annotation: @@ -325,7 +334,7 @@ def stringify(annotation: Any) -> str: elif annotation is NoneType: return 'None' elif annotation in INVALID_BUILTIN_CLASSES: - return INVALID_BUILTIN_CLASSES[annotation] + return prefix + INVALID_BUILTIN_CLASSES[annotation] elif str(annotation).startswith('typing.Annotated'): # for py310+ pass elif (getattr(annotation, '__module__', None) == 'builtins' and @@ -338,28 +347,36 @@ def stringify(annotation: Any) -> str: return '...' if sys.version_info >= (3, 7): # py37+ - return _stringify_py37(annotation) + return _stringify_py37(annotation, smartref) else: - return _stringify_py36(annotation) + return _stringify_py36(annotation, smartref) -def _stringify_py37(annotation: Any) -> str: +def _stringify_py37(annotation: Any, smartref: bool = False) -> str: """stringify() for py37+.""" module = getattr(annotation, '__module__', None) - if module == 'typing': + modprefix = '' + if module == 'typing' and getattr(annotation, '__forward_arg__', None): + qualname = annotation.__forward_arg__ + elif 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 + + if smartref: + modprefix = '~%s.' % module elif hasattr(annotation, '__qualname__'): - qualname = '%s.%s' % (module, annotation.__qualname__) + if smartref: + modprefix = '~%s.' % module + else: + modprefix = '%s.' % module + qualname = annotation.__qualname__ elif hasattr(annotation, '__origin__'): # instantiated generic provided by a user - qualname = stringify(annotation.__origin__) + qualname = stringify(annotation.__origin__, smartref) elif UnionType and isinstance(annotation, UnionType): # types.Union (for py3.10+) qualname = 'types.Union' else: @@ -374,54 +391,63 @@ def _stringify_py37(annotation: Any) -> str: elif qualname in ('Optional', 'Union'): if len(annotation.__args__) > 1 and annotation.__args__[-1] is NoneType: if len(annotation.__args__) > 2: - args = ', '.join(stringify(a) for a in annotation.__args__[:-1]) - return 'Optional[Union[%s]]' % args + args = ', '.join(stringify(a, smartref) for a in annotation.__args__[:-1]) + return '%sOptional[%sUnion[%s]]' % (modprefix, modprefix, args) else: - return 'Optional[%s]' % stringify(annotation.__args__[0]) + return '%sOptional[%s]' % (modprefix, + stringify(annotation.__args__[0], smartref)) else: - args = ', '.join(stringify(a) for a in annotation.__args__) - return 'Union[%s]' % args + args = ', '.join(stringify(a, smartref) for a in annotation.__args__) + return '%sUnion[%s]' % (modprefix, args) elif qualname == 'types.Union': if len(annotation.__args__) > 1 and None in annotation.__args__: args = ' | '.join(stringify(a) for a in annotation.__args__ if a) - return 'Optional[%s]' % args + return '%sOptional[%s]' % (modprefix, args) else: return ' | '.join(stringify(a) for a in annotation.__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) + args = ', '.join(stringify(a, smartref) for a in annotation.__args__[:-1]) + returns = stringify(annotation.__args__[-1], smartref) + return '%s%s[[%s], %s]' % (modprefix, qualname, args, returns) elif qualname == 'Literal': args = ', '.join(repr(a) for a in annotation.__args__) - return '%s[%s]' % (qualname, args) + return '%s%s[%s]' % (modprefix, qualname, args) elif str(annotation).startswith('typing.Annotated'): # for py39+ - return stringify(annotation.__args__[0]) + return stringify(annotation.__args__[0], smartref) elif all(is_system_TypeVar(a) for a in annotation.__args__): # Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT]) - return qualname + return modprefix + qualname else: - args = ', '.join(stringify(a) for a in annotation.__args__) - return '%s[%s]' % (qualname, args) + args = ', '.join(stringify(a, smartref) for a in annotation.__args__) + return '%s%s[%s]' % (modprefix, qualname, args) - return qualname + return modprefix + qualname -def _stringify_py36(annotation: Any) -> str: +def _stringify_py36(annotation: Any, smartref: bool = False) -> str: """stringify() for py36.""" module = getattr(annotation, '__module__', None) - if module == 'typing': + modprefix = '' + if module == 'typing' and getattr(annotation, '__forward_arg__', None): + qualname = annotation.__forward_arg__ + elif 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.', '') + + if smartref: + modprefix = '~%s.' % module elif hasattr(annotation, '__qualname__'): - qualname = '%s.%s' % (module, annotation.__qualname__) + if smartref: + modprefix = '~%s.' % module + else: + modprefix = '%s.' % module + qualname = annotation.__qualname__ else: qualname = repr(annotation) @@ -429,10 +455,10 @@ def _stringify_py36(annotation: Any) -> str: 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) + param_str = ', '.join(stringify(p, smartref) for p in params) + return '%s%s[%s]' % (modprefix, qualname, param_str) else: - return qualname + return modprefix + qualname elif isinstance(annotation, typing.GenericMeta): params = None if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA @@ -440,28 +466,28 @@ def _stringify_py36(annotation: Any) -> str: elif annotation.__origin__ == Generator: # type: ignore params = annotation.__args__ # type: ignore else: # typing.Callable - args = ', '.join(stringify(arg) for arg + args = ', '.join(stringify(arg, smartref) for arg in annotation.__args__[:-1]) # type: ignore result = stringify(annotation.__args__[-1]) # type: ignore - return '%s[[%s], %s]' % (qualname, args, result) + return '%s%s[[%s], %s]' % (modprefix, qualname, args, result) if params is not None: - param_str = ', '.join(stringify(p) for p in params) - return '%s[%s]' % (qualname, param_str) + param_str = ', '.join(stringify(p, smartref) for p in params) + return '%s%s[%s]' % (modprefix, qualname, param_str) elif (hasattr(annotation, '__origin__') and annotation.__origin__ is typing.Union): params = annotation.__args__ if params is not None: if len(params) > 1 and params[-1] is NoneType: if len(params) > 2: - param_str = ", ".join(stringify(p) for p in params[:-1]) - return 'Optional[Union[%s]]' % param_str + param_str = ", ".join(stringify(p, smartref) for p in params[:-1]) + return '%sOptional[%sUnion[%s]]' % (modprefix, modprefix, param_str) else: - return 'Optional[%s]' % stringify(params[0]) + return '%sOptional[%s]' % (modprefix, stringify(params[0])) else: - param_str = ', '.join(stringify(p) for p in params) - return 'Union[%s]' % param_str + param_str = ', '.join(stringify(p, smartref) for p in params) + return '%sUnion[%s]' % (modprefix, param_str) - return qualname + return modprefix + qualname deprecated_alias('sphinx.util.typing', diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 3f032e616..6f7e20241 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -757,9 +757,7 @@ class LaTeXTranslator(SphinxTranslator): self._depart_signature_line(node) def visit_desc_content(self, node: Element) -> None: - if node.children and not isinstance(node.children[0], nodes.paragraph): - # avoid empty desc environment which causes a formatting bug - self.body.append('~') + pass def depart_desc_content(self, node: Element) -> None: pass diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index 6df558323..9a281dc9a 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -194,6 +194,7 @@ class TexinfoTranslator(SphinxTranslator): self.curfilestack: List[str] = [] self.footnotestack: List[Dict[str, List[Union[collected_footnote, bool]]]] = [] # NOQA self.in_footnote = 0 + self.in_samp = 0 self.handled_abbrs: Set[str] = set() self.colwidths: List[int] = None @@ -545,9 +546,12 @@ class TexinfoTranslator(SphinxTranslator): def add_xref(self, id: str, name: str, node: Node) -> None: name = self.escape_menu(name) sid = self.get_short_id(id) - self.body.append('@ref{%s,,%s}' % (sid, name)) - self.referenced_ids.add(sid) - self.referenced_ids.add(self.escape_id(id)) + if self.config.texinfo_cross_references: + self.body.append('@ref{%s,,%s}' % (sid, name)) + self.referenced_ids.add(sid) + self.referenced_ids.add(self.escape_id(id)) + else: + self.body.append(name) # -- Visiting @@ -809,15 +813,23 @@ class TexinfoTranslator(SphinxTranslator): self.body.append('}') def visit_emphasis(self, node: Element) -> None: - self.body.append('@emph{') + element = 'emph' if not self.in_samp else 'var' + self.body.append('@%s{' % element) def depart_emphasis(self, node: Element) -> None: self.body.append('}') + def is_samp(self, node: Element) -> bool: + return 'samp' in node['classes'] + def visit_literal(self, node: Element) -> None: + if self.is_samp(node): + self.in_samp += 1 self.body.append('@code{') def depart_literal(self, node: Element) -> None: + if self.is_samp(node): + self.in_samp -= 1 self.body.append('}') def visit_superscript(self, node: Element) -> None: @@ -1223,7 +1235,11 @@ class TexinfoTranslator(SphinxTranslator): self.depart_topic(node) def visit_label(self, node: Element) -> None: - self.body.append('@w{(') + # label numbering is automatically generated by Texinfo + if self.in_footnote: + raise nodes.SkipNode + else: + self.body.append('@w{(') def depart_label(self, node: Element) -> None: self.body.append(')} ') diff --git a/tests/test_build_texinfo.py b/tests/test_build_texinfo.py index 546ccaabf..bece3a558 100644 --- a/tests/test_build_texinfo.py +++ b/tests/test_build_texinfo.py @@ -112,3 +112,36 @@ def test_texinfo_escape_id(app, status, warning): assert translator.escape_id('Hello(world)') == 'Hello world' assert translator.escape_id('Hello world.') == 'Hello world' assert translator.escape_id('.') == '.' + + +@pytest.mark.sphinx('texinfo', testroot='footnotes') +def test_texinfo_footnote(app, status, warning): + app.builder.build_all() + + output = (app.outdir / 'python.texi').read_text() + assert 'First footnote: @footnote{\nFirst\n}' in output + + +@pytest.mark.sphinx('texinfo') +def test_texinfo_xrefs(app, status, warning): + app.builder.build_all() + output = (app.outdir / 'sphinxtests.texi').read_text() + assert re.search(r'@ref{\w+,,--plugin\.option}', output) + + # Now rebuild it without xrefs + app.config.texinfo_cross_references = False + app.builder.build_all() + output = (app.outdir / 'sphinxtests.texi').read_text() + assert not re.search(r'@ref{\w+,,--plugin\.option}', output) + assert 'Link to perl +p, --ObjC++, --plugin.option, create-auth-token, arg and -j' in output + + +@pytest.mark.sphinx('texinfo', testroot='root') +def test_texinfo_samp_with_variable(app, status, warning): + app.build() + + output = (app.outdir / 'sphinxtests.texi').read_text() + + assert '@code{@var{variable_only}}' in output + assert '@code{@var{variable} and text}' in output + assert '@code{Show @var{variable} in the middle}' in output diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 353640cda..dbd594f83 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -350,6 +350,18 @@ def test_parse_annotation(app): assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="None") +def test_parse_annotation_suppress(app): + doctree = _parse_annotation("~typing.Dict[str, str]", app.env) + assert_node(doctree, ([pending_xref, "Dict"], + [desc_sig_punctuation, "["], + [pending_xref, "str"], + [desc_sig_punctuation, ","], + desc_sig_space, + [pending_xref, "str"], + [desc_sig_punctuation, "]"])) + assert_node(doctree[0], pending_xref, refdomain="py", reftype="class", reftarget="typing.Dict") + + @pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') def test_parse_annotation_Literal(app): doctree = _parse_annotation("Literal[True, False]", app.env) diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 643899286..f3bcd6a97 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -1142,6 +1142,99 @@ def test_autodoc_typehints_description_and_type_aliases(app): ' myint\n' == context) +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_unqualified_typehints': True}) +def test_autodoc_unqualified_typehints(app): + if sys.version_info < (3, 7): + Any = 'Any' + else: + Any = '~typing.Any' + + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.typehints', options) + assert list(actual) == [ + '', + '.. py:module:: target.typehints', + '', + '', + '.. py:data:: CONST1', + ' :module: target.typehints', + ' :type: int', + '', + '', + '.. py:class:: Math(s: str, o: ~typing.Optional[%s] = None)' % Any, + ' :module: target.typehints', + '', + '', + ' .. py:attribute:: Math.CONST1', + ' :module: target.typehints', + ' :type: int', + '', + '', + ' .. py:attribute:: Math.CONST2', + ' :module: target.typehints', + ' :type: int', + ' :value: 1', + '', + '', + ' .. py:method:: Math.decr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.horse(a: str, b: int) -> None', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.incr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + ' .. py:method:: Math.nothing() -> None', + ' :module: target.typehints', + '', + '', + ' .. py:property:: Math.prop', + ' :module: target.typehints', + ' :type: int', + '', + '', + '.. py:class:: NewAnnotation(i: int)', + ' :module: target.typehints', + '', + '', + '.. py:class:: NewComment(i: int)', + ' :module: target.typehints', + '', + '', + '.. py:class:: SignatureFromMetaclass(a: int)', + ' :module: target.typehints', + '', + '', + '.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, ' + 'Union[str, Unknown]] = None, *args: str, **kwargs: str) -> None', + ' :module: target.typehints', + '', + '', + '.. py:function:: decr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + '.. py:function:: incr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', + '.. py:function:: missing_attr(c, a: str, b: Optional[str] = None) -> str', + ' :module: target.typehints', + '', + '', + '.. py:function:: tuple_args(x: ~typing.Tuple[int, ~typing.Union[int, str]]) ' + '-> ~typing.Tuple[int, int]', + ' :module: target.typehints', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_default_options(app): # no settings diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 1bf077b4f..f331acb23 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -259,6 +259,10 @@ def test_signature_annotations(): sig = inspect.signature(f7) assert stringify_signature(sig, show_return_annotation=False) == '(x: Optional[int] = None, y: dict = {})' + # unqualified_typehints is True + sig = inspect.signature(f7) + assert stringify_signature(sig, unqualified_typehints=True) == '(x: ~typing.Optional[int] = None, y: dict = {}) -> None' + @pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') @pytest.mark.sphinx(testroot='ext-autodoc') diff --git a/tests/test_util_logging.py b/tests/test_util_logging.py index 5abcd02ef..b8a0ca2e2 100644 --- a/tests/test_util_logging.py +++ b/tests/test_util_logging.py @@ -10,8 +10,6 @@ import codecs import os -import platform -import sys import pytest from docutils import nodes @@ -318,8 +316,6 @@ def test_colored_logs(app, status, warning): @pytest.mark.xfail(os.name != 'posix', reason="Not working on windows") -@pytest.mark.xfail(platform.system() == 'Darwin' and sys.version_info > (3, 8), - reason="Not working on macOS and py38") def test_logging_in_ParallelTasks(app, status, warning): logging.setup(app, status, warning) logger = logging.getLogger(__name__) diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index c34c4bebc..0b2324e29 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -178,78 +178,156 @@ def test_restify_mock(): def test_stringify(): - assert stringify(int) == "int" - assert stringify(str) == "str" - assert stringify(None) == "None" - assert stringify(Integral) == "numbers.Integral" - assert stringify(Struct) == "struct.Struct" - assert stringify(TracebackType) == "types.TracebackType" - assert stringify(Any) == "Any" + assert stringify(int, False) == "int" + assert stringify(int, True) == "int" + + assert stringify(str, False) == "str" + assert stringify(str, True) == "str" + + assert stringify(None, False) == "None" + assert stringify(None, True) == "None" + + assert stringify(Integral, False) == "numbers.Integral" + assert stringify(Integral, True) == "~numbers.Integral" + + assert stringify(Struct, False) == "struct.Struct" + assert stringify(Struct, True) == "~struct.Struct" + + assert stringify(TracebackType, False) == "types.TracebackType" + assert stringify(TracebackType, True) == "~types.TracebackType" + + assert stringify(Any, False) == "Any" + assert stringify(Any, True) == "~typing.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(Tuple[()]) == "Tuple[()]" - assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]" - assert stringify(MyList[Tuple[int, int]]) == "tests.test_util_typing.MyList[Tuple[int, int]]" - assert stringify(Generator[None, None, None]) == "Generator[None, None, None]" + assert stringify(List, False) == "List" + assert stringify(List, True) == "~typing.List" + + assert stringify(Dict, False) == "Dict" + assert stringify(Dict, True) == "~typing.Dict" + + assert stringify(List[int], False) == "List[int]" + assert stringify(List[int], True) == "~typing.List[int]" + + assert stringify(List[str], False) == "List[str]" + assert stringify(List[str], True) == "~typing.List[str]" + + assert stringify(Dict[str, float], False) == "Dict[str, float]" + assert stringify(Dict[str, float], True) == "~typing.Dict[str, float]" + + assert stringify(Tuple[str, str, str], False) == "Tuple[str, str, str]" + assert stringify(Tuple[str, str, str], True) == "~typing.Tuple[str, str, str]" + + assert stringify(Tuple[str, ...], False) == "Tuple[str, ...]" + assert stringify(Tuple[str, ...], True) == "~typing.Tuple[str, ...]" + + assert stringify(Tuple[()], False) == "Tuple[()]" + assert stringify(Tuple[()], True) == "~typing.Tuple[()]" + + assert stringify(List[Dict[str, Tuple]], False) == "List[Dict[str, Tuple]]" + assert stringify(List[Dict[str, Tuple]], True) == "~typing.List[~typing.Dict[str, ~typing.Tuple]]" + + assert stringify(MyList[Tuple[int, int]], False) == "tests.test_util_typing.MyList[Tuple[int, int]]" + assert stringify(MyList[Tuple[int, int]], True) == "~tests.test_util_typing.MyList[~typing.Tuple[int, int]]" + + assert stringify(Generator[None, None, None], False) == "Generator[None, None, None]" + assert stringify(Generator[None, None, None], True) == "~typing.Generator[None, None, None]" @pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') def test_stringify_type_hints_pep_585(): - 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(tuple[()]) == "tuple[()]" - assert stringify(list[dict[str, tuple]]) == "list[dict[str, tuple]]" - assert stringify(type[int]) == "type[int]" + assert stringify(list[int], False) == "list[int]" + assert stringify(list[int], True) == "list[int]" + + assert stringify(list[str], False) == "list[str]" + assert stringify(list[str], True) == "list[str]" + + assert stringify(dict[str, float], False) == "dict[str, float]" + assert stringify(dict[str, float], True) == "dict[str, float]" + + assert stringify(tuple[str, str, str], False) == "tuple[str, str, str]" + assert stringify(tuple[str, str, str], True) == "tuple[str, str, str]" + + assert stringify(tuple[str, ...], False) == "tuple[str, ...]" + assert stringify(tuple[str, ...], True) == "tuple[str, ...]" + + assert stringify(tuple[()], False) == "tuple[()]" + assert stringify(tuple[()], True) == "tuple[()]" + + assert stringify(list[dict[str, tuple]], False) == "list[dict[str, tuple]]" + assert stringify(list[dict[str, tuple]], True) == "list[dict[str, tuple]]" + + assert stringify(type[int], False) == "type[int]" + assert stringify(type[int], True) == "type[int]" @pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') def test_stringify_Annotated(): from typing import Annotated # type: ignore - assert stringify(Annotated[str, "foo", "bar"]) == "str" # NOQA + assert stringify(Annotated[str, "foo", "bar"], False) == "str" # NOQA + assert stringify(Annotated[str, "foo", "bar"], True) == "str" # NOQA 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" + assert stringify("int", False) == "int" + assert stringify("int", True) == "int" + + assert stringify("str", False) == "str" + assert stringify("str", True) == "str" + + assert stringify(List["int"], False) == "List[int]" + assert stringify(List["int"], True) == "~typing.List[int]" + + assert stringify("Tuple[str]", False) == "Tuple[str]" + assert stringify("Tuple[str]", True) == "Tuple[str]" + + assert stringify("unknown", False) == "unknown" + assert stringify("unknown", True) == "unknown" def test_stringify_type_hints_Callable(): - assert stringify(Callable) == "Callable" + assert stringify(Callable, False) == "Callable" + assert stringify(Callable, True) == "~typing.Callable" if sys.version_info >= (3, 7): - assert stringify(Callable[[str], int]) == "Callable[[str], int]" - assert stringify(Callable[..., int]) == "Callable[[...], int]" + assert stringify(Callable[[str], int], False) == "Callable[[str], int]" + assert stringify(Callable[[str], int], True) == "~typing.Callable[[str], int]" + + assert stringify(Callable[..., int], False) == "Callable[[...], int]" + assert stringify(Callable[..., int], True) == "~typing.Callable[[...], int]" else: - assert stringify(Callable[[str], int]) == "Callable[str, int]" - assert stringify(Callable[..., int]) == "Callable[..., int]" + assert stringify(Callable[[str], int], False) == "Callable[str, int]" + assert stringify(Callable[[str], int], True) == "~typing.Callable[str, int]" + + assert stringify(Callable[..., int], False) == "Callable[..., int]" + assert stringify(Callable[..., int], True) == "~typing.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]" + assert stringify(Optional[int], False) == "Optional[int]" + assert stringify(Optional[int], True) == "~typing.Optional[int]" + + assert stringify(Union[str, None], False) == "Optional[str]" + assert stringify(Union[str, None], True) == "~typing.Optional[str]" + + assert stringify(Union[int, str], False) == "Union[int, str]" + assert stringify(Union[int, str], True) == "~typing.Union[int, str]" if sys.version_info >= (3, 7): - assert stringify(Union[int, Integral]) == "Union[int, numbers.Integral]" - assert (stringify(Union[MyClass1, MyClass2]) == + assert stringify(Union[int, Integral], False) == "Union[int, numbers.Integral]" + assert stringify(Union[int, Integral], True) == "~typing.Union[int, ~numbers.Integral]" + + assert (stringify(Union[MyClass1, MyClass2], False) == "Union[tests.test_util_typing.MyClass1, tests.test_util_typing.]") + assert (stringify(Union[MyClass1, MyClass2], True) == + "~typing.Union[~tests.test_util_typing.MyClass1, ~tests.test_util_typing.]") else: - assert stringify(Union[int, Integral]) == "numbers.Integral" - assert stringify(Union[MyClass1, MyClass2]) == "tests.test_util_typing.MyClass1" + assert stringify(Union[int, Integral], False) == "numbers.Integral" + assert stringify(Union[int, Integral], True) == "~numbers.Integral" + + assert stringify(Union[MyClass1, MyClass2], False) == "tests.test_util_typing.MyClass1" + assert stringify(Union[MyClass1, MyClass2], True) == "~tests.test_util_typing.MyClass1" def test_stringify_type_hints_typevars(): @@ -258,52 +336,83 @@ def test_stringify_type_hints_typevars(): T_contra = TypeVar('T_contra', contravariant=True) if sys.version_info < (3, 7): - assert stringify(T) == "T" - assert stringify(T_co) == "T_co" - assert stringify(T_contra) == "T_contra" - assert stringify(List[T]) == "List[T]" + assert stringify(T, False) == "T" + assert stringify(T, True) == "T" + + assert stringify(T_co, False) == "T_co" + assert stringify(T_co, True) == "T_co" + + assert stringify(T_contra, False) == "T_contra" + assert stringify(T_contra, True) == "T_contra" + + assert stringify(List[T], False) == "List[T]" + assert stringify(List[T], True) == "~typing.List[T]" else: - assert stringify(T) == "tests.test_util_typing.T" - assert stringify(T_co) == "tests.test_util_typing.T_co" - assert stringify(T_contra) == "tests.test_util_typing.T_contra" - assert stringify(List[T]) == "List[tests.test_util_typing.T]" + assert stringify(T, False) == "tests.test_util_typing.T" + assert stringify(T, True) == "~tests.test_util_typing.T" + + assert stringify(T_co, False) == "tests.test_util_typing.T_co" + assert stringify(T_co, True) == "~tests.test_util_typing.T_co" + + assert stringify(T_contra, False) == "tests.test_util_typing.T_contra" + assert stringify(T_contra, True) == "~tests.test_util_typing.T_contra" + + assert stringify(List[T], False) == "List[tests.test_util_typing.T]" + assert stringify(List[T], True) == "~typing.List[~tests.test_util_typing.T]" if sys.version_info >= (3, 10): - assert stringify(MyInt) == "tests.test_util_typing.MyInt" + assert stringify(MyInt, False) == "tests.test_util_typing.MyInt" + assert stringify(MyInt, True) == "~tests.test_util_typing.MyInt" else: - assert stringify(MyInt) == "MyInt" + assert stringify(MyInt, False) == "MyInt" + assert stringify(MyInt, True) == "MyInt" def test_stringify_type_hints_custom_class(): - assert stringify(MyClass1) == "tests.test_util_typing.MyClass1" - assert stringify(MyClass2) == "tests.test_util_typing." + assert stringify(MyClass1, False) == "tests.test_util_typing.MyClass1" + assert stringify(MyClass1, True) == "~tests.test_util_typing.MyClass1" + + assert stringify(MyClass2, False) == "tests.test_util_typing." + assert stringify(MyClass2, True) == "~tests.test_util_typing." def test_stringify_type_hints_alias(): MyStr = str MyTuple = Tuple[str, str] - assert stringify(MyStr) == "str" - assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore + + assert stringify(MyStr, False) == "str" + assert stringify(MyStr, True) == "str" + + assert stringify(MyTuple, False) == "Tuple[str, str]" # type: ignore + assert stringify(MyTuple, True) == "~typing.Tuple[str, str]" # type: ignore @pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') def test_stringify_type_Literal(): from typing import Literal # type: ignore - assert stringify(Literal[1, "2", "\r"]) == "Literal[1, '2', '\\r']" + assert stringify(Literal[1, "2", "\r"], False) == "Literal[1, '2', '\\r']" + assert stringify(Literal[1, "2", "\r"], True) == "~typing.Literal[1, '2', '\\r']" @pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.') def test_stringify_type_union_operator(): - assert stringify(int | None) == "int | None" # type: ignore - assert stringify(int | str) == "int | str" # type: ignore - assert stringify(int | str | None) == "int | str | None" # type: ignore + assert stringify(int | None, False) == "int | None" # type: ignore + assert stringify(int | None, True) == "int | None" # type: ignore + + assert stringify(int | str, False) == "int | str" # type: ignore + assert stringify(int | str, True) == "int | str" # type: ignore + + assert stringify(int | str | None, False) == "int | str | None" # type: ignore + assert stringify(int | str | None, True) == "int | str | None" # type: ignore def test_stringify_broken_type_hints(): - assert stringify(BrokenType) == 'tests.test_util_typing.BrokenType' + assert stringify(BrokenType, False) == 'tests.test_util_typing.BrokenType' + assert stringify(BrokenType, True) == '~tests.test_util_typing.BrokenType' def test_stringify_mock(): with mock(['unknown']): import unknown - assert stringify(unknown.secret.Class) == 'unknown.secret.Class' + assert stringify(unknown.secret.Class, False) == 'unknown.secret.Class' + assert stringify(unknown.secret.Class, True) == 'unknown.secret.Class' diff --git a/tox.ini b/tox.ini index e703cd646..c006fa5a6 100644 --- a/tox.ini +++ b/tox.ini @@ -64,7 +64,7 @@ basepython = python3 description = Run code coverage checks. setenv = - PYTEST_ADDOPTS = --cov sphinx --cov-config {toxinidir}/setup.cfg + PYTEST_ADDOPTS = --cov sphinx --cov-config "{toxinidir}/setup.cfg" commands = {[testenv]commands} coverage report