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

version: fix normalize_version() according to PEP440 #344

Merged
merged 2 commits into from
May 10, 2022
Merged
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
2 changes: 1 addition & 1 deletion src/poetry/core/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def module_name(name: str) -> str:


def normalize_version(version: str) -> str:
return PEP440Version.parse(version).to_string(short=True)
return PEP440Version.parse(version).to_string()


@contextmanager
Expand Down
71 changes: 36 additions & 35 deletions src/poetry/core/version/pep440/segments.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@
from typing import Union


RELEASE_PHASE_ALPHA = "alpha"
RELEASE_PHASE_BETA = "beta"
RELEASE_PHASE_RC = "rc"
RELEASE_PHASE_PREVIEW = "preview"
RELEASE_PHASE_POST = "post"
RELEASE_PHASE_REV = "rev"
RELEASE_PHASE_DEV = "dev"
RELEASE_PHASES = {
RELEASE_PHASE_ALPHA: "a",
RELEASE_PHASE_BETA: "b",
RELEASE_PHASE_RC: "c",
RELEASE_PHASE_PREVIEW: "pre",
RELEASE_PHASE_POST: "-", # shorthand of 1.2.3-post1 is 1.2.3-1
RELEASE_PHASE_REV: "r",
RELEASE_PHASE_DEV: "dev",
# Release phase IDs according to PEP440
RELEASE_PHASE_ID_ALPHA = "a"
RELEASE_PHASE_ID_BETA = "b"
RELEASE_PHASE_ID_RC = "rc"
RELEASE_PHASE_ID_POST = "post"
RELEASE_PHASE_ID_DEV = "dev"

RELEASE_PHASE_SPELLINGS = {
RELEASE_PHASE_ID_ALPHA: {RELEASE_PHASE_ID_ALPHA, "alpha"},
RELEASE_PHASE_ID_BETA: {RELEASE_PHASE_ID_BETA, "beta"},
RELEASE_PHASE_ID_RC: {RELEASE_PHASE_ID_RC, "c", "pre", "preview"},
RELEASE_PHASE_ID_POST: {RELEASE_PHASE_ID_POST, "r", "rev", "-"},
RELEASE_PHASE_ID_DEV: {RELEASE_PHASE_ID_DEV},
}
RELEASE_PHASE_NORMALIZATIONS = {
s: id_ for id_, spellings in RELEASE_PHASE_SPELLINGS.items() for s in spellings
}
RELEASE_PHASES_SHORT = {v: k for k, v in RELEASE_PHASES.items() if k != "post"}


@dataclasses.dataclass(frozen=True, eq=True, order=True)
Expand Down Expand Up @@ -109,37 +109,38 @@ class ReleaseTag:
number: int = dataclasses.field(default=0)

def __post_init__(self) -> None:
object.__setattr__(self, "phase", self.expand(self.phase))

@classmethod
def shorten(cls, phase: str) -> str:
return RELEASE_PHASES.get(phase, phase)

@classmethod
def expand(cls, phase: str) -> str:
return RELEASE_PHASES_SHORT.get(phase, phase)
object.__setattr__(
self, "phase", RELEASE_PHASE_NORMALIZATIONS.get(self.phase, self.phase)
)

def to_string(self, short: bool = False) -> str:
if short:
return f"{self.shorten(self.phase)}{self.number}"
return f"{self.phase}.{self.number}"
import warnings

warnings.warn(
"Parameter 'short' has no effect and will be removed. "
"(Release tags are always normalized according to PEP 440 now.)",
DeprecationWarning,
stacklevel=2,
)

return f"{self.phase}{self.number}"

def next(self) -> ReleaseTag:
return dataclasses.replace(self, phase=self.phase, number=self.number + 1)

def next_phase(self) -> ReleaseTag | None:
if self.phase in [
RELEASE_PHASE_POST,
RELEASE_PHASE_RC,
RELEASE_PHASE_REV,
RELEASE_PHASE_DEV,
RELEASE_PHASE_ID_POST,
RELEASE_PHASE_ID_RC,
RELEASE_PHASE_ID_DEV,
]:
return None

if self.phase == RELEASE_PHASE_ALPHA:
_phase = RELEASE_PHASE_BETA
elif self.phase == RELEASE_PHASE_BETA:
_phase = RELEASE_PHASE_RC
if self.phase == RELEASE_PHASE_ID_ALPHA:
_phase = RELEASE_PHASE_ID_BETA
elif self.phase == RELEASE_PHASE_ID_BETA:
_phase = RELEASE_PHASE_ID_RC
else:
return None

Expand Down
50 changes: 30 additions & 20 deletions src/poetry/core/version/pep440/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from typing import Any
from typing import TypeVar

from poetry.core.version.pep440.segments import RELEASE_PHASE_ALPHA
from poetry.core.version.pep440.segments import RELEASE_PHASE_DEV
from poetry.core.version.pep440.segments import RELEASE_PHASE_POST
from poetry.core.version.pep440.segments import RELEASE_PHASE_ID_ALPHA
from poetry.core.version.pep440.segments import RELEASE_PHASE_ID_DEV
from poetry.core.version.pep440.segments import RELEASE_PHASE_ID_POST
from poetry.core.version.pep440.segments import Release
from poetry.core.version.pep440.segments import ReleaseTag

Expand Down Expand Up @@ -143,28 +143,36 @@ def non_semver_parts(self) -> tuple[int, ...]:
return self.release.extra

def to_string(self, short: bool = False) -> str:
dash = "-" if not short else ""

version_string = dash.join(
part
for part in [
self.release.to_string(),
self.pre.to_string(short) if self.pre else None,
self.post.to_string(short) if self.post else None,
self.dev.to_string(short) if self.dev else None,
]
if part
)
if short:
import warnings

warnings.warn(
"Parameter 'short' has no effect and will be removed. "
"(Versions are always normalized according to PEP 440 now.)",
DeprecationWarning,
stacklevel=2,
)

version_string = self.release.to_string()

if self.epoch:
# if epoch is non-zero we should include it
version_string = f"{self.epoch}!{version_string}"

if self.pre:
version_string += self.pre.to_string()

if self.post:
version_string = f"{version_string}.{self.post.to_string()}"

if self.dev:
version_string = f"{version_string}.{self.dev.to_string()}"

if self.local:
assert isinstance(self.local, tuple)
version_string += "+" + ".".join(map(str, self.local))

return version_string
return version_string.lower()

@classmethod
def parse(cls: type[T], value: str) -> T:
Expand Down Expand Up @@ -219,15 +227,15 @@ def next_prerelease(self: T, next_phase: bool = False) -> PEP440Version:
assert self.pre is not None
pre = self.pre.next_phase() if next_phase else self.pre.next()
else:
pre = ReleaseTag(RELEASE_PHASE_ALPHA)
pre = ReleaseTag(RELEASE_PHASE_ID_ALPHA)
return self.__class__(epoch=self.epoch, release=self.release, pre=pre)

def next_postrelease(self: T) -> T:
if self.is_postrelease():
assert self.post is not None
post = self.post.next()
else:
post = ReleaseTag(RELEASE_PHASE_POST)
post = ReleaseTag(RELEASE_PHASE_ID_POST)
return self.__class__(
epoch=self.epoch,
release=self.release,
Expand All @@ -241,7 +249,7 @@ def next_devrelease(self: T) -> T:
assert self.dev is not None
dev = self.dev.next()
else:
dev = ReleaseTag(RELEASE_PHASE_DEV)
dev = ReleaseTag(RELEASE_PHASE_ID_DEV)
return self.__class__(
epoch=self.epoch,
release=self.release,
Expand All @@ -252,7 +260,9 @@ def next_devrelease(self: T) -> T:

def first_prerelease(self: T) -> T:
return self.__class__(
epoch=self.epoch, release=self.release, pre=ReleaseTag(RELEASE_PHASE_ALPHA)
epoch=self.epoch,
release=self.release,
pre=ReleaseTag(RELEASE_PHASE_ID_ALPHA),
)

def replace(self: T, **kwargs: Any) -> T:
Expand Down
71 changes: 71 additions & 0 deletions tests/utils/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,82 @@

from poetry.core.utils.helpers import canonicalize_name
from poetry.core.utils.helpers import combine_unicode
from poetry.core.utils.helpers import normalize_version
from poetry.core.utils.helpers import parse_requires
from poetry.core.utils.helpers import readme_content_type
from poetry.core.utils.helpers import temporary_directory


@pytest.mark.parametrize(
radoering marked this conversation as resolved.
Show resolved Hide resolved
"version,normalized_version",
[
( # already normalized version
"1!2.3.4.5.6a7.post8.dev9+local1.123.abc",
"1!2.3.4.5.6a7.post8.dev9+local1.123.abc",
),
# PEP 440 Normalization
# Case sensitivity
("1.1RC1", "1.1rc1"),
# Integer Normalization
("00", "0"),
("09000", "9000"),
("1.0+foo0100", "1.0+foo0100"),
# Pre-release separators
("1.1.a1", "1.1a1"),
("1.1-a1", "1.1a1"),
("1.1_a1", "1.1a1"),
("1.1a.1", "1.1a1"),
("1.1a-1", "1.1a1"),
("1.1a_1", "1.1a1"),
# Pre-release spelling
("1.1alpha1", "1.1a1"),
("1.1beta2", "1.1b2"),
("1.1c3", "1.1rc3"),
("1.1pre4", "1.1rc4"),
("1.1preview5", "1.1rc5"),
# Implicit pre-release number
("1.2a", "1.2a0"),
# Post release separators
("1.2.post2", "1.2.post2"),
("1.2-post2", "1.2.post2"),
("1.2_post2", "1.2.post2"),
("1.2post.2", "1.2.post2"),
("1.2post-2", "1.2.post2"),
("1.2post_2", "1.2.post2"),
# Post release spelling
("1.0-r4", "1.0.post4"),
("1.0-rev4", "1.0.post4"),
# Implicit post release number
("1.2.post", "1.2.post0"),
# Implicit post releases
("1.0-1", "1.0.post1"),
# Development release separators
("1.2.dev2", "1.2.dev2"),
("1.2-dev2", "1.2.dev2"),
("1.2_dev2", "1.2.dev2"),
("1.2dev.2", "1.2.dev2"),
("1.2dev-2", "1.2.dev2"),
("1.2dev_2", "1.2.dev2"),
# Implicit development release number
("1.2.dev", "1.2.dev0"),
# Local version segments
("1.0+ubuntu-1", "1.0+ubuntu.1"),
("1.0+ubuntu_1", "1.0+ubuntu.1"),
# Preceding v character
("v1.0", "1.0"),
# Leading and Trailing Whitespace
(" 1.0 ", "1.0"),
("\t1.0\t", "1.0"),
("\n1.0\n", "1.0"),
("\r\n1.0\r\n", "1.0"),
("\f1.0\f", "1.0"),
("\v1.0\v", "1.0"),
],
)
def test_normalize_version(version: str, normalized_version: str) -> None:
assert normalize_version(version) == normalized_version


def test_parse_requires() -> None:
requires = """\
jsonschema>=2.6.0.0,<3.0.0.0
Expand Down
9 changes: 3 additions & 6 deletions tests/version/test_version_pep440.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from poetry.core.version.pep440 import PEP440Version
from poetry.core.version.pep440 import Release
from poetry.core.version.pep440 import ReleaseTag
from poetry.core.version.pep440.segments import RELEASE_PHASES
from poetry.core.version.pep440.segments import RELEASE_PHASES_SHORT
from poetry.core.version.pep440.segments import RELEASE_PHASE_NORMALIZATIONS


@pytest.mark.parametrize(
Expand Down Expand Up @@ -67,12 +66,10 @@ def test_pep440_release_tag_next_phase(
assert ReleaseTag(*parts).next_phase() == result


@pytest.mark.parametrize(
"phase", list({*RELEASE_PHASES.keys(), *RELEASE_PHASES_SHORT.keys()})
)
@pytest.mark.parametrize("phase", list({*RELEASE_PHASE_NORMALIZATIONS.keys()}))
def test_pep440_release_tag_next(phase: str) -> None:
tag = ReleaseTag(phase=phase).next()
assert tag.phase == ReleaseTag.expand(phase)
assert tag.phase == RELEASE_PHASE_NORMALIZATIONS[phase]
assert tag.number == 1


Expand Down