diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5651ad3..d0dc95fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,6 @@ jobs: - "3.10" - "3.11" - "3.12" - - "3.13" if: | always() && !cancelled() && !contains(needs.*.result, 'failure') && @@ -144,7 +143,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: "Setup environment" run: | - pipx install poetry==1.8.5 + pipx install poetry==1.8.5 --python python${{ matrix.python-version }} poetry config virtualenvs.create true --local pip install invoke toml codecov - name: "Install Package" diff --git a/changelog/251.fixed.md b/changelog/251.fixed.md new file mode 100644 index 00000000..84e39163 --- /dev/null +++ b/changelog/251.fixed.md @@ -0,0 +1 @@ +Fix typing for Python 3.9 and remove support for Python 3.13 \ No newline at end of file diff --git a/infrahub_sdk/ctl/check.py b/infrahub_sdk/ctl/check.py index 3d5319dd..0626d884 100644 --- a/infrahub_sdk/ctl/check.py +++ b/infrahub_sdk/ctl/check.py @@ -5,7 +5,7 @@ from asyncio import run as aiorun from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import typer from rich.console import Console @@ -50,8 +50,8 @@ def run( format_json: bool, list_available: bool, variables: dict[str, str], - name: str | None = None, - branch: str | None = None, + name: Optional[str] = None, + branch: Optional[str] = None, ) -> None: """Locate and execute all checks under the defined path.""" diff --git a/infrahub_sdk/ctl/cli_commands.py b/infrahub_sdk/ctl/cli_commands.py index 19980eb2..7a10f136 100644 --- a/infrahub_sdk/ctl/cli_commands.py +++ b/infrahub_sdk/ctl/cli_commands.py @@ -7,7 +7,7 @@ import platform import sys from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, Optional import jinja2 import typer @@ -74,13 +74,13 @@ @catch_exception(console=console) def check( check_name: str = typer.Argument(default="", help="Name of the Python check"), - branch: str | None = None, + branch: Optional[str] = None, path: str = typer.Option(".", help="Root directory"), debug: bool = False, format_json: bool = False, _: str = CONFIG_PARAM, list_available: bool = typer.Option(False, "--list", help="Show available Python checks"), - variables: list[str] | None = typer.Argument( + variables: Optional[list[str]] = typer.Argument( None, help="Variables to pass along with the query. Format key=value key=value." ), ) -> None: @@ -102,12 +102,12 @@ def check( @catch_exception(console=console) async def generator( generator_name: str = typer.Argument(default="", help="Name of the Generator"), - branch: str | None = None, + branch: Optional[str] = None, path: str = typer.Option(".", help="Root directory"), debug: bool = False, _: str = CONFIG_PARAM, list_available: bool = typer.Option(False, "--list", help="Show available Generators"), - variables: list[str] | None = typer.Argument( + variables: Optional[list[str]] = typer.Argument( None, help="Variables to pass along with the query. Format key=value key=value." ), ) -> None: @@ -130,13 +130,13 @@ async def run( debug: bool = False, _: str = CONFIG_PARAM, branch: str = typer.Option(None, help="Branch on which to run the script."), - concurrent: int | None = typer.Option( + concurrent: Optional[int] = typer.Option( None, help="Maximum number of requests to execute at the same time.", envvar="INFRAHUB_MAX_CONCURRENT_EXECUTION", ), timeout: int = typer.Option(60, help="Timeout in sec", envvar="INFRAHUB_TIMEOUT"), - variables: list[str] | None = typer.Argument( + variables: Optional[list[str]] = typer.Argument( None, help="Variables to pass along with the query. Format key=value key=value." ), ) -> None: @@ -259,7 +259,7 @@ def _run_transform( @catch_exception(console=console) def render( transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False), - variables: list[str] | None = typer.Argument( + variables: Optional[list[str]] = typer.Argument( None, help="Variables to pass along with the query. Format key=value key=value." ), branch: str = typer.Option(None, help="Branch on which to render the transform."), @@ -309,7 +309,7 @@ def render( @catch_exception(console=console) def transform( transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False), - variables: list[str] | None = typer.Argument( + variables: Optional[list[str]] = typer.Argument( None, help="Variables to pass along with the query. Format key=value key=value." ), branch: str = typer.Option(None, help="Branch on which to run the transformation"), diff --git a/infrahub_sdk/ctl/generator.py b/infrahub_sdk/ctl/generator.py index 5258caa6..22501568 100644 --- a/infrahub_sdk/ctl/generator.py +++ b/infrahub_sdk/ctl/generator.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import typer from rich.console import Console @@ -23,7 +23,7 @@ async def run( debug: bool, list_available: bool, branch: str | None = None, - variables: list[str] | None = None, + variables: Optional[list[str]] = None, ) -> None: init_logging(debug=debug) repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE)) diff --git a/infrahub_sdk/ctl/importer.py b/infrahub_sdk/ctl/importer.py index 4645fa06..e9181c8b 100644 --- a/infrahub_sdk/ctl/importer.py +++ b/infrahub_sdk/ctl/importer.py @@ -2,6 +2,7 @@ from asyncio import run as aiorun from pathlib import Path +from typing import Optional import typer from rich.console import Console @@ -26,7 +27,7 @@ def load( quiet: bool = typer.Option(False, help="No console output"), _: str = CONFIG_PARAM, branch: str = typer.Option(None, help="Branch from which to export"), - concurrent: int | None = typer.Option( + concurrent: Optional[int] = typer.Option( None, help="Maximum number of requests to execute at the same time.", envvar="INFRAHUB_MAX_CONCURRENT_EXECUTION", diff --git a/infrahub_sdk/ctl/repository.py b/infrahub_sdk/ctl/repository.py index 4e6d3535..1992a537 100644 --- a/infrahub_sdk/ctl/repository.py +++ b/infrahub_sdk/ctl/repository.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from typing import Optional import typer import yaml @@ -68,7 +69,7 @@ async def add( name: str, location: str, description: str = "", - username: str | None = None, + username: Optional[str] = None, password: str = "", ref: str = "", read_only: bool = False, @@ -114,7 +115,7 @@ async def add( @app.command() async def list( - branch: str = typer.Option(None, help="Branch on which to list repositories."), + branch: Optional[str] = typer.Option(None, help="Branch on which to list repositories."), debug: bool = False, _: str = CONFIG_PARAM, ) -> None: diff --git a/infrahub_sdk/ctl/utils.py b/infrahub_sdk/ctl/utils.py index e4474d8d..4c627119 100644 --- a/infrahub_sdk/ctl/utils.py +++ b/infrahub_sdk/ctl/utils.py @@ -6,7 +6,7 @@ from collections.abc import Coroutine from functools import wraps from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, NoReturn, TypeVar +from typing import TYPE_CHECKING, Any, Callable, NoReturn, Optional, TypeVar import pendulum import typer @@ -145,7 +145,7 @@ def print_graphql_errors(console: Console, errors: list) -> None: console.print(f"[red]{escape(str(error))}") -def parse_cli_vars(variables: list[str] | None) -> dict[str, str]: +def parse_cli_vars(variables: Optional[list[str]]) -> dict[str, str]: if not variables: return {} diff --git a/infrahub_sdk/ctl/validate.py b/infrahub_sdk/ctl/validate.py index d93260b2..99318239 100644 --- a/infrahub_sdk/ctl/validate.py +++ b/infrahub_sdk/ctl/validate.py @@ -2,6 +2,7 @@ import sys from pathlib import Path +from typing import Optional import typer import ujson @@ -57,7 +58,7 @@ async def validate_schema(schema: Path, _: str = CONFIG_PARAM) -> None: @catch_exception(console=console) def validate_graphql( query: str, - variables: list[str] | None = typer.Argument( + variables: Optional[list[str]] = typer.Argument( None, help="Variables to pass along with the query. Format key=value key=value." ), debug: bool = typer.Option(False, help="Display more troubleshooting information."), diff --git a/poetry.lock b/poetry.lock index fc23269b..3b60de6a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1301,10 +1301,7 @@ files = [ [package.dependencies] annotated-types = ">=0.6.0" pydantic-core = "2.23.3" -typing-extensions = [ - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, -] +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} [package.extras] email = ["email-validator (>=2.0.0)"] @@ -2237,5 +2234,5 @@ tests = ["Jinja2", "pytest", "pyyaml", "rich"] [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "8df86b45a0478a859bbe784b5d9027b2f033da597a52815f6006fd81fd424fdf" +python-versions = "^3.9, < 3.13" +content-hash = "7cf3b9fd5e6ad627c30cb1660ef9c45d5b6a264150d064bc47cc7ae7a2be4030" diff --git a/pyproject.toml b/pyproject.toml index 7cc22f4b..9fbab36f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,11 +16,10 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", ] [tool.poetry.dependencies] -python = "^3.9" +python = "^3.9, < 3.13" pydantic = ">=2.0.0,!=2.0.1,!=2.1.0,<3.0.0" pydantic-settings = ">=2.0" graphql-core = ">=3.1,<3.3" @@ -248,6 +247,10 @@ max-complexity = 17 "ANN401", # Dynamically typed expressions (typing.Any) are disallowed ] +"infrahub_sdk/ctl/**/*.py" = [ + "UP007", # Use `X | Y` for type annotations | Required for Typer until we can drop the support for Python 3.9 +] + "infrahub_sdk/client.py" = [ ################################################################################################## # Review and change the below later # diff --git a/tests/unit/ctl/test_cli.py b/tests/unit/ctl/test_cli.py index d73a7105..30d63e92 100644 --- a/tests/unit/ctl/test_cli.py +++ b/tests/unit/ctl/test_cli.py @@ -1,16 +1,10 @@ -import sys - -import pytest from typer.testing import CliRunner from infrahub_sdk.ctl.cli import app runner = CliRunner() -requires_python_310 = pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10 or higher") - -@requires_python_310 def test_main_app(): result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -29,14 +23,12 @@ def test_validate_all_groups_have_names(): assert group.name -@requires_python_310 def test_version_command(): result = runner.invoke(app, ["version"]) assert result.exit_code == 0 assert "Python SDK: v" in result.stdout -@requires_python_310 def test_info_command_success(mock_query_infrahub_version, mock_query_infrahub_user): result = runner.invoke(app, ["info"]) assert result.exit_code == 0 @@ -44,14 +36,12 @@ def test_info_command_success(mock_query_infrahub_version, mock_query_infrahub_u assert expected in result.stdout, f"'{expected}' not found in info command output" -@requires_python_310 def test_info_command_failure(): result = runner.invoke(app, ["info"]) assert result.exit_code == 0 assert "Connection Error" in result.stdout -@requires_python_310 def test_info_detail_command_success(mock_query_infrahub_version, mock_query_infrahub_user): result = runner.invoke(app, ["info", "--detail"]) assert result.exit_code == 0 @@ -65,7 +55,6 @@ def test_info_detail_command_success(mock_query_infrahub_version, mock_query_inf assert expected in result.stdout, f"'{expected}' not found in detailed info command output" -@requires_python_310 def test_info_detail_command_failure(): result = runner.invoke(app, ["info", "--detail"]) assert result.exit_code == 0 diff --git a/tests/unit/ctl/test_repository_app.py b/tests/unit/ctl/test_repository_app.py index 7fb01ee1..ba0a7721 100644 --- a/tests/unit/ctl/test_repository_app.py +++ b/tests/unit/ctl/test_repository_app.py @@ -1,6 +1,5 @@ """Integration tests for infrahubctl commands.""" -import sys from unittest import mock import pytest @@ -13,8 +12,6 @@ runner = CliRunner() -requires_python_310 = pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10 or higher") - @pytest.fixture def mock_client() -> mock.Mock: @@ -29,7 +26,6 @@ def mock_client() -> mock.Mock: class TestInfrahubctlRepository: """Groups the 'infrahubctl repository' test cases.""" - @requires_python_310 @mock.patch("infrahub_sdk.ctl.repository.initialize_client") def test_repo_no_username_or_password(self, mock_init_client, mock_client) -> None: """Case allow no username to be passed in and set it as None rather than blank string that fails.""" @@ -77,7 +73,6 @@ def test_repo_no_username_or_password(self, mock_init_client, mock_client) -> No tracker="mutation-repository-create", ) - @requires_python_310 @mock.patch("infrahub_sdk.ctl.repository.initialize_client") def test_repo_no_username(self, mock_init_client, mock_client) -> None: """Case allow no username to be passed in and set it as None rather than blank string that fails.""" @@ -137,7 +132,6 @@ def test_repo_no_username(self, mock_init_client, mock_client) -> None: tracker="mutation-repository-create", ) - @requires_python_310 @mock.patch("infrahub_sdk.ctl.repository.initialize_client") def test_repo_username(self, mock_init_client, mock_client) -> None: """Case allow no username to be passed in and set it as None rather than blank string that fails.""" @@ -199,7 +193,6 @@ def test_repo_username(self, mock_init_client, mock_client) -> None: tracker="mutation-repository-create", ) - @requires_python_310 @mock.patch("infrahub_sdk.ctl.repository.initialize_client") def test_repo_readonly_true(self, mock_init_client, mock_client) -> None: """Case allow no username to be passed in and set it as None rather than blank string that fails.""" @@ -260,7 +253,6 @@ def test_repo_readonly_true(self, mock_init_client, mock_client) -> None: tracker="mutation-repository-create", ) - @requires_python_310 @mock.patch("infrahub_sdk.ctl.repository.initialize_client") def test_repo_description_commit_branch(self, mock_init_client, mock_client) -> None: """Case allow no username to be passed in and set it as None rather than blank string that fails.""" diff --git a/tests/unit/ctl/test_validate_app.py b/tests/unit/ctl/test_validate_app.py index 3264b087..43d41ddb 100644 --- a/tests/unit/ctl/test_validate_app.py +++ b/tests/unit/ctl/test_validate_app.py @@ -1,5 +1,3 @@ -import sys - import pytest from typer.testing import CliRunner @@ -9,10 +7,7 @@ runner = CliRunner() -requires_python_310 = pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10 or higher") - -@requires_python_310 def test_validate_schema_valid(): fixture_file = get_fixtures_dir() / "models" / "valid_model_01.json" @@ -21,7 +16,6 @@ def test_validate_schema_valid(): assert "Schema is valid" in result.stdout -@requires_python_310 def test_validate_schema_empty(): fixture_file = get_fixtures_dir() / "models" / "empty.json" @@ -30,7 +24,6 @@ def test_validate_schema_empty(): assert "Empty YAML/JSON file" in remove_ansi_color(result.stdout) -@requires_python_310 def test_validate_schema_non_valid(): fixture_file = get_fixtures_dir() / "models" / "non_valid_model_01.json"