Skip to content

Commit 32c230d

Browse files
authored
refactor poetry tilde caret operators remove semver (#596)
1 parent 2485e77 commit 32c230d

File tree

5 files changed

+325
-659
lines changed

5 files changed

+325
-659
lines changed

environment.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,3 @@ dependencies:
2828
- tomli-w
2929
- libcblas
3030
- beautifulsoup4
31-
- semver >=3.0.0,<4.0.0

grayskull/strategy/parse_poetry_version.py

+76-55
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import re
22

3-
import semver
43
from packaging.version import Version
54

65
VERSION_REGEX = re.compile(
@@ -45,30 +44,49 @@ def parse_version(version: str) -> dict[str, int | None]:
4544
}
4645

4746

48-
def vdict_to_vinfo(version_dict: dict[str, int | None]) -> semver.VersionInfo:
47+
def get_padded_base_version(version: str | Version) -> str:
4948
"""
50-
Coerces version dictionary to a semver.VersionInfo object. If minor or patch
51-
numbers are missing, 0 is substituted in their place.
52-
"""
53-
ver = {key: 0 if value is None else value for key, value in version_dict.items()}
54-
return semver.VersionInfo(**ver)
55-
49+
Returns the same PEP440 version padded with zeroes if
50+
minor or micro are not specified.
5651
57-
def coerce_to_semver(version: str) -> str:
58-
"""
59-
Coerces a version string to a semantic version.
52+
>>> get_padded_base_version("0.2.3")
53+
'0.2.3'
54+
>>> get_padded_base_version("1")
55+
'1.0.0'
56+
>>> get_padded_base_version("1.2")
57+
'1.2.0'
58+
>>> get_padded_base_version("1.2.3")
59+
'1.2.3'
60+
>>> get_padded_base_version("1.2.3.post1")
61+
'1.2.3.post1'
62+
>>> get_padded_base_version("2!1.2.post1")
63+
'2!1.2.0.post1'
6064
"""
61-
if semver.VersionInfo.is_valid(version):
62-
return version
63-
64-
parsed_version = parse_version(version)
65-
vinfo = vdict_to_vinfo(parsed_version)
66-
return str(vinfo)
67-
65+
if not isinstance(version, Version):
66+
version = Version(version)
6867

69-
def get_caret_ceiling(target: str) -> str:
68+
# Start with the normalized release
69+
floor = f"{version.major}.{version.minor}.{version.micro}"
70+
71+
# Add other components as they appear
72+
if version.epoch is not None and version.epoch > 0:
73+
floor = f"{version.epoch}!{floor}" # Add epoch if present
74+
if version.pre is not None:
75+
floor += (
76+
f"{version.pre[0]}{version.pre[1]}" # Add pre-release (e.g., a1, b1, rc1)
77+
)
78+
if version.post is not None:
79+
floor += f".post{version.post}" # Add post-release (e.g., .post1)
80+
if version.dev is not None:
81+
floor += f".dev{version.dev}" # Add development release (e.g., .dev1)
82+
if version.local is not None:
83+
floor += f"+{version.local}" # Add local metadata (e.g., +local)
84+
return floor
85+
86+
87+
def get_caret_ceiling(version: str | Version) -> str:
7088
"""
71-
Accepts a Poetry caret target and returns the exclusive version ceiling.
89+
Accepts a Poetry caret version and returns the exclusive version ceiling.
7290
7391
Targets that are invalid semver strings (e.g. "1.2", "0") are handled
7492
according to the Poetry caret requirements specification, which is based on
@@ -97,34 +115,24 @@ def get_caret_ceiling(target: str) -> str:
97115
'2.0.0'
98116
>>> get_caret_ceiling("1.2.3")
99117
'2.0.0'
118+
>>> get_caret_ceiling("1.2.3.post1")
119+
'2.0.0'
120+
>>> get_caret_ceiling("2!1.2.3.post1")
121+
'2.0.0'
100122
"""
101-
if not semver.VersionInfo.is_valid(target):
102-
target_dict = parse_version(target)
103-
104-
if target_dict["major"] == 0:
105-
if target_dict["minor"] is None:
106-
target_dict["major"] += 1
107-
elif target_dict["patch"] is None:
108-
target_dict["minor"] += 1
109-
else:
110-
target_dict["patch"] += 1
111-
return str(vdict_to_vinfo(target_dict))
112-
113-
vdict_to_vinfo(target_dict)
114-
return str(vdict_to_vinfo(target_dict).bump_major())
115-
116-
target_vinfo = semver.VersionInfo.parse(target)
117-
118-
if target_vinfo.major == 0:
119-
if target_vinfo.minor == 0:
120-
return str(target_vinfo.bump_patch())
121-
else:
122-
return str(target_vinfo.bump_minor())
123+
if not isinstance(version, Version):
124+
version = Version(version)
125+
# Determine the upper bound
126+
if version.major > 0 or len(version.release) == 1:
127+
ceiling = f"{version.major + 1}.0.0"
128+
elif version.minor > 0 or len(version.release) == 2:
129+
ceiling = f"0.{version.minor + 1}.0"
123130
else:
124-
return str(target_vinfo.bump_major())
131+
ceiling = f"0.0.{version.micro + 1}"
132+
return ceiling
125133

126134

127-
def get_tilde_ceiling(target: str) -> str:
135+
def get_tilde_ceiling(version: str | Version) -> str:
128136
"""
129137
Accepts a Poetry tilde target and returns the exclusive version ceiling.
130138
@@ -136,11 +144,14 @@ def get_tilde_ceiling(target: str) -> str:
136144
>>> get_tilde_ceiling("1.2.3")
137145
'1.3.0'
138146
"""
139-
target_dict = parse_version(target)
140-
if target_dict["minor"]:
141-
return str(vdict_to_vinfo(target_dict).bump_minor())
142-
143-
return str(vdict_to_vinfo(target_dict).bump_major())
147+
if not isinstance(version, Version):
148+
version = Version(version)
149+
# Determine the upper bound based on the specified components
150+
if len(version.release) in [2, 3]: # Major, Minor, Micro, or Major, Minor
151+
tilde_ceiling = f"{version.major}.{version.minor + 1}.0"
152+
else: # Major, Minor or Only Major
153+
tilde_ceiling = f"{version.major + 1}.0.0"
154+
return tilde_ceiling
144155

145156

146157
def encode_poetry_version(poetry_specifier: str) -> str:
@@ -190,6 +201,11 @@ def encode_poetry_version(poetry_specifier: str) -> str:
190201
>>> encode_poetry_version("^1.2.3")
191202
'>=1.2.3,<2.0.0'
192203
204+
# handle caret operator with a PEP440 version
205+
# correctly
206+
>>> encode_poetry_version("^0.8.post1")
207+
'>=0.8.0.post1,<0.9.0'
208+
193209
# handle tilde operator correctly
194210
# examples from Poetry docs
195211
>>> encode_poetry_version("~1")
@@ -199,6 +215,11 @@ def encode_poetry_version(poetry_specifier: str) -> str:
199215
>>> encode_poetry_version("~1.2.3")
200216
'>=1.2.3,<1.3.0'
201217
218+
# # handle tilde operator with a PEP440 version
219+
# # correctly
220+
# >>> encode_poetry_version("~0.8.post1")
221+
# '>=0.8.0.post1,<0.9.0.a0'
222+
202223
# handle or operator correctly
203224
>>> encode_poetry_version("1.2.3|1.2.4")
204225
'1.2.3|1.2.4'
@@ -221,9 +242,9 @@ def encode_poetry_version(poetry_specifier: str) -> str:
221242
poetry_clause = poetry_clause.replace(" ", "")
222243
if poetry_clause.startswith("^"):
223244
# handle ^ operator
224-
target = poetry_clause[1:]
225-
floor = coerce_to_semver(target)
226-
ceiling = get_caret_ceiling(target)
245+
caret_version = Version(poetry_clause[1:])
246+
floor = get_padded_base_version(caret_version)
247+
ceiling = get_caret_ceiling(caret_version)
227248
conda_clauses.append(">=" + floor)
228249
conda_clauses.append("<" + ceiling)
229250
continue
@@ -236,9 +257,9 @@ def encode_poetry_version(poetry_specifier: str) -> str:
236257

237258
if poetry_clause.startswith("~"):
238259
# handle ~ operator
239-
target = poetry_clause[1:]
240-
floor = coerce_to_semver(target)
241-
ceiling = get_tilde_ceiling(target)
260+
tilde_version = poetry_clause[1:]
261+
floor = get_padded_base_version(tilde_version)
262+
ceiling = get_tilde_ceiling(tilde_version)
242263
conda_clauses.append(">=" + floor)
243264
conda_clauses.append("<" + ceiling)
244265
continue

0 commit comments

Comments
 (0)