Commit aac9fee8 by Sébastien Eustace

Be more compliant with PEP 440

parent 56d6aee3
......@@ -16,5 +16,10 @@ class BuildCommand(Command):
if self.option('format'):
fmt = self.option('format')
package = self.poetry.package
self.line(f'Building <info>{package.pretty_name}</> '
f'(<comment>{package.version}</>)')
self.line('')
builder = Builder(self.poetry, self.output)
builder.build(fmt)
......@@ -58,13 +58,13 @@ class SdistBuilder(Builder):
target_dir.mkdir(parents=True)
target = target_dir / f'{self._package.pretty_name}' \
f'-{self._package.pretty_version}.tar.gz'
f'-{self._package.version}.tar.gz'
gz = GzipFile(target.as_posix(), mode='wb')
tar = tarfile.TarFile(target.as_posix(), mode='w', fileobj=gz,
format=tarfile.PAX_FORMAT)
try:
tar_dir = f'{self._package.pretty_name}-{self._package.pretty_version}'
tar_dir = f'{self._package.pretty_name}-{self._package.version}'
files_to_add = self.find_files_to_add()
......
......@@ -128,7 +128,7 @@ class WheelBuilder(Builder):
tag = ('py2.' if self.supports_python2() else '') + 'py3-none-any'
return '{}-{}-{}.whl'.format(
re.sub("[^\w\d.]+", "_", self._package.pretty_name, flags=re.UNICODE),
re.sub("[^\w\d.]+", "_", self._package.pretty_version, flags=re.UNICODE),
re.sub("[^\w\d.]+", "_", self._package.version, flags=re.UNICODE),
tag)
def supports_python2(self):
......
......@@ -2,6 +2,7 @@ from typing import Union
from poetry.semver.helpers import parse_stability
from poetry.semver.version_parser import VersionParser
from poetry.version import parse as parse_version
from .dependency import Dependency
from .vcs_dependency import VCSDependency
......@@ -41,7 +42,7 @@ class Package:
self._pretty_name = name
self._name = name.lower()
self._version = version
self._version = str(parse_version(version))
self._pretty_version = pretty_version or version
self.description = ''
......
......@@ -6,7 +6,6 @@ from .packages import Locker
from .packages import Package
from .repositories import Pool
from .repositories.pypi_repository import PyPiRepository
from .semver.helpers import normalize_version
from .utils.toml_file import TomlFile
......@@ -71,9 +70,8 @@ class Poetry:
# Load package
name = local_config['name']
pretty_version = local_config['version']
version = normalize_version(pretty_version)
package = Package(name, version, pretty_version)
version = local_config['version']
package = Package(name, version, version)
for author in local_config['authors']:
package.authors.append(author)
......
......@@ -2,9 +2,10 @@ import re
from poetry.semver.constraints import Constraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.helpers import normalize_version
from poetry.semver.version_parser import VersionParser
from poetry.version import parse as parse_version
from .base_repository import BaseRepository
......@@ -21,7 +22,7 @@ class Repository(BaseRepository):
def package(self, name, version):
name = name.lower()
version = normalize_version(version)
version = str(parse_version(version))
for package in self.packages:
if name == package.name and package.version == version:
......
import operator
from pkg_resources import parse_version
from poetry.version import parse as parse_version
from poetry.version import version_compare
from ..helpers import normalize_version
from .base_constraint import BaseConstraint
......@@ -43,7 +44,7 @@ class Constraint(BaseConstraint):
self._operator = self._trans_op_str[operator]
self._string_operator = operator
self._version = version
self._version = str(parse_version(version))
@property
def supported_operators(self) -> list:
......@@ -87,10 +88,7 @@ class Constraint(BaseConstraint):
except ValueError:
pass
return self._trans_op_str[operator](
parse_version(a),
parse_version(b)
)
return version_compare(a, b, operator)
def match_specific(self, provider: 'Constraint') -> bool:
no_equal_op = self._trans_op_int[self._operator].replace('=', '')
......
......@@ -70,8 +70,8 @@ class VersionParser:
and isinstance(or_groups[1], MultiConstraint)
and len(or_groups[0].constraints)
and len(or_groups[1].constraints)
and a[:3] == '[>=' and pos_a != -1
and b[:3] == '[>=' and pos_b != -1
and a[:3] == '>=' and pos_a != -1
and b[:3] == '>=' and pos_b != -1
and a[pos_a + 2:-1] == b[4:pos_b - 5]
):
constraint = MultiConstraint(
......@@ -144,7 +144,7 @@ class VersionParser:
# Allows changes that do not modify
# the left-most non-zero digit in the [major, minor, patch] tuple.
# In other words, this allows:
# - patch and minor updatesfor versions 1.0.0 and above,
# - patch and minor updates for versions 1.0.0 and above,
# - patch updates for versions 0.X >=0.1.0,
# - and no updates for versions 0.0.X
m = re.match('^\^{}($)'.format(version_regex), constraint)
......
import operator
from typing import Union
from .exceptions import InvalidVersion
from .legacy_version import LegacyVersion
from .version import Version
OP_EQ = operator.eq
OP_LT = operator.lt
OP_LE = operator.le
OP_GT = operator.gt
OP_GE = operator.ge
OP_NE = operator.ne
_trans_op = {
'=': OP_EQ,
'==': OP_EQ,
'<': OP_LT,
'<=': OP_LE,
'>': OP_GT,
'>=': OP_GE,
'!=': OP_NE
}
def parse(version: str, strict: bool = False) -> Union[Version, LegacyVersion]:
"""
Parse the given version string and return either a :class:`Version` object
or a LegacyVersion object depending on if the given version is
a valid PEP 440 version or a legacy version.
If strict=True only PEP 440 versions will be accepted.
"""
try:
return Version(version)
except InvalidVersion:
if strict:
raise
return LegacyVersion(version)
def version_compare(version1: str, version2: str, operator) -> bool:
if operator in _trans_op:
operator = _trans_op[operator]
elif operator in _trans_op.values():
pass
else:
raise ValueError('Invalid operator')
version1 = parse(version1)
version2 = parse(version2)
return operator(version1, version2)
class BaseVersion:
def __hash__(self):
return hash(self._key)
def __lt__(self, other):
return self._compare(other, lambda s, o: s < o)
def __le__(self, other):
return self._compare(other, lambda s, o: s <= o)
def __eq__(self, other):
return self._compare(other, lambda s, o: s == o)
def __ge__(self, other):
return self._compare(other, lambda s, o: s >= o)
def __gt__(self, other):
return self._compare(other, lambda s, o: s > o)
def __ne__(self, other):
return self._compare(other, lambda s, o: s != o)
def _compare(self, other, method):
if not isinstance(other, BaseVersion):
return NotImplemented
return method(self._key, other._key)
class InvalidVersion(ValueError):
pass
import re
from .base import BaseVersion
class LegacyVersion(BaseVersion):
def __init__(self, version):
self._version = str(version)
self._key = _legacy_cmpkey(self._version)
def __str__(self):
return self._version
def __repr__(self):
return "<LegacyVersion({0})>".format(repr(str(self)))
@property
def public(self):
return self._version
@property
def base_version(self):
return self._version
@property
def local(self):
return None
@property
def is_prerelease(self):
return False
@property
def is_postrelease(self):
return False
_legacy_version_component_re = re.compile(
r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE,
)
_legacy_version_replacement_map = {
"pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@",
}
def _parse_version_parts(s):
for part in _legacy_version_component_re.split(s):
part = _legacy_version_replacement_map.get(part, part)
if not part or part == ".":
continue
if part[:1] in "0123456789":
# pad for numeric comparison
yield part.zfill(8)
else:
yield "*" + part
# ensure that alpha/beta/candidate are before final
yield "*final"
def _legacy_cmpkey(version):
# We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
# greater than or equal to 0. This will effectively put the LegacyVersion,
# which uses the defacto standard originally implemented by setuptools,
# as before all PEP 440 versions.
epoch = -1
# This scheme is taken from pkg_resources.parse_version setuptools prior to
# it's adoption of the packaging library.
parts = []
for part in _parse_version_parts(version.lower()):
if part.startswith("*"):
# remove "-" before a prerelease tag
if part < "*final":
while parts and parts[-1] == "*final-":
parts.pop()
# remove trailing zeros from each series of numeric parts
while parts and parts[-1] == "00000000":
parts.pop()
parts.append(part)
parts = tuple(parts)
return epoch, parts
class Infinity(object):
def __repr__(self):
return "Infinity"
def __hash__(self):
return hash(repr(self))
def __lt__(self, other):
return False
def __le__(self, other):
return False
def __eq__(self, other):
return isinstance(other, self.__class__)
def __ne__(self, other):
return not isinstance(other, self.__class__)
def __gt__(self, other):
return True
def __ge__(self, other):
return True
def __neg__(self):
return NegativeInfinity
Infinity = Infinity()
class NegativeInfinity(object):
def __repr__(self):
return "-Infinity"
def __hash__(self):
return hash(repr(self))
def __lt__(self, other):
return True
def __le__(self, other):
return True
def __eq__(self, other):
return isinstance(other, self.__class__)
def __ne__(self, other):
return not isinstance(other, self.__class__)
def __gt__(self, other):
return False
def __ge__(self, other):
return False
def __neg__(self):
return Infinity
NegativeInfinity = NegativeInfinity()
import re
from collections import namedtuple
from itertools import dropwhile
from .base import BaseVersion
from .exceptions import InvalidVersion
from .utils import Infinity
_Version = namedtuple(
"_Version",
["epoch", "release", "dev", "pre", "post", "local"],
)
VERSION_PATTERN = re.compile("""
^
v?
(?:
(?:(?P<epoch>[0-9]+)!)? # epoch
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
(?P<pre> # pre-release
[-_.]?
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
[-_.]?
(?P<pre_n>[0-9]+)?
)?
(?P<post> # post release
(?:-(?P<post_n1>[0-9]+))
|
(?:
[-_.]?
(?P<post_l>post|rev|r)
[-_.]?
(?P<post_n2>[0-9]+)?
)
)?
(?P<dev> # dev release
[-_.]?
(?P<dev_l>dev)
[-_.]?
(?P<dev_n>[0-9]+)?
)?
)
(?:\+(?P<local>[a-z0-9]+(?:[-_.][a-z0-9]+)*))? # local version
$
""", re.IGNORECASE | re.VERBOSE)
class Version(BaseVersion):
def __init__(self, version):
# Validate the version and parse it into pieces
match = VERSION_PATTERN.match(version)
if not match:
raise InvalidVersion("Invalid version: '{0}'".format(version))
# Store the parsed out pieces of the version
self._version = _Version(
epoch=int(match.group("epoch")) if match.group("epoch") else 0,
release=tuple(int(i) for i in match.group("release").split(".")),
pre=_parse_letter_version(
match.group("pre_l"),
match.group("pre_n"),
),
post=_parse_letter_version(
match.group("post_l"),
match.group("post_n1") or match.group("post_n2"),
),
dev=_parse_letter_version(
match.group("dev_l"),
match.group("dev_n"),
),
local=_parse_local_version(match.group("local")),
)
# Generate a key which will be used for sorting
self._key = _cmpkey(
self._version.epoch,
self._version.release,
self._version.pre,
self._version.post,
self._version.dev,
self._version.local,
)
def __repr__(self):
return "<Version({0})>".format(repr(str(self)))
def __str__(self):
parts = []
# Epoch
if self._version.epoch != 0:
parts.append("{0}!".format(self._version.epoch))
# Release segment
parts.append(".".join(str(x) for x in self._version.release))
# Pre-release
if self._version.pre is not None:
parts.append("".join(str(x) for x in self._version.pre))
# Post-release
if self._version.post is not None:
parts.append(".post{0}".format(self._version.post[1]))
# Development release
if self._version.dev is not None:
parts.append(".dev{0}".format(self._version.dev[1]))
# Local version segment
if self._version.local is not None:
parts.append(
"+{0}".format(".".join(str(x) for x in self._version.local))
)
return "".join(parts)
@property
def public(self):
return str(self).split("+", 1)[0]
@property
def base_version(self):
parts = []
# Epoch
if self._version.epoch != 0:
parts.append("{0}!".format(self._version.epoch))
# Release segment
parts.append(".".join(str(x) for x in self._version.release))
return "".join(parts)
@property
def local(self):
version_string = str(self)
if "+" in version_string:
return version_string.split("+", 1)[1]
@property
def is_prerelease(self):
return bool(self._version.dev or self._version.pre)
@property
def is_postrelease(self):
return bool(self._version.post)
def _parse_letter_version(letter, number):
if letter:
# We consider there to be an implicit 0 in a pre-release if there is
# not a numeral associated with it.
if number is None:
number = 0
# We normalize any letters to their lower case form
letter = letter.lower()
# We consider some words to be alternate spellings of other words and
# in those cases we want to normalize the spellings to our preferred
# spelling.
if letter == "alpha":
letter = "a"
elif letter == "beta":
letter = "b"
elif letter in ["c", "pre", "preview"]:
letter = "rc"
elif letter in ["rev", "r"]:
letter = "post"
return letter, int(number)
if not letter and number:
# We assume if we are given a number, but we are not given a letter
# then this is using the implicit post release syntax (e.g. 1.0-1)
letter = "post"
return letter, int(number)
_local_version_seperators = re.compile(r"[._-]")
def _parse_local_version(local):
"""
Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
"""
if local is not None:
return tuple(
part.lower() if not part.isdigit() else int(part)
for part in _local_version_seperators.split(local)
)
def _cmpkey(epoch, release, pre, post, dev, local):
# When we compare a release version, we want to compare it with all of the
# trailing zeros removed. So we'll use a reverse the list, drop all the now
# leading zeros until we come to something non zero, then take the rest
# re-reverse it back into the correct order and make it a tuple and use
# that for our sorting key.
release = tuple(
reversed(list(
dropwhile(
lambda x: x == 0,
reversed(release),
)
))
)
# We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
# We'll do this by abusing the pre segment, but we _only_ want to do this
# if there is not a pre or a post segment. If we have one of those then
# the normal sorting rules will handle this case correctly.
if pre is None and post is None and dev is not None:
pre = -Infinity
# Versions without a pre-release (except as noted above) should sort after
# those with one.
elif pre is None:
pre = Infinity
# Versions without a post segment should sort before those with one.
if post is None:
post = -Infinity
# Versions without a development segment should sort after those with one.
if dev is None:
dev = Infinity
if local is None:
# Versions without a local segment should sort before those with one.
local = -Infinity
else:
# Versions with a local segment need that segment parsed to implement
# the sorting rules in PEP440.
# - Alpha numeric segments sort before numeric segments
# - Alpha numeric segments sort lexicographically
# - Numeric segments sort numerically
# - Shorter versions sort before longer versions when the prefixes
# match exactly
local = tuple(
(i, "") if isinstance(i, int) else (-Infinity, i)
for i in local
)
return epoch, release, pre, post, dev, local
"""Example module"""
__version__ = '0.1'
[tool.poetry]
name = "prerelease"
version = "0.1-beta.1"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io/"
......@@ -116,3 +116,14 @@ def test_package():
sdist = fixtures_dir / 'complete' / 'dist' / 'my-package-1.2.3.tar.gz'
assert sdist.exists()
def test_prelease():
poetry = Poetry.create(project('prerelease'))
builder = SdistBuilder(poetry, NullIO())
builder.build()
sdist = fixtures_dir / 'prerelease' / 'dist' / 'prerelease-0.1b1.tar.gz'
assert sdist.exists()
......@@ -42,3 +42,12 @@ def test_wheel_package():
whl = module_path / 'dist' / 'my_package-1.2.3-py3-none-any.whl'
assert whl.exists()
def test_wheel_prerelease():
module_path = fixtures_dir / 'prerelease'
WheelBuilder.make(Poetry.create(str(module_path)), NullIO())
whl = module_path / 'dist' / 'prerelease-0.1b1-py2.py3-none-any.whl'
assert whl.exists()
......@@ -12,7 +12,7 @@ def test_poetry():
package = poetry.package
assert package.name == 'my-package'
assert package.version == '1.2.3.0'
assert package.version == '1.2.3'
assert package.description == 'Some description.'
assert package.authors == ['Sébastien Eustace <sebastien@eustace.io>']
assert package.license == 'MIT'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment