Skip to content

Commit bf018dd

Browse files
committed
.github: Ensure python3 -m pip is used in GitHub Actions
Ensure that "python3 -m pip install " is used consistantly in GitHub Actions. Find all .yml files in the .github/workflows directory and find all lines that contain " pip install " and ensure that they also contain "python3 -m pip install ". If there is any line that has the first but not the second, then print the file name, line number, and the line. Raise an error if any non-compliant files are found.
1 parent d818ca8 commit bf018dd

File tree

1 file changed

+82
-0
lines changed

1 file changed

+82
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""
2+
Ensure that "python3 -m pip install " is used consistantly in GitHub Actions.
3+
4+
Find all .yml files in the .github/workflows directory and find all lines that contain
5+
" pip install " and ensure that they also contain "python3 -m pip install ".
6+
7+
If there is any line that has the first but not the second, then print the file name,
8+
line number, and the line. Raise an error if any non-compliant files are found.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from pathlib import Path
14+
from typing import Iterable
15+
from unittest import mock
16+
17+
import pytest
18+
19+
20+
def process_file(file_path: Path) -> Iterable[tuple[int, str]]:
21+
"""Process a file and yield any lines that contain " pip install " but not
22+
"python3 -m pip install ".
23+
"""
24+
with file_path.open() as in_file:
25+
for line_number, line in enumerate(in_file, start=1):
26+
if " pip install " in line and "python3 -m pip install " not in line:
27+
yield line_number, line.strip()
28+
29+
30+
def test_pip_install_without_python3_minus_m_prefix() -> None:
31+
workflows_dir = Path(".github/workflows")
32+
if not workflows_dir.is_dir():
33+
msg = f"Directory {workflows_dir} does not exist."
34+
raise FileNotFoundError(msg)
35+
36+
errors = []
37+
for yml_file in workflows_dir.glob("*.yml"):
38+
errors.extend(
39+
f"{yml_file}:{line_number}: {line}"
40+
for line_number, line in process_file(yml_file)
41+
)
42+
if errors:
43+
msg = (
44+
"Use 'python3 -m pip install' instead of 'pip install' "
45+
f"({len(errors)} errors). "
46+
)
47+
print(f"{msg}\n" + "\n".join(sorted(errors)))
48+
raise ValueError(msg)
49+
50+
51+
@pytest.mark.parametrize(
52+
"lines,expected",
53+
[
54+
(
55+
[
56+
"This is a test line.\n",
57+
" pip install some_package\n",
58+
"python3 -m pip install another_package\n",
59+
" pip install yet_another_package\n",
60+
],
61+
[(2, "pip install some_package"), (4, "pip install yet_another_package")],
62+
),
63+
(
64+
["No pip install here.\n", "Just a random line.\n"],
65+
[(1, "No pip install here.")],
66+
),
67+
(
68+
[" pip install package1\n", "python3 -m pip install package2\n"],
69+
[(1, "pip install package1")],
70+
),
71+
(["pip install package1\n", "python3 -m pip install package2\n"], []),
72+
([], []),
73+
],
74+
)
75+
def test_process_file(lines, expected) -> None:
76+
"""Mocking the file reading to simulate the content of a file"""
77+
mock_file_path = mock.Mock(spec=Path)
78+
# Use mock_open to properly handle file context manager and iteration
79+
mock_file = mock.mock_open(read_data="".join(lines))
80+
mock_file.return_value.__iter__ = lambda self: iter(lines)
81+
mock_file_path.open = mock_file
82+
assert list(process_file(mock_file_path)) == expected

0 commit comments

Comments
 (0)