Skip to content

Commit 24b416e

Browse files
Fix python selectors and delimiters (sagemath#12)
1 parent 2d8f96c commit 24b416e

File tree

2 files changed

+112
-32
lines changed

2 files changed

+112
-32
lines changed

grayskull/pypi/pypi.py

+90-32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import re
3-
from collections import OrderedDict, namedtuple
3+
from collections import namedtuple
4+
from typing import Dict, List, Optional, Tuple
45

56
import requests
67
from requests import HTTPError
@@ -25,9 +26,10 @@ def __init__(self, name=None, version=None, force_setup=False):
2526
super(PyPi, self).__init__(name=name, version=version)
2627

2728
def _populate_fields_by_distutils(self):
29+
# TODO: Implement injection in distutils when there is no PyPi metadata
2830
pass
2931

30-
def refresh_section(self, section="", force_distutils=False):
32+
def refresh_section(self, section: str = "", force_distutils: bool = False):
3133
if self._force_setup or force_distutils:
3234
self._populate_fields_by_distutils()
3335
return
@@ -38,7 +40,7 @@ def refresh_section(self, section="", force_distutils=False):
3840
self._force_setup = True
3941
self.refresh_section(section, force_distutils=True)
4042

41-
def _get_pypi_metadata(self):
43+
def _get_pypi_metadata(self) -> dict:
4244
if not self.package.version:
4345
log.info(
4446
f"Version for {self.package.name} not specified.\n"
@@ -85,36 +87,28 @@ def _get_pypi_metadata(self):
8587
return self._pypi_metadata
8688

8789
@staticmethod
88-
def get_sha256_from_pypi_metadata(pypi_metadata):
90+
def get_sha256_from_pypi_metadata(pypi_metadata: dict) -> str:
8991
for pkg_info in pypi_metadata.get("urls"):
9092
if pkg_info.get("packagetype", "") == "sdist":
9193
return pkg_info["digests"]["sha256"]
9294
raise ValueError("Hash information for sdist was not found on PyPi metadata.")
9395

94-
def _extract_pypi_requirements(self, metadata):
96+
def _extract_pypi_requirements(self, metadata: dict) -> Requirements:
9597
if not metadata["info"].get("requires_dist"):
9698
return Requirements(host=["python", "pip"], run=["python"])
97-
limit_python = metadata["info"].get("requires_python", "")
98-
if limit_python is None:
99-
limit_python = ""
10099
run_req = []
101100

102101
for req in metadata["info"].get("requires_dist"):
103102
list_raw_requirements = req.split(";")
104103

105104
selector = ""
106105
if len(list_raw_requirements) > 1:
107-
self._is_using_selectors = True
108-
if limit_python:
109-
self.build.skip = f"true {PyPi.py_version_to_selector(metadata)}"
110-
else:
111-
self.build.skip = None
112-
limit_python = ""
113106
option, operation, value = PyPi._get_extra_from_requires_dist(
114107
list_raw_requirements[1]
115108
)
116-
if value == "testing":
109+
if option == "extra" or value == "testing":
117110
continue
111+
self._is_using_selectors = True
118112
selector = PyPi._parse_extra_metadata_to_selector(
119113
option, operation, value
120114
)
@@ -123,19 +117,39 @@ def _extract_pypi_requirements(self, metadata):
123117
)
124118
run_req.append(f"{pkg_name} {version}{selector}".strip())
125119

120+
limit_python = metadata["info"].get("requires_python", "")
121+
if limit_python and self._is_using_selectors:
122+
self.build.skip = f"true {PyPi.py_version_to_selector(metadata)}"
123+
limit_python = ""
124+
else:
125+
self.build.skip = None
126+
limit_python = PyPi.py_version_to_limit_python(metadata)
127+
126128
host_req = [f"python{limit_python}", "pip"]
127129
run_req.insert(0, f"python{limit_python}")
128130
return Requirements(host=host_req, run=run_req)
129131

130132
@staticmethod
131-
def _get_extra_from_requires_dist(string_parse):
133+
def _get_extra_from_requires_dist(string_parse: str) -> Tuple[str, str, str]:
134+
"""Receives the extra metadata e parse it to get the option, operation
135+
and value.
136+
137+
:param string_parse: metadata extra
138+
:return: return the option , operation and value of the extra metadata
139+
"""
132140
option, operation, value = re.match(
133141
r"^\s*(\w+)\s+(\W*)\s+(.*)", string_parse, re.DOTALL
134142
).groups()
135143
return option, operation, re.sub(r"['\"]", "", value)
136144

137145
@staticmethod
138-
def _get_name_version_from_requires_dist(string_parse):
146+
def _get_name_version_from_requires_dist(string_parse: str) -> Tuple[str, str]:
147+
"""Extract the name and the version from `requires_dist` present in
148+
PyPi`s metadata
149+
150+
:param string_parse: requires_dist value from PyPi metadata
151+
:return: Name and version of a package
152+
"""
139153
pkg = re.match(r"^\s*([^\s]+)\s*(\(.*\))?\s*", string_parse, re.DOTALL)
140154
pkg_name = pkg.group(1).strip()
141155
version = ""
@@ -144,7 +158,17 @@ def _get_name_version_from_requires_dist(string_parse):
144158
return pkg_name.strip(), re.sub(r"[\(\)]", "", version).strip()
145159

146160
@staticmethod
147-
def py_version_to_selector(pypi_metadata):
161+
def _generic_py_ver_to(
162+
pypi_metadata: dict, is_selector: bool = False
163+
) -> Optional[str]:
164+
"""Generic function which abstract the parse of the requires_python
165+
present in the PyPi metadata. Basically it can generate the selectors
166+
for Python or the delimiters if it is a `noarch: python` python package
167+
168+
:param pypi_metadata: PyPi metadata
169+
:param is_selector:
170+
:return:
171+
"""
148172
req_python = re.findall(
149173
r"([><=!]+)\s*(\d+)(?:\.(\d+))?", pypi_metadata["info"]["requires_python"],
150174
)
@@ -156,24 +180,52 @@ def py_version_to_selector(pypi_metadata):
156180
if all(all_py):
157181
return None
158182
if all(all_py[1:]):
159-
return "# [py2k]"
183+
return (
184+
"# [py2k]"
185+
if is_selector
186+
else f">={SUPPORTED_PY[1].major}.{SUPPORTED_PY[1].minor}"
187+
)
160188
if py_ver_enabled.get(PyVer(2, 7)) and any(all_py[1:]) is False:
161-
return "# [py3k]"
189+
return "# [py3k]" if is_selector else "<3.0"
162190

163191
for pos, py_ver in enumerate(SUPPORTED_PY[1:], 1):
164192
if all(all_py[pos:]) and any(all_py[:pos]) is False:
165-
return f"# [py<{py_ver.major}{py_ver.minor}]"
193+
return (
194+
f"# [py<{py_ver.major}{py_ver.minor}]"
195+
if is_selector
196+
else f">={py_ver.major}.{py_ver.minor}"
197+
)
166198
elif any(all_py[pos:]) is False:
167-
return f"# [py>={py_ver.major}{py_ver.minor}]"
199+
return (
200+
f"# [py>={py_ver.major}{py_ver.minor}]"
201+
if is_selector
202+
else f"<{py_ver.major}.{py_ver.minor}"
203+
)
168204

169-
all_selector = PyPi._get_multiple_selectors(py_ver_enabled)
205+
all_selector = PyPi._get_multiple_selectors(
206+
py_ver_enabled, is_selector=is_selector
207+
)
170208
if all_selector:
171-
return "# [{}]".format(" or ".join(all_selector))
209+
return (
210+
"# [{}]".format(" or ".join(all_selector))
211+
if is_selector
212+
else ",".join(all_selector)
213+
)
172214
return None
173215

174216
@staticmethod
175-
def _get_py_version_available(req_python):
176-
py_ver_enabled = OrderedDict([(py_ver, True) for py_ver in SUPPORTED_PY])
217+
def py_version_to_limit_python(pypi_metadata: dict) -> Optional[str]:
218+
return PyPi._generic_py_ver_to(pypi_metadata, is_selector=False)
219+
220+
@staticmethod
221+
def py_version_to_selector(pypi_metadata: dict) -> Optional[str]:
222+
return PyPi._generic_py_ver_to(pypi_metadata, is_selector=True)
223+
224+
@staticmethod
225+
def _get_py_version_available(
226+
req_python: List[Tuple[str, str, str]]
227+
) -> Dict[PyVer, bool]:
228+
py_ver_enabled = {py_ver: True for py_ver in SUPPORTED_PY}
177229
for op, major, minor in req_python:
178230
if not minor:
179231
minor = 0
@@ -186,18 +238,24 @@ def _get_py_version_available(req_python):
186238
return py_ver_enabled
187239

188240
@staticmethod
189-
def _get_multiple_selectors(result):
241+
def _get_multiple_selectors(selectors: Dict[PyVer, bool], is_selector=False):
190242
all_selector = []
191-
if result[PyVer(2, 7)] is False:
192-
all_selector.append("py2k")
193-
for py_ver, is_enabled in result.items():
243+
if selectors[PyVer(2, 7)] is False:
244+
all_selector += ["py2k"] if is_selector else [">3.0"]
245+
for py_ver, is_enabled in selectors.items():
194246
if py_ver == PyVer(2, 7) or is_enabled:
195247
continue
196-
all_selector.append(f"py=={py_ver.major}{py_ver.minor}")
248+
all_selector += (
249+
[f"py=={py_ver.major}{py_ver.minor}"]
250+
if is_selector
251+
else [f"!={py_ver.major}.{py_ver.minor}"]
252+
)
197253
return all_selector
198254

199255
@staticmethod
200-
def _parse_extra_metadata_to_selector(option, operation, value):
256+
def _parse_extra_metadata_to_selector(
257+
option: str, operation: str, value: str
258+
) -> str:
201259
if option == "extra":
202260
return ""
203261
if option == "python_version":

tests/test_pypi.py

+22
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,28 @@ def test_py_version_to_selector(requires_python, exp_selector):
9393
assert PyPi.py_version_to_selector(metadata) == f"# [py{exp_selector}]"
9494

9595

96+
@pytest.mark.parametrize(
97+
"requires_python, exp_limit",
98+
[
99+
(">=3.5", ">=3.6"),
100+
(">=3.6", ">=3.6"),
101+
(">=3.7", ">=3.7"),
102+
("<=3.7", "<3.8"),
103+
("<=3.7.1", "<3.8"),
104+
("<3.7", "<3.7"),
105+
(">2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", ">=3.6"),
106+
(">=2.7, !=3.6.*", "!=3.6"),
107+
(">3.7", ">=3.8"),
108+
(">2.7", ">=3.6"),
109+
("<3", "<3.0"),
110+
("!=3.7", "!=3.7"),
111+
],
112+
)
113+
def test_py_version_to_limit_python(requires_python, exp_limit):
114+
metadata = {"info": {"requires_python": requires_python}}
115+
assert PyPi.py_version_to_limit_python(metadata) == f"{exp_limit}"
116+
117+
96118
def test_get_sha256_from_pypi_metadata():
97119
metadata = {
98120
"urls": [

0 commit comments

Comments
 (0)