python (3.11.7)

(root)/
lib/
python3.11/
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', 'setuptools')
      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          create_if_needed(libpath)
     147          # Issue 21197: create lib64 as a symlink to lib on 64-bit non-OS X POSIX
     148          if ((sys.maxsize > 2**32) and (os.name == 'posix') and
     149              (sys.platform != 'darwin')):
     150              link_path = os.path.join(env_dir, 'lib64')
     151              if not os.path.exists(link_path):   # Issue #21643
     152                  os.symlink('lib', link_path)
     153          context.bin_path = binpath
     154          context.bin_name = os.path.relpath(binpath, env_dir)
     155          context.env_exe = os.path.join(binpath, exename)
     156          create_if_needed(binpath)
     157          # Assign and update the command to use when launching the newly created
     158          # environment, in case it isn't simply the executable script (e.g. bpo-45337)
     159          context.env_exec_cmd = context.env_exe
     160          if sys.platform == 'win32':
     161              # bpo-45337: Fix up env_exec_cmd to account for file system redirections.
     162              # Some redirects only apply to CreateFile and not CreateProcess
     163              real_env_exe = os.path.realpath(context.env_exe)
     164              if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe):
     165                  logger.warning('Actual environment location may have moved due to '
     166                                 'redirects, links or junctions.\n'
     167                                 '  Requested location: "%s"\n'
     168                                 '  Actual location:    "%s"',
     169                                 context.env_exe, real_env_exe)
     170                  context.env_exec_cmd = real_env_exe
     171          return context
     172  
     173      def create_configuration(self, context):
     174          """
     175          Create a configuration file indicating where the environment's Python
     176          was copied from, and whether the system site-packages should be made
     177          available in the environment.
     178  
     179          :param context: The information for the environment creation request
     180                          being processed.
     181          """
     182          context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg')
     183          with open(path, 'w', encoding='utf-8') as f:
     184              f.write('home = %s\n' % context.python_dir)
     185              if self.system_site_packages:
     186                  incl = 'true'
     187              else:
     188                  incl = 'false'
     189              f.write('include-system-site-packages = %s\n' % incl)
     190              f.write('version = %d.%d.%d\n' % sys.version_info[:3])
     191              if self.prompt is not None:
     192                  f.write(f'prompt = {self.prompt!r}\n')
     193              f.write('executable = %s\n' % os.path.realpath(sys.executable))
     194              args = []
     195              nt = os.name == 'nt'
     196              if nt and self.symlinks:
     197                  args.append('--symlinks')
     198              if not nt and not self.symlinks:
     199                  args.append('--copies')
     200              if not self.with_pip:
     201                  args.append('--without-pip')
     202              if self.system_site_packages:
     203                  args.append('--system-site-packages')
     204              if self.clear:
     205                  args.append('--clear')
     206              if self.upgrade:
     207                  args.append('--upgrade')
     208              if self.upgrade_deps:
     209                  args.append('--upgrade-deps')
     210              if self.orig_prompt is not None:
     211                  args.append(f'--prompt="{self.orig_prompt}"')
     212  
     213              args.append(context.env_dir)
     214              args = ' '.join(args)
     215              f.write(f'command = {sys.executable} -m venv {args}\n')
     216  
     217      if os.name != 'nt':
     218          def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
     219              """
     220              Try symlinking a file, and if that fails, fall back to copying.
     221              """
     222              force_copy = not self.symlinks
     223              if not force_copy:
     224                  try:
     225                      if not os.path.islink(dst): # can't link to itself!
     226                          if relative_symlinks_ok:
     227                              assert os.path.dirname(src) == os.path.dirname(dst)
     228                              os.symlink(os.path.basename(src), dst)
     229                          else:
     230                              os.symlink(src, dst)
     231                  except Exception:   # may need to use a more specific exception
     232                      logger.warning('Unable to symlink %r to %r', src, dst)
     233                      force_copy = True
     234              if force_copy:
     235                  shutil.copyfile(src, dst)
     236      else:
     237          def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
     238              """
     239              Try symlinking a file, and if that fails, fall back to copying.
     240              """
     241              bad_src = os.path.lexists(src) and not os.path.exists(src)
     242              if self.symlinks and not bad_src and not os.path.islink(dst):
     243                  try:
     244                      if relative_symlinks_ok:
     245                          assert os.path.dirname(src) == os.path.dirname(dst)
     246                          os.symlink(os.path.basename(src), dst)
     247                      else:
     248                          os.symlink(src, dst)
     249                      return
     250                  except Exception:   # may need to use a more specific exception
     251                      logger.warning('Unable to symlink %r to %r', src, dst)
     252  
     253              # On Windows, we rewrite symlinks to our base python.exe into
     254              # copies of venvlauncher.exe
     255              basename, ext = os.path.splitext(os.path.basename(src))
     256              srcfn = os.path.join(os.path.dirname(__file__),
     257                                   "scripts",
     258                                   "nt",
     259                                   basename + ext)
     260              # Builds or venv's from builds need to remap source file
     261              # locations, as we do not put them into Lib/venv/scripts
     262              if sysconfig.is_python_build() or not os.path.isfile(srcfn):
     263                  if basename.endswith('_d'):
     264                      ext = '_d' + ext
     265                      basename = basename[:-2]
     266                  if basename == 'python':
     267                      basename = 'venvlauncher'
     268                  elif basename == 'pythonw':
     269                      basename = 'venvwlauncher'
     270                  src = os.path.join(os.path.dirname(src), basename + ext)
     271              else:
     272                  src = srcfn
     273              if not os.path.exists(src):
     274                  if not bad_src:
     275                      logger.warning('Unable to copy %r', src)
     276                  return
     277  
     278              shutil.copyfile(src, dst)
     279  
     280      def setup_python(self, context):
     281          """
     282          Set up a Python executable in the environment.
     283  
     284          :param context: The information for the environment creation request
     285                          being processed.
     286          """
     287          binpath = context.bin_path
     288          path = context.env_exe
     289          copier = self.symlink_or_copy
     290          dirname = context.python_dir
     291          if os.name != 'nt':
     292              copier(context.executable, path)
     293              if not os.path.islink(path):
     294                  os.chmod(path, 0o755)
     295              for suffix in ('python', 'python3', f'python3.{sys.version_info[1]}'):
     296                  path = os.path.join(binpath, suffix)
     297                  if not os.path.exists(path):
     298                      # Issue 18807: make copies if
     299                      # symlinks are not wanted
     300                      copier(context.env_exe, path, relative_symlinks_ok=True)
     301                      if not os.path.islink(path):
     302                          os.chmod(path, 0o755)
     303          else:
     304              if self.symlinks:
     305                  # For symlinking, we need a complete copy of the root directory
     306                  # If symlinks fail, you'll get unnecessary copies of files, but
     307                  # we assume that if you've opted into symlinks on Windows then
     308                  # you know what you're doing.
     309                  suffixes = [
     310                      f for f in os.listdir(dirname) if
     311                      os.path.normcase(os.path.splitext(f)[1]) in ('.exe', '.dll')
     312                  ]
     313                  if sysconfig.is_python_build():
     314                      suffixes = [
     315                          f for f in suffixes if
     316                          os.path.normcase(f).startswith(('python', 'vcruntime'))
     317                      ]
     318              else:
     319                  suffixes = {'python.exe', 'python_d.exe', 'pythonw.exe', 'pythonw_d.exe'}
     320                  base_exe = os.path.basename(context.env_exe)
     321                  suffixes.add(base_exe)
     322  
     323              for suffix in suffixes:
     324                  src = os.path.join(dirname, suffix)
     325                  if os.path.lexists(src):
     326                      copier(src, os.path.join(binpath, suffix))
     327  
     328              if sysconfig.is_python_build():
     329                  # copy init.tcl
     330                  for root, dirs, files in os.walk(context.python_dir):
     331                      if 'init.tcl' in files:
     332                          tcldir = os.path.basename(root)
     333                          tcldir = os.path.join(context.env_dir, 'Lib', tcldir)
     334                          if not os.path.exists(tcldir):
     335                              os.makedirs(tcldir)
     336                          src = os.path.join(root, 'init.tcl')
     337                          dst = os.path.join(tcldir, 'init.tcl')
     338                          shutil.copyfile(src, dst)
     339                          break
     340  
     341      def _call_new_python(self, context, *py_args, **kwargs):
     342          """Executes the newly created Python using safe-ish options"""
     343          # gh-98251: We do not want to just use '-I' because that masks
     344          # legitimate user preferences (such as not writing bytecode). All we
     345          # really need is to ensure that the path variables do not overrule
     346          # normal venv handling.
     347          args = [context.env_exec_cmd, *py_args]
     348          kwargs['env'] = env = os.environ.copy()
     349          env['VIRTUAL_ENV'] = context.env_dir
     350          env.pop('PYTHONHOME', None)
     351          env.pop('PYTHONPATH', None)
     352          kwargs['cwd'] = context.env_dir
     353          kwargs['executable'] = context.env_exec_cmd
     354          subprocess.check_output(args, **kwargs)
     355  
     356      def _setup_pip(self, context):
     357          """Installs or upgrades pip in a virtual environment"""
     358          self._call_new_python(context, '-m', 'ensurepip', '--upgrade',
     359                                '--default-pip', stderr=subprocess.STDOUT)
     360  
     361      def setup_scripts(self, context):
     362          """
     363          Set up scripts into the created environment from a directory.
     364  
     365          This method installs the default scripts into the environment
     366          being created. You can prevent the default installation by overriding
     367          this method if you really need to, or if you need to specify
     368          a different location for the scripts to install. By default, the
     369          'scripts' directory in the venv package is used as the source of
     370          scripts to install.
     371          """
     372          path = os.path.abspath(os.path.dirname(__file__))
     373          path = os.path.join(path, 'scripts')
     374          self.install_scripts(context, path)
     375  
     376      def post_setup(self, context):
     377          """
     378          Hook for post-setup modification of the venv. Subclasses may install
     379          additional packages or scripts here, add activation shell scripts, etc.
     380  
     381          :param context: The information for the environment creation request
     382                          being processed.
     383          """
     384          pass
     385  
     386      def replace_variables(self, text, context):
     387          """
     388          Replace variable placeholders in script text with context-specific
     389          variables.
     390  
     391          Return the text passed in , but with variables replaced.
     392  
     393          :param text: The text in which to replace placeholder variables.
     394          :param context: The information for the environment creation request
     395                          being processed.
     396          """
     397          text = text.replace('__VENV_DIR__', context.env_dir)
     398          text = text.replace('__VENV_NAME__', context.env_name)
     399          text = text.replace('__VENV_PROMPT__', context.prompt)
     400          text = text.replace('__VENV_BIN_NAME__', context.bin_name)
     401          text = text.replace('__VENV_PYTHON__', context.env_exe)
     402          return text
     403  
     404      def install_scripts(self, context, path):
     405          """
     406          Install scripts into the created environment from a directory.
     407  
     408          :param context: The information for the environment creation request
     409                          being processed.
     410          :param path:    Absolute pathname of a directory containing script.
     411                          Scripts in the 'common' subdirectory of this directory,
     412                          and those in the directory named for the platform
     413                          being run on, are installed in the created environment.
     414                          Placeholder variables are replaced with environment-
     415                          specific values.
     416          """
     417          binpath = context.bin_path
     418          plen = len(path)
     419          for root, dirs, files in os.walk(path):
     420              if root == path: # at top-level, remove irrelevant dirs
     421                  for d in dirs[:]:
     422                      if d not in ('common', os.name):
     423                          dirs.remove(d)
     424                  continue # ignore files in top level
     425              for f in files:
     426                  if (os.name == 'nt' and f.startswith('python')
     427                          and f.endswith(('.exe', '.pdb'))):
     428                      continue
     429                  srcfile = os.path.join(root, f)
     430                  suffix = root[plen:].split(os.sep)[2:]
     431                  if not suffix:
     432                      dstdir = binpath
     433                  else:
     434                      dstdir = os.path.join(binpath, *suffix)
     435                  if not os.path.exists(dstdir):
     436                      os.makedirs(dstdir)
     437                  dstfile = os.path.join(dstdir, f)
     438                  with open(srcfile, 'rb') as f:
     439                      data = f.read()
     440                  if not srcfile.endswith(('.exe', '.pdb')):
     441                      try:
     442                          data = data.decode('utf-8')
     443                          data = self.replace_variables(data, context)
     444                          data = data.encode('utf-8')
     445                      except UnicodeError as e:
     446                          data = None
     447                          logger.warning('unable to copy script %r, '
     448                                         'may be binary: %s', srcfile, e)
     449                  if data is not None:
     450                      with open(dstfile, 'wb') as f:
     451                          f.write(data)
     452                      shutil.copymode(srcfile, dstfile)
     453  
     454      def upgrade_dependencies(self, context):
     455          logger.debug(
     456              f'Upgrading {CORE_VENV_DEPS} packages in {context.bin_path}'
     457          )
     458          self._call_new_python(context, '-m', 'pip', 'install', '--upgrade',
     459                                *CORE_VENV_DEPS)
     460  
     461  
     462  def create(env_dir, system_site_packages=False, clear=False,
     463             symlinks=False, with_pip=False, prompt=None, upgrade_deps=False):
     464      """Create a virtual environment in a directory."""
     465      builder = EnvBuilder(system_site_packages=system_site_packages,
     466                           clear=clear, symlinks=symlinks, with_pip=with_pip,
     467                           prompt=prompt, upgrade_deps=upgrade_deps)
     468      builder.create(env_dir)
     469  
     470  def main(args=None):
     471      compatible = True
     472      if sys.version_info < (3, 3):
     473          compatible = False
     474      elif not hasattr(sys, 'base_prefix'):
     475          compatible = False
     476      if not compatible:
     477          raise ValueError('This script is only for use with Python >= 3.3')
     478      else:
     479          import argparse
     480  
     481          parser = argparse.ArgumentParser(prog=__name__,
     482                                           description='Creates virtual Python '
     483                                                       'environments in one or '
     484                                                       'more target '
     485                                                       'directories.',
     486                                           epilog='Once an environment has been '
     487                                                  'created, you may wish to '
     488                                                  'activate it, e.g. by '
     489                                                  'sourcing an activate script '
     490                                                  'in its bin directory.')
     491          parser.add_argument('dirs', metavar='ENV_DIR', nargs='+',
     492                              help='A directory to create the environment in.')
     493          parser.add_argument('--system-site-packages', default=False,
     494                              action='store_true', dest='system_site',
     495                              help='Give the virtual environment access to the '
     496                                   'system site-packages dir.')
     497          if os.name == 'nt':
     498              use_symlinks = False
     499          else:
     500              use_symlinks = True
     501          group = parser.add_mutually_exclusive_group()
     502          group.add_argument('--symlinks', default=use_symlinks,
     503                             action='store_true', dest='symlinks',
     504                             help='Try to use symlinks rather than copies, '
     505                                  'when symlinks are not the default for '
     506                                  'the platform.')
     507          group.add_argument('--copies', default=not use_symlinks,
     508                             action='store_false', dest='symlinks',
     509                             help='Try to use copies rather than symlinks, '
     510                                  'even when symlinks are the default for '
     511                                  'the platform.')
     512          parser.add_argument('--clear', default=False, action='store_true',
     513                              dest='clear', help='Delete the contents of the '
     514                                                 'environment directory if it '
     515                                                 'already exists, before '
     516                                                 'environment creation.')
     517          parser.add_argument('--upgrade', default=False, action='store_true',
     518                              dest='upgrade', help='Upgrade the environment '
     519                                                 'directory to use this version '
     520                                                 'of Python, assuming Python '
     521                                                 'has been upgraded in-place.')
     522          parser.add_argument('--without-pip', dest='with_pip',
     523                              default=True, action='store_false',
     524                              help='Skips installing or upgrading pip in the '
     525                                   'virtual environment (pip is bootstrapped '
     526                                   'by default)')
     527          parser.add_argument('--prompt',
     528                              help='Provides an alternative prompt prefix for '
     529                                   'this environment.')
     530          parser.add_argument('--upgrade-deps', default=False, action='store_true',
     531                              dest='upgrade_deps',
     532                              help='Upgrade core dependencies: {} to the latest '
     533                                   'version in PyPI'.format(
     534                                   ' '.join(CORE_VENV_DEPS)))
     535          options = parser.parse_args(args)
     536          if options.upgrade and options.clear:
     537              raise ValueError('you cannot supply --upgrade and --clear together.')
     538          builder = EnvBuilder(system_site_packages=options.system_site,
     539                               clear=options.clear,
     540                               symlinks=options.symlinks,
     541                               upgrade=options.upgrade,
     542                               with_pip=options.with_pip,
     543                               prompt=options.prompt,
     544                               upgrade_deps=options.upgrade_deps)
     545          for d in options.dirs:
     546              builder.create(d)
     547  
     548  if __name__ == '__main__':
     549      rc = 1
     550      try:
     551          main()
     552          rc = 0
     553      except Exception as e:
     554          print('Error: %s' % e, file=sys.stderr)
     555      sys.exit(rc)