diff --git a/README.md b/README.md index 94702c0..dcda0d5 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ unihttp-openapi-generator generate SPEC [options] | `--optional` | `none` · `omitted` (`none`) — `omitted` distinguishes absent from null (adaptix) | | `--strip-prefix` | `auto` or a dotted prefix to drop from schema names (e.g. `io.k8s.api.core.v1.Pod` → `CoreV1Pod`) | | `--inheritance` | off by default — render `allOf: [$ref]` as a base class instead of merging its fields in | -| `--check` | run `ruff` and `mypy --strict` on the output | +| `--check` | run `ruff` and `mypy --strict` on the output ([details](#checking-the-output--check)) | | `--config` | TOML config file | ### Config file @@ -429,6 +429,26 @@ What to do with `allOf: [{$ref: Base}, ...]`. file uploads, typed responses, and `deprecated`. - Security: apiKey, http bearer/basic, oauth2, openIdConnect. +## Checking the output — `--check` + +`--check` runs `ruff check` and `mypy --strict` over the generated package. + +Both tools are ordinary dependencies of the generator, so installing it installs them — +there is nothing extra to add. They are also resolved from the generator's *own* +environment rather than from `PATH`, so an unrelated `ruff` or `mypy` installed +system-wide can never take over and lint the output by different rules. + +One thing to know if you installed the generator standalone (`uv tool install`, `pipx`): +`mypy --strict` has to resolve the generated code's imports — `unihttp` and your chosen +serializer — and a standalone install has neither. Activate the project virtualenv you +intend to install the client into before running with `--check`, and the generator points +mypy at it. Without an activated virtualenv, `--check` from a standalone install reports +`import-not-found`; install the generator into the project environment instead: + +```bash +uv add --dev unihttp-openapi-generator +``` + ## Limitations - Response headers are not exposed; methods return the response body. diff --git a/pyproject.toml b/pyproject.toml index 124879f..8f854f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,14 +44,18 @@ dependencies = [ "referencing>=0.35.0", "pydantic>=2.0.0", "pydantic-settings>=2.0.0", - # ruff is invoked at runtime to import-sort and format every generated file. + # Both are invoked at runtime, so they are hard requirements rather than extras: + # ruff import-sorts and formats every generated file, and `--check` runs both over + # the result. The generator resolves them from its own environment (see + # `tooling.py`), never from PATH, so a co-installed copy must always exist. "ruff>=0.6.0", + "mypy>=1.10.0", ] [project.optional-dependencies] -# `--check` runs ruff + mypy --strict on the generated package. ruff is already a -# core dependency, so this extra only needs to add mypy. -check = ["mypy>=1.10.0"] +# Retained so existing `unihttp-openapi-generator[check]` installs keep resolving; +# mypy moved into the core dependencies, so this adds nothing. +check = [] [project.scripts] unihttp-openapi-generator = "unihttp_openapi_generator.cli:app" diff --git a/src/unihttp_openapi_generator/pipeline.py b/src/unihttp_openapi_generator/pipeline.py index 1173c1d..b6a7656 100644 --- a/src/unihttp_openapi_generator/pipeline.py +++ b/src/unihttp_openapi_generator/pipeline.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -import shutil import subprocess from pathlib import Path @@ -12,6 +11,11 @@ from unihttp_openapi_generator.ir.builder import build_ir from unihttp_openapi_generator.loader import load_spec from unihttp_openapi_generator.refs import RefResolver +from unihttp_openapi_generator.tooling import ( + mypy_command, + ruff_executable, + target_python_executable, +) logger = logging.getLogger("unihttp_openapi_generator") @@ -20,31 +24,36 @@ class CheckError(Exception): """Raised when ``--check`` finds problems in the generated package.""" -def _run_check(tool: str, args: list[str], package_dir: Path) -> None: - found = shutil.which(tool) - if found is None: - raise CheckError(f"{tool} executable not found on PATH (required by --check)") - result = subprocess.run([found, *args, str(package_dir)], capture_output=True, text=True) +def _run_check(tool: str, command: list[str], package_dir: Path) -> None: + result = subprocess.run([*command, str(package_dir)], capture_output=True, text=True) if result.returncode != 0: raise CheckError(f"{tool} check failed for {package_dir}:\n{result.stdout}{result.stderr}") logger.info("%s check passed for %s", tool, package_dir) +def _mypy_args() -> list[str]: + args = [ + "--strict", + "--disable-error-code", + "no-untyped-call", + "--explicit-package-bases", + ] + # The generated code imports unihttp and the chosen serializer. Those resolve from + # the environment mypy runs in, which is the generator's -- and when the generator + # was installed standalone, that environment has neither. An activated virtualenv + # is the better answer, so hand it to mypy explicitly. + target = target_python_executable() + if target is not None: + args += ["--python-executable", target] + return args + + def _check_package(package_dir: Path) -> None: # Generated packages ship without a ``[tool.ruff]`` table and are meant to lint # under ruff's defaults; ``--isolated`` ignores any ambient config that ruff # would otherwise discover from the cwd/parent dirs. - _run_check("ruff", ["check", "--isolated"], package_dir) - _run_check( - "mypy", - [ - "--strict", - "--disable-error-code", - "no-untyped-call", - "--explicit-package-bases", - ], - package_dir, - ) + _run_check("ruff", [ruff_executable(), "check", "--isolated"], package_dir) + _run_check("mypy", [*mypy_command(), *_mypy_args()], package_dir) def run_generation(spec_source: str, config: GeneratorConfig) -> Path: diff --git a/src/unihttp_openapi_generator/postprocess.py b/src/unihttp_openapi_generator/postprocess.py index 7ee6299..ff5732c 100644 --- a/src/unihttp_openapi_generator/postprocess.py +++ b/src/unihttp_openapi_generator/postprocess.py @@ -2,25 +2,19 @@ from __future__ import annotations -import shutil import subprocess from pathlib import Path +from unihttp_openapi_generator.tooling import ruff_executable + class PostProcessError(Exception): """Raised when an external formatter/checker fails.""" -def _ruff() -> str: - found = shutil.which("ruff") - if found is None: - raise PostProcessError("ruff executable not found on PATH") - return found - - def _run(args: list[str], source: str, *, filename: str) -> str: result = subprocess.run( - [_ruff(), *args, "--stdin-filename", filename, "-"], + [ruff_executable(), *args, "--stdin-filename", filename, "-"], input=source, capture_output=True, text=True, @@ -46,14 +40,14 @@ def format_path(path: Path) -> None: """Run ruff import-sorting and formatting over files on disk (project-aware).""" target = str(path) fix = subprocess.run( - [_ruff(), "check", "--select", "I,F401", "--fix", "--quiet", target], + [ruff_executable(), "check", "--select", "I,F401", "--fix", "--quiet", target], capture_output=True, text=True, ) if fix.returncode not in (0, 1): # 1 == remaining lint findings, acceptable here raise PostProcessError(f"ruff check failed for {target}:\n{fix.stderr or fix.stdout}") fmt = subprocess.run( - [_ruff(), "format", "--quiet", target], + [ruff_executable(), "format", "--quiet", target], capture_output=True, text=True, ) diff --git a/src/unihttp_openapi_generator/tooling.py b/src/unihttp_openapi_generator/tooling.py new file mode 100644 index 0000000..3512cd6 --- /dev/null +++ b/src/unihttp_openapi_generator/tooling.py @@ -0,0 +1,124 @@ +"""Locate the ruff/mypy that ship with the generator, not whatever is on ``PATH``. + +``ruff`` and ``mypy`` are declared dependencies, so installing the generator installs +them into the generator's own environment. That environment's script directory is not +necessarily on ``PATH``: ``uv tool install`` and ``pipx`` expose only the console +scripts a distribution declares, so ``shutil.which("ruff")`` reaches past the pinned +copy and finds an unrelated system install -- a different version, formatting the +generated code by different rules -- or nothing at all. + +Every lookup here therefore starts from the interpreter running the generator and only +falls back to ``PATH`` as a last resort. +""" + +from __future__ import annotations + +import functools +import importlib.util +import os +import shutil +import sys +import sysconfig + + +class ToolNotFoundError(Exception): + """Raised when a tool the generator depends on cannot be located.""" + + +def _install_scripts_dir() -> str | None: + """Scripts directory of the environment *this module* is installed into. + + Searched first, because ruff arrived through the same install as the generator. + ``sysconfig``'s default scheme cannot stand in for this: under ``pip install + --user`` it names the system ``bin`` while the generator and its ruff live under + the user scheme, so a too-old system ruff would win. Returns ``None`` from a source + checkout, where the module does not sit under ``site-packages`` at all. + """ + site_packages = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + if os.path.basename(site_packages) not in ("site-packages", "dist-packages"): + return None + if os.name == "nt": # /Lib/site-packages -> /Scripts + return os.path.join(os.path.dirname(os.path.dirname(site_packages)), "Scripts") + # /lib/python3.X/site-packages -> /bin + prefix = os.path.dirname(os.path.dirname(os.path.dirname(site_packages))) + return os.path.join(prefix, "bin") + + +def _script_dirs() -> list[str]: + """Script directories to search, own-environment first, ambient last.""" + candidates = [ + # Where the generator itself was installed -- ruff came from the same install. + _install_scripts_dir(), + # The active environment (inside a venv, its ``bin``/``Scripts``). + sysconfig.get_path("scripts"), + # Alongside the interpreter itself, for layouts sysconfig describes oddly. + os.path.dirname(sys.executable) if sys.executable else "", + # ``pip install --user`` puts scripts in the user scheme, e.g. ``~/.local/bin``. + sysconfig.get_path("scripts", scheme=sysconfig.get_preferred_scheme("user")), + # The base interpreter, for a venv created with ``--system-site-packages``. + sysconfig.get_path("scripts", vars={"base": sys.base_prefix}), + ] + unique: list[str] = [] + for directory in candidates: + if directory and directory not in unique: + unique.append(directory) + return unique + + +def _find_executable(name: str) -> str | None: + exe = name + (sysconfig.get_config_var("EXE") or "") + for directory in _script_dirs(): + candidate = os.path.join(directory, exe) + if os.path.isfile(candidate): + return candidate + return shutil.which(name) + + +@functools.cache +def ruff_executable() -> str: + """Absolute path to ruff, preferring the copy installed with the generator.""" + found = _find_executable("ruff") + if found is None: + raise ToolNotFoundError( + "ruff was not found. It is a dependency of unihttp-openapi-generator, so " + "reinstalling the generator restores it: " + "pip install --force-reinstall unihttp-openapi-generator" + ) + return found + + +def mypy_command() -> list[str]: + """Command prefix that runs the mypy installed with the generator. + + ``python -m mypy`` rather than a resolved script path, because the interpreter is + also what mypy resolves the checked package's imports against by default. + """ + if importlib.util.find_spec("mypy") is None: + raise ToolNotFoundError( + "mypy was not found. It is a dependency of unihttp-openapi-generator, so " + "reinstalling the generator restores it: " + "pip install --force-reinstall unihttp-openapi-generator" + ) + return [sys.executable, "-m", "mypy"] + + +def target_python_executable() -> str | None: + """The interpreter whose site-packages the *generated* code should resolve against. + + ``--check`` type-checks generated code that imports unihttp and the chosen + serializer. Those live in the user's project environment, which is a different + environment from the generator's whenever the generator was installed as a + standalone tool. When an activated virtualenv says so, point mypy at it; otherwise + the interpreter running the generator is already the right answer. + """ + venv = os.environ.get("VIRTUAL_ENV") + if not venv: + return None + # Compare environment roots, not the interpreters: two venvs built from the same + # base python have ``bin/python`` symlinks that resolve to one shared binary, so + # ``samefile`` would call distinct environments identical. + if os.path.normpath(venv) == os.path.normpath(sys.prefix): + return None + bindir = "Scripts" if os.name == "nt" else "bin" + exe = os.path.join(venv, bindir, "python" + (sysconfig.get_config_var("EXE") or "")) + return exe if os.path.isfile(exe) else None diff --git a/tests/conftest.py b/tests/conftest.py index 9feb882..810e1f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,10 +2,21 @@ from __future__ import annotations +from collections.abc import Iterator from typing import Any import pytest +from unihttp_openapi_generator.tooling import ruff_executable + + +@pytest.fixture(autouse=True) +def _reset_tool_lookup_cache() -> Iterator[None]: + """Keep a monkeypatched ruff lookup in one test from leaking into the next.""" + ruff_executable.cache_clear() + yield + ruff_executable.cache_clear() + @pytest.fixture def sample_spec() -> dict[str, Any]: diff --git a/tests/test_pipeline_cli_extra.py b/tests/test_pipeline_cli_extra.py index e16683c..f50609b 100644 --- a/tests/test_pipeline_cli_extra.py +++ b/tests/test_pipeline_cli_extra.py @@ -2,7 +2,7 @@ from __future__ import annotations -import shutil +import sys from pathlib import Path import pytest @@ -10,15 +10,30 @@ from unihttp_openapi_generator import __version__ from unihttp_openapi_generator.cli import app -from unihttp_openapi_generator.pipeline import CheckError, _run_check +from unihttp_openapi_generator.pipeline import CheckError, _mypy_args, _run_check runner = CliRunner() -def test_run_check_tool_missing(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(shutil, "which", lambda name: None) - with pytest.raises(CheckError, match="not found on PATH"): - _run_check("ruff", ["check"], Path("/nonexistent")) +def test_run_check_reports_the_tool_output(tmp_path: Path) -> None: + script = "print('boom'); raise SystemExit(1)" + with pytest.raises(CheckError, match="boom"): + _run_check("ruff", [sys.executable, "-c", script], tmp_path) + + +def test_mypy_args_omit_python_executable_without_a_separate_venv( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("VIRTUAL_ENV", raising=False) + assert "--python-executable" not in _mypy_args() + + +def test_mypy_args_target_a_separate_activated_venv(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + "unihttp_openapi_generator.pipeline.target_python_executable", + lambda: "/proj/.venv/bin/python", + ) + assert _mypy_args()[-2:] == ["--python-executable", "/proj/.venv/bin/python"] def test_version_flag() -> None: diff --git a/tests/test_postprocess.py b/tests/test_postprocess.py index 56baa4b..b53a72a 100644 --- a/tests/test_postprocess.py +++ b/tests/test_postprocess.py @@ -2,12 +2,12 @@ from __future__ import annotations -import shutil import subprocess from pathlib import Path import pytest +from unihttp_openapi_generator import postprocess from unihttp_openapi_generator.postprocess import ( PostProcessError, format_path, @@ -22,14 +22,12 @@ def __init__(self, returncode: int) -> None: self.stderr = "stderr" -def test_ruff_not_found(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(shutil, "which", lambda name: None) - with pytest.raises(PostProcessError, match="ruff executable not found"): - format_python("x = 1\n") +@pytest.fixture +def stub_ruff(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(postprocess, "ruff_executable", lambda: "/usr/bin/ruff") -def test_format_python_nonzero_returncode(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/ruff") +def test_format_python_nonzero_returncode(monkeypatch: pytest.MonkeyPatch, stub_ruff: None) -> None: def fake_run(args: list[str], **kwargs: object) -> _Completed: return _Completed(1) @@ -39,9 +37,9 @@ def fake_run(args: list[str], **kwargs: object) -> _Completed: format_python("x = 1\n") -def test_format_path_check_failure(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/ruff") - +def test_format_path_check_failure( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, stub_ruff: None +) -> None: def fake_run(args: list[str], **kwargs: object) -> _Completed: # returncode 2 from ``ruff check`` is neither clean (0) nor lint-only (1) return _Completed(2 if "check" in args else 0) @@ -51,9 +49,9 @@ def fake_run(args: list[str], **kwargs: object) -> _Completed: format_path(tmp_path / "f.py") -def test_format_path_format_failure(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/ruff") - +def test_format_path_format_failure( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, stub_ruff: None +) -> None: def fake_run(args: list[str], **kwargs: object) -> _Completed: # check passes (1 == remaining lint findings, tolerated); format then fails return _Completed(1 if "check" in args else 3) diff --git a/tests/test_tooling.py b/tests/test_tooling.py new file mode 100644 index 0000000..e192467 --- /dev/null +++ b/tests/test_tooling.py @@ -0,0 +1,182 @@ +"""The generator must use the ruff/mypy installed with it, never a system copy.""" + +from __future__ import annotations + +import importlib.util +import os +import shutil +import subprocess +import sys +import sysconfig +from pathlib import Path + +import pytest + +from unihttp_openapi_generator import tooling +from unihttp_openapi_generator.tooling import ( + ToolNotFoundError, + mypy_command, + ruff_executable, + target_python_executable, +) + + +def _make_executable(directory: Path, name: str) -> Path: + directory.mkdir(parents=True, exist_ok=True) + path = directory / name + path.write_text("#!/bin/sh\n") + path.chmod(0o755) + return path + + +def test_prefers_own_environment_over_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """The regression: a system ruff on PATH must not win over the co-installed one.""" + own = _make_executable(tmp_path / "venv" / "bin", "ruff") + system = _make_executable(tmp_path / "usr" / "bin", "ruff") + + monkeypatch.setattr(sysconfig, "get_path", lambda *a, **kw: str(own.parent)) + monkeypatch.setattr(shutil, "which", lambda name: str(system)) + + assert ruff_executable() == str(own) + + +def test_falls_back_to_path_when_env_has_none( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + system = _make_executable(tmp_path / "usr" / "bin", "ruff") + monkeypatch.setattr(sysconfig, "get_path", lambda *a, **kw: str(tmp_path / "empty")) + monkeypatch.setattr(sys, "executable", str(tmp_path / "empty" / "python")) + monkeypatch.setattr(shutil, "which", lambda name: str(system)) + + assert ruff_executable() == str(system) + + +def test_ruff_missing_everywhere_names_the_fix( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setattr(sysconfig, "get_path", lambda *a, **kw: str(tmp_path / "empty")) + # sys.executable's own directory is a candidate too, and the real one has ruff. + monkeypatch.setattr(sys, "executable", str(tmp_path / "empty" / "python")) + monkeypatch.setattr(shutil, "which", lambda name: None) + + with pytest.raises(ToolNotFoundError, match="force-reinstall"): + ruff_executable() + + +def test_lookup_result_is_cached(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + own = _make_executable(tmp_path / "bin", "ruff") + calls = 0 + + def counting_get_path(*args: object, **kwargs: object) -> str: + nonlocal calls + calls += 1 + return str(own.parent) + + monkeypatch.setattr(sysconfig, "get_path", counting_get_path) + ruff_executable() + after_first = calls + ruff_executable() + assert calls == after_first + + +class TestInstallScriptsDir: + """The generator's own install location tells us where its ruff landed.""" + + @pytest.mark.parametrize( + ("module_file", "expected"), + [ + ("/env/lib/python3.12/site-packages/unihttp_openapi_generator/tooling.py", "/env/bin"), + ("/usr/lib/python3/dist-packages/unihttp_openapi_generator/tooling.py", "/usr/bin"), + ], + ) + def test_derives_the_prefix_bin( + self, monkeypatch: pytest.MonkeyPatch, module_file: str, expected: str + ) -> None: + monkeypatch.setattr(tooling, "__file__", module_file) + assert tooling._install_scripts_dir() == expected + + def test_windows_layout(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + tooling, "__file__", "/env/Lib/site-packages/unihttp_openapi_generator/tooling.py" + ) + monkeypatch.setattr(os, "name", "nt") + assert tooling._install_scripts_dir() == "/env/Scripts" + + def test_none_from_a_source_checkout(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(tooling, "__file__", "/repo/src/unihttp_openapi_generator/tooling.py") + assert tooling._install_scripts_dir() is None + + def test_beats_an_ambient_install_of_the_wrong_version( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """A too-old system ruff sits in the default scripts dir; ours must still win.""" + own = _make_executable(tmp_path / "userbase" / "bin", "ruff") + _make_executable(tmp_path / "usr" / "local" / "bin", "ruff") + monkeypatch.setattr( + tooling, + "__file__", + str(tmp_path / "userbase" / "lib" / "python3.12" / "site-packages") + + "/unihttp_openapi_generator/tooling.py", + ) + monkeypatch.setattr( + sysconfig, "get_path", lambda *a, **kw: str(tmp_path / "usr" / "local" / "bin") + ) + + assert ruff_executable() == str(own) + + +def test_script_dirs_survive_a_missing_sys_executable(monkeypatch: pytest.MonkeyPatch) -> None: + """A frozen/embedded interpreter reports no executable; lookup must not crash.""" + monkeypatch.setattr(sys, "executable", "") + assert "" not in tooling._script_dirs() + + +def test_script_dirs_are_deduplicated(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(sysconfig, "get_path", lambda *a, **kw: "/same/bin") + monkeypatch.setattr(sys, "executable", "/same/bin/python") + assert tooling._script_dirs() == ["/same/bin"] + + +def test_mypy_runs_through_this_interpreter() -> None: + assert mypy_command() == [sys.executable, "-m", "mypy"] + + +def test_mypy_command_actually_runs() -> None: + result = subprocess.run([*mypy_command(), "--version"], capture_output=True, text=True) + assert result.returncode == 0 + assert result.stdout.startswith("mypy ") + + +def test_mypy_missing_names_the_fix(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(importlib.util, "find_spec", lambda name: None) + with pytest.raises(ToolNotFoundError, match="force-reinstall"): + mypy_command() + + +class TestTargetPython: + """``--check`` resolves the generated package's imports against the user's venv.""" + + def test_none_without_an_activated_venv(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("VIRTUAL_ENV", raising=False) + assert target_python_executable() is None + + def test_none_when_the_venv_is_the_generator_s_own( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("VIRTUAL_ENV", sys.prefix) + assert target_python_executable() is None + + def test_points_at_a_different_activated_venv( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + bindir = "Scripts" if os.name == "nt" else "bin" + python = _make_executable(tmp_path / "proj" / bindir, "python") + monkeypatch.setenv("VIRTUAL_ENV", str(tmp_path / "proj")) + + assert target_python_executable() == str(python) + + def test_none_when_the_venv_path_is_stale( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setenv("VIRTUAL_ENV", str(tmp_path / "deleted")) + assert target_python_executable() is None diff --git a/uv.lock b/uv.lock index 5f4eacc..0b55cb7 100644 --- a/uv.lock +++ b/uv.lock @@ -1569,6 +1569,7 @@ source = { editable = "." } dependencies = [ { name = "httpx" }, { name = "jinja2" }, + { name = "mypy" }, { name = "openapi-spec-validator" }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -1578,11 +1579,6 @@ dependencies = [ { name = "typer" }, ] -[package.optional-dependencies] -check = [ - { name = "mypy" }, -] - [package.dev-dependencies] dev = [ { name = "adaptix" }, @@ -1600,7 +1596,7 @@ dev = [ requires-dist = [ { name = "httpx", specifier = ">=0.28.0" }, { name = "jinja2", specifier = ">=3.1.0" }, - { name = "mypy", marker = "extra == 'check'", specifier = ">=1.10.0" }, + { name = "mypy", specifier = ">=1.10.0" }, { name = "openapi-spec-validator", specifier = ">=0.7.1" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pydantic-settings", specifier = ">=2.0.0" },