16
16
import shutil
17
17
import sys
18
18
import traceback
19
- from contextlib import suppress
19
+ from contextlib import ExitStack , suppress
20
20
from enum import Enum
21
+ from functools import lru_cache
21
22
from inspect import cleandoc
22
23
from itertools import chain
23
24
from pathlib import Path
33
34
Tuple ,
34
35
TypeVar ,
35
36
Union ,
37
+ cast ,
36
38
)
37
39
38
40
from .. import (
42
44
errors ,
43
45
namespaces ,
44
46
)
47
+ from .._wheelbuilder import WheelBuilder
48
+ from ..extern .packaging .tags import sys_tags
45
49
from ..discovery import find_package_path
46
50
from ..dist import Distribution
47
51
from ..warnings import (
51
55
)
52
56
from .build_py import build_py as build_py_cls
53
57
54
- if TYPE_CHECKING :
55
- from wheel .wheelfile import WheelFile # noqa
56
-
57
58
if sys .version_info >= (3 , 8 ):
58
59
from typing import Protocol
59
60
elif TYPE_CHECKING :
63
64
64
65
_Path = Union [str , Path ]
65
66
_P = TypeVar ("_P" , bound = _Path )
67
+ _Tag = Tuple [str , str , str ]
66
68
_logger = logging .getLogger (__name__ )
67
69
68
70
@@ -117,6 +119,20 @@ def convert(cls, mode: Optional[str]) -> "_EditableMode":
117
119
"""
118
120
119
121
122
+ @lru_cache (maxsize = 0 )
123
+ def _any_compat_tag () -> _Tag :
124
+ """
125
+ PEP 660 does not require the tag to be identical to the tag that will be used
126
+ in production, it only requires the tag to be compatible with the current system.
127
+ Moreover, PEP 660 also guarantees that the generated wheel file should be used in
128
+ the same system where it was produced.
129
+ Therefore we can just be pragmatic and pick one of the compatible tags.
130
+ """
131
+ tag = next (sys_tags ())
132
+ components = (tag .interpreter , tag .abi , tag .platform )
133
+ return cast (_Tag , tuple (map (_normalization .filename_component , components )))
134
+
135
+
120
136
class editable_wheel (Command ):
121
137
"""Build 'editable' wheel for development.
122
138
This command is private and reserved for internal use of setuptools,
@@ -142,34 +158,34 @@ def finalize_options(self):
142
158
self .project_dir = dist .src_root or os .curdir
143
159
self .package_dir = dist .package_dir or {}
144
160
self .dist_dir = Path (self .dist_dir or os .path .join (self .project_dir , "dist" ))
161
+ if self .dist_info_dir :
162
+ self .dist_info_dir = Path (self .dist_info_dir )
145
163
146
164
def run (self ):
147
165
try :
148
166
self .dist_dir .mkdir (exist_ok = True )
149
- self ._ensure_dist_info ()
150
-
151
- # Add missing dist_info files
152
- self .reinitialize_command ("bdist_wheel" )
153
- bdist_wheel = self .get_finalized_command ("bdist_wheel" )
154
- bdist_wheel .write_wheelfile (self .dist_info_dir )
155
-
156
- self ._create_wheel_file (bdist_wheel )
167
+ self ._create_wheel_file ()
157
168
except Exception :
158
169
traceback .print_exc ()
159
170
project = self .distribution .name or self .distribution .get_name ()
160
171
_DebuggingTips .emit (project = project )
161
172
raise
162
173
163
- def _ensure_dist_info (self ):
174
+ def _get_dist_info_name (self , tmp_dir ):
164
175
if self .dist_info_dir is None :
165
176
dist_info = self .reinitialize_command ("dist_info" )
166
- dist_info .output_dir = self . dist_dir
177
+ dist_info .output_dir = tmp_dir
167
178
dist_info .ensure_finalized ()
168
- dist_info .run ()
169
179
self .dist_info_dir = dist_info .dist_info_dir
170
- else :
171
- assert str (self .dist_info_dir ).endswith (".dist-info" )
172
- assert Path (self .dist_info_dir , "METADATA" ).exists ()
180
+ return dist_info .name
181
+
182
+ assert str (self .dist_info_dir ).endswith (".dist-info" )
183
+ assert (self .dist_info_dir / "METADATA" ).exists ()
184
+ return self .dist_info_dir .name [: - len (".dist-info" )]
185
+
186
+ def _ensure_dist_info (self ):
187
+ if not Path (self .dist_info_dir , "METADATA" ).exists ():
188
+ self .distribution .run_command ("dist_info" )
173
189
174
190
def _install_namespaces (self , installation_dir , pth_prefix ):
175
191
# XXX: Only required to support the deprecated namespace practice
@@ -209,8 +225,7 @@ def _configure_build(
209
225
scripts = str (Path (unpacked_wheel , f"{ name } .data" , "scripts" ))
210
226
211
227
# egg-info may be generated again to create a manifest (used for package data)
212
- egg_info = dist .reinitialize_command ("egg_info" , reinit_subcommands = True )
213
- egg_info .egg_base = str (tmp_dir )
228
+ egg_info = dist .get_command_obj ("egg_info" )
214
229
egg_info .ignore_egg_info_in_manifest = True
215
230
216
231
build = dist .reinitialize_command ("build" , reinit_subcommands = True )
@@ -322,31 +337,29 @@ def _safely_run(self, cmd_name: str):
322
337
# needs work.
323
338
)
324
339
325
- def _create_wheel_file (self , bdist_wheel ):
326
- from wheel .wheelfile import WheelFile
327
-
328
- dist_info = self .get_finalized_command ("dist_info" )
329
- dist_name = dist_info .name
330
- tag = "-" .join (bdist_wheel .get_tag ())
331
- build_tag = "0.editable" # According to PEP 427 needs to start with digit
332
- archive_name = f"{ dist_name } -{ build_tag } -{ tag } .whl"
333
- wheel_path = Path (self .dist_dir , archive_name )
334
- if wheel_path .exists ():
335
- wheel_path .unlink ()
336
-
337
- unpacked_wheel = TemporaryDirectory (suffix = archive_name )
338
- build_lib = TemporaryDirectory (suffix = ".build-lib" )
339
- build_tmp = TemporaryDirectory (suffix = ".build-temp" )
340
-
341
- with unpacked_wheel as unpacked , build_lib as lib , build_tmp as tmp :
342
- unpacked_dist_info = Path (unpacked , Path (self .dist_info_dir ).name )
343
- shutil .copytree (self .dist_info_dir , unpacked_dist_info )
344
- self ._install_namespaces (unpacked , dist_info .name )
340
+ def _create_wheel_file (self ):
341
+ with ExitStack () as stack :
342
+ lib = stack .enter_context (TemporaryDirectory (suffix = ".build-lib" ))
343
+ tmp = stack .enter_context (TemporaryDirectory (suffix = ".build-temp" ))
344
+ dist_name = self ._get_dist_info_name (tmp )
345
+
346
+ tag = "-" .join (_any_compat_tag ()) # Loose tag for the sake of simplicity...
347
+ build_tag = "0.editable" # According to PEP 427 needs to start with digit.
348
+ archive_name = f"{ dist_name } -{ build_tag } -{ tag } .whl"
349
+ wheel_path = Path (self .dist_dir , archive_name )
350
+ if wheel_path .exists ():
351
+ wheel_path .unlink ()
352
+
353
+ unpacked = stack .enter_context (TemporaryDirectory (suffix = archive_name ))
354
+ self ._install_namespaces (unpacked , dist_name )
345
355
files , mapping = self ._run_build_commands (dist_name , unpacked , lib , tmp )
346
- strategy = self ._select_strategy (dist_name , tag , lib )
347
- with strategy , WheelFile (wheel_path , "w" ) as wheel_obj :
348
- strategy (wheel_obj , files , mapping )
349
- wheel_obj .write_files (unpacked )
356
+
357
+ strategy = stack .enter_context (self ._select_strategy (dist_name , tag , lib ))
358
+ builder = stack .enter_context (WheelBuilder (wheel_path ))
359
+ strategy (builder , files , mapping )
360
+ builder .add_tree (unpacked , exclude = ["*.dist-info/*" , "*.egg-info/*" ])
361
+ self ._ensure_dist_info ()
362
+ builder .add_tree (self .dist_info_dir , prefix = self .dist_info_dir .name )
350
363
351
364
return wheel_path
352
365
@@ -384,7 +397,7 @@ def _select_strategy(
384
397
385
398
386
399
class EditableStrategy (Protocol ):
387
- def __call__ (self , wheel : "WheelFile" , files : List [str ], mapping : Dict [str , str ]):
400
+ def __call__ (self , wheel : WheelBuilder , files : List [str ], mapping : Dict [str , str ]):
388
401
...
389
402
390
403
def __enter__ (self ):
@@ -400,10 +413,10 @@ def __init__(self, dist: Distribution, name: str, path_entries: List[Path]):
400
413
self .name = name
401
414
self .path_entries = path_entries
402
415
403
- def __call__ (self , wheel : "WheelFile" , files : List [str ], mapping : Dict [str , str ]):
416
+ def __call__ (self , wheel : WheelBuilder , files : List [str ], mapping : Dict [str , str ]):
404
417
entries = "\n " .join ((str (p .resolve ()) for p in self .path_entries ))
405
418
contents = _encode_pth (f"{ entries } \n " )
406
- wheel .writestr (f"__editable__.{ self .name } .pth" , contents )
419
+ wheel .new_file (f"__editable__.{ self .name } .pth" , contents )
407
420
408
421
def __enter__ (self ):
409
422
msg = f"""
@@ -440,7 +453,7 @@ def __init__(
440
453
self ._file = dist .get_command_obj ("build_py" ).copy_file
441
454
super ().__init__ (dist , name , [self .auxiliary_dir ])
442
455
443
- def __call__ (self , wheel : "WheelFile" , files : List [str ], mapping : Dict [str , str ]):
456
+ def __call__ (self , wheel : WheelBuilder , files : List [str ], mapping : Dict [str , str ]):
444
457
self ._create_links (files , mapping )
445
458
super ().__call__ (wheel , files , mapping )
446
459
@@ -492,7 +505,7 @@ def __init__(self, dist: Distribution, name: str):
492
505
self .dist = dist
493
506
self .name = name
494
507
495
- def __call__ (self , wheel : "WheelFile" , files : List [str ], mapping : Dict [str , str ]):
508
+ def __call__ (self , wheel : WheelBuilder , files : List [str ], mapping : Dict [str , str ]):
496
509
src_root = self .dist .src_root or os .curdir
497
510
top_level = chain (_find_packages (self .dist ), _find_top_level_modules (self .dist ))
498
511
package_dir = self .dist .package_dir or {}
@@ -507,11 +520,9 @@ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]
507
520
508
521
name = f"__editable__.{ self .name } .finder"
509
522
finder = _normalization .safe_identifier (name )
510
- content = bytes (_finder_template (name , roots , namespaces_ ), "utf-8" )
511
- wheel .writestr (f"{ finder } .py" , content )
512
-
523
+ wheel .new_file (f"{ finder } .py" , _finder_template (name , roots , namespaces_ ))
513
524
content = _encode_pth (f"import { finder } ; { finder } .install()" )
514
- wheel .writestr (f"__editable__.{ self .name } .pth" , content )
525
+ wheel .new_file (f"__editable__.{ self .name } .pth" , content )
515
526
516
527
def __enter__ (self ):
517
528
msg = "Editable install will be performed using a meta path finder.\n "
0 commit comments