diff options
Diffstat (limited to '')
-rw-r--r-- | tests/test_util/__init__.py | 0 | ||||
-rw-r--r-- | tests/test_util/intersphinx_data.py | 64 | ||||
-rw-r--r-- | tests/test_util/test_util.py (renamed from tests/test_util.py) | 0 | ||||
-rw-r--r-- | tests/test_util/test_util_console.py | 90 | ||||
-rw-r--r-- | tests/test_util/test_util_display.py (renamed from tests/test_util_display.py) | 22 | ||||
-rw-r--r-- | tests/test_util/test_util_docstrings.py (renamed from tests/test_util_docstrings.py) | 0 | ||||
-rw-r--r-- | tests/test_util/test_util_docutils.py (renamed from tests/test_util_docutils.py) | 0 | ||||
-rw-r--r-- | tests/test_util/test_util_docutils_sphinx_directive.py | 139 | ||||
-rw-r--r-- | tests/test_util/test_util_fileutil.py (renamed from tests/test_util_fileutil.py) | 43 | ||||
-rw-r--r-- | tests/test_util/test_util_i18n.py (renamed from tests/test_util_i18n.py) | 12 | ||||
-rw-r--r-- | tests/test_util/test_util_images.py (renamed from tests/test_util_images.py) | 0 | ||||
-rw-r--r-- | tests/test_util/test_util_inspect.py (renamed from tests/test_util_inspect.py) | 124 | ||||
-rw-r--r-- | tests/test_util/test_util_inventory.py (renamed from tests/test_util_inventory.py) | 79 | ||||
-rw-r--r-- | tests/test_util/test_util_logging.py (renamed from tests/test_util_logging.py) | 24 | ||||
-rw-r--r-- | tests/test_util/test_util_matching.py (renamed from tests/test_util_matching.py) | 0 | ||||
-rw-r--r-- | tests/test_util/test_util_nodes.py (renamed from tests/test_util_nodes.py) | 4 | ||||
-rw-r--r-- | tests/test_util/test_util_rst.py (renamed from tests/test_util_rst.py) | 0 | ||||
-rw-r--r-- | tests/test_util/test_util_template.py (renamed from tests/test_util_template.py) | 0 | ||||
-rw-r--r-- | tests/test_util/test_util_typing.py (renamed from tests/test_util_typing.py) | 428 | ||||
-rw-r--r-- | tests/test_util/typing_test_data.py (renamed from tests/typing_test_data.py) | 10 |
20 files changed, 782 insertions, 257 deletions
diff --git a/tests/test_util/__init__.py b/tests/test_util/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_util/__init__.py diff --git a/tests/test_util/intersphinx_data.py b/tests/test_util/intersphinx_data.py new file mode 100644 index 0000000..95cf80a --- /dev/null +++ b/tests/test_util/intersphinx_data.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import zlib +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Final + +INVENTORY_V1: Final[bytes] = b'''\ +# Sphinx inventory version 1 +# Project: foo +# Version: 1.0 +module mod foo.html +module.cls class foo.html +''' + +INVENTORY_V2: Final[bytes] = b'''\ +# Sphinx inventory version 2 +# Project: foo +# Version: 2.0 +# The remainder of this file is compressed with zlib. +''' + zlib.compress(b'''\ +module1 py:module 0 foo.html#module-module1 Long Module desc +module2 py:module 0 foo.html#module-$ - +module1.func py:function 1 sub/foo.html#$ - +module1.Foo.bar py:method 1 index.html#foo.Bar.baz - +CFunc c:function 2 cfunc.html#CFunc - +std cpp:type 1 index.html#std - +std::uint8_t cpp:type 1 index.html#std_uint8_t - +foo::Bar cpp:class 1 index.html#cpp_foo_bar - +foo::Bar::baz cpp:function 1 index.html#cpp_foo_bar_baz - +foons cpp:type 1 index.html#foons - +foons::bartype cpp:type 1 index.html#foons_bartype - +a term std:term -1 glossary.html#term-a-term - +ls.-l std:cmdoption 1 index.html#cmdoption-ls-l - +docname std:doc -1 docname.html - +foo js:module 1 index.html#foo - +foo.bar js:class 1 index.html#foo.bar - +foo.bar.baz js:method 1 index.html#foo.bar.baz - +foo.bar.qux js:data 1 index.html#foo.bar.qux - +a term including:colon std:term -1 glossary.html#term-a-term-including-colon - +The-Julia-Domain std:label -1 write_inventory/#$ The Julia Domain +''') + +INVENTORY_V2_NO_VERSION: Final[bytes] = b'''\ +# Sphinx inventory version 2 +# Project: foo +# Version: +# The remainder of this file is compressed with zlib. +''' + 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.py b/tests/test_util/test_util.py index 4389894..4389894 100644 --- a/tests/test_util.py +++ b/tests/test_util/test_util.py diff --git a/tests/test_util/test_util_console.py b/tests/test_util/test_util_console.py new file mode 100644 index 0000000..b617a33 --- /dev/null +++ b/tests/test_util/test_util_console.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import itertools +import operator +from typing import TYPE_CHECKING + +import pytest + +from sphinx.util.console import blue, reset, strip_colors, strip_escape_sequences + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from typing import Final, TypeVar + + _T = TypeVar('_T') + +CURSOR_UP: Final[str] = '\x1b[2A' # ignored ANSI code +ERASE_LINE: Final[str] = '\x1b[2K' # supported ANSI code +TEXT: Final[str] = '\x07 Hello world!' + + +@pytest.mark.parametrize( + ('strip_function', 'ansi_base_blocks', 'text_base_blocks'), + [ + ( + strip_colors, + # double ERASE_LINE so that the tested strings may have 2 of them + [TEXT, blue(TEXT), reset(TEXT), ERASE_LINE, ERASE_LINE, CURSOR_UP], + # :func:`strip_colors` removes color codes but keeps ERASE_LINE and CURSOR_UP + [TEXT, TEXT, TEXT, ERASE_LINE, ERASE_LINE, CURSOR_UP], + ), + ( + strip_escape_sequences, + # double ERASE_LINE so that the tested strings may have 2 of them + [TEXT, blue(TEXT), reset(TEXT), ERASE_LINE, ERASE_LINE, CURSOR_UP], + # :func:`strip_escape_sequences` strips ANSI codes known by Sphinx + [TEXT, TEXT, TEXT, '', '', CURSOR_UP], + ), + ], + ids=[strip_colors.__name__, strip_escape_sequences.__name__], +) +def test_strip_ansi( + strip_function: Callable[[str], str], + ansi_base_blocks: Sequence[str], + text_base_blocks: Sequence[str], +) -> None: + assert callable(strip_function) + assert len(text_base_blocks) == len(ansi_base_blocks) + N = len(ansi_base_blocks) + + def next_ansi_blocks(choices: Sequence[str], n: int) -> Sequence[str]: + # Get a list of *n* words from a cyclic sequence of *choices*. + # + # For instance ``next_ansi_blocks(['a', 'b'], 3) == ['a', 'b', 'a']``. + stream = itertools.cycle(choices) + return list(map(operator.itemgetter(0), zip(stream, range(n)))) + + # generate all permutations of length N + for sigma in itertools.permutations(range(N), N): + # apply the permutation on the blocks with ANSI codes + ansi_blocks = list(map(ansi_base_blocks.__getitem__, sigma)) + # apply the permutation on the blocks with stripped codes + text_blocks = list(map(text_base_blocks.__getitem__, sigma)) + + for glue, n in itertools.product(['.', '\n', '\r\n'], range(4 * N)): + ansi_strings = next_ansi_blocks(ansi_blocks, n) + text_strings = next_ansi_blocks(text_blocks, n) + assert len(ansi_strings) == len(text_strings) == n + + ansi_string = glue.join(ansi_strings) + text_string = glue.join(text_strings) + assert strip_function(ansi_string) == text_string + + +def test_strip_ansi_short_forms(): + # In Sphinx, we always "normalize" the color codes so that they + # match "\x1b\[(\d\d;){0,2}(\d\d)m" but it might happen that + # some messages use '\x1b[0m' instead of ``reset(s)``, so we + # test whether this alternative form is supported or not. + + for strip_function in [strip_colors, strip_escape_sequences]: + # \x1b[m and \x1b[0m are equivalent to \x1b[00m + assert strip_function('\x1b[m') == '' + assert strip_function('\x1b[0m') == '' + + # \x1b[1m is equivalent to \x1b[01m + assert strip_function('\x1b[1mbold\x1b[0m') == 'bold' + + # \x1b[K is equivalent to \x1b[0K + assert strip_escape_sequences('\x1b[K') == '' diff --git a/tests/test_util_display.py b/tests/test_util/test_util_display.py index 9ecdd6a..a18fa1e 100644 --- a/tests/test_util_display.py +++ b/tests/test_util/test_util_display.py @@ -2,8 +2,8 @@ import pytest -from sphinx.testing.util import strip_escseq from sphinx.util import logging +from sphinx.util.console import strip_colors from sphinx.util.display import ( SkipProgressMessage, display_chunk, @@ -28,13 +28,14 @@ def test_status_iterator_length_0(app, status, warning): status.seek(0) status.truncate(0) yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ')) - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing ... hello sphinx world \n' in output assert yields == ['hello', 'sphinx', 'world'] @pytest.mark.sphinx('dummy') -def test_status_iterator_verbosity_0(app, status, warning): +def test_status_iterator_verbosity_0(app, status, warning, monkeypatch): + monkeypatch.setenv("FORCE_COLOR", "1") logging.setup(app, status, warning) # test for status_iterator (verbosity=0) @@ -42,7 +43,7 @@ def test_status_iterator_verbosity_0(app, status, warning): status.truncate(0) yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ', length=3, verbosity=0)) - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing ... [ 33%] hello\r' in output assert 'testing ... [ 67%] sphinx\r' in output assert 'testing ... [100%] world\r\n' in output @@ -50,7 +51,8 @@ def test_status_iterator_verbosity_0(app, status, warning): @pytest.mark.sphinx('dummy') -def test_status_iterator_verbosity_1(app, status, warning): +def test_status_iterator_verbosity_1(app, status, warning, monkeypatch): + monkeypatch.setenv("FORCE_COLOR", "1") logging.setup(app, status, warning) # test for status_iterator (verbosity=1) @@ -58,7 +60,7 @@ def test_status_iterator_verbosity_1(app, status, warning): status.truncate(0) yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ', length=3, verbosity=1)) - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing ... [ 33%] hello\n' in output assert 'testing ... [ 67%] sphinx\n' in output assert 'testing ... [100%] world\n\n' in output @@ -73,14 +75,14 @@ def test_progress_message(app, status, warning): with progress_message('testing'): logger.info('blah ', nonl=True) - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing... blah done\n' in output # skipping case with progress_message('testing'): raise SkipProgressMessage('Reason: %s', 'error') # NoQA: EM101 - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing... skipped\nReason: error\n' in output # error case @@ -90,7 +92,7 @@ def test_progress_message(app, status, warning): except Exception: pass - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing... failed\n' in output # decorator @@ -99,5 +101,5 @@ def test_progress_message(app, status, warning): logger.info('in func ', nonl=True) func() - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing... in func done\n' in output diff --git a/tests/test_util_docstrings.py b/tests/test_util/test_util_docstrings.py index 813e84e..813e84e 100644 --- a/tests/test_util_docstrings.py +++ b/tests/test_util/test_util_docstrings.py diff --git a/tests/test_util_docutils.py b/tests/test_util/test_util_docutils.py index 69999eb..69999eb 100644 --- a/tests/test_util_docutils.py +++ b/tests/test_util/test_util_docutils.py 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_fileutil.py b/tests/test_util/test_util_fileutil.py index 9c23821..2071fc3 100644 --- a/tests/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_i18n.py b/tests/test_util/test_util_i18n.py index 9a1ecc5..f2f3249 100644 --- a/tests/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 @@ -160,6 +154,8 @@ def test_CatalogRepository(tmp_path): (tmp_path / 'loc2' / 'xx' / 'LC_MESSAGES').mkdir(parents=True, exist_ok=True) (tmp_path / 'loc2' / 'xx' / 'LC_MESSAGES' / 'test1.po').write_text('#', encoding='utf8') (tmp_path / 'loc2' / 'xx' / 'LC_MESSAGES' / 'test7.po').write_text('#', encoding='utf8') + (tmp_path / 'loc1' / 'xx' / 'LC_MESSAGES' / '.dotdir2').mkdir(parents=True, exist_ok=True) + (tmp_path / 'loc1' / 'xx' / 'LC_MESSAGES' / '.dotdir2' / 'test8.po').write_text('#', encoding='utf8') # for language xx repo = i18n.CatalogRepository(tmp_path, ['loc1', 'loc2'], 'xx', 'utf-8') diff --git a/tests/test_util_images.py b/tests/test_util/test_util_images.py index 15853c7..15853c7 100644 --- a/tests/test_util_images.py +++ b/tests/test_util/test_util_images.py diff --git a/tests/test_util_inspect.py b/tests/test_util/test_util_inspect.py index 73f9656..764ca20 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util/test_util_inspect.py @@ -62,6 +62,7 @@ async def coroutinefunc(): async def asyncgenerator(): yield + partial_func = functools.partial(func) partial_coroutinefunc = functools.partial(coroutinefunc) @@ -222,170 +223,146 @@ def test_signature_partialmethod(): def test_signature_annotations(): - from .typing_test_data import ( - Node, - f0, - f1, - f2, - f3, - f4, - f5, - f6, - f7, - f8, - f9, - f10, - f11, - f12, - f13, - f14, - f15, - f16, - f17, - f18, - f19, - f20, - f21, - f22, - f23, - f24, - f25, - ) + import tests.test_util.typing_test_data as mod # Class annotations - sig = inspect.signature(f0) + sig = inspect.signature(mod.f0) assert stringify_signature(sig) == '(x: int, y: numbers.Integral) -> None' # Generic types with concrete parameters - sig = inspect.signature(f1) + sig = inspect.signature(mod.f1) assert stringify_signature(sig) == '(x: list[int]) -> typing.List[int]' # TypeVars and generic types with TypeVars - sig = inspect.signature(f2) - assert stringify_signature(sig) == ('(x: typing.List[tests.typing_test_data.T],' - ' y: typing.List[tests.typing_test_data.T_co],' - ' z: tests.typing_test_data.T' - ') -> typing.List[tests.typing_test_data.T_contra]') + sig = inspect.signature(mod.f2) + assert stringify_signature(sig) == ('(x: typing.List[tests.test_util.typing_test_data.T],' + ' y: typing.List[tests.test_util.typing_test_data.T_co],' + ' z: tests.test_util.typing_test_data.T' + ') -> typing.List[tests.test_util.typing_test_data.T_contra]') # Union types - sig = inspect.signature(f3) + sig = inspect.signature(mod.f3) assert stringify_signature(sig) == '(x: str | numbers.Integral) -> None' # Quoted annotations - sig = inspect.signature(f4) + sig = inspect.signature(mod.f4) assert stringify_signature(sig) == '(x: str, y: str) -> None' # Keyword-only arguments - sig = inspect.signature(f5) + sig = inspect.signature(mod.f5) assert stringify_signature(sig) == '(x: int, *, y: str, z: str) -> None' # Keyword-only arguments with varargs - sig = inspect.signature(f6) + sig = inspect.signature(mod.f6) assert stringify_signature(sig) == '(x: int, *args, y: str, z: str) -> None' # Space around '=' for defaults - sig = inspect.signature(f7) + sig = inspect.signature(mod.f7) if sys.version_info[:2] <= (3, 10): assert stringify_signature(sig) == '(x: int | None = None, y: dict = {}) -> None' else: assert stringify_signature(sig) == '(x: int = None, y: dict = {}) -> None' # Callable types - sig = inspect.signature(f8) + sig = inspect.signature(mod.f8) assert stringify_signature(sig) == '(x: typing.Callable[[int, str], int]) -> None' - sig = inspect.signature(f9) + sig = inspect.signature(mod.f9) assert stringify_signature(sig) == '(x: typing.Callable) -> None' # Tuple types - sig = inspect.signature(f10) + sig = inspect.signature(mod.f10) assert stringify_signature(sig) == '(x: typing.Tuple[int, str], y: typing.Tuple[int, ...]) -> None' # Instance annotations - sig = inspect.signature(f11) + sig = inspect.signature(mod.f11) assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None' # tuple with more than two items - sig = inspect.signature(f12) + sig = inspect.signature(mod.f12) assert stringify_signature(sig) == '() -> typing.Tuple[int, str, int]' # optional - sig = inspect.signature(f13) + sig = inspect.signature(mod.f13) assert stringify_signature(sig) == '() -> str | None' # optional union - sig = inspect.signature(f20) + sig = inspect.signature(mod.f20) assert stringify_signature(sig) in ('() -> int | str | None', '() -> str | int | None') # Any - sig = inspect.signature(f14) + sig = inspect.signature(mod.f14) assert stringify_signature(sig) == '() -> typing.Any' # ForwardRef - sig = inspect.signature(f15) + sig = inspect.signature(mod.f15) assert stringify_signature(sig) == '(x: Unknown, y: int) -> typing.Any' # keyword only arguments (1) - sig = inspect.signature(f16) + sig = inspect.signature(mod.f16) assert stringify_signature(sig) == '(arg1, arg2, *, arg3=None, arg4=None)' # keyword only arguments (2) - sig = inspect.signature(f17) + sig = inspect.signature(mod.f17) assert stringify_signature(sig) == '(*, arg3, arg4)' - sig = inspect.signature(f18) + sig = inspect.signature(mod.f18) assert stringify_signature(sig) == ('(self, arg1: int | typing.Tuple = 10) -> ' 'typing.List[typing.Dict]') # annotations for variadic and keyword parameters - sig = inspect.signature(f19) + sig = inspect.signature(mod.f19) assert stringify_signature(sig) == '(*args: int, **kwargs: str)' # default value is inspect.Signature.empty - sig = inspect.signature(f21) + sig = inspect.signature(mod.f21) assert stringify_signature(sig) == "(arg1='whatever', arg2)" # type hints by string - sig = inspect.signature(Node.children) - assert stringify_signature(sig) == '(self) -> typing.List[tests.typing_test_data.Node]' + sig = inspect.signature(mod.Node.children) + assert stringify_signature(sig) == '(self) -> typing.List[tests.test_util.typing_test_data.Node]' - sig = inspect.signature(Node.__init__) - assert stringify_signature(sig) == '(self, parent: tests.typing_test_data.Node | None) -> None' + sig = inspect.signature(mod.Node.__init__) + assert stringify_signature(sig) == '(self, parent: tests.test_util.typing_test_data.Node | None) -> None' # show_annotation is False - sig = inspect.signature(f7) + sig = inspect.signature(mod.f7) assert stringify_signature(sig, show_annotation=False) == '(x=None, y={})' # show_return_annotation is False - sig = inspect.signature(f7) + sig = inspect.signature(mod.f7) if sys.version_info[:2] <= (3, 10): assert stringify_signature(sig, show_return_annotation=False) == '(x: int | None = None, y: dict = {})' else: assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})' # unqualified_typehints is True - sig = inspect.signature(f7) + sig = inspect.signature(mod.f7) if sys.version_info[:2] <= (3, 10): assert stringify_signature(sig, unqualified_typehints=True) == '(x: int | None = None, y: dict = {}) -> None' else: assert stringify_signature(sig, unqualified_typehints=True) == '(x: int = None, y: dict = {}) -> None' # case: separator at head - sig = inspect.signature(f22) + sig = inspect.signature(mod.f22) assert stringify_signature(sig) == '(*, a, b)' # case: separator in the middle - sig = inspect.signature(f23) + sig = inspect.signature(mod.f23) assert stringify_signature(sig) == '(a, b, /, c, d)' - sig = inspect.signature(f24) + sig = inspect.signature(mod.f24) assert stringify_signature(sig) == '(a, /, *, b)' # case: separator at tail - sig = inspect.signature(f25) + 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)' @@ -667,6 +644,17 @@ def test_object_description_enum(): assert inspect.object_description(MyEnum.FOO) == "MyEnum.FOO" +def test_object_description_enum_custom_repr(): + class MyEnum(enum.Enum): + FOO = 1 + BAR = 2 + + def __repr__(self): + return self.name + + assert inspect.object_description(MyEnum.FOO) == "FOO" + + def test_getslots(): class Foo: pass @@ -678,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} @@ -842,7 +830,7 @@ def test_getdoc_inherited_decorated_method(): """ class Bar(Foo): - @functools.lru_cache # noqa: B019 + @functools.lru_cache # NoQA: B019 def meth(self): # inherited and decorated method pass diff --git a/tests/test_util_inventory.py b/tests/test_util/test_util_inventory.py index 2c20763..211dc17 100644 --- a/tests/test_util_inventory.py +++ b/tests/test_util/test_util_inventory.py @@ -1,59 +1,22 @@ """Test inventory util functions.""" import os import posixpath -import zlib from io import BytesIO +import sphinx.locale from sphinx.testing.util import SphinxTestApp from sphinx.util.inventory import InventoryFile -inventory_v1 = b'''\ -# Sphinx inventory version 1 -# Project: foo -# Version: 1.0 -module mod foo.html -module.cls class foo.html -''' - -inventory_v2 = b'''\ -# Sphinx inventory version 2 -# Project: foo -# Version: 2.0 -# The remainder of this file is compressed with zlib. -''' + zlib.compress(b'''\ -module1 py:module 0 foo.html#module-module1 Long Module desc -module2 py:module 0 foo.html#module-$ - -module1.func py:function 1 sub/foo.html#$ - -module1.Foo.bar py:method 1 index.html#foo.Bar.baz - -CFunc c:function 2 cfunc.html#CFunc - -std cpp:type 1 index.html#std - -std::uint8_t cpp:type 1 index.html#std_uint8_t - -foo::Bar cpp:class 1 index.html#cpp_foo_bar - -foo::Bar::baz cpp:function 1 index.html#cpp_foo_bar_baz - -foons cpp:type 1 index.html#foons - -foons::bartype cpp:type 1 index.html#foons_bartype - -a term std:term -1 glossary.html#term-a-term - -ls.-l std:cmdoption 1 index.html#cmdoption-ls-l - -docname std:doc -1 docname.html - -foo js:module 1 index.html#foo - -foo.bar js:class 1 index.html#foo.bar - -foo.bar.baz js:method 1 index.html#foo.bar.baz - -foo.bar.qux js:data 1 index.html#foo.bar.qux - -a term including:colon std:term -1 glossary.html#term-a-term-including-colon - -''') - -inventory_v2_not_having_version = b'''\ -# Sphinx inventory version 2 -# Project: foo -# Version: -# The remainder of this file is compressed with zlib. -''' + zlib.compress(b'''\ -module1 py:module 0 foo.html#module-module1 Long Module desc -''') +from tests.test_util.intersphinx_data import ( + INVENTORY_V1, + INVENTORY_V2, + INVENTORY_V2_AMBIGUOUS_TERMS, + INVENTORY_V2_NO_VERSION, +) def test_read_inventory_v1(): - f = BytesIO(inventory_v1) + f = BytesIO(INVENTORY_V1) invdata = InventoryFile.load(f, '/util', posixpath.join) assert invdata['py:module']['module'] == \ ('foo', '1.0', '/util/foo.html#module-module', '-') @@ -62,7 +25,7 @@ def test_read_inventory_v1(): def test_read_inventory_v2(): - f = BytesIO(inventory_v2) + f = BytesIO(INVENTORY_V2) invdata = InventoryFile.load(f, '/util', posixpath.join) assert len(invdata['py:module']) == 2 @@ -80,12 +43,30 @@ def test_read_inventory_v2(): def test_read_inventory_v2_not_having_version(): - f = BytesIO(inventory_v2_not_having_version) + f = BytesIO(INVENTORY_V2_NO_VERSION) invdata = InventoryFile.load(f, '/util', posixpath.join) assert invdata['py:module']['module1'] == \ ('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) @@ -99,8 +80,8 @@ def _write_appconfig(dir, language, prefix=None): def _build_inventory(srcdir): app = SphinxTestApp(srcdir=srcdir) app.build() - app.cleanup() - return (app.outdir / 'objects.inv') + sphinx.locale.translators.clear() + return app.outdir / 'objects.inv' def test_inventory_localization(tmp_path): diff --git a/tests/test_util_logging.py b/tests/test_util/test_util_logging.py index 4d506a8..4ee548a 100644 --- a/tests/test_util_logging.py +++ b/tests/test_util/test_util_logging.py @@ -8,9 +8,8 @@ import pytest from docutils import nodes from sphinx.errors import SphinxWarning -from sphinx.testing.util import strip_escseq from sphinx.util import logging, osutil -from sphinx.util.console import colorize +from sphinx.util.console import colorize, strip_colors from sphinx.util.logging import is_suppressed_warning, prefixed_warnings from sphinx.util.parallel import ParallelTasks @@ -110,7 +109,7 @@ def test_once_warning_log(app, status, warning): logger.warning('message: %d', 1, once=True) logger.warning('message: %d', 2, once=True) - assert 'WARNING: message: 1\nWARNING: message: 2\n' in strip_escseq(warning.getvalue()) + assert 'WARNING: message: 1\nWARNING: message: 2\n' in strip_colors(warning.getvalue()) def test_is_suppressed_warning(): @@ -278,7 +277,7 @@ def test_pending_warnings(app, status, warning): assert 'WARNING: message3' not in warning.getvalue() # actually logged as ordered - assert 'WARNING: message2\nWARNING: message3' in strip_escseq(warning.getvalue()) + assert 'WARNING: message2\nWARNING: message3' in strip_colors(warning.getvalue()) def test_colored_logs(app, status, warning): @@ -396,3 +395,20 @@ def test_get_node_location_abspath(): location = logging.get_node_location(n) assert location == absolute_filename + ':' + + +@pytest.mark.sphinx(confoverrides={'show_warning_types': True}) +def test_show_warning_types(app, status, warning): + logging.setup(app, status, warning) + logger = logging.getLogger(__name__) + logger.warning('message2') + logger.warning('message3', type='test') + logger.warning('message4', type='test', subtype='logging') + + warnings = strip_colors(warning.getvalue()).splitlines() + + assert warnings == [ + 'WARNING: message2', + 'WARNING: message3 [test]', + 'WARNING: message4 [test.logging]', + ] diff --git a/tests/test_util_matching.py b/tests/test_util/test_util_matching.py index 7d865ba..7d865ba 100644 --- a/tests/test_util_matching.py +++ b/tests/test_util/test_util_matching.py diff --git a/tests/test_util_nodes.py b/tests/test_util/test_util_nodes.py index 92e4dc1..ddd5974 100644 --- a/tests/test_util_nodes.py +++ b/tests/test_util/test_util_nodes.py @@ -237,8 +237,8 @@ def test_split_explicit_target(title, expected): def test_apply_source_workaround_literal_block_no_source(): """Regression test for #11091. - Test that apply_source_workaround doesn't raise. - """ + Test that apply_source_workaround doesn't raise. + """ literal_block = nodes.literal_block('', '') list_item = nodes.list_item('', literal_block) bullet_list = nodes.bullet_list('', list_item) diff --git a/tests/test_util_rst.py b/tests/test_util/test_util_rst.py index d50c90c..d50c90c 100644 --- a/tests/test_util_rst.py +++ b/tests/test_util/test_util_rst.py diff --git a/tests/test_util_template.py b/tests/test_util/test_util_template.py index 4601179..4601179 100644 --- a/tests/test_util_template.py +++ b/tests/test_util/test_util_template.py diff --git a/tests/test_util_typing.py b/tests/test_util/test_util_typing.py index d79852e..956cffe 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -1,16 +1,42 @@ """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 from struct import Struct -from types import TracebackType +from types import ( + AsyncGeneratorType, + BuiltinFunctionType, + BuiltinMethodType, + CellType, + ClassMethodDescriptorType, + CodeType, + CoroutineType, + FrameType, + FunctionType, + GeneratorType, + GetSetDescriptorType, + LambdaType, + MappingProxyType, + MemberDescriptorType, + MethodDescriptorType, + MethodType, + MethodWrapperType, + ModuleType, + TracebackType, + WrapperDescriptorType, +) from typing import ( + Annotated, Any, - Callable, Dict, - Generator, + ForwardRef, List, + Literal, NewType, Optional, Tuple, @@ -21,7 +47,7 @@ from typing import ( import pytest from sphinx.ext.autodoc import mock -from sphinx.util.typing import INVALID_BUILTIN_CLASSES, restify, stringify_annotation +from sphinx.util.typing import _INVALID_BUILTIN_CLASSES, restify, stringify_annotation class MyClass1: @@ -48,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`" @@ -76,11 +107,55 @@ def test_restify(): def test_is_invalid_builtin_class(): # if these tests start failing, it means that the __module__ - # of one of these classes has changed, and INVALID_BUILTIN_CLASSES + # of one of these classes has changed, and _INVALID_BUILTIN_CLASSES # in sphinx.util.typing needs to be updated. - assert INVALID_BUILTIN_CLASSES.keys() == {Struct, TracebackType} + assert _INVALID_BUILTIN_CLASSES.keys() == { + Context, + ContextVar, + Token, + Struct, + AsyncGeneratorType, + BuiltinFunctionType, + BuiltinMethodType, + CellType, + ClassMethodDescriptorType, + CodeType, + CoroutineType, + FrameType, + FunctionType, + GeneratorType, + GetSetDescriptorType, + LambdaType, + MappingProxyType, + MemberDescriptorType, + MethodDescriptorType, + MethodType, + MethodWrapperType, + ModuleType, + TracebackType, + WrapperDescriptorType, + } assert Struct.__module__ == '_struct' + assert AsyncGeneratorType.__module__ == 'builtins' + assert BuiltinFunctionType.__module__ == 'builtins' + assert BuiltinMethodType.__module__ == 'builtins' + assert CellType.__module__ == 'builtins' + assert ClassMethodDescriptorType.__module__ == 'builtins' + assert CodeType.__module__ == 'builtins' + assert CoroutineType.__module__ == 'builtins' + assert FrameType.__module__ == 'builtins' + assert FunctionType.__module__ == 'builtins' + assert GeneratorType.__module__ == 'builtins' + assert GetSetDescriptorType.__module__ == 'builtins' + assert LambdaType.__module__ == 'builtins' + assert MappingProxyType.__module__ == 'builtins' + assert MemberDescriptorType.__module__ == 'builtins' + assert MethodDescriptorType.__module__ == 'builtins' + assert MethodType.__module__ == 'builtins' + assert MethodWrapperType.__module__ == 'builtins' + assert ModuleType.__module__ == 'builtins' assert TracebackType.__module__ == 'builtins' + assert WrapperDescriptorType.__module__ == 'builtins' def test_restify_type_hints_containers(): @@ -103,42 +178,80 @@ def test_restify_type_hints_containers(): assert restify(List[Dict[str, Tuple]]) == (":py:class:`~typing.List`\\ " "[:py:class:`~typing.Dict`\\ " "[:py:class:`str`, :py:class:`~typing.Tuple`]]") - assert restify(MyList[Tuple[int, int]]) == (":py:class:`tests.test_util_typing.MyList`\\ " + 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(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(): - assert restify(Optional[int]) == ":py:obj:`~typing.Optional`\\ [:py:class:`int`]" - assert restify(Union[str, None]) == ":py:obj:`~typing.Optional`\\ [:py:class:`str`]" - assert restify(Union[int, str]) == (":py:obj:`~typing.Union`\\ " - "[:py:class:`int`, :py:class:`str`]") - assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ " - "[:py:class:`int`, :py:class:`numbers.Integral`]") - assert restify(Union[int, Integral], "smart") == (":py:obj:`~typing.Union`\\ " - "[:py:class:`int`," - " :py:class:`~numbers.Integral`]") + assert restify(Union[int]) == ":py:class:`int`" + assert restify(Union[int, str]) == ":py:class:`int` | :py:class:`str`" + assert restify(Optional[int]) == ":py:class:`int` | :py:obj:`None`" + + assert restify(Union[str, None]) == ":py:class:`str` | :py:obj:`None`" + assert restify(Union[None, str]) == ":py:obj:`None` | :py:class:`str`" + assert restify(Optional[str]) == ":py:class:`str` | :py:obj:`None`" + + assert restify(Union[int, str, None]) == ( + ":py:class:`int` | :py:class:`str` | :py:obj:`None`" + ) + assert restify(Optional[Union[int, str]]) in { + ":py:class:`str` | :py:class:`int` | :py:obj:`None`", + ":py:class:`int` | :py:class:`str` | :py:obj:`None`", + } + + assert restify(Union[int, Integral]) == ( + ":py:class:`int` | :py:class:`numbers.Integral`" + ) + assert restify(Union[int, Integral], "smart") == ( + ":py:class:`int` | :py:class:`~numbers.Integral`" + ) assert (restify(Union[MyClass1, MyClass2]) == - (":py:obj:`~typing.Union`\\ " - "[:py:class:`tests.test_util_typing.MyClass1`, " - ":py:class:`tests.test_util_typing.<MyClass2>`]")) + (":py:class:`tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`tests.test_util.test_util_typing.<MyClass2>`")) assert (restify(Union[MyClass1, MyClass2], "smart") == - (":py:obj:`~typing.Union`\\ " - "[:py:class:`~tests.test_util_typing.MyClass1`," - " :py:class:`~tests.test_util_typing.<MyClass2>`]")) + (":py:class:`~tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`~tests.test_util.test_util_typing.<MyClass2>`")) + + assert (restify(Optional[Union[MyClass1, MyClass2]]) == + (":py:class:`tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`tests.test_util.test_util_typing.<MyClass2>`" + " | :py:obj:`None`")) + assert (restify(Optional[Union[MyClass1, MyClass2]], "smart") == + (":py:class:`~tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`~tests.test_util.test_util_typing.<MyClass2>`" + " | :py:obj:`None`")) def test_restify_type_hints_typevars(): @@ -146,35 +259,35 @@ def test_restify_type_hints_typevars(): T_co = TypeVar('T_co', covariant=True) T_contra = TypeVar('T_contra', contravariant=True) - assert restify(T) == ":py:obj:`tests.test_util_typing.T`" - assert restify(T, "smart") == ":py:obj:`~tests.test_util_typing.T`" + assert restify(T) == ":py:obj:`tests.test_util.test_util_typing.T`" + assert restify(T, "smart") == ":py:obj:`~tests.test_util.test_util_typing.T`" - assert restify(T_co) == ":py:obj:`tests.test_util_typing.T_co`" - assert restify(T_co, "smart") == ":py:obj:`~tests.test_util_typing.T_co`" + assert restify(T_co) == ":py:obj:`tests.test_util.test_util_typing.T_co`" + assert restify(T_co, "smart") == ":py:obj:`~tests.test_util.test_util_typing.T_co`" - assert restify(T_contra) == ":py:obj:`tests.test_util_typing.T_contra`" - assert restify(T_contra, "smart") == ":py:obj:`~tests.test_util_typing.T_contra`" + assert restify(T_contra) == ":py:obj:`tests.test_util.test_util_typing.T_contra`" + assert restify(T_contra, "smart") == ":py:obj:`~tests.test_util.test_util_typing.T_contra`" - assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util_typing.T`]" - assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util_typing.T`]" + assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util.test_util_typing.T`]" + assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util.test_util_typing.T`]" - assert restify(list[T]) == ":py:class:`list`\\ [:py:obj:`tests.test_util_typing.T`]" - assert restify(list[T], "smart") == ":py:class:`list`\\ [:py:obj:`~tests.test_util_typing.T`]" + assert restify(list[T]) == ":py:class:`list`\\ [:py:obj:`tests.test_util.test_util_typing.T`]" + assert restify(list[T], "smart") == ":py:class:`list`\\ [:py:obj:`~tests.test_util.test_util_typing.T`]" if sys.version_info[:2] >= (3, 10): - assert restify(MyInt) == ":py:class:`tests.test_util_typing.MyInt`" - assert restify(MyInt, "smart") == ":py:class:`~tests.test_util_typing.MyInt`" + assert restify(MyInt) == ":py:class:`tests.test_util.test_util_typing.MyInt`" + assert restify(MyInt, "smart") == ":py:class:`~tests.test_util.test_util_typing.MyInt`" else: assert restify(MyInt) == ":py:class:`MyInt`" assert restify(MyInt, "smart") == ":py:class:`MyInt`" def test_restify_type_hints_custom_class(): - assert restify(MyClass1) == ":py:class:`tests.test_util_typing.MyClass1`" - assert restify(MyClass1, "smart") == ":py:class:`~tests.test_util_typing.MyClass1`" + assert restify(MyClass1) == ":py:class:`tests.test_util.test_util_typing.MyClass1`" + assert restify(MyClass1, "smart") == ":py:class:`~tests.test_util.test_util_typing.MyClass1`" - assert restify(MyClass2) == ":py:class:`tests.test_util_typing.<MyClass2>`" - assert restify(MyClass2, "smart") == ":py:class:`~tests.test_util_typing.<MyClass2>`" + assert restify(MyClass2) == ":py:class:`tests.test_util.test_util_typing.<MyClass2>`" + assert restify(MyClass2, "smart") == ":py:class:`~tests.test_util.test_util_typing.<MyClass2>`" def test_restify_type_hints_alias(): @@ -187,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`]" @@ -196,11 +308,10 @@ 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_typing.MyEnum.a`]' - assert restify(Literal[MyEnum.a], 'smart') == ':py:obj:`~typing.Literal`\\ [:py:attr:`~tests.test_util_typing.MyEnum.a`]' + assert restify(Literal[MyEnum.a], 'fully-qualified-except-typing') == ':py:obj:`~typing.Literal`\\ [:py:attr:`tests.test_util.test_util_typing.MyEnum.a`]' + assert restify(Literal[MyEnum.a], 'smart') == ':py:obj:`~typing.Literal`\\ [:py:attr:`~tests.test_util.test_util_typing.MyEnum.a`]' def test_restify_pep_585(): @@ -223,22 +334,47 @@ def test_restify_pep_585(): "[:py:class:`str`, :py:class:`~typing.Tuple`\\ " "[:py:class:`str`, ...]]]") assert restify(tuple[MyList[list[int]], int]) == (":py:class:`tuple`\\ [" - ":py:class:`tests.test_util_typing.MyList`\\ " + ":py:class:`tests.test_util.test_util_typing.MyList`\\ " "[:py:class:`list`\\ [:py:class:`int`]], " ":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] + assert restify(None | int) == ":py:obj:`None` | :py:class:`int`" # type: ignore[attr-defined] assert restify(int | str) == ":py:class:`int` | :py:class:`str`" # type: ignore[attr-defined] assert restify(int | str | None) == (":py:class:`int` | :py:class:`str` | " # type: ignore[attr-defined] ":py:obj:`None`") def test_restify_broken_type_hints(): - assert restify(BrokenType) == ':py:class:`tests.test_util_typing.BrokenType`' - assert restify(BrokenType, "smart") == ':py:class:`~tests.test_util_typing.BrokenType`' + assert restify(BrokenType) == ':py:class:`tests.test_util.test_util_typing.BrokenType`' + assert restify(BrokenType, "smart") == ':py:class:`~tests.test_util.test_util_typing.BrokenType`' def test_restify_mock(): @@ -249,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" @@ -315,13 +466,25 @@ def test_stringify_type_hints_containers(): assert stringify_annotation(List[Dict[str, Tuple]], "fully-qualified") == "typing.List[typing.Dict[str, typing.Tuple]]" assert stringify_annotation(List[Dict[str, Tuple]], "smart") == "~typing.List[~typing.Dict[str, ~typing.Tuple]]" - assert stringify_annotation(MyList[Tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util_typing.MyList[Tuple[int, int]]" - assert stringify_annotation(MyList[Tuple[int, int]], "fully-qualified") == "tests.test_util_typing.MyList[typing.Tuple[int, int]]" - assert stringify_annotation(MyList[Tuple[int, int]], "smart") == "~tests.test_util_typing.MyList[~typing.Tuple[int, int]]" + assert stringify_annotation(MyList[Tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyList[Tuple[int, int]]" + 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(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(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(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(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(): @@ -346,9 +509,9 @@ def test_stringify_type_hints_pep_585(): assert stringify_annotation(list[dict[str, tuple]], 'fully-qualified-except-typing') == "list[dict[str, tuple]]" assert stringify_annotation(list[dict[str, tuple]], "smart") == "list[dict[str, tuple]]" - assert stringify_annotation(MyList[tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util_typing.MyList[tuple[int, int]]" - assert stringify_annotation(MyList[tuple[int, int]], "fully-qualified") == "tests.test_util_typing.MyList[tuple[int, int]]" - assert stringify_annotation(MyList[tuple[int, int]], "smart") == "~tests.test_util_typing.MyList[tuple[int, int]]" + assert stringify_annotation(MyList[tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyList[tuple[int, int]]" + assert stringify_annotation(MyList[tuple[int, int]], "fully-qualified") == "tests.test_util.test_util_typing.MyList[tuple[int, int]]" + assert stringify_annotation(MyList[tuple[int, int]], "smart") == "~tests.test_util.test_util_typing.MyList[tuple[int, int]]" assert stringify_annotation(type[int], 'fully-qualified-except-typing') == "type[int]" assert stringify_annotation(type[int], "smart") == "type[int]" @@ -359,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(): @@ -395,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(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[[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[..., 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(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(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(): @@ -413,9 +615,12 @@ def test_stringify_type_hints_Union(): assert stringify_annotation(Optional[int], "fully-qualified") == "int | None" assert stringify_annotation(Optional[int], "smart") == "int | None" - assert stringify_annotation(Union[str, None], 'fully-qualified-except-typing') == "str | None" - assert stringify_annotation(Union[str, None], "fully-qualified") == "str | None" - assert stringify_annotation(Union[str, None], "smart") == "str | None" + assert stringify_annotation(Union[int, None], 'fully-qualified-except-typing') == "int | None" + assert stringify_annotation(Union[None, int], 'fully-qualified-except-typing') == "None | int" + assert stringify_annotation(Union[int, None], "fully-qualified") == "int | None" + assert stringify_annotation(Union[None, int], "fully-qualified") == "None | int" + assert stringify_annotation(Union[int, None], "smart") == "int | None" + assert stringify_annotation(Union[None, int], "smart") == "None | int" assert stringify_annotation(Union[int, str], 'fully-qualified-except-typing') == "int | str" assert stringify_annotation(Union[int, str], "fully-qualified") == "int | str" @@ -426,11 +631,11 @@ def test_stringify_type_hints_Union(): assert stringify_annotation(Union[int, Integral], "smart") == "int | ~numbers.Integral" assert (stringify_annotation(Union[MyClass1, MyClass2], 'fully-qualified-except-typing') == - "tests.test_util_typing.MyClass1 | tests.test_util_typing.<MyClass2>") + "tests.test_util.test_util_typing.MyClass1 | tests.test_util.test_util_typing.<MyClass2>") assert (stringify_annotation(Union[MyClass1, MyClass2], "fully-qualified") == - "tests.test_util_typing.MyClass1 | tests.test_util_typing.<MyClass2>") + "tests.test_util.test_util_typing.MyClass1 | tests.test_util.test_util_typing.<MyClass2>") assert (stringify_annotation(Union[MyClass1, MyClass2], "smart") == - "~tests.test_util_typing.MyClass1 | ~tests.test_util_typing.<MyClass2>") + "~tests.test_util.test_util_typing.MyClass1 | ~tests.test_util.test_util_typing.<MyClass2>") def test_stringify_type_hints_typevars(): @@ -438,35 +643,35 @@ def test_stringify_type_hints_typevars(): T_co = TypeVar('T_co', covariant=True) T_contra = TypeVar('T_contra', contravariant=True) - assert stringify_annotation(T, 'fully-qualified-except-typing') == "tests.test_util_typing.T" - assert stringify_annotation(T, "smart") == "~tests.test_util_typing.T" + assert stringify_annotation(T, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.T" + assert stringify_annotation(T, "smart") == "~tests.test_util.test_util_typing.T" - assert stringify_annotation(T_co, 'fully-qualified-except-typing') == "tests.test_util_typing.T_co" - assert stringify_annotation(T_co, "smart") == "~tests.test_util_typing.T_co" + assert stringify_annotation(T_co, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.T_co" + assert stringify_annotation(T_co, "smart") == "~tests.test_util.test_util_typing.T_co" - assert stringify_annotation(T_contra, 'fully-qualified-except-typing') == "tests.test_util_typing.T_contra" - assert stringify_annotation(T_contra, "smart") == "~tests.test_util_typing.T_contra" + assert stringify_annotation(T_contra, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.T_contra" + assert stringify_annotation(T_contra, "smart") == "~tests.test_util.test_util_typing.T_contra" - assert stringify_annotation(List[T], 'fully-qualified-except-typing') == "List[tests.test_util_typing.T]" - assert stringify_annotation(List[T], "smart") == "~typing.List[~tests.test_util_typing.T]" + assert stringify_annotation(List[T], 'fully-qualified-except-typing') == "List[tests.test_util.test_util_typing.T]" + assert stringify_annotation(List[T], "smart") == "~typing.List[~tests.test_util.test_util_typing.T]" - assert stringify_annotation(list[T], 'fully-qualified-except-typing') == "list[tests.test_util_typing.T]" - assert stringify_annotation(list[T], "smart") == "list[~tests.test_util_typing.T]" + assert stringify_annotation(list[T], 'fully-qualified-except-typing') == "list[tests.test_util.test_util_typing.T]" + assert stringify_annotation(list[T], "smart") == "list[~tests.test_util.test_util_typing.T]" if sys.version_info[:2] >= (3, 10): - assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util_typing.MyInt" - assert stringify_annotation(MyInt, "smart") == "~tests.test_util_typing.MyInt" + assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyInt" + assert stringify_annotation(MyInt, "smart") == "~tests.test_util.test_util_typing.MyInt" else: assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "MyInt" assert stringify_annotation(MyInt, "smart") == "MyInt" def test_stringify_type_hints_custom_class(): - assert stringify_annotation(MyClass1, 'fully-qualified-except-typing') == "tests.test_util_typing.MyClass1" - assert stringify_annotation(MyClass1, "smart") == "~tests.test_util_typing.MyClass1" + assert stringify_annotation(MyClass1, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyClass1" + assert stringify_annotation(MyClass1, "smart") == "~tests.test_util.test_util_typing.MyClass1" - assert stringify_annotation(MyClass2, 'fully-qualified-except-typing') == "tests.test_util_typing.<MyClass2>" - assert stringify_annotation(MyClass2, "smart") == "~tests.test_util_typing.<MyClass2>" + assert stringify_annotation(MyClass2, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.<MyClass2>" + assert stringify_annotation(MyClass2, "smart") == "~tests.test_util.test_util_typing.<MyClass2>" def test_stringify_type_hints_alias(): @@ -481,13 +686,12 @@ 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']" - assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified-except-typing') == 'Literal[tests.test_util_typing.MyEnum.a]' - assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified') == 'typing.Literal[tests.test_util_typing.MyEnum.a]' + assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified-except-typing') == 'Literal[tests.test_util.test_util_typing.MyEnum.a]' + assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified') == 'typing.Literal[tests.test_util.test_util_typing.MyEnum.a]' assert stringify_annotation(Literal[MyEnum.a], 'smart') == '~typing.Literal[MyEnum.a]' @@ -510,8 +714,8 @@ def test_stringify_type_union_operator(): def test_stringify_broken_type_hints(): - assert stringify_annotation(BrokenType, 'fully-qualified-except-typing') == 'tests.test_util_typing.BrokenType' - assert stringify_annotation(BrokenType, "smart") == '~tests.test_util_typing.BrokenType' + assert stringify_annotation(BrokenType, 'fully-qualified-except-typing') == 'tests.test_util.test_util_typing.BrokenType' + assert stringify_annotation(BrokenType, "smart") == '~tests.test_util.test_util_typing.BrokenType' def test_stringify_mock(): @@ -523,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" @@ -534,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/typing_test_data.py b/tests/test_util/typing_test_data.py index 8a7ebc4..0588836 100644 --- a/tests/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: @@ -39,7 +39,7 @@ def f6(x: int, *args, y: str, z: str) -> None: pass -def f7(x: int = None, y: dict = {}) -> None: # NoQA: B006 +def f7(x: int = None, y: dict = {}) -> None: # NoQA: B006,RUF013 pass @@ -77,7 +77,7 @@ def f14() -> Any: pass -def f15(x: "Unknown", y: "int") -> Any: # noqa: F821 # type: ignore[attr-defined] +def f15(x: "Unknown", y: "int") -> Any: # NoQA: F821 # type: ignore[attr-defined] pass @@ -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 |