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