mirror of https://github.com/python/cpython.git
GH-83417: Allow `venv` to add a `.gitignore` file to environments via a new `scm_ignore_file` parameter (GH-108125)
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>
This commit is contained in:
parent
6b179adb8c
commit
e218e5022e
|
@ -143,7 +143,8 @@ creation according to their needs, the :class:`EnvBuilder` class.
|
||||||
|
|
||||||
.. class:: EnvBuilder(system_site_packages=False, clear=False, \
|
.. class:: EnvBuilder(system_site_packages=False, clear=False, \
|
||||||
symlinks=False, upgrade=False, with_pip=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
|
The :class:`EnvBuilder` class accepts the following keyword arguments on
|
||||||
instantiation:
|
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
|
* ``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
|
.. versionchanged:: 3.4
|
||||||
Added the ``with_pip`` parameter
|
Added the ``with_pip`` parameter
|
||||||
|
|
||||||
|
@ -181,6 +188,9 @@ creation according to their needs, the :class:`EnvBuilder` class.
|
||||||
.. versionadded:: 3.9
|
.. versionadded:: 3.9
|
||||||
Added the ``upgrade_deps`` parameter
|
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
|
Creators of third-party virtual environment tools will be free to use the
|
||||||
provided :class:`EnvBuilder` class as a base class.
|
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
|
The directories are allowed to exist (for when an existing environment
|
||||||
is being upgraded).
|
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:
|
There is also a module-level convenience function:
|
||||||
|
|
||||||
.. function:: create(env_dir, system_site_packages=False, clear=False, \
|
.. function:: create(env_dir, system_site_packages=False, clear=False, \
|
||||||
symlinks=False, with_pip=False, prompt=None, \
|
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
|
Create an :class:`EnvBuilder` with the given keyword arguments, and call its
|
||||||
:meth:`~EnvBuilder.create` method with the *env_dir* argument.
|
:meth:`~EnvBuilder.create` method with the *env_dir* argument.
|
||||||
|
@ -359,6 +376,9 @@ There is also a module-level convenience function:
|
||||||
.. versionchanged:: 3.9
|
.. versionchanged:: 3.9
|
||||||
Added the ``upgrade_deps`` parameter
|
Added the ``upgrade_deps`` parameter
|
||||||
|
|
||||||
|
.. versionchanged:: 3.13
|
||||||
|
Added the ``scm_ignore_files`` parameter
|
||||||
|
|
||||||
An example of extending ``EnvBuilder``
|
An example of extending ``EnvBuilder``
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -35,37 +35,48 @@ your :ref:`Python installation <using-on-windows>`::
|
||||||
|
|
||||||
The command, if run with ``-h``, will show the available options::
|
The command, if run with ``-h``, will show the available options::
|
||||||
|
|
||||||
usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
|
usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
|
||||||
[--upgrade] [--without-pip] [--prompt PROMPT] [--upgrade-deps]
|
[--upgrade] [--without-pip] [--prompt PROMPT] [--upgrade-deps]
|
||||||
ENV_DIR [ENV_DIR ...]
|
[--without-scm-ignore-file]
|
||||||
|
ENV_DIR [ENV_DIR ...]
|
||||||
|
|
||||||
Creates virtual Python environments in one or more target directories.
|
Creates virtual Python environments in one or more target directories.
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
ENV_DIR A directory to create the environment in.
|
ENV_DIR A directory to create the environment in.
|
||||||
|
|
||||||
optional arguments:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--system-site-packages
|
--system-site-packages
|
||||||
Give the virtual environment access to the system
|
Give the virtual environment access to the system
|
||||||
site-packages dir.
|
site-packages dir.
|
||||||
--symlinks Try to use symlinks rather than copies, when symlinks
|
--symlinks Try to use symlinks rather than copies, when
|
||||||
are not the default for the platform.
|
symlinks are not the default for the platform.
|
||||||
--copies Try to use copies rather than symlinks, even when
|
--copies Try to use copies rather than symlinks, even when
|
||||||
symlinks are the default for the platform.
|
symlinks are the default for the platform.
|
||||||
--clear Delete the contents of the environment directory if it
|
--clear Delete the contents of the environment directory if
|
||||||
already exists, before environment creation.
|
it already exists, before environment creation.
|
||||||
--upgrade Upgrade the environment directory to use this version
|
--upgrade Upgrade the environment directory to use this
|
||||||
of Python, assuming Python has been upgraded in-place.
|
version of Python, assuming Python has been upgraded
|
||||||
--without-pip Skips installing or upgrading pip in the virtual
|
in-place.
|
||||||
environment (pip is bootstrapped by default)
|
--without-pip Skips installing or upgrading pip in the virtual
|
||||||
--prompt PROMPT Provides an alternative prompt prefix for this
|
environment (pip is bootstrapped by default)
|
||||||
environment.
|
--prompt PROMPT Provides an alternative prompt prefix for this
|
||||||
--upgrade-deps Upgrade core dependencies (pip) to the
|
environment.
|
||||||
latest version in PyPI
|
--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
|
Once an environment has been created, you may wish to activate it, e.g. by
|
||||||
sourcing an activate script in its bin directory.
|
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
|
.. versionchanged:: 3.12
|
||||||
|
|
||||||
|
|
|
@ -220,6 +220,16 @@ typing
|
||||||
check whether a class is a :class:`typing.Protocol`. (Contributed by Jelle Zijlstra in
|
check whether a class is a :class:`typing.Protocol`. (Contributed by Jelle Zijlstra in
|
||||||
:gh:`104873`.)
|
: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
|
Optimizations
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,13 @@ def setUp(self):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
rmtree(self.env_dir)
|
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):
|
def run_with_capture(self, func, *args, **kwargs):
|
||||||
with captured_stdout() as output:
|
with captured_stdout() as output:
|
||||||
with captured_stderr() as error:
|
with captured_stderr() as error:
|
||||||
|
@ -138,7 +145,8 @@ def _check_output_of_default_create(self):
|
||||||
self.assertIn('executable = %s' %
|
self.assertIn('executable = %s' %
|
||||||
os.path.realpath(sys.executable), data)
|
os.path.realpath(sys.executable), data)
|
||||||
copies = '' if os.name=='nt' else ' --copies'
|
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)
|
self.assertIn(cmd, data)
|
||||||
fn = self.get_env_file(self.bindir, self.exe)
|
fn = self.get_env_file(self.bindir, self.exe)
|
||||||
if not os.path.exists(fn): # diagnostics for Windows buildbot failures
|
if not os.path.exists(fn): # diagnostics for Windows buildbot failures
|
||||||
|
@ -148,35 +156,37 @@ def _check_output_of_default_create(self):
|
||||||
self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
|
self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
|
||||||
|
|
||||||
def test_config_file_command_key(self):
|
def test_config_file_command_key(self):
|
||||||
attrs = [
|
options = [
|
||||||
(None, None),
|
(None, None, None), # Default case.
|
||||||
('symlinks', '--copies'),
|
('--copies', 'symlinks', False),
|
||||||
('with_pip', '--without-pip'),
|
('--without-pip', 'with_pip', False),
|
||||||
('system_site_packages', '--system-site-packages'),
|
('--system-site-packages', 'system_site_packages', True),
|
||||||
('clear', '--clear'),
|
('--clear', 'clear', True),
|
||||||
('upgrade', '--upgrade'),
|
('--upgrade', 'upgrade', True),
|
||||||
('upgrade_deps', '--upgrade-deps'),
|
('--upgrade-deps', 'upgrade_deps', True),
|
||||||
('prompt', '--prompt'),
|
('--prompt', 'prompt', True),
|
||||||
|
('--without-scm-ignore-files', 'scm_ignore_files', frozenset()),
|
||||||
]
|
]
|
||||||
for attr, opt in attrs:
|
for opt, attr, value in options:
|
||||||
rmtree(self.env_dir)
|
with self.subTest(opt=opt, attr=attr, value=value):
|
||||||
if not attr:
|
rmtree(self.env_dir)
|
||||||
b = venv.EnvBuilder()
|
if not attr:
|
||||||
else:
|
kwargs = {}
|
||||||
b = venv.EnvBuilder(
|
else:
|
||||||
**{attr: False if attr in ('with_pip', 'symlinks') else True})
|
kwargs = {attr: value}
|
||||||
b.upgrade_dependencies = Mock() # avoid pip command to upgrade deps
|
b = venv.EnvBuilder(**kwargs)
|
||||||
b._setup_pip = Mock() # avoid pip setup
|
b.upgrade_dependencies = Mock() # avoid pip command to upgrade deps
|
||||||
self.run_with_capture(b.create, self.env_dir)
|
b._setup_pip = Mock() # avoid pip setup
|
||||||
data = self.get_text_file_contents('pyvenv.cfg')
|
self.run_with_capture(b.create, self.env_dir)
|
||||||
if not attr:
|
data = self.get_text_file_contents('pyvenv.cfg')
|
||||||
for opt in ('--system-site-packages', '--clear', '--upgrade',
|
if not attr or opt.endswith('git'):
|
||||||
'--upgrade-deps', '--prompt'):
|
for opt in ('--system-site-packages', '--clear', '--upgrade',
|
||||||
self.assertNotRegex(data, rf'command = .* {opt}')
|
'--upgrade-deps', '--prompt'):
|
||||||
elif os.name=='nt' and attr=='symlinks':
|
self.assertNotRegex(data, rf'command = .* {opt}')
|
||||||
pass
|
elif os.name=='nt' and attr=='symlinks':
|
||||||
else:
|
pass
|
||||||
self.assertRegex(data, rf'command = .* {opt}')
|
else:
|
||||||
|
self.assertRegex(data, rf'command = .* {opt}')
|
||||||
|
|
||||||
def test_prompt(self):
|
def test_prompt(self):
|
||||||
env_name = os.path.split(self.env_dir)[1]
|
env_name = os.path.split(self.env_dir)[1]
|
||||||
|
@ -243,8 +253,7 @@ def test_prefixes(self):
|
||||||
# check a venv's prefixes
|
# check a venv's prefixes
|
||||||
rmtree(self.env_dir)
|
rmtree(self.env_dir)
|
||||||
self.run_with_capture(venv.create, self.env_dir)
|
self.run_with_capture(venv.create, self.env_dir)
|
||||||
envpy = os.path.join(self.env_dir, self.bindir, self.exe)
|
cmd = [self.envpy(), '-c', None]
|
||||||
cmd = [envpy, '-c', None]
|
|
||||||
for prefix, expected in (
|
for prefix, expected in (
|
||||||
('prefix', self.env_dir),
|
('prefix', self.env_dir),
|
||||||
('exec_prefix', self.env_dir),
|
('exec_prefix', self.env_dir),
|
||||||
|
@ -261,8 +270,7 @@ def test_sysconfig(self):
|
||||||
"""
|
"""
|
||||||
rmtree(self.env_dir)
|
rmtree(self.env_dir)
|
||||||
self.run_with_capture(venv.create, self.env_dir, symlinks=False)
|
self.run_with_capture(venv.create, self.env_dir, symlinks=False)
|
||||||
envpy = os.path.join(self.env_dir, self.bindir, self.exe)
|
cmd = [self.envpy(), '-c', None]
|
||||||
cmd = [envpy, '-c', None]
|
|
||||||
for call, expected in (
|
for call, expected in (
|
||||||
# installation scheme
|
# installation scheme
|
||||||
('get_preferred_scheme("prefix")', 'venv'),
|
('get_preferred_scheme("prefix")', 'venv'),
|
||||||
|
@ -284,8 +292,7 @@ def test_sysconfig_symlinks(self):
|
||||||
"""
|
"""
|
||||||
rmtree(self.env_dir)
|
rmtree(self.env_dir)
|
||||||
self.run_with_capture(venv.create, self.env_dir, symlinks=True)
|
self.run_with_capture(venv.create, self.env_dir, symlinks=True)
|
||||||
envpy = os.path.join(self.env_dir, self.bindir, self.exe)
|
cmd = [self.envpy(), '-c', None]
|
||||||
cmd = [envpy, '-c', None]
|
|
||||||
for call, expected in (
|
for call, expected in (
|
||||||
# installation scheme
|
# installation scheme
|
||||||
('get_preferred_scheme("prefix")', 'venv'),
|
('get_preferred_scheme("prefix")', 'venv'),
|
||||||
|
@ -424,8 +431,7 @@ def test_executable(self):
|
||||||
"""
|
"""
|
||||||
rmtree(self.env_dir)
|
rmtree(self.env_dir)
|
||||||
self.run_with_capture(venv.create, self.env_dir)
|
self.run_with_capture(venv.create, self.env_dir)
|
||||||
envpy = os.path.join(os.path.realpath(self.env_dir),
|
envpy = self.envpy(real_env_dir=True)
|
||||||
self.bindir, self.exe)
|
|
||||||
out, err = check_output([envpy, '-c',
|
out, err = check_output([envpy, '-c',
|
||||||
'import sys; print(sys.executable)'])
|
'import sys; print(sys.executable)'])
|
||||||
self.assertEqual(out.strip(), envpy.encode())
|
self.assertEqual(out.strip(), envpy.encode())
|
||||||
|
@ -438,8 +444,7 @@ def test_executable_symlinks(self):
|
||||||
rmtree(self.env_dir)
|
rmtree(self.env_dir)
|
||||||
builder = venv.EnvBuilder(clear=True, symlinks=True)
|
builder = venv.EnvBuilder(clear=True, symlinks=True)
|
||||||
builder.create(self.env_dir)
|
builder.create(self.env_dir)
|
||||||
envpy = os.path.join(os.path.realpath(self.env_dir),
|
envpy = self.envpy(real_env_dir=True)
|
||||||
self.bindir, self.exe)
|
|
||||||
out, err = check_output([envpy, '-c',
|
out, err = check_output([envpy, '-c',
|
||||||
'import sys; print(sys.executable)'])
|
'import sys; print(sys.executable)'])
|
||||||
self.assertEqual(out.strip(), envpy.encode())
|
self.assertEqual(out.strip(), envpy.encode())
|
||||||
|
@ -454,7 +459,6 @@ def test_unicode_in_batch_file(self):
|
||||||
builder = venv.EnvBuilder(clear=True)
|
builder = venv.EnvBuilder(clear=True)
|
||||||
builder.create(env_dir)
|
builder.create(env_dir)
|
||||||
activate = os.path.join(env_dir, self.bindir, 'activate.bat')
|
activate = os.path.join(env_dir, self.bindir, 'activate.bat')
|
||||||
envpy = os.path.join(env_dir, self.bindir, self.exe)
|
|
||||||
out, err = check_output(
|
out, err = check_output(
|
||||||
[activate, '&', self.exe, '-c', 'print(0)'],
|
[activate, '&', self.exe, '-c', 'print(0)'],
|
||||||
encoding='oem',
|
encoding='oem',
|
||||||
|
@ -473,9 +477,7 @@ def test_multiprocessing(self):
|
||||||
|
|
||||||
rmtree(self.env_dir)
|
rmtree(self.env_dir)
|
||||||
self.run_with_capture(venv.create, self.env_dir)
|
self.run_with_capture(venv.create, self.env_dir)
|
||||||
envpy = os.path.join(os.path.realpath(self.env_dir),
|
out, err = check_output([self.envpy(real_env_dir=True), '-c',
|
||||||
self.bindir, self.exe)
|
|
||||||
out, err = check_output([envpy, '-c',
|
|
||||||
'from multiprocessing import Pool; '
|
'from multiprocessing import Pool; '
|
||||||
'pool = Pool(1); '
|
'pool = Pool(1); '
|
||||||
'print(pool.apply_async("Python".lower).get(3)); '
|
'print(pool.apply_async("Python".lower).get(3)); '
|
||||||
|
@ -491,10 +493,8 @@ def test_multiprocessing_recursion(self):
|
||||||
|
|
||||||
rmtree(self.env_dir)
|
rmtree(self.env_dir)
|
||||||
self.run_with_capture(venv.create, 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')
|
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')
|
@unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
|
||||||
def test_deactivate_with_strict_bash_opts(self):
|
def test_deactivate_with_strict_bash_opts(self):
|
||||||
|
@ -521,9 +521,7 @@ def test_macos_env(self):
|
||||||
builder = venv.EnvBuilder()
|
builder = venv.EnvBuilder()
|
||||||
builder.create(self.env_dir)
|
builder.create(self.env_dir)
|
||||||
|
|
||||||
envpy = os.path.join(os.path.realpath(self.env_dir),
|
out, err = check_output([self.envpy(real_env_dir=True), '-c',
|
||||||
self.bindir, self.exe)
|
|
||||||
out, err = check_output([envpy, '-c',
|
|
||||||
'import os; print("__PYVENV_LAUNCHER__" in os.environ)'])
|
'import os; print("__PYVENV_LAUNCHER__" in os.environ)'])
|
||||||
self.assertEqual(out.strip(), 'False'.encode())
|
self.assertEqual(out.strip(), 'False'.encode())
|
||||||
|
|
||||||
|
@ -585,6 +583,7 @@ def test_zippath_from_non_installed_posix(self):
|
||||||
"-m",
|
"-m",
|
||||||
"venv",
|
"venv",
|
||||||
"--without-pip",
|
"--without-pip",
|
||||||
|
"--without-scm-ignore-files",
|
||||||
self.env_dir]
|
self.env_dir]
|
||||||
# Our fake non-installed python is not fully functional because
|
# Our fake non-installed python is not fully functional because
|
||||||
# it cannot find the extensions. Set PYTHONPATH so it can run the
|
# it cannot find the extensions. Set PYTHONPATH so it can run the
|
||||||
|
@ -609,13 +608,13 @@ def test_zippath_from_non_installed_posix(self):
|
||||||
# prevent https://github.com/python/cpython/issues/104839
|
# prevent https://github.com/python/cpython/issues/104839
|
||||||
child_env["ASAN_OPTIONS"] = asan_options
|
child_env["ASAN_OPTIONS"] = asan_options
|
||||||
subprocess.check_call(cmd, env=child_env)
|
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
|
# Now check the venv created from the non-installed python has
|
||||||
# correct zip path in pythonpath.
|
# 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)
|
out, err = check_output(cmd)
|
||||||
self.assertTrue(zip_landmark.encode() in out)
|
self.assertTrue(zip_landmark.encode() in out)
|
||||||
|
|
||||||
|
@requireVenvCreate
|
||||||
def test_activate_shell_script_has_no_dos_newlines(self):
|
def test_activate_shell_script_has_no_dos_newlines(self):
|
||||||
"""
|
"""
|
||||||
Test that the `activate` shell script contains no CR LF.
|
Test that the `activate` shell script contains no CR LF.
|
||||||
|
@ -632,13 +631,80 @@ def test_activate_shell_script_has_no_dos_newlines(self):
|
||||||
error_message = f"CR LF found in line {i}"
|
error_message = f"CR LF found in line {i}"
|
||||||
self.assertFalse(line.endswith(b'\r\n'), error_message)
|
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
|
@requireVenvCreate
|
||||||
class EnsurePipTest(BaseTest):
|
class EnsurePipTest(BaseTest):
|
||||||
"""Test venv module installation of pip."""
|
"""Test venv module installation of pip."""
|
||||||
def assert_pip_not_installed(self):
|
def assert_pip_not_installed(self):
|
||||||
envpy = os.path.join(os.path.realpath(self.env_dir),
|
out, err = check_output([self.envpy(real_env_dir=True), '-c',
|
||||||
self.bindir, self.exe)
|
|
||||||
out, err = check_output([envpy, '-c',
|
|
||||||
'try:\n import pip\nexcept ImportError:\n print("OK")'])
|
'try:\n import pip\nexcept ImportError:\n print("OK")'])
|
||||||
# We force everything to text, so unittest gives the detailed diff
|
# We force everything to text, so unittest gives the detailed diff
|
||||||
# if we get unexpected results
|
# if we get unexpected results
|
||||||
|
@ -705,9 +771,9 @@ def do_test_with_pip(self, system_site_packages):
|
||||||
system_site_packages=system_site_packages,
|
system_site_packages=system_site_packages,
|
||||||
with_pip=True)
|
with_pip=True)
|
||||||
# Ensure pip is available in the virtual environment
|
# 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
|
# 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',
|
'-W', 'ignore::ImportWarning', '-I',
|
||||||
'-m', 'pip', '--version'])
|
'-m', 'pip', '--version'])
|
||||||
# We force everything to text, so unittest gives the detailed diff
|
# We force everything to text, so unittest gives the detailed diff
|
||||||
|
@ -728,7 +794,7 @@ def do_test_with_pip(self, system_site_packages):
|
||||||
# It seems ensurepip._uninstall calls subprocesses which do not
|
# It seems ensurepip._uninstall calls subprocesses which do not
|
||||||
# inherit the interpreter settings.
|
# inherit the interpreter settings.
|
||||||
envvars["PYTHONWARNINGS"] = "ignore"
|
envvars["PYTHONWARNINGS"] = "ignore"
|
||||||
out, err = check_output([envpy,
|
out, err = check_output([self.envpy(real_env_dir=True),
|
||||||
'-W', 'ignore::DeprecationWarning',
|
'-W', 'ignore::DeprecationWarning',
|
||||||
'-W', 'ignore::ImportWarning', '-I',
|
'-W', 'ignore::ImportWarning', '-I',
|
||||||
'-m', 'ensurepip._uninstall'])
|
'-m', 'ensurepip._uninstall'])
|
||||||
|
|
|
@ -41,11 +41,13 @@ class EnvBuilder:
|
||||||
environment
|
environment
|
||||||
:param prompt: Alternative terminal prefix for the environment.
|
:param prompt: Alternative terminal prefix for the environment.
|
||||||
:param upgrade_deps: Update the base venv modules to the latest on PyPI
|
: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,
|
def __init__(self, system_site_packages=False, clear=False,
|
||||||
symlinks=False, upgrade=False, with_pip=False, prompt=None,
|
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.system_site_packages = system_site_packages
|
||||||
self.clear = clear
|
self.clear = clear
|
||||||
self.symlinks = symlinks
|
self.symlinks = symlinks
|
||||||
|
@ -56,6 +58,7 @@ def __init__(self, system_site_packages=False, clear=False,
|
||||||
prompt = os.path.basename(os.getcwd())
|
prompt = os.path.basename(os.getcwd())
|
||||||
self.prompt = prompt
|
self.prompt = prompt
|
||||||
self.upgrade_deps = upgrade_deps
|
self.upgrade_deps = upgrade_deps
|
||||||
|
self.scm_ignore_files = frozenset(map(str.lower, scm_ignore_files))
|
||||||
|
|
||||||
def create(self, env_dir):
|
def create(self, env_dir):
|
||||||
"""
|
"""
|
||||||
|
@ -66,6 +69,8 @@ def create(self, env_dir):
|
||||||
"""
|
"""
|
||||||
env_dir = os.path.abspath(env_dir)
|
env_dir = os.path.abspath(env_dir)
|
||||||
context = self.ensure_directories(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
|
# See issue 24875. We need system_site_packages to be False
|
||||||
# until after pip is installed.
|
# until after pip is installed.
|
||||||
true_system_site_packages = self.system_site_packages
|
true_system_site_packages = self.system_site_packages
|
||||||
|
@ -210,6 +215,8 @@ def create_configuration(self, context):
|
||||||
args.append('--upgrade-deps')
|
args.append('--upgrade-deps')
|
||||||
if self.orig_prompt is not None:
|
if self.orig_prompt is not None:
|
||||||
args.append(f'--prompt="{self.orig_prompt}"')
|
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.append(context.env_dir)
|
||||||
args = ' '.join(args)
|
args = ' '.join(args)
|
||||||
|
@ -278,6 +285,19 @@ def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
|
||||||
|
|
||||||
shutil.copyfile(src, dst)
|
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):
|
def setup_python(self, context):
|
||||||
"""
|
"""
|
||||||
Set up a Python executable in the environment.
|
Set up a Python executable in the environment.
|
||||||
|
@ -461,11 +481,13 @@ def upgrade_dependencies(self, context):
|
||||||
|
|
||||||
|
|
||||||
def create(env_dir, system_site_packages=False, clear=False,
|
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."""
|
"""Create a virtual environment in a directory."""
|
||||||
builder = EnvBuilder(system_site_packages=system_site_packages,
|
builder = EnvBuilder(system_site_packages=system_site_packages,
|
||||||
clear=clear, symlinks=symlinks, with_pip=with_pip,
|
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)
|
builder.create(env_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@ -525,6 +547,11 @@ def main(args=None):
|
||||||
dest='upgrade_deps',
|
dest='upgrade_deps',
|
||||||
help=f'Upgrade core dependencies ({", ".join(CORE_VENV_DEPS)}) '
|
help=f'Upgrade core dependencies ({", ".join(CORE_VENV_DEPS)}) '
|
||||||
'to the latest version in PyPI')
|
'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)
|
options = parser.parse_args(args)
|
||||||
if options.upgrade and options.clear:
|
if options.upgrade and options.clear:
|
||||||
raise ValueError('you cannot supply --upgrade and --clear together.')
|
raise ValueError('you cannot supply --upgrade and --clear together.')
|
||||||
|
@ -534,7 +561,8 @@ def main(args=None):
|
||||||
upgrade=options.upgrade,
|
upgrade=options.upgrade,
|
||||||
with_pip=options.with_pip,
|
with_pip=options.with_pip,
|
||||||
prompt=options.prompt,
|
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:
|
for d in options.dirs:
|
||||||
builder.create(d)
|
builder.create(d)
|
||||||
|
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
main()
|
main()
|
||||||
rc = 0
|
rc = 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('Error: %s' % e, file=sys.stderr)
|
print('Error:', e, file=sys.stderr)
|
||||||
sys.exit(rc)
|
sys.exit(rc)
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue