diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 77a874a4..ccd08101 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -12,6 +12,7 @@ on: pull_request: paths: - ".github/workflows/third_party.yml" + - "scripts/remove_uv_version_requirement.py" workflow_dispatch: permissions: @@ -65,6 +66,8 @@ jobs: with: path: typing-extensions-latest persist-credentials: false + - name: Remove uv version requirement + run: uv run --no-project --python '>=3.11' typing-extensions-latest/scripts/remove_uv_version_requirement.py pydantic - name: Run tests with typing_extensions main branch working-directory: pydantic run: | @@ -101,6 +104,8 @@ jobs: with: path: typing-extensions-latest persist-credentials: false + - name: Remove uv version requirement + run: uv run --no-project --python '>=3.11' typing-extensions-latest/scripts/remove_uv_version_requirement.py typing_inspect - name: Run tests with typing_extensions main branch working-directory: typing_inspect run: | @@ -139,6 +144,8 @@ jobs: with: path: typing-extensions-latest persist-credentials: false + - name: Remove uv version requirement + run: uv run --no-project --python '>=3.11' typing-extensions-latest/scripts/remove_uv_version_requirement.py pycroscope - name: Run tests with typing_extensions main branch working-directory: pycroscope run: | @@ -177,6 +184,8 @@ jobs: with: path: typing-extensions-latest persist-credentials: false + - name: Remove uv version requirement + run: uv run --no-project --python '>=3.11' typing-extensions-latest/scripts/remove_uv_version_requirement.py typeguard - name: Run tests with typing_extensions main branch env: PYTHON_COLORS: 0 # A test fails if tracebacks are colorized @@ -218,6 +227,8 @@ jobs: with: path: typing-extensions-latest persist-credentials: false + - name: Remove uv version requirement + run: uv run --no-project --python '>=3.11' typing-extensions-latest/scripts/remove_uv_version_requirement.py typed-argument-parser - name: Configure git for typed-argument-parser tests # typed-argument parser does this in their CI, # and the tests fail unless we do this @@ -264,6 +275,8 @@ jobs: with: path: typing-extensions-latest persist-credentials: false + - name: Remove uv version requirement + run: uv run --no-project --python '>=3.11' typing-extensions-latest/scripts/remove_uv_version_requirement.py mypy - name: Run tests with typing_extensions main branch working-directory: mypy run: | @@ -303,6 +316,8 @@ jobs: with: path: typing-extensions-latest persist-credentials: false + - name: Remove uv version requirement + run: uv run --no-project --python '>=3.11' typing-extensions-latest/scripts/remove_uv_version_requirement.py cattrs - name: Run tests with typing_extensions main branch working-directory: cattrs run: | @@ -344,6 +359,8 @@ jobs: with: path: typing-extensions-latest persist-credentials: false + - name: Remove uv version requirement + run: uv run --no-project --python '>=3.11' typing-extensions-latest/scripts/remove_uv_version_requirement.py sqlalchemy - name: Run sqlalchemy tests with typing_extensions main branch working-directory: sqlalchemy run: | @@ -379,6 +396,8 @@ jobs: with: path: typing-extensions-latest persist-credentials: false + - name: Remove uv version requirement + run: uv run --no-project --python '>=3.11' typing-extensions-latest/scripts/remove_uv_version_requirement.py litestar - name: Run litestar tests with typing_extensions main branch working-directory: litestar run: | diff --git a/scripts/remove_uv_version_requirement.py b/scripts/remove_uv_version_requirement.py new file mode 100644 index 00000000..3df84369 --- /dev/null +++ b/scripts/remove_uv_version_requirement.py @@ -0,0 +1,68 @@ +"""Remove uv's required-version from a third-party checkout. + +CI pins uv, which may not satisfy a checkout's uv version requirement. +Preserve the checkout's other uv settings in pyproject.toml and uv.toml. +Fail if a requirement cannot be removed safely as a single-line assignment. + +Using tomlkit would make this simpler and support more TOML layouts, but we +would need to declare the dependency in a PEP 723 script. We would then +want a uv-pre-commit hook to check in CI that the script's lockfile is up to +date, and Renovate configuration to update the hook alongside CI's uv pin +so the hook uses the same uv version as CI. We therefore keep this script +stdlib-only for now, to avoid that spiralling complexity. This can be +reconsidered if we ever need a more general solution in the future. +""" + +import argparse +import tomllib +from pathlib import Path + +parser = argparse.ArgumentParser() +parser.add_argument("repo_dir", type=Path) +repo_dir = parser.parse_args().repo_dir +if not repo_dir.is_dir(): + parser.error(f"{repo_dir} is not a directory") + +# Collect each updated file's path and contents so neither file is changed if +# a requirement in either file cannot be removed safely. +config_updates: dict[Path, str] = {} + +for config_filename in ("pyproject.toml", "uv.toml"): + config_path = repo_dir / config_filename + try: + raw_config = config_path.read_bytes().decode("utf-8") + except FileNotFoundError: + continue + + parsed_config = tomllib.loads(raw_config) + uv_config = ( + parsed_config.get("tool", {}).get("uv", {}) + if config_filename == "pyproject.toml" + else parsed_config + ) + if "required-version" in uv_config: + del uv_config["required-version"] + raw_config_lines = raw_config.splitlines(keepends=True) + for index, config_line in enumerate(raw_config_lines): + if "=" not in config_line: + continue + try: + key = tomllib.loads(f"{config_line.partition('=')[0]} = true") + if key != {"required-version": True}: + continue + updated_config = "".join( + raw_config_lines[:index] + raw_config_lines[index + 1 :] + ) + if tomllib.loads(updated_config) == parsed_config: + config_updates[config_path] = updated_config + break + except tomllib.TOMLDecodeError: + continue + else: + raise SystemExit( + f"cannot safely remove required-version from {config_path}; " + "expected a single-line required-version assignment" + ) + +for config_path, updated_raw_config in config_updates.items(): + config_path.write_bytes(updated_raw_config.encode("utf-8"))