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
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``.
19 from contextlib
import suppress
21 from inspect
import cleandoc
22 from itertools
import chain
23 from pathlib
import Path
24 from tempfile
import TemporaryDirectory
45 from ..discovery
import find_package_path
46 from ..dist
import Distribution
47 from ..warnings
import (
49 SetuptoolsDeprecationWarning
,
52 from .build_py
import build_py
as build_py_cls
55 from wheel
.wheelfile
import WheelFile
# noqa
57 if sys
.version_info
>= (3, 8):
58 from typing
import Protocol
60 from typing_extensions
import Protocol
62 from abc
import ABC
as Protocol
64 _Path
= Union
[str, Path
]
65 _P
= TypeVar("_P", bound
=_Path
)
66 _logger
= logging
.getLogger(__name__
)
69 class _EditableMode(Enum
):
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).
79 COMPAT
= "compat" # TODO: Remove `compat` after Dec/2022.
82 def convert(cls
, mode
: Optional
[str]) -> "_EditableMode":
84 return _EditableMode
.LENIENT
# default
87 if _mode
not in _EditableMode
.__members
__:
88 raise errors
.OptionError(f
"Invalid editable mode: {mode!r}. Try: 'strict'.")
91 SetuptoolsDeprecationWarning
.emit(
92 "Compat editable installs",
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
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
104 # Moreover, `pip` is still hiding this warning, so users are not aware.
107 return _EditableMode
[_mode
]
110 _STRICT_WARNING
= """
111 New or renamed files may not be automatically picked up without a new installation.
114 _LENIENT_WARNING
= """
115 Options like `package-data`, `include/exclude-package-data` or
116 `packages.find.exclude/include` may have no effect.
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.
126 description
= "DO NOT CALL DIRECTLY, INTERNAL ONLY: create PEP 660 editable wheel"
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 "")),
134 def initialize_options(self
):
136 self
.dist_info_dir
= None
137 self
.project_dir
= None
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"))
148 self
.dist_dir
.mkdir(exist_ok
=True)
149 self
._ensure
_dist
_info
()
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
)
156 self
._create
_wheel
_file
(bdist_wheel
)
158 traceback
.print_exc()
159 project
= self
.distribution
.name
or self
.distribution
.get_name()
160 _DebuggingTips
.emit(project
=project
)
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()
169 self
.dist_info_dir
= dist_info
.dist_info_dir
171 assert str(self
.dist_info_dir
).endswith(".dist-info")
172 assert Path(self
.dist_info_dir
, "METADATA").exists()
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
:
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()
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)
189 def _configure_build(
190 self
, name
: str, unpacked_wheel
: _Path
, build_lib
: _Path
, tmp_dir
: _Path
192 """Configure commands to behave in the following ways:
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.
201 # Non-editable files (data, headers, scripts) are written directly to the
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"))
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
216 build
= dist
.reinitialize_command("build", reinit_subcommands
=True)
217 install
= dist
.reinitialize_command("install", reinit_subcommands
=True)
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
225 install_scripts
= dist
.get_command_obj("install_scripts")
226 install_scripts
.no_ep
= True
228 build
.build_temp
= str(tmp_dir
)
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
()
234 self
._set
_editable
_mode
()
236 build
.ensure_finalized()
237 install
.ensure_finalized()
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
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")
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 {})
262 return files
, mapping
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
275 def _run_build_subcommands(self
):
277 Issue #3501 indicates that some plugins/customizations might rely on:
279 1. ``build_py`` not running
280 2. ``build_py`` always copying files to ``build_lib``
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.
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
)
295 self
.run_command(name
)
297 def _safely_run(self
, cmd_name
: str):
299 return self
.run_command(cmd_name
)
301 SetuptoolsDeprecationWarning
.emit(
302 "Customization incompatible with editable install",
304 {traceback.format_exc()}
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.
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.
316 For the time being `setuptools` will silence this error and ignore
317 the faulty command, but this behaviour will change in future versions.
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
325 def _create_wheel_file(self
, bdist_wheel
):
326 from wheel
.wheelfile
import WheelFile
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():
337 unpacked_wheel
= TemporaryDirectory(suffix
=archive_name
)
338 build_lib
= TemporaryDirectory(suffix
=".build-lib")
339 build_tmp
= TemporaryDirectory(suffix
=".build-temp")
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
)
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}")
359 def _select_strategy(
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
)
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
)
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
)])
382 # Use a MetaPathFinder to avoid adding accidental top-level packages/modules
383 return _TopLevelFinder(self
.distribution
, name
)
386 class EditableStrategy(Protocol
):
387 def __call__(self
, wheel
: "WheelFile", files
: List
[str], mapping
: Dict
[str, str]):
393 def __exit__(self
, _exc_type
, _exc_value
, _traceback
):
398 def __init__(self
, dist
: Distribution
, name
: str, path_entries
: List
[Path
]):
401 self
.path_entries
= path_entries
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
)
410 Editable install will be performed using .pth file to extend `sys.path` with:
411 {list(map(os.fspath, self.path_entries))!r}
413 _logger
.warning(msg
+ _LENIENT_WARNING
)
416 def __exit__(self
, _exc_type
, _exc_value
, _traceback
):
420 class _LinkTree(_StaticPth
):
422 Creates a ``.pth`` file that points to a link tree in the ``auxiliary_dir``.
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.
427 By collocating ``auxiliary_dir`` and the original source code, limitations
428 with hardlinks should be avoided.
435 auxiliary_dir
: _Path
,
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
])
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
)
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
, '/')
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
)
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
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
)
471 for relative
, src
in mappings
.items():
472 self
._create
_file
(relative
, src
, link
=link_type
)
475 msg
= "Strict editable install will be performed using a link tree.\n"
476 _logger
.warning(msg
+ _STRICT_WARNING
)
479 def __exit__(self
, _exc_type
, _exc_value
, _traceback
):
481 Strict editable installation performed using the auxiliary directory:
484 Please be careful to not remove this directory, otherwise you might not be able
485 to import/use your package.
487 InformationOnly
.emit("Editable installation.", msg
)
490 class _TopLevelFinder
:
491 def __init__(self
, dist
: Distribution
, name
: str):
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
)
501 namespaces_
: Dict
[str, List
[str]] = dict(
503 _find_namespaces(self
.dist
.packages
or [], roots
),
504 ((ns
, []) for ns
in _find_virtual_namespaces(roots
)),
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
)
513 content
= _encode_pth(f
"import {finder}; {finder}.install()")
514 wheel
.writestr(f
"__editable__.{self.name}.pth", content
)
517 msg
= "Editable install will be performed using a meta path finder.\n"
518 _logger
.warning(msg
+ _LENIENT_WARNING
)
521 def __exit__(self
, _exc_type
, _exc_value
, _traceback
):
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.
526 InformationOnly
.emit("Editable installation.", msg
)
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)``).
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
)
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":
557 os
.link(path1
, path2
) # Ensure hard links can be created
558 except Exception as ex
:
560 "File system does not seem to support either symlinks or hard links. "
561 "Strict editable installs require one of them to be supported."
563 raise LinksNotSupported(msg
) from ex
568 packages
: Iterable
[str], package_dir
: Dict
[str, str], project_dir
: Path
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``.
574 >>> _simple_layout(['a'], {"": "src"}, "/tmp/myproj")
576 >>> _simple_layout(['a', 'a.b'], {"": "src"}, "/tmp/myproj")
578 >>> _simple_layout(['a', 'a.b'], {}, "/tmp/myproj")
580 >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"": "src"}, "/tmp/myproj")
582 >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "a", "b": "b"}, ".")
584 >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "_a", "b": "_b"}, ".")
586 >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "_a"}, "/tmp/myproj")
588 >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a.a1.a2": "_a2"}, ".")
590 >>> _simple_layout(['a', 'a.b'], {"": "src", "a.b": "_ab"}, "/tmp/myproj")
592 >>> # Special cases, no packages yet:
593 >>> _simple_layout([], {"": "src"}, "/tmp/myproj")
595 >>> _simple_layout([], {"a": "_a", "": "src"}, "/tmp/myproj")
598 layout
= {pkg: find_package_path(pkg, package_dir, project_dir) for pkg in packages}
600 return set(package_dir
) in ({}, {""}
)
601 parent
= os
.path
.commonpath([_parent_path(k
, v
) for k
, v
in layout
.items()])
603 _path
.same_path(Path(parent
, *key
.split('.')), value
)
604 for key
, value
in layout
.items()
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
613 >>> _parent_path("a", "src/a")
615 >>> _parent_path("b", "src/c")
618 parent
= pkg_path
[: -len(pkg
)] if pkg_path
.endswith(pkg
) else pkg_path
619 return parent
.rstrip("/" + os
.sep
)
622 def _find_packages(dist
: Distribution
) -> Iterator
[str]:
623 yield from iter(dist
.packages
or [])
625 py_modules
= dist
.py_modules
or []
626 nested_modules
= [mod
for mod
in py_modules
if "." in mod
]
628 yield dist
.ext_package
630 ext_modules
= dist
.ext_modules
or []
631 nested_modules
+= [x
.name
for x
in ext_modules
if "." in x
.name
]
633 for module
in nested_modules
:
634 package
, _
, _
= module
.rpartition(".")
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
)
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
)
647 def _find_package_roots(
648 packages
: Iterable
[str],
649 package_dir
: Mapping
[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
)
657 return _remove_nested(pkg_roots
)
660 def _absolute_root(path
: _Path
) -> str:
661 """Works for packages and top-level modules"""
663 parent
= path_
.parent
666 return str(path_
.resolve())
668 return str(parent
.resolve() / path_
.name
)
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.
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.
681 This function will try to find these kinds of namespaces.
683 for pkg
in pkg_roots
:
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
695 def _find_namespaces(
696 packages
: List
[str], pkg_roots
: Dict
[str, str]
697 ) -> Iterator
[Tuple
[str, List
[str]]]:
699 path
= find_package_path(pkg
, pkg_roots
, "")
700 if Path(path
).exists() and not Path(path
, "__init__.py").exists():
704 def _remove_nested(pkg_roots
: Dict
[str, str]) -> Dict
[str, str]:
705 output
= dict(pkg_roots
.copy())
707 for pkg
, path
in reversed(list(pkg_roots
.items())):
709 pkg
!= other
and _is_nested(pkg
, path
, other
, other_path
)
710 for other
, other_path
in pkg_roots
.items()
717 def _is_nested(pkg
: str, pkg_path
: str, parent
: str, parent_path
: str) -> bool:
719 Return ``True`` if ``pkg`` is nested inside ``parent`` both logically and in the
721 >>> _is_nested("a.b", "path/a/b", "a", "path/a")
723 >>> _is_nested("a.b", "path/a/b", "a", "otherpath/a")
725 >>> _is_nested("a.b", "path/a/b", "c", "path/c")
727 >>> _is_nested("a.a", "path/a/a", "a", "path/a")
729 >>> _is_nested("b.a", "path/b/a", "a", "path/a")
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
)
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)
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
755 def _get_target(self
):
756 """Installation target."""
757 return os
.path
.join(self
.installation_dir
, self
.editable_name
)
760 """Where the modules/packages should be loaded from."""
761 return repr(str(self
.src_root
))
764 _FINDER_TEMPLATE
= """\
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
772 MAPPING = {mapping!r}
773 NAMESPACES = {namespaces!r}
774 PATH_PLACEHOLDER = {name!r} + ".__path_hook__"
777 class _EditableFinder: # MetaPathFinder
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))
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]])
792 # Other levels of nesting should be handled automatically by importlib
793 # using the parent path.
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)
805 class _EditableNamespaceFinder: # PathEntryFinder
807 def _path_hook(cls, path):
808 if path == PATH_PLACEHOLDER:
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]
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)
826 def find_module(cls, fullname):
831 if not any(finder == _EditableFinder for finder in sys.meta_path):
832 sys.meta_path.append(_EditableFinder)
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
845 def _finder_template(
846 name
: str, mapping
: Mapping
[str, str], namespaces
: Dict
[str, List
[str]]
848 """Create a string containing the code for the``MetaPathFinder`` and
851 mapping
= dict(sorted(mapping
.items(), key
=lambda p
: p
[0]))
852 return _FINDER_TEMPLATE
.format(name
=name
, mapping
=mapping
, namespaces
=namespaces
)
855 class LinksNotSupported(errors
.FileError
):
856 """File system does not seem to support either symlinks or hard links."""
859 class _DebuggingTips(SetuptoolsWarning
):
860 _SUMMARY
= "Problem in editable installation."
862 An error happened while installing `{project}` in editable mode.
864 The following steps are recommended to help debug this problem:
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.
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:
880 https://github.com/pypa/setuptools/issues
882 _SEE_DOCS
= "userguide/development_mode.html"