about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/numpy/typing/tests/test_typing.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/numpy/typing/tests/test_typing.py')
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/typing/tests/test_typing.py300
1 files changed, 300 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/numpy/typing/tests/test_typing.py b/.venv/lib/python3.12/site-packages/numpy/typing/tests/test_typing.py
new file mode 100644
index 00000000..6f778e55
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/typing/tests/test_typing.py
@@ -0,0 +1,300 @@
+from __future__ import annotations
+
+import importlib.util
+import os
+import re
+import shutil
+from collections import defaultdict
+from collections.abc import Iterator
+from typing import TYPE_CHECKING
+
+import pytest
+from numpy.typing.mypy_plugin import _EXTENDED_PRECISION_LIST
+
+
+# Only trigger a full `mypy` run if this environment variable is set
+# Note that these tests tend to take over a minute even on a macOS M1 CPU,
+# and more than that in CI.
+RUN_MYPY = "NPY_RUN_MYPY_IN_TESTSUITE" in os.environ
+if RUN_MYPY and RUN_MYPY not in ('0', '', 'false'):
+    RUN_MYPY = True
+
+# Skips all functions in this file
+pytestmark = pytest.mark.skipif(
+    not RUN_MYPY,
+    reason="`NPY_RUN_MYPY_IN_TESTSUITE` not set"
+)
+
+
+# Only trigger a full `mypy` run if this environment variable is set
+# Note that these tests tend to take over a minute even on a macOS M1 CPU,
+# and more than that in CI.
+RUN_MYPY = "NPY_RUN_MYPY_IN_TESTSUITE" in os.environ
+if RUN_MYPY and RUN_MYPY not in ('0', '', 'false'):
+    RUN_MYPY = True
+
+# Skips all functions in this file
+pytestmark = pytest.mark.skipif(
+    not RUN_MYPY,
+    reason="`NPY_RUN_MYPY_IN_TESTSUITE` not set"
+)
+
+
+try:
+    from mypy import api
+except ImportError:
+    NO_MYPY = True
+else:
+    NO_MYPY = False
+
+if TYPE_CHECKING:
+    # We need this as annotation, but it's located in a private namespace.
+    # As a compromise, do *not* import it during runtime
+    from _pytest.mark.structures import ParameterSet
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
+PASS_DIR = os.path.join(DATA_DIR, "pass")
+FAIL_DIR = os.path.join(DATA_DIR, "fail")
+REVEAL_DIR = os.path.join(DATA_DIR, "reveal")
+MISC_DIR = os.path.join(DATA_DIR, "misc")
+MYPY_INI = os.path.join(DATA_DIR, "mypy.ini")
+CACHE_DIR = os.path.join(DATA_DIR, ".mypy_cache")
+
+#: A dictionary with file names as keys and lists of the mypy stdout as values.
+#: To-be populated by `run_mypy`.
+OUTPUT_MYPY: defaultdict[str, list[str]] = defaultdict(list)
+
+
+def _key_func(key: str) -> str:
+    """Split at the first occurrence of the ``:`` character.
+
+    Windows drive-letters (*e.g.* ``C:``) are ignored herein.
+    """
+    drive, tail = os.path.splitdrive(key)
+    return os.path.join(drive, tail.split(":", 1)[0])
+
+
+def _strip_filename(msg: str) -> tuple[int, str]:
+    """Strip the filename and line number from a mypy message."""
+    _, tail = os.path.splitdrive(msg)
+    _, lineno, msg = tail.split(":", 2)
+    return int(lineno), msg.strip()
+
+
+def strip_func(match: re.Match[str]) -> str:
+    """`re.sub` helper function for stripping module names."""
+    return match.groups()[1]
+
+
+@pytest.fixture(scope="module", autouse=True)
+def run_mypy() -> None:
+    """Clears the cache and run mypy before running any of the typing tests.
+
+    The mypy results are cached in `OUTPUT_MYPY` for further use.
+
+    The cache refresh can be skipped using
+
+    NUMPY_TYPING_TEST_CLEAR_CACHE=0 pytest numpy/typing/tests
+    """
+    if (
+        os.path.isdir(CACHE_DIR)
+        and bool(os.environ.get("NUMPY_TYPING_TEST_CLEAR_CACHE", True))
+    ):
+        shutil.rmtree(CACHE_DIR)
+
+    split_pattern = re.compile(r"(\s+)?\^(\~+)?")
+    for directory in (PASS_DIR, REVEAL_DIR, FAIL_DIR, MISC_DIR):
+        # Run mypy
+        stdout, stderr, exit_code = api.run([
+            "--config-file",
+            MYPY_INI,
+            "--cache-dir",
+            CACHE_DIR,
+            directory,
+        ])
+        if stderr:
+            pytest.fail(f"Unexpected mypy standard error\n\n{stderr}")
+        elif exit_code not in {0, 1}:
+            pytest.fail(f"Unexpected mypy exit code: {exit_code}\n\n{stdout}")
+
+        str_concat = ""
+        filename: str | None = None
+        for i in stdout.split("\n"):
+            if "note:" in i:
+                continue
+            if filename is None:
+                filename = _key_func(i)
+
+            str_concat += f"{i}\n"
+            if split_pattern.match(i) is not None:
+                OUTPUT_MYPY[filename].append(str_concat)
+                str_concat = ""
+                filename = None
+
+
+def get_test_cases(directory: str) -> Iterator[ParameterSet]:
+    for root, _, files in os.walk(directory):
+        for fname in files:
+            short_fname, ext = os.path.splitext(fname)
+            if ext in (".pyi", ".py"):
+                fullpath = os.path.join(root, fname)
+                yield pytest.param(fullpath, id=short_fname)
+
+
+@pytest.mark.slow
+@pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed")
+@pytest.mark.parametrize("path", get_test_cases(PASS_DIR))
+def test_success(path) -> None:
+    # Alias `OUTPUT_MYPY` so that it appears in the local namespace
+    output_mypy = OUTPUT_MYPY
+    if path in output_mypy:
+        msg = "Unexpected mypy output\n\n"
+        msg += "\n".join(_strip_filename(v)[1] for v in output_mypy[path])
+        raise AssertionError(msg)
+
+
+@pytest.mark.slow
+@pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed")
+@pytest.mark.parametrize("path", get_test_cases(FAIL_DIR))
+def test_fail(path: str) -> None:
+    __tracebackhide__ = True
+
+    with open(path) as fin:
+        lines = fin.readlines()
+
+    errors = defaultdict(lambda: "")
+
+    output_mypy = OUTPUT_MYPY
+    assert path in output_mypy
+
+    for error_line in output_mypy[path]:
+        lineno, error_line = _strip_filename(error_line)
+        errors[lineno] += f'{error_line}\n'
+
+    for i, line in enumerate(lines):
+        lineno = i + 1
+        if (
+            line.startswith('#')
+            or (" E:" not in line and lineno not in errors)
+        ):
+            continue
+
+        target_line = lines[lineno - 1]
+        if "# E:" in target_line:
+            expression, _, marker = target_line.partition("  # E: ")
+            expected_error = errors[lineno].strip()
+            marker = marker.strip()
+            _test_fail(path, expression, marker, expected_error, lineno)
+        else:
+            pytest.fail(
+                f"Unexpected mypy output at line {lineno}\n\n{errors[lineno]}"
+            )
+
+
+_FAIL_MSG1 = """Extra error at line {}
+
+Expression: {}
+Extra error: {!r}
+"""
+
+_FAIL_MSG2 = """Error mismatch at line {}
+
+Expression: {}
+Expected error: {}
+Observed error: {!r}
+"""
+
+
+def _test_fail(
+    path: str,
+    expression: str,
+    error: str,
+    expected_error: None | str,
+    lineno: int,
+) -> None:
+    if expected_error is None:
+        raise AssertionError(_FAIL_MSG1.format(lineno, expression, error))
+    elif error not in expected_error:
+        raise AssertionError(_FAIL_MSG2.format(
+            lineno, expression, expected_error, error
+        ))
+
+
+_REVEAL_MSG = """Reveal mismatch at line {}
+
+{}
+"""
+
+
+@pytest.mark.slow
+@pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed")
+@pytest.mark.parametrize("path", get_test_cases(REVEAL_DIR))
+def test_reveal(path: str) -> None:
+    """Validate that mypy correctly infers the return-types of
+    the expressions in `path`.
+    """
+    __tracebackhide__ = True
+
+    output_mypy = OUTPUT_MYPY
+    if path not in output_mypy:
+        return
+
+    for error_line in output_mypy[path]:
+        lineno, error_line = _strip_filename(error_line)
+        raise AssertionError(_REVEAL_MSG.format(lineno, error_line))
+
+
+@pytest.mark.slow
+@pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed")
+@pytest.mark.parametrize("path", get_test_cases(PASS_DIR))
+def test_code_runs(path: str) -> None:
+    """Validate that the code in `path` properly during runtime."""
+    path_without_extension, _ = os.path.splitext(path)
+    dirname, filename = path.split(os.sep)[-2:]
+
+    spec = importlib.util.spec_from_file_location(
+        f"{dirname}.{filename}", path
+    )
+    assert spec is not None
+    assert spec.loader is not None
+
+    test_module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(test_module)
+
+
+LINENO_MAPPING = {
+    11: "uint128",
+    12: "uint256",
+    14: "int128",
+    15: "int256",
+    17: "float80",
+    18: "float96",
+    19: "float128",
+    20: "float256",
+    22: "complex160",
+    23: "complex192",
+    24: "complex256",
+    25: "complex512",
+}
+
+
+@pytest.mark.slow
+@pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed")
+def test_extended_precision() -> None:
+    path = os.path.join(MISC_DIR, "extended_precision.pyi")
+    output_mypy = OUTPUT_MYPY
+    assert path in output_mypy
+
+    with open(path) as f:
+        expression_list = f.readlines()
+
+    for _msg in output_mypy[path]:
+        lineno, msg = _strip_filename(_msg)
+        expression = expression_list[lineno - 1].rstrip("\n")
+
+        if LINENO_MAPPING[lineno] in _EXTENDED_PRECISION_LIST:
+            raise AssertionError(_REVEAL_MSG.format(lineno, msg))
+        elif "error" not in msg:
+            _test_fail(
+                path, expression, msg, 'Expression is of type "Any"', lineno
+            )