Skip to content

Commit 0ca28fa

Browse files
Make pre_push_check able to pass, and fix the defects it was hiding (#138)
Closes #133. Confirms #135 resolved by #137. pre_push_check.py promised "run this before pushing to ensure GitHub Actions won't fail" and could not pass. It shelled out to bare black/flake8/mypy/pytest, so it ran whatever was first on PATH -- Anaconda's mypy 1.19 rather than the project's 2.3, reporting 27 missing-stub errors for stubs pyproject declares. Tools now run under the script's own interpreter. Its flake8 step reported 91 findings. Four were real defects, including a remote program wrapped in an outer f-string, so {torch.__version__}, {i}, {props.name} and {e} interpolated locally and the test raised NameError before sending anything; and json.loads with no module-level import, swallowed by a bare except. The other 74 were one deliberate pattern -- standalone scripts that adjust sys.path or the environment before importing the package they exercise -- now recorded once in .flake8. Also fixes two things that made master red after its tests had passed, in steps that never run on a pull request: the coverage-badge updater treated a deliberately absent badge (#115) as an error, and the follow-on step pushed with nothing to push. And the dependabot advisory against black, which stayed open because it points at docs/requirements.txt, where black arrives transitively and was unconstrained.
2 parents 7e5953c + bf524a4 commit 0ca28fa

15 files changed

Lines changed: 88 additions & 41 deletions

.flake8

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,11 @@ per-file-ignores =
1616
clustrix/local_executor.py:F401,F811,F841
1717
clustrix/utils.py:E501
1818
# Test files are more lenient - allow unused imports, redefinitions, etc.
19-
tests/*.py:F401,F811,F841,F541,E722,E501,E226,E712,F402,W293,E713,E731
20-
tests/*/*.py:F401,F811,F841,F541,E722,E501,E226,E712,F402,W293,E713,E731
19+
# E402 (import not at top): the standalone validation and debug scripts
20+
# under tests/ adjust sys.path, or set an environment variable clustrix
21+
# reads at import time, before importing the package they exercise. That
22+
# ordering is the point of them running standalone, and it accounts for
23+
# every E402 in the tree -- 30 files, all of that shape.
24+
tests/*.py:F401,F811,F841,F541,E722,E501,E226,E712,F402,W293,E713,E731,E402
25+
tests/*/*.py:F401,F811,F841,F541,E722,E501,E226,E712,F402,W293,E713,E731,E402
26+
tests/*/*/*.py:F401,F811,F841,F541,E722,E501,E226,E712,F402,W293,E713,E731,E402

.github/scripts/update_coverage_badge.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,18 @@ def update_readme_badge(readme_path, coverage_percent):
8181
print(f"🔍 Found badge: {match.group()}")
8282
return True # Success - no change needed
8383
else:
84-
print("❌ No coverage badge found to update")
85-
print(f"🔍 Searched for pattern: {badge_pattern}")
86-
return False
84+
# Not an error. The README deliberately carries no coverage
85+
# badge: the figure it used to show was one of several
86+
# conflicting numbers, none of them measured in a way anyone
87+
# could reproduce, and it was removed rather than left to
88+
# assert something untrue (see #115). Failing here made every
89+
# push to master red *after* the tests had passed, and only on
90+
# master, since this step does not run for pull requests.
91+
print("ℹ️ README carries no coverage badge; nothing to update.")
92+
print(f" Measured coverage this run: {coverage_percent}%")
93+
print(" Add a badge matching the documented pattern to have")
94+
print(" it kept up to date automatically.")
95+
return True
8796

8897
except Exception as e:
8998
print(f"❌ Error updating README: {e}")

.github/workflows/tests.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,15 @@ jobs:
6969
git config --local user.email "action@github.com"
7070
git config --local user.name "GitHub Action"
7171
git add README.md
72-
git diff --staged --quiet || git commit -m "Update coverage badge [skip ci]"
73-
git push
72+
# Only push when there is something to push. The README carries no
73+
# coverage badge, so there never is; pushing anyway asked the runner to
74+
# resolve a branch it may not be on, for no change.
75+
if git diff --staged --quiet; then
76+
echo "No coverage badge change to commit."
77+
else
78+
git commit -m "Update coverage badge [skip ci]"
79+
git push
80+
fi
7481
env:
7582
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7683

docs/requirements.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,11 @@ sphinx-wagtail-theme>=6.0.0
33
sphinx-autodoc-typehints>=1.12
44
nbsphinx>=0.8
55
jupyter>=1.0
6-
ipython>=7.0
6+
ipython>=7.0
7+
8+
# Not used to build the docs: black arrives transitively through the Jupyter
9+
# stack, and every release from 24.3.0 up to 26.3.1 carries a high-severity
10+
# advisory (arbitrary file writes via the cache file name). Pinning a floor
11+
# here keeps the docs environment off the affected range; pyproject and
12+
# setup.py carry the same constraint for the package itself.
13+
black>=26.3.1

scripts/check_quality.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def check_tests():
2121
print(f"✅ Tests: {line}")
2222
return True
2323
else:
24-
print(f"❌ Tests failed")
24+
print("❌ Tests failed")
2525
return False
2626

2727

@@ -109,7 +109,7 @@ def main():
109109

110110
# Display badge URLs
111111
if results["coverage"] is not None:
112-
print(f"\n🏷️ Coverage badge URL:")
112+
print("\n🏷️ Coverage badge URL:")
113113
color = (
114114
"brightgreen"
115115
if results["coverage"] >= 80

scripts/fix_notebooks.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"""
55

66
import json
7-
import os
87
from pathlib import Path
98

109

scripts/pre_push_check.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,28 @@
88
import sys
99
from pathlib import Path
1010

11+
#: Tools that must come from the environment running this script, not from
12+
#: whatever is first on PATH. Anaconda's mypy 1.19 sat ahead of the project's
13+
#: 2.3 here and reported 27 "Library stubs not installed" errors for stubs
14+
#: pyproject does declare -- so this script failed while CI, which installs
15+
#: the dev extra, passed.
16+
PYTHON_TOOLS = ("black", "flake8", "mypy", "pytest")
17+
18+
19+
def _use_this_interpreter(cmd):
20+
"""Rewrite `black ...` as `<this python> -m black ...`."""
21+
tool = cmd.split(None, 1)[0]
22+
if tool in PYTHON_TOOLS:
23+
return f"{sys.executable} -m {cmd}"
24+
return cmd
25+
1126

1227
def run_command(cmd, description):
1328
"""Run a command and return success status."""
1429
print(f"Running {description}...")
1530
try:
1631
result = subprocess.run(
17-
cmd,
32+
_use_this_interpreter(cmd),
1833
shell=True,
1934
capture_output=True,
2035
text=True,
@@ -44,7 +59,8 @@ def main():
4459
checks = [
4560
("black clustrix/ tests/", "Black formatting"), # Format, don't just check
4661
(
47-
"flake8 clustrix/ tests/ --max-line-length=88 --extend-ignore=E203,W503,F401,E722,F541,F841,F811,E731,E501,W291,W293,F824",
62+
"flake8 clustrix/ tests/ --max-line-length=88 --extend-ignore="
63+
"E203,W503,F401,E722,F541,F841,F811,E731,E501,W291,W293,F824",
4864
"Flake8 linting",
4965
),
5066
("mypy clustrix/", "MyPy type checking"),

scripts/run_real_world_tests.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import argparse
1111
import subprocess
1212
from pathlib import Path
13-
from typing import List, Dict, Optional
13+
from typing import Dict
1414

1515

1616
class RealWorldTestRunner:
@@ -24,8 +24,6 @@ def __init__(self):
2424
def check_dependencies(self) -> bool:
2525
"""Check if required dependencies are installed."""
2626
try:
27-
import pytest
28-
import clustrix
2927

3028
print("✅ Required dependencies available")
3129
return True

scripts/setup_validation_credentials.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
in a secure way using 1Password CLI.
66
"""
77

8-
import json
98
import sys
109
from pathlib import Path
1110

1211
# Add clustrix to path
1312
sys.path.insert(0, str(Path(__file__).parent.parent))
1413

15-
from clustrix.secure_credentials import (
14+
# Imported after the path is set, which is the point of this script running
15+
# standalone against a checkout.
16+
from clustrix.secure_credentials import ( # noqa: E402
1617
SecureCredentialManager,
1718
ensure_secure_environment,
1819
)
@@ -131,19 +132,19 @@ def guide_credential_setup():
131132
},
132133
]
133134

134-
print(f"\n📝 To set up credentials in 1Password:")
135-
print(f" 1. Open 1Password app")
136-
print(f" 2. Navigate to 'clustrix-dev' vault (or create it)")
137-
print(f" 3. Create new items with these exact names:")
135+
print("\n📝 To set up credentials in 1Password:")
136+
print(" 1. Open 1Password app")
137+
print(" 2. Navigate to 'clustrix-dev' vault (or create it)")
138+
print(" 3. Create new items with these exact names:")
138139
print()
139140

140141
for cred in credentials_to_setup:
141142
print(f"🔑 {cred['name']}")
142143
print(f" Description: {cred['description']}")
143-
print(f" Fields to add:")
144+
print(" Fields to add:")
144145
for field_name, field_desc in cred["fields"].items():
145146
print(f" - {field_name}: {field_desc}")
146-
print(f" Setup notes:")
147+
print(" Setup notes:")
147148
for note in cred["setup_notes"]:
148149
print(f" • {note}")
149150
print()

tests/integration/test_direct_gpu_detection.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,28 @@ def direct_gpu_check():
3131
import subprocess
3232
import os
3333

34-
# Show environment first
35-
cuda_env = os.environ.get("CUDA_VISIBLE_DEVICES", "NOT_SET")
36-
3734
# Simple GPU count check
3835
result = subprocess.run(
3936
[
4037
"python",
4138
"-c",
42-
f"""
39+
# A plain string, not an f-string. This program is meant to
40+
# run on the far side of `python -c`, but every {...} in it was
41+
# being interpolated *here*: {torch.__version__}, {i},
42+
# {props.name} and {e} are all undefined locally, so the test
43+
# raised NameError before it could send anything. The one value
44+
# that genuinely came from this side, CUDA_VISIBLE_DEVICES, is
45+
# read remotely instead.
46+
"""
4347
import os
44-
print(f'CUDA_VISIBLE_DEVICES_ENV: {cuda_env}')
48+
print(f'CUDA_VISIBLE_DEVICES_ENV: {os.environ.get("CUDA_VISIBLE_DEVICES", "NOT_SET")}')
4549
4650
try:
4751
import torch
4852
print(f'TORCH_VERSION: {torch.__version__}')
4953
print(f'CUDA_AVAILABLE: {torch.cuda.is_available()}')
5054
print(f'GPU_COUNT: {torch.cuda.device_count()}')
51-
55+
5256
if torch.cuda.is_available():
5357
for i in range(torch.cuda.device_count()):
5458
props = torch.cuda.get_device_properties(i)
@@ -61,11 +65,11 @@ def direct_gpu_check():
6165
# Try nvidia-smi as backup
6266
try:
6367
import subprocess
64-
result = subprocess.run(['nvidia-smi', '--list-gpus'],
65-
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
66-
universal_newlines=True, timeout=10)
68+
result = subprocess.run(['nvidia-smi', '--list-gpus'],
69+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
70+
universal_newlines=True, timeout=10)
6771
if result.returncode == 0:
68-
gpu_lines = [line for line in result.stdout.strip().split('\\n') if line.strip()]
72+
gpu_lines = [ln for ln in result.stdout.strip().split('\\n') if ln.strip()]
6973
print(f'NVIDIA_SMI_GPU_COUNT: {len(gpu_lines)}')
7074
for i, line in enumerate(gpu_lines):
7175
print(f'NVIDIA_GPU_{i}: {line}')

0 commit comments

Comments
 (0)