From 1924a117aa74b5494e07280e4771c90e1784ab15 Mon Sep 17 00:00:00 2001 From: gchqdev227 <62302861+gchqdev227@users.noreply.github.com> Date: Wed, 1 Oct 2025 12:06:10 +0100 Subject: [PATCH 1/7] Dropped support for Python 3.9 in documentation and metadata files --- .github/workflows/unit-tests.yaml | 2 +- README.md | 2 +- pyproject.toml | 3 +-- sonar-project.properties | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 26b0ed8..ab77702 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14.0-beta.2"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14.0-beta.2"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index e355cda..84c89aa 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![version](https://img.shields.io/badge/version-0.8.0-informational) ![pre-release](https://img.shields.io/badge/pre--release-beta-red) -![python](https://img.shields.io/badge/python-%3E%3D3.9-informational) +![python](https://img.shields.io/badge/python-%3E%3D10-informational) ![coverage](https://img.shields.io/badge/coverage-96%25-brightgreen) ![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=orange) diff --git a/pyproject.toml b/pyproject.toml index a553f3c..ef67792 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "concoursetools" dynamic = ["version"] description = "Easily create Concourse resource types in Python." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" keywords = [ "concourse", "ci", @@ -18,7 +18,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/sonar-project.properties b/sonar-project.properties index df018d6..fae2695 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,7 @@ sonar.projectDescription="Static analysis for the Concourse Tools Python library sonar.sources=concoursetools sonar.tests=tests -sonar.python.version=3.9,3.10,3.11,3.12,3.13,3.14 +sonar.python.version=3.10,3.11,3.12,3.13,3.14 sonar.coverage.exclusions=docs/**/*,tests/**/*,coverage.xml,concoursetools/colour.py,concoursetools/typing.py,**/__init__.py,**/__main__.py sonar.python.coverage.reportPaths=coverage.xml From f5bced738109ac8121f5c317f8a38b61dd30a731 Mon Sep 17 00:00:00 2001 From: gchqdev227 <62302861+gchqdev227@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:15:02 +0100 Subject: [PATCH 2/7] Ran pyupgrade over codebase --- concoursetools/additional.py | 8 ++++---- concoursetools/cli/docstring.py | 6 +++--- concoursetools/cli/parser.py | 2 +- concoursetools/colour.py | 2 +- concoursetools/importing.py | 2 +- concoursetools/metadata.py | 2 +- concoursetools/mocking.py | 6 +++--- concoursetools/resource.py | 8 ++++---- concoursetools/testing.py | 9 ++++----- examples/build_status.py | 2 +- examples/pipeline.py | 2 +- examples/xkcd.py | 4 ++-- tests/test_additional.py | 2 +- tests/test_examples.py | 6 +++--- 14 files changed, 30 insertions(+), 31 deletions(-) diff --git a/concoursetools/additional.py b/concoursetools/additional.py index a7b69d2..0a14282 100644 --- a/concoursetools/additional.py +++ b/concoursetools/additional.py @@ -44,7 +44,7 @@ class DatetimeVersion(TypedVersion): execution_date: datetime @classmethod - def now(cls) -> "DatetimeVersion": + def now(cls) -> DatetimeVersion: """Return the version corresponding to now.""" return cls(datetime.now()) @@ -236,7 +236,7 @@ def to_flat_dict(self) -> VersionConfig: } @classmethod - def from_flat_dict(cls: "type[MultiVersion[SortableVersionT]]", version_dict: VersionConfig) -> "MultiVersion[SortableVersionT]": + def from_flat_dict(cls: type[MultiVersion[SortableVersionT]], version_dict: VersionConfig) -> MultiVersion[SortableVersionT]: """ Load an instance from a dictionary representing the version. @@ -382,7 +382,7 @@ def fetch_all_versions(self) -> set[SortableVersionT]: class _PseudoConcourseResource(ConcourseResource[VersionT]): - def __new__(cls) -> "_PseudoConcourseResource[VersionT]": + def __new__(cls) -> _PseudoConcourseResource[VersionT]: raise TypeError(f"Cannot instantiate a {cls.__name__} type") @@ -425,7 +425,7 @@ class MultiResourceConcourseResource(_PseudoConcourseResource[VersionT]): A special Resource class which delegates to multiple other resource classes. """ @classmethod - def _from_resource_config(cls, resource_config: ResourceConfig) -> "ConcourseResource[VersionT]": + def _from_resource_config(cls, resource_config: ResourceConfig) -> ConcourseResource[VersionT]: try: resource_key = resource_config.pop(param_key) except KeyError as error: diff --git a/concoursetools/cli/docstring.py b/concoursetools/cli/docstring.py index 6ac5056..15cd9d4 100644 --- a/concoursetools/cli/docstring.py +++ b/concoursetools/cli/docstring.py @@ -32,7 +32,7 @@ class Docstring: parameters: dict[str, str] @classmethod - def from_object(cls, obj: object) -> "Docstring": + def from_object(cls, obj: object) -> Docstring: """ Parse an object with a docstring. @@ -42,7 +42,7 @@ def from_object(cls, obj: object) -> "Docstring": return cls.from_string(raw_docstring) @classmethod - def from_string(cls, raw_docstring: str) -> "Docstring": + def from_string(cls, raw_docstring: str) -> Docstring: """ Parse a docstring. @@ -63,7 +63,7 @@ def from_string(cls, raw_docstring: str) -> "Docstring": return cls(first_line, description.strip(), parameters) -def _pair_up(data: list[str]) -> Generator[tuple[str, str], None, None]: +def _pair_up(data: list[str]) -> Generator[tuple[str, str]]: """ Pair up a list of items. diff --git a/concoursetools/cli/parser.py b/concoursetools/cli/parser.py index 9033b37..ad66063 100644 --- a/concoursetools/cli/parser.py +++ b/concoursetools/cli/parser.py @@ -377,7 +377,7 @@ def add_to_parser(self, parser: ArgumentParser) -> None: ... @classmethod - def yield_from_function(cls, func: CLIFunction, allow_short: set[str]) -> Generator[Parameter[Any], None, None]: + def yield_from_function(cls, func: CLIFunction, allow_short: set[str]) -> Generator[Parameter[Any]]: """ Yield parameters from a function. diff --git a/concoursetools/colour.py b/concoursetools/colour.py index cbc0468..e772be7 100644 --- a/concoursetools/colour.py +++ b/concoursetools/colour.py @@ -77,7 +77,7 @@ def colour_print(*values: object, colour: str = MISSING_COLOUR, bold: bool = Fal @contextmanager -def print_in_colour(colour: str, bold: bool = False, underline: bool = False) -> Generator[None, None, None]: +def print_in_colour(colour: str, bold: bool = False, underline: bool = False) -> Generator[None]: """ Print anything in colour within a :ref:`context manager `. diff --git a/concoursetools/importing.py b/concoursetools/importing.py index 2987e0a..05d5b9e 100644 --- a/concoursetools/importing.py +++ b/concoursetools/importing.py @@ -127,7 +127,7 @@ def import_py_file(import_path: str, file_path: Path) -> ModuleType: @contextmanager -def edit_sys_path(prepend: Sequence[Path] = (), append: Sequence[Path] = ()) -> Generator[None, None, None]: +def edit_sys_path(prepend: Sequence[Path] = (), append: Sequence[Path] = ()) -> Generator[None]: """ Temporarily add to :data:`sys.path` within a context manager. diff --git a/concoursetools/metadata.py b/concoursetools/metadata.py index d05ce26..32dbd78 100644 --- a/concoursetools/metadata.py +++ b/concoursetools/metadata.py @@ -217,7 +217,7 @@ def format_string(self, string: str, additional_values: dict[str, str] | None = return template.safe_substitute(possible_values) if ignore_missing else template.substitute(possible_values) @classmethod - def from_env(cls) -> "BuildMetadata": + def from_env(cls) -> BuildMetadata: """Return an instance populated from the environment.""" return cls( BUILD_ID=os.environ["BUILD_ID"], diff --git a/concoursetools/mocking.py b/concoursetools/mocking.py index 446a500..98bd362 100644 --- a/concoursetools/mocking.py +++ b/concoursetools/mocking.py @@ -156,7 +156,7 @@ def final_state(self) -> FolderDict: raise RuntimeError("Final state is not set whilst temporary directory is still open.") return self._final_state - def __enter__(self) -> "TemporaryDirectoryState": + def __enter__(self) -> TemporaryDirectoryState: self._temp_dir = TemporaryDirectory(**self.temporary_directory_kwargs) self._set_folder_from_dict(self.path, self.starting_state, encoding=self.encoding) return self @@ -278,7 +278,7 @@ def clear(self) -> None: self.inner_io = StringIO() @contextmanager - def capture_stdout_and_stderr(self) -> ContextManager["StringIOWrapper"]: + def capture_stdout_and_stderr(self) -> ContextManager[StringIOWrapper]: """ Capture both :data:`~sys.stdout` and :data:`~sys.stderr`. @@ -289,7 +289,7 @@ def capture_stdout_and_stderr(self) -> ContextManager["StringIOWrapper"]: yield self @contextmanager - def capture_stderr(self) -> ContextManager["StringIOWrapper"]: + def capture_stderr(self) -> ContextManager[StringIOWrapper]: """ Capture :data:`~sys.stderr`. diff --git a/concoursetools/resource.py b/concoursetools/resource.py index c721668..cca39b6 100644 --- a/concoursetools/resource.py +++ b/concoursetools/resource.py @@ -275,7 +275,7 @@ def out_main(cls) -> None: _output(output) @classmethod - def _parse_check_input(cls) -> tuple["ConcourseResource[VersionT]", VersionT | None]: + def _parse_check_input(cls) -> tuple[ConcourseResource[VersionT], VersionT | None]: """Parse input from the command line.""" check_payload = sys.stdin.read() @@ -291,7 +291,7 @@ def _parse_check_input(cls) -> tuple["ConcourseResource[VersionT]", VersionT | N return resource, previous_version @classmethod - def _parse_in_input(cls) -> tuple["ConcourseResource[VersionT]", VersionT, Path, Params]: + def _parse_in_input(cls) -> tuple[ConcourseResource[VersionT], VersionT, Path, Params]: """Parse input from the command line.""" in_payload = sys.stdin.read() @@ -309,7 +309,7 @@ def _parse_in_input(cls) -> tuple["ConcourseResource[VersionT]", VersionT, Path, return resource, version, destination_dir, params @classmethod - def _parse_out_input(cls) -> tuple["ConcourseResource[VersionT]", Path, Params]: + def _parse_out_input(cls) -> tuple[ConcourseResource[VersionT], Path, Params]: """Parse input from the command line.""" out_payload = sys.stdin.read() @@ -326,7 +326,7 @@ def _parse_out_input(cls) -> tuple["ConcourseResource[VersionT]", Path, Params]: return resource, sources_dir, params @classmethod - def _from_resource_config(cls, resource_config: ResourceConfig) -> "ConcourseResource[VersionT]": + def _from_resource_config(cls, resource_config: ResourceConfig) -> ConcourseResource[VersionT]: return cls(**resource_config) diff --git a/concoursetools/testing.py b/concoursetools/testing.py index 2e4d3e4..56f7dcc 100644 --- a/concoursetools/testing.py +++ b/concoursetools/testing.py @@ -105,7 +105,7 @@ def capture_debugging(self) -> ContextManager[StringIOWrapper]: yield self._debugging_output @contextmanager - def capture_directory_state(self, starting_state: FolderDict | None = None) -> ContextManager["TemporaryDirectoryState"]: + def capture_directory_state(self, starting_state: FolderDict | None = None) -> ContextManager[TemporaryDirectoryState]: """ Open a context manager to expose the internal state of the resource. @@ -514,9 +514,9 @@ def publish_new_version(self, params: Params | None = None) -> tuple[VersionConf return new_version_config, metadata_pairs @classmethod - def from_assets_dir(cls: type["FileTestResourceWrapper"], inner_resource_config: ResourceConfig, assets_dir: Path, + def from_assets_dir(cls: type[FileTestResourceWrapper], inner_resource_config: ResourceConfig, assets_dir: Path, directory_dict: FolderDict | None = None, one_off_build: bool = False, - instance_vars: dict[str, str] | None = None, **env_vars: str) -> "FileTestResourceWrapper": + instance_vars: dict[str, str] | None = None, **env_vars: str) -> FileTestResourceWrapper: """ Create an instance from a single folder of asset files. @@ -958,8 +958,7 @@ def run_command(command: str, additional_args: list[str] | None = None, env: dic cwd=cwd, check=True, input=stdin.encode() if stdin is not None else None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, ) except subprocess.CalledProcessError as error: error_message: bytes = error.stderr diff --git a/examples/build_status.py b/examples/build_status.py index adad7af..096e91f 100644 --- a/examples/build_status.py +++ b/examples/build_status.py @@ -41,7 +41,7 @@ def __call__(self, request: requests.Request) -> requests.Request: return request @classmethod - def from_client_credentials(cls, client_id: str, client_secret: str) -> "BitbucketOAuth": + def from_client_credentials(cls, client_id: str, client_secret: str) -> BitbucketOAuth: token_auth = HTTPBasicAuth(client_id, client_secret) url = "https://bitbucket.org/site/oauth2/access_token" diff --git a/examples/pipeline.py b/examples/pipeline.py index 1b960b0..c432877 100644 --- a/examples/pipeline.py +++ b/examples/pipeline.py @@ -113,7 +113,7 @@ def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata, new_version = ExecutionVersion(execution_arn) return new_version, metadata - def _yield_potential_execution_versions(self) -> Generator[ExecutionVersion, None, None]: + def _yield_potential_execution_versions(self) -> Generator[ExecutionVersion]: kwargs = { "PipelineName": self.pipeline_name, "SortOrder": "Descending", diff --git a/examples/xkcd.py b/examples/xkcd.py index d0d5331..5097314 100644 --- a/examples/xkcd.py +++ b/examples/xkcd.py @@ -80,14 +80,14 @@ def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) raise NotImplementedError -def yield_comic_ids(xml_data: str) -> Generator[int, None, None]: +def yield_comic_ids(xml_data: str) -> Generator[int]: for comic_url in yield_comic_links(xml_data): parsed_url = urllib.parse.urlparse(comic_url) comic_id = parsed_url.path.strip("/") yield int(comic_id) -def yield_comic_links(xml_data: str) -> Generator[str, None, None]: +def yield_comic_links(xml_data: str) -> Generator[str]: root = ET.fromstring(xml_data) for entry in root: if entry.tag.endswith("entry"): diff --git a/tests/test_additional.py b/tests/test_additional.py index 0977600..ca88e05 100644 --- a/tests/test_additional.py +++ b/tests/test_additional.py @@ -52,7 +52,7 @@ def to_flat_dict(self) -> VersionConfig: return {"files": json.dumps(list(self.files))} @classmethod - def from_flat_dict(cls, version_dict: VersionConfig) -> "FileVersion": + def from_flat_dict(cls, version_dict: VersionConfig) -> FileVersion: return cls(set(json.loads(version_dict["files"]))) diff --git a/tests/test_examples.py b/tests/test_examples.py index 987b871..0d61f1d 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -58,11 +58,11 @@ def __init__(self, data: str, status_code: int = 200) -> None: def text(self) -> str: return self._data - def get(self, *args: object, **kwargs: object) -> "MockedTextResponse": + def get(self, *args: object, **kwargs: object) -> MockedTextResponse: return self @classmethod - def from_file(cls, file_path: str, status_code: int = 200) -> "MockedTextResponse": + def from_file(cls, file_path: str, status_code: int = 200) -> MockedTextResponse: with open(file_path) as rf: data = rf.read() return cls(data, status_code) @@ -76,7 +76,7 @@ def __init__(self, json_data: Any, status_code: int = 200) -> None: self._json_data = json_data self._status_code = status_code - def get(self, *args: object, **kwargs: object) -> "MockedJSONResponse": + def get(self, *args: object, **kwargs: object) -> MockedJSONResponse: return self def json(self) -> Any: From 7fe066ada3e2fb29f1b00e3648f94c128f78a5ad Mon Sep 17 00:00:00 2001 From: gchqdev227 <62302861+gchqdev227@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:17:31 +0100 Subject: [PATCH 3/7] Replaced ContextManager generic with Generator --- concoursetools/mocking.py | 14 ++++++-------- concoursetools/testing.py | 12 +++++------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/concoursetools/mocking.py b/concoursetools/mocking.py index 98bd362..aa60826 100644 --- a/concoursetools/mocking.py +++ b/concoursetools/mocking.py @@ -13,13 +13,11 @@ import sys from tempfile import TemporaryDirectory from types import TracebackType -from typing import Any, TypeVar +from typing import Any from unittest import mock from concoursetools import BuildMetadata -T = TypeVar("T") -ContextManager = Generator[T, None, None] FolderDict = dict[str, Any] @@ -278,7 +276,7 @@ def clear(self) -> None: self.inner_io = StringIO() @contextmanager - def capture_stdout_and_stderr(self) -> ContextManager[StringIOWrapper]: + def capture_stdout_and_stderr(self) -> Generator[StringIOWrapper]: """ Capture both :data:`~sys.stdout` and :data:`~sys.stderr`. @@ -289,7 +287,7 @@ def capture_stdout_and_stderr(self) -> ContextManager[StringIOWrapper]: yield self @contextmanager - def capture_stderr(self) -> ContextManager[StringIOWrapper]: + def capture_stderr(self) -> Generator[StringIOWrapper]: """ Capture :data:`~sys.stderr`. @@ -300,7 +298,7 @@ def capture_stderr(self) -> ContextManager[StringIOWrapper]: @contextmanager -def mock_environ(new_environ: dict[str, str]) -> ContextManager[None]: +def mock_environ(new_environ: dict[str, str]) -> Generator[None]: """ Mock :data:`os.environ` in a context manager. @@ -317,7 +315,7 @@ def mock_environ(new_environ: dict[str, str]) -> ContextManager[None]: @contextmanager -def mock_stdin(stdin: str) -> ContextManager[None]: +def mock_stdin(stdin: str) -> Generator[None]: """ Mock :data:`~sys.stdin` in a context manager. @@ -338,7 +336,7 @@ def mock_stdin(stdin: str) -> ContextManager[None]: @contextmanager -def mock_argv(*args: str) -> ContextManager[None]: +def mock_argv(*args: str) -> Generator[None]: """ Mock :data:`sys.argv` in a context manager. diff --git a/concoursetools/testing.py b/concoursetools/testing.py index 56f7dcc..fd7e1ea 100644 --- a/concoursetools/testing.py +++ b/concoursetools/testing.py @@ -19,7 +19,7 @@ import secrets import subprocess from tempfile import TemporaryDirectory -from typing import Any, Generic, TypeVar +from typing import Any, Generic from concoursetools import BuildMetadata, ConcourseResource, Version from concoursetools.dockertools import MethodName, ScriptName, create_script_file @@ -27,8 +27,6 @@ from concoursetools.parsing import format_check_input, format_in_input, format_out_input, parse_metadata from concoursetools.typing import Metadata, MetadataPair, Params, ResourceConfig, VersionConfig, VersionT -T = TypeVar("T") -ContextManager = Generator[T, None, None] FolderDict = dict[str, Any] @@ -90,7 +88,7 @@ def publish_new_version(self) -> object: """ @contextmanager - def capture_debugging(self) -> ContextManager[StringIOWrapper]: + def capture_debugging(self) -> Generator[StringIOWrapper]: """ Redirect the resource debugging output within a context manager into a variable. @@ -105,7 +103,7 @@ def capture_debugging(self) -> ContextManager[StringIOWrapper]: yield self._debugging_output @contextmanager - def capture_directory_state(self, starting_state: FolderDict | None = None) -> ContextManager[TemporaryDirectoryState]: + def capture_directory_state(self, starting_state: FolderDict | None = None) -> Generator[TemporaryDirectoryState]: """ Open a context manager to expose the internal state of the resource. @@ -127,7 +125,7 @@ def capture_directory_state(self, starting_state: FolderDict | None = None) -> C self._directory_state.starting_state = old_start_state @contextmanager - def _forbid_methods(self, *methods: Callable[..., object]) -> ContextManager[None]: + def _forbid_methods(self, *methods: Callable[..., object]) -> Generator[None]: try: for method in methods: def new_method(*args: object, **kwargs: object) -> object: @@ -638,7 +636,7 @@ def publish_new_version(self, **params: object) -> tuple[VersionT, Metadata]: return new_version, metadata @contextmanager - def _temporarily_create_script_file(self, script_name: ScriptName, method_name: MethodName) -> ContextManager[None]: + def _temporarily_create_script_file(self, script_name: ScriptName, method_name: MethodName) -> Generator[None]: attribute_name = f"{script_name}_script" try: with TemporaryDirectory() as temp_dir: From 7f08143523aee644ee032719d7a81020d0bd59b2 Mon Sep 17 00:00:00 2001 From: gchqdev227 <62302861+gchqdev227@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:43:58 +0100 Subject: [PATCH 4/7] Dropped try/except in TypedVersion.__init_subclass__ as Python<3.10 no longer needs to be handled --- concoursetools/version.py | 10 +--------- tests/test_resource.py | 4 ++-- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/concoursetools/version.py b/concoursetools/version.py index 36b5607..b857871 100644 --- a/concoursetools/version.py +++ b/concoursetools/version.py @@ -262,15 +262,7 @@ class TypedVersion(Version): _un_flatten_functions: ClassVar[MutableMapping[type[Any], Callable[[type[Any], str], Any]]] = _TypeKeyDict() def __init_subclass__(cls) -> None: - try: - annotations = inspect.get_annotations(cls) - except AttributeError: - # Function isn't available in Python 3.9, so use the old code until EOL - try: - annotations = vars(cls)["__annotations__"] # avoid MRO lookup - except KeyError: - annotations = {} - + annotations = inspect.get_annotations(cls) if len(annotations) == 0: raise TypeError("Can't instantiate dataclass TypedVersion without any fields") diff --git a/tests/test_resource.py b/tests/test_resource.py index 483b421..7a6d515 100644 --- a/tests/test_resource.py +++ b/tests/test_resource.py @@ -301,7 +301,7 @@ def setUp(self) -> None: """Code to run before each test.""" self.temp_dir = TemporaryDirectory() cli_commands.assets(self.temp_dir.name, resource_file="tests/resource.py", - class_name=TestResource.__name__, executable="/usr/bin/env python3") + class_name=TestResource.__name__, executable=sys.executable) config = { "uri": "git://some-uri", @@ -409,7 +409,7 @@ def setUp(self) -> None: "branch": "develop", "private_key": "...", } - self.wrapper = FileConversionTestResourceWrapper(TestResource, config, executable="/usr/bin/env python3") + self.wrapper = FileConversionTestResourceWrapper(TestResource, config, executable=sys.executable) def test_check_step_with_version_no_debugging(self) -> None: version = TestVersion("61cbef") From e56ab916ef900027715188b4858b1f345e56c824 Mon Sep 17 00:00:00 2001 From: gchqdev227 <62302861+gchqdev227@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:50:45 +0100 Subject: [PATCH 5/7] Removed CLI parsing logic checks for Python<3.10 --- concoursetools/cli/parser.py | 13 +++---------- tests/test_cli_parsing.py | 4 +--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/concoursetools/cli/parser.py b/concoursetools/cli/parser.py index ad66063..a6daa39 100644 --- a/concoursetools/cli/parser.py +++ b/concoursetools/cli/parser.py @@ -10,20 +10,14 @@ from dataclasses import dataclass import inspect import shutil -import sys import textwrap +from types import UnionType from typing import Any, Generic, TypeVar from concoursetools import __version__ from concoursetools.cli.docstring import Docstring -_CURRENT_PYTHON_VERSION = (sys.version_info.major, sys.version_info.minor) - -if _CURRENT_PYTHON_VERSION >= (3, 10): - from types import UnionType - _ANNOTATIONS_TO_TYPES: dict[type[Any] | UnionType | str, type] = {} -else: - _ANNOTATIONS_TO_TYPES: dict[type[Any] | Any | str, type] = {} # type: ignore[no-redef] +_ANNOTATIONS_TO_TYPES: dict[type[Any] | UnionType | str, type] = {} CLIFunction = Callable[..., None] CLIFunctionT = TypeVar("CLIFunctionT", bound=CLIFunction) @@ -37,8 +31,7 @@ type_.__name__: type_, f"{type_.__name__} | None": type_, }) - if _CURRENT_PYTHON_VERSION >= (3, 10): - _ANNOTATIONS_TO_TYPES[type_ | None] = type_ + _ANNOTATIONS_TO_TYPES[type_ | None] = type_ class _CLIParser(ABC): diff --git a/tests/test_cli_parsing.py b/tests/test_cli_parsing.py index 0e2e187..4a05d30 100644 --- a/tests/test_cli_parsing.py +++ b/tests/test_cli_parsing.py @@ -9,7 +9,7 @@ import unittest.mock from concoursetools import __version__ -from concoursetools.cli.parser import _ANNOTATIONS_TO_TYPES, _CURRENT_PYTHON_VERSION, CLI, Docstring +from concoursetools.cli.parser import _ANNOTATIONS_TO_TYPES, CLI, Docstring class ParsingTests(unittest.TestCase): @@ -256,8 +256,6 @@ def test_types(self) -> None: self.assertEqual(_ANNOTATIONS_TO_TYPES[float], float) def test_optional_types(self) -> None: - if _CURRENT_PYTHON_VERSION < (3, 10): - self.skipTest("Union types with '|' only valid for Python >= 3.10.") self.assertEqual(_ANNOTATIONS_TO_TYPES[str | None], str) self.assertEqual(_ANNOTATIONS_TO_TYPES[bool | None], bool) self.assertEqual(_ANNOTATIONS_TO_TYPES[int | None], int) From 79b93f0fb310d3b0044825e58de94b9aa8e8d592 Mon Sep 17 00:00:00 2001 From: gchqdev227 <62302861+gchqdev227@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:44:44 +0100 Subject: [PATCH 6/7] Bumped version of Python 3.14 in CI to release-candidate 3 --- .github/workflows/unit-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index ab77702..1409c2b 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14.0-beta.2"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14.0-rc.3"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} From 41bc9e7c7bcc600d2a0906f7435f242dbdf6e4a1 Mon Sep 17 00:00:00 2001 From: gchqdev227 <62302861+gchqdev227@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:45:09 +0100 Subject: [PATCH 7/7] Updated changelog --- docs/source/changelog/development.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/changelog/development.rst b/docs/source/changelog/development.rst index f832f16..be7e4b9 100644 --- a/docs/source/changelog/development.rst +++ b/docs/source/changelog/development.rst @@ -4,3 +4,4 @@ Development * Added Python 3.14 support. * Documentation is now built with Python 3.13. +* Dropped support for Python 3.9.