dazpycheck 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dazpycheck/__init__.py +11 -0
- dazpycheck/main.py +222 -0
- dazpycheck/main_test.py +99 -0
- dazpycheck-0.4.0.dist-info/METADATA +65 -0
- dazpycheck-0.4.0.dist-info/RECORD +8 -0
- dazpycheck-0.4.0.dist-info/WHEEL +5 -0
- dazpycheck-0.4.0.dist-info/entry_points.txt +2 -0
- dazpycheck-0.4.0.dist-info/top_level.txt +1 -0
dazpycheck/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# dazpycheck: ignore-banned-words
|
|
2
|
+
"""
|
|
3
|
+
dazpycheck - A tool to check and validate Python code repositories
|
|
4
|
+
|
|
5
|
+
This tool enforces code quality standards, test coverage, and anti-mocking practices.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .main import main, cli
|
|
9
|
+
|
|
10
|
+
__version__ = "0.4.0"
|
|
11
|
+
__all__ = ["main", "cli"]
|
dazpycheck/main.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from multiprocessing import Pool, cpu_count
|
|
5
|
+
import coverage
|
|
6
|
+
import argparse
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
# dazpycheck: ignore-banned-words
|
|
10
|
+
BANNED_WORDS = ["mock", "fallback", "simulate", "pretend", "fake"]
|
|
11
|
+
BANNED_WORDS_SPIEL = """
|
|
12
|
+
Banned word found. This isn't about specific words - it's about practices. Mocking is always bad.
|
|
13
|
+
Fallbacks are bad - if something fails, we want it to fail, not pretend to work. If you need or
|
|
14
|
+
want a mock in a test, it suggests the structure of your actual code needs improvement. Look at
|
|
15
|
+
your code and see if it can be restructured to separate dependencies so tests can run fast
|
|
16
|
+
without mocks. Focus on creating small, testable functions with clear interfaces.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_command(command):
|
|
21
|
+
try:
|
|
22
|
+
subprocess.run(command, capture_output=True, text=True, check=True)
|
|
23
|
+
return True, ""
|
|
24
|
+
except subprocess.CalledProcessError as e:
|
|
25
|
+
return False, f"{e.stdout}\n{e.stderr}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_banned_words_in_file(file_path):
|
|
29
|
+
try:
|
|
30
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
31
|
+
content = f.read()
|
|
32
|
+
if "dazpycheck: ignore-banned-words" in content:
|
|
33
|
+
return True, ""
|
|
34
|
+
lines = content.splitlines()
|
|
35
|
+
for line_num, line in enumerate(lines, 1):
|
|
36
|
+
for word in BANNED_WORDS:
|
|
37
|
+
if word in line:
|
|
38
|
+
return (
|
|
39
|
+
False,
|
|
40
|
+
f"{file_path}:{line_num}: Banned word '{word}' found.\n{BANNED_WORDS_SPIEL}",
|
|
41
|
+
)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass # Ignore files that can't be read
|
|
44
|
+
return True, ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def compile_file(file_path):
|
|
48
|
+
return run_command(["python", "-m", "py_compile", file_path])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run_test_on_file(file_path):
|
|
52
|
+
source_file = file_path.replace("_test.py", ".py")
|
|
53
|
+
if not os.path.exists(source_file):
|
|
54
|
+
return (
|
|
55
|
+
False,
|
|
56
|
+
f"Test file {file_path} exists but corresponding source file {source_file} does not.",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Add the directory of the test file to the python path
|
|
60
|
+
sys.path.insert(0, os.path.dirname(file_path))
|
|
61
|
+
|
|
62
|
+
cov = coverage.Coverage(source=[source_file])
|
|
63
|
+
cov.start()
|
|
64
|
+
|
|
65
|
+
# Try pytest first, then fall back to unittest
|
|
66
|
+
pytest_success, pytest_message = run_command(
|
|
67
|
+
["python", "-m", "pytest", file_path, "-q", "--tb=no"]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if not pytest_success:
|
|
71
|
+
# Try unittest as fallback
|
|
72
|
+
suite = unittest.TestLoader().discover(
|
|
73
|
+
start_dir=os.path.dirname(file_path), pattern=os.path.basename(file_path)
|
|
74
|
+
)
|
|
75
|
+
result = unittest.TextTestRunner(
|
|
76
|
+
failfast=True, stream=open(os.devnull, "w")
|
|
77
|
+
).run(suite)
|
|
78
|
+
if not result.wasSuccessful():
|
|
79
|
+
cov.stop()
|
|
80
|
+
sys.path.pop(0)
|
|
81
|
+
return (
|
|
82
|
+
False,
|
|
83
|
+
f"Tests failed in {file_path} (tried both pytest and unittest)",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
cov.stop()
|
|
87
|
+
cov.save()
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
filename, statements, excluded, missing, formatted = cov.analysis2(source_file)
|
|
91
|
+
total_statements = len(statements)
|
|
92
|
+
executed_statements = total_statements - len(missing)
|
|
93
|
+
coverage_percentage = (
|
|
94
|
+
(executed_statements / total_statements) * 100
|
|
95
|
+
if total_statements > 0
|
|
96
|
+
else 100
|
|
97
|
+
)
|
|
98
|
+
except coverage.misc.NoSource:
|
|
99
|
+
sys.path.pop(0)
|
|
100
|
+
return (
|
|
101
|
+
False,
|
|
102
|
+
f"Coverage data not available for {source_file}. Module may not have been imported.",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if coverage_percentage < 50:
|
|
106
|
+
sys.path.pop(0)
|
|
107
|
+
return (
|
|
108
|
+
False,
|
|
109
|
+
f"Coverage for {source_file} is {coverage_percentage:.2f}%, which is less than 50%.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Remove the directory from the python path
|
|
113
|
+
sys.path.pop(0)
|
|
114
|
+
|
|
115
|
+
return True, ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def main(directory, fix, single_thread, full):
|
|
119
|
+
if fix:
|
|
120
|
+
run_command(["python3", "-m", "black", directory])
|
|
121
|
+
|
|
122
|
+
py_files = []
|
|
123
|
+
test_files = []
|
|
124
|
+
for root, dirs, files in os.walk(directory):
|
|
125
|
+
# Skip build, dist, and cache directories
|
|
126
|
+
dirs[:] = [
|
|
127
|
+
d
|
|
128
|
+
for d in dirs
|
|
129
|
+
if d not in ("__pycache__", "build", "dist", ".git", ".pytest_cache")
|
|
130
|
+
]
|
|
131
|
+
for file in files:
|
|
132
|
+
if file.endswith(".py"):
|
|
133
|
+
py_files.append(os.path.join(root, file))
|
|
134
|
+
if file.endswith("_test.py"):
|
|
135
|
+
test_files.append(os.path.join(root, file))
|
|
136
|
+
|
|
137
|
+
# Initial scan for missing tests and banned words
|
|
138
|
+
has_errors = False
|
|
139
|
+
for py_file in py_files:
|
|
140
|
+
if not py_file.endswith("_test.py"):
|
|
141
|
+
# Skip setup.py, __init__.py, and build directory
|
|
142
|
+
if (
|
|
143
|
+
py_file.endswith("/setup.py")
|
|
144
|
+
or py_file == "setup.py"
|
|
145
|
+
or py_file.endswith("/__init__.py")
|
|
146
|
+
or "/build/" in py_file
|
|
147
|
+
):
|
|
148
|
+
continue
|
|
149
|
+
test_file = py_file.replace(".py", "_test.py")
|
|
150
|
+
if not os.path.exists(test_file):
|
|
151
|
+
has_errors = True
|
|
152
|
+
print(f"Missing test file for {py_file}", file=sys.stderr)
|
|
153
|
+
if not full:
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
success, message = check_banned_words_in_file(py_file)
|
|
157
|
+
if not success:
|
|
158
|
+
has_errors = True
|
|
159
|
+
print(message, file=sys.stderr)
|
|
160
|
+
if not full:
|
|
161
|
+
return 1
|
|
162
|
+
|
|
163
|
+
# Parallelizable jobs
|
|
164
|
+
jobs = []
|
|
165
|
+
jobs.append(
|
|
166
|
+
(run_command, ["python3", "-m", "flake8", "--max-line-length=120", directory])
|
|
167
|
+
)
|
|
168
|
+
for py_file in py_files:
|
|
169
|
+
jobs.append((compile_file, py_file))
|
|
170
|
+
for test_file in test_files:
|
|
171
|
+
jobs.append((run_test_on_file, test_file))
|
|
172
|
+
|
|
173
|
+
if single_thread:
|
|
174
|
+
for job, *args in jobs:
|
|
175
|
+
success, message = job(*args)
|
|
176
|
+
if not success:
|
|
177
|
+
has_errors = True
|
|
178
|
+
print(message, file=sys.stderr)
|
|
179
|
+
if not full:
|
|
180
|
+
return 1
|
|
181
|
+
else:
|
|
182
|
+
with Pool(processes=cpu_count()) as pool:
|
|
183
|
+
results = []
|
|
184
|
+
for job_func, *args in jobs:
|
|
185
|
+
result = pool.apply_async(job_func, args)
|
|
186
|
+
results.append(result)
|
|
187
|
+
for result in results:
|
|
188
|
+
success, message = result.get()
|
|
189
|
+
if not success:
|
|
190
|
+
has_errors = True
|
|
191
|
+
print(message, file=sys.stderr)
|
|
192
|
+
if not full:
|
|
193
|
+
return 1
|
|
194
|
+
|
|
195
|
+
return 1 if has_errors else 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cli():
|
|
199
|
+
parser = argparse.ArgumentParser(
|
|
200
|
+
description="A tool to check and validate a Python code repository."
|
|
201
|
+
)
|
|
202
|
+
parser.add_argument(
|
|
203
|
+
"--full", action="store_true", help="Run all checks regardless of failures."
|
|
204
|
+
)
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"--readonly",
|
|
207
|
+
action="store_true",
|
|
208
|
+
help="Only check for issues, don't modify files.",
|
|
209
|
+
)
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"--single-thread", action="store_true", help="Run checks sequentially."
|
|
212
|
+
)
|
|
213
|
+
parser.add_argument(
|
|
214
|
+
"directory",
|
|
215
|
+
nargs="?",
|
|
216
|
+
default=".",
|
|
217
|
+
help="The directory to check (default: current directory).",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
args = parser.parse_args()
|
|
221
|
+
|
|
222
|
+
sys.exit(main(args.directory, not args.readonly, args.single_thread, args.full))
|
dazpycheck/main_test.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# dazpycheck: ignore-banned-words
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import unittest
|
|
5
|
+
from dazpycheck.main import (
|
|
6
|
+
check_banned_words_in_file,
|
|
7
|
+
compile_file,
|
|
8
|
+
run_test_on_file,
|
|
9
|
+
main,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestDazpycheck(unittest.TestCase):
|
|
14
|
+
|
|
15
|
+
def setUp(self):
|
|
16
|
+
self.output_dir = "output"
|
|
17
|
+
self.test_project_dir = os.path.join(self.output_dir, "test_project")
|
|
18
|
+
os.makedirs(self.test_project_dir, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
def tearDown(self):
|
|
21
|
+
shutil.rmtree(self.output_dir)
|
|
22
|
+
|
|
23
|
+
def test_check_banned_words_in_file(self):
|
|
24
|
+
file_path = os.path.join(self.test_project_dir, "bad_file.py")
|
|
25
|
+
with open(file_path, "w") as f:
|
|
26
|
+
f.write("print('This is a mock file.')\n")
|
|
27
|
+
success, message = check_banned_words_in_file(file_path)
|
|
28
|
+
print(f"Success: {success}, Message: {message}")
|
|
29
|
+
self.assertFalse(success)
|
|
30
|
+
self.assertIn("Banned word 'mock' found", message)
|
|
31
|
+
|
|
32
|
+
def test_check_banned_words_in_file_ignored(self):
|
|
33
|
+
file_path = os.path.join(self.test_project_dir, "bad_file_ignored.py")
|
|
34
|
+
with open(file_path, "w") as f:
|
|
35
|
+
f.write("# dazpycheck: ignore-banned-words\n")
|
|
36
|
+
f.write("print('This is a mock file.')\n")
|
|
37
|
+
success, message = check_banned_words_in_file(file_path)
|
|
38
|
+
self.assertTrue(success)
|
|
39
|
+
|
|
40
|
+
def test_compile_file(self):
|
|
41
|
+
file_path = os.path.join(self.test_project_dir, "good_file.py")
|
|
42
|
+
with open(file_path, "w") as f:
|
|
43
|
+
f.write("print('Hello, world!')\n")
|
|
44
|
+
success, message = compile_file(file_path)
|
|
45
|
+
self.assertTrue(success)
|
|
46
|
+
|
|
47
|
+
def test_compile_file_with_error(self):
|
|
48
|
+
file_path = os.path.join(self.test_project_dir, "bad_file.py")
|
|
49
|
+
with open(file_path, "w") as f:
|
|
50
|
+
f.write("print('Hello, world!\n")
|
|
51
|
+
success, message = compile_file(file_path)
|
|
52
|
+
self.assertFalse(success)
|
|
53
|
+
|
|
54
|
+
def test_run_test_on_file_with_low_coverage(self):
|
|
55
|
+
source_file = os.path.join(self.test_project_dir, "my_module.py")
|
|
56
|
+
test_file = os.path.join(self.test_project_dir, "my_module_test.py")
|
|
57
|
+
with open(source_file, "w") as f:
|
|
58
|
+
f.write(
|
|
59
|
+
"def my_function():\n return 1\n\n\ndef another_function():\n return 2\n"
|
|
60
|
+
)
|
|
61
|
+
with open(test_file, "w") as f:
|
|
62
|
+
f.write(
|
|
63
|
+
"import unittest\nfrom my_module import my_function\n\n\n"
|
|
64
|
+
"class MyTest(unittest.TestCase):\n def test_my_function(self):\n"
|
|
65
|
+
" self.assertEqual(my_function(), 1)\n"
|
|
66
|
+
)
|
|
67
|
+
success, message = run_test_on_file(test_file)
|
|
68
|
+
self.assertFalse(success)
|
|
69
|
+
self.assertIn("less than 50%", message)
|
|
70
|
+
|
|
71
|
+
def test_integration_main(self):
|
|
72
|
+
# This is an integration test that runs the main function
|
|
73
|
+
# with a project that has multiple issues.
|
|
74
|
+
source_file = os.path.join(self.test_project_dir, "my_module.py")
|
|
75
|
+
test_file = os.path.join(self.test_project_dir, "my_module_test.py")
|
|
76
|
+
with open(source_file, "w") as f:
|
|
77
|
+
f.write(
|
|
78
|
+
"def my_function():\n return 1\n\ndef another_function():\n return 2\n"
|
|
79
|
+
)
|
|
80
|
+
with open(test_file, "w") as f:
|
|
81
|
+
f.write(
|
|
82
|
+
"import unittest\nfrom my_module import my_function\n\n"
|
|
83
|
+
"class MyTest(unittest.TestCase):\n def test_my_function(self):\n"
|
|
84
|
+
" self.assertEqual(my_function(), 1)\n"
|
|
85
|
+
)
|
|
86
|
+
with open(os.path.join(self.test_project_dir, "bad_file.py"), "w") as f:
|
|
87
|
+
f.write("print('This is a mock file.')\n")
|
|
88
|
+
|
|
89
|
+
# Fail fast should stop after the first error
|
|
90
|
+
result = main(self.test_project_dir, False, True, False)
|
|
91
|
+
self.assertEqual(result, 1)
|
|
92
|
+
|
|
93
|
+
# Full run should report all errors
|
|
94
|
+
result = main(self.test_project_dir, False, True, True)
|
|
95
|
+
self.assertEqual(result, 1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
unittest.main()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dazpycheck
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A tool to check and validate a Python code repository.
|
|
5
|
+
Author-email: Darren Oakey <darren@oakey.net>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# dazpycheck: Your AI Code's Quality Guardian
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
**dazpycheck** is a powerful and comprehensive tool for ensuring your Python code is clean, correct, and consistent. It's especially useful for checking code generated by AI, which can sometimes take shortcuts or introduce subtle errors.
|
|
17
|
+
|
|
18
|
+
## Why dazpycheck?
|
|
19
|
+
|
|
20
|
+
AI is a powerful tool, but it's not perfect. AI-generated code can sometimes be messy, inefficient, or even contain bugs. `dazpycheck` helps you catch these issues before they become problems. It's the perfect last step in your development workflow to ensure your code is production-ready.
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
* **All-in-One Checking:** dazpycheck integrates `black` for formatting, `flake8` for linting, `pyright` for type checking, and `py_compile` for compilation, giving you a complete picture of your code's health.
|
|
27
|
+
* **Automated Fixing:** By default, `dazpycheck` will automatically fix formatting issues. Use the `--readonly` flag to see what would be changed without modifying any files.
|
|
28
|
+
* **Banned Words Enforcement:** Define a list of "banned words" to enforce your own coding standards. `dazpycheck` will scan your entire repository and flag any occurrences of these words.
|
|
29
|
+
* **Parallel Execution:** `dazpycheck` runs all readonly checks in parallel, making it fast and efficient, even on large codebases.
|
|
30
|
+
* **Integrated Test Runner:** By default, `dazpycheck` will run all your `unittest` and `pytest` tests. Use the `--no-test` flag to skip testing.
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
`dazpycheck` is available on PyPI. You can install it using `pip`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install dazpycheck
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
Once installed, you can run `dazpycheck` from anywhere on your system:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
dazpycheck
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This will run all checks and tests on the current directory. You can also specify a directory:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
dazpycheck /path/to/your/project
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Options
|
|
57
|
+
|
|
58
|
+
* `--readonly`: Only check for issues, don't modify files.
|
|
59
|
+
* `--no-test`: Skip running tests.
|
|
60
|
+
* `--single-thread`: Run checks sequentially.
|
|
61
|
+
* `--full`: Run all checks regardless of failures.
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
We hope you find `dazpycheck` to be a valuable addition to your development workflow!
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
dazpycheck/__init__.py,sha256=9C9LMndxieKt8YrCA5PPGiwCMjGKfcWBrP79FEi7pXE,274
|
|
2
|
+
dazpycheck/main.py,sha256=yie6YsDgWCGOI2yRaXUoPxvIgDRhRlYYZDVDR1XVgrw,7291
|
|
3
|
+
dazpycheck/main_test.py,sha256=IMYLSUH9kKUWP9-NlAWjTiYgEAgIyM8wP-4BPgp9RJE,3945
|
|
4
|
+
dazpycheck-0.4.0.dist-info/METADATA,sha256=ulqo3HUWu-oziPnVc9zWA3heR0z-c9WQe8LBSvrHAys,2684
|
|
5
|
+
dazpycheck-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
dazpycheck-0.4.0.dist-info/entry_points.txt,sha256=oo17m7-EPI_ac2Yv4aJaZAziv9B0x1f78wE4qbWadF4,51
|
|
7
|
+
dazpycheck-0.4.0.dist-info/top_level.txt,sha256=FYXWrnnS8ScPGFGy-SUx4yr_GIgGYUq2FtRHic3dgWA,11
|
|
8
|
+
dazpycheck-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dazpycheck
|