summaryrefslogtreecommitdiffstats
path: root/tests/test_util
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-09-19 04:57:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-09-19 04:57:09 +0000
commit2722609ed8cf1f24bb6a8b8a5ad9d7ac6dec58c3 (patch)
treee0f8becff83e01bc4228b1824e81a6a355d6e439 /tests/test_util
parentReleasing progress-linux version 7.3.7-3~progress7.99u1. (diff)
downloadsphinx-2722609ed8cf1f24bb6a8b8a5ad9d7ac6dec58c3.tar.xz
sphinx-2722609ed8cf1f24bb6a8b8a5ad9d7ac6dec58c3.zip
Merging upstream version 7.4.7.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests/test_util')
-rw-r--r--tests/test_util/intersphinx_data.py12
-rw-r--r--tests/test_util/test_util_docutils_sphinx_directive.py139
-rw-r--r--tests/test_util/test_util_fileutil.py43
-rw-r--r--tests/test_util/test_util_i18n.py10
-rw-r--r--tests/test_util/test_util_inspect.py6
-rw-r--r--tests/test_util/test_util_inventory.py19
-rw-r--r--tests/test_util/test_util_typing.py197
-rw-r--r--tests/test_util/typing_test_data.py6
8 files changed, 376 insertions, 56 deletions
diff --git a/tests/test_util/intersphinx_data.py b/tests/test_util/intersphinx_data.py
index 042ee76..95cf80a 100644
--- a/tests/test_util/intersphinx_data.py
+++ b/tests/test_util/intersphinx_data.py
@@ -50,3 +50,15 @@ INVENTORY_V2_NO_VERSION: Final[bytes] = b'''\
''' + zlib.compress(b'''\
module1 py:module 0 foo.html#module-module1 Long Module desc
''')
+
+INVENTORY_V2_AMBIGUOUS_TERMS: Final[bytes] = b'''\
+# Sphinx inventory version 2
+# Project: foo
+# Version: 2.0
+# The remainder of this file is compressed with zlib.
+''' + zlib.compress(b'''\
+a term std:term -1 glossary.html#term-a-term -
+A term std:term -1 glossary.html#term-a-term -
+b term std:term -1 document.html#id5 -
+B term std:term -1 document.html#B -
+''')
diff --git a/tests/test_util/test_util_docutils_sphinx_directive.py b/tests/test_util/test_util_docutils_sphinx_directive.py
new file mode 100644
index 0000000..8f5ab3f
--- /dev/null
+++ b/tests/test_util/test_util_docutils_sphinx_directive.py
@@ -0,0 +1,139 @@
+from __future__ import annotations
+
+from types import SimpleNamespace
+
+from docutils import nodes
+from docutils.parsers.rst.languages import en as english # type: ignore[attr-defined]
+from docutils.parsers.rst.states import Inliner, RSTState, RSTStateMachine, state_classes
+from docutils.statemachine import StringList
+
+from sphinx.util.docutils import SphinxDirective, new_document
+
+
+def make_directive(*, env: SimpleNamespace, input_lines: StringList | None = None) -> SphinxDirective:
+ _, directive = make_directive_and_state(env=env, input_lines=input_lines)
+ return directive
+
+
+def make_directive_and_state(*, env: SimpleNamespace, input_lines: StringList | None = None) -> tuple[RSTState, SphinxDirective]:
+ sm = RSTStateMachine(state_classes, initial_state='Body')
+ sm.reporter = object()
+ if input_lines is not None:
+ sm.input_lines = input_lines
+ state = RSTState(sm)
+ state.document = new_document('<tests>')
+ state.document.settings.env = env
+ state.document.settings.tab_width = 4
+ state.document.settings.pep_references = None
+ state.document.settings.rfc_references = None
+ inliner = Inliner()
+ inliner.init_customizations(state.document.settings)
+ state.inliner = inliner
+ state.parent = None
+ state.memo = SimpleNamespace(
+ document=state.document,
+ language=english,
+ inliner=state.inliner,
+ reporter=state.document.reporter,
+ section_level=0,
+ title_styles=[],
+ )
+ directive = SphinxDirective(
+ name='test_directive',
+ arguments=[],
+ options={},
+ content=StringList(),
+ lineno=0,
+ content_offset=0,
+ block_text='',
+ state=state,
+ state_machine=state.state_machine,
+ )
+ return state, directive
+
+
+def test_sphinx_directive_env():
+ state, directive = make_directive_and_state(env=SimpleNamespace())
+
+ assert hasattr(directive, 'env')
+ assert directive.env is state.document.settings.env
+
+
+def test_sphinx_directive_config():
+ env = SimpleNamespace(config=object())
+ state, directive = make_directive_and_state(env=env)
+
+ assert hasattr(directive, 'config')
+ assert directive.config is directive.env.config
+ assert directive.config is state.document.settings.env.config
+
+
+def test_sphinx_directive_get_source_info():
+ env = SimpleNamespace()
+ input_lines = StringList(['spam'], source='<source>')
+ directive = make_directive(env=env, input_lines=input_lines)
+
+ assert directive.get_source_info() == ('<source>', 1)
+
+
+def test_sphinx_directive_set_source_info():
+ env = SimpleNamespace()
+ input_lines = StringList(['spam'], source='<source>')
+ directive = make_directive(env=env, input_lines=input_lines)
+
+ node = nodes.Element()
+ directive.set_source_info(node)
+ assert node.source == '<source>'
+ assert node.line == 1
+
+
+def test_sphinx_directive_get_location():
+ env = SimpleNamespace()
+ input_lines = StringList(['spam'], source='<source>')
+ directive = make_directive(env=env, input_lines=input_lines)
+
+ assert directive.get_location() == '<source>:1'
+
+
+def test_sphinx_directive_parse_content_to_nodes():
+ directive = make_directive(env=SimpleNamespace())
+ content = 'spam\n====\n\nEggs! *Lobster thermidor.*'
+ directive.content = StringList(content.split('\n'), source='<source>')
+
+ parsed = directive.parse_content_to_nodes(allow_section_headings=True)
+ assert len(parsed) == 1
+ node = parsed[0]
+ assert isinstance(node, nodes.section)
+ assert len(node.children) == 2
+ assert isinstance(node.children[0], nodes.title)
+ assert node.children[0].astext() == 'spam'
+ assert isinstance(node.children[1], nodes.paragraph)
+ assert node.children[1].astext() == 'Eggs! Lobster thermidor.'
+
+
+def test_sphinx_directive_parse_text_to_nodes():
+ directive = make_directive(env=SimpleNamespace())
+ content = 'spam\n====\n\nEggs! *Lobster thermidor.*'
+
+ parsed = directive.parse_text_to_nodes(content, allow_section_headings=True)
+ assert len(parsed) == 1
+ node = parsed[0]
+ assert isinstance(node, nodes.section)
+ assert len(node.children) == 2
+ assert isinstance(node.children[0], nodes.title)
+ assert node.children[0].astext() == 'spam'
+ assert isinstance(node.children[1], nodes.paragraph)
+ assert node.children[1].astext() == 'Eggs! Lobster thermidor.'
+
+
+def test_sphinx_directive_parse_inline():
+ directive = make_directive(env=SimpleNamespace())
+ content = 'Eggs! *Lobster thermidor.*'
+
+ parsed, messages = directive.parse_inline(content)
+ assert len(parsed) == 2
+ assert messages == []
+ assert parsed[0] == nodes.Text('Eggs! ')
+ assert isinstance(parsed[1], nodes.emphasis)
+ assert parsed[1].rawsource == '*Lobster thermidor.*'
+ assert parsed[1][0] == nodes.Text('Lobster thermidor.')
diff --git a/tests/test_util/test_util_fileutil.py b/tests/test_util/test_util_fileutil.py
index 9c23821..2071fc3 100644
--- a/tests/test_util/test_util_fileutil.py
+++ b/tests/test_util/test_util_fileutil.py
@@ -2,8 +2,11 @@
from unittest import mock
+import pytest
+
from sphinx.jinja2glue import BuiltinTemplateLoader
-from sphinx.util.fileutil import copy_asset, copy_asset_file
+from sphinx.util import strip_colors
+from sphinx.util.fileutil import _template_basename, copy_asset, copy_asset_file
class DummyTemplateLoader(BuiltinTemplateLoader):
@@ -28,9 +31,9 @@ def test_copy_asset_file(tmp_path):
assert src.read_text(encoding='utf8') == dest.read_text(encoding='utf8')
# copy template file
- src = (tmp_path / 'asset.txt_t')
+ src = (tmp_path / 'asset.txt.jinja')
src.write_text('# {{var1}} data', encoding='utf8')
- dest = (tmp_path / 'output.txt_t')
+ dest = (tmp_path / 'output.txt.jinja')
copy_asset_file(str(src), str(dest), {'var1': 'template'}, renderer)
assert not dest.exists()
@@ -38,7 +41,7 @@ def test_copy_asset_file(tmp_path):
assert (tmp_path / 'output.txt').read_text(encoding='utf8') == '# template data'
# copy template file to subdir
- src = (tmp_path / 'asset.txt_t')
+ src = (tmp_path / 'asset.txt.jinja')
src.write_text('# {{var1}} data', encoding='utf8')
subdir1 = (tmp_path / 'subdir')
subdir1.mkdir(parents=True, exist_ok=True)
@@ -48,14 +51,14 @@ def test_copy_asset_file(tmp_path):
assert (subdir1 / 'asset.txt').read_text(encoding='utf8') == '# template data'
# copy template file without context
- src = (tmp_path / 'asset.txt_t')
+ src = (tmp_path / 'asset.txt.jinja')
subdir2 = (tmp_path / 'subdir2')
subdir2.mkdir(parents=True, exist_ok=True)
copy_asset_file(src, subdir2)
assert not (subdir2 / 'asset.txt').exists()
- assert (subdir2 / 'asset.txt_t').exists()
- assert (subdir2 / 'asset.txt_t').read_text(encoding='utf8') == '# {{var1}} data'
+ assert (subdir2 / 'asset.txt.jinja').exists()
+ assert (subdir2 / 'asset.txt.jinja').read_text(encoding='utf8') == '# {{var1}} data'
def test_copy_asset(tmp_path):
@@ -65,12 +68,12 @@ def test_copy_asset(tmp_path):
source = (tmp_path / 'source')
source.mkdir(parents=True, exist_ok=True)
(source / 'index.rst').write_text('index.rst', encoding='utf8')
- (source / 'foo.rst_t').write_text('{{var1}}.rst', encoding='utf8')
+ (source / 'foo.rst.jinja').write_text('{{var1}}.rst', encoding='utf8')
(source / '_static').mkdir(parents=True, exist_ok=True)
(source / '_static' / 'basic.css').write_text('basic.css', encoding='utf8')
(source / '_templates').mkdir(parents=True, exist_ok=True)
(source / '_templates' / 'layout.html').write_text('layout.html', encoding='utf8')
- (source / '_templates' / 'sidebar.html_t').write_text('sidebar: {{var2}}', encoding='utf8')
+ (source / '_templates' / 'sidebar.html.jinja').write_text('sidebar: {{var2}}', encoding='utf8')
# copy a single file
assert not (tmp_path / 'test1').exists()
@@ -101,3 +104,25 @@ def test_copy_asset(tmp_path):
assert not (destdir / '_static' / 'basic.css').exists()
assert (destdir / '_templates' / 'layout.html').exists()
assert not (destdir / '_templates' / 'sidebar.html').exists()
+
+
+@pytest.mark.sphinx('html', testroot='util-copyasset_overwrite')
+def test_copy_asset_overwrite(app):
+ app.build()
+ src = app.srcdir / 'myext_static' / 'custom-styles.css'
+ dst = app.outdir / '_static' / 'custom-styles.css'
+ assert (
+ f'Copying the source path {src} to {dst} will overwrite data, '
+ 'as a file already exists at the destination path '
+ 'and the content does not match.\n'
+ ) in strip_colors(app.status.getvalue())
+
+
+def test_template_basename():
+ assert _template_basename('asset.txt') is None
+ assert _template_basename('asset.txt.jinja') == 'asset.txt'
+ assert _template_basename('sidebar.html.jinja') == 'sidebar.html'
+
+
+def test_legacy_template_basename():
+ assert _template_basename('asset.txt_t') == 'asset.txt'
diff --git a/tests/test_util/test_util_i18n.py b/tests/test_util/test_util_i18n.py
index f6baa04..f2f3249 100644
--- a/tests/test_util/test_util_i18n.py
+++ b/tests/test_util/test_util_i18n.py
@@ -75,16 +75,10 @@ def test_format_date():
format = '%x'
assert i18n.format_date(format, date=datet, language='en') == 'Feb 7, 2016'
format = '%X'
- if BABEL_VERSION >= (2, 12):
- assert i18n.format_date(format, date=datet, language='en') == '5:11:17\u202fAM'
- else:
- assert i18n.format_date(format, date=datet, language='en') == '5:11:17 AM'
+ assert i18n.format_date(format, date=datet, language='en') == '5:11:17\u202fAM'
assert i18n.format_date(format, date=date, language='en') == 'Feb 7, 2016'
format = '%c'
- if BABEL_VERSION >= (2, 12):
- assert i18n.format_date(format, date=datet, language='en') == 'Feb 7, 2016, 5:11:17\u202fAM'
- else:
- assert i18n.format_date(format, date=datet, language='en') == 'Feb 7, 2016, 5:11:17 AM'
+ assert i18n.format_date(format, date=datet, language='en') == 'Feb 7, 2016, 5:11:17\u202fAM'
assert i18n.format_date(format, date=date, language='en') == 'Feb 7, 2016'
# timezone
diff --git a/tests/test_util/test_util_inspect.py b/tests/test_util/test_util_inspect.py
index 32840b8..764ca20 100644
--- a/tests/test_util/test_util_inspect.py
+++ b/tests/test_util/test_util_inspect.py
@@ -359,6 +359,10 @@ def test_signature_annotations():
sig = inspect.signature(mod.f25)
assert stringify_signature(sig) == '(a, b, /)'
+ # collapse Literal types
+ sig = inspect.signature(mod.f26)
+ assert stringify_signature(sig) == "(x: typing.Literal[1, 2, 3] = 1, y: typing.Literal['a', 'b'] = 'a') -> None"
+
def test_signature_from_str_basic():
signature = '(a, b, *args, c=0, d="blah", **kwargs)'
@@ -662,7 +666,7 @@ def test_getslots():
__slots__ = {'attr': 'docstring'}
class Qux:
- __slots__ = 'attr'
+ __slots__ = 'attr' # NoQA: PLC0205
assert inspect.getslots(Foo) is None
assert inspect.getslots(Bar) == {'attr': None}
diff --git a/tests/test_util/test_util_inventory.py b/tests/test_util/test_util_inventory.py
index 81d31b0..211dc17 100644
--- a/tests/test_util/test_util_inventory.py
+++ b/tests/test_util/test_util_inventory.py
@@ -10,6 +10,7 @@ from sphinx.util.inventory import InventoryFile
from tests.test_util.intersphinx_data import (
INVENTORY_V1,
INVENTORY_V2,
+ INVENTORY_V2_AMBIGUOUS_TERMS,
INVENTORY_V2_NO_VERSION,
)
@@ -48,6 +49,24 @@ def test_read_inventory_v2_not_having_version():
('foo', '', '/util/foo.html#module-module1', 'Long Module desc')
+def test_ambiguous_definition_warning(warning, status):
+ f = BytesIO(INVENTORY_V2_AMBIGUOUS_TERMS)
+ InventoryFile.load(f, '/util', posixpath.join)
+
+ def _multiple_defs_notice_for(entity: str) -> str:
+ return f'contains multiple definitions for {entity}'
+
+ # was warning-level; reduced to info-level - see https://github.com/sphinx-doc/sphinx/issues/12613
+ mult_defs_a, mult_defs_b = (
+ _multiple_defs_notice_for('std:term:a'),
+ _multiple_defs_notice_for('std:term:b'),
+ )
+ assert mult_defs_a not in warning.getvalue().lower()
+ assert mult_defs_a not in status.getvalue().lower()
+ assert mult_defs_b not in warning.getvalue().lower()
+ assert mult_defs_b in status.getvalue().lower()
+
+
def _write_appconfig(dir, language, prefix=None):
prefix = prefix or language
os.makedirs(dir / prefix, exist_ok=True)
diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py
index 9c28029..956cffe 100644
--- a/tests/test_util/test_util_typing.py
+++ b/tests/test_util/test_util_typing.py
@@ -1,6 +1,9 @@
"""Tests util.typing functions."""
+import dataclasses
import sys
+import typing as t
+from collections import abc
from contextvars import Context, ContextVar, Token
from enum import Enum
from numbers import Integral
@@ -28,12 +31,12 @@ from types import (
WrapperDescriptorType,
)
from typing import (
+ Annotated,
Any,
- Callable,
Dict,
- Generator,
- Iterator,
+ ForwardRef,
List,
+ Literal,
NewType,
Optional,
Tuple,
@@ -71,6 +74,11 @@ class BrokenType:
__args__ = int
+@dataclasses.dataclass(frozen=True)
+class Gt:
+ gt: float
+
+
def test_restify():
assert restify(int) == ":py:class:`int`"
assert restify(int, "smart") == ":py:class:`int`"
@@ -173,20 +181,36 @@ def test_restify_type_hints_containers():
assert restify(MyList[Tuple[int, int]]) == (":py:class:`tests.test_util.test_util_typing.MyList`\\ "
"[:py:class:`~typing.Tuple`\\ "
"[:py:class:`int`, :py:class:`int`]]")
- assert restify(Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ "
- "[:py:obj:`None`, :py:obj:`None`, "
- ":py:obj:`None`]")
- assert restify(Iterator[None]) == (":py:class:`~typing.Iterator`\\ "
- "[:py:obj:`None`]")
+ assert restify(t.Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ "
+ "[:py:obj:`None`, :py:obj:`None`, "
+ ":py:obj:`None`]")
+ assert restify(abc.Generator[None, None, None]) == (":py:class:`collections.abc.Generator`\\ "
+ "[:py:obj:`None`, :py:obj:`None`, "
+ ":py:obj:`None`]")
+ assert restify(t.Iterator[None]) == (":py:class:`~typing.Iterator`\\ "
+ "[:py:obj:`None`]")
+ assert restify(abc.Iterator[None]) == (":py:class:`collections.abc.Iterator`\\ "
+ "[:py:obj:`None`]")
-def test_restify_type_hints_Callable():
- assert restify(Callable) == ":py:class:`~typing.Callable`"
+def test_restify_Annotated():
+ assert restify(Annotated[str, "foo", "bar"]) == ":py:class:`~typing.Annotated`\\ [:py:class:`str`, 'foo', 'bar']"
+ assert restify(Annotated[str, "foo", "bar"], 'smart') == ":py:class:`~typing.Annotated`\\ [:py:class:`str`, 'foo', 'bar']"
+ assert restify(Annotated[float, Gt(-10.0)]) == ':py:class:`~typing.Annotated`\\ [:py:class:`float`, :py:class:`tests.test_util.test_util_typing.Gt`\\ (gt=\\ -10.0)]'
+ assert restify(Annotated[float, Gt(-10.0)], 'smart') == ':py:class:`~typing.Annotated`\\ [:py:class:`float`, :py:class:`~tests.test_util.test_util_typing.Gt`\\ (gt=\\ -10.0)]'
- assert restify(Callable[[str], int]) == (":py:class:`~typing.Callable`\\ "
- "[[:py:class:`str`], :py:class:`int`]")
- assert restify(Callable[..., int]) == (":py:class:`~typing.Callable`\\ "
- "[[...], :py:class:`int`]")
+
+def test_restify_type_hints_Callable():
+ assert restify(t.Callable) == ":py:class:`~typing.Callable`"
+ assert restify(t.Callable[[str], int]) == (":py:class:`~typing.Callable`\\ "
+ "[[:py:class:`str`], :py:class:`int`]")
+ assert restify(t.Callable[..., int]) == (":py:class:`~typing.Callable`\\ "
+ "[[...], :py:class:`int`]")
+ assert restify(abc.Callable) == ":py:class:`collections.abc.Callable`"
+ assert restify(abc.Callable[[str], int]) == (":py:class:`collections.abc.Callable`\\ "
+ "[[:py:class:`str`], :py:class:`int`]")
+ assert restify(abc.Callable[..., int]) == (":py:class:`collections.abc.Callable`\\ "
+ "[[...], :py:class:`int`]")
def test_restify_type_hints_Union():
@@ -276,7 +300,6 @@ def test_restify_type_hints_alias():
def test_restify_type_ForwardRef():
- from typing import ForwardRef # type: ignore[attr-defined]
assert restify(ForwardRef("MyInt")) == ":py:class:`MyInt`"
assert restify(list[ForwardRef("MyInt")]) == ":py:class:`list`\\ [:py:class:`MyInt`]"
@@ -285,7 +308,6 @@ def test_restify_type_ForwardRef():
def test_restify_type_Literal():
- from typing import Literal # type: ignore[attr-defined]
assert restify(Literal[1, "2", "\r"]) == ":py:obj:`~typing.Literal`\\ [1, '2', '\\r']"
assert restify(Literal[MyEnum.a], 'fully-qualified-except-typing') == ':py:obj:`~typing.Literal`\\ [:py:attr:`tests.test_util.test_util_typing.MyEnum.a`]'
@@ -317,6 +339,30 @@ def test_restify_pep_585():
":py:class:`int`]")
+def test_restify_Unpack():
+ from typing_extensions import Unpack as UnpackCompat
+
+ class X(t.TypedDict):
+ x: int
+ y: int
+ label: str
+
+ # Unpack is considered as typing special form so we always have '~'
+ if sys.version_info[:2] >= (3, 12):
+ expect = r':py:obj:`~typing.Unpack`\ [:py:class:`X`]'
+ assert restify(UnpackCompat['X'], 'fully-qualified-except-typing') == expect
+ assert restify(UnpackCompat['X'], 'smart') == expect
+ else:
+ expect = r':py:obj:`~typing_extensions.Unpack`\ [:py:class:`X`]'
+ assert restify(UnpackCompat['X'], 'fully-qualified-except-typing') == expect
+ assert restify(UnpackCompat['X'], 'smart') == expect
+
+ if sys.version_info[:2] >= (3, 11):
+ expect = r':py:obj:`~typing.Unpack`\ [:py:class:`X`]'
+ assert restify(t.Unpack['X'], 'fully-qualified-except-typing') == expect
+ assert restify(t.Unpack['X'], 'smart') == expect
+
+
@pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.')
def test_restify_type_union_operator():
assert restify(int | None) == ":py:class:`int` | :py:obj:`None`" # type: ignore[attr-defined]
@@ -339,6 +385,21 @@ def test_restify_mock():
assert restify(unknown.secret.Class, "smart") == ':py:class:`~unknown.secret.Class`'
+@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='ParamSpec not supported in Python 3.9.')
+def test_restify_type_hints_paramspec():
+ from typing import ParamSpec
+ P = ParamSpec('P')
+
+ assert restify(P) == ":py:obj:`tests.test_util.test_util_typing.P`"
+ assert restify(P, "smart") == ":py:obj:`~tests.test_util.test_util_typing.P`"
+
+ assert restify(P.args) == "P.args"
+ assert restify(P.args, "smart") == "P.args"
+
+ assert restify(P.kwargs) == "P.kwargs"
+ assert restify(P.kwargs, "smart") == "P.kwargs"
+
+
def test_stringify_annotation():
assert stringify_annotation(int, 'fully-qualified-except-typing') == "int"
assert stringify_annotation(int, "smart") == "int"
@@ -409,13 +470,21 @@ def test_stringify_type_hints_containers():
assert stringify_annotation(MyList[Tuple[int, int]], "fully-qualified") == "tests.test_util.test_util_typing.MyList[typing.Tuple[int, int]]"
assert stringify_annotation(MyList[Tuple[int, int]], "smart") == "~tests.test_util.test_util_typing.MyList[~typing.Tuple[int, int]]"
- assert stringify_annotation(Generator[None, None, None], 'fully-qualified-except-typing') == "Generator[None, None, None]"
- assert stringify_annotation(Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]"
- assert stringify_annotation(Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]"
+ assert stringify_annotation(t.Generator[None, None, None], 'fully-qualified-except-typing') == "Generator[None, None, None]"
+ assert stringify_annotation(t.Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]"
+ assert stringify_annotation(t.Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]"
+
+ assert stringify_annotation(abc.Generator[None, None, None], 'fully-qualified-except-typing') == "collections.abc.Generator[None, None, None]"
+ assert stringify_annotation(abc.Generator[None, None, None], "fully-qualified") == "collections.abc.Generator[None, None, None]"
+ assert stringify_annotation(abc.Generator[None, None, None], "smart") == "~collections.abc.Generator[None, None, None]"
- assert stringify_annotation(Iterator[None], 'fully-qualified-except-typing') == "Iterator[None]"
- assert stringify_annotation(Iterator[None], "fully-qualified") == "typing.Iterator[None]"
- assert stringify_annotation(Iterator[None], "smart") == "~typing.Iterator[None]"
+ assert stringify_annotation(t.Iterator[None], 'fully-qualified-except-typing') == "Iterator[None]"
+ assert stringify_annotation(t.Iterator[None], "fully-qualified") == "typing.Iterator[None]"
+ assert stringify_annotation(t.Iterator[None], "smart") == "~typing.Iterator[None]"
+
+ assert stringify_annotation(abc.Iterator[None], 'fully-qualified-except-typing') == "collections.abc.Iterator[None]"
+ assert stringify_annotation(abc.Iterator[None], "fully-qualified") == "collections.abc.Iterator[None]"
+ assert stringify_annotation(abc.Iterator[None], "smart") == "~collections.abc.Iterator[None]"
def test_stringify_type_hints_pep_585():
@@ -453,9 +522,36 @@ def test_stringify_type_hints_pep_585():
def test_stringify_Annotated():
- from typing import Annotated # type: ignore[attr-defined]
- assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "str"
- assert stringify_annotation(Annotated[str, "foo", "bar"], "smart") == "str"
+ assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "Annotated[str, 'foo', 'bar']"
+ assert stringify_annotation(Annotated[str, "foo", "bar"], 'smart') == "~typing.Annotated[str, 'foo', 'bar']"
+ assert stringify_annotation(Annotated[float, Gt(-10.0)], 'fully-qualified-except-typing') == "Annotated[float, tests.test_util.test_util_typing.Gt(gt=-10.0)]"
+ assert stringify_annotation(Annotated[float, Gt(-10.0)], 'smart') == "~typing.Annotated[float, ~tests.test_util.test_util_typing.Gt(gt=-10.0)]"
+
+
+def test_stringify_Unpack():
+ from typing_extensions import Unpack as UnpackCompat
+
+ class X(t.TypedDict):
+ x: int
+ y: int
+ label: str
+
+ if sys.version_info[:2] >= (3, 11):
+ # typing.Unpack is introduced in 3.11 but typing_extensions.Unpack only
+ # uses typing.Unpack in 3.12+, so the objects are not synchronised with
+ # each other, but we will assume that users use typing.Unpack.
+ import typing
+
+ UnpackCompat = typing.Unpack # NoQA: F811
+ assert stringify_annotation(UnpackCompat['X']) == 'Unpack[X]'
+ assert stringify_annotation(UnpackCompat['X'], 'smart') == '~typing.Unpack[X]'
+ else:
+ assert stringify_annotation(UnpackCompat['X']) == 'typing_extensions.Unpack[X]'
+ assert stringify_annotation(UnpackCompat['X'], 'smart') == '~typing_extensions.Unpack[X]'
+
+ if sys.version_info[:2] >= (3, 11):
+ assert stringify_annotation(t.Unpack['X']) == 'Unpack[X]'
+ assert stringify_annotation(t.Unpack['X'], 'smart') == '~typing.Unpack[X]'
def test_stringify_type_hints_string():
@@ -489,17 +585,29 @@ def test_stringify_type_hints_string():
def test_stringify_type_hints_Callable():
- assert stringify_annotation(Callable, 'fully-qualified-except-typing') == "Callable"
- assert stringify_annotation(Callable, "fully-qualified") == "typing.Callable"
- assert stringify_annotation(Callable, "smart") == "~typing.Callable"
+ assert stringify_annotation(t.Callable, 'fully-qualified-except-typing') == "Callable"
+ assert stringify_annotation(t.Callable, "fully-qualified") == "typing.Callable"
+ assert stringify_annotation(t.Callable, "smart") == "~typing.Callable"
+
+ assert stringify_annotation(t.Callable[[str], int], 'fully-qualified-except-typing') == "Callable[[str], int]"
+ assert stringify_annotation(t.Callable[[str], int], "fully-qualified") == "typing.Callable[[str], int]"
+ assert stringify_annotation(t.Callable[[str], int], "smart") == "~typing.Callable[[str], int]"
- assert stringify_annotation(Callable[[str], int], 'fully-qualified-except-typing') == "Callable[[str], int]"
- assert stringify_annotation(Callable[[str], int], "fully-qualified") == "typing.Callable[[str], int]"
- assert stringify_annotation(Callable[[str], int], "smart") == "~typing.Callable[[str], int]"
+ assert stringify_annotation(t.Callable[..., int], 'fully-qualified-except-typing') == "Callable[[...], int]"
+ assert stringify_annotation(t.Callable[..., int], "fully-qualified") == "typing.Callable[[...], int]"
+ assert stringify_annotation(t.Callable[..., int], "smart") == "~typing.Callable[[...], int]"
- assert stringify_annotation(Callable[..., int], 'fully-qualified-except-typing') == "Callable[[...], int]"
- assert stringify_annotation(Callable[..., int], "fully-qualified") == "typing.Callable[[...], int]"
- assert stringify_annotation(Callable[..., int], "smart") == "~typing.Callable[[...], int]"
+ assert stringify_annotation(abc.Callable, 'fully-qualified-except-typing') == "collections.abc.Callable"
+ assert stringify_annotation(abc.Callable, "fully-qualified") == "collections.abc.Callable"
+ assert stringify_annotation(abc.Callable, "smart") == "~collections.abc.Callable"
+
+ assert stringify_annotation(abc.Callable[[str], int], 'fully-qualified-except-typing') == "collections.abc.Callable[[str], int]"
+ assert stringify_annotation(abc.Callable[[str], int], "fully-qualified") == "collections.abc.Callable[[str], int]"
+ assert stringify_annotation(abc.Callable[[str], int], "smart") == "~collections.abc.Callable[[str], int]"
+
+ assert stringify_annotation(abc.Callable[..., int], 'fully-qualified-except-typing') == "collections.abc.Callable[[...], int]"
+ assert stringify_annotation(abc.Callable[..., int], "fully-qualified") == "collections.abc.Callable[[...], int]"
+ assert stringify_annotation(abc.Callable[..., int], "smart") == "~collections.abc.Callable[[...], int]"
def test_stringify_type_hints_Union():
@@ -578,7 +686,6 @@ def test_stringify_type_hints_alias():
def test_stringify_type_Literal():
- from typing import Literal # type: ignore[attr-defined]
assert stringify_annotation(Literal[1, "2", "\r"], 'fully-qualified-except-typing') == "Literal[1, '2', '\\r']"
assert stringify_annotation(Literal[1, "2", "\r"], "fully-qualified") == "typing.Literal[1, '2', '\\r']"
assert stringify_annotation(Literal[1, "2", "\r"], "smart") == "~typing.Literal[1, '2', '\\r']"
@@ -620,8 +727,6 @@ def test_stringify_mock():
def test_stringify_type_ForwardRef():
- from typing import ForwardRef # type: ignore[attr-defined]
-
assert stringify_annotation(ForwardRef("MyInt")) == "MyInt"
assert stringify_annotation(ForwardRef("MyInt"), 'smart') == "MyInt"
@@ -631,3 +736,21 @@ def test_stringify_type_ForwardRef():
assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]]) == "Tuple[dict[MyInt, str], list[List[int]]]" # type: ignore[attr-defined]
assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]], 'fully-qualified-except-typing') == "Tuple[dict[MyInt, str], list[List[int]]]" # type: ignore[attr-defined]
assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]], 'smart') == "~typing.Tuple[dict[MyInt, str], list[~typing.List[int]]]" # type: ignore[attr-defined]
+
+
+@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='ParamSpec not supported in Python 3.9.')
+def test_stringify_type_hints_paramspec():
+ from typing import ParamSpec
+ P = ParamSpec('P')
+
+ assert stringify_annotation(P, 'fully-qualified') == "~P"
+ assert stringify_annotation(P, 'fully-qualified-except-typing') == "~P"
+ assert stringify_annotation(P, "smart") == "~P"
+
+ assert stringify_annotation(P.args, 'fully-qualified') == "typing.~P"
+ assert stringify_annotation(P.args, 'fully-qualified-except-typing') == "~P"
+ assert stringify_annotation(P.args, "smart") == "~typing.~P"
+
+ assert stringify_annotation(P.kwargs, 'fully-qualified') == "typing.~P"
+ assert stringify_annotation(P.kwargs, 'fully-qualified-except-typing') == "~P"
+ assert stringify_annotation(P.kwargs, "smart") == "~typing.~P"
diff --git a/tests/test_util/typing_test_data.py b/tests/test_util/typing_test_data.py
index e29b600..0588836 100644
--- a/tests/test_util/typing_test_data.py
+++ b/tests/test_util/typing_test_data.py
@@ -1,6 +1,6 @@
from inspect import Signature
from numbers import Integral
-from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union
+from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, TypeVar, Union
def f0(x: int, y: Integral) -> None:
@@ -121,6 +121,10 @@ def f25(a, b, /):
pass
+def f26(x: Literal[1, 2, 3] = 1, y: Union[Literal["a"], Literal["b"]] = "a") -> None:
+ pass
+
+
class Node:
def __init__(self, parent: Optional['Node']) -> None:
pass