Skip to content

Commit d815750

Browse files
committed
Added mode to identify max package versions
1 parent 9f14b02 commit d815750

File tree

1 file changed

+202
-12
lines changed

1 file changed

+202
-12
lines changed

min_versions.py renamed to versions.py

Lines changed: 202 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python
22

33
import argparse
4+
from urllib import request
45
import re
56

67
import pandas as pd
@@ -88,23 +89,32 @@ def check_scipy_compatibility(row, min_python, min_numpy):
8889
return python_compatible & numpy_compatible
8990

9091

91-
def get_min_scipy_version(min_python, min_numpy):
92+
def get_scipy_version_df():
9293
"""
93-
Determine the SciPy version compatibility
94+
Retrieve raw SciPy version table as DataFrame
9495
"""
9596
colnames = pd.read_html(
9697
"https://docs.scipy.org/doc/scipy/dev/toolchain.html#numpy",
9798
storage_options=HEADERS,
9899
)[1].columns
99100
converter = {colname: str for colname in colnames}
100-
df = (
101+
return (
101102
pd.read_html(
102103
"https://docs.scipy.org/doc/scipy/dev/toolchain.html#numpy",
103104
storage_options=HEADERS,
104105
converters=converter,
105106
)[1]
106107
.rename(columns=lambda x: x.replace(" ", "_"))
107108
.replace({".x": ""}, regex=True)
109+
)
110+
111+
112+
def get_min_scipy_version(min_python, min_numpy):
113+
"""
114+
Determine the SciPy version compatibility
115+
"""
116+
df = (
117+
get_scipy_version_df()
108118
.pipe(
109119
lambda df: df.assign(
110120
SciPy_version=df.SciPy_version.str.replace(
@@ -157,6 +167,168 @@ def get_min_scipy_version(min_python, min_numpy):
157167
return df.SciPy_version
158168

159169

170+
def get_minor_versions_between(start_version_str, end_version_str):
171+
"""
172+
Returns a list of all minor Python versions between two specified minor versions.
173+
Assumes semantic versioning (MAJOR.MINOR.PATCH) and only considers minor versions
174+
within the same major version.
175+
176+
Args:
177+
start_version_str (str): The starting version string (e.g., "3.6.0").
178+
end_version_str (str): The ending version string (e.g., "3.9.5").
179+
180+
Returns:
181+
list: A list of strings representing the minor versions in between,
182+
including the start and end minor versions if they are distinct.
183+
Returns an empty list if the start version is greater than or equal
184+
to the end version, or if major versions differ.
185+
"""
186+
try:
187+
start_parts = [int(x) for x in start_version_str.split('.')]
188+
end_parts = [int(x) for x in end_version_str.split('.')]
189+
except ValueError:
190+
raise ValueError("Invalid version string format. Expected 'MAJOR.MINOR.PATCH'.")
191+
192+
if len(start_parts) < 2 or len(end_parts) < 2:
193+
raise ValueError("Version string must include at least major and minor parts.")
194+
195+
start_major, start_minor = start_parts[0], start_parts[1]
196+
end_major, end_minor = end_parts[0], end_parts[1]
197+
198+
if start_major != end_major:
199+
print("Warning: Major versions differ. Returning an empty list.")
200+
return []
201+
202+
if start_minor >= end_minor:
203+
print("Warning: Start minor version is not less than end minor version. Returning an empty list.")
204+
return []
205+
206+
versions = []
207+
for minor in range(start_minor, end_minor + 1):
208+
versions.append(f"{start_major}.{minor}")
209+
210+
return versions
211+
212+
213+
def get_latest_numpy_version():
214+
url = "https://pypi.org/project/numpy/"
215+
req = request.Request(
216+
url,
217+
data=None,
218+
headers=HEADERS
219+
)
220+
html = request.urlopen(req).read().decode("utf-8")
221+
match = re.search(r'numpy (\d+\.\d+\.\d+)', html, re.DOTALL)
222+
return match.groups()[0]
223+
224+
225+
def check_python_version(row):
226+
versions = get_minor_versions_between(row.START_PYTHON_VERSION, row.END_PYTHON_VERSION)
227+
228+
compatible_version = None
229+
for version in versions:
230+
if Version(version) in row.NUMBA_PYTHON_SPEC & row.SCIPY_PYTHON_SPEC:
231+
compatible_version = version
232+
return compatible_version
233+
234+
def check_numpy_version(row):
235+
if row.NUMPY in row.NUMPY_SPEC:
236+
return row.NUMPY
237+
else:
238+
return None
239+
240+
241+
def get_all_max_versions():
242+
"""
243+
Find the maximum version of Python that is compatible with Numba and NumPy
244+
"""
245+
df = (
246+
pd.read_html(
247+
"https://numba.readthedocs.io/en/stable/user/installing.html#version-support-information", # noqa
248+
storage_options=HEADERS,
249+
)[0]
250+
.dropna()
251+
.drop(columns=["Numba.1", "llvmlite", "LLVM", "TBB"])
252+
.query('`Python`.str.contains("2.7") == False')
253+
.query('`Numba`.str.contains(".x") == False')
254+
.query('`Numba`.str.contains("{") == False')
255+
.pipe(
256+
lambda df: df.assign(
257+
START_PYTHON_VERSION=(
258+
df.Python.str.split().str[0].replace({".x": ""}, regex=True)
259+
)
260+
)
261+
)
262+
.pipe(
263+
lambda df: df.assign(
264+
END_PYTHON_VERSION=(
265+
df.Python.str.split().str[4].replace({".x": ""}, regex=True)
266+
)
267+
)
268+
)
269+
.pipe(
270+
lambda df: df.assign(
271+
NUMBA_PYTHON_SPEC=(
272+
df.Python.str.split().str[1].replace({"<": ">"}, regex=True)
273+
+ df.Python.str.split().str[0].replace({".x": ""}, regex=True)
274+
+ ", "
275+
+ df.Python.str.split().str[3]
276+
+ df.Python.str.split().str[4].replace({".x": ""}, regex=True)
277+
).apply(SpecifierSet)
278+
)
279+
)
280+
.assign(
281+
NUMPY = get_latest_numpy_version()
282+
)
283+
.pipe(
284+
lambda df: df.assign(
285+
NUMPY_SPEC=(
286+
df.NumPy
287+
.str.replace(r' [;†\.]$', '', regex=True)
288+
.str.split().str[-2:].replace({".x": ""}, regex=True)
289+
.str.join('')
290+
).apply(SpecifierSet)
291+
)
292+
)
293+
.assign(
294+
NUMPY=lambda row: row.apply(
295+
check_numpy_version, axis=1
296+
)
297+
)
298+
.assign(
299+
SCIPY = get_scipy_version_df().iloc[0].SciPy_version
300+
)
301+
.assign(
302+
SCIPY_PYTHON_SPEC = get_scipy_version_df().iloc[0].Python_versions
303+
)
304+
.pipe(
305+
lambda df:
306+
df.assign(
307+
SCIPY_PYTHON_SPEC = df.SCIPY_PYTHON_SPEC.apply(SpecifierSet)
308+
)
309+
)
310+
.assign(
311+
MAX_PYTHON=lambda row: row.apply(
312+
check_python_version, axis=1
313+
)
314+
)
315+
.pipe(lambda df: df.assign(MAJOR=df.MAX_PYTHON.str.split(".").str[0]))
316+
.pipe(lambda df: df.assign(MINOR=df.MAX_PYTHON.str.split(".").str[1]))
317+
.sort_values(["MAJOR", "MINOR"], ascending=[False, False])
318+
.iloc[0]
319+
)
320+
321+
print(
322+
f"python: {df.MAX_PYTHON}\n"
323+
f"numba: {df.Numba}\n"
324+
f"numpy: {df.NUMPY}\n"
325+
f"scipy: {df.SCIPY}"
326+
)
327+
328+
329+
330+
331+
160332
def match_pkg_version(line, pkg_name):
161333
"""
162334
Regular expression to match package versions
@@ -226,15 +398,7 @@ def test_pkg_mismatch_regex():
226398
raise ValueError(f'Package mismatch regex fails to cover/match "{line}"')
227399

228400

229-
if __name__ == "__main__":
230-
parser = argparse.ArgumentParser()
231-
parser.add_argument("min_python", nargs="?", default=None)
232-
args = parser.parse_args()
233-
234-
if args.min_python is not None:
235-
MIN_PYTHON = str(args.min_python)
236-
else:
237-
MIN_PYTHON = get_min_python_version()
401+
def get_all_min_versions(MIN_PYTHON):
238402
MIN_NUMBA, MIN_NUMPY = get_min_numba_numpy_version(MIN_PYTHON)
239403
MIN_SCIPY = get_min_scipy_version(MIN_PYTHON, MIN_NUMPY)
240404

@@ -271,3 +435,29 @@ def test_pkg_mismatch_regex():
271435
f"{pkg_name} {pkg_version} Mismatch: Version {version} "
272436
f"found in {fname}:{line_num}"
273437
)
438+
439+
440+
441+
if __name__ == "__main__":
442+
parser = argparse.ArgumentParser()
443+
parser.add_argument("-mode", type=str, default='min', help='Options: ["min", "max"]')
444+
parser.add_argument("python_version", nargs="?", default=None)
445+
args = parser.parse_args()
446+
# Example
447+
# ./versions.py
448+
# ./versions.py 3.11
449+
# ./versions.py -mode max
450+
451+
print(f'mode: {args.mode}')
452+
453+
if args.mode == 'min':
454+
if args.python_version is not None:
455+
MIN_PYTHON = str(args.python_version)
456+
else:
457+
MIN_PYTHON = get_min_python_version()
458+
get_all_min_versions(MIN_PYTHON)
459+
elif args.mode == 'max':
460+
get_all_max_versions()
461+
else:
462+
raise ValueError(f'Unrecognized mode: "{args.mode}"')
463+

0 commit comments

Comments
 (0)