Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support the [project] section (PEP 621) #181

Merged
merged 11 commits into from
Jun 17, 2024
8 changes: 7 additions & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,22 @@ jobs:
poetry-version: '1.7.1'
- python-version: '3.7'
poetry-version: '1.8.1'
include:
- python-version: '3.11'
poetry-version: 'git+https://github.com/radoering/poetry.git@pep621-support'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- if: ${{ startsWith(matrix.poetry-version, 'git') }}
run: |
echo "USE_PEP621=1" >> $GITHUB_ENV
- run: |
pip install pipx
pipx install poetry==${{ matrix.poetry-version }}
pipx install ${{ startsWith(matrix.poetry-version, 'git') && matrix.poetry-version || format('poetry=={0}', matrix.poetry-version) }}
pipx install invoke

poetry install --extras plugin
Expand Down
91 changes: 70 additions & 21 deletions poetry_dynamic_versioning/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import subprocess
import sys
import textwrap
from enum import Enum
from importlib import import_module
from pathlib import Path
from typing import Mapping, MutableMapping, Optional, Sequence, Tuple, Union
Expand Down Expand Up @@ -118,17 +119,26 @@ class _Config(Mapping):
pass


class _Mode(Enum):
Classic = "classic"
Pep621 = "pep621"


class _ProjectState:
def __init__(
self,
path: Path,
original_version: str,
original_version: Optional[str],
version: str,
mode: _Mode,
dynamic_index: Optional[int],
substitutions: Optional[MutableMapping[Path, str]] = None,
) -> None:
self.path = path
self.original_version = original_version
self.version = version
self.mode = mode
self.dynamic_index = dynamic_index
self.substitutions = (
{} if substitutions is None else substitutions
) # type: MutableMapping[Path, str]
Expand Down Expand Up @@ -590,11 +600,21 @@ def _substitute_version_in_text(version: str, content: str, patterns: Sequence[_


def _apply_version(
version: str, instance: Version, config: _Config, pyproject_path: Path, retain: bool = False
name: str,
version: str,
instance: Version,
config: _Config,
pyproject_path: Path,
mode: _Mode,
retain: bool = False,
) -> None:
pyproject = tomlkit.parse(pyproject_path.read_bytes().decode("utf-8"))

pyproject["tool"]["poetry"]["version"] = version # type: ignore
if mode == _Mode.Classic:
pyproject["tool"]["poetry"]["version"] = version # type: ignore
elif mode == _Mode.Pep621:
pyproject["project"]["dynamic"].remove("version") # type: ignore
pyproject["project"]["version"] = version # type: ignore

# Disable the plugin in case we're building a source distribution,
# which won't have access to the VCS info at install time.
Expand All @@ -604,8 +624,6 @@ def _apply_version(

pyproject_path.write_bytes(tomlkit.dumps(pyproject).encode("utf-8"))

name = pyproject["tool"]["poetry"]["name"] # type: ignore

for file_name, file_info in config["files"].items():
full_file = pyproject_path.parent.joinpath(file_name)

Expand Down Expand Up @@ -635,19 +653,12 @@ def _apply_version(


def _get_and_apply_version(
name: Optional[str] = None,
original: Optional[str] = None,
pyproject: Optional[Mapping] = None,
pyproject_path: Optional[Path] = None,
retain: bool = False,
force: bool = False,
# fmt: off
io: bool = True
# fmt: on
io: bool = True,
) -> Optional[str]:
if name is not None and name in _state.projects:
return name

if pyproject_path is None:
pyproject_path = _get_pyproject_path()
if pyproject_path is None:
Expand All @@ -656,11 +667,32 @@ def _get_and_apply_version(
if pyproject is None:
pyproject = tomlkit.parse(pyproject_path.read_bytes().decode("utf-8"))

if name is None or original is None:
classic = (
"tool" in pyproject
and "poetry" in pyproject["tool"]
and "name" in pyproject["tool"]["poetry"]
)
pep621 = (
"project" in pyproject
and "name" in pyproject["project"]
and "dynamic" in pyproject["project"]
and "version" in pyproject["project"]["dynamic"]
and "version" not in pyproject["project"]
)

if classic:
name = pyproject["tool"]["poetry"]["name"]
original = pyproject["tool"]["poetry"]["version"]
if name in _state.projects:
return name
dynamic_index = None
elif pep621:
name = pyproject["project"]["name"]
original = pyproject["project"].get("version")
dynamic_index = pyproject["project"]["dynamic"].index("version")
else:
return name if name in _state.projects else None

if name in _state.projects:
return name

config = _get_config(pyproject)
if not config["enable"] and not force:
Expand All @@ -674,11 +706,20 @@ def _get_and_apply_version(
finally:
os.chdir(str(initial_dir))

# Condition will always be true, but it makes Mypy happy.
if name is not None and original is not None:
_state.projects[name] = _ProjectState(pyproject_path, original, version)
if classic and name is not None and original is not None:
mode = _Mode.Classic
_state.projects[name] = _ProjectState(
pyproject_path, original, version, mode, dynamic_index
)
if io:
_apply_version(name, version, instance, config, pyproject_path, mode, retain)
elif pep621 and name is not None:
mode = _Mode.Pep621
_state.projects[name] = _ProjectState(
pyproject_path, original, version, mode, dynamic_index
)
if io:
_apply_version(version, instance, config, pyproject_path, retain)
_apply_version(name, version, instance, config, pyproject_path, mode, retain)

return name

Expand All @@ -704,7 +745,15 @@ def _revert_version(retain: bool = False) -> None:
# Reread pyproject.toml in case the substitutions affected it.
pyproject = tomlkit.parse(state.path.read_bytes().decode("utf-8"))

pyproject["tool"]["poetry"]["version"] = state.original_version # type: ignore
if state.mode == _Mode.Classic:
if state.original_version is not None:
pyproject["tool"]["poetry"]["version"] = state.original_version # type: ignore
elif state.mode == _Mode.Pep621:
if state.dynamic_index is not None:
index = state.dynamic_index
pyproject["project"]["dynamic"].insert(index, "version") # type: ignore
if "version" in pyproject["project"]: # type: ignore
pyproject["project"].pop("version") # type: ignore

if not retain and not _state.cli_mode:
pyproject["tool"]["poetry-dynamic-versioning"]["enable"] = True # type: ignore
Expand Down
2 changes: 0 additions & 2 deletions poetry_dynamic_versioning/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ def alt_poetry_create(cls, *args, **kwargs):

if not _state.cli_mode:
name = _get_and_apply_version(
name=instance.local_config["name"],
original=instance.local_config["version"],
pyproject=instance.pyproject.data,
pyproject_path=_get_pyproject_path_from_poetry(instance.pyproject),
)
Expand Down
2 changes: 0 additions & 2 deletions poetry_dynamic_versioning/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ def _apply_version_via_plugin(
# fmt: on
) -> None:
name = _get_and_apply_version(
name=poetry.local_config["name"],
original=poetry.local_config["version"],
pyproject=poetry.pyproject.data,
pyproject_path=_get_pyproject_path_from_poetry(poetry.pyproject),
retain=retain,
Expand Down
1 change: 1 addition & 0 deletions tests/project-pep621/project_pep621/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.0"
15 changes: 15 additions & 0 deletions tests/project-pep621/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[project]
name = "project-pep621"
dynamic = ["version"]

[tool.poetry]
# The plugin itself doesn't need this, but Poetry does:
# https://github.com/python-poetry/poetry-core/blob/c80dcc53793316104862d2c3ac888dde3c263b08/tests/test_factory.py#L39-L42
version = "0.0.0"

[tool.poetry-dynamic-versioning]
enable = true

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
64 changes: 58 additions & 6 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pathlib import Path
from typing import Optional, Sequence, Tuple

import dunamai
import pytest
import tomlkit

Expand All @@ -16,6 +17,10 @@
DUMMY_DIST = DUMMY / "dist"
DUMMY_PYPROJECT = DUMMY / "pyproject.toml"

DUMMY_PEP621 = ROOT / "tests" / "project-pep621"
DUMMY_PEP621_DIST = DUMMY_PEP621 / "dist"
DUMMY_PEP621_PYPROJECT = DUMMY_PEP621 / "pyproject.toml"

DUMMY_VERSION = "0.0.999"
DEPENDENCY_DYNAMIC_VERSION = "0.0.888"

Expand Down Expand Up @@ -74,11 +79,12 @@ def before_all():

@pytest.fixture(autouse=True)
def before_each():
run(f"git checkout -- {DUMMY.as_posix()}")
delete(DUMMY / "dist")
delete(DUMMY / "poetry.lock")
for file in DUMMY.glob("*.whl"):
delete(file)
for project in [DUMMY, DUMMY_PEP621]:
run(f"git checkout -- {project.as_posix()}")
delete(project / "dist")
delete(project / "poetry.lock")
for file in project.glob("*.whl"):
delete(file)


def test_plugin_enabled():
Expand Down Expand Up @@ -123,7 +129,10 @@ def test_invalid_config_for_vcs():
def test_keep_pyproject_modifications():
package = "cachy"
# Using --optional to avoid actually installing the package
run(f"poetry add --optional {package}", where=DUMMY)
if "USE_PEP621" in os.environ:
run(f"poetry add --optional main {package}", where=DUMMY)
else:
run(f"poetry add --optional {package}", where=DUMMY)
# Make sure pyproject.toml contains the new package dependency
data = DUMMY_PYPROJECT.read_bytes().decode("utf-8")
assert package in data
Expand Down Expand Up @@ -267,3 +276,46 @@ def test_plugin_show():
# Just skip it for now.
if "CI" not in os.environ:
assert "poetry-dynamic-versioning" in out


@pytest.mark.skipif("USE_PEP621" not in os.environ, reason="Requires Poetry with PEP-621 support")
def test_pep621_with_dynamic_version():
version = dunamai.Version.from_git().serialize()

run("poetry-dynamic-versioning", where=DUMMY_PEP621)
pyproject = tomlkit.parse(DUMMY_PEP621_PYPROJECT.read_bytes().decode("utf-8"))
assert pyproject["project"]["version"] == version
assert "version" not in pyproject["project"]["dynamic"]
assert f'__version__ = "{version}"' in (
DUMMY_PEP621 / "project_pep621" / "__init__.py"
).read_bytes().decode("utf-8")


@pytest.mark.skipif("USE_PEP621" not in os.environ, reason="Requires Poetry with PEP-621 support")
def test_pep621_with_dynamic_version_and_cleanup():
version = dunamai.Version.from_git().serialize()

run("poetry build", where=DUMMY_PEP621)
pyproject = tomlkit.parse(DUMMY_PEP621_PYPROJECT.read_bytes().decode("utf-8"))
assert "version" not in pyproject["project"]
assert "version" in pyproject["project"]["dynamic"]
assert '__version__ = "0.0.0"' in (
DUMMY_PEP621 / "project_pep621" / "__init__.py"
).read_bytes().decode("utf-8")

artifact = next(DUMMY_PEP621_DIST.glob("*.whl"))
assert f"-{version}-" in artifact.name


@pytest.mark.skipif("USE_PEP621" not in os.environ, reason="Requires Poetry with PEP-621 support")
def test_pep621_without_dynamic_version():
pyproject = tomlkit.parse(DUMMY_PEP621_PYPROJECT.read_bytes().decode("utf-8"))
pyproject["project"]["dynamic"] = []
DUMMY_PEP621_PYPROJECT.write_bytes(tomlkit.dumps(pyproject).encode("utf-8"))

run("poetry-dynamic-versioning", codes=[1], where=DUMMY_PEP621)
pyproject = tomlkit.parse(DUMMY_PEP621_PYPROJECT.read_bytes().decode("utf-8"))
assert "version" not in pyproject["project"]
assert '__version__ = "0.0.0"' in (
DUMMY_PEP621 / "project_pep621" / "__init__.py"
).read_bytes().decode("utf-8")