22Python packaging operations, including PEP-517 support, for use by a `setup.py`
33script.
44
5- The intention is to take care of as many packaging details as possible so that
6- setup.py contains only project-specific information, while also giving as much
7- flexibility as possible.
5+ Overview:
86
9- For example we provide a function `build_extension()` that can be used to build
10- a SWIG extension, but we also give access to the located compiler/linker so
11- that a `setup.py` script can take over the details itself .
7+ The intention is to take care of as many packaging details as possible so
8+ that setup.py contains only project-specific information, while also giving
9+ as much flexibility as possible .
1210
13- Run doctests with: `python -m doctest pipcl.py`
11+ For example we provide a function `build_extension()` that can be used
12+ to build a SWIG extension, but we also give access to the located
13+ compiler/linker so that a `setup.py` script can take over the details
14+ itself.
1415
15- For Graal we require that PIPCL_GRAAL_PYTHON is set to non-graal Python (we
16- build for non-graal except with Graal Python's include paths and library
17- directory).
16+ Doctests:
17+ Doctest strings are provided in some comments.
18+
19+ Test in the usual way with:
20+ python -m doctest pipcl.py
21+
22+ Test specific functions/classes with:
23+ python pipcl.py --doctest run_if ...
24+
25+ If no functions or classes are specified, this tests everything.
26+
27+ Graal:
28+ For Graal we require that PIPCL_GRAAL_PYTHON is set to non-graal Python (we
29+ build for non-graal except with Graal Python's include paths and library
30+ directory).
1831'''
1932
2033import base64
@@ -532,6 +545,12 @@ def assert_str_or_multi( v):
532545 assert_str_or_multi ( requires_external )
533546 assert_str_or_multi ( project_url )
534547 assert_str_or_multi ( provides_extra )
548+
549+ assert re .match ('^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])\\ Z' , name , re .IGNORECASE ), (
550+ f'Invalid package name'
551+ f' (https://packaging.python.org/en/latest/specifications/name-normalization/)'
552+ f': { name !r} '
553+ )
535554
536555 # https://packaging.python.org/en/latest/specifications/core-metadata/.
537556 assert re .match ('([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$' , name , re .IGNORECASE ), \
@@ -761,7 +780,7 @@ def build_sdist(self,
761780 else :
762781 items = self .fn_sdist ()
763782
764- prefix = f'{ _normalise (self .name )} -{ self .version } '
783+ prefix = f'{ _normalise2 (self .name )} -{ self .version } '
765784 os .makedirs (sdist_directory , exist_ok = True )
766785 tarpath = f'{ sdist_directory } /{ prefix } .tar.gz'
767786 log2 (f'Creating sdist: { tarpath } ' )
@@ -833,9 +852,11 @@ def tag_python(self):
833852 Get two-digit python version, e.g. 'cp3.8' for python-3.8.6.
834853 '''
835854 if self .tag_python_ :
836- return self .tag_python_
855+ ret = self .tag_python_
837856 else :
838- return 'cp' + '' .join (platform .python_version ().split ('.' )[:2 ])
857+ ret = 'cp' + '' .join (platform .python_version ().split ('.' )[:2 ])
858+ assert '-' not in ret
859+ return ret
839860
840861 def tag_abi (self ):
841862 '''
@@ -891,10 +912,13 @@ def tag_platform(self):
891912 ret = ret2
892913
893914 log0 ( f'tag_platform(): returning { ret = } .' )
915+ assert '-' not in ret
894916 return ret
895917
896918 def wheel_name (self ):
897- return f'{ _normalise (self .name )} -{ self .version } -{ self .tag_python ()} -{ self .tag_abi ()} -{ self .tag_platform ()} .whl'
919+ ret = f'{ _normalise2 (self .name )} -{ self .version } -{ self .tag_python ()} -{ self .tag_abi ()} -{ self .tag_platform ()} .whl'
920+ assert ret .count ('-' ) == 4 , f'Expected 4 dash characters in { ret = } .'
921+ return ret
898922
899923 def wheel_name_match (self , wheel ):
900924 '''
@@ -923,7 +947,7 @@ def wheel_name_match(self, wheel):
923947 log2 (f'py_limited_api; { tag_python = } compatible with { self .tag_python ()= } .' )
924948 py_limited_api_compatible = True
925949
926- log2 (f'{ _normalise (self .name ) == name = } ' )
950+ log2 (f'{ _normalise2 (self .name ) == name = } ' )
927951 log2 (f'{ self .version == version = } ' )
928952 log2 (f'{ self .tag_python () == tag_python = } { self .tag_python ()= } { tag_python = } ' )
929953 log2 (f'{ py_limited_api_compatible = } ' )
@@ -932,7 +956,7 @@ def wheel_name_match(self, wheel):
932956 log2 (f'{ self .tag_platform ()= } ' )
933957 log2 (f'{ tag_platform .split ("." )= } ' )
934958 ret = (1
935- and _normalise (self .name ) == name
959+ and _normalise2 (self .name ) == name
936960 and self .version == version
937961 and (self .tag_python () == tag_python or py_limited_api_compatible )
938962 and self .tag_abi () == tag_abi
@@ -1059,7 +1083,7 @@ def _argv_dist_info(self, root):
10591083 it writes to a slightly different directory.
10601084 '''
10611085 if root is None :
1062- root = f'{ self .name } -{ self .version } .dist-info'
1086+ root = f'{ normalise2 ( self .name ) } -{ self .version } .dist-info'
10631087 self ._write_info (f'{ root } /METADATA' )
10641088 if self .license :
10651089 with open ( f'{ root } /COPYING' , 'w' ) as f :
@@ -1347,7 +1371,7 @@ def __str__(self):
13471371 )
13481372
13491373 def _dist_info_dir ( self ):
1350- return f'{ _normalise (self .name )} -{ self .version } .dist-info'
1374+ return f'{ _normalise2 (self .name )} -{ self .version } .dist-info'
13511375
13521376 def _metainfo (self ):
13531377 '''
@@ -1487,7 +1511,7 @@ def _fromto(self, p):
14871511 to_ = f'{ self ._dist_info_dir ()} /{ to_ [ len (prefix ):]} '
14881512 prefix = '$data/'
14891513 if to_ .startswith ( prefix ):
1490- to_ = f'{ self .name } -{ self .version } .data/{ to_ [ len (prefix ):]} '
1514+ to_ = f'{ _normalise2 ( self .name ) } -{ self .version } .data/{ to_ [ len (prefix ):]} '
14911515 if isinstance (from_ , str ):
14921516 from_ , _ = self ._path_relative_to_root ( from_ , assert_within_root = False )
14931517 to_ = self ._path_relative_to_root (to_ )
@@ -2569,7 +2593,7 @@ def _cpu_name():
25692593 return f'x{ 32 if sys .maxsize == 2 ** 31 - 1 else 64 } '
25702594
25712595
2572- def run_if ( command , out , * prerequisites ):
2596+ def run_if ( command , out , * prerequisites , caller = 1 ):
25732597 '''
25742598 Runs a command only if the output file is not up to date.
25752599
@@ -2599,21 +2623,26 @@ def run_if( command, out, *prerequisites):
25992623 ... os.remove( out)
26002624 >>> if os.path.exists( f'{out}.cmd'):
26012625 ... os.remove( f'{out}.cmd')
2602- >>> run_if( f'touch {out}', out)
2626+ >>> run_if( f'touch {out}', out, caller=0 )
26032627 pipcl.py:run_if(): Running command because: File does not exist: 'run_if_test_out'
26042628 pipcl.py:run_if(): Running: touch run_if_test_out
26052629 True
26062630
26072631 If we repeat, the output file will be up to date so the command is not run:
26082632
2609- >>> run_if( f'touch {out}', out)
2633+ >>> run_if( f'touch {out}', out, caller=0 )
26102634 pipcl.py:run_if(): Not running command because up to date: 'run_if_test_out'
26112635
26122636 If we change the command, the command is run:
26132637
2614- >>> run_if( f'touch {out}', out)
2615- pipcl.py:run_if(): Running command because: Command has changed
2616- pipcl.py:run_if(): Running: touch run_if_test_out
2638+ >>> run_if( f'touch {out};', out, caller=0)
2639+ pipcl.py:run_if(): Running command because: Command has changed:
2640+ pipcl.py:run_if(): @@ -1,2 +1,2 @@
2641+ pipcl.py:run_if(): touch
2642+ pipcl.py:run_if(): -run_if_test_out
2643+ pipcl.py:run_if(): +run_if_test_out;
2644+ pipcl.py:run_if():
2645+ pipcl.py:run_if(): Running: touch run_if_test_out;
26172646 True
26182647
26192648 If we add a prerequisite that is newer than the output, the command is run:
@@ -2622,15 +2651,20 @@ def run_if( command, out, *prerequisites):
26222651 >>> prerequisite = 'run_if_test_prerequisite'
26232652 >>> run( f'touch {prerequisite}', caller=0)
26242653 pipcl.py:run(): Running: touch run_if_test_prerequisite
2625- >>> run_if( f'touch {out}', out, prerequisite)
2626- pipcl.py:run_if(): Running command because: Prerequisite is new: 'run_if_test_prerequisite'
2654+ >>> run_if( f'touch {out}', out, prerequisite, caller=0)
2655+ pipcl.py:run_if(): Running command because: Command has changed:
2656+ pipcl.py:run_if(): @@ -1,2 +1,2 @@
2657+ pipcl.py:run_if(): touch
2658+ pipcl.py:run_if(): -run_if_test_out;
2659+ pipcl.py:run_if(): +run_if_test_out
2660+ pipcl.py:run_if():
26272661 pipcl.py:run_if(): Running: touch run_if_test_out
26282662 True
26292663
26302664 If we repeat, the output will be newer than the prerequisite, so the
26312665 command is not run:
26322666
2633- >>> run_if( f'touch {out}', out, prerequisite)
2667+ >>> run_if( f'touch {out}', out, prerequisite, caller=0 )
26342668 pipcl.py:run_if(): Not running command because up to date: 'run_if_test_out'
26352669 '''
26362670 doit = False
@@ -2687,9 +2721,9 @@ def _make_prerequisites(p):
26872721 for p in prerequisites :
26882722 prerequisites_all += _make_prerequisites ( p )
26892723 if 0 :
2690- log2 ( 'prerequisites_all:' )
2724+ log2 ( 'prerequisites_all:' , caller = caller + 1 )
26912725 for i in prerequisites_all :
2692- log2 ( f' { i !r} ' )
2726+ log2 ( f' { i !r} ' , caller = caller + 1 )
26932727 pre_mtime = 0
26942728 pre_path = None
26952729 for prerequisite in prerequisites_all :
@@ -2715,16 +2749,16 @@ def _make_prerequisites(p):
27152749 os .remove ( cmd_path )
27162750 except Exception :
27172751 pass
2718- log1 ( f'Running command because: { doit } ' , caller = 2 )
2752+ log1 ( f'Running command because: { doit } ' , caller = caller + 1 )
27192753
2720- run ( command , caller = 2 )
2754+ run ( command , caller = caller + 1 )
27212755
27222756 # Write the command we ran, into `cmd_path`.
27232757 with open ( cmd_path , 'w' ) as f :
27242758 f .write ( command )
27252759 return True
27262760 else :
2727- log1 ( f'Not running command because up to date: { out !r} ' , caller = 2 )
2761+ log1 ( f'Not running command because up to date: { out !r} ' , caller = caller + 1 )
27282762
27292763 if 0 :
27302764 log2 ( f'out_mtime={ time .ctime (out_mtime )} pre_mtime={ time .ctime (pre_mtime )} .'
@@ -2796,6 +2830,11 @@ def _normalise(name):
27962830 return re .sub (r"[-_.]+" , "-" , name ).lower ()
27972831
27982832
2833+ def _normalise2 (name ):
2834+ # https://packaging.python.org/en/latest/specifications/binary-distribution-format/
2835+ return _normalise (name ).replace ('-' , '_' )
2836+
2837+
27992838def _assert_version_pep_440 (version ):
28002839 assert re .match (
28012840 r'^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$' ,
@@ -2848,19 +2887,30 @@ def _log(text, level, caller):
28482887 print (f'{ filename } :{ fr .function } (): { line } ' , file = sys .stdout , flush = 1 )
28492888
28502889
2851- def relpath (path , start = None ):
2890+ def relpath (path , start = None , allow_up = True ):
28522891 '''
28532892 A safe alternative to os.path.relpath(), avoiding an exception on Windows
28542893 if the drive needs to change - in this case we use os.path.abspath().
2894+
2895+ Args:
2896+ path:
2897+ Path to be processed.
2898+ start:
2899+ Start directory or current directory if None.
2900+ allow_up:
2901+ If false we return absolute path is <path> is not within <start>.
28552902 '''
28562903 if windows ():
28572904 try :
2858- return os .path .relpath (path , start )
2905+ ret = os .path .relpath (path , start )
28592906 except ValueError :
28602907 # os.path.relpath() fails if trying to change drives.
2861- return os .path .abspath (path )
2908+ ret = os .path .abspath (path )
28622909 else :
2863- return os .path .relpath (path , start )
2910+ ret = os .path .relpath (path , start )
2911+ if not allow_up and ret .startswith ('../' ) or ret .startswith ('..\\ ' ):
2912+ ret = os .path .abspath (path )
2913+ return ret
28642914
28652915
28662916def _so_suffix (use_so_versioning = True ):
@@ -3218,7 +3268,15 @@ def venv_run(args, path, recreate=True, clean=False):
32183268 # graal_legacy_python_config is true.
32193269 #
32203270 includes , ldflags = sysconfig_python_flags ()
3221- if sys .argv [1 :] == ['--graal-legacy-python-config' , '--includes' ]:
3271+ if sys .argv [1 ] == '--doctest' :
3272+ import doctest
3273+ if sys .argv [2 :]:
3274+ for f in sys .argv [2 :]:
3275+ ff = globals ()[f ]
3276+ doctest .run_docstring_examples (ff , globals ())
3277+ else :
3278+ doctest .testmod (None )
3279+ elif sys .argv [1 :] == ['--graal-legacy-python-config' , '--includes' ]:
32223280 print (includes )
32233281 elif sys .argv [1 :] == ['--graal-legacy-python-config' , '--ldflags' ]:
32243282 print (ldflags )
0 commit comments