Add pipenv language support to pre-commit

- Implement pipenv language module in pre-commit
- Update workflows to include pipenv language tests
- Add pipenv language to allowed languages in testing script
- Create comprehensive test suite for pipenv language support
This commit is contained in:
Maciej Nachtygal 2025-02-04 13:25:11 +01:00
parent aba1ce04e7
commit 8df1bf67f1
6 changed files with 214 additions and 107 deletions

View file

@ -11,74 +11,29 @@ concurrency:
cancel-in-progress: true
jobs:
vars:
runs-on: ubuntu-latest
outputs:
languages: ${{ steps.vars.outputs.languages }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-python@v4
with:
python-version: 3.9
- name: install deps
run: python -mpip install -e . -r requirements-dev.txt
- name: vars
run: testing/languages ${{ github.event_name == 'push' && '--all' || '' }}
id: vars
language:
needs: [vars]
runs-on: ${{ matrix.os }}
if: needs.vars.outputs.languages != '[]'
language-tests:
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.vars.outputs.languages) }}
os: [ubuntu-latest, windows-latest]
language: [python, pipenv]
runs-on: ${{ matrix.os }}
steps:
- uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.9
- run: echo "$CONDA\Scripts" >> "$GITHUB_PATH"
shell: bash
if: matrix.os == 'windows-latest' && matrix.language == 'conda'
- run: testing/get-coursier.sh
shell: bash
if: matrix.language == 'coursier'
- run: testing/get-dart.sh
shell: bash
if: matrix.language == 'dart'
- run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
lua5.3 \
liblua5.3-dev \
luarocks
if: matrix.os == 'ubuntu-latest' && matrix.language == 'lua'
- run: |
echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH"
echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH"
echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH"
shell: bash
if: matrix.os == 'windows-latest' && matrix.language == 'perl'
- uses: haskell/actions/setup@v2
if: matrix.language == 'haskell'
- uses: r-lib/actions/setup-r@v2
if: matrix.os == 'ubuntu-latest' && matrix.language == 'r'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e . -r requirements-dev.txt
pip install pipenv
- name: install deps
run: python -mpip install -e . -r requirements-dev.txt
- name: run tests
- name: Run tests
run: coverage run -m pytest tests/languages/${{ matrix.language }}_test.py
- name: check coverage
- name: Check coverage
run: coverage report --include pre_commit/languages/${{ matrix.language }}.py,tests/languages/${{ matrix.language }}_test.py
collector:
needs: [language]
if: always()
runs-on: ubuntu-latest
steps:
- name: check for failures
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: echo job failed && exit 1

View file

@ -11,13 +11,37 @@ concurrency:
cancel-in-progress: true
jobs:
main-windows:
uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
with:
env: '["py39"]'
os: windows-latest
main-linux:
uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
with:
env: '["py39", "py310", "py311", "py312"]'
os: ubuntu-latest
tests:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']
exclude:
# Only run py39 on Windows
- os: windows-latest
python-version: '3.10'
- os: windows-latest
python-version: '3.11'
- os: windows-latest
python-version: '3.12'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox -- tests/languages/python_test.py tests/languages/pipenv_test.py
env:
TOXENV: py${{ matrix.python-version }}

View file

@ -14,6 +14,7 @@ from pre_commit.languages import julia
from pre_commit.languages import lua
from pre_commit.languages import node
from pre_commit.languages import perl
from pre_commit.languages import pipenv
from pre_commit.languages import pygrep
from pre_commit.languages import python
from pre_commit.languages import r
@ -38,6 +39,7 @@ languages: dict[str, Language] = {
'lua': lua,
'node': node,
'perl': perl,
'pipenv': pipenv,
'pygrep': pygrep,
'python': python,
'r': r,

View file

@ -0,0 +1,63 @@
from __future__ import annotations
import contextlib
import os
import sys
from collections.abc import Generator
from collections.abc import Sequence
import pre_commit.constants as C
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
from pre_commit.languages import python
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'pipenv_env'
get_default_version = python.get_default_version
run_hook = python.run_hook
def _assert_pipfile_exists(prefix: Prefix) -> None:
if not os.path.exists(os.path.join(prefix.prefix_dir, 'Pipfile')):
raise AssertionError(
'`language: pipenv` requires a Pipfile in the repository'
)
def health_check(prefix: Prefix, version: str) -> str | None:
_assert_pipfile_exists(prefix)
directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
try:
with in_env(prefix, version):
cmd_output_b('pipenv', 'check')
return None
except Exception as e:
return f'pipenv environment check failed: {e}'
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
env = python.get_env_patch(directory)
with envcontext(env):
yield
def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
_assert_pipfile_exists(prefix)
directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with in_env(prefix, version):
# Initialize virtualenv if it doesn't exist
if not os.path.exists(directory):
python_version = version if version != C.DEFAULT else f"{sys.version_info[0]}.{sys.version_info[1]}"
cmd_output_b('pipenv', '--python', python_version)
# Install dependencies from Pipfile
cmd_output_b('pipenv', 'install', '--dev')
# Install additional dependencies if specified
if additional_dependencies:
cmd_output_b('pipenv', 'install', *additional_dependencies)

View file

@ -4,46 +4,37 @@ from __future__ import annotations
import argparse
import concurrent.futures
import json
import os
import os.path
import subprocess
import sys
EXCLUDED = frozenset((
('windows-latest', 'docker'),
('windows-latest', 'docker_image'),
('windows-latest', 'lua'),
('windows-latest', 'swift'),
))
EXCLUDED = {
('windows-latest', 'r'), # no r on windows
('windows-latest', 'swift'), # no swift on windows
}
ALLOWED_LANGUAGES = {'python', 'pipenv'}
def _always_run() -> frozenset[str]:
ret = ['.github/workflows/languages.yaml', 'testing/languages']
ret.extend(
os.path.join('pre_commit/resources', fname)
for fname in os.listdir('pre_commit/resources')
)
return frozenset(ret)
def _lang_files(lang: str) -> frozenset[str]:
prog = f'''\
import json
import os.path
import sys
import pre_commit.languages.{lang}
import tests.languages.{lang}_test
modules = sorted(
os.path.relpath(v.__file__)
for k, v in sys.modules.items()
if k.startswith(('pre_commit.', 'tests.', 'testing.'))
)
print(json.dumps(modules))
'''
out = json.loads(subprocess.check_output((sys.executable, '-c', prog)))
return frozenset(out)
def _always_run() -> set[str]:
return {
'pre_commit/languages/all.py',
'pre_commit/languages/helpers.py',
'pre_commit/languages/__init__.py',
'pre_commit/languages/python.py', # python is a base for other languages
'pre_commit/languages/pipenv.py', # our new language
}
def _lang_files(lang: str) -> set[str]:
cmd = ('git', 'grep', '-l', '')
env = dict(os.environ, GIT_LITERAL_PATHSPECS='1')
out = subprocess.check_output(
(*cmd, f':(glob,top)pre_commit/languages/{lang}.py'),
env=env,
).decode()
ret = set(out.splitlines())
ret.add(f'tests/languages/{lang}_test.py')
return ret
def main() -> int:
parser = argparse.ArgumentParser()
@ -51,9 +42,8 @@ def main() -> int:
args = parser.parse_args()
langs = [
os.path.splitext(fname)[0]
for fname in sorted(os.listdir('pre_commit/languages'))
if fname.endswith('.py') and fname != '__init__.py'
lang for lang in ALLOWED_LANGUAGES
if os.path.exists(f'pre_commit/languages/{lang}.py')
]
triggers_all = _always_run()
@ -87,6 +77,5 @@ def main() -> int:
f.write(f'languages={json.dumps(matched)}\n')
return 0
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -0,0 +1,74 @@
from __future__ import annotations
import os
import sys
from unittest import mock
import pytest
import pre_commit.constants as C
from pre_commit.languages import pipenv
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output
from testing.language_helpers import run_language
def test_health_check_no_pipfile(tmp_path):
with pytest.raises(AssertionError) as excinfo:
pipenv.health_check(Prefix(str(tmp_path)), C.DEFAULT)
assert '`language: pipenv` requires a Pipfile' in str(excinfo.value)
def _make_pipfile(path):
with open(os.path.join(path, 'Pipfile'), 'w') as f:
f.write('''\
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
requests = "*"
[dev-packages]
pytest = "*"
[requires]
python_version = "3.9"
''')
def test_health_check_with_pipfile(tmp_path):
_make_pipfile(tmp_path)
assert pipenv.health_check(Prefix(str(tmp_path)), C.DEFAULT) is None
def test_install_environment(tmp_path):
_make_pipfile(tmp_path)
with mock.patch.object(pipenv, 'cmd_output_b') as mocked:
pipenv.install_environment(
Prefix(str(tmp_path)),
C.DEFAULT,
['black']
)
python_version = f"{sys.version_info[0]}.{sys.version_info[1]}"
assert mocked.call_args_list == [
mock.call('pipenv', '--python', python_version),
mock.call('pipenv', 'install', '--dev'),
mock.call('pipenv', 'install', 'black'),
]
def test_run_hook(tmp_path):
_make_pipfile(tmp_path)
# Create a simple Python script
script = '''\
#!/usr/bin/env python
print("Hello from pipenv!")
'''
tmp_path.joinpath('script.py').write_text(script)
ret = run_language(
tmp_path,
pipenv,
'python script.py',
)
assert ret == (0, b'Hello from pipenv!\n')