Repository: pre-commit/pre-commit

Issue:
deprecate `python_venv` language
this has been an alias to `python` for a very long time but it cannot be removed without a deprecation period

this is going to need a long deprecation period since it's sorta subtle and usually not the user's fault and will need hook authors to (potentially) make updates

the plan is to do the following:

1. introduce the following in a minor release
    - migrate-config will autofix `.pre-commit-config.yaml` usages of `language: python_venv` (there isn't an equivalent `migrate-manifest` -- though users outnumber hook authors by several orders of magnitude)
1. introduce the following in a minor release
    - a warning is shown for configuration using the `language: python_venv`
    - a warning is shown for repos using `language: python_venv` (do this at install time so it only shows once as to not be super annoying for users who have no control)
    - a recommendation for hook authors to also set `minimum_pre_commit_version` to this version
1. a long time passes (typically my deprecation period has been 12-18+ months)
1. introduce the following in a major release
        - removal of the `python_venv` alias



Discussion:

Resolving patch:
diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py
--- a/pre_commit/commands/migrate_config.py
+++ b/pre_commit/commands/migrate_config.py
@@ -42,6 +42,14 @@ def _migrate_sha_to_rev(contents: str) -> str:
     return re.sub(r'(\n\s+)sha:', r'\1rev:', contents)
 
 
+def _migrate_python_venv(contents: str) -> str:
+    return re.sub(
+        r'(\n\s+)language: python_venv\b',
+        r'\1language: python',
+        contents,
+    )
+
+
 def migrate_config(config_file: str, quiet: bool = False) -> int:
     with open(config_file) as f:
         orig_contents = contents = f.read()
@@ -55,6 +63,7 @@ def migrate_config(config_file: str, quiet: bool = False) -> int:
 
     contents = _migrate_map(contents)
     contents = _migrate_sha_to_rev(contents)
+    contents = _migrate_python_venv(contents)
 
     if contents != orig_contents:
         with open(config_file, 'w') as f:
diff --git a/pre_commit/repository.py b/pre_commit/repository.py
--- a/pre_commit/repository.py
+++ b/pre_commit/repository.py
@@ -3,6 +3,7 @@
 import json
 import logging
 import os
+import shlex
 from typing import Any
 from typing import Sequence
 
@@ -68,6 +69,14 @@ def _hook_install(hook: Hook) -> None:
     logger.info('Once installed this environment will be reused.')
     logger.info('This may take a few minutes...')
 
+    if hook.language == 'python_venv':
+        logger.warning(
+            f'`repo: {hook.src}` uses deprecated `language: python_venv`.  '
+            f'This is an alias for `language: python`.  '
+            f'Often `pre-commit autoupdate --repo {shlex.quote(hook.src)}` '
+            f'will fix this.',
+        )
+
     lang = languages[hook.language]
     assert lang.ENVIRONMENT_DIR is not None
 


Test patch:
diff --git a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml
deleted file mode 100644
--- a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
--   id: foo
-    name: Foo
-    entry: foo
-    language: python_venv
-    files: \.py$
diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py
deleted file mode 100644
--- a/testing/resources/python_venv_hooks_repo/foo.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from __future__ import annotations
-
-import sys
-
-
-def main():
-    print(repr(sys.argv[1:]))
-    print('Hello World')
-    return 0
diff --git a/testing/resources/python_venv_hooks_repo/setup.py b/testing/resources/python_venv_hooks_repo/setup.py
deleted file mode 100644
--- a/testing/resources/python_venv_hooks_repo/setup.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from __future__ import annotations
-
-from setuptools import setup
-
-setup(
-    name='foo',
-    version='0.0.0',
-    py_modules=['foo'],
-    entry_points={'console_scripts': ['foo = foo:main']},
-)
diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py
--- a/tests/commands/migrate_config_test.py
+++ b/tests/commands/migrate_config_test.py
@@ -134,6 +134,39 @@ def test_migrate_config_sha_to_rev(tmpdir):
     )
 
 
+def test_migrate_config_language_python_venv(tmp_path):
+    src = '''\
+repos:
+-   repo: local
+    hooks:
+    -   id: example
+        name: example
+        entry: example
+        language: python_venv
+    -   id: example
+        name: example
+        entry: example
+        language: system
+'''
+    expected = '''\
+repos:
+-   repo: local
+    hooks:
+    -   id: example
+        name: example
+        entry: example
+        language: python
+    -   id: example
+        name: example
+        entry: example
+        language: system
+'''
+    cfg = tmp_path.joinpath('cfg.yaml')
+    cfg.write_text(src)
+    assert migrate_config(str(cfg)) == 0
+    assert cfg.read_text() == expected
+
+
 def test_migrate_config_invalid_yaml(tmpdir):
     contents = '['
     cfg = tmpdir.join(C.CONFIG_FILE)
diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py
new file mode 100644
--- /dev/null
+++ b/tests/languages/all_test.py
@@ -0,0 +1,7 @@
+from __future__ import annotations
+
+from pre_commit.languages.all import languages
+
+
+def test_python_venv_is_an_alias_to_python():
+    assert languages['python_venv'] is languages['python']
diff --git a/tests/repository_test.py b/tests/repository_test.py
--- a/tests/repository_test.py
+++ b/tests/repository_test.py
@@ -129,11 +129,21 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store):
     )
 
 
-def test_python_venv(tempdir_factory, store):
-    _test_hook_repo(
-        tempdir_factory, store, 'python_venv_hooks_repo',
-        'foo', [os.devnull],
-        f'[{os.devnull!r}]\nHello World\n'.encode(),
+def test_python_venv_deprecation(store, caplog):
+    config = {
+        'repo': 'local',
+        'hooks': [{
+            'id': 'example',
+            'name': 'example',
+            'language': 'python_venv',
+            'entry': 'echo hi',
+        }],
+    }
+    _get_hook(config, store, 'example')
+    assert caplog.messages[-1] == (
+        '`repo: local` uses deprecated `language: python_venv`.  '
+        'This is an alias for `language: python`.  '
+        'Often `pre-commit autoupdate --repo local` will fix this.'
     )