diff --git a/docs/sources/reference/generated-files.md b/docs/sources/reference/generated-files.md index f3e1644c..3a21cc61 100644 --- a/docs/sources/reference/generated-files.md +++ b/docs/sources/reference/generated-files.md @@ -109,12 +109,21 @@ Includes pyupgrade, isort, black, zpretty, flake8, codespell, check-manifest, py **Template:** {file}`pyproject.toml.j2` -**Purpose:** Python tooling configuration for isort, black, codespell, check-manifest, and z3c.dependencychecker. -Also includes towncrier configuration if a {file}`news/` folder exists. +**Purpose:** Python distribution metadata and tooling configuration. + +Contrary to all other generated files, {file}`pyproject.toml` keeps whatever is within some special markers. + +These markers are meant to be around the `[project]` table, it should look like: + +```toml +# START-MARKER-MANUAL-CONFIG +[project] +name = "plone.meta" +version = "1.2.4" +... +# END-MARKER-MANUAL-CONFIG +``` -:::{note} -plone.meta overwrites {file}`pyproject.toml` completely, like all other generated files. All customization must go through {file}`.meta.toml`. -::: ## tox.ini diff --git a/news/315.feature.2 b/news/315.feature.2 new file mode 100644 index 00000000..23aca729 --- /dev/null +++ b/news/315.feature.2 @@ -0,0 +1 @@ +Ensure the `[project]` table is not removed from `pyproject.toml` @gforcada diff --git a/src/plone/meta/config_package.py b/src/plone/meta/config_package.py index 92c20e22..3f4b3a2a 100755 --- a/src/plone/meta/config_package.py +++ b/src/plone/meta/config_package.py @@ -6,6 +6,7 @@ from functools import cached_property from importlib.metadata import version from packaging.version import Version +from pathlib import Path import argparse import collections @@ -425,6 +426,8 @@ def pyproject_toml(self): "If you want to use Towncrier, you have to create a 'news/' folder manually.", ) + options["project_metadata"] = self._get_manual_metadata() + filename = self.copy_with_meta( "pyproject.toml.j2", **options, @@ -432,6 +435,20 @@ def pyproject_toml(self): files.append(filename) return files + def _get_manual_metadata(self): + metadata = "" + suffix = "MARKER-MANUAL-CONFIG" + pyproject_path = Path(self.path) / "pyproject.toml" + if not pyproject_path.exists(): + return "" + actual_pyproject = pyproject_path.read_text() + start_marker = actual_pyproject.find(f"# START-{suffix}") + end_marker = actual_pyproject.find(f"# END-{suffix}") + if start_marker > -1 and end_marker > -1: + end_marker = end_marker + len(f"# END-{suffix}") + metadata = actual_pyproject[start_marker:end_marker] + return metadata + def tox(self): options = self._get_options_for( "tox", diff --git a/src/plone/meta/default/pyproject.toml.j2 b/src/plone/meta/default/pyproject.toml.j2 index cfad7dcc..07ba76ac 100644 --- a/src/plone/meta/default/pyproject.toml.j2 +++ b/src/plone/meta/default/pyproject.toml.j2 @@ -1,6 +1,8 @@ [build-system] requires = ["setuptools>=68.2,<%(setuptools_upper_bound)s", "wheel"] +%(project_metadata)s + {% if news_folder_exists %} [tool.towncrier] directory = "news/" diff --git a/tests/conftest.py b/tests/conftest.py index 1c65e73a..2abf5009 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,11 +44,23 @@ def mock_args(mock_git_repo): @pytest.fixture -def package_config(meta_toml_factory, mock_args): +def pyproject_toml(mock_git_repo): + """Create an empty pyproject.toml""" + + def _create(): + toml_path = mock_git_repo / "pyproject.toml" + toml_path.touch() + + return _create + + +@pytest.fixture +def package_config(pyproject_toml, meta_toml_factory, mock_args): """Create a PackageConfiguration with mocked subprocess calls.""" from plone.meta.config_package import PackageConfiguration meta_toml_factory() # creates default .meta.toml + pyproject_toml() # creates an empty pyproject.toml with ( patch( "plone.meta.config_package.git_server_url", diff --git a/tests/test_package_config_ci.py b/tests/test_package_config_ci.py index 229b71c6..2b566b99 100644 --- a/tests/test_package_config_ci.py +++ b/tests/test_package_config_ci.py @@ -79,9 +79,11 @@ def test_flake8(self, package_config): class TestPyproject: def test_minimal_files(self, package_config): + pyproject_file_path = package_config.path / "pyproject.toml" result = package_config.pyproject_toml() assert len(result) == 1 - assert (package_config.path / "pyproject.toml").exists() + text = pyproject_file_path.read_text() + assert len(text.splitlines()) > 50 def test_if_news_folder_exists(self, package_config): (package_config.path / "news").mkdir(parents=True, exist_ok=True) @@ -96,6 +98,21 @@ def test_if_changes_md_exists(self, package_config): assert len(result) == 2 assert (package_config.path / "news" / ".changelog_template.jinja").exists() + def test_metadata_is_kept(self, package_config): + pyproject_file_path = package_config.path / "pyproject.toml" + text = [ + "# START-MARKER-MANUAL-CONFIG", + "[project]", + 'name="random-project"', + "# END-MARKER-MANUAL-CONFIG", + ] + pyproject_file_path.write_text("\n".join(text)) + package_config.pyproject_toml() + final_toml_text = pyproject_file_path.read_text() + assert len(final_toml_text.splitlines()) > len(text) + for line in text: + assert line in final_toml_text + class TestSetuptoolsUpperBound: @pytest.mark.parametrize(["is_native", "expected"], [[True, "82"], [False, "83"]])