]> jfr.im git - dlqueue.git/blob - venv/lib/python3.11/site-packages/setuptools/command/editable_wheel.py
init: venv aand flask
[dlqueue.git] / venv / lib / python3.11 / site-packages / setuptools / command / editable_wheel.py
1 """
2 Create a wheel that, when installed, will make the source package 'editable'
3 (add it to the interpreter's path, including metadata) per PEP 660. Replaces
4 'setup.py develop'.
5
6 .. note::
7 One of the mechanisms briefly mentioned in PEP 660 to implement editable installs is
8 to create a separated directory inside ``build`` and use a .pth file to point to that
9 directory. In the context of this file such directory is referred as
10 *auxiliary build directory* or ``auxiliary_dir``.
11 """
12
13 import logging
14 import io
15 import os
16 import shutil
17 import sys
18 import traceback
19 from contextlib import suppress
20 from enum import Enum
21 from inspect import cleandoc
22 from itertools import chain
23 from pathlib import Path
24 from tempfile import TemporaryDirectory
25 from typing import (
26 TYPE_CHECKING,
27 Dict,
28 Iterable,
29 Iterator,
30 List,
31 Mapping,
32 Optional,
33 Tuple,
34 TypeVar,
35 Union,
36 )
37
38 from .. import (
39 Command,
40 _normalization,
41 _path,
42 errors,
43 namespaces,
44 )
45 from ..discovery import find_package_path
46 from ..dist import Distribution
47 from ..warnings import (
48 InformationOnly,
49 SetuptoolsDeprecationWarning,
50 SetuptoolsWarning,
51 )
52 from .build_py import build_py as build_py_cls
53
54 if TYPE_CHECKING:
55 from wheel.wheelfile import WheelFile # noqa
56
57 if sys.version_info >= (3, 8):
58 from typing import Protocol
59 elif TYPE_CHECKING:
60 from typing_extensions import Protocol
61 else:
62 from abc import ABC as Protocol
63
64 _Path = Union[str, Path]
65 _P = TypeVar("_P", bound=_Path)
66 _logger = logging.getLogger(__name__)
67
68
69 class _EditableMode(Enum):
70 """
71 Possible editable installation modes:
72 `lenient` (new files automatically added to the package - DEFAULT);
73 `strict` (requires a new installation when files are added/removed); or
74 `compat` (attempts to emulate `python setup.py develop` - DEPRECATED).
75 """
76
77 STRICT = "strict"
78 LENIENT = "lenient"
79 COMPAT = "compat" # TODO: Remove `compat` after Dec/2022.
80
81 @classmethod
82 def convert(cls, mode: Optional[str]) -> "_EditableMode":
83 if not mode:
84 return _EditableMode.LENIENT # default
85
86 _mode = mode.upper()
87 if _mode not in _EditableMode.__members__:
88 raise errors.OptionError(f"Invalid editable mode: {mode!r}. Try: 'strict'.")
89
90 if _mode == "COMPAT":
91 SetuptoolsDeprecationWarning.emit(
92 "Compat editable installs",
93 """
94 The 'compat' editable mode is transitional and will be removed
95 in future versions of `setuptools`.
96 Please adapt your code accordingly to use either the 'strict' or the
97 'lenient' modes.
98 """,
99 see_docs="userguide/development_mode.html",
100 # TODO: define due_date
101 # There is a series of shortcomings with the available editable install
102 # methods, and they are very controversial. This is something that still
103 # needs work.
104 # Moreover, `pip` is still hiding this warning, so users are not aware.
105 )
106
107 return _EditableMode[_mode]
108
109
110 _STRICT_WARNING = """
111 New or renamed files may not be automatically picked up without a new installation.
112 """
113
114 _LENIENT_WARNING = """
115 Options like `package-data`, `include/exclude-package-data` or
116 `packages.find.exclude/include` may have no effect.
117 """
118
119
120 class editable_wheel(Command):
121 """Build 'editable' wheel for development.
122 This command is private and reserved for internal use of setuptools,
123 users should rely on ``setuptools.build_meta`` APIs.
124 """
125
126 description = "DO NOT CALL DIRECTLY, INTERNAL ONLY: create PEP 660 editable wheel"
127
128 user_options = [
129 ("dist-dir=", "d", "directory to put final built distributions in"),
130 ("dist-info-dir=", "I", "path to a pre-build .dist-info directory"),
131 ("mode=", None, cleandoc(_EditableMode.__doc__ or "")),
132 ]
133
134 def initialize_options(self):
135 self.dist_dir = None
136 self.dist_info_dir = None
137 self.project_dir = None
138 self.mode = None
139
140 def finalize_options(self):
141 dist = self.distribution
142 self.project_dir = dist.src_root or os.curdir
143 self.package_dir = dist.package_dir or {}
144 self.dist_dir = Path(self.dist_dir or os.path.join(self.project_dir, "dist"))
145
146 def run(self):
147 try:
148 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)
157 except Exception:
158 traceback.print_exc()
159 project = self.distribution.name or self.distribution.get_name()
160 _DebuggingTips.emit(project=project)
161 raise
162
163 def _ensure_dist_info(self):
164 if self.dist_info_dir is None:
165 dist_info = self.reinitialize_command("dist_info")
166 dist_info.output_dir = self.dist_dir
167 dist_info.ensure_finalized()
168 dist_info.run()
169 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()
173
174 def _install_namespaces(self, installation_dir, pth_prefix):
175 # XXX: Only required to support the deprecated namespace practice
176 dist = self.distribution
177 if not dist.namespace_packages:
178 return
179
180 src_root = Path(self.project_dir, self.package_dir.get("", ".")).resolve()
181 installer = _NamespaceInstaller(dist, installation_dir, pth_prefix, src_root)
182 installer.install_namespaces()
183
184 def _find_egg_info_dir(self) -> Optional[str]:
185 parent_dir = Path(self.dist_info_dir).parent if self.dist_info_dir else Path()
186 candidates = map(str, parent_dir.glob("*.egg-info"))
187 return next(candidates, None)
188
189 def _configure_build(
190 self, name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
191 ):
192 """Configure commands to behave in the following ways:
193
194 - Build commands can write to ``build_lib`` if they really want to...
195 (but this folder is expected to be ignored and modules are expected to live
196 in the project directory...)
197 - Binary extensions should be built in-place (editable_mode = True)
198 - Data/header/script files are not part of the "editable" specification
199 so they are written directly to the unpacked_wheel directory.
200 """
201 # Non-editable files (data, headers, scripts) are written directly to the
202 # unpacked_wheel
203
204 dist = self.distribution
205 wheel = str(unpacked_wheel)
206 build_lib = str(build_lib)
207 data = str(Path(unpacked_wheel, f"{name}.data", "data"))
208 headers = str(Path(unpacked_wheel, f"{name}.data", "headers"))
209 scripts = str(Path(unpacked_wheel, f"{name}.data", "scripts"))
210
211 # 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)
214 egg_info.ignore_egg_info_in_manifest = True
215
216 build = dist.reinitialize_command("build", reinit_subcommands=True)
217 install = dist.reinitialize_command("install", reinit_subcommands=True)
218
219 build.build_platlib = build.build_purelib = build.build_lib = build_lib
220 install.install_purelib = install.install_platlib = install.install_lib = wheel
221 install.install_scripts = build.build_scripts = scripts
222 install.install_headers = headers
223 install.install_data = data
224
225 install_scripts = dist.get_command_obj("install_scripts")
226 install_scripts.no_ep = True
227
228 build.build_temp = str(tmp_dir)
229
230 build_py = dist.get_command_obj("build_py")
231 build_py.compile = False
232 build_py.existing_egg_info_dir = self._find_egg_info_dir()
233
234 self._set_editable_mode()
235
236 build.ensure_finalized()
237 install.ensure_finalized()
238
239 def _set_editable_mode(self):
240 """Set the ``editable_mode`` flag in the build sub-commands"""
241 dist = self.distribution
242 build = dist.get_command_obj("build")
243 for cmd_name in build.get_sub_commands():
244 cmd = dist.get_command_obj(cmd_name)
245 if hasattr(cmd, "editable_mode"):
246 cmd.editable_mode = True
247 elif hasattr(cmd, "inplace"):
248 cmd.inplace = True # backward compatibility with distutils
249
250 def _collect_build_outputs(self) -> Tuple[List[str], Dict[str, str]]:
251 files: List[str] = []
252 mapping: Dict[str, str] = {}
253 build = self.get_finalized_command("build")
254
255 for cmd_name in build.get_sub_commands():
256 cmd = self.get_finalized_command(cmd_name)
257 if hasattr(cmd, "get_outputs"):
258 files.extend(cmd.get_outputs() or [])
259 if hasattr(cmd, "get_output_mapping"):
260 mapping.update(cmd.get_output_mapping() or {})
261
262 return files, mapping
263
264 def _run_build_commands(
265 self, dist_name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
266 ) -> Tuple[List[str], Dict[str, str]]:
267 self._configure_build(dist_name, unpacked_wheel, build_lib, tmp_dir)
268 self._run_build_subcommands()
269 files, mapping = self._collect_build_outputs()
270 self._run_install("headers")
271 self._run_install("scripts")
272 self._run_install("data")
273 return files, mapping
274
275 def _run_build_subcommands(self):
276 """
277 Issue #3501 indicates that some plugins/customizations might rely on:
278
279 1. ``build_py`` not running
280 2. ``build_py`` always copying files to ``build_lib``
281
282 However both these assumptions may be false in editable_wheel.
283 This method implements a temporary workaround to support the ecosystem
284 while the implementations catch up.
285 """
286 # TODO: Once plugins/customisations had the chance to catch up, replace
287 # `self._run_build_subcommands()` with `self.run_command("build")`.
288 # Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023.
289 build: Command = self.get_finalized_command("build")
290 for name in build.get_sub_commands():
291 cmd = self.get_finalized_command(name)
292 if name == "build_py" and type(cmd) != build_py_cls:
293 self._safely_run(name)
294 else:
295 self.run_command(name)
296
297 def _safely_run(self, cmd_name: str):
298 try:
299 return self.run_command(cmd_name)
300 except Exception:
301 SetuptoolsDeprecationWarning.emit(
302 "Customization incompatible with editable install",
303 f"""
304 {traceback.format_exc()}
305
306 If you are seeing this warning it is very likely that a setuptools
307 plugin or customization overrides the `{cmd_name}` command, without
308 taking into consideration how editable installs run build steps
309 starting from setuptools v64.0.0.
310
311 Plugin authors and developers relying on custom build steps are
312 encouraged to update their `{cmd_name}` implementation considering the
313 information about editable installs in
314 https://setuptools.pypa.io/en/latest/userguide/extension.html.
315
316 For the time being `setuptools` will silence this error and ignore
317 the faulty command, but this behaviour will change in future versions.
318 """,
319 # TODO: define due_date
320 # There is a series of shortcomings with the available editable install
321 # methods, and they are very controversial. This is something that still
322 # needs work.
323 )
324
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)
345 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)
350
351 return wheel_path
352
353 def _run_install(self, category: str):
354 has_category = getattr(self.distribution, f"has_{category}", None)
355 if has_category and has_category():
356 _logger.info(f"Installing {category} as non editable")
357 self.run_command(f"install_{category}")
358
359 def _select_strategy(
360 self,
361 name: str,
362 tag: str,
363 build_lib: _Path,
364 ) -> "EditableStrategy":
365 """Decides which strategy to use to implement an editable installation."""
366 build_name = f"__editable__.{name}-{tag}"
367 project_dir = Path(self.project_dir)
368 mode = _EditableMode.convert(self.mode)
369
370 if mode is _EditableMode.STRICT:
371 auxiliary_dir = _empty_dir(Path(self.project_dir, "build", build_name))
372 return _LinkTree(self.distribution, name, auxiliary_dir, build_lib)
373
374 packages = _find_packages(self.distribution)
375 has_simple_layout = _simple_layout(packages, self.package_dir, project_dir)
376 is_compat_mode = mode is _EditableMode.COMPAT
377 if set(self.package_dir) == {""} and has_simple_layout or is_compat_mode:
378 # src-layout(ish) is relatively safe for a simple pth file
379 src_dir = self.package_dir.get("", ".")
380 return _StaticPth(self.distribution, name, [Path(project_dir, src_dir)])
381
382 # Use a MetaPathFinder to avoid adding accidental top-level packages/modules
383 return _TopLevelFinder(self.distribution, name)
384
385
386 class EditableStrategy(Protocol):
387 def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
388 ...
389
390 def __enter__(self):
391 ...
392
393 def __exit__(self, _exc_type, _exc_value, _traceback):
394 ...
395
396
397 class _StaticPth:
398 def __init__(self, dist: Distribution, name: str, path_entries: List[Path]):
399 self.dist = dist
400 self.name = name
401 self.path_entries = path_entries
402
403 def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
404 entries = "\n".join((str(p.resolve()) for p in self.path_entries))
405 contents = _encode_pth(f"{entries}\n")
406 wheel.writestr(f"__editable__.{self.name}.pth", contents)
407
408 def __enter__(self):
409 msg = f"""
410 Editable install will be performed using .pth file to extend `sys.path` with:
411 {list(map(os.fspath, self.path_entries))!r}
412 """
413 _logger.warning(msg + _LENIENT_WARNING)
414 return self
415
416 def __exit__(self, _exc_type, _exc_value, _traceback):
417 ...
418
419
420 class _LinkTree(_StaticPth):
421 """
422 Creates a ``.pth`` file that points to a link tree in the ``auxiliary_dir``.
423
424 This strategy will only link files (not dirs), so it can be implemented in
425 any OS, even if that means using hardlinks instead of symlinks.
426
427 By collocating ``auxiliary_dir`` and the original source code, limitations
428 with hardlinks should be avoided.
429 """
430
431 def __init__(
432 self,
433 dist: Distribution,
434 name: str,
435 auxiliary_dir: _Path,
436 build_lib: _Path,
437 ):
438 self.auxiliary_dir = Path(auxiliary_dir)
439 self.build_lib = Path(build_lib).resolve()
440 self._file = dist.get_command_obj("build_py").copy_file
441 super().__init__(dist, name, [self.auxiliary_dir])
442
443 def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
444 self._create_links(files, mapping)
445 super().__call__(wheel, files, mapping)
446
447 def _normalize_output(self, file: str) -> Optional[str]:
448 # Files relative to build_lib will be normalized to None
449 with suppress(ValueError):
450 path = Path(file).resolve().relative_to(self.build_lib)
451 return str(path).replace(os.sep, '/')
452 return None
453
454 def _create_file(self, relative_output: str, src_file: str, link=None):
455 dest = self.auxiliary_dir / relative_output
456 if not dest.parent.is_dir():
457 dest.parent.mkdir(parents=True)
458 self._file(src_file, dest, link=link)
459
460 def _create_links(self, outputs, output_mapping):
461 self.auxiliary_dir.mkdir(parents=True, exist_ok=True)
462 link_type = "sym" if _can_symlink_files(self.auxiliary_dir) else "hard"
463 mappings = {self._normalize_output(k): v for k, v in output_mapping.items()}
464 mappings.pop(None, None) # remove files that are not relative to build_lib
465
466 for output in outputs:
467 relative = self._normalize_output(output)
468 if relative and relative not in mappings:
469 self._create_file(relative, output)
470
471 for relative, src in mappings.items():
472 self._create_file(relative, src, link=link_type)
473
474 def __enter__(self):
475 msg = "Strict editable install will be performed using a link tree.\n"
476 _logger.warning(msg + _STRICT_WARNING)
477 return self
478
479 def __exit__(self, _exc_type, _exc_value, _traceback):
480 msg = f"""\n
481 Strict editable installation performed using the auxiliary directory:
482 {self.auxiliary_dir}
483
484 Please be careful to not remove this directory, otherwise you might not be able
485 to import/use your package.
486 """
487 InformationOnly.emit("Editable installation.", msg)
488
489
490 class _TopLevelFinder:
491 def __init__(self, dist: Distribution, name: str):
492 self.dist = dist
493 self.name = name
494
495 def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
496 src_root = self.dist.src_root or os.curdir
497 top_level = chain(_find_packages(self.dist), _find_top_level_modules(self.dist))
498 package_dir = self.dist.package_dir or {}
499 roots = _find_package_roots(top_level, package_dir, src_root)
500
501 namespaces_: Dict[str, List[str]] = dict(
502 chain(
503 _find_namespaces(self.dist.packages or [], roots),
504 ((ns, []) for ns in _find_virtual_namespaces(roots)),
505 )
506 )
507
508 name = f"__editable__.{self.name}.finder"
509 finder = _normalization.safe_identifier(name)
510 content = bytes(_finder_template(name, roots, namespaces_), "utf-8")
511 wheel.writestr(f"{finder}.py", content)
512
513 content = _encode_pth(f"import {finder}; {finder}.install()")
514 wheel.writestr(f"__editable__.{self.name}.pth", content)
515
516 def __enter__(self):
517 msg = "Editable install will be performed using a meta path finder.\n"
518 _logger.warning(msg + _LENIENT_WARNING)
519 return self
520
521 def __exit__(self, _exc_type, _exc_value, _traceback):
522 msg = """\n
523 Please be careful with folders in your working directory with the same
524 name as your package as they may take precedence during imports.
525 """
526 InformationOnly.emit("Editable installation.", msg)
527
528
529 def _encode_pth(content: str) -> bytes:
530 """.pth files are always read with 'locale' encoding, the recommendation
531 from the cpython core developers is to write them as ``open(path, "w")``
532 and ignore warnings (see python/cpython#77102, pypa/setuptools#3937).
533 This function tries to simulate this behaviour without having to create an
534 actual file, in a way that supports a range of active Python versions.
535 (There seems to be some variety in the way different version of Python handle
536 ``encoding=None``, not all of them use ``locale.getpreferredencoding(False)``).
537 """
538 encoding = "locale" if sys.version_info >= (3, 10) else None
539 with io.BytesIO() as buffer:
540 wrapper = io.TextIOWrapper(buffer, encoding)
541 wrapper.write(content)
542 wrapper.flush()
543 buffer.seek(0)
544 return buffer.read()
545
546
547 def _can_symlink_files(base_dir: Path) -> bool:
548 with TemporaryDirectory(dir=str(base_dir.resolve())) as tmp:
549 path1, path2 = Path(tmp, "file1.txt"), Path(tmp, "file2.txt")
550 path1.write_text("file1", encoding="utf-8")
551 with suppress(AttributeError, NotImplementedError, OSError):
552 os.symlink(path1, path2)
553 if path2.is_symlink() and path2.read_text(encoding="utf-8") == "file1":
554 return True
555
556 try:
557 os.link(path1, path2) # Ensure hard links can be created
558 except Exception as ex:
559 msg = (
560 "File system does not seem to support either symlinks or hard links. "
561 "Strict editable installs require one of them to be supported."
562 )
563 raise LinksNotSupported(msg) from ex
564 return False
565
566
567 def _simple_layout(
568 packages: Iterable[str], package_dir: Dict[str, str], project_dir: Path
569 ) -> bool:
570 """Return ``True`` if:
571 - all packages are contained by the same parent directory, **and**
572 - all packages become importable if the parent directory is added to ``sys.path``.
573
574 >>> _simple_layout(['a'], {"": "src"}, "/tmp/myproj")
575 True
576 >>> _simple_layout(['a', 'a.b'], {"": "src"}, "/tmp/myproj")
577 True
578 >>> _simple_layout(['a', 'a.b'], {}, "/tmp/myproj")
579 True
580 >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"": "src"}, "/tmp/myproj")
581 True
582 >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "a", "b": "b"}, ".")
583 True
584 >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "_a", "b": "_b"}, ".")
585 False
586 >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "_a"}, "/tmp/myproj")
587 False
588 >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a.a1.a2": "_a2"}, ".")
589 False
590 >>> _simple_layout(['a', 'a.b'], {"": "src", "a.b": "_ab"}, "/tmp/myproj")
591 False
592 >>> # Special cases, no packages yet:
593 >>> _simple_layout([], {"": "src"}, "/tmp/myproj")
594 True
595 >>> _simple_layout([], {"a": "_a", "": "src"}, "/tmp/myproj")
596 False
597 """
598 layout = {pkg: find_package_path(pkg, package_dir, project_dir) for pkg in packages}
599 if not layout:
600 return set(package_dir) in ({}, {""})
601 parent = os.path.commonpath([_parent_path(k, v) for k, v in layout.items()])
602 return all(
603 _path.same_path(Path(parent, *key.split('.')), value)
604 for key, value in layout.items()
605 )
606
607
608 def _parent_path(pkg, pkg_path):
609 """Infer the parent path containing a package, that if added to ``sys.path`` would
610 allow importing that package.
611 When ``pkg`` is directly mapped into a directory with a different name, return its
612 own path.
613 >>> _parent_path("a", "src/a")
614 'src'
615 >>> _parent_path("b", "src/c")
616 'src/c'
617 """
618 parent = pkg_path[: -len(pkg)] if pkg_path.endswith(pkg) else pkg_path
619 return parent.rstrip("/" + os.sep)
620
621
622 def _find_packages(dist: Distribution) -> Iterator[str]:
623 yield from iter(dist.packages or [])
624
625 py_modules = dist.py_modules or []
626 nested_modules = [mod for mod in py_modules if "." in mod]
627 if dist.ext_package:
628 yield dist.ext_package
629 else:
630 ext_modules = dist.ext_modules or []
631 nested_modules += [x.name for x in ext_modules if "." in x.name]
632
633 for module in nested_modules:
634 package, _, _ = module.rpartition(".")
635 yield package
636
637
638 def _find_top_level_modules(dist: Distribution) -> Iterator[str]:
639 py_modules = dist.py_modules or []
640 yield from (mod for mod in py_modules if "." not in mod)
641
642 if not dist.ext_package:
643 ext_modules = dist.ext_modules or []
644 yield from (x.name for x in ext_modules if "." not in x.name)
645
646
647 def _find_package_roots(
648 packages: Iterable[str],
649 package_dir: Mapping[str, str],
650 src_root: _Path,
651 ) -> Dict[str, str]:
652 pkg_roots: Dict[str, str] = {
653 pkg: _absolute_root(find_package_path(pkg, package_dir, src_root))
654 for pkg in sorted(packages)
655 }
656
657 return _remove_nested(pkg_roots)
658
659
660 def _absolute_root(path: _Path) -> str:
661 """Works for packages and top-level modules"""
662 path_ = Path(path)
663 parent = path_.parent
664
665 if path_.exists():
666 return str(path_.resolve())
667 else:
668 return str(parent.resolve() / path_.name)
669
670
671 def _find_virtual_namespaces(pkg_roots: Dict[str, str]) -> Iterator[str]:
672 """By carefully designing ``package_dir``, it is possible to implement the logical
673 structure of PEP 420 in a package without the corresponding directories.
674
675 Moreover a parent package can be purposefully/accidentally skipped in the discovery
676 phase (e.g. ``find_packages(include=["mypkg.*"])``, when ``mypkg.foo`` is included
677 by ``mypkg`` itself is not).
678 We consider this case to also be a virtual namespace (ignoring the original
679 directory) to emulate a non-editable installation.
680
681 This function will try to find these kinds of namespaces.
682 """
683 for pkg in pkg_roots:
684 if "." not in pkg:
685 continue
686 parts = pkg.split(".")
687 for i in range(len(parts) - 1, 0, -1):
688 partial_name = ".".join(parts[:i])
689 path = Path(find_package_path(partial_name, pkg_roots, ""))
690 if not path.exists() or partial_name not in pkg_roots:
691 # partial_name not in pkg_roots ==> purposefully/accidentally skipped
692 yield partial_name
693
694
695 def _find_namespaces(
696 packages: List[str], pkg_roots: Dict[str, str]
697 ) -> Iterator[Tuple[str, List[str]]]:
698 for pkg in packages:
699 path = find_package_path(pkg, pkg_roots, "")
700 if Path(path).exists() and not Path(path, "__init__.py").exists():
701 yield (pkg, [path])
702
703
704 def _remove_nested(pkg_roots: Dict[str, str]) -> Dict[str, str]:
705 output = dict(pkg_roots.copy())
706
707 for pkg, path in reversed(list(pkg_roots.items())):
708 if any(
709 pkg != other and _is_nested(pkg, path, other, other_path)
710 for other, other_path in pkg_roots.items()
711 ):
712 output.pop(pkg)
713
714 return output
715
716
717 def _is_nested(pkg: str, pkg_path: str, parent: str, parent_path: str) -> bool:
718 """
719 Return ``True`` if ``pkg`` is nested inside ``parent`` both logically and in the
720 file system.
721 >>> _is_nested("a.b", "path/a/b", "a", "path/a")
722 True
723 >>> _is_nested("a.b", "path/a/b", "a", "otherpath/a")
724 False
725 >>> _is_nested("a.b", "path/a/b", "c", "path/c")
726 False
727 >>> _is_nested("a.a", "path/a/a", "a", "path/a")
728 True
729 >>> _is_nested("b.a", "path/b/a", "a", "path/a")
730 False
731 """
732 norm_pkg_path = _path.normpath(pkg_path)
733 rest = pkg.replace(parent, "", 1).strip(".").split(".")
734 return pkg.startswith(parent) and norm_pkg_path == _path.normpath(
735 Path(parent_path, *rest)
736 )
737
738
739 def _empty_dir(dir_: _P) -> _P:
740 """Create a directory ensured to be empty. Existing files may be removed."""
741 shutil.rmtree(dir_, ignore_errors=True)
742 os.makedirs(dir_)
743 return dir_
744
745
746 class _NamespaceInstaller(namespaces.Installer):
747 def __init__(self, distribution, installation_dir, editable_name, src_root):
748 self.distribution = distribution
749 self.src_root = src_root
750 self.installation_dir = installation_dir
751 self.editable_name = editable_name
752 self.outputs = []
753 self.dry_run = False
754
755 def _get_target(self):
756 """Installation target."""
757 return os.path.join(self.installation_dir, self.editable_name)
758
759 def _get_root(self):
760 """Where the modules/packages should be loaded from."""
761 return repr(str(self.src_root))
762
763
764 _FINDER_TEMPLATE = """\
765 import sys
766 from importlib.machinery import ModuleSpec, PathFinder
767 from importlib.machinery import all_suffixes as module_suffixes
768 from importlib.util import spec_from_file_location
769 from itertools import chain
770 from pathlib import Path
771
772 MAPPING = {mapping!r}
773 NAMESPACES = {namespaces!r}
774 PATH_PLACEHOLDER = {name!r} + ".__path_hook__"
775
776
777 class _EditableFinder: # MetaPathFinder
778 @classmethod
779 def find_spec(cls, fullname, path=None, target=None):
780 # Top-level packages and modules (we know these exist in the FS)
781 if fullname in MAPPING:
782 pkg_path = MAPPING[fullname]
783 return cls._find_spec(fullname, Path(pkg_path))
784
785 # Handle immediate children modules (required for namespaces to work)
786 # To avoid problems with case sensitivity in the file system we delegate
787 # to the importlib.machinery implementation.
788 parent, _, child = fullname.rpartition(".")
789 if parent and parent in MAPPING:
790 return PathFinder.find_spec(fullname, path=[MAPPING[parent]])
791
792 # Other levels of nesting should be handled automatically by importlib
793 # using the parent path.
794 return None
795
796 @classmethod
797 def _find_spec(cls, fullname, candidate_path):
798 init = candidate_path / "__init__.py"
799 candidates = (candidate_path.with_suffix(x) for x in module_suffixes())
800 for candidate in chain([init], candidates):
801 if candidate.exists():
802 return spec_from_file_location(fullname, candidate)
803
804
805 class _EditableNamespaceFinder: # PathEntryFinder
806 @classmethod
807 def _path_hook(cls, path):
808 if path == PATH_PLACEHOLDER:
809 return cls
810 raise ImportError
811
812 @classmethod
813 def _paths(cls, fullname):
814 # Ensure __path__ is not empty for the spec to be considered a namespace.
815 return NAMESPACES[fullname] or MAPPING.get(fullname) or [PATH_PLACEHOLDER]
816
817 @classmethod
818 def find_spec(cls, fullname, target=None):
819 if fullname in NAMESPACES:
820 spec = ModuleSpec(fullname, None, is_package=True)
821 spec.submodule_search_locations = cls._paths(fullname)
822 return spec
823 return None
824
825 @classmethod
826 def find_module(cls, fullname):
827 return None
828
829
830 def install():
831 if not any(finder == _EditableFinder for finder in sys.meta_path):
832 sys.meta_path.append(_EditableFinder)
833
834 if not NAMESPACES:
835 return
836
837 if not any(hook == _EditableNamespaceFinder._path_hook for hook in sys.path_hooks):
838 # PathEntryFinder is needed to create NamespaceSpec without private APIS
839 sys.path_hooks.append(_EditableNamespaceFinder._path_hook)
840 if PATH_PLACEHOLDER not in sys.path:
841 sys.path.append(PATH_PLACEHOLDER) # Used just to trigger the path hook
842 """
843
844
845 def _finder_template(
846 name: str, mapping: Mapping[str, str], namespaces: Dict[str, List[str]]
847 ) -> str:
848 """Create a string containing the code for the``MetaPathFinder`` and
849 ``PathEntryFinder``.
850 """
851 mapping = dict(sorted(mapping.items(), key=lambda p: p[0]))
852 return _FINDER_TEMPLATE.format(name=name, mapping=mapping, namespaces=namespaces)
853
854
855 class LinksNotSupported(errors.FileError):
856 """File system does not seem to support either symlinks or hard links."""
857
858
859 class _DebuggingTips(SetuptoolsWarning):
860 _SUMMARY = "Problem in editable installation."
861 _DETAILS = """
862 An error happened while installing `{project}` in editable mode.
863
864 The following steps are recommended to help debug this problem:
865
866 - Try to install the project normally, without using the editable mode.
867 Does the error still persist?
868 (If it does, try fixing the problem before attempting the editable mode).
869 - If you are using binary extensions, make sure you have all OS-level
870 dependencies installed (e.g. compilers, toolchains, binary libraries, ...).
871 - Try the latest version of setuptools (maybe the error was already fixed).
872 - If you (or your project dependencies) are using any setuptools extension
873 or customization, make sure they support the editable mode.
874
875 After following the steps above, if the problem still persists and
876 you think this is related to how setuptools handles editable installations,
877 please submit a reproducible example
878 (see https://stackoverflow.com/help/minimal-reproducible-example) to:
879
880 https://github.com/pypa/setuptools/issues
881 """
882 _SEE_DOCS = "userguide/development_mode.html"