Skip to content

Commit

Permalink
Track whether [tool.incremental] was found
Browse files Browse the repository at this point in the history
As the Hatch plugin is opt-in via some pyproject.toml noise I don't want
to require a redundant [tool.incremental] section.
  • Loading branch information
twm committed Jul 20, 2024
1 parent 5433dfd commit 8449989
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 63 deletions.
45 changes: 30 additions & 15 deletions src/incremental/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ def _get_setuptools_version(dist): # type: (_Distribution) -> None
[1]: https://setuptools.pypa.io/en/latest/userguide/extension.html#customizing-distribution-options
"""
config = _load_pyproject_toml("./pyproject.toml")
if not config:
if not config or not config.has_tool_incremental:
return

dist.metadata.version = _existing_version(config.path).public()
Expand Down Expand Up @@ -448,20 +448,26 @@ def _load_toml(f): # type: (BinaryIO) -> Any
@dataclass(frozen=True)
class _IncrementalConfig:
"""
@ivar package: The package name, capitalized as in the package metadata.
Configuration loaded from a ``pyproject.toml`` file.
"""

@ivar path: Path to the package root
has_tool_incremental: bool
"""
Does the pyproject.toml file contain a [tool.incremental]
section? This indicates that the package has explicitly
opted-in to Incremental versioning.
"""

package: str
"""The package name, capitalized as in the package metadata."""

path: str
"""Path to the package root"""


def _load_pyproject_toml(toml_path): # type: (str) -> Optional[_IncrementalConfig]
"""
Does the pyproject.toml file contain a [tool.incremental]
section? This indicates that the package has opted-in to Incremental
versioning.
Load Incremental configuration from a ``pyproject.toml``
If the [tool.incremental] section is empty we take the project name
from the [project] section. Otherwise we require only a C{name} key
Expand All @@ -474,16 +480,9 @@ def _load_pyproject_toml(toml_path): # type: (str) -> Optional[_IncrementalConf
except FileNotFoundError:
return None

if "tool" not in data:
return None
if "incremental" not in data["tool"]:
return None
tool_incremental = _extract_tool_incremental(data)

tool_incremental = data["tool"]["incremental"]
if not isinstance(tool_incremental, dict):
raise ValueError("[tool.incremental] must be a table")

if tool_incremental == {}:
if tool_incremental is None or tool_incremental == {}:
try:
package = data["project"]["name"]
except KeyError:
Expand All @@ -509,11 +508,27 @@ def _load_pyproject_toml(toml_path): # type: (str) -> Optional[_IncrementalConf
)

return _IncrementalConfig(
has_tool_incremental=tool_incremental is not None,
package=package,
path=_findPath(os.path.dirname(toml_path), package),
)


def _extract_tool_incremental(data): # type: (Dict[str, object]) -> Optional[Dict[str, object]]
if "tool" not in data:
return None
if not isinstance(data["tool"], dict):
raise ValueError("[tool] must be a table")
if "incremental" not in data["tool"]:
return None

tool_incremental = data["tool"]["incremental"]
if not isinstance(tool_incremental, dict):
raise ValueError("[tool.incremental] must be a table")

return tool_incremental


from ._version import __version__ # noqa: E402


Expand Down
118 changes: 70 additions & 48 deletions src/incremental/tests/test_pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"""Test handling of ``pyproject.toml`` configuration"""

import os
from typing import Optional
from pathlib import Path
from twisted.trial.unittest import TestCase

from incremental import _load_pyproject_toml, _IncrementalConfig
Expand All @@ -12,106 +14,126 @@
class VerifyPyprojectDotTomlTests(TestCase):
"""Test the `_load_pyproject_toml` helper function"""

def test_fileNotFound(self):
def _loadToml(
self, toml: str, *, path: os.PathLike | None = None
) -> Optional[_IncrementalConfig]:
"""
Verification fails when no ``pyproject.toml`` file exists.
Read a TOML snipped from a temporary file with `_load_pyproject_toml`
@param toml: TOML content of the temporary file
@param path: Path to which the TOML is written
"""
path = os.path.join(self.mktemp(), "pyproject.toml")
self.assertFalse(_load_pyproject_toml(path))
path = path or self.mktemp()

def test_noToolIncrementalSection(self):
with open(path, "w") as f:
f.write(toml)

try:
return _load_pyproject_toml(path)
except Exception as e:
if hasattr(e, "add_note"):
e.add_note(f"While loading:\n\n{toml}") # pragma: no coverage
raise

def test_fileNotFound(self):
"""
Verification fails when there isn't a ``[tool.incremental]`` section.
An absent ``pyproject.toml`` file produces no result
"""
path = self.mktemp()
for toml in [
"\n",
"[tool]\n",
"[tool.notincremental]\n",
'[project]\nname = "foo"\n',
]:
with open(path, "w") as f:
f.write(toml)
self.assertIsNone(_load_pyproject_toml(path))
path = os.path.join(self.mktemp(), "pyproject.toml")
self.assertIsNone(_load_pyproject_toml(path))

def test_nameMissing(self):
"""
`ValueError` is raised when ``[tool.incremental]`` is present but
he project name isn't available.
"""
path = self.mktemp()
for toml in [
"\n",
"[tool.notincremental]\n",
"[tool.incremental]\n",
"[project]\n[tool.incremental]\n",
]:
with open(path, "w") as f:
f.write(toml)

self.assertRaises(ValueError, _load_pyproject_toml, path)
self.assertRaises(ValueError, self._loadToml, toml)

def test_nameInvalid(self):
"""
`TypeError` is raised when the project name isn't a string.
"""
path = self.mktemp()
for toml in [
"[tool.incremental]\nname = -1\n",
"[tool.incremental]\n[project]\nname = 1.0\n",
]:
with open(path, "w") as f:
f.write(toml)

self.assertRaises(TypeError, _load_pyproject_toml, path)
self.assertRaises(TypeError, self._loadToml, toml)

def test_toolIncrementalInvalid(self):
"""
`ValueError` is raised when the ``[tool.incremental]`` section isn't
a dict.
`ValueError` is raised when the ``[tool]`` or ``[tool.incremental]``
isn't a table.
"""
path = self.mktemp()
for toml in [
"tool = false\n",
"[tool]\nincremental = false\n",
"[tool]\nincremental = 123\n",
"[tool]\nincremental = null\n",
]:
with open(path, "w") as f:
f.write(toml)

self.assertRaises(ValueError, _load_pyproject_toml, path)
self.assertRaises(ValueError, self._loadToml, toml)

def test_toolIncrementalUnexpecteKeys(self):
"""
Raise `ValueError` when the ``[tool.incremental]`` section contains
keys other than ``"name"``
"""
path = self.mktemp()
for toml in [
"[tool.incremental]\nfoo = false\n",
'[tool.incremental]\nname = "OK"\nother = false\n',
]:
with open(path, "w") as f:
f.write(toml)

self.assertRaises(ValueError, _load_pyproject_toml, path)
self.assertRaises(ValueError, self._loadToml, toml)

def test_ok(self):
def test_setuptoolsOptIn(self):
"""
The package has opted-in to Incremental version management when
the ``[tool.incremental]`` section is an empty dict.
the ``[tool.incremental]`` section is a dict. The project name
is taken from ``[tool.incremental] name`` or ``[project] name``.
"""
root = self.mktemp()
path = os.path.join(root, "src", "foo")
os.makedirs(path)
toml_path = os.path.join(root, "pyproject.toml")
root = Path(self.mktemp())
pkg = root / "src" / "foo"
pkg.mkdir(parents=True)

for toml in [
'[project]\nname = "Foo"\n[tool.incremental]\n',
'[tool.incremental]\nname = "Foo"\n',
]:
with open(toml_path, "w") as f:
f.write(toml)
config = self._loadToml(toml, path=root / "pyproject.toml")

self.assertEqual(
_load_pyproject_toml(toml_path),
_IncrementalConfig(package="Foo", path=path),
config,
_IncrementalConfig(
has_tool_incremental=True,
package="Foo",
path=str(pkg),
),
)

def test_noToolIncrementalSection(self):
"""
The ``has_tool_incremental`` flag is false when there
isn't a ``[tool.incremental]`` section.
"""
root = Path(self.mktemp())
pkg = root / "foo"
pkg.mkdir(parents=True)

config = self._loadToml(
'[project]\nname = "foo"\n',
path=root / "pyproject.toml",
)

self.assertEqual(
config,
_IncrementalConfig(
has_tool_incremental=False,
package="foo",
path=str(pkg),
),
)

0 comments on commit 8449989

Please sign in to comment.