python (3.12.0)

(root)/
lib/
python3.12/
venv/
__init__.py
       1  """
       2  Virtual environment (venv) package for Python. Based on PEP 405.
       3  
       4  Copyright (C) 2011-2014 Vinay Sajip.
       5  Licensed to the PSF under a contributor agreement.
       6  """
       7  import logging
       8  import os
       9  import shutil
      10  import subprocess
      11  import sys
      12  import sysconfig
      13  import types
      14  
      15  
      16  CORE_VENV_DEPS = ('pip',)
      17  logger = logging.getLogger(__name__)
      18  
      19  
      20  class ESC[4;38;5;81mEnvBuilder:
      21      """
      22      This class exists to allow virtual environment creation to be
      23      customized. The constructor parameters determine the builder's
      24      behaviour when called upon to create a virtual environment.
      25  
      26      By default, the builder makes the system (global) site-packages dir
      27      *un*available to the created environment.
      28  
      29      If invoked using the Python -m option, the default is to use copying
      30      on Windows platforms but symlinks elsewhere. If instantiated some
      31      other way, the default is to *not* use symlinks.
      32  
      33      :param system_site_packages: If True, the system (global) site-packages
      34                                   dir is available to created environments.
      35      :param clear: If True, delete the contents of the environment directory if
      36                    it already exists, before environment creation.
      37      :param symlinks: If True, attempt to symlink rather than copy files into
      38                       virtual environment.
      39      :param upgrade: If True, upgrade an existing virtual environment.
      40      :param with_pip: If True, ensure pip is installed in the virtual
      41                       environment
      42      :param prompt: Alternative terminal prefix for the environment.
      43      :param upgrade_deps: Update the base venv modules to the latest on PyPI
      44      """
      45  
      46      def __init__(self, system_site_packages=False, clear=False,
      47                   symlinks=False, upgrade=False, with_pip=False, prompt=None,
      48                   upgrade_deps=False):
      49          self.system_site_packages = system_site_packages
      50          self.clear = clear
      51          self.symlinks = symlinks
      52          self.upgrade = upgrade
      53          self.with_pip = with_pip
      54          self.orig_prompt = prompt
      55          if prompt == '.':  # see bpo-38901
      56              prompt = os.path.basename(os.getcwd())
      57          self.prompt = prompt
      58          self.upgrade_deps = upgrade_deps
      59  
      60      def create(self, env_dir):
      61          """
      62          Create a virtual environment in a directory.
      63  
      64          :param env_dir: The target directory to create an environment in.
      65  
      66          """
      67          env_dir = os.path.abspath(env_dir)
      68          context = self.ensure_directories(env_dir)
      69          # See issue 24875. We need system_site_packages to be False
      70          # until after pip is installed.
      71          true_system_site_packages = self.system_site_packages
      72          self.system_site_packages = False
      73          self.create_configuration(context)
      74          self.setup_python(context)
      75          if self.with_pip:
      76              self._setup_pip(context)
      77          if not self.upgrade:
      78              self.setup_scripts(context)
      79              self.post_setup(context)
      80          if true_system_site_packages:
      81              # We had set it to False before, now
      82              # restore it and rewrite the configuration
      83              self.system_site_packages = True
      84              self.create_configuration(context)
      85          if self.upgrade_deps:
      86              self.upgrade_dependencies(context)
      87  
      88      def clear_directory(self, path):
      89          for fn in os.listdir(path):
      90              fn = os.path.join(path, fn)
      91              if os.path.islink(fn) or os.path.isfile(fn):
      92                  os.remove(fn)
      93              elif os.path.isdir(fn):
      94                  shutil.rmtree(fn)
      95  
      96      def _venv_path(self, env_dir, name):
      97          vars = {
      98              'base': env_dir,
      99              'platbase': env_dir,
     100              'installed_base': env_dir,
     101              'installed_platbase': env_dir,
     102          }
     103          return sysconfig.get_path(name, scheme='venv', vars=vars)
     104  
     105      def ensure_directories(self, env_dir):
     106          """
     107          Create the directories for the environment.
     108  
     109          Returns a context object which holds paths in the environment,
     110          for use by subsequent logic.
     111          """
     112  
     113          def create_if_needed(d):
     114              if not os.path.exists(d):
     115                  os.makedirs(d)
     116              elif os.path.islink(d) or os.path.isfile(d):
     117                  raise ValueError('Unable to create directory %r' % d)
     118  
     119          if os.pathsep in os.fspath(env_dir):
     120              raise ValueError(f'Refusing to create a venv in {env_dir} because '
     121                               f'it contains the PATH separator {os.pathsep}.')
     122          if os.path.exists(env_dir) and self.clear:
     123              self.clear_directory(env_dir)
     124          context = types.SimpleNamespace()
     125          context.env_dir = env_dir
     126          context.env_name = os.path.split(env_dir)[1]
     127          prompt = self.prompt if self.prompt is not None else context.env_name
     128          context.prompt = '(%s) ' % prompt
     129          create_if_needed(env_dir)
     130          executable = sys._base_executable
     131          if not executable:  # see gh-96861
     132              raise ValueError('Unable to determine path to the running '
     133                               'Python interpreter. Provide an explicit path or '
     134                               'check that your PATH environment variable is '
     135                               'correctly set.')
     136          dirname, exename = os.path.split(os.path.abspath(executable))
     137          context.executable = executable
     138          context.python_dir = dirname
     139          context.python_exe = exename
     140          binpath = self._venv_path(env_dir, 'scripts')
     141          incpath = self._venv_path(env_dir, 'include')
     142          libpath = self._venv_path(env_dir, 'purelib')
     143  
     144          context.inc_path = incpath
     145          create_if_needed(incpath)
     146          context.lib_path = libpath
     147          create_if_needed(libpath)
     148          # Issue 21197: create lib64 as a symlink to lib on 64-bit non-OS X POSIX
     149          if ((sys.maxsize > 2**32) and (os.name == 'posix') and
     150              (sys.platform != 'darwin')):
     151              link_path = os.path.join(env_dir, 'lib64')
     152              if not os.path.exists(link_path):   # Issue #21643
     153                  os.symlink('lib', link_path)
     154          context.bin_path = binpath
     155          context.bin_name = os.path.relpath(binpath, env_dir)
     156          context.env_exe = os.path.join(binpath, exename)
     157          create_if_needed(binpath)
     158          # Assign and update the command to use when launching the newly created
     159          # environment, in case it isn't simply the executable script (e.g. bpo-45337)
     160          context.env_exec_cmd = context.env_exe
     161          if sys.platform == 'win32':
     162              # bpo-45337: Fix up env_exec_cmd to account for file system redirections.
     163              # Some redirects only apply to CreateFile and not CreateProcess
     164              real_env_exe = os.path.realpath(context.env_exe)
     165              if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe):
     166                  logger.warning('Actual environment location may have moved due to '
     167                                 'redirects, links or junctions.\n'
     168                                 '  Requested location: "%s"\n'
     169                                 '  Actual location:    "%s"',
     170                                 context.env_exe, real_env_exe)
     171                  context.env_exec_cmd = real_env_exe
     172          return context
     173  
     174      def create_configuration(self, context):
     175          """
     176          Create a configuration file indicating where the environment's Python
     177          was copied from, and whether the system site-packages should be made
     178          available in the environment.
     179  
     180          :param context: The information for the environment creation request
     181                          being processed.
     182          """
     183          context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg')
     184          with open(path, 'w', encoding='utf-8') as f:
     185              f.write('home = %s\n' % context.python_dir)
     186              if self.system_site_packages:
     187                  incl = 'true'
     188              else:
     189                  incl = 'false'
     190              f.write('include-system-site-packages = %s\n' % incl)
     191              f.write('version = %d.%d.%d\n' % sys.version_info[:3])
     192              if self.prompt is not None:
     193                  f.write(f'prompt = {self.prompt!r}\n')
     194              f.write('executable = %s\n' % os.path.realpath(sys.executable))
     195              args = []
     196              nt = os.name == 'nt'
     197              if nt and self.symlinks:
     198                  args.append('--symlinks')
     199              if not nt and not self.symlinks:
     200                  args.append('--copies')
     201              if not self.with_pip:
     202                  args.append('--without-pip')
     203              if self.system_site_packages:
     204                  args.append('--system-site-packages')
     205              if self.clear:
     206                  args.append('--clear')
     207              if self.upgrade:
     208                  args.append('--upgrade')
     209              if self.upgrade_deps:
     210                  args.append('--upgrade-deps')
     211              if self.orig_prompt is not None:
     212                  args.append(f'--prompt="{self.orig_prompt}"')
     213  
     214              args.append(context.env_dir)
     215              args = ' '.join(args)
     216              f.write(f'command = {sys.executable} -m venv {args}\n')
     217  
     218      if os.name != 'nt':
     219          def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
     220              """
     221              Try symlinking a file, and if that fails, fall back to copying.
     222              """
     223              force_copy = not self.symlinks
     224              if not force_copy:
     225                  try:
     226                      if not os.path.islink(dst):  # can't link to itself!
     227                          if relative_symlinks_ok:
     228                              assert os.path.dirname(src) == os.path.dirname(dst)
     229                              os.symlink(os.path.basename(src), dst)
     230                          else:
     231                              os.symlink(src, dst)
     232                  except Exception:   # may need to use a more specific exception
     233                      logger.warning('Unable to symlink %r to %r', src, dst)
     234                      force_copy = True
     235              if force_copy:
     236                  shutil.copyfile(src, dst)
     237      else:
     238          def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
     239              """
     240              Try symlinking a file, and if that fails, fall back to copying.
     241              """
     242              bad_src = os.path.lexists(src) and not os.path.exists(src)
     243              if self.symlinks and not bad_src and not os.path.islink(dst):
     244                  try:
     245                      if relative_symlinks_ok:
     246                          assert os.path.dirname(src) == os.path.dirname(dst)
     247                          os.symlink(os.path.basename(src), dst)
     248                      else:
     249                          os.symlink(src, dst)
     250                      return
     251                  except Exception:   # may need to use a more specific exception
     252                      logger.warning('Unable to symlink %r to %r', src, dst)
     253  
     254              # On Windows, we rewrite symlinks to our base python.exe into
     255              # copies of venvlauncher.exe
     256              basename, ext = os.path.splitext(os.path.basename(src))
     257              srcfn = os.path.join(os.path.dirname(__file__),
     258                                   "scripts",
     259                                   "nt",
     260                                   basename + ext)
     261              # Builds or venv's from builds need to remap source file
     262              # locations, as we do not put them into Lib/venv/scripts
     263              if sysconfig.is_python_build() or not os.path.isfile(srcfn):
     264                  if basename.endswith('_d'):
     265                      ext = '_d' + ext
     266                      basename = basename[:-2]
     267                  if basename == 'python':
     268                      basename = 'venvlauncher'
     269                  elif basename == 'pythonw':
     270                      basename = 'venvwlauncher'
     271                  src = os.path.join(os.path.dirname(src), basename + ext)
     272              else:
     273                  src = srcfn
     274              if not os.path.exists(src):
     275                  if not bad_src:
     276                      logger.warning('Unable to copy %r', src)
     277                  return
     278  
     279              shutil.copyfile(src, dst)
     280  
     281      def setup_python(self, context):
     282          """
     283          Set up a Python executable in the environment.
     284  
     285          :param context: The information for the environment creation request
     286                          being processed.
     287          """
     288          binpath = context.bin_path
     289          path = context.env_exe
     290          copier = self.symlink_or_copy
     291          dirname = context.python_dir
     292          if os.name != 'nt':
     293              copier(context.executable, path)
     294              if not os.path.islink(path):
     295                  os.chmod(path, 0o755)
     296              for suffix in ('python', 'python3', f'python3.{sys.version_info[1]}'):
     297                  path = os.path.join(binpath, suffix)
     298                  if not os.path.exists(path):
     299                      # Issue 18807: make copies if
     300                      # symlinks are not wanted
     301                      copier(context.env_exe, path, relative_symlinks_ok=True)
     302                      if not os.path.islink(path):
     303                          os.chmod(path, 0o755)
     304          else:
     305              if self.symlinks:
     306                  # For symlinking, we need a complete copy of the root directory
     307                  # If symlinks fail, you'll get unnecessary copies of files, but
     308                  # we assume that if you've opted into symlinks on Windows then
     309                  # you know what you're doing.
     310                  suffixes = [
     311                      f for f in os.listdir(dirname) if
     312                      os.path.normcase(os.path.splitext(f)[1]) in ('.exe', '.dll')
     313                  ]
     314                  if sysconfig.is_python_build():
     315                      suffixes = [
     316                          f for f in suffixes if
     317                          os.path.normcase(f).startswith(('python', 'vcruntime'))
     318                      ]
     319              else:
     320                  suffixes = {'python.exe', 'python_d.exe', 'pythonw.exe', 'pythonw_d.exe'}
     321                  base_exe = os.path.basename(context.env_exe)
     322                  suffixes.add(base_exe)
     323  
     324              for suffix in suffixes:
     325                  src = os.path.join(dirname, suffix)
     326                  if os.path.lexists(src):
     327                      copier(src, os.path.join(binpath, suffix))
     328  
     329              if sysconfig.is_python_build():
     330                  # copy init.tcl
     331                  for root, dirs, files in os.walk(context.python_dir):
     332                      if 'init.tcl' in files:
     333                          tcldir = os.path.basename(root)
     334                          tcldir = os.path.join(context.env_dir, 'Lib', tcldir)
     335                          if not os.path.exists(tcldir):
     336                              os.makedirs(tcldir)
     337                          src = os.path.join(root, 'init.tcl')
     338                          dst = os.path.join(tcldir, 'init.tcl')
     339                          shutil.copyfile(src, dst)
     340                          break
     341  
     342      def _call_new_python(self, context, *py_args, **kwargs):
     343          """Executes the newly created Python using safe-ish options"""
     344          # gh-98251: We do not want to just use '-I' because that masks
     345          # legitimate user preferences (such as not writing bytecode). All we
     346          # really need is to ensure that the path variables do not overrule
     347          # normal venv handling.
     348          args = [context.env_exec_cmd, *py_args]
     349          kwargs['env'] = env = os.environ.copy()
     350          env['VIRTUAL_ENV'] = context.env_dir
     351          env.pop('PYTHONHOME', None)
     352          env.pop('PYTHONPATH', None)
     353          kwargs['cwd'] = context.env_dir
     354          kwargs['executable'] = context.env_exec_cmd
     355          subprocess.check_output(args, **kwargs)
     356  
     357      def _setup_pip(self, context):
     358          """Installs or upgrades pip in a virtual environment"""
     359          self._call_new_python(context, '-m', 'ensurepip', '--upgrade',
     360                                '--default-pip', stderr=subprocess.STDOUT)
     361  
     362      def setup_scripts(self, context):
     363          """
     364          Set up scripts into the created environment from a directory.
     365  
     366          This method installs the default scripts into the environment
     367          being created. You can prevent the default installation by overriding
     368          this method if you really need to, or if you need to specify
     369          a different location for the scripts to install. By default, the
     370          'scripts' directory in the venv package is used as the source of
     371          scripts to install.
     372          """
     373          path = os.path.abspath(os.path.dirname(__file__))
     374          path = os.path.join(path, 'scripts')
     375          self.install_scripts(context, path)
     376  
     377      def post_setup(self, context):
     378          """
     379          Hook for post-setup modification of the venv. Subclasses may install
     380          additional packages or scripts here, add activation shell scripts, etc.
     381  
     382          :param context: The information for the environment creation request
     383                          being processed.
     384          """
     385          pass
     386  
     387      def replace_variables(self, text, context):
     388          """
     389          Replace variable placeholders in script text with context-specific
     390          variables.
     391  
     392          Return the text passed in , but with variables replaced.
     393  
     394          :param text: The text in which to replace placeholder variables.
     395          :param context: The information for the environment creation request
     396                          being processed.
     397          """
     398          text = text.replace('__VENV_DIR__', context.env_dir)
     399          text = text.replace('__VENV_NAME__', context.env_name)
     400          text = text.replace('__VENV_PROMPT__', context.prompt)
     401          text = text.replace('__VENV_BIN_NAME__', context.bin_name)
     402          text = text.replace('__VENV_PYTHON__', context.env_exe)
     403          return text
     404  
     405      def install_scripts(self, context, path):
     406          """
     407          Install scripts into the created environment from a directory.
     408  
     409          :param context: The information for the environment creation request
     410                          being processed.
     411          :param path:    Absolute pathname of a directory containing script.
     412                          Scripts in the 'common' subdirectory of this directory,
     413                          and those in the directory named for the platform
     414                          being run on, are installed in the created environment.
     415                          Placeholder variables are replaced with environment-
     416                          specific values.
     417          """
     418          binpath = context.bin_path
     419          plen = len(path)
     420          for root, dirs, files in os.walk(path):
     421              if root == path:  # at top-level, remove irrelevant dirs
     422                  for d in dirs[:]:
     423                      if d not in ('common', os.name):
     424                          dirs.remove(d)
     425                  continue  # ignore files in top level
     426              for f in files:
     427                  if (os.name == 'nt' and f.startswith('python')
     428                          and f.endswith(('.exe', '.pdb'))):
     429                      continue
     430                  srcfile = os.path.join(root, f)
     431                  suffix = root[plen:].split(os.sep)[2:]
     432                  if not suffix:
     433                      dstdir = binpath
     434                  else:
     435                      dstdir = os.path.join(binpath, *suffix)
     436                  if not os.path.exists(dstdir):
     437                      os.makedirs(dstdir)
     438                  dstfile = os.path.join(dstdir, f)
     439                  with open(srcfile, 'rb') as f:
     440                      data = f.read()
     441                  if not srcfile.endswith(('.exe', '.pdb')):
     442                      try:
     443                          data = data.decode('utf-8')
     444                          data = self.replace_variables(data, context)
     445                          data = data.encode('utf-8')
     446                      except UnicodeError as e:
     447                          data = None
     448                          logger.warning('unable to copy script %r, '
     449                                         'may be binary: %s', srcfile, e)
     450                  if data is not None:
     451                      with open(dstfile, 'wb') as f:
     452                          f.write(data)
     453                      shutil.copymode(srcfile, dstfile)
     454  
     455      def upgrade_dependencies(self, context):
     456          logger.debug(
     457              f'Upgrading {CORE_VENV_DEPS} packages in {context.bin_path}'
     458          )
     459          self._call_new_python(context, '-m', 'pip', 'install', '--upgrade',
     460                                *CORE_VENV_DEPS)
     461  
     462  
     463  def create(env_dir, system_site_packages=False, clear=False,
     464             symlinks=False, with_pip=False, prompt=None, upgrade_deps=False):
     465      """Create a virtual environment in a directory."""
     466      builder = EnvBuilder(system_site_packages=system_site_packages,
     467                           clear=clear, symlinks=symlinks, with_pip=with_pip,
     468                           prompt=prompt, upgrade_deps=upgrade_deps)
     469      builder.create(env_dir)
     470  
     471  
     472  def main(args=None):
     473      import argparse
     474  
     475      parser = argparse.ArgumentParser(prog=__name__,
     476                                       description='Creates virtual Python '
     477                                                   'environments in one or '
     478                                                   'more target '
     479                                                   'directories.',
     480                                       epilog='Once an environment has been '
     481                                              'created, you may wish to '
     482                                              'activate it, e.g. by '
     483                                              'sourcing an activate script '
     484                                              'in its bin directory.')
     485      parser.add_argument('dirs', metavar='ENV_DIR', nargs='+',
     486                          help='A directory to create the environment in.')
     487      parser.add_argument('--system-site-packages', default=False,
     488                          action='store_true', dest='system_site',
     489                          help='Give the virtual environment access to the '
     490                               'system site-packages dir.')
     491      if os.name == 'nt':
     492          use_symlinks = False
     493      else:
     494          use_symlinks = True
     495      group = parser.add_mutually_exclusive_group()
     496      group.add_argument('--symlinks', default=use_symlinks,
     497                         action='store_true', dest='symlinks',
     498                         help='Try to use symlinks rather than copies, '
     499                              'when symlinks are not the default for '
     500                              'the platform.')
     501      group.add_argument('--copies', default=not use_symlinks,
     502                         action='store_false', dest='symlinks',
     503                         help='Try to use copies rather than symlinks, '
     504                              'even when symlinks are the default for '
     505                              'the platform.')
     506      parser.add_argument('--clear', default=False, action='store_true',
     507                          dest='clear', help='Delete the contents of the '
     508                                             'environment directory if it '
     509                                             'already exists, before '
     510                                             'environment creation.')
     511      parser.add_argument('--upgrade', default=False, action='store_true',
     512                          dest='upgrade', help='Upgrade the environment '
     513                                               'directory to use this version '
     514                                               'of Python, assuming Python '
     515                                               'has been upgraded in-place.')
     516      parser.add_argument('--without-pip', dest='with_pip',
     517                          default=True, action='store_false',
     518                          help='Skips installing or upgrading pip in the '
     519                               'virtual environment (pip is bootstrapped '
     520                               'by default)')
     521      parser.add_argument('--prompt',
     522                          help='Provides an alternative prompt prefix for '
     523                               'this environment.')
     524      parser.add_argument('--upgrade-deps', default=False, action='store_true',
     525                          dest='upgrade_deps',
     526                          help=f'Upgrade core dependencies ({", ".join(CORE_VENV_DEPS)}) '
     527                               'to the latest version in PyPI')
     528      options = parser.parse_args(args)
     529      if options.upgrade and options.clear:
     530          raise ValueError('you cannot supply --upgrade and --clear together.')
     531      builder = EnvBuilder(system_site_packages=options.system_site,
     532                           clear=options.clear,
     533                           symlinks=options.symlinks,
     534                           upgrade=options.upgrade,
     535                           with_pip=options.with_pip,
     536                           prompt=options.prompt,
     537                           upgrade_deps=options.upgrade_deps)
     538      for d in options.dirs:
     539          builder.create(d)
     540  
     541  
     542  if __name__ == '__main__':
     543      rc = 1
     544      try:
     545          main()
     546          rc = 0
     547      except Exception as e:
     548          print('Error: %s' % e, file=sys.stderr)
     549      sys.exit(rc)