python-code-validator 0.1.1__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.
- code_validator/__init__.py +25 -0
- code_validator/__main__.py +11 -0
- code_validator/cli.py +88 -0
- code_validator/components/__init__.py +0 -0
- code_validator/components/ast_utils.py +40 -0
- code_validator/components/definitions.py +88 -0
- code_validator/components/factories.py +243 -0
- code_validator/components/scope_handler.py +59 -0
- code_validator/config.py +99 -0
- code_validator/core.py +100 -0
- code_validator/exceptions.py +48 -0
- code_validator/output.py +90 -0
- code_validator/rules_library/__init__.py +0 -0
- code_validator/rules_library/basic_rules.py +167 -0
- code_validator/rules_library/constraint_logic.py +257 -0
- code_validator/rules_library/selector_nodes.py +319 -0
- python_code_validator-0.1.1.dist-info/METADATA +308 -0
- python_code_validator-0.1.1.dist-info/RECORD +22 -0
- python_code_validator-0.1.1.dist-info/WHEEL +5 -0
- python_code_validator-0.1.1.dist-info/entry_points.txt +2 -0
- python_code_validator-0.1.1.dist-info/licenses/LICENSE +21 -0
- python_code_validator-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,319 @@
|
|
1
|
+
"""Contains concrete implementations of all Selector components.
|
2
|
+
|
3
|
+
Each class in this module implements the `Selector` protocol and is responsible
|
4
|
+
for finding and returning specific types of nodes from an Abstract Syntax Tree.
|
5
|
+
They use `ast.walk` to traverse the tree and can be constrained to specific
|
6
|
+
scopes using the ScopedSelector base class.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import ast
|
10
|
+
from typing import Any
|
11
|
+
|
12
|
+
from ..components.ast_utils import get_full_name
|
13
|
+
from ..components.definitions import Selector
|
14
|
+
from ..components.scope_handler import find_scope_node
|
15
|
+
|
16
|
+
|
17
|
+
class ScopedSelector(Selector):
|
18
|
+
"""An abstract base class for selectors that support scoping.
|
19
|
+
|
20
|
+
This class provides a common mechanism for subclasses to narrow their search
|
21
|
+
to a specific part of the AST (e.g., a single function or class) before
|
22
|
+
performing their selection logic.
|
23
|
+
|
24
|
+
Attributes:
|
25
|
+
in_scope_config (dict | str | None): The configuration dictionary or
|
26
|
+
string that defines the desired scope.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(self, **kwargs: Any):
|
30
|
+
"""Initializes the ScopedSelector.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
**kwargs: Keyword arguments containing the scope configuration.
|
34
|
+
Expects `in_scope` key.
|
35
|
+
"""
|
36
|
+
self.in_scope_config = kwargs.get("in_scope")
|
37
|
+
|
38
|
+
def _get_search_tree(self, tree: ast.Module) -> ast.AST | None:
|
39
|
+
"""Determines the root node for the search based on the scope config.
|
40
|
+
|
41
|
+
If no scope is defined or if it's 'global', the whole tree is used.
|
42
|
+
Otherwise, it uses `find_scope_node` to locate the specific subtree.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
tree: The root of the full AST.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
The AST node to start the search from, or None if the scope
|
49
|
+
could not be found.
|
50
|
+
"""
|
51
|
+
if not self.in_scope_config or self.in_scope_config == "global":
|
52
|
+
return tree
|
53
|
+
|
54
|
+
scope_node = find_scope_node(tree, self.in_scope_config)
|
55
|
+
return scope_node
|
56
|
+
|
57
|
+
def select(self, tree: ast.Module) -> list[ast.AST]:
|
58
|
+
"""Abstract select method to be implemented by subclasses."""
|
59
|
+
raise NotImplementedError
|
60
|
+
|
61
|
+
|
62
|
+
class FunctionDefSelector(ScopedSelector):
|
63
|
+
"""Selects function definition (`def`) nodes from an AST.
|
64
|
+
|
65
|
+
JSON Params:
|
66
|
+
name (str): The name of the function to find. Use "*" to find all.
|
67
|
+
"""
|
68
|
+
|
69
|
+
def __init__(self, **kwargs: Any):
|
70
|
+
super().__init__(**kwargs)
|
71
|
+
self.name_to_find = kwargs.get("name")
|
72
|
+
|
73
|
+
def select(self, tree: ast.Module) -> list[ast.AST]:
|
74
|
+
"""Finds all `ast.FunctionDef` nodes that match the name criteria."""
|
75
|
+
search_tree = self._get_search_tree(tree)
|
76
|
+
if not search_tree:
|
77
|
+
return []
|
78
|
+
|
79
|
+
found_nodes: list[ast.AST] = []
|
80
|
+
for node in ast.walk(search_tree):
|
81
|
+
if isinstance(node, ast.FunctionDef):
|
82
|
+
if self.name_to_find == "*" or node.name == self.name_to_find:
|
83
|
+
found_nodes.append(node)
|
84
|
+
return found_nodes
|
85
|
+
|
86
|
+
|
87
|
+
class ClassDefSelector(ScopedSelector):
|
88
|
+
"""Selects class definition (`class`) nodes from an AST.
|
89
|
+
|
90
|
+
JSON Params:
|
91
|
+
name (str): The name of the class to find. Use "*" to find all.
|
92
|
+
"""
|
93
|
+
|
94
|
+
def __init__(self, **kwargs: Any):
|
95
|
+
"""Initializes the selector."""
|
96
|
+
super().__init__(**kwargs)
|
97
|
+
self.name_to_find = kwargs.get("name")
|
98
|
+
|
99
|
+
def select(self, tree: ast.Module) -> list[ast.AST]:
|
100
|
+
"""Finds all `ast.ClassDef` nodes that match the name criteria."""
|
101
|
+
search_tree = self._get_search_tree(tree)
|
102
|
+
if not search_tree:
|
103
|
+
return []
|
104
|
+
|
105
|
+
found_nodes: list[ast.AST] = []
|
106
|
+
for node in ast.walk(search_tree):
|
107
|
+
if isinstance(node, ast.ClassDef):
|
108
|
+
if self.name_to_find == "*" or node.name == self.name_to_find:
|
109
|
+
found_nodes.append(node)
|
110
|
+
return found_nodes
|
111
|
+
|
112
|
+
|
113
|
+
class ImportStatementSelector(ScopedSelector):
|
114
|
+
"""Selects import nodes (`import` or `from...import`) from an AST.
|
115
|
+
|
116
|
+
JSON Params:
|
117
|
+
name (str): The name of the module to find (e.g., "os", "requests").
|
118
|
+
"""
|
119
|
+
|
120
|
+
def __init__(self, **kwargs: Any):
|
121
|
+
super().__init__(**kwargs)
|
122
|
+
self.module_name_to_find = kwargs.get("name")
|
123
|
+
|
124
|
+
def select(self, tree: ast.Module) -> list[ast.AST]:
|
125
|
+
"""Finds all import-related nodes that match the name criteria."""
|
126
|
+
if not self.module_name_to_find:
|
127
|
+
return []
|
128
|
+
|
129
|
+
search_tree = self._get_search_tree(tree)
|
130
|
+
if not search_tree:
|
131
|
+
return []
|
132
|
+
|
133
|
+
found_nodes: list[ast.AST] = []
|
134
|
+
for node in ast.walk(search_tree):
|
135
|
+
if isinstance(node, ast.Import):
|
136
|
+
for alias in node.names:
|
137
|
+
# Проверяем 'os' в 'import os.path'
|
138
|
+
module_parts = alias.name.split(".")
|
139
|
+
if alias.name.startswith(self.module_name_to_find) or self.module_name_to_find in module_parts:
|
140
|
+
found_nodes.append(node)
|
141
|
+
break
|
142
|
+
elif isinstance(node, ast.ImportFrom):
|
143
|
+
if node.module and node.module.startswith(self.module_name_to_find):
|
144
|
+
found_nodes.append(node)
|
145
|
+
|
146
|
+
return found_nodes
|
147
|
+
|
148
|
+
|
149
|
+
class FunctionCallSelector(ScopedSelector):
|
150
|
+
"""Selects function call nodes from an AST.
|
151
|
+
|
152
|
+
This can find simple function calls (`my_func()`) and method calls
|
153
|
+
(`requests.get()`).
|
154
|
+
|
155
|
+
JSON Params:
|
156
|
+
name (str): The full name of the function being called.
|
157
|
+
"""
|
158
|
+
|
159
|
+
def __init__(self, **kwargs: Any):
|
160
|
+
"""Initializes the selector."""
|
161
|
+
super().__init__(**kwargs)
|
162
|
+
self.name_to_find = kwargs.get("name")
|
163
|
+
|
164
|
+
def select(self, tree: ast.Module) -> list[ast.AST]:
|
165
|
+
"""Finds all `ast.Call` nodes that match the name criteria."""
|
166
|
+
search_tree = self._get_search_tree(tree)
|
167
|
+
if not search_tree:
|
168
|
+
return []
|
169
|
+
|
170
|
+
found_nodes: list[ast.AST] = []
|
171
|
+
for node in ast.walk(search_tree):
|
172
|
+
if isinstance(node, ast.Call):
|
173
|
+
# Используем наш helper, чтобы получить полное имя вызываемого объекта
|
174
|
+
full_name = get_full_name(node.func)
|
175
|
+
if full_name and full_name == self.name_to_find:
|
176
|
+
found_nodes.append(node)
|
177
|
+
return found_nodes
|
178
|
+
|
179
|
+
|
180
|
+
class AssignmentSelector(ScopedSelector):
|
181
|
+
"""Selects assignment nodes (`=` or `:=` or type-annotated).
|
182
|
+
|
183
|
+
This can find assignments to simple variables (`x = 5`) and attributes
|
184
|
+
(`self.player = ...`).
|
185
|
+
|
186
|
+
JSON Params:
|
187
|
+
name (str): The full name of the variable or attribute being assigned to.
|
188
|
+
"""
|
189
|
+
|
190
|
+
def __init__(self, **kwargs: Any):
|
191
|
+
super().__init__(**kwargs)
|
192
|
+
self.target_name_to_find = kwargs.get("name")
|
193
|
+
|
194
|
+
def select(self, tree: ast.Module) -> list[ast.AST]:
|
195
|
+
"""Finds all `ast.Assign` or `ast.AnnAssign` nodes matching the target name."""
|
196
|
+
search_tree = self._get_search_tree(tree)
|
197
|
+
if not search_tree:
|
198
|
+
return []
|
199
|
+
|
200
|
+
found_nodes: list[ast.AST] = []
|
201
|
+
for node in ast.walk(search_tree):
|
202
|
+
# Мы поддерживаем и простое присваивание (x=5), и с аннотацией (x: int = 5)
|
203
|
+
if isinstance(node, (ast.Assign, ast.AnnAssign)):
|
204
|
+
# Целей присваивания может быть несколько (a = b = 5)
|
205
|
+
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
|
206
|
+
for target in targets:
|
207
|
+
full_name = get_full_name(target)
|
208
|
+
if full_name and (self.target_name_to_find == "*" or full_name == self.target_name_to_find):
|
209
|
+
found_nodes.append(node)
|
210
|
+
return found_nodes
|
211
|
+
|
212
|
+
|
213
|
+
class UsageSelector(ScopedSelector):
|
214
|
+
"""Selects nodes where a variable or attribute is used (read).
|
215
|
+
|
216
|
+
This finds nodes in a "load" context, meaning the value of the variable
|
217
|
+
is being accessed, not assigned.
|
218
|
+
|
219
|
+
JSON Params:
|
220
|
+
name (str): The name of the variable or attribute being used.
|
221
|
+
"""
|
222
|
+
|
223
|
+
def __init__(self, **kwargs: Any):
|
224
|
+
super().__init__(**kwargs)
|
225
|
+
self.variable_name_to_find = kwargs.get("name")
|
226
|
+
|
227
|
+
def select(self, tree: ast.Module) -> list[ast.AST]:
|
228
|
+
"""Finds all `ast.Name` nodes (in load context) matching the name."""
|
229
|
+
search_tree = self._get_search_tree(tree)
|
230
|
+
if not search_tree:
|
231
|
+
return []
|
232
|
+
|
233
|
+
found_nodes: list[ast.AST] = []
|
234
|
+
for node in ast.walk(search_tree):
|
235
|
+
# Проверяем и простые имена, и атрибуты, когда их "читают"
|
236
|
+
if isinstance(node, (ast.Name, ast.Attribute)) and isinstance(getattr(node, "ctx", None), ast.Load):
|
237
|
+
full_name = get_full_name(node)
|
238
|
+
if full_name and full_name == self.variable_name_to_find:
|
239
|
+
found_nodes.append(node)
|
240
|
+
return found_nodes
|
241
|
+
|
242
|
+
|
243
|
+
class LiteralSelector(ScopedSelector):
|
244
|
+
"""Selects literal nodes (e.g., numbers, strings), ignoring docstrings.
|
245
|
+
|
246
|
+
JSON Params:
|
247
|
+
name (str): The type of literal to find. Supported: "number", "string".
|
248
|
+
"""
|
249
|
+
|
250
|
+
def __init__(self, **kwargs: Any):
|
251
|
+
super().__init__(**kwargs)
|
252
|
+
self.literal_type = kwargs.get("name") # 'name' - это наш унифицированный ключ
|
253
|
+
|
254
|
+
def select(self, tree: ast.Module) -> list[ast.AST]:
|
255
|
+
search_tree = self._get_search_tree(tree)
|
256
|
+
if not search_tree:
|
257
|
+
return []
|
258
|
+
|
259
|
+
type_map = {"number": (int, float), "string": (str,)}
|
260
|
+
expected_py_types = type_map.get(self.literal_type)
|
261
|
+
if not expected_py_types:
|
262
|
+
return []
|
263
|
+
|
264
|
+
found_nodes: list[ast.AST] = []
|
265
|
+
for node in ast.walk(search_tree):
|
266
|
+
# Мы ищем только узлы Constant
|
267
|
+
if not isinstance(node, ast.Constant):
|
268
|
+
continue
|
269
|
+
|
270
|
+
# Проверяем тип значения внутри константы
|
271
|
+
if not isinstance(node.value, expected_py_types):
|
272
|
+
continue
|
273
|
+
|
274
|
+
# Пропускаем докстринги
|
275
|
+
if hasattr(node, "parent") and isinstance(node.parent, ast.Expr):
|
276
|
+
continue
|
277
|
+
|
278
|
+
# Пропускаем f-строки
|
279
|
+
if hasattr(node, "parent") and isinstance(node.parent, ast.JoinedStr):
|
280
|
+
continue
|
281
|
+
|
282
|
+
found_nodes.append(node)
|
283
|
+
|
284
|
+
return found_nodes
|
285
|
+
|
286
|
+
|
287
|
+
class AstNodeSelector(ScopedSelector):
|
288
|
+
"""A generic selector for finding any AST node by its class name.
|
289
|
+
|
290
|
+
This is a powerful, low-level selector for advanced use cases.
|
291
|
+
|
292
|
+
JSON Params:
|
293
|
+
node_type (str | list[str]): The name(s) of the AST node types to find,
|
294
|
+
as defined in the `ast` module (e.g., "For", "While", "Try").
|
295
|
+
"""
|
296
|
+
|
297
|
+
def __init__(self, **kwargs: Any):
|
298
|
+
super().__init__(**kwargs)
|
299
|
+
node_type_arg = kwargs.get("node_type")
|
300
|
+
|
301
|
+
# Поддерживаем и одну строку, и список строк
|
302
|
+
if isinstance(node_type_arg, list):
|
303
|
+
self.node_types_to_find = tuple(getattr(ast, nt) for nt in node_type_arg if hasattr(ast, nt))
|
304
|
+
elif isinstance(node_type_arg, str) and hasattr(ast, node_type_arg):
|
305
|
+
self.node_types_to_find = (getattr(ast, node_type_arg),)
|
306
|
+
else:
|
307
|
+
self.node_types_to_find = ()
|
308
|
+
|
309
|
+
def select(self, tree: ast.Module) -> list[ast.AST]:
|
310
|
+
"""Finds all AST nodes that are instances of the specified types."""
|
311
|
+
search_tree = self._get_search_tree(tree)
|
312
|
+
if not search_tree or not self.node_types_to_find:
|
313
|
+
return []
|
314
|
+
|
315
|
+
found_nodes: list[ast.AST] = []
|
316
|
+
for node in ast.walk(search_tree):
|
317
|
+
if isinstance(node, self.node_types_to_find):
|
318
|
+
found_nodes.append(node)
|
319
|
+
return found_nodes
|
@@ -0,0 +1,308 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: python-code-validator
|
3
|
+
Version: 0.1.1
|
4
|
+
Summary: A flexible framework for static validation of Python code based on JSON rules.
|
5
|
+
Author-email: Qu1nel <covach.qn@gmail.com>
|
6
|
+
License: MIT License
|
7
|
+
|
8
|
+
Copyright (c) 2025 Ivan Kovach
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.
|
27
|
+
|
28
|
+
Project-URL: Homepage, https://github.com/Qu1nel/PythonCodeValidator
|
29
|
+
Project-URL: Bug Tracker, https://github.com/Qu1nel/PythonCodeValidator/issues
|
30
|
+
Keywords: validation,linter,static analysis,testing,education
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
33
|
+
Classifier: Programming Language :: Python :: 3.11
|
34
|
+
Classifier: Programming Language :: Python :: 3.12
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
36
|
+
Classifier: Operating System :: OS Independent
|
37
|
+
Requires-Python: >=3.11
|
38
|
+
Description-Content-Type: text/markdown
|
39
|
+
License-File: LICENSE
|
40
|
+
Provides-Extra: dev
|
41
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
42
|
+
Requires-Dist: flake8>=7.0.0; extra == "dev"
|
43
|
+
Requires-Dist: build; extra == "dev"
|
44
|
+
Requires-Dist: twine; extra == "dev"
|
45
|
+
Requires-Dist: coverage>=7.5.0; extra == "dev"
|
46
|
+
Provides-Extra: docs
|
47
|
+
Requires-Dist: sphinx>=7.0.0; extra == "docs"
|
48
|
+
Requires-Dist: furo; extra == "docs"
|
49
|
+
Requires-Dist: myst-parser; extra == "docs"
|
50
|
+
Requires-Dist: sphinx-design; extra == "docs"
|
51
|
+
Dynamic: license-file
|
52
|
+
|
53
|
+
<div align="center">
|
54
|
+
<br/>
|
55
|
+
<!-- <img src=".github/assets/logo.png" alt="logo" width="200" height="auto" /> -->
|
56
|
+
<h1>Python Code Validator</h1>
|
57
|
+
<p>
|
58
|
+
<b>A flexible, AST-based framework for static validation of Python code using declarative JSON rules.</b>
|
59
|
+
</p>
|
60
|
+
<br/>
|
61
|
+
|
62
|
+
<!-- Badges -->
|
63
|
+
<p>
|
64
|
+
<a href="https://github.com/Qu1nel/PythonCodeValidator/stargazers"><img src="https://img.shields.io/github/stars/Qu1nel/PythonCodeValidator" alt="GitHub Stars"></a>
|
65
|
+
<a href="https://github.com/Qu1nel/PythonCodeValidator/network/members"><img src="https://img.shields.io/github/forks/Qu1nel/PythonCodeValidator" alt="GitHub Forks"></a>
|
66
|
+
<a href="https://github.com/Qu1nel/PythonCodeValidator/graphs/contributors"><img src="https://img.shields.io/github/contributors/Qu1nel/PythonCodeValidator" alt="Contributors"></a>
|
67
|
+
<a href="https://github.com/Qu1nel/PythonCodeValidator/issues/"><img src="https://img.shields.io/github/issues/Qu1nel/PythonCodeValidator" alt="Open Issues"></a>
|
68
|
+
<a href="https://github.com/Qu1nel/PythonCodeValidator/commits/main"><img src="https://img.shields.io/github/last-commit/Qu1nel/PythonCodeValidator" alt="Last Commit"></a>
|
69
|
+
</p>
|
70
|
+
<p>
|
71
|
+
<a href="https://github.com/Qu1nel/PythonCodeValidator/actions/workflows/ci.yml"><img src="https://github.com/Qu1nel/PythonCodeValidator/actions/workflows/ci.yml/badge.svg" alt="CI Status"></a>
|
72
|
+
<a href="https://app.codecov.io/gh/Qu1nel/PythonCodeValidator"><img src="https://codecov.io/gh/Qu1nel/PythonCodeValidator/graph/badge.svg" alt="Coverage"></a>
|
73
|
+
<a href="https://pypi.org/project/python-code-validator/"><img src="https://img.shields.io/pypi/v/python-code-validator.svg" alt="PyPI Version"></a>
|
74
|
+
<a href="https://pypi.org/project/python-code-validator/"><img src="https://img.shields.io/pypi/pyversions/python-code-validator.svg" alt="Python Versions"></a>
|
75
|
+
<a href="https://github.com/Qu1nel/PythonCodeValidator/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Qu1nel/PythonCodeValidator" alt="License"></a>
|
76
|
+
</p>
|
77
|
+
|
78
|
+
<h4>
|
79
|
+
<a href="#-quick-usage-example">Usage Examples</a>
|
80
|
+
<span>·</span>
|
81
|
+
<a href="https://[your-project].readthedocs.io">Full Documentation</a>
|
82
|
+
<span>·</span>
|
83
|
+
<a href="#">AI documentation</a>
|
84
|
+
<span>·</span>
|
85
|
+
<a href="https://github.com/Qu1nel/PythonCodeValidator/blob/main/docs/how_it_works/index.md">Developer's Guide</a>
|
86
|
+
<span>·</span>
|
87
|
+
<a href="https://github.com/Qu1nel/PythonCodeValidator/issues/new?template=1-bug-report.md">Report a Bug</a>
|
88
|
+
<span>·</span>
|
89
|
+
<a href="https://github.com/Qu1nel/PythonCodeValidator/issues/new?template=4-feature-request.md">Request Feature</a>
|
90
|
+
</h4>
|
91
|
+
</div>
|
92
|
+
|
93
|
+
<br/>
|
94
|
+
|
95
|
+
---
|
96
|
+
|
97
|
+
## Table of Contents
|
98
|
+
|
99
|
+
- [About The Project](#-about-the-project)
|
100
|
+
- [The Power of Combinatorics](#-the-power-of-combinatorics)
|
101
|
+
- [Key Features](#-key-features)
|
102
|
+
- [Getting Started](#-getting-started)
|
103
|
+
- [Installation](#installation)
|
104
|
+
- [Usage Examples](#-quick-usage-example)
|
105
|
+
- [Example 1: Simple Check](#example-1-simple-check)
|
106
|
+
- [Example 2: Advanced Check](#example-2-advanced-check)
|
107
|
+
- [Documentation](#-documentation)
|
108
|
+
- [Contributing](#-contributing)
|
109
|
+
- [License](#-license)
|
110
|
+
- [Contact](#Contact)
|
111
|
+
|
112
|
+
## 📖 About The Project
|
113
|
+
|
114
|
+
**Python Code Validator** is an engine designed for educational platforms and automated testing systems. It solves a key
|
115
|
+
problem: **how to verify that a student's code meets specific structural and stylistic requirements *before* running
|
116
|
+
resource-intensive dynamic tests.**
|
117
|
+
|
118
|
+
Instead of writing complex Python scripts for each new validation rule, you can define them declaratively in a simple,
|
119
|
+
powerful **JSON format**. This allows teachers and curriculum developers to easily create and adapt validation scenarios
|
120
|
+
without deep programming knowledge. The framework analyzes the code's Abstract Syntax Tree (AST), providing a deep and
|
121
|
+
reliable way to enforce best practices.
|
122
|
+
|
123
|
+
## 📈 The Power of Combinatorics
|
124
|
+
|
125
|
+
The framework's power lies in its combinatorial architecture. It is built on a small set of primitive "bricks":
|
126
|
+
**Selectors** ($S$) that define *what* to find in the code, and **Constraints** ($C$) that define *what condition* to
|
127
|
+
check.
|
128
|
+
|
129
|
+
The number of unique validation rules ($R$) is not a sum, but a product of these components. A single rule can be
|
130
|
+
represented as:
|
131
|
+
|
132
|
+
$$R_{\text{single}} = S \times C$$
|
133
|
+
|
134
|
+
With approximately $10$ types of selectors and $10$ types of constraints, this already provides ~$100$ unique checks.
|
135
|
+
However,
|
136
|
+
the true flexibility comes from logical composition, allowing for a near-infinite number of validation scenarios:
|
137
|
+
|
138
|
+
$$R_{\text{total}} \approx S \times \sum_{k=1}^{|C|} \binom{|C|}{k} = S \times (2^{|C|} - 1)$$
|
139
|
+
|
140
|
+
This design provides **thousands of potential validation scenarios** out-of-the-box, offering extreme flexibility with
|
141
|
+
minimal complexity.
|
142
|
+
|
143
|
+
## ✨ Key Features
|
144
|
+
|
145
|
+
- **Declarative JSON Rules**: Define validation logic in a human-readable format.
|
146
|
+
- **Powerful Static Analysis**:
|
147
|
+
- ✅ Check syntax and PEP8 compliance (`flake8`).
|
148
|
+
- ✅ Enforce or forbid specific `import` statements.
|
149
|
+
- ✅ Verify class structure, inheritance, and function signatures.
|
150
|
+
- ✅ Forbid "magic numbers" or specific function calls like `eval`.
|
151
|
+
- **Precise Scoping**: Apply rules globally, or narrowly to a specific function, class, or method.
|
152
|
+
- **Extensible Architecture**: Easily add new, custom checks by creating new Selector or Constraint components.
|
153
|
+
|
154
|
+
## 🚀 Getting Started
|
155
|
+
|
156
|
+
### Installation
|
157
|
+
|
158
|
+
**1. For Users (from PyPI):**
|
159
|
+
|
160
|
+
Install the package with one command. This will make the `validate-code` command-line tool available.
|
161
|
+
|
162
|
+
```bash
|
163
|
+
pip install python-code-validator
|
164
|
+
```
|
165
|
+
|
166
|
+
**2. For Users (from source):**
|
167
|
+
|
168
|
+
If you want to install directly from the repository:
|
169
|
+
|
170
|
+
```bash
|
171
|
+
git clone https://github.com/Qu1nel/PythonCodeValidator.git
|
172
|
+
cd PythonCodeValidator
|
173
|
+
pip install .
|
174
|
+
```
|
175
|
+
|
176
|
+
**3. For Developers:**
|
177
|
+
|
178
|
+
To set up a full development environment, see the [Contributing Guidelines](./CONTRIBUTING.md).
|
179
|
+
|
180
|
+
## ⚡ Quick Usage Example
|
181
|
+
|
182
|
+
The validator is a command-line tool named `validate-code`.
|
183
|
+
|
184
|
+
### Example 1: Simple Check
|
185
|
+
|
186
|
+
Let's check if a required function exists.
|
187
|
+
|
188
|
+
**`solution_simple.py`:**
|
189
|
+
|
190
|
+
```python
|
191
|
+
# This file is missing the 'solve' function
|
192
|
+
def main():
|
193
|
+
print("Hello")
|
194
|
+
```
|
195
|
+
|
196
|
+
**`rules_simple.json`:**
|
197
|
+
|
198
|
+
```json
|
199
|
+
{
|
200
|
+
"validation_rules": [
|
201
|
+
{
|
202
|
+
"rule_id": 1,
|
203
|
+
"message": "Required function 'solve' is missing.",
|
204
|
+
"check": {
|
205
|
+
"selector": {
|
206
|
+
"type": "function_def",
|
207
|
+
"name": "solve"
|
208
|
+
},
|
209
|
+
"constraint": {
|
210
|
+
"type": "is_required"
|
211
|
+
}
|
212
|
+
}
|
213
|
+
}
|
214
|
+
]
|
215
|
+
}
|
216
|
+
```
|
217
|
+
|
218
|
+
**Running the validator:**
|
219
|
+
|
220
|
+
```bash
|
221
|
+
$ validate-code solution_simple.py rules_simple.json
|
222
|
+
Starting validation for: solution_simple.py
|
223
|
+
Required function 'solve' is missing.
|
224
|
+
Validation failed.
|
225
|
+
```
|
226
|
+
|
227
|
+
### Example 2: Advanced Check
|
228
|
+
|
229
|
+
Let's enforce a complex rule: "In our game class, the `update` method must not contain any `print` statements."
|
230
|
+
|
231
|
+
**`game.py`:**
|
232
|
+
|
233
|
+
```python
|
234
|
+
import arcade
|
235
|
+
|
236
|
+
|
237
|
+
class MyGame(arcade.Window):
|
238
|
+
def update(self, delta_time):
|
239
|
+
print("Debugging player position...") # Forbidden call
|
240
|
+
self.player.x += 1
|
241
|
+
```
|
242
|
+
|
243
|
+
**`rules_advanced.json`:**
|
244
|
+
|
245
|
+
```json
|
246
|
+
{
|
247
|
+
"validation_rules": [
|
248
|
+
{
|
249
|
+
"rule_id": 101,
|
250
|
+
"message": "Do not use 'print' inside the 'update' method.",
|
251
|
+
"check": {
|
252
|
+
"selector": {
|
253
|
+
"type": "function_call",
|
254
|
+
"name": "print",
|
255
|
+
"in_scope": {
|
256
|
+
"class": "MyGame",
|
257
|
+
"method": "update"
|
258
|
+
}
|
259
|
+
},
|
260
|
+
"constraint": {
|
261
|
+
"type": "is_forbidden"
|
262
|
+
}
|
263
|
+
}
|
264
|
+
}
|
265
|
+
]
|
266
|
+
}
|
267
|
+
```
|
268
|
+
|
269
|
+
**Running the validator:**
|
270
|
+
|
271
|
+
```bash
|
272
|
+
$ validate-code game.py rules_advanced.json
|
273
|
+
Starting validation for: game.py
|
274
|
+
Do not use 'print' inside the 'update' method.
|
275
|
+
Validation failed.
|
276
|
+
```
|
277
|
+
|
278
|
+
## 📚 Documentation
|
279
|
+
|
280
|
+
- **Full User Guide & JSON Specification**: Our complete documentation is hosted on
|
281
|
+
**[Read the Docs](https://[your-project].readthedocs.io)**.
|
282
|
+
- **Developer's Guide**: For a deep dive into the architecture, see the
|
283
|
+
**[How It Works guide](./docs/how_it_works/index.md)**.
|
284
|
+
- **Interactive AI-Powered Docs**: *(Coming Soon)* An interactive documentation experience.
|
285
|
+
|
286
|
+
## 🤝 Contributing
|
287
|
+
|
288
|
+
Contributions make the open-source community an amazing place to learn, inspire, and create. Any contributions you make
|
289
|
+
are **greatly appreciated**.
|
290
|
+
|
291
|
+
Please read our **[Contributing Guidelines](./CONTRIBUTING.md)** to get started. This project adheres to the
|
292
|
+
**[Code of Conduct](./CODE_OF_CONDUCT.md)**.
|
293
|
+
|
294
|
+
## 📜 License
|
295
|
+
|
296
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
297
|
+
|
298
|
+
---
|
299
|
+
|
300
|
+
### Contact
|
301
|
+
|
302
|
+
Developed by **[Ivan Kovach (@Qu1nel)](https://github.com/Qu1nel)**.
|
303
|
+
|
304
|
+
Email: **[covach.qn@gmail.com](mailto:covach.qn@gmail.com)** Telegram: **[@qnllnq](https://t.me/qnllnq)**
|
305
|
+
|
306
|
+
<br/>
|
307
|
+
|
308
|
+
<p align="right"><a href="./LICENSE">MIT</a> © <a href="https://github.com/Qu1nel/">Ivan Kovach</a></p>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
code_validator/__init__.py,sha256=bOv6iOVODFBTt2Y0lC6hYCI2XAfex3HKaNoyT-HwUG4,963
|
2
|
+
code_validator/__main__.py,sha256=c7P8Lz3EuwYRHarTEp_DExMUauod9X42p6aTZkpvi10,330
|
3
|
+
code_validator/cli.py,sha256=fLbSP_4ABZGZTH6-L4oAXKVRTW-OVMdpcZgT9F4ibtY,3466
|
4
|
+
code_validator/config.py,sha256=kTqD8-SFbtUQ9oif-TylqNFPadTq5JSGBv84cI0R8dY,2657
|
5
|
+
code_validator/core.py,sha256=4sPdlunmODfNpDrP9QH2jIrGFVLFlo_r8MAoB4_63vo,4560
|
6
|
+
code_validator/exceptions.py,sha256=XkiRNQ25FWJkjS2wBaUaKQcEL5WF9tN_HSV3tqJwDcE,1627
|
7
|
+
code_validator/output.py,sha256=VRJLGwm6X9i8SnovosNrHu96ueZXz9GIvUQXy4xDtHw,3304
|
8
|
+
code_validator/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
code_validator/components/ast_utils.py,sha256=7DzyKmLekj_qtuZcS8BrcWXqQXVEPPQ_rwh3BmpIk9o,1412
|
10
|
+
code_validator/components/definitions.py,sha256=cFbKL7UZYHqWjz2gJCmZ6fU_ZdQ5L_h1tlQBYkxSO7Q,3126
|
11
|
+
code_validator/components/factories.py,sha256=qrQotS7lyqIGhoNGRvSNQKy6p9YJKQC7YaPi4eCtpww,10436
|
12
|
+
code_validator/components/scope_handler.py,sha256=vb4pCE-4DyxGkRYGeW2JWxP2IiZH3uRRfReo0IBgM5Y,2459
|
13
|
+
code_validator/rules_library/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
|
+
code_validator/rules_library/basic_rules.py,sha256=0K45Ed8yjy-pXDZpI1Xvgj94zvaFz-PucFRz__X4uRI,6652
|
15
|
+
code_validator/rules_library/constraint_logic.py,sha256=X95g0673hoZey3LGCBH6AscbIuHYq_aY9WRKIkJhOnk,9525
|
16
|
+
code_validator/rules_library/selector_nodes.py,sha256=JxhUBXS95Dad_60Ut-8XkW2MFM-7bkeRb_yk7vXlNPE,12200
|
17
|
+
python_code_validator-0.1.1.dist-info/licenses/LICENSE,sha256=Lq69RwIO4Dge7OsjgAamJfYSDq2DWI2yzVYI1VX1s6c,1089
|
18
|
+
python_code_validator-0.1.1.dist-info/METADATA,sha256=e_wxbF2xUH4kdixYAAktx_J7op4a6t5bsThjhvjnhZQ,11682
|
19
|
+
python_code_validator-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
20
|
+
python_code_validator-0.1.1.dist-info/entry_points.txt,sha256=pw_HijiZyPxokVJHStTkGCwheTjukDomdk81JyHzv74,66
|
21
|
+
python_code_validator-0.1.1.dist-info/top_level.txt,sha256=yowMDfABI5oqgW3hhTdec_7UHGeprkvc2BnqRzNbI5w,15
|
22
|
+
python_code_validator-0.1.1.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Ivan Kovach
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
code_validator
|