Skip to content

Commit

Permalink
Merge pull request #181 from edgarrmondragon/poetry-pep621
Browse files Browse the repository at this point in the history
Support the `[project]` section (PEP 621)
  • Loading branch information
mtkennerly committed Jun 17, 2024
2 parents 983f69f + fb39941 commit 5188507
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 32 deletions.
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")

0 comments on commit 5188507

Please sign in to comment.