]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-83417: Allow `venv` to add a `.gitignore` file to environments via a new `scm_igno...
authorBrett Cannon <brett@python.org>
Fri, 15 Sep 2023 22:38:08 +0000 (15:38 -0700)
committerGitHub <noreply@github.com>
Fri, 15 Sep 2023 22:38:08 +0000 (22:38 +0000)
This feature is off by default via code but on by default via the CLI. The `.gitignore` file contains `*` which causes the entire directory to be ignored.

Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
Doc/library/venv.rst
Doc/using/venv-create.inc
Doc/whatsnew/3.13.rst
Lib/test/test_venv.py
Lib/venv/__init__.py
Lib/venv/__main__.py
Misc/NEWS.d/next/Library/2023-08-18-22-58-07.gh-issue-83417.61J4yM.rst [new file with mode: 0644]

index 2482441d649790555eb4e6e2229f7e6f17794f2d..b72f3041f1da116ccacef5b8351ade002fafec31 100644 (file)
@@ -143,7 +143,8 @@ creation according to their needs, the :class:`EnvBuilder` class.
 
 .. class:: EnvBuilder(system_site_packages=False, clear=False, \
                       symlinks=False, upgrade=False, with_pip=False, \
-                      prompt=None, upgrade_deps=False)
+                      prompt=None, upgrade_deps=False, \
+                      *, scm_ignore_files=frozenset())
 
     The :class:`EnvBuilder` class accepts the following keyword arguments on
     instantiation:
@@ -172,6 +173,12 @@ creation according to their needs, the :class:`EnvBuilder` class.
 
     * ``upgrade_deps`` -- Update the base venv modules to the latest on PyPI
 
+    * ``scm_ignore_files`` -- Create ignore files based for the specified source
+      control managers (SCM) in the iterable. Support is defined by having a
+      method named ``create_{scm}_ignore_file``. The only value supported by
+      default is ``"git"`` via :meth:`create_git_ignore_file`.
+
+
     .. versionchanged:: 3.4
        Added the ``with_pip`` parameter
 
@@ -181,6 +188,9 @@ creation according to their needs, the :class:`EnvBuilder` class.
     .. versionadded:: 3.9
        Added the ``upgrade_deps`` parameter
 
+    .. versionadded:: 3.13
+       Added the ``scm_ignore_files`` parameter
+
     Creators of third-party virtual environment tools will be free to use the
     provided :class:`EnvBuilder` class as a base class.
 
@@ -339,11 +349,18 @@ creation according to their needs, the :class:`EnvBuilder` class.
         The directories are allowed to exist (for when an existing environment
         is being upgraded).
 
+    .. method:: create_git_ignore_file(context)
+
+       Creates a ``.gitignore`` file within the virtual environment that causes
+       the entire directory to be ignored by the ``git`` source control manager.
+
+       .. versionadded:: 3.13
+
 There is also a module-level convenience function:
 
 .. function:: create(env_dir, system_site_packages=False, clear=False, \
                      symlinks=False, with_pip=False, prompt=None, \
-                     upgrade_deps=False)
+                     upgrade_deps=False, *, scm_ignore_files=frozenset())
 
     Create an :class:`EnvBuilder` with the given keyword arguments, and call its
     :meth:`~EnvBuilder.create` method with the *env_dir* argument.
@@ -359,6 +376,9 @@ There is also a module-level convenience function:
     .. versionchanged:: 3.9
        Added the ``upgrade_deps`` parameter
 
+    .. versionchanged:: 3.13
+       Added the ``scm_ignore_files`` parameter
+
 An example of extending ``EnvBuilder``
 --------------------------------------
 
index 2fc90126482268910f6dd58ecd2a43ae95f9c51c..1cf438b198a9afa0995eacaf70151e72e1f8b2a1 100644 (file)
@@ -35,37 +35,48 @@ your :ref:`Python installation <using-on-windows>`::
 
 The command, if run with ``-h``, will show the available options::
 
-    usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
-                [--upgrade] [--without-pip] [--prompt PROMPT] [--upgrade-deps]
-                ENV_DIR [ENV_DIR ...]
-
-    Creates virtual Python environments in one or more target directories.
-
-    positional arguments:
-      ENV_DIR               A directory to create the environment in.
-
-    optional arguments:
-      -h, --help            show this help message and exit
-      --system-site-packages
-                            Give the virtual environment access to the system
-                            site-packages dir.
-      --symlinks            Try to use symlinks rather than copies, when symlinks
-                            are not the default for the platform.
-      --copies              Try to use copies rather than symlinks, even when
-                            symlinks are the default for the platform.
-      --clear               Delete the contents of the environment directory if it
-                            already exists, before environment creation.
-      --upgrade             Upgrade the environment directory to use this version
-                            of Python, assuming Python has been upgraded in-place.
-      --without-pip         Skips installing or upgrading pip in the virtual
-                            environment (pip is bootstrapped by default)
-      --prompt PROMPT       Provides an alternative prompt prefix for this
-                            environment.
-      --upgrade-deps        Upgrade core dependencies (pip) to the
-                            latest version in PyPI
-
-    Once an environment has been created, you may wish to activate it, e.g. by
-    sourcing an activate script in its bin directory.
+   usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
+               [--upgrade] [--without-pip] [--prompt PROMPT] [--upgrade-deps]
+               [--without-scm-ignore-file]
+               ENV_DIR [ENV_DIR ...]
+
+   Creates virtual Python environments in one or more target directories.
+
+   positional arguments:
+   ENV_DIR               A directory to create the environment in.
+
+   options:
+   -h, --help            show this help message and exit
+   --system-site-packages
+                           Give the virtual environment access to the system
+                           site-packages dir.
+   --symlinks            Try to use symlinks rather than copies, when
+                           symlinks are not the default for the platform.
+   --copies              Try to use copies rather than symlinks, even when
+                           symlinks are the default for the platform.
+   --clear               Delete the contents of the environment directory if
+                           it already exists, before environment creation.
+   --upgrade             Upgrade the environment directory to use this
+                           version of Python, assuming Python has been upgraded
+                           in-place.
+   --without-pip         Skips installing or upgrading pip in the virtual
+                           environment (pip is bootstrapped by default)
+   --prompt PROMPT       Provides an alternative prompt prefix for this
+                           environment.
+   --upgrade-deps        Upgrade core dependencies (pip) to the latest
+                           version in PyPI
+   --without-scm-ignore-file
+                           Skips adding the default SCM ignore file to the
+                           environment directory (the default is a .gitignore
+                           file).
+
+   Once an environment has been created, you may wish to activate it, e.g. by
+   sourcing an activate script in its bin directory.
+
+.. versionchanged:: 3.13
+
+   ``--without-scm-ignore-file`` was added along with creating an ignore file
+   for ``git`` by default.
 
 .. versionchanged:: 3.12
 
index 43d06b886e59e41eb98ea83b7889a90f40036d97..f71bdabd31f1d36fec491f23d6682ac4dd55373b 100644 (file)
@@ -220,6 +220,16 @@ typing
   check whether a class is a :class:`typing.Protocol`. (Contributed by Jelle Zijlstra in
   :gh:`104873`.)
 
+venv
+----
+
+* Add support for adding source control management (SCM) ignore files to a
+  virtual environment's directory. By default, Git is supported. This is
+  implemented as opt-in via the API which can be extended to support other SCMs
+  (:class:`venv.EnvBuilder` and :func:`venv.create`), and opt-out via the CLI
+  (using ``--without-scm-ignore-files``). (Contributed by Brett Cannon in
+  :gh:`108125`.)
+
 Optimizations
 =============
 
index aa6a8fbf8cfd17e9a55a133745ad239ae5717863..a894bb10bd04dacb57500a47ebb421e7d0394068 100644 (file)
@@ -82,6 +82,13 @@ class BaseTest(unittest.TestCase):
     def tearDown(self):
         rmtree(self.env_dir)
 
+    def envpy(self, *, real_env_dir=False):
+        if real_env_dir:
+            env_dir = os.path.realpath(self.env_dir)
+        else:
+            env_dir = self.env_dir
+        return os.path.join(env_dir, self.bindir, self.exe)
+
     def run_with_capture(self, func, *args, **kwargs):
         with captured_stdout() as output:
             with captured_stderr() as error:
@@ -138,7 +145,8 @@ class BasicTest(BaseTest):
         self.assertIn('executable = %s' %
                       os.path.realpath(sys.executable), data)
         copies = '' if os.name=='nt' else ' --copies'
-        cmd = f'command = {sys.executable} -m venv{copies} --without-pip {self.env_dir}'
+        cmd = (f'command = {sys.executable} -m venv{copies} --without-pip '
+               f'--without-scm-ignore-files {self.env_dir}')
         self.assertIn(cmd, data)
         fn = self.get_env_file(self.bindir, self.exe)
         if not os.path.exists(fn):  # diagnostics for Windows buildbot failures
@@ -148,35 +156,37 @@ class BasicTest(BaseTest):
         self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
 
     def test_config_file_command_key(self):
-        attrs = [
-            (None, None),
-            ('symlinks', '--copies'),
-            ('with_pip', '--without-pip'),
-            ('system_site_packages', '--system-site-packages'),
-            ('clear', '--clear'),
-            ('upgrade', '--upgrade'),
-            ('upgrade_deps', '--upgrade-deps'),
-            ('prompt', '--prompt'),
+        options = [
+            (None, None, None),  # Default case.
+            ('--copies', 'symlinks', False),
+            ('--without-pip', 'with_pip', False),
+            ('--system-site-packages', 'system_site_packages', True),
+            ('--clear', 'clear', True),
+            ('--upgrade', 'upgrade', True),
+            ('--upgrade-deps', 'upgrade_deps', True),
+            ('--prompt', 'prompt', True),
+            ('--without-scm-ignore-files', 'scm_ignore_files', frozenset()),
         ]
-        for attr, opt in attrs:
-            rmtree(self.env_dir)
-            if not attr:
-                b = venv.EnvBuilder()
-            else:
-                b = venv.EnvBuilder(
-                    **{attr: False if attr in ('with_pip', 'symlinks') else True})
-            b.upgrade_dependencies = Mock() # avoid pip command to upgrade deps
-            b._setup_pip = Mock() # avoid pip setup
-            self.run_with_capture(b.create, self.env_dir)
-            data = self.get_text_file_contents('pyvenv.cfg')
-            if not attr:
-                for opt in ('--system-site-packages', '--clear', '--upgrade',
-                        '--upgrade-deps', '--prompt'):
-                    self.assertNotRegex(data, rf'command = .* {opt}')
-            elif os.name=='nt' and attr=='symlinks':
-                pass
-            else:
-                self.assertRegex(data, rf'command = .* {opt}')
+        for opt, attr, value in options:
+            with self.subTest(opt=opt, attr=attr, value=value):
+                rmtree(self.env_dir)
+                if not attr:
+                    kwargs = {}
+                else:
+                    kwargs = {attr: value}
+                b = venv.EnvBuilder(**kwargs)
+                b.upgrade_dependencies = Mock() # avoid pip command to upgrade deps
+                b._setup_pip = Mock() # avoid pip setup
+                self.run_with_capture(b.create, self.env_dir)
+                data = self.get_text_file_contents('pyvenv.cfg')
+                if not attr or opt.endswith('git'):
+                    for opt in ('--system-site-packages', '--clear', '--upgrade',
+                                '--upgrade-deps', '--prompt'):
+                        self.assertNotRegex(data, rf'command = .* {opt}')
+                elif os.name=='nt' and attr=='symlinks':
+                    pass
+                else:
+                    self.assertRegex(data, rf'command = .* {opt}')
 
     def test_prompt(self):
         env_name = os.path.split(self.env_dir)[1]
@@ -243,8 +253,7 @@ class BasicTest(BaseTest):
         # check a venv's prefixes
         rmtree(self.env_dir)
         self.run_with_capture(venv.create, self.env_dir)
-        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
-        cmd = [envpy, '-c', None]
+        cmd = [self.envpy(), '-c', None]
         for prefix, expected in (
             ('prefix', self.env_dir),
             ('exec_prefix', self.env_dir),
@@ -261,8 +270,7 @@ class BasicTest(BaseTest):
         """
         rmtree(self.env_dir)
         self.run_with_capture(venv.create, self.env_dir, symlinks=False)
-        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
-        cmd = [envpy, '-c', None]
+        cmd = [self.envpy(), '-c', None]
         for call, expected in (
             # installation scheme
             ('get_preferred_scheme("prefix")', 'venv'),
@@ -284,8 +292,7 @@ class BasicTest(BaseTest):
         """
         rmtree(self.env_dir)
         self.run_with_capture(venv.create, self.env_dir, symlinks=True)
-        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
-        cmd = [envpy, '-c', None]
+        cmd = [self.envpy(), '-c', None]
         for call, expected in (
             # installation scheme
             ('get_preferred_scheme("prefix")', 'venv'),
@@ -424,8 +431,7 @@ class BasicTest(BaseTest):
         """
         rmtree(self.env_dir)
         self.run_with_capture(venv.create, self.env_dir)
-        envpy = os.path.join(os.path.realpath(self.env_dir),
-                             self.bindir, self.exe)
+        envpy = self.envpy(real_env_dir=True)
         out, err = check_output([envpy, '-c',
             'import sys; print(sys.executable)'])
         self.assertEqual(out.strip(), envpy.encode())
@@ -438,8 +444,7 @@ class BasicTest(BaseTest):
         rmtree(self.env_dir)
         builder = venv.EnvBuilder(clear=True, symlinks=True)
         builder.create(self.env_dir)
-        envpy = os.path.join(os.path.realpath(self.env_dir),
-                             self.bindir, self.exe)
+        envpy = self.envpy(real_env_dir=True)
         out, err = check_output([envpy, '-c',
             'import sys; print(sys.executable)'])
         self.assertEqual(out.strip(), envpy.encode())
@@ -454,7 +459,6 @@ class BasicTest(BaseTest):
         builder = venv.EnvBuilder(clear=True)
         builder.create(env_dir)
         activate = os.path.join(env_dir, self.bindir, 'activate.bat')
-        envpy = os.path.join(env_dir, self.bindir, self.exe)
         out, err = check_output(
             [activate, '&', self.exe, '-c', 'print(0)'],
             encoding='oem',
@@ -473,9 +477,7 @@ class BasicTest(BaseTest):
 
         rmtree(self.env_dir)
         self.run_with_capture(venv.create, self.env_dir)
-        envpy = os.path.join(os.path.realpath(self.env_dir),
-                             self.bindir, self.exe)
-        out, err = check_output([envpy, '-c',
+        out, err = check_output([self.envpy(real_env_dir=True), '-c',
             'from multiprocessing import Pool; '
             'pool = Pool(1); '
             'print(pool.apply_async("Python".lower).get(3)); '
@@ -491,10 +493,8 @@ class BasicTest(BaseTest):
 
         rmtree(self.env_dir)
         self.run_with_capture(venv.create, self.env_dir)
-        envpy = os.path.join(os.path.realpath(self.env_dir),
-                             self.bindir, self.exe)
         script = os.path.join(TEST_HOME_DIR, '_test_venv_multiprocessing.py')
-        subprocess.check_call([envpy, script])
+        subprocess.check_call([self.envpy(real_env_dir=True), script])
 
     @unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
     def test_deactivate_with_strict_bash_opts(self):
@@ -521,9 +521,7 @@ class BasicTest(BaseTest):
         builder = venv.EnvBuilder()
         builder.create(self.env_dir)
 
-        envpy = os.path.join(os.path.realpath(self.env_dir),
-                             self.bindir, self.exe)
-        out, err = check_output([envpy, '-c',
+        out, err = check_output([self.envpy(real_env_dir=True), '-c',
             'import os; print("__PYVENV_LAUNCHER__" in os.environ)'])
         self.assertEqual(out.strip(), 'False'.encode())
 
@@ -585,6 +583,7 @@ class BasicTest(BaseTest):
                "-m",
                "venv",
                "--without-pip",
+               "--without-scm-ignore-files",
                self.env_dir]
         # Our fake non-installed python is not fully functional because
         # it cannot find the extensions. Set PYTHONPATH so it can run the
@@ -609,13 +608,13 @@ class BasicTest(BaseTest):
             # prevent https://github.com/python/cpython/issues/104839
             child_env["ASAN_OPTIONS"] = asan_options
         subprocess.check_call(cmd, env=child_env)
-        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
         # Now check the venv created from the non-installed python has
         # correct zip path in pythonpath.
-        cmd = [envpy, '-S', '-c', 'import sys; print(sys.path)']
+        cmd = [self.envpy(), '-S', '-c', 'import sys; print(sys.path)']
         out, err = check_output(cmd)
         self.assertTrue(zip_landmark.encode() in out)
 
+    @requireVenvCreate
     def test_activate_shell_script_has_no_dos_newlines(self):
         """
         Test that the `activate` shell script contains no CR LF.
@@ -632,13 +631,80 @@ class BasicTest(BaseTest):
                 error_message = f"CR LF found in line {i}"
                 self.assertFalse(line.endswith(b'\r\n'), error_message)
 
+    @requireVenvCreate
+    def test_scm_ignore_files_git(self):
+        """
+        Test that a .gitignore file is created when "git" is specified.
+        The file should contain a `*\n` line.
+        """
+        self.run_with_capture(venv.create, self.env_dir,
+                              scm_ignore_files={'git'})
+        file_lines = self.get_text_file_contents('.gitignore').splitlines()
+        self.assertIn('*', file_lines)
+
+    @requireVenvCreate
+    def test_create_scm_ignore_files_multiple(self):
+        """
+        Test that ``scm_ignore_files`` can work with multiple SCMs.
+        """
+        bzrignore_name = ".bzrignore"
+        contents = "# For Bazaar.\n*\n"
+
+        class BzrEnvBuilder(venv.EnvBuilder):
+            def create_bzr_ignore_file(self, context):
+                gitignore_path = os.path.join(context.env_dir, bzrignore_name)
+                with open(gitignore_path, 'w', encoding='utf-8') as file:
+                    file.write(contents)
+
+        builder = BzrEnvBuilder(scm_ignore_files={'git', 'bzr'})
+        self.run_with_capture(builder.create, self.env_dir)
+
+        gitignore_lines = self.get_text_file_contents('.gitignore').splitlines()
+        self.assertIn('*', gitignore_lines)
+
+        bzrignore = self.get_text_file_contents(bzrignore_name)
+        self.assertEqual(bzrignore, contents)
+
+    @requireVenvCreate
+    def test_create_scm_ignore_files_empty(self):
+        """
+        Test that no default ignore files are created when ``scm_ignore_files``
+        is empty.
+        """
+        # scm_ignore_files is set to frozenset() by default.
+        self.run_with_capture(venv.create, self.env_dir)
+        with self.assertRaises(FileNotFoundError):
+            self.get_text_file_contents('.gitignore')
+
+        self.assertIn("--without-scm-ignore-files",
+                      self.get_text_file_contents('pyvenv.cfg'))
+
+    @requireVenvCreate
+    def test_cli_with_scm_ignore_files(self):
+        """
+        Test that default SCM ignore files are created by default via the CLI.
+        """
+        self.run_with_capture(venv.main, ['--without-pip', self.env_dir])
+
+        gitignore_lines = self.get_text_file_contents('.gitignore').splitlines()
+        self.assertIn('*', gitignore_lines)
+
+    @requireVenvCreate
+    def test_cli_without_scm_ignore_files(self):
+        """
+        Test that ``--without-scm-ignore-files`` doesn't create SCM ignore files.
+        """
+        args = ['--without-pip', '--without-scm-ignore-files', self.env_dir]
+        self.run_with_capture(venv.main, args)
+
+        with self.assertRaises(FileNotFoundError):
+            self.get_text_file_contents('.gitignore')
+
 @requireVenvCreate
 class EnsurePipTest(BaseTest):
     """Test venv module installation of pip."""
     def assert_pip_not_installed(self):
-        envpy = os.path.join(os.path.realpath(self.env_dir),
-                             self.bindir, self.exe)
-        out, err = check_output([envpy, '-c',
+        out, err = check_output([self.envpy(real_env_dir=True), '-c',
             'try:\n import pip\nexcept ImportError:\n print("OK")'])
         # We force everything to text, so unittest gives the detailed diff
         # if we get unexpected results
@@ -705,9 +771,9 @@ class EnsurePipTest(BaseTest):
                                           system_site_packages=system_site_packages,
                                           with_pip=True)
         # Ensure pip is available in the virtual environment
-        envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe)
         # Ignore DeprecationWarning since pip code is not part of Python
-        out, err = check_output([envpy, '-W', 'ignore::DeprecationWarning',
+        out, err = check_output([self.envpy(real_env_dir=True),
+               '-W', 'ignore::DeprecationWarning',
                '-W', 'ignore::ImportWarning', '-I',
                '-m', 'pip', '--version'])
         # We force everything to text, so unittest gives the detailed diff
@@ -728,7 +794,7 @@ class EnsurePipTest(BaseTest):
                 # It seems ensurepip._uninstall calls subprocesses which do not
                 # inherit the interpreter settings.
                 envvars["PYTHONWARNINGS"] = "ignore"
-                out, err = check_output([envpy,
+                out, err = check_output([self.envpy(real_env_dir=True),
                     '-W', 'ignore::DeprecationWarning',
                     '-W', 'ignore::ImportWarning', '-I',
                     '-m', 'ensurepip._uninstall'])
index 2173c9b13e5cf7fda7a17ea2becef0cba587a1a1..d960bf3bd82ac5db5171e5544e66566ec074f70e 100644 (file)
@@ -41,11 +41,13 @@ class EnvBuilder:
                      environment
     :param prompt: Alternative terminal prefix for the environment.
     :param upgrade_deps: Update the base venv modules to the latest on PyPI
+    :param scm_ignore_files: Create ignore files for the SCMs specified by the
+                             iterable.
     """
 
     def __init__(self, system_site_packages=False, clear=False,
                  symlinks=False, upgrade=False, with_pip=False, prompt=None,
-                 upgrade_deps=False):
+                 upgrade_deps=False, *, scm_ignore_files=frozenset()):
         self.system_site_packages = system_site_packages
         self.clear = clear
         self.symlinks = symlinks
@@ -56,6 +58,7 @@ class EnvBuilder:
             prompt = os.path.basename(os.getcwd())
         self.prompt = prompt
         self.upgrade_deps = upgrade_deps
+        self.scm_ignore_files = frozenset(map(str.lower, scm_ignore_files))
 
     def create(self, env_dir):
         """
@@ -66,6 +69,8 @@ class EnvBuilder:
         """
         env_dir = os.path.abspath(env_dir)
         context = self.ensure_directories(env_dir)
+        for scm in self.scm_ignore_files:
+            getattr(self, f"create_{scm}_ignore_file")(context)
         # See issue 24875. We need system_site_packages to be False
         # until after pip is installed.
         true_system_site_packages = self.system_site_packages
@@ -210,6 +215,8 @@ class EnvBuilder:
                 args.append('--upgrade-deps')
             if self.orig_prompt is not None:
                 args.append(f'--prompt="{self.orig_prompt}"')
+            if not self.scm_ignore_files:
+                args.append('--without-scm-ignore-files')
 
             args.append(context.env_dir)
             args = ' '.join(args)
@@ -278,6 +285,19 @@ class EnvBuilder:
 
             shutil.copyfile(src, dst)
 
+    def create_git_ignore_file(self, context):
+        """
+        Create a .gitignore file in the environment directory.
+
+        The contents of the file cause the entire environment directory to be
+        ignored by git.
+        """
+        gitignore_path = os.path.join(context.env_dir, '.gitignore')
+        with open(gitignore_path, 'w', encoding='utf-8') as file:
+            file.write('# Created by venv; '
+                       'see https://docs.python.org/3/library/venv.html\n')
+            file.write('*\n')
+
     def setup_python(self, context):
         """
         Set up a Python executable in the environment.
@@ -461,11 +481,13 @@ class EnvBuilder:
 
 
 def create(env_dir, system_site_packages=False, clear=False,
-           symlinks=False, with_pip=False, prompt=None, upgrade_deps=False):
+           symlinks=False, with_pip=False, prompt=None, upgrade_deps=False,
+           *, scm_ignore_files=frozenset()):
     """Create a virtual environment in a directory."""
     builder = EnvBuilder(system_site_packages=system_site_packages,
                          clear=clear, symlinks=symlinks, with_pip=with_pip,
-                         prompt=prompt, upgrade_deps=upgrade_deps)
+                         prompt=prompt, upgrade_deps=upgrade_deps,
+                         scm_ignore_files=scm_ignore_files)
     builder.create(env_dir)
 
 
@@ -525,6 +547,11 @@ def main(args=None):
                         dest='upgrade_deps',
                         help=f'Upgrade core dependencies ({", ".join(CORE_VENV_DEPS)}) '
                              'to the latest version in PyPI')
+    parser.add_argument('--without-scm-ignore-files', dest='scm_ignore_files',
+                        action='store_const', const=frozenset(),
+                        default=frozenset(['git']),
+                        help='Skips adding SCM ignore files to the environment '
+                             'directory (Git is supported by default).')
     options = parser.parse_args(args)
     if options.upgrade and options.clear:
         raise ValueError('you cannot supply --upgrade and --clear together.')
@@ -534,7 +561,8 @@ def main(args=None):
                          upgrade=options.upgrade,
                          with_pip=options.with_pip,
                          prompt=options.prompt,
-                         upgrade_deps=options.upgrade_deps)
+                         upgrade_deps=options.upgrade_deps,
+                         scm_ignore_files=options.scm_ignore_files)
     for d in options.dirs:
         builder.create(d)
 
index 912423e4a781988accc165be8f0f6bb2e289482d..88f55439dc210c47139d31c665ad58f7f0f3f2ca 100644 (file)
@@ -6,5 +6,5 @@ try:
     main()
     rc = 0
 except Exception as e:
-    print('Error: %s' % e, file=sys.stderr)
+    print('Error:', e, file=sys.stderr)
 sys.exit(rc)
diff --git a/Misc/NEWS.d/next/Library/2023-08-18-22-58-07.gh-issue-83417.61J4yM.rst b/Misc/NEWS.d/next/Library/2023-08-18-22-58-07.gh-issue-83417.61J4yM.rst
new file mode 100644 (file)
index 0000000..fbb8bdb
--- /dev/null
@@ -0,0 +1,3 @@
+Add the ability for venv to create a ``.gitignore`` file which causes the
+created environment to be ignored by Git. It is on by default when venv is
+called via its CLI.