Skip to content

Commit

Permalink
pythonGH-83417: Allow venv add a .gitignore file to environments
Browse files Browse the repository at this point in the history
Off by default via code but on by default via the CLI, the `.gitignore` file contains `*` which causes the entire directory to be ignored.
  • Loading branch information
brettcannon committed Aug 18, 2023
1 parent dc7b630 commit f889dc6
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 9 deletions.
11 changes: 9 additions & 2 deletions Doc/library/venv.rst
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ 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, \*, gitignore=False)

The :class:`EnvBuilder` class accepts the following keyword arguments on
instantiation:
Expand Down Expand Up @@ -172,6 +172,10 @@ creation according to their needs, the :class:`EnvBuilder` class.

* ``upgrade_deps`` -- Update the base venv modules to the latest on PyPI

* ``gitignore`` -- a Boolean value which, if true, will create a
``.gitignore`` file in the target directory, containing ``*`` to have the
environment ignored by git.

.. versionchanged:: 3.4
Added the ``with_pip`` parameter

Expand All @@ -181,6 +185,9 @@ creation according to their needs, the :class:`EnvBuilder` class.
.. versionadded:: 3.9
Added the ``upgrade_deps`` parameter

.. versionadded:: 3.13
Added the ``gitignore`` parameter

Creators of third-party virtual environment tools will be free to use the
provided :class:`EnvBuilder` class as a base class.

Expand Down Expand Up @@ -343,7 +350,7 @@ 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, \*, gitignore=False)

Create an :class:`EnvBuilder` with the given keyword arguments, and call its
:meth:`~EnvBuilder.create` method with the *env_dir* argument.
Expand Down
18 changes: 16 additions & 2 deletions Lib/test/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ def _check_output_of_default_create(self):
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-gitignore {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
Expand All @@ -156,14 +157,16 @@ def test_config_file_command_key(self):
('upgrade', '--upgrade'),
('upgrade_deps', '--upgrade-deps'),
('prompt', '--prompt'),
('gitignore', '--without-gitignore'),
]
negated_attrs = {'with_pip', 'symlinks', 'gitignore'}
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})
**{attr: False if attr in negated_attrs 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)
Expand Down Expand Up @@ -586,6 +589,7 @@ def test_zippath_from_non_installed_posix(self):
"-m",
"venv",
"--without-pip",
"--without-gitignore",
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
Expand Down Expand Up @@ -633,6 +637,16 @@ def test_activate_shell_script_has_no_dos_newlines(self):
error_message = f"CR LF found in line {i}"
self.assertFalse(line.endswith(b'\r\n'), error_message)

def test_gitignore(self):
"""
Test that a .gitignore file is created when requested.
The file should contain a `*\n` line.
"""
self.run_with_capture(venv.create, self.env_dir, gitignore=True)
file_lines = self.get_text_file_contents('.gitignore').splitlines()
self.assertIn('*', file_lines)


@requireVenvCreate
class EnsurePipTest(BaseTest):
"""Test venv module installation of pip."""
Expand Down
36 changes: 32 additions & 4 deletions Lib/venv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 gitignore: Create a .gitignore file in the environment directory
which causes it to be ignored by git.
"""

def __init__(self, system_site_packages=False, clear=False,
symlinks=False, upgrade=False, with_pip=False, prompt=None,
upgrade_deps=False):
upgrade_deps=False, *, gitignore=False):
self.system_site_packages = system_site_packages
self.clear = clear
self.symlinks = symlinks
Expand All @@ -56,6 +58,7 @@ def __init__(self, system_site_packages=False, clear=False,
prompt = os.path.basename(os.getcwd())
self.prompt = prompt
self.upgrade_deps = upgrade_deps
self.gitignore = gitignore

def create(self, env_dir):
"""
Expand All @@ -66,6 +69,8 @@ def create(self, env_dir):
"""
env_dir = os.path.abspath(env_dir)
context = self.ensure_directories(env_dir)
if self.gitignore:
self._setup_gitignore(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
Expand Down Expand Up @@ -210,6 +215,8 @@ def create_configuration(self, context):
args.append('--upgrade-deps')
if self.orig_prompt is not None:
args.append(f'--prompt="{self.orig_prompt}"')
if not self.gitignore:
args.append('--without-gitignore')

args.append(context.env_dir)
args = ' '.join(args)
Expand Down Expand Up @@ -278,6 +285,19 @@ def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):

shutil.copyfile(src, dst)

def _setup_gitignore(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.
Expand Down Expand Up @@ -461,11 +481,13 @@ def upgrade_dependencies(self, context):


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,
*, gitignore=False):
"""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,
gitignore=gitignore)
builder.create(env_dir)


Expand Down Expand Up @@ -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-gitignore', dest='gitignore',
default=True, action='store_false',
help='Skips adding a .gitignore file to the '
'environment directory which causes git to ignore '
'the environment directory.')
options = parser.parse_args(args)
if options.upgrade and options.clear:
raise ValueError('you cannot supply --upgrade and --clear together.')
Expand All @@ -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,
gitignore=options.gitignore)
for d in options.dirs:
builder.create(d)

Expand Down
2 changes: 1 addition & 1 deletion Lib/venv/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
main()
rc = 0
except Exception as e:
print('Error: %s' % e, file=sys.stderr)
print('Error:', e, file=sys.stderr)
sys.exit(rc)

0 comments on commit f889dc6

Please sign in to comment.