From 7639ecb7a4483605bad9565f826daba139b80f29 Mon Sep 17 00:00:00 2001 From: reginaldalfret Date: Tue, 27 Oct 2026 22:49:23 +0530 Subject: [PATCH] fix: handle VCS checkout CPython versions with trailing '+' in config CPython builds from a VCS checkout append '+' to the version string (e.g. '3.13.15+'). When running on such builds, packaging.version.Version rejects the bare trailing '+' with an InvalidVersion exception, causing merge_configs and default Config instantiation to fail on every file. Introduce current_python_version() helper that strips any trailing '+' from platform.python_version() before parsing as a Version, ensuring compatibility with development builds while strictly preserving user configuration validation. Closes #576 Signed-off-by: reginaldalfret --- src/fixit/config.py | 4 ++-- src/fixit/ftypes.py | 14 +++++++++++--- src/fixit/tests/config.py | 12 ++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/fixit/config.py b/src/fixit/config.py index 64b0d096..3f832d5f 100644 --- a/src/fixit/config.py +++ b/src/fixit/config.py @@ -10,7 +10,6 @@ import platform import sys from contextlib import contextmanager, ExitStack - from pathlib import Path from types import ModuleType from typing import ( @@ -35,6 +34,7 @@ from .format import FORMAT_STYLES from .ftypes import ( Config, + current_python_version, is_collection, is_sequence, Options, @@ -415,7 +415,7 @@ def merge_configs( enable_rules: Set[QualifiedRule] = {QualifiedRule("fixit.rules")} disable_rules: Set[QualifiedRule] = set() rule_options: RuleOptionsTable = {} - target_python_version: Optional[Version] = Version(platform.python_version()) + target_python_version: Optional[Version] = current_python_version() target_formatter: Optional[str] = None output_format: OutputFormat = OutputFormat.fixit output_template: str = "" diff --git a/src/fixit/ftypes.py b/src/fixit/ftypes.py index 1e3c1d8a..3bd07239 100644 --- a/src/fixit/ftypes.py +++ b/src/fixit/ftypes.py @@ -206,6 +206,16 @@ class LSPOptions: debounce_interval: float = 0.5 +def current_python_version() -> Version: + """ + Return the running Python version as a packaging Version object. + + Strips any trailing '+' from VCS checkout builds (e.g. '3.13.15+') + to ensure compatibility with PEP 440 version parsing. + """ + return Version(platform.python_version().rstrip("+")) + + @dataclass class Config: """ @@ -226,9 +236,7 @@ class Config: options: RuleOptionsTable = field(default_factory=dict) # filtering criteria - python_version: Optional[Version] = field( - default_factory=lambda: Version(platform.python_version()) - ) + python_version: Optional[Version] = field(default_factory=current_python_version) tags: Tags = field(default_factory=Tags) # post-run processing diff --git a/src/fixit/tests/config.py b/src/fixit/tests/config.py index 04729734..33997c66 100644 --- a/src/fixit/tests/config.py +++ b/src/fixit/tests/config.py @@ -9,6 +9,7 @@ from textwrap import dedent from typing import List, Sequence, Tuple, Type from unittest import TestCase +from unittest.mock import patch from click.testing import CliRunner @@ -351,6 +352,17 @@ def test_merge_configs(self) -> None: actual = config.merge_configs(target, raw_configs) self.assertEqual(expected, actual) + def test_merge_configs_vcs_python_version(self) -> None: + target = self.tdp / "foo.py" + with patch("platform.python_version", return_value="3.13.15+"): + cfg = config.merge_configs(target, []) + self.assertEqual(cfg.python_version, Version("3.13.15")) + + def test_config_default_vcs_python_version(self) -> None: + with patch("platform.python_version", return_value="3.13.15+"): + cfg = Config() + self.assertEqual(cfg.python_version, Version("3.13.15")) + def test_generate_config(self) -> None: for name, path, root, options, expected in ( (