diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-09-19 04:57:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-09-19 04:57:09 +0000 |
commit | 2722609ed8cf1f24bb6a8b8a5ad9d7ac6dec58c3 (patch) | |
tree | e0f8becff83e01bc4228b1824e81a6a355d6e439 /tests/test_util | |
parent | Releasing progress-linux version 7.3.7-3~progress7.99u1. (diff) | |
download | sphinx-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.py | 12 | ||||
-rw-r--r-- | tests/test_util/test_util_docutils_sphinx_directive.py | 139 | ||||
-rw-r--r-- | tests/test_util/test_util_fileutil.py | 43 | ||||
-rw-r--r-- | tests/test_util/test_util_i18n.py | 10 | ||||
-rw-r--r-- | tests/test_util/test_util_inspect.py | 6 | ||||
-rw-r--r-- | tests/test_util/test_util_inventory.py | 19 | ||||
-rw-r--r-- | tests/test_util/test_util_typing.py | 197 | ||||
-rw-r--r-- | tests/test_util/typing_test_data.py | 6 |
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 |