2
2
Python packaging operations, including PEP-517 support, for use by a `setup.py`
3
3
script.
4
4
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:
8
6
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 .
12
10
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.
14
15
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).
18
31
'''
19
32
20
33
import base64
@@ -532,6 +545,12 @@ def assert_str_or_multi( v):
532
545
assert_str_or_multi ( requires_external )
533
546
assert_str_or_multi ( project_url )
534
547
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
+ )
535
554
536
555
# https://packaging.python.org/en/latest/specifications/core-metadata/.
537
556
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,
761
780
else :
762
781
items = self .fn_sdist ()
763
782
764
- prefix = f'{ _normalise (self .name )} -{ self .version } '
783
+ prefix = f'{ _normalise2 (self .name )} -{ self .version } '
765
784
os .makedirs (sdist_directory , exist_ok = True )
766
785
tarpath = f'{ sdist_directory } /{ prefix } .tar.gz'
767
786
log2 (f'Creating sdist: { tarpath } ' )
@@ -833,9 +852,11 @@ def tag_python(self):
833
852
Get two-digit python version, e.g. 'cp3.8' for python-3.8.6.
834
853
'''
835
854
if self .tag_python_ :
836
- return self .tag_python_
855
+ ret = self .tag_python_
837
856
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
839
860
840
861
def tag_abi (self ):
841
862
'''
@@ -891,10 +912,13 @@ def tag_platform(self):
891
912
ret = ret2
892
913
893
914
log0 ( f'tag_platform(): returning { ret = } .' )
915
+ assert '-' not in ret
894
916
return ret
895
917
896
918
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
898
922
899
923
def wheel_name_match (self , wheel ):
900
924
'''
@@ -923,7 +947,7 @@ def wheel_name_match(self, wheel):
923
947
log2 (f'py_limited_api; { tag_python = } compatible with { self .tag_python ()= } .' )
924
948
py_limited_api_compatible = True
925
949
926
- log2 (f'{ _normalise (self .name ) == name = } ' )
950
+ log2 (f'{ _normalise2 (self .name ) == name = } ' )
927
951
log2 (f'{ self .version == version = } ' )
928
952
log2 (f'{ self .tag_python () == tag_python = } { self .tag_python ()= } { tag_python = } ' )
929
953
log2 (f'{ py_limited_api_compatible = } ' )
@@ -932,7 +956,7 @@ def wheel_name_match(self, wheel):
932
956
log2 (f'{ self .tag_platform ()= } ' )
933
957
log2 (f'{ tag_platform .split ("." )= } ' )
934
958
ret = (1
935
- and _normalise (self .name ) == name
959
+ and _normalise2 (self .name ) == name
936
960
and self .version == version
937
961
and (self .tag_python () == tag_python or py_limited_api_compatible )
938
962
and self .tag_abi () == tag_abi
@@ -1059,7 +1083,7 @@ def _argv_dist_info(self, root):
1059
1083
it writes to a slightly different directory.
1060
1084
'''
1061
1085
if root is None :
1062
- root = f'{ self .name } -{ self .version } .dist-info'
1086
+ root = f'{ normalise2 ( self .name ) } -{ self .version } .dist-info'
1063
1087
self ._write_info (f'{ root } /METADATA' )
1064
1088
if self .license :
1065
1089
with open ( f'{ root } /COPYING' , 'w' ) as f :
@@ -1347,7 +1371,7 @@ def __str__(self):
1347
1371
)
1348
1372
1349
1373
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'
1351
1375
1352
1376
def _metainfo (self ):
1353
1377
'''
@@ -1487,7 +1511,7 @@ def _fromto(self, p):
1487
1511
to_ = f'{ self ._dist_info_dir ()} /{ to_ [ len (prefix ):]} '
1488
1512
prefix = '$data/'
1489
1513
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 ):]} '
1491
1515
if isinstance (from_ , str ):
1492
1516
from_ , _ = self ._path_relative_to_root ( from_ , assert_within_root = False )
1493
1517
to_ = self ._path_relative_to_root (to_ )
@@ -2569,7 +2593,7 @@ def _cpu_name():
2569
2593
return f'x{ 32 if sys .maxsize == 2 ** 31 - 1 else 64 } '
2570
2594
2571
2595
2572
- def run_if ( command , out , * prerequisites ):
2596
+ def run_if ( command , out , * prerequisites , caller = 1 ):
2573
2597
'''
2574
2598
Runs a command only if the output file is not up to date.
2575
2599
@@ -2599,21 +2623,26 @@ def run_if( command, out, *prerequisites):
2599
2623
... os.remove( out)
2600
2624
>>> if os.path.exists( f'{out}.cmd'):
2601
2625
... os.remove( f'{out}.cmd')
2602
- >>> run_if( f'touch {out}', out)
2626
+ >>> run_if( f'touch {out}', out, caller=0 )
2603
2627
pipcl.py:run_if(): Running command because: File does not exist: 'run_if_test_out'
2604
2628
pipcl.py:run_if(): Running: touch run_if_test_out
2605
2629
True
2606
2630
2607
2631
If we repeat, the output file will be up to date so the command is not run:
2608
2632
2609
- >>> run_if( f'touch {out}', out)
2633
+ >>> run_if( f'touch {out}', out, caller=0 )
2610
2634
pipcl.py:run_if(): Not running command because up to date: 'run_if_test_out'
2611
2635
2612
2636
If we change the command, the command is run:
2613
2637
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;
2617
2646
True
2618
2647
2619
2648
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):
2622
2651
>>> prerequisite = 'run_if_test_prerequisite'
2623
2652
>>> run( f'touch {prerequisite}', caller=0)
2624
2653
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():
2627
2661
pipcl.py:run_if(): Running: touch run_if_test_out
2628
2662
True
2629
2663
2630
2664
If we repeat, the output will be newer than the prerequisite, so the
2631
2665
command is not run:
2632
2666
2633
- >>> run_if( f'touch {out}', out, prerequisite)
2667
+ >>> run_if( f'touch {out}', out, prerequisite, caller=0 )
2634
2668
pipcl.py:run_if(): Not running command because up to date: 'run_if_test_out'
2635
2669
'''
2636
2670
doit = False
@@ -2687,9 +2721,9 @@ def _make_prerequisites(p):
2687
2721
for p in prerequisites :
2688
2722
prerequisites_all += _make_prerequisites ( p )
2689
2723
if 0 :
2690
- log2 ( 'prerequisites_all:' )
2724
+ log2 ( 'prerequisites_all:' , caller = caller + 1 )
2691
2725
for i in prerequisites_all :
2692
- log2 ( f' { i !r} ' )
2726
+ log2 ( f' { i !r} ' , caller = caller + 1 )
2693
2727
pre_mtime = 0
2694
2728
pre_path = None
2695
2729
for prerequisite in prerequisites_all :
@@ -2715,16 +2749,16 @@ def _make_prerequisites(p):
2715
2749
os .remove ( cmd_path )
2716
2750
except Exception :
2717
2751
pass
2718
- log1 ( f'Running command because: { doit } ' , caller = 2 )
2752
+ log1 ( f'Running command because: { doit } ' , caller = caller + 1 )
2719
2753
2720
- run ( command , caller = 2 )
2754
+ run ( command , caller = caller + 1 )
2721
2755
2722
2756
# Write the command we ran, into `cmd_path`.
2723
2757
with open ( cmd_path , 'w' ) as f :
2724
2758
f .write ( command )
2725
2759
return True
2726
2760
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 )
2728
2762
2729
2763
if 0 :
2730
2764
log2 ( f'out_mtime={ time .ctime (out_mtime )} pre_mtime={ time .ctime (pre_mtime )} .'
@@ -2796,6 +2830,11 @@ def _normalise(name):
2796
2830
return re .sub (r"[-_.]+" , "-" , name ).lower ()
2797
2831
2798
2832
2833
+ def _normalise2 (name ):
2834
+ # https://packaging.python.org/en/latest/specifications/binary-distribution-format/
2835
+ return _normalise (name ).replace ('-' , '_' )
2836
+
2837
+
2799
2838
def _assert_version_pep_440 (version ):
2800
2839
assert re .match (
2801
2840
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):
2848
2887
print (f'{ filename } :{ fr .function } (): { line } ' , file = sys .stdout , flush = 1 )
2849
2888
2850
2889
2851
- def relpath (path , start = None ):
2890
+ def relpath (path , start = None , allow_up = True ):
2852
2891
'''
2853
2892
A safe alternative to os.path.relpath(), avoiding an exception on Windows
2854
2893
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>.
2855
2902
'''
2856
2903
if windows ():
2857
2904
try :
2858
- return os .path .relpath (path , start )
2905
+ ret = os .path .relpath (path , start )
2859
2906
except ValueError :
2860
2907
# os.path.relpath() fails if trying to change drives.
2861
- return os .path .abspath (path )
2908
+ ret = os .path .abspath (path )
2862
2909
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
2864
2914
2865
2915
2866
2916
def _so_suffix (use_so_versioning = True ):
@@ -3218,7 +3268,15 @@ def venv_run(args, path, recreate=True, clean=False):
3218
3268
# graal_legacy_python_config is true.
3219
3269
#
3220
3270
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' ]:
3222
3280
print (includes )
3223
3281
elif sys .argv [1 :] == ['--graal-legacy-python-config' , '--ldflags' ]:
3224
3282
print (ldflags )
0 commit comments