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 PEP 440 #15

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Here are some of the arguments that you can use for `git` provider:
|-------|-------------|---------|
| `version_prefix` | filter tags only starts with this prefix | `v` |
| `format` | plugin will use commit hash (long) or not (short) to build a project version | `short` |
| `bump_segment` | plugin will bump the selected version's segment with the number of commits after the last tag (allowed `release`, `post`, `dev`) | `release` |


Example:

Expand All @@ -72,6 +74,19 @@ Building awesome_package (0.1.0)
- Built awesome_package-0.1.0-py3-none-any.whl
```

Logic behind `bump_segment` with 10 commits:
* `release`: bumps `patch` (as in `<major>.<minor>.<patch>`) or the lowest
* `1 -> 1.0.10`
* `1.0 -> 1.0.10`
* `1.0.2 -> 1.0.12`
* `1.0.0.2 -> 1.0.0.12`
* `post`: bumps `post`
* `1.0 -> 1.0.post10`
* `1.0.post2 -> 1.0.post12`
* `dev`: bumps `dev`
* `1.0 -> 1.0.dev10`
* `1.0.dev2 -> 1.0.dev12`

## How to develop

Before getting started with development, you'll need to have poetry installed.
Expand Down
9 changes: 9 additions & 0 deletions poem_plugins/config/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,23 @@ class GitVersionFormatEnum(StrEnum):
SHORT = "short"


@unique
class VersionSegmentBumpEnum(StrEnum):
RELEASE = "release"
POST_RELEASE = "post"
DEV = "dev"


@dataclass
class GitProviderSettings(BaseConfig):
MAPPERS = MappingProxyType(
{
"format": GitVersionFormatEnum,
"version_prefix": str,
"bump_segment": VersionSegmentBumpEnum,
},
)

format: GitVersionFormatEnum = GitVersionFormatEnum.SHORT
version_prefix: str = "v"
bump_segment: VersionSegmentBumpEnum = VersionSegmentBumpEnum.RELEASE
28 changes: 21 additions & 7 deletions poem_plugins/general/version/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
from typing import NamedTuple, Optional
import re
from typing import NamedTuple, Optional, Tuple


_REGEX_PRE = re.compile(r"^(?P<id>(a|b|rc))(?P<val>\d+)$")


class Version(NamedTuple):
major: int
minor: int
patch: int
release: Tuple[int, ...]
epoch: Optional[int] = None
pre: Optional[str] = None
post: Optional[int] = None
dev: Optional[int] = None
commit: Optional[str] = None

def __str__(self) -> str:
if not self.commit:
return f"{self.major}.{self.minor}.{self.patch}"
return f"{self.major}.{self.minor}.{self.patch}+g{self.commit}"
epoch = f"{self.epoch}!" if self.epoch is not None else ""
pre = self.pre or ""
post = f".post{self.post}" if self.post is not None else ""
dev = f".dev{self.dev}" if self.dev is not None else ""
commit = f"+{self.commit}" if self.commit is not None else ""
version = (
epoch +
".".join(map(str, self.release)) +
"".join((pre, post, dev, commit))
)
return version
99 changes: 63 additions & 36 deletions poem_plugins/general/version/drivers/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import warnings
from dataclasses import dataclass
from shutil import which
from types import MappingProxyType
from typing import Any, Callable, ClassVar, Mapping, Match, Optional

from poem_plugins.config.git import GitProviderSettings, GitVersionFormatEnum
from poem_plugins.config.git import (
GitProviderSettings, GitVersionFormatEnum, VersionSegmentBumpEnum,
)
from poem_plugins.general.version import Version
from poem_plugins.general.version.drivers import IVersionDriver

Expand All @@ -32,17 +32,59 @@ class GitVersionDriver(IVersionDriver):
'__version__ = "{version}"\n'
)

CONVERTERS: ClassVar[
Mapping[str, Callable[[Any], Any]]
] = MappingProxyType(
{
"major": int,
"minor": int,
"patch": int,
},
)
def _get_version_pep_440(self, raw_version: str) -> Version:
if raw_version.startswith(self.settings.version_prefix):
raw_version = raw_version.removeprefix(self.settings.version_prefix)
else:
raise RuntimeError(
f"Version tag must start with '{self.settings.version_prefix}' "
f"as defined in the plugin's config (or by default).",
)
regex = (
r"^((?P<epoch>\d+)!)?(?P<release>\d+(\.\d+)*)"
r"(?P<pre>(a|b|rc)\d+)?(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?"
r"-(?P<commit_count>\d+)-(?P<commit>\S+)$"
)
match = re.match(regex, raw_version)
if not match:
raise RuntimeError(
f"Failed to parse git version: '{raw_version}' must conform to "
f"PEP 440",
)
groups = match.groupdict()
epoch = groups["epoch"]
pre, post, dev = groups["pre"], groups["post"], groups["dev"]
commit_count = int(groups["commit_count"])
commit = groups["commit"]
release = list(map(int, groups["release"].split(".")))
if len(release) < 3:
release += [0] * (3 - len(release))

def get_version(self) -> Version:
segments = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

specify please typed dict to avoid using ingore

"epoch": int(epoch) if epoch is not None else None,
"pre": pre,
"post": int(post) if post is not None else None,
"dev": int(dev) if dev is not None else None,
"commit": commit,
}

if commit_count:
bump_segment = self.settings.bump_segment
if bump_segment == VersionSegmentBumpEnum.RELEASE:
print(release)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems redundant

release[-1] += commit_count
else:
segments[bump_segment.value] = segments[bump_segment.value] or 0
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump_segment is a StrEnum, does it need to get value from it?

segments[bump_segment.value] += commit_count # type: ignore

segments["release"] = tuple(release)

if self.settings.format == GitVersionFormatEnum.SHORT:
segments["commit"] = None

return Version(**segments) # type: ignore
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why ignore here?


def _git_describe(self) -> str:
if GIT_BIN is None:
raise RuntimeError(WARNING_TEXT)

Expand All @@ -53,36 +95,21 @@ def get_version(self) -> Version:
)

if result.returncode != 0:
raise RuntimeError("Cannot found git version")
raw_version = result.stdout.strip()
regex = (
r"(?P<major>\d+)\.(?P<minor>\d+)-(?P<patch>\d+)-(?P<commit>\S+)"
)
match: Optional[Match[str]] = re.match(
self.settings.version_prefix + regex,
raw_version,
)
raise RuntimeError("Failed to find git version tag")
return result.stdout.strip()

if match is None:
raise RuntimeError("Cannot parse git version")
raw_kwargs = dict(match.groupdict())
if self.settings.format == GitVersionFormatEnum.SHORT:
raw_kwargs.pop("commit", None)
kwargs = {
k: self.CONVERTERS.get(k, lambda x: x)(v)
for k, v in raw_kwargs.items()
}

return Version(**kwargs)
def get_version(self) -> Version:
raw_version = self._git_describe()
return self._get_version_pep_440(raw_version)

def render_version_file(
self,
version: Version,
) -> str:
return self.VERSION_TEMPLATE.format(
whoami='poem-plugins "git" plugin',
major=version.major,
minor=version.minor,
patch=version.patch,
major=version.release[0],
minor=version.release[1],
patch=version.release[2],
version=str(version),
)
5 changes: 3 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ def simple_project(tmpdir) -> Iterator[Path]:

@pytest.fixture
def expected_long_version() -> Version:
return Version(1, 2, 0, "g3c3e199")
return Version(release=(1, 2, 0), commit="g3c3e199")


@pytest.fixture
def expected_version() -> Version:
return Version(1, 2, 0)
return Version(release=(1, 2, 0))
20 changes: 20 additions & 0 deletions tests/general/versions/drivers/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@
def git_settings() -> GitProviderSettings:
return GitProviderSettings()


@pytest.fixture
def git_version_driver(git_settings) -> GitVersionDriver:
return GitVersionDriver(git_settings)


class MockedGitVersionDriver(GitVersionDriver):

_describe: str

def __init__(self, *args, describe: str, **kwargs):
super().__init__(*args, **kwargs)
self._describe = describe

def _git_describe(self) -> str:
return self._describe


@pytest.fixture
def get_mocked_git_version_driver(git_settings):
return lambda describe, git_settings=git_settings: (
MockedGitVersionDriver(git_settings, describe=describe)
)
84 changes: 84 additions & 0 deletions tests/general/versions/drivers/git/test_bump_segment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import pytest

from poem_plugins.config.git import (
GitProviderSettings, GitVersionFormatEnum, VersionSegmentBumpEnum,
)
from poem_plugins.general.version import Version


@pytest.fixture
def git_settings() -> GitProviderSettings:
return GitProviderSettings(format=GitVersionFormatEnum.SHORT)


@pytest.mark.parametrize(
"describe,segment,expected", (
(
"1",
VersionSegmentBumpEnum.RELEASE,
Version(release=(1, 0, 10)),
),
(
"1.2",
VersionSegmentBumpEnum.RELEASE,
Version(release=(1, 2, 10)),
),
(
"1.2.3",
VersionSegmentBumpEnum.RELEASE,
Version(release=(1, 2, 13)),
),
(
"1.2.3.4",
VersionSegmentBumpEnum.RELEASE,
Version(release=(1, 2, 3, 14)),
),
(
"1.2.post3",
VersionSegmentBumpEnum.POST_RELEASE,
Version(release=(1, 2, 0), post=13),
),
(
"1.2.dev3",
VersionSegmentBumpEnum.DEV,
Version(release=(1, 2, 0), dev=13),
),
),
)
def test_pep_440(
get_mocked_git_version_driver, describe, segment, expected,
) -> None:
describe = "v" + describe + "-10-g3c3e199"

git_settings = GitProviderSettings(bump_segment=segment)
driver = get_mocked_git_version_driver(describe=describe, git_settings=git_settings)
assert driver.get_version() == expected


@pytest.mark.parametrize(
"describe,segment,expected", (
(
"1.2.3.4",
VersionSegmentBumpEnum.RELEASE,
Version(release=(1, 2, 3, 4)),
),
(
"1.2",
VersionSegmentBumpEnum.POST_RELEASE,
Version(release=(1, 2, 0)),
),
(
"1.2",
VersionSegmentBumpEnum.DEV,
Version(release=(1, 2, 0)),
),
),
)
def test_pep_440_zero_commits(
get_mocked_git_version_driver, describe, segment, expected,
) -> None:
describe = "v" + describe + "-0-g3c3e199"

git_settings = GitProviderSettings(bump_segment=segment)
driver = get_mocked_git_version_driver(describe=describe, git_settings=git_settings)
assert driver.get_version() == expected
Loading
Loading