1616import shutil
1717import sys
1818import traceback
19- from contextlib import suppress
19+ from contextlib import ExitStack , suppress
2020from enum import Enum
21+ from functools import lru_cache
2122from inspect import cleandoc
2223from itertools import chain
2324from pathlib import Path
3334 Tuple ,
3435 TypeVar ,
3536 Union ,
37+ cast ,
3638)
3739
3840from .. import (
4244 errors ,
4345 namespaces ,
4446)
47+ from .._wheelbuilder import WheelBuilder
48+ from ..extern .packaging .tags import sys_tags
4549from ..discovery import find_package_path
4650from ..dist import Distribution
4751from ..warnings import (
5155)
5256from .build_py import build_py as build_py_cls
5357
54- if TYPE_CHECKING :
55- from wheel .wheelfile import WheelFile # noqa
56-
5758if sys .version_info >= (3 , 8 ):
5859 from typing import Protocol
5960elif TYPE_CHECKING :
6364
6465_Path = Union [str , Path ]
6566_P = TypeVar ("_P" , bound = _Path )
67+ _Tag = Tuple [str , str , str ]
6668_logger = logging .getLogger (__name__ )
6769
6870
@@ -117,6 +119,20 @@ def convert(cls, mode: Optional[str]) -> "_EditableMode":
117119"""
118120
119121
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+
120136class editable_wheel (Command ):
121137 """Build 'editable' wheel for development.
122138 This command is private and reserved for internal use of setuptools,
@@ -142,34 +158,34 @@ def finalize_options(self):
142158 self .project_dir = dist .src_root or os .curdir
143159 self .package_dir = dist .package_dir or {}
144160 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 )
145163
146164 def run (self ):
147165 try :
148166 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 ()
157168 except Exception :
158169 traceback .print_exc ()
159170 project = self .distribution .name or self .distribution .get_name ()
160171 _DebuggingTips .emit (project = project )
161172 raise
162173
163- def _ensure_dist_info (self ):
174+ def _get_dist_info_name (self , tmp_dir ):
164175 if self .dist_info_dir is None :
165176 dist_info = self .reinitialize_command ("dist_info" )
166- dist_info .output_dir = self . dist_dir
177+ dist_info .output_dir = tmp_dir
167178 dist_info .ensure_finalized ()
168- dist_info .run ()
169179 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" )
173189
174190 def _install_namespaces (self , installation_dir , pth_prefix ):
175191 # XXX: Only required to support the deprecated namespace practice
@@ -209,8 +225,7 @@ def _configure_build(
209225 scripts = str (Path (unpacked_wheel , f"{ name } .data" , "scripts" ))
210226
211227 # 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" )
214229 egg_info .ignore_egg_info_in_manifest = True
215230
216231 build = dist .reinitialize_command ("build" , reinit_subcommands = True )
@@ -322,31 +337,29 @@ def _safely_run(self, cmd_name: str):
322337 # needs work.
323338 )
324339
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 )
345355 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 )
350363
351364 return wheel_path
352365
@@ -384,7 +397,7 @@ def _select_strategy(
384397
385398
386399class 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 ]):
388401 ...
389402
390403 def __enter__ (self ):
@@ -400,10 +413,10 @@ def __init__(self, dist: Distribution, name: str, path_entries: List[Path]):
400413 self .name = name
401414 self .path_entries = path_entries
402415
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 ]):
404417 entries = "\n " .join ((str (p .resolve ()) for p in self .path_entries ))
405418 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 )
407420
408421 def __enter__ (self ):
409422 msg = f"""
@@ -440,7 +453,7 @@ def __init__(
440453 self ._file = dist .get_command_obj ("build_py" ).copy_file
441454 super ().__init__ (dist , name , [self .auxiliary_dir ])
442455
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 ]):
444457 self ._create_links (files , mapping )
445458 super ().__call__ (wheel , files , mapping )
446459
@@ -492,7 +505,7 @@ def __init__(self, dist: Distribution, name: str):
492505 self .dist = dist
493506 self .name = name
494507
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 ]):
496509 src_root = self .dist .src_root or os .curdir
497510 top_level = chain (_find_packages (self .dist ), _find_top_level_modules (self .dist ))
498511 package_dir = self .dist .package_dir or {}
@@ -507,11 +520,9 @@ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]
507520
508521 name = f"__editable__.{ self .name } .finder"
509522 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_ ))
513524 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 )
515526
516527 def __enter__ (self ):
517528 msg = "Editable install will be performed using a meta path finder.\n "
0 commit comments