1515import shutil
1616import sys
1717import traceback
18- from contextlib import suppress
18+ from contextlib import ExitStack , suppress
1919from enum import Enum
20+ from functools import lru_cache
2021from inspect import cleandoc
2122from itertools import chain
2223from pathlib import Path
3233 Tuple ,
3334 TypeVar ,
3435 Union ,
36+ cast ,
3537)
3638
3739from .. import (
4143 errors ,
4244 namespaces ,
4345)
46+ from .._wheelbuilder import WheelBuilder
47+ from ..extern .packaging .tags import sys_tags
4448from ..discovery import find_package_path
4549from ..dist import Distribution
4650from ..warnings import (
5054)
5155from .build_py import build_py as build_py_cls
5256
53- if TYPE_CHECKING :
54- from wheel .wheelfile import WheelFile # noqa
55-
5657if sys .version_info >= (3 , 8 ):
5758 from typing import Protocol
5859elif TYPE_CHECKING :
6263
6364_Path = Union [str , Path ]
6465_P = TypeVar ("_P" , bound = _Path )
66+ _Tag = Tuple [str , str , str ]
6567_logger = logging .getLogger (__name__ )
6668
6769
@@ -116,6 +118,20 @@ def convert(cls, mode: Optional[str]) -> "_EditableMode":
116118"""
117119
118120
121+ @lru_cache (maxsize = 0 )
122+ def _any_compat_tag () -> _Tag :
123+ """
124+ PEP 660 does not require the tag to be identical to the tag that will be used
125+ in production, it only requires the tag to be compatible with the current system.
126+ Moreover, PEP 660 also guarantees that the generated wheel file should be used in
127+ the same system where it was produced.
128+ Therefore we can just be pragmatic and pick one of the compatible tags.
129+ """
130+ tag = next (sys_tags ())
131+ components = (tag .interpreter , tag .abi , tag .platform )
132+ return cast (_Tag , tuple (map (_normalization .filename_component , components )))
133+
134+
119135class editable_wheel (Command ):
120136 """Build 'editable' wheel for development.
121137 This command is private and reserved for internal use of setuptools,
@@ -141,34 +157,34 @@ def finalize_options(self):
141157 self .project_dir = dist .src_root or os .curdir
142158 self .package_dir = dist .package_dir or {}
143159 self .dist_dir = Path (self .dist_dir or os .path .join (self .project_dir , "dist" ))
160+ if self .dist_info_dir :
161+ self .dist_info_dir = Path (self .dist_info_dir )
144162
145163 def run (self ):
146164 try :
147165 self .dist_dir .mkdir (exist_ok = True )
148- self ._ensure_dist_info ()
149-
150- # Add missing dist_info files
151- self .reinitialize_command ("bdist_wheel" )
152- bdist_wheel = self .get_finalized_command ("bdist_wheel" )
153- bdist_wheel .write_wheelfile (self .dist_info_dir )
154-
155- self ._create_wheel_file (bdist_wheel )
166+ self ._create_wheel_file ()
156167 except Exception :
157168 traceback .print_exc ()
158169 project = self .distribution .name or self .distribution .get_name ()
159170 _DebuggingTips .emit (project = project )
160171 raise
161172
162- def _ensure_dist_info (self ):
173+ def _get_dist_info_name (self , tmp_dir ):
163174 if self .dist_info_dir is None :
164175 dist_info = self .reinitialize_command ("dist_info" )
165- dist_info .output_dir = self . dist_dir
176+ dist_info .output_dir = tmp_dir
166177 dist_info .ensure_finalized ()
167- dist_info .run ()
168178 self .dist_info_dir = dist_info .dist_info_dir
169- else :
170- assert str (self .dist_info_dir ).endswith (".dist-info" )
171- assert Path (self .dist_info_dir , "METADATA" ).exists ()
179+ return dist_info .name
180+
181+ assert str (self .dist_info_dir ).endswith (".dist-info" )
182+ assert (self .dist_info_dir / "METADATA" ).exists ()
183+ return self .dist_info_dir .name [: - len (".dist-info" )]
184+
185+ def _ensure_dist_info (self ):
186+ if not Path (self .dist_info_dir , "METADATA" ).exists ():
187+ self .distribution .run_command ("dist_info" )
172188
173189 def _install_namespaces (self , installation_dir , pth_prefix ):
174190 # XXX: Only required to support the deprecated namespace practice
@@ -208,8 +224,7 @@ def _configure_build(
208224 scripts = str (Path (unpacked_wheel , f"{ name } .data" , "scripts" ))
209225
210226 # egg-info may be generated again to create a manifest (used for package data)
211- egg_info = dist .reinitialize_command ("egg_info" , reinit_subcommands = True )
212- egg_info .egg_base = str (tmp_dir )
227+ egg_info = dist .get_command_obj ("egg_info" )
213228 egg_info .ignore_egg_info_in_manifest = True
214229
215230 build = dist .reinitialize_command ("build" , reinit_subcommands = True )
@@ -321,31 +336,29 @@ def _safely_run(self, cmd_name: str):
321336 # needs work.
322337 )
323338
324- def _create_wheel_file (self , bdist_wheel ):
325- from wheel .wheelfile import WheelFile
326-
327- dist_info = self .get_finalized_command ("dist_info" )
328- dist_name = dist_info .name
329- tag = "-" .join (bdist_wheel .get_tag ())
330- build_tag = "0.editable" # According to PEP 427 needs to start with digit
331- archive_name = f"{ dist_name } -{ build_tag } -{ tag } .whl"
332- wheel_path = Path (self .dist_dir , archive_name )
333- if wheel_path .exists ():
334- wheel_path .unlink ()
335-
336- unpacked_wheel = TemporaryDirectory (suffix = archive_name )
337- build_lib = TemporaryDirectory (suffix = ".build-lib" )
338- build_tmp = TemporaryDirectory (suffix = ".build-temp" )
339-
340- with unpacked_wheel as unpacked , build_lib as lib , build_tmp as tmp :
341- unpacked_dist_info = Path (unpacked , Path (self .dist_info_dir ).name )
342- shutil .copytree (self .dist_info_dir , unpacked_dist_info )
343- self ._install_namespaces (unpacked , dist_info .name )
339+ def _create_wheel_file (self ):
340+ with ExitStack () as stack :
341+ lib = stack .enter_context (TemporaryDirectory (suffix = ".build-lib" ))
342+ tmp = stack .enter_context (TemporaryDirectory (suffix = ".build-temp" ))
343+ dist_name = self ._get_dist_info_name (tmp )
344+
345+ tag = "-" .join (_any_compat_tag ()) # Loose tag for the sake of simplicity...
346+ build_tag = "0.editable" # According to PEP 427 needs to start with digit.
347+ archive_name = f"{ dist_name } -{ build_tag } -{ tag } .whl"
348+ wheel_path = Path (self .dist_dir , archive_name )
349+ if wheel_path .exists ():
350+ wheel_path .unlink ()
351+
352+ unpacked = stack .enter_context (TemporaryDirectory (suffix = archive_name ))
353+ self ._install_namespaces (unpacked , dist_name )
344354 files , mapping = self ._run_build_commands (dist_name , unpacked , lib , tmp )
345- strategy = self ._select_strategy (dist_name , tag , lib )
346- with strategy , WheelFile (wheel_path , "w" ) as wheel_obj :
347- strategy (wheel_obj , files , mapping )
348- wheel_obj .write_files (unpacked )
355+
356+ strategy = stack .enter_context (self ._select_strategy (dist_name , tag , lib ))
357+ builder = stack .enter_context (WheelBuilder (wheel_path ))
358+ strategy (builder , files , mapping )
359+ builder .add_tree (unpacked , exclude = ["*.dist-info/*" , "*.egg-info/*" ])
360+ self ._ensure_dist_info ()
361+ builder .add_tree (self .dist_info_dir , prefix = self .dist_info_dir .name )
349362
350363 return wheel_path
351364
@@ -383,7 +396,7 @@ def _select_strategy(
383396
384397
385398class EditableStrategy (Protocol ):
386- def __call__ (self , wheel : "WheelFile" , files : List [str ], mapping : Dict [str , str ]):
399+ def __call__ (self , wheel : WheelBuilder , files : List [str ], mapping : Dict [str , str ]):
387400 ...
388401
389402 def __enter__ (self ):
@@ -399,10 +412,9 @@ def __init__(self, dist: Distribution, name: str, path_entries: List[Path]):
399412 self .name = name
400413 self .path_entries = path_entries
401414
402- def __call__ (self , wheel : "WheelFile" , files : List [str ], mapping : Dict [str , str ]):
415+ def __call__ (self , wheel : WheelBuilder , files : List [str ], mapping : Dict [str , str ]):
403416 entries = "\n " .join ((str (p .resolve ()) for p in self .path_entries ))
404- contents = bytes (f"{ entries } \n " , "utf-8" )
405- wheel .writestr (f"__editable__.{ self .name } .pth" , contents )
417+ wheel .new_file (f"__editable__.{ self .name } .pth" , f"{ entries } \n " )
406418
407419 def __enter__ (self ):
408420 msg = f"""
@@ -426,8 +438,10 @@ class _LinkTree(_StaticPth):
426438 By collocating ``auxiliary_dir`` and the original source code, limitations
427439 with hardlinks should be avoided.
428440 """
441+
429442 def __init__ (
430- self , dist : Distribution ,
443+ self ,
444+ dist : Distribution ,
431445 name : str ,
432446 auxiliary_dir : _Path ,
433447 build_lib : _Path ,
@@ -437,7 +451,7 @@ def __init__(
437451 self ._file = dist .get_command_obj ("build_py" ).copy_file
438452 super ().__init__ (dist , name , [self .auxiliary_dir ])
439453
440- def __call__ (self , wheel : "WheelFile" , files : List [str ], mapping : Dict [str , str ]):
454+ def __call__ (self , wheel : WheelBuilder , files : List [str ], mapping : Dict [str , str ]):
441455 self ._create_links (files , mapping )
442456 super ().__call__ (wheel , files , mapping )
443457
@@ -492,24 +506,24 @@ def __init__(self, dist: Distribution, name: str):
492506 self .dist = dist
493507 self .name = name
494508
495- def __call__ (self , wheel : "WheelFile" , files : List [str ], mapping : Dict [str , str ]):
509+ def __call__ (self , wheel : WheelBuilder , files : List [str ], mapping : Dict [str , str ]):
496510 src_root = self .dist .src_root or os .curdir
497511 top_level = chain (_find_packages (self .dist ), _find_top_level_modules (self .dist ))
498512 package_dir = self .dist .package_dir or {}
499513 roots = _find_package_roots (top_level , package_dir , src_root )
500514
501- namespaces_ : Dict [str , List [str ]] = dict (chain (
502- _find_namespaces (self .dist .packages or [], roots ),
503- ((ns , []) for ns in _find_virtual_namespaces (roots )),
504- ))
515+ namespaces_ : Dict [str , List [str ]] = dict (
516+ chain (
517+ _find_namespaces (self .dist .packages or [], roots ),
518+ ((ns , []) for ns in _find_virtual_namespaces (roots )),
519+ )
520+ )
505521
506522 name = f"__editable__.{ self .name } .finder"
507523 finder = _normalization .safe_identifier (name )
508- content = bytes (_finder_template (name , roots , namespaces_ ), "utf-8" )
509- wheel .writestr (f"{ finder } .py" , content )
510-
511- content = bytes (f"import { finder } ; { finder } .install()" , "utf-8" )
512- wheel .writestr (f"__editable__.{ self .name } .pth" , content )
524+ wheel .new_file (f"{ finder } .py" , _finder_template (name , roots , namespaces_ ))
525+ pth = f"__editable__.{ self .name } .pth"
526+ wheel .new_file (pth , f"import { finder } ; { finder } .install()" )
513527
514528 def __enter__ (self ):
515529 msg = "Editable install will be performed using a meta path finder.\n "
0 commit comments