python-code-validator 0.1.2__py3-none-any.whl → 0.2.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 +43 -11
- code_validator/__main__.py +12 -3
- code_validator/cli.py +48 -24
- code_validator/components/ast_utils.py +6 -2
- code_validator/components/definitions.py +6 -5
- code_validator/components/factories.py +35 -16
- code_validator/components/scope_handler.py +5 -3
- code_validator/config.py +60 -7
- code_validator/core.py +161 -38
- code_validator/output.py +160 -14
- code_validator/rules_library/basic_rules.py +23 -16
- code_validator/rules_library/constraint_logic.py +301 -257
- code_validator/rules_library/selector_nodes.py +66 -5
- {python_code_validator-0.1.2.dist-info → python_code_validator-0.2.1.dist-info}/METADATA +13 -30
- python_code_validator-0.2.1.dist-info/RECORD +22 -0
- python_code_validator-0.1.2.dist-info/RECORD +0 -22
- {python_code_validator-0.1.2.dist-info → python_code_validator-0.2.1.dist-info}/WHEEL +0 -0
- {python_code_validator-0.1.2.dist-info → python_code_validator-0.2.1.dist-info}/entry_points.txt +0 -0
- {python_code_validator-0.1.2.dist-info → python_code_validator-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {python_code_validator-0.1.2.dist-info → python_code_validator-0.2.1.dist-info}/top_level.txt +0 -0
code_validator/core.py
CHANGED
@@ -1,100 +1,223 @@
|
|
1
|
+
"""The core engine of the Python Code Validator.
|
2
|
+
|
3
|
+
This module contains the main orchestrator class, `StaticValidator`, which is
|
4
|
+
responsible for managing the entire validation lifecycle. It loads the source
|
5
|
+
code and a set of JSON rules, then uses a factory-based component system to
|
6
|
+
execute each rule and report the results.
|
7
|
+
|
8
|
+
The core is designed to be decoupled from the specific implementations of rules,
|
9
|
+
selectors, and constraints, allowing for high extensibility.
|
10
|
+
|
11
|
+
Example:
|
12
|
+
To run a validation, you would typically use the CLI, but the core can also
|
13
|
+
be used programmatically:
|
14
|
+
|
15
|
+
.. code-block:: python
|
16
|
+
|
17
|
+
from code_validator import StaticValidator, AppConfig, LogLevel
|
18
|
+
from code_validator.output import Console, setup_logging
|
19
|
+
from pathlib import Path
|
20
|
+
import logging
|
21
|
+
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
console = Console(logger)
|
24
|
+
config = AppConfig(
|
25
|
+
solution_path=Path("path/to/solution.py"),
|
26
|
+
rules_path=Path("path/to/rules.json"),
|
27
|
+
log_level=LogLevel.INFO,
|
28
|
+
is_silent=False,
|
29
|
+
stop_on_first_fail=False
|
30
|
+
)
|
31
|
+
|
32
|
+
validator = StaticValidator(config, console)
|
33
|
+
is_valid = validator.run()
|
34
|
+
|
35
|
+
if is_valid:
|
36
|
+
print("Validation Passed!")
|
37
|
+
else:
|
38
|
+
print(f"Validation Failed. Errors in: {validator.failed_rules_id}")
|
39
|
+
|
40
|
+
"""
|
41
|
+
|
1
42
|
import ast
|
2
43
|
import json
|
3
44
|
|
4
45
|
from .components.ast_utils import enrich_ast_with_parents
|
5
46
|
from .components.definitions import Rule
|
6
47
|
from .components.factories import RuleFactory
|
7
|
-
from .config import AppConfig, LogLevel
|
48
|
+
from .config import AppConfig, LogLevel, ShortRuleConfig
|
8
49
|
from .exceptions import RuleParsingError
|
9
|
-
from .output import Console
|
50
|
+
from .output import Console, log_initialization
|
10
51
|
|
11
52
|
|
12
53
|
class StaticValidator:
|
13
|
-
"""Orchestrates the static validation process.
|
54
|
+
"""Orchestrates the static validation process.
|
55
|
+
|
56
|
+
This class is the main entry point for running a validation session. It
|
57
|
+
manages loading of source files and rules, parsing the code into an AST,
|
58
|
+
and iterating through the rules to execute them.
|
59
|
+
|
60
|
+
Attributes:
|
61
|
+
_config (AppConfig): The application configuration object.
|
62
|
+
_console (Console): The handler for all logging and stdout printing.
|
63
|
+
_rule_factory (RuleFactory): The factory responsible for creating rule objects.
|
64
|
+
_source_code (str): The raw text content of the Python file being validated.
|
65
|
+
_ast_tree (ast.Module | None): The Abstract Syntax Tree of the source code.
|
66
|
+
_rules (list[Rule]): A list of initialized, executable rule objects.
|
67
|
+
_failed_rules (list[int]): A list of rule IDs that failed during the run.
|
68
|
+
"""
|
14
69
|
|
70
|
+
@log_initialization(level=LogLevel.DEBUG)
|
15
71
|
def __init__(self, config: AppConfig, console: Console):
|
16
|
-
"""Initializes the
|
72
|
+
"""Initializes the StaticValidator.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
config: An `AppConfig` object containing all necessary run
|
76
|
+
configurations, such as file paths and flags.
|
77
|
+
console: A `Console` object for handling all output.
|
78
|
+
"""
|
17
79
|
self._config = config
|
18
80
|
self._console = console
|
81
|
+
|
19
82
|
self._rule_factory = RuleFactory(self._console)
|
20
83
|
self._source_code: str = ""
|
21
84
|
self._ast_tree: ast.Module | None = None
|
22
|
-
self.
|
85
|
+
self._rules: list[Rule] = []
|
23
86
|
self._failed_rules: list[int] = []
|
24
87
|
|
25
88
|
@property
|
26
89
|
def failed_rules_id(self) -> list[int]:
|
27
|
-
"""
|
90
|
+
"""list[int]: A list of rule IDs that failed during the last run."""
|
28
91
|
return self._failed_rules
|
29
92
|
|
30
93
|
def _load_source_code(self) -> None:
|
31
|
-
"""Loads the content of the student's solution file.
|
32
|
-
|
94
|
+
"""Loads the content of the student's solution file into memory.
|
95
|
+
|
96
|
+
Raises:
|
97
|
+
FileNotFoundError: If the source file specified in the config does not exist.
|
98
|
+
RuleParsingError: If the source file cannot be read for any other reason.
|
99
|
+
"""
|
100
|
+
self._console.print(f"Reading source file: {self._config.solution_path}", level=LogLevel.DEBUG)
|
33
101
|
try:
|
34
102
|
self._source_code = self._config.solution_path.read_text(encoding="utf-8")
|
103
|
+
self._console.print(f"Source code:\n{self._source_code}\n", level=LogLevel.TRACE)
|
35
104
|
except FileNotFoundError:
|
105
|
+
self._console.print("During reading source file raised FileNotFound", level=LogLevel.TRACE)
|
36
106
|
raise
|
37
107
|
except Exception as e:
|
108
|
+
self._console.print("During reading source file raised some exception..", level=LogLevel.TRACE)
|
38
109
|
raise RuleParsingError(f"Cannot read source file: {e}") from e
|
39
110
|
|
40
|
-
def _parse_ast_tree(self) -> bool:
|
41
|
-
"""Parses the loaded source code into an AST."""
|
42
|
-
self._console.print("Parsing Abstract Syntax Tree (AST)...")
|
43
|
-
try:
|
44
|
-
self._ast_tree = ast.parse(self._source_code)
|
45
|
-
enrich_ast_with_parents(self._ast_tree)
|
46
|
-
return True
|
47
|
-
except SyntaxError as e:
|
48
|
-
# Ищем правило check_syntax, чтобы вывести его кастомное сообщение
|
49
|
-
for rule in self._validation_rules:
|
50
|
-
if getattr(rule.config, "type", None) == "check_syntax":
|
51
|
-
self._console.print(rule.config.message, level="ERROR")
|
52
|
-
self._failed_rules.append(rule.config.rule_id)
|
53
|
-
return False
|
54
|
-
# Если такого правила нет, выводим стандартное сообщение
|
55
|
-
self._console.print(f"Syntax Error found: {e}", level="ERROR")
|
56
|
-
return False
|
57
|
-
|
58
111
|
def _load_and_parse_rules(self) -> None:
|
59
|
-
"""Loads and parses the JSON file
|
60
|
-
|
112
|
+
"""Loads and parses the JSON file into executable Rule objects.
|
113
|
+
|
114
|
+
This method reads the JSON rules file, validates its basic structure,
|
115
|
+
and then uses the `RuleFactory` to instantiate a list of concrete
|
116
|
+
Rule objects.
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
FileNotFoundError: If the rules file does not exist.
|
120
|
+
RuleParsingError: If the JSON is malformed or a rule configuration
|
121
|
+
is invalid.
|
122
|
+
"""
|
123
|
+
self._console.print(f"Loading rules from: {self._config.rules_path}", level=LogLevel.DEBUG)
|
61
124
|
try:
|
62
125
|
rules_data = json.loads(self._config.rules_path.read_text(encoding="utf-8"))
|
126
|
+
self._console.print(f"Load rules:\n{rules_data}", level=LogLevel.TRACE)
|
63
127
|
raw_rules = rules_data.get("validation_rules")
|
64
128
|
if not isinstance(raw_rules, list):
|
65
129
|
raise RuleParsingError("`validation_rules` key not found or is not a list.")
|
66
130
|
|
67
|
-
self.
|
68
|
-
self.
|
131
|
+
self._console.print(f"Found {len(raw_rules)}.", level=LogLevel.DEBUG)
|
132
|
+
self._rules = [self._rule_factory.create(rule) for rule in raw_rules]
|
133
|
+
self._console.print(f"Successfully parsed {len(self._rules)} rules.", level=LogLevel.DEBUG)
|
69
134
|
except json.JSONDecodeError as e:
|
135
|
+
self._console.print("During reading file of rules raised JsonDecodeError..", level=LogLevel.TRACE)
|
70
136
|
raise RuleParsingError(f"Invalid JSON in rules file: {e}") from e
|
71
137
|
except FileNotFoundError:
|
138
|
+
self._console.print("During reading file of rules raised FileNotFound", level=LogLevel.TRACE)
|
72
139
|
raise
|
73
140
|
|
141
|
+
def _parse_ast_tree(self) -> bool:
|
142
|
+
"""Parses the loaded source code into an AST and enriches it.
|
143
|
+
|
144
|
+
This method attempts to parse the source code. If successful, it calls
|
145
|
+
a helper to add parent references to each node in the tree, which is
|
146
|
+
crucial for many advanced checks. If a `SyntaxError` occurs, it
|
147
|
+
checks if a `check_syntax` rule was defined to provide a custom message.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
bool: True if parsing was successful, False otherwise.
|
151
|
+
"""
|
152
|
+
self._console.print("Parsing Abstract Syntax Tree (AST)...", level=LogLevel.DEBUG)
|
153
|
+
try:
|
154
|
+
self._console.print("Start parse source code.", level=LogLevel.TRACE)
|
155
|
+
self._ast_tree = ast.parse(self._source_code)
|
156
|
+
enrich_ast_with_parents(self._ast_tree)
|
157
|
+
return True
|
158
|
+
except SyntaxError as e:
|
159
|
+
self._console.print("In source code SyntaxError..", level=LogLevel.TRACE)
|
160
|
+
for rule in self._rules:
|
161
|
+
if getattr(rule.config, "type", None) == "check_syntax":
|
162
|
+
self._console.print(rule.config.message, level=LogLevel.ERROR, show_user=True)
|
163
|
+
self._console.print(f"Failed rule id: {rule.config.rule_id}", level=LogLevel.DEBUG)
|
164
|
+
self._failed_rules.append(rule.config.rule_id)
|
165
|
+
return False
|
166
|
+
self._console.print(f"Syntax Error found: {e}", level=LogLevel.ERROR)
|
167
|
+
return False
|
168
|
+
|
74
169
|
def run(self) -> bool:
|
75
|
-
"""Runs the entire validation process.
|
170
|
+
"""Runs the entire validation process from start to finish.
|
171
|
+
|
172
|
+
This is the main public method of the class. It orchestrates the
|
173
|
+
sequence of loading, parsing, and rule execution.
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
bool: True if all validation rules passed, False otherwise.
|
177
|
+
|
178
|
+
Raises:
|
179
|
+
RuleParsingError: Propagated from loading/parsing steps.
|
180
|
+
FileNotFoundError: Propagated from loading steps.
|
181
|
+
"""
|
76
182
|
try:
|
77
183
|
self._load_source_code()
|
78
|
-
self._load_and_parse_rules()
|
184
|
+
self._load_and_parse_rules()
|
79
185
|
|
80
186
|
if not self._parse_ast_tree():
|
81
187
|
return False
|
82
188
|
|
83
|
-
|
189
|
+
self._console.print("Lead source code, load and parse rules and parsing code - PASS", level=LogLevel.DEBUG)
|
190
|
+
|
191
|
+
except (FileNotFoundError, RuleParsingError) as e:
|
192
|
+
self._console.print(
|
193
|
+
f"In method `run` of 'StaticValidator' raised exception {e.__class__.__name__}", level=LogLevel.WARNING
|
194
|
+
)
|
84
195
|
raise
|
85
196
|
|
86
|
-
|
87
|
-
|
197
|
+
self._console.print("Starting check rules..", level=LogLevel.DEBUG)
|
198
|
+
for rule in self._rules:
|
88
199
|
if getattr(rule.config, "type", None) == "check_syntax":
|
89
200
|
continue
|
90
201
|
|
91
|
-
self._console.print(
|
202
|
+
self._console.print(
|
203
|
+
f"Executing rule: {rule.config.rule_id}"
|
204
|
+
+ (
|
205
|
+
f" [{rule.config.check.selector.type}, {rule.config.check.constraint.type}, "
|
206
|
+
f"is_critical={rule.config.is_critical}]"
|
207
|
+
if not isinstance(rule.config, ShortRuleConfig)
|
208
|
+
else ""
|
209
|
+
),
|
210
|
+
level=LogLevel.INFO,
|
211
|
+
)
|
92
212
|
is_passed = rule.execute(self._ast_tree, self._source_code)
|
93
213
|
if not is_passed:
|
94
|
-
self._console.print(rule.config.message, level=
|
214
|
+
self._console.print(rule.config.message, level=LogLevel.WARNING, show_user=True)
|
215
|
+
self._console.print(f"Rule {rule.config.rule_id} - FAIL", level=LogLevel.INFO)
|
95
216
|
self._failed_rules.append(rule.config.rule_id)
|
96
217
|
if getattr(rule.config, "is_critical", False) or self._config.stop_on_first_fail:
|
97
|
-
self._console.print("Critical rule failed. Halting validation.", level=
|
218
|
+
self._console.print("Critical rule failed. Halting validation.", level=LogLevel.WARNING)
|
98
219
|
break
|
220
|
+
else:
|
221
|
+
self._console.print(f"Rule {rule.config.rule_id} - PASS", level=LogLevel.INFO)
|
99
222
|
|
100
223
|
return not self._failed_rules
|
code_validator/output.py
CHANGED
@@ -7,12 +7,50 @@ controlled via configuration (e.g., log levels, silent mode).
|
|
7
7
|
|
8
8
|
import logging
|
9
9
|
import sys
|
10
|
-
from
|
10
|
+
from functools import wraps
|
11
|
+
from typing import Callable, Concatenate, Literal, ParamSpec, TypeVar
|
11
12
|
|
12
13
|
from .config import LogLevel
|
13
14
|
|
14
|
-
LOG_FORMAT = "%(asctime)s | %(filename)-15s | %(funcName)-15s (%(lineno)-3s) | [%(levelname)s] - %(message)s"
|
15
15
|
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
16
|
+
LOG_FORMAT = (
|
17
|
+
"{asctime}.{msecs:03.0f} | "
|
18
|
+
"{levelname:^8} | "
|
19
|
+
"[{processName}({process})/{threadName}({thread})] | "
|
20
|
+
"{filename}:{funcName}:{lineno} | "
|
21
|
+
"{message}"
|
22
|
+
)
|
23
|
+
|
24
|
+
TRACE_LEVEL_NUM = 5
|
25
|
+
logging.addLevelName(TRACE_LEVEL_NUM, "TRACE")
|
26
|
+
|
27
|
+
|
28
|
+
def trace(self, message, *args, **kws):
|
29
|
+
"""Logs a message with TRACE level (below DEBUG).
|
30
|
+
|
31
|
+
This method allows logging messages with a custom TRACE level,
|
32
|
+
defined at level number 5. It only emits the log if the logger
|
33
|
+
is enabled for this level.
|
34
|
+
|
35
|
+
To enable usage, attach this method to the `logging.Logger` class:
|
36
|
+
|
37
|
+
logging.Logger.trace = trace
|
38
|
+
|
39
|
+
Args:
|
40
|
+
self: logger instance.
|
41
|
+
message: The log message format string.
|
42
|
+
*args: Arguments to be merged into the message format string.
|
43
|
+
**kws: Optional keyword arguments passed to the logger,
|
44
|
+
e.g., `exc_info`, `stacklevel`, or `extra`.
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
None
|
48
|
+
"""
|
49
|
+
if self.isEnabledFor(TRACE_LEVEL_NUM):
|
50
|
+
self._log(TRACE_LEVEL_NUM, message, args, **kws)
|
51
|
+
|
52
|
+
|
53
|
+
logging.Logger.trace = trace
|
16
54
|
|
17
55
|
|
18
56
|
def setup_logging(log_level: LogLevel) -> logging.Logger:
|
@@ -27,8 +65,75 @@ def setup_logging(log_level: LogLevel) -> logging.Logger:
|
|
27
65
|
Returns:
|
28
66
|
The configured root logger instance.
|
29
67
|
"""
|
30
|
-
logging.
|
31
|
-
|
68
|
+
formatter = logging.Formatter(fmt=LOG_FORMAT, datefmt=DATE_FORMAT, style="{")
|
69
|
+
|
70
|
+
handler = logging.StreamHandler(sys.stderr)
|
71
|
+
handler.setFormatter(formatter)
|
72
|
+
|
73
|
+
root_logger = logging.getLogger()
|
74
|
+
root_logger.setLevel(log_level)
|
75
|
+
|
76
|
+
if root_logger.hasHandlers():
|
77
|
+
root_logger.handlers.clear()
|
78
|
+
|
79
|
+
root_logger.addHandler(handler)
|
80
|
+
|
81
|
+
return root_logger
|
82
|
+
|
83
|
+
|
84
|
+
P = ParamSpec("P")
|
85
|
+
T_self = TypeVar("T_self")
|
86
|
+
|
87
|
+
|
88
|
+
def log_initialization(
|
89
|
+
level: LogLevel | Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = LogLevel.TRACE,
|
90
|
+
start_message: str = "Initializing {class_name}...",
|
91
|
+
end_message: str = "{class_name} initialized.",
|
92
|
+
) -> Callable[[Callable[Concatenate[T_self, P], None]], Callable[Concatenate[T_self, P], None]]:
|
93
|
+
"""Decorator factory for logging the initialization of a class instance.
|
94
|
+
|
95
|
+
This decorator wraps a class's `__init__` method to automatically log
|
96
|
+
messages before and after the constructor's execution. It helps in
|
97
|
+
observing the lifecycle of objects, especially complex ones, without
|
98
|
+
cluttering the `__init__` method itself.
|
99
|
+
|
100
|
+
The log messages can include a `{class_name}` placeholder, which will
|
101
|
+
be replaced by the actual name of the class being initialized.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
level: The logging level (e.g., `LogLevel.DEBUG`, `LogLevel.INFO`)
|
105
|
+
at which the messages should be logged.
|
106
|
+
start_message: the message string to log immediately before the
|
107
|
+
`__init__` method is called. This string can contain the
|
108
|
+
`{class_name}` placeholder.
|
109
|
+
end_message: The message string to log immediately after the
|
110
|
+
`__init__` method completes its execution. This string can
|
111
|
+
contain the `{class_name}` placeholder.
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
A decorator function that can be applied to an `__init__` method
|
115
|
+
of a class.
|
116
|
+
"""
|
117
|
+
|
118
|
+
def decorator(init_method: Callable[Concatenate[T_self, P], None]) -> Callable[Concatenate[T_self, P], None]:
|
119
|
+
"""The actual decorator function."""
|
120
|
+
|
121
|
+
@wraps(init_method)
|
122
|
+
def wrapper(self: T_self, *args: P.args, **kwargs: P.kwargs) -> None:
|
123
|
+
"""The wrapper function that adds logging around __init__."""
|
124
|
+
class_name = self.__class__.__name__
|
125
|
+
logger = logging.getLogger(self.__class__.__module__)
|
126
|
+
level_num = logging.getLevelName(level if isinstance(level, LogLevel) else level)
|
127
|
+
|
128
|
+
logger.log(level_num, start_message.format(class_name=class_name))
|
129
|
+
result = init_method(self, *args, **kwargs)
|
130
|
+
logger.log(level_num, end_message.format(class_name=class_name))
|
131
|
+
|
132
|
+
return result
|
133
|
+
|
134
|
+
return wrapper
|
135
|
+
|
136
|
+
return decorator
|
32
137
|
|
33
138
|
|
34
139
|
class Console:
|
@@ -40,7 +145,7 @@ class Console:
|
|
40
145
|
|
41
146
|
Attributes:
|
42
147
|
_logger (logging.Logger): The logger instance used for all log records.
|
43
|
-
|
148
|
+
_is_quiet (bool): A flag to suppress printing to stdout.
|
44
149
|
_stdout (TextIO): The stream to write messages to (defaults to sys.stdout).
|
45
150
|
|
46
151
|
Example:
|
@@ -53,22 +158,58 @@ class Console:
|
|
53
158
|
This is a warning.
|
54
159
|
"""
|
55
160
|
|
56
|
-
|
161
|
+
@log_initialization(level=LogLevel.TRACE)
|
162
|
+
def __init__(self, logger: logging.Logger, *, is_quiet: bool = False, show_verdict: bool = True):
|
57
163
|
"""Initializes the Console handler.
|
58
164
|
|
59
165
|
Args:
|
60
166
|
logger: The configured logger instance to use for logging.
|
61
|
-
|
167
|
+
is_quiet: If True, suppresses output to stdout. Defaults to False.
|
168
|
+
show_verdict: If False, suppresses showing verdicts. Default to True
|
62
169
|
"""
|
63
170
|
self._logger = logger
|
64
|
-
self.
|
171
|
+
self._is_quiet = is_quiet
|
172
|
+
self._show_verdict = show_verdict
|
65
173
|
self._stdout = sys.stdout
|
66
174
|
|
175
|
+
def should_print(self, is_verdict: bool, show_user: bool) -> bool:
|
176
|
+
"""Decides whether a message should be printed to stdout based on console flags.
|
177
|
+
|
178
|
+
Quiet mode (Q) suppresses all output if enabled. For non-verdict messages (¬V),
|
179
|
+
printing is allowed only when show_user (O) is True. For verdict messages (V),
|
180
|
+
printing is allowed only when show_verdict (S) is True.
|
181
|
+
|
182
|
+
Mathematically:
|
183
|
+
F = ¬Q ∧ ( (¬V ∧ O) ∨ (V ∧ S) )
|
184
|
+
where
|
185
|
+
Q = self._is_quiet,
|
186
|
+
V = is_verdict,
|
187
|
+
S = self._show_verdict,
|
188
|
+
O = show_user.
|
189
|
+
|
190
|
+
Proof sketch:
|
191
|
+
1. If Q is True, then ¬Q = False ⇒ F = False (silent mode).
|
192
|
+
2. If Q is False, split on V:
|
193
|
+
a. V = False ⇒ F = ¬Q ∧ O;
|
194
|
+
b. V = True ⇒ F = ¬Q ∧ S.
|
195
|
+
3. Combine branches into F = ¬Q ∧ ( (¬V ∧ O) ∨ (V ∧ S) ).
|
196
|
+
|
197
|
+
Args:
|
198
|
+
is_verdict: True if this message is a verdict; controls branch V.
|
199
|
+
show_user: True if non-verdict messages should be printed; controls branch O.
|
200
|
+
|
201
|
+
Returns:
|
202
|
+
True if and only if the combined condition F holds, i.e. the message may be printed.
|
203
|
+
"""
|
204
|
+
return not self._is_quiet and ((not is_verdict and show_user) or (is_verdict and self._show_verdict))
|
205
|
+
|
67
206
|
def print(
|
68
207
|
self,
|
69
208
|
message: str,
|
70
209
|
*,
|
71
|
-
level: LogLevel | Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = LogLevel.
|
210
|
+
level: LogLevel | Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = LogLevel.TRACE,
|
211
|
+
is_verdict: bool = False,
|
212
|
+
show_user: bool = False,
|
72
213
|
) -> None:
|
73
214
|
"""Prints a message to stdout and logs it simultaneously.
|
74
215
|
|
@@ -80,11 +221,16 @@ class Console:
|
|
80
221
|
message: The string message to be displayed and logged.
|
81
222
|
level: The logging level for the message. Accepts both LogLevel
|
82
223
|
enum members and their string representations.
|
83
|
-
Defaults to LogLevel.
|
224
|
+
Defaults to LogLevel.TRACE.
|
225
|
+
is_verdict: If True, this message is considered a
|
226
|
+
verdict and its printing is controlled by the
|
227
|
+
console’s `show_verdict` flag. Defaults to False.
|
228
|
+
show_user: If True and `is_verdict=False`, allows
|
229
|
+
printing non-verdict messages to stdout. Defaults to
|
230
|
+
False.
|
84
231
|
"""
|
85
|
-
|
86
|
-
|
87
|
-
log_method(message)
|
232
|
+
level_num = logging.getLevelName(level if isinstance(level, LogLevel) else level)
|
233
|
+
self._logger.log(level_num, message, stacklevel=2)
|
88
234
|
|
89
|
-
if not self.
|
235
|
+
if (not self._is_quiet) and ((not is_verdict and show_user) or (is_verdict and self._show_verdict)):
|
90
236
|
print(message, file=self._stdout)
|
@@ -1,10 +1,10 @@
|
|
1
|
-
# src/code_validator/rules_library/basic_rules.py
|
2
|
-
|
3
1
|
"""Contains concrete implementations of executable validation rules.
|
4
2
|
|
5
3
|
This module defines the handler classes for both "short" (pre-defined) and
|
6
|
-
"full" (custom selector/constraint) rules. Each class
|
7
|
-
protocol and encapsulates the logic for a
|
4
|
+
"full" (custom selector/constraint) rules. Each class in this module implements
|
5
|
+
the `Rule` protocol from `definitions.py` and encapsulates the logic for a
|
6
|
+
specific type of validation check. The `RuleFactory` uses these classes to
|
7
|
+
instantiate the correct handler for each rule defined in a JSON file.
|
8
8
|
"""
|
9
9
|
|
10
10
|
import ast
|
@@ -13,7 +13,7 @@ import sys
|
|
13
13
|
|
14
14
|
from ..components.definitions import Constraint, Rule, Selector
|
15
15
|
from ..config import FullRuleConfig, ShortRuleConfig
|
16
|
-
from ..output import Console, LogLevel
|
16
|
+
from ..output import Console, LogLevel, log_initialization
|
17
17
|
|
18
18
|
|
19
19
|
class CheckSyntaxRule(Rule):
|
@@ -27,6 +27,7 @@ class CheckSyntaxRule(Rule):
|
|
27
27
|
formally defined in the JSON configuration.
|
28
28
|
"""
|
29
29
|
|
30
|
+
@log_initialization(level=LogLevel.TRACE)
|
30
31
|
def __init__(self, config: ShortRuleConfig, console: Console):
|
31
32
|
"""Initializes the syntax check rule handler.
|
32
33
|
|
@@ -45,7 +46,7 @@ class CheckSyntaxRule(Rule):
|
|
45
46
|
Returns:
|
46
47
|
Always returns True.
|
47
48
|
"""
|
48
|
-
self._console.print(f"Rule {self.config.rule_id}: Syntax is valid.", level=LogLevel.
|
49
|
+
self._console.print(f"Rule {self.config.rule_id}: Syntax is valid.", level=LogLevel.INFO)
|
49
50
|
return True
|
50
51
|
|
51
52
|
|
@@ -57,6 +58,7 @@ class CheckLinterRule(Rule):
|
|
57
58
|
configurable via the 'params' field in the JSON rule.
|
58
59
|
"""
|
59
60
|
|
61
|
+
@log_initialization(level=LogLevel.TRACE)
|
60
62
|
def __init__(self, config: ShortRuleConfig, console: Console):
|
61
63
|
"""Initializes a PEP8 linter check rule handler."""
|
62
64
|
self.config = config
|
@@ -77,10 +79,10 @@ class CheckLinterRule(Rule):
|
|
77
79
|
True if no PEP8 violations are found, False otherwise.
|
78
80
|
"""
|
79
81
|
if not source_code:
|
80
|
-
self._console.print("Source code is empty, skipping PEP8 check.", level=
|
82
|
+
self._console.print("Source code is empty, skipping PEP8 check.", level=LogLevel.WARNING)
|
81
83
|
return True
|
82
84
|
|
83
|
-
self._console.print(f"Rule {self.config.rule_id}: Running
|
85
|
+
self._console.print(f"Rule {self.config.rule_id}: Running PEP8 linter...", level=LogLevel.INFO)
|
84
86
|
|
85
87
|
params = self.config.params
|
86
88
|
args = [sys.executable, "-m", "flake8", "-"]
|
@@ -90,6 +92,8 @@ class CheckLinterRule(Rule):
|
|
90
92
|
elif ignore_list := params.get("ignore"):
|
91
93
|
args.append(f"--ignore={','.join(ignore_list)}")
|
92
94
|
|
95
|
+
self._console.print(f"Arguments for flake8: {args}", level=LogLevel.TRACE)
|
96
|
+
|
93
97
|
try:
|
94
98
|
process = subprocess.run(
|
95
99
|
args,
|
@@ -102,19 +106,21 @@ class CheckLinterRule(Rule):
|
|
102
106
|
|
103
107
|
if process.returncode != 0 and process.stdout:
|
104
108
|
linter_output = process.stdout.strip()
|
105
|
-
self._console.print(f"Flake8 found issues:\n{linter_output}", level=
|
109
|
+
self._console.print(f"Flake8 found issues:\n{linter_output}", level=LogLevel.WARNING, show_user=True)
|
106
110
|
return False
|
107
111
|
elif process.returncode != 0:
|
108
|
-
self._console.print(
|
112
|
+
self._console.print(
|
113
|
+
f"Flake8 exited with code {process.returncode}:\n{process.stderr}", level=LogLevel.ERROR
|
114
|
+
)
|
109
115
|
return False
|
110
116
|
|
111
|
-
self._console.print("PEP8 check passed.", level=
|
117
|
+
self._console.print("PEP8 check passed.", level=LogLevel.INFO)
|
112
118
|
return True
|
113
119
|
except FileNotFoundError:
|
114
|
-
self._console.print("flake8 not found. Is it installed in the venv?", level=
|
120
|
+
self._console.print("flake8 not found. Is it installed in the venv?", level=LogLevel.CRITICAL)
|
115
121
|
return False
|
116
122
|
except Exception as e:
|
117
|
-
self._console.print(f"An unexpected error occurred while running flake8: {e}", level=
|
123
|
+
self._console.print(f"An unexpected error occurred while running flake8: {e}", level=LogLevel.CRITICAL)
|
118
124
|
return False
|
119
125
|
|
120
126
|
|
@@ -132,6 +138,7 @@ class FullRuleHandler(Rule):
|
|
132
138
|
_console (Console): The console handler for logging.
|
133
139
|
"""
|
134
140
|
|
141
|
+
@log_initialization(level=LogLevel.TRACE)
|
135
142
|
def __init__(self, config: FullRuleConfig, selector: Selector, constraint: Constraint, console: Console):
|
136
143
|
"""Initializes a full rule handler.
|
137
144
|
|
@@ -157,11 +164,11 @@ class FullRuleHandler(Rule):
|
|
157
164
|
The boolean result of applying the constraint to the selected nodes.
|
158
165
|
"""
|
159
166
|
if not tree:
|
160
|
-
self._console.print("AST not available, skipping rule.", level=
|
167
|
+
self._console.print("AST not available, skipping rule.", level=LogLevel.WARNING)
|
161
168
|
return True
|
162
169
|
|
163
|
-
self._console.print(f"Applying selector: {self._selector.__class__.__name__}", level=
|
170
|
+
self._console.print(f"Applying selector: {self._selector.__class__.__name__}", level=LogLevel.TRACE)
|
164
171
|
selected_nodes = self._selector.select(tree)
|
165
172
|
|
166
|
-
self._console.print(f"Applying constraint: {self._constraint.__class__.__name__}", level=
|
173
|
+
self._console.print(f"Applying constraint: {self._constraint.__class__.__name__}", level=LogLevel.TRACE)
|
167
174
|
return self._constraint.check(selected_nodes)
|