python-code-validator 0.1.3__py3-none-any.whl → 0.3.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.
- code_validator/__init__.py +1 -1
- code_validator/cli.py +45 -19
- code_validator/components/factories.py +31 -13
- code_validator/config.py +7 -4
- code_validator/core.py +115 -36
- code_validator/output.py +162 -14
- code_validator/rules_library/basic_rules.py +12 -7
- code_validator/rules_library/constraint_logic.py +301 -292
- code_validator/rules_library/selector_nodes.py +9 -0
- {python_code_validator-0.1.3.dist-info → python_code_validator-0.3.0.dist-info}/METADATA +2 -2
- python_code_validator-0.3.0.dist-info/RECORD +22 -0
- python_code_validator-0.1.3.dist-info/RECORD +0 -22
- {python_code_validator-0.1.3.dist-info → python_code_validator-0.3.0.dist-info}/WHEEL +0 -0
- {python_code_validator-0.1.3.dist-info → python_code_validator-0.3.0.dist-info}/entry_points.txt +0 -0
- {python_code_validator-0.1.3.dist-info → python_code_validator-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {python_code_validator-0.1.3.dist-info → python_code_validator-0.3.0.dist-info}/top_level.txt +0 -0
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,59 @@ 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,
|
213
|
+
exc_info: bool = False,
|
72
214
|
) -> None:
|
73
215
|
"""Prints a message to stdout and logs it simultaneously.
|
74
216
|
|
@@ -80,11 +222,17 @@ class Console:
|
|
80
222
|
message: The string message to be displayed and logged.
|
81
223
|
level: The logging level for the message. Accepts both LogLevel
|
82
224
|
enum members and their string representations.
|
83
|
-
Defaults to LogLevel.
|
225
|
+
Defaults to LogLevel.TRACE.
|
226
|
+
is_verdict: If True, this message is considered a
|
227
|
+
verdict and its printing is controlled by the
|
228
|
+
console’s `show_verdict` flag. Defaults to False.
|
229
|
+
show_user: If True and `is_verdict=False`, allows
|
230
|
+
printing non-verdict messages to stdout. Defaults to
|
231
|
+
False.
|
232
|
+
exc_info: If True this work as loggings.exception("<message>").
|
84
233
|
"""
|
85
|
-
|
86
|
-
|
87
|
-
log_method(message)
|
234
|
+
level_num = logging.getLevelName(level if isinstance(level, LogLevel) else level)
|
235
|
+
self._logger.log(level_num, message, stacklevel=2, exc_info=exc_info)
|
88
236
|
|
89
|
-
if not self.
|
237
|
+
if (not self._is_quiet) and ((not is_verdict and show_user) or (is_verdict and self._show_verdict)):
|
90
238
|
print(message, file=self._stdout)
|
@@ -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
|
@@ -80,7 +82,7 @@ class CheckLinterRule(Rule):
|
|
80
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,7 +106,7 @@ 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=LogLevel.
|
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
112
|
self._console.print(
|
@@ -110,7 +114,7 @@ class CheckLinterRule(Rule):
|
|
110
114
|
)
|
111
115
|
return False
|
112
116
|
|
113
|
-
self._console.print("PEP8 check passed.", level=LogLevel.
|
117
|
+
self._console.print("PEP8 check passed.", level=LogLevel.INFO)
|
114
118
|
return True
|
115
119
|
except FileNotFoundError:
|
116
120
|
self._console.print("flake8 not found. Is it installed in the venv?", level=LogLevel.CRITICAL)
|
@@ -134,6 +138,7 @@ class FullRuleHandler(Rule):
|
|
134
138
|
_console (Console): The console handler for logging.
|
135
139
|
"""
|
136
140
|
|
141
|
+
@log_initialization(level=LogLevel.TRACE)
|
137
142
|
def __init__(self, config: FullRuleConfig, selector: Selector, constraint: Constraint, console: Console):
|
138
143
|
"""Initializes a full rule handler.
|
139
144
|
|
@@ -162,8 +167,8 @@ class FullRuleHandler(Rule):
|
|
162
167
|
self._console.print("AST not available, skipping rule.", level=LogLevel.WARNING)
|
163
168
|
return True
|
164
169
|
|
165
|
-
self._console.print(f"Applying selector: {self._selector.__class__.__name__}", level=LogLevel.
|
170
|
+
self._console.print(f"Applying selector: {self._selector.__class__.__name__}", level=LogLevel.TRACE)
|
166
171
|
selected_nodes = self._selector.select(tree)
|
167
172
|
|
168
|
-
self._console.print(f"Applying constraint: {self._constraint.__class__.__name__}", level=LogLevel.
|
173
|
+
self._console.print(f"Applying constraint: {self._constraint.__class__.__name__}", level=LogLevel.TRACE)
|
169
174
|
return self._constraint.check(selected_nodes)
|