python (3.11.7)

(root)/
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 ESC[4;38;5;81m_EditableMode(ESC[4;38;5;149mEnum):
      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 ESC[4;38;5;81meditable_wheel(ESC[4;38;5;149mCommand):
     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 ESC[4;38;5;81mEditableStrategy(ESC[4;38;5;149mProtocol):
     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 ESC[4;38;5;81m_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 ESC[4;38;5;81m_LinkTree(ESC[4;38;5;149m_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 ESC[4;38;5;81m_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 ESC[4;38;5;81m_NamespaceInstaller(ESC[4;38;5;149mnamespacesESC[4;38;5;149m.ESC[4;38;5;149mInstaller):
     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 ESC[4;38;5;81mInformationOnly(ESC[4;38;5;149mUserWarning):
     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 ESC[4;38;5;81mLinksNotSupported(ESC[4;38;5;149merrorsESC[4;38;5;149m.ESC[4;38;5;149mFileError):
     844      """File system does not seem to support either symlinks or hard links."""