python (3.11.7)
       1  import collections
       2  import logging
       3  import os
       4  from typing import Container, Dict, Generator, Iterable, List, NamedTuple, Optional, Set
       5  
       6  from pip._vendor.packaging.utils import canonicalize_name
       7  from pip._vendor.packaging.version import Version
       8  
       9  from pip._internal.exceptions import BadCommand, InstallationError
      10  from pip._internal.metadata import BaseDistribution, get_environment
      11  from pip._internal.req.constructors import (
      12      install_req_from_editable,
      13      install_req_from_line,
      14  )
      15  from pip._internal.req.req_file import COMMENT_RE
      16  from pip._internal.utils.direct_url_helpers import direct_url_as_pep440_direct_reference
      17  
      18  logger = logging.getLogger(__name__)
      19  
      20  
      21  class ESC[4;38;5;81m_EditableInfo(ESC[4;38;5;149mNamedTuple):
      22      requirement: str
      23      comments: List[str]
      24  
      25  
      26  def freeze(
      27      requirement: Optional[List[str]] = None,
      28      local_only: bool = False,
      29      user_only: bool = False,
      30      paths: Optional[List[str]] = None,
      31      isolated: bool = False,
      32      exclude_editable: bool = False,
      33      skip: Container[str] = (),
      34  ) -> Generator[str, None, None]:
      35      installations: Dict[str, FrozenRequirement] = {}
      36  
      37      dists = get_environment(paths).iter_installed_distributions(
      38          local_only=local_only,
      39          skip=(),
      40          user_only=user_only,
      41      )
      42      for dist in dists:
      43          req = FrozenRequirement.from_dist(dist)
      44          if exclude_editable and req.editable:
      45              continue
      46          installations[req.canonical_name] = req
      47  
      48      if requirement:
      49          # the options that don't get turned into an InstallRequirement
      50          # should only be emitted once, even if the same option is in multiple
      51          # requirements files, so we need to keep track of what has been emitted
      52          # so that we don't emit it again if it's seen again
      53          emitted_options: Set[str] = set()
      54          # keep track of which files a requirement is in so that we can
      55          # give an accurate warning if a requirement appears multiple times.
      56          req_files: Dict[str, List[str]] = collections.defaultdict(list)
      57          for req_file_path in requirement:
      58              with open(req_file_path) as req_file:
      59                  for line in req_file:
      60                      if (
      61                          not line.strip()
      62                          or line.strip().startswith("#")
      63                          or line.startswith(
      64                              (
      65                                  "-r",
      66                                  "--requirement",
      67                                  "-f",
      68                                  "--find-links",
      69                                  "-i",
      70                                  "--index-url",
      71                                  "--pre",
      72                                  "--trusted-host",
      73                                  "--process-dependency-links",
      74                                  "--extra-index-url",
      75                                  "--use-feature",
      76                              )
      77                          )
      78                      ):
      79                          line = line.rstrip()
      80                          if line not in emitted_options:
      81                              emitted_options.add(line)
      82                              yield line
      83                          continue
      84  
      85                      if line.startswith("-e") or line.startswith("--editable"):
      86                          if line.startswith("-e"):
      87                              line = line[2:].strip()
      88                          else:
      89                              line = line[len("--editable") :].strip().lstrip("=")
      90                          line_req = install_req_from_editable(
      91                              line,
      92                              isolated=isolated,
      93                          )
      94                      else:
      95                          line_req = install_req_from_line(
      96                              COMMENT_RE.sub("", line).strip(),
      97                              isolated=isolated,
      98                          )
      99  
     100                      if not line_req.name:
     101                          logger.info(
     102                              "Skipping line in requirement file [%s] because "
     103                              "it's not clear what it would install: %s",
     104                              req_file_path,
     105                              line.strip(),
     106                          )
     107                          logger.info(
     108                              "  (add #egg=PackageName to the URL to avoid"
     109                              " this warning)"
     110                          )
     111                      else:
     112                          line_req_canonical_name = canonicalize_name(line_req.name)
     113                          if line_req_canonical_name not in installations:
     114                              # either it's not installed, or it is installed
     115                              # but has been processed already
     116                              if not req_files[line_req.name]:
     117                                  logger.warning(
     118                                      "Requirement file [%s] contains %s, but "
     119                                      "package %r is not installed",
     120                                      req_file_path,
     121                                      COMMENT_RE.sub("", line).strip(),
     122                                      line_req.name,
     123                                  )
     124                              else:
     125                                  req_files[line_req.name].append(req_file_path)
     126                          else:
     127                              yield str(installations[line_req_canonical_name]).rstrip()
     128                              del installations[line_req_canonical_name]
     129                              req_files[line_req.name].append(req_file_path)
     130  
     131          # Warn about requirements that were included multiple times (in a
     132          # single requirements file or in different requirements files).
     133          for name, files in req_files.items():
     134              if len(files) > 1:
     135                  logger.warning(
     136                      "Requirement %s included multiple times [%s]",
     137                      name,
     138                      ", ".join(sorted(set(files))),
     139                  )
     140  
     141          yield ("## The following requirements were added by pip freeze:")
     142      for installation in sorted(installations.values(), key=lambda x: x.name.lower()):
     143          if installation.canonical_name not in skip:
     144              yield str(installation).rstrip()
     145  
     146  
     147  def _format_as_name_version(dist: BaseDistribution) -> str:
     148      dist_version = dist.version
     149      if isinstance(dist_version, Version):
     150          return f"{dist.raw_name}=={dist_version}"
     151      return f"{dist.raw_name}==={dist_version}"
     152  
     153  
     154  def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
     155      """
     156      Compute and return values (req, comments) for use in
     157      FrozenRequirement.from_dist().
     158      """
     159      editable_project_location = dist.editable_project_location
     160      assert editable_project_location
     161      location = os.path.normcase(os.path.abspath(editable_project_location))
     162  
     163      from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs
     164  
     165      vcs_backend = vcs.get_backend_for_dir(location)
     166  
     167      if vcs_backend is None:
     168          display = _format_as_name_version(dist)
     169          logger.debug(
     170              'No VCS found for editable requirement "%s" in: %r',
     171              display,
     172              location,
     173          )
     174          return _EditableInfo(
     175              requirement=location,
     176              comments=[f"# Editable install with no version control ({display})"],
     177          )
     178  
     179      vcs_name = type(vcs_backend).__name__
     180  
     181      try:
     182          req = vcs_backend.get_src_requirement(location, dist.raw_name)
     183      except RemoteNotFoundError:
     184          display = _format_as_name_version(dist)
     185          return _EditableInfo(
     186              requirement=location,
     187              comments=[f"# Editable {vcs_name} install with no remote ({display})"],
     188          )
     189      except RemoteNotValidError as ex:
     190          display = _format_as_name_version(dist)
     191          return _EditableInfo(
     192              requirement=location,
     193              comments=[
     194                  f"# Editable {vcs_name} install ({display}) with either a deleted "
     195                  f"local remote or invalid URI:",
     196                  f"# '{ex.url}'",
     197              ],
     198          )
     199      except BadCommand:
     200          logger.warning(
     201              "cannot determine version of editable source in %s "
     202              "(%s command not found in path)",
     203              location,
     204              vcs_backend.name,
     205          )
     206          return _EditableInfo(requirement=location, comments=[])
     207      except InstallationError as exc:
     208          logger.warning("Error when trying to get requirement for VCS system %s", exc)
     209      else:
     210          return _EditableInfo(requirement=req, comments=[])
     211  
     212      logger.warning("Could not determine repository location of %s", location)
     213  
     214      return _EditableInfo(
     215          requirement=location,
     216          comments=["## !! Could not determine repository location"],
     217      )
     218  
     219  
     220  class ESC[4;38;5;81mFrozenRequirement:
     221      def __init__(
     222          self,
     223          name: str,
     224          req: str,
     225          editable: bool,
     226          comments: Iterable[str] = (),
     227      ) -> None:
     228          self.name = name
     229          self.canonical_name = canonicalize_name(name)
     230          self.req = req
     231          self.editable = editable
     232          self.comments = comments
     233  
     234      @classmethod
     235      def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
     236          editable = dist.editable
     237          if editable:
     238              req, comments = _get_editable_info(dist)
     239          else:
     240              comments = []
     241              direct_url = dist.direct_url
     242              if direct_url:
     243                  # if PEP 610 metadata is present, use it
     244                  req = direct_url_as_pep440_direct_reference(direct_url, dist.raw_name)
     245              else:
     246                  # name==version requirement
     247                  req = _format_as_name_version(dist)
     248  
     249          return cls(dist.raw_name, req, editable, comments=comments)
     250  
     251      def __str__(self) -> str:
     252          req = self.req
     253          if self.editable:
     254              req = f"-e {req}"
     255          return "\n".join(list(self.comments) + [str(req)]) + "\n"