11#!/usr/bin/env python
22
33import argparse
4+ from urllib import request
45import re
56
67import 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+
160332def 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