diff --git a/news/381.feature b/news/381.feature new file mode 100644 index 0000000..efb3d73 --- /dev/null +++ b/news/381.feature @@ -0,0 +1 @@ +Update trove classifiers to match the python test matrix @gforcada diff --git a/pyproject.toml b/pyproject.toml index 0383eee..2049a5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,8 +32,9 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - "Jinja2", + "check-python-versions", "editorconfig", + "Jinja2", "packaging", "pyyaml", "tomlkit", diff --git a/src/plone/meta/config_package.py b/src/plone/meta/config_package.py index 1049977..dfb84bf 100755 --- a/src/plone/meta/config_package.py +++ b/src/plone/meta/config_package.py @@ -341,6 +341,22 @@ def _minimal_python_version(self): min_version = py_version return min_version + def _maximum_python_version(self): + """Detect the maximum Python version to be used in tooling (black, pre-commit...). + + Returns something like "3.14". + """ + options = self._get_options_for("tox", ("test_matrix",)) + test_matrix = get_test_matrix(options.get("test_matrix")) + max_version = None + for python_versions in test_matrix.values(): + for py_version in python_versions: + if py_version.startswith("pypy"): + continue + if max_version is None or Version(py_version) > Version(max_version): + max_version = py_version + return max_version + def _setuptools_upper_bound(self): """Determine upper bound for setuptools in build-system. @@ -487,6 +503,8 @@ def pyproject_toml(self): "pyproject.toml.j2", **options, ) + self._update_python_versions_on_pyproject_toml() + files.append(filename) return files @@ -504,6 +522,33 @@ def _get_manual_metadata(self): metadata = actual_pyproject[start_marker:end_marker] return metadata + def _update_python_versions_on_pyproject_toml(self): + """Update trove classifiers and `requires-python` on pyproject.toml + + Based on the test matrix. + """ + min_version = self._minimal_python_version() + max_version = self._maximum_python_version() + + config = (self.path / "pyproject.toml").read_text() + if "classifiers" not in config: + self.print_warning( + "Python versions", + "Project metadata, classifiers at least, are not found on pyproject.toml," + "thus they can not be updated to state.", + ) + return + + with change_dir(self.path): + call( + "check-python-versions", + "--update", + f"{min_version}-{max_version}", + "--only", + "pyproject.toml", + input="y\ny\n", + ) + def tox(self): options = self._get_options_for( "tox", diff --git a/src/plone/meta/shared/call.py b/src/plone/meta/shared/call.py index 304bab6..2441a4e 100644 --- a/src/plone/meta/shared/call.py +++ b/src/plone/meta/shared/call.py @@ -10,12 +10,14 @@ def abort(exitcode): sys.exit(exitcode) -def call(*args, capture_output=False, cwd=None, allowed_return_codes=(0,)): +def call(*args, capture_output=False, cwd=None, allowed_return_codes=(0,), input=None): """Call `args` as a subprocess. If it fails exit the process. """ - result = subprocess.run(args, capture_output=capture_output, text=True, cwd=cwd) + result = subprocess.run( + args, capture_output=capture_output, text=True, cwd=cwd, input=input + ) if result.returncode not in allowed_return_codes: print(f"ERROR: exit code {result.returncode}.") print("output:") diff --git a/tests/test_package_config_ci.py b/tests/test_package_config_ci.py index f513beb..75e3fc4 100644 --- a/tests/test_package_config_ci.py +++ b/tests/test_package_config_ci.py @@ -188,6 +188,23 @@ def test_metadata_is_kept(self, package_config): for line in text: assert line in final_toml_text + def test_update_classifiers(self, package_config): + pyproject_file_path = package_config.path / "pyproject.toml" + text = [ + "# START-MARKER-MANUAL-CONFIG", + "[project]", + 'name="random-project"', + "classifiers = []", + "# END-MARKER-MANUAL-CONFIG", + ] + pyproject_file_path.write_text("\n".join(text)) + # set a specific test matrix to ensure test stability + package_config.meta_cfg["tox"]["test_matrix"] = {"6.2": ["3.14", "3.13"]} + package_config.pyproject_toml() + final_toml_text = pyproject_file_path.read_text() + assert '"Programming Language :: Python :: 3.13",' in final_toml_text + assert '"Programming Language :: Python :: 3.14",' in final_toml_text + class TestSetuptoolsUpperBound: @pytest.mark.parametrize(["is_native", "expected"], [[True, "82"], [False, "83"]]) @@ -222,6 +239,28 @@ def test_python_version(self, package_config, matrix, output): assert result == output +class TestMaximalPythonVersion: + @pytest.mark.parametrize( + ["matrix", "output"], + [ + [None, "3.14"], + [{"6.0": ["*"]}, "3.13"], + [{"6.1": ["*"]}, "3.13"], + [{"6.2": ["*"]}, "3.14"], + [{"6.2": ["3.13"]}, "3.13"], + [{"6.2": ["3.13"], "6.1": ["3.9"]}, "3.13"], + [{"6.2": ["3.13", "3.10"], "5.2": ["3.8"]}, "3.13"], + [{"6.2": ["pypy3.10", "3.11"]}, "3.11"], + ], + ) + def test_python_version(self, package_config, matrix, output): + if matrix: + package_config.meta_cfg["tox"]["test_matrix"] = matrix + + result = package_config._maximum_python_version() + assert result == output + + class TestHandleGhActions: def test_returns_empty_when_disabled(self, package_config): package_config.meta_cfg["tox"]["use_test_matrix"] = False diff --git a/tests/test_shared_call.py b/tests/test_shared_call.py index 201b9cc..e8c7b02 100644 --- a/tests/test_shared_call.py +++ b/tests/test_shared_call.py @@ -23,7 +23,7 @@ def test_passes_args_to_subprocess(self, mock_run): ) call("git", "status") mock_run.assert_called_once_with( - ("git", "status"), capture_output=False, text=True, cwd=None + ("git", "status"), capture_output=False, text=True, cwd=None, input=None ) @patch("plone.meta.shared.call.subprocess.run") @@ -33,7 +33,7 @@ def test_capture_output(self, mock_run): ) call("cmd", capture_output=True) mock_run.assert_called_once_with( - ("cmd",), capture_output=True, text=True, cwd=None + ("cmd",), capture_output=True, text=True, cwd=None, input=None ) @patch("plone.meta.shared.call.subprocess.run") @@ -41,7 +41,7 @@ def test_cwd_kwarg(self, mock_run): mock_run.return_value = subprocess.CompletedProcess(args=["cmd"], returncode=0) call("cmd", cwd="/tmp") mock_run.assert_called_once_with( - ("cmd",), capture_output=False, text=True, cwd="/tmp" + ("cmd",), capture_output=False, text=True, cwd="/tmp", input=None ) @patch("plone.meta.shared.call.abort")