python-code-validator 0.1.3__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 +1 -1
- code_validator/cli.py +27 -17
- code_validator/components/factories.py +31 -13
- code_validator/config.py +3 -2
- code_validator/core.py +63 -33
- code_validator/output.py +160 -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.2.1.dist-info}/METADATA +3 -25
- python_code_validator-0.2.1.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.2.1.dist-info}/WHEEL +0 -0
- {python_code_validator-0.1.3.dist-info → python_code_validator-0.2.1.dist-info}/entry_points.txt +0 -0
- {python_code_validator-0.1.3.dist-info → python_code_validator-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {python_code_validator-0.1.3.dist-info → python_code_validator-0.2.1.dist-info}/top_level.txt +0 -0
code_validator/__init__.py
CHANGED
code_validator/cli.py
CHANGED
@@ -35,19 +35,22 @@ def setup_arg_parser() -> argparse.ArgumentParser:
|
|
35
35
|
prog="validate-code",
|
36
36
|
description="Validates a Python source file against a set of JSON rules.",
|
37
37
|
)
|
38
|
+
|
38
39
|
parser.add_argument("solution_path", type=Path, help="Path to the Python solution file to validate.")
|
39
40
|
parser.add_argument("rules_path", type=Path, help="Path to the JSON file with validation rules.")
|
41
|
+
|
40
42
|
parser.add_argument(
|
41
|
-
"
|
42
|
-
"--log-level",
|
43
|
+
"--log",
|
43
44
|
type=LogLevel,
|
44
|
-
|
45
|
-
|
46
|
-
help="Set the logging level (default: WARNING).",
|
45
|
+
default=LogLevel.ERROR,
|
46
|
+
help=("Set the logging level for stderr (TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL). Default: ERROR."),
|
47
47
|
)
|
48
|
-
parser.add_argument(
|
48
|
+
parser.add_argument(
|
49
|
+
"--quiet", action="store_true", help="Suppress all stdout output (validation errors and final verdict)."
|
50
|
+
)
|
51
|
+
parser.add_argument("--no-verdict", action="store_true", help="Suppress stdout output verdict, show failed rules.")
|
49
52
|
parser.add_argument("--stop-on-first-fail", action="store_true", help="Stop after the first failed rule.")
|
50
|
-
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
53
|
+
parser.add_argument("--version", "-v", action="version", version=f"%(prog)s {__version__}")
|
51
54
|
return parser
|
52
55
|
|
53
56
|
|
@@ -68,35 +71,42 @@ def run_from_cli() -> None:
|
|
68
71
|
parser = setup_arg_parser()
|
69
72
|
args = parser.parse_args()
|
70
73
|
|
71
|
-
logger = setup_logging(args.
|
72
|
-
console = Console(logger,
|
74
|
+
logger = setup_logging(args.log)
|
75
|
+
console = Console(logger, is_quiet=args.quiet, show_verdict=not args.no_verdict)
|
76
|
+
console.print(f"Level of logging: {args.log}", level=LogLevel.DEBUG)
|
73
77
|
config = AppConfig(
|
74
78
|
solution_path=args.solution_path,
|
75
79
|
rules_path=args.rules_path,
|
76
|
-
log_level=args.
|
77
|
-
|
80
|
+
log_level=args.log,
|
81
|
+
is_quiet=args.quiet,
|
78
82
|
stop_on_first_fail=args.stop_on_first_fail,
|
79
83
|
)
|
84
|
+
console.print(f"Config is: {config}", level=LogLevel.TRACE)
|
80
85
|
|
81
86
|
try:
|
82
87
|
console.print(f"Starting validation for: {config.solution_path}", level=LogLevel.INFO)
|
83
88
|
validator = StaticValidator(config, console)
|
89
|
+
|
90
|
+
console.print("Start of validation..", level=LogLevel.TRACE)
|
84
91
|
is_valid = validator.run()
|
92
|
+
console.print(f"End of validation with result: {is_valid = }", level=LogLevel.TRACE)
|
85
93
|
|
86
94
|
if is_valid:
|
87
|
-
console.print("Validation successful.", level=LogLevel.INFO)
|
95
|
+
console.print("Validation successful.", level=LogLevel.INFO, is_verdict=True)
|
88
96
|
sys.exit(ExitCode.SUCCESS)
|
89
97
|
else:
|
90
|
-
console.print("Validation failed.", level=LogLevel.
|
98
|
+
console.print("Validation failed.", level=LogLevel.INFO, is_verdict=True)
|
91
99
|
sys.exit(ExitCode.VALIDATION_FAILED)
|
92
100
|
|
93
101
|
except CodeValidatorError as e:
|
94
|
-
console.print(
|
102
|
+
console.print("Error: Internal Error of validator!", level=LogLevel.CRITICAL)
|
103
|
+
logger.exception(f"Traceback for CodeValidatorError: {e}")
|
95
104
|
sys.exit(ExitCode.VALIDATION_FAILED)
|
96
105
|
except FileNotFoundError as e:
|
97
|
-
console.print(f"Error: File not found - {e.
|
106
|
+
console.print(f"Error: File not found - {e.filename}!", level=LogLevel.CRITICAL)
|
107
|
+
logger.exception(f"Traceback for FileNotFoundError: {e}")
|
98
108
|
sys.exit(ExitCode.FILE_NOT_FOUND)
|
99
109
|
except Exception as e:
|
100
|
-
console.print(f"An unexpected error occurred: {e}", level=LogLevel.CRITICAL)
|
101
|
-
logger.exception("Traceback for unexpected error:")
|
110
|
+
console.print(f"An unexpected error occurred: {e.__class__.__name__}!", level=LogLevel.CRITICAL)
|
111
|
+
logger.exception(f"Traceback for unexpected error: {e}")
|
102
112
|
sys.exit(ExitCode.UNEXPECTED_ERROR)
|
@@ -10,9 +10,9 @@ rules file and instantiating the appropriate handler classes from the
|
|
10
10
|
import dataclasses
|
11
11
|
from typing import Any, Type, TypeVar
|
12
12
|
|
13
|
-
from ..config import ConstraintConfig, FullRuleCheck, FullRuleConfig, SelectorConfig, ShortRuleConfig
|
13
|
+
from ..config import ConstraintConfig, FullRuleCheck, FullRuleConfig, LogLevel, SelectorConfig, ShortRuleConfig
|
14
14
|
from ..exceptions import RuleParsingError
|
15
|
-
from ..output import Console
|
15
|
+
from ..output import Console, log_initialization
|
16
16
|
from ..rules_library.basic_rules import CheckLinterRule, CheckSyntaxRule, FullRuleHandler
|
17
17
|
from ..rules_library.constraint_logic import (
|
18
18
|
IsForbiddenConstraint,
|
@@ -71,6 +71,7 @@ class RuleFactory:
|
|
71
71
|
_constraint_factory (ConstraintFactory): A factory for creating constraint objects.
|
72
72
|
"""
|
73
73
|
|
74
|
+
@log_initialization(level=LogLevel.TRACE)
|
74
75
|
def __init__(self, console: Console):
|
75
76
|
"""Initializes the RuleFactory.
|
76
77
|
|
@@ -101,8 +102,10 @@ class RuleFactory:
|
|
101
102
|
required keys, or specifies an unknown type.
|
102
103
|
"""
|
103
104
|
rule_id = rule_config.get("rule_id")
|
105
|
+
self._console.print(f"Start parsing rule ({rule_id}):\n{rule_config}", level=LogLevel.TRACE)
|
104
106
|
try:
|
105
107
|
if "type" in rule_config:
|
108
|
+
self._console.print(f"Rule {rule_id} is shorted rule - {rule_config['type']}", level=LogLevel.DEBUG)
|
106
109
|
config = _create_dataclass_from_dict(ShortRuleConfig, rule_config)
|
107
110
|
return self._create_short_rule(config)
|
108
111
|
|
@@ -110,20 +113,31 @@ class RuleFactory:
|
|
110
113
|
raw_selector_cfg = rule_config["check"]["selector"]
|
111
114
|
raw_constraint_cfg = rule_config["check"]["constraint"]
|
112
115
|
|
113
|
-
selector = self._selector_factory.create(raw_selector_cfg)
|
114
|
-
constraint = self._constraint_factory.create(raw_constraint_cfg)
|
115
|
-
|
116
116
|
selector_cfg = _create_dataclass_from_dict(SelectorConfig, raw_selector_cfg)
|
117
117
|
constraint_cfg = _create_dataclass_from_dict(ConstraintConfig, raw_constraint_cfg)
|
118
|
+
|
119
|
+
selector = self._selector_factory.create(selector_cfg)
|
120
|
+
constraint = self._constraint_factory.create(constraint_cfg)
|
121
|
+
|
122
|
+
self._console.print(
|
123
|
+
f"Rule {rule_id} is general rule with: selector - "
|
124
|
+
f"{selector_cfg.type}, constraint - {raw_constraint_cfg['type']}",
|
125
|
+
level=LogLevel.DEBUG,
|
126
|
+
)
|
127
|
+
|
118
128
|
check_cfg = FullRuleCheck(selector=selector_cfg, constraint=constraint_cfg)
|
129
|
+
self._console.print(f"Create FullRuleCheck: {check_cfg}", level=LogLevel.TRACE)
|
130
|
+
|
119
131
|
config = FullRuleConfig(
|
120
132
|
rule_id=rule_config["rule_id"],
|
121
133
|
message=rule_config["message"],
|
122
134
|
check=check_cfg,
|
123
135
|
is_critical=rule_config.get("is_critical", False),
|
124
136
|
)
|
137
|
+
self._console.print(f"Create FullRuleConfig: {config}", level=LogLevel.TRACE)
|
125
138
|
return FullRuleHandler(config, selector, constraint, self._console)
|
126
139
|
else:
|
140
|
+
self._console.print(f"Invalid syntax of rule: {rule_id}", level=LogLevel.WARNING)
|
127
141
|
raise RuleParsingError("Rule must contain 'type' or 'check' key.", rule_id)
|
128
142
|
except (TypeError, KeyError, RuleParsingError) as e:
|
129
143
|
raise RuleParsingError(f"Invalid config for rule '{rule_id}': {e}", rule_id) from e
|
@@ -165,21 +179,23 @@ class SelectorFactory:
|
|
165
179
|
any state.
|
166
180
|
"""
|
167
181
|
|
182
|
+
@log_initialization(level=LogLevel.TRACE)
|
183
|
+
def __init__(self) -> None:
|
184
|
+
pass
|
185
|
+
|
168
186
|
@staticmethod
|
169
|
-
def create(
|
187
|
+
def create(config: SelectorConfig) -> Selector:
|
170
188
|
"""Creates a specific selector instance based on its type.
|
171
189
|
|
172
190
|
This method uses the 'type' field from the selector configuration
|
173
191
|
to determine which concrete Selector class to instantiate.
|
174
192
|
|
175
193
|
Args:
|
176
|
-
|
194
|
+
config: The 'selector' block from a JSON rule.
|
177
195
|
|
178
196
|
Returns:
|
179
197
|
An instance of a class that conforms to the Selector protocol.
|
180
198
|
"""
|
181
|
-
config = _create_dataclass_from_dict(SelectorConfig, selector_config)
|
182
|
-
|
183
199
|
match config.type:
|
184
200
|
case "function_def":
|
185
201
|
return FunctionDefSelector(name=config.name, in_scope_config=config.in_scope)
|
@@ -210,21 +226,23 @@ class ConstraintFactory:
|
|
210
226
|
to a list of AST nodes. This class uses a static `create` method.
|
211
227
|
"""
|
212
228
|
|
229
|
+
@log_initialization(level=LogLevel.TRACE)
|
230
|
+
def __init__(self) -> None:
|
231
|
+
pass
|
232
|
+
|
213
233
|
@staticmethod
|
214
|
-
def create(
|
234
|
+
def create(config: ConstraintConfig) -> Constraint:
|
215
235
|
"""Creates a specific constraint instance based on its type.
|
216
236
|
|
217
237
|
This method uses the 'type' field from the constraint configuration
|
218
238
|
to determine which concrete Constraint class to instantiate.
|
219
239
|
|
220
240
|
Args:
|
221
|
-
|
241
|
+
config: The 'constraint' block from a JSON rule.
|
222
242
|
|
223
243
|
Returns:
|
224
244
|
An instance of a class that conforms to the Constraint protocol.
|
225
245
|
"""
|
226
|
-
config = _create_dataclass_from_dict(ConstraintConfig, constraint_config)
|
227
|
-
|
228
246
|
match config.type:
|
229
247
|
case "is_required":
|
230
248
|
return IsRequiredConstraint(count=config.count)
|
code_validator/config.py
CHANGED
@@ -25,6 +25,7 @@ class ExitCode(IntEnum):
|
|
25
25
|
class LogLevel(StrEnum):
|
26
26
|
"""Defines the supported logging levels for the application."""
|
27
27
|
|
28
|
+
TRACE = "TRACE"
|
28
29
|
DEBUG = "DEBUG"
|
29
30
|
INFO = "INFO"
|
30
31
|
WARNING = "WARNING"
|
@@ -40,14 +41,14 @@ class AppConfig:
|
|
40
41
|
solution_path: The file path to the Python solution to be validated.
|
41
42
|
rules_path: The file path to the JSON rules file.
|
42
43
|
log_level: The minimum logging level for console output.
|
43
|
-
|
44
|
+
is_quiet: If True, suppresses all non-log output to stdout.
|
44
45
|
stop_on_first_fail: If True, halts validation after the first failed rule.
|
45
46
|
"""
|
46
47
|
|
47
48
|
solution_path: Path
|
48
49
|
rules_path: Path
|
49
50
|
log_level: LogLevel
|
50
|
-
|
51
|
+
is_quiet: bool
|
51
52
|
stop_on_first_fail: bool
|
52
53
|
|
53
54
|
|
code_validator/core.py
CHANGED
@@ -45,9 +45,9 @@ import json
|
|
45
45
|
from .components.ast_utils import enrich_ast_with_parents
|
46
46
|
from .components.definitions import Rule
|
47
47
|
from .components.factories import RuleFactory
|
48
|
-
from .config import AppConfig, LogLevel
|
48
|
+
from .config import AppConfig, LogLevel, ShortRuleConfig
|
49
49
|
from .exceptions import RuleParsingError
|
50
|
-
from .output import Console
|
50
|
+
from .output import Console, log_initialization
|
51
51
|
|
52
52
|
|
53
53
|
class StaticValidator:
|
@@ -67,6 +67,7 @@ class StaticValidator:
|
|
67
67
|
_failed_rules (list[int]): A list of rule IDs that failed during the run.
|
68
68
|
"""
|
69
69
|
|
70
|
+
@log_initialization(level=LogLevel.DEBUG)
|
70
71
|
def __init__(self, config: AppConfig, console: Console):
|
71
72
|
"""Initializes the StaticValidator.
|
72
73
|
|
@@ -77,6 +78,7 @@ class StaticValidator:
|
|
77
78
|
"""
|
78
79
|
self._config = config
|
79
80
|
self._console = console
|
81
|
+
|
80
82
|
self._rule_factory = RuleFactory(self._console)
|
81
83
|
self._source_code: str = ""
|
82
84
|
self._ast_tree: ast.Module | None = None
|
@@ -95,39 +97,17 @@ class StaticValidator:
|
|
95
97
|
FileNotFoundError: If the source file specified in the config does not exist.
|
96
98
|
RuleParsingError: If the source file cannot be read for any other reason.
|
97
99
|
"""
|
98
|
-
self._console.print(f"Reading source file: {self._config.solution_path}")
|
100
|
+
self._console.print(f"Reading source file: {self._config.solution_path}", level=LogLevel.DEBUG)
|
99
101
|
try:
|
100
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)
|
101
104
|
except FileNotFoundError:
|
105
|
+
self._console.print("During reading source file raised FileNotFound", level=LogLevel.TRACE)
|
102
106
|
raise
|
103
107
|
except Exception as e:
|
108
|
+
self._console.print("During reading source file raised some exception..", level=LogLevel.TRACE)
|
104
109
|
raise RuleParsingError(f"Cannot read source file: {e}") from e
|
105
110
|
|
106
|
-
def _parse_ast_tree(self) -> bool:
|
107
|
-
"""Parses the loaded source code into an AST and enriches it.
|
108
|
-
|
109
|
-
This method attempts to parse the source code. If successful, it calls
|
110
|
-
a helper to add parent references to each node in the tree, which is
|
111
|
-
crucial for many advanced checks. If a `SyntaxError` occurs, it
|
112
|
-
checks if a `check_syntax` rule was defined to provide a custom message.
|
113
|
-
|
114
|
-
Returns:
|
115
|
-
bool: True if parsing was successful, False otherwise.
|
116
|
-
"""
|
117
|
-
self._console.print("Parsing Abstract Syntax Tree (AST)...")
|
118
|
-
try:
|
119
|
-
self._ast_tree = ast.parse(self._source_code)
|
120
|
-
enrich_ast_with_parents(self._ast_tree)
|
121
|
-
return True
|
122
|
-
except SyntaxError as e:
|
123
|
-
for rule in self._rules:
|
124
|
-
if getattr(rule.config, "type", None) == "check_syntax":
|
125
|
-
self._console.print(rule.config.message, level=LogLevel.ERROR)
|
126
|
-
self._failed_rules.append(rule.config.rule_id)
|
127
|
-
return False
|
128
|
-
self._console.print(f"Syntax Error found: {e}", level=LogLevel.ERROR)
|
129
|
-
return False
|
130
|
-
|
131
111
|
def _load_and_parse_rules(self) -> None:
|
132
112
|
"""Loads and parses the JSON file into executable Rule objects.
|
133
113
|
|
@@ -140,20 +120,52 @@ class StaticValidator:
|
|
140
120
|
RuleParsingError: If the JSON is malformed or a rule configuration
|
141
121
|
is invalid.
|
142
122
|
"""
|
143
|
-
self._console.print(f"Loading rules from: {self._config.rules_path}")
|
123
|
+
self._console.print(f"Loading rules from: {self._config.rules_path}", level=LogLevel.DEBUG)
|
144
124
|
try:
|
145
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)
|
146
127
|
raw_rules = rules_data.get("validation_rules")
|
147
128
|
if not isinstance(raw_rules, list):
|
148
129
|
raise RuleParsingError("`validation_rules` key not found or is not a list.")
|
149
130
|
|
131
|
+
self._console.print(f"Found {len(raw_rules)}.", level=LogLevel.DEBUG)
|
150
132
|
self._rules = [self._rule_factory.create(rule) for rule in raw_rules]
|
151
|
-
self._console.print(f"Successfully parsed {len(self._rules)} rules.")
|
133
|
+
self._console.print(f"Successfully parsed {len(self._rules)} rules.", level=LogLevel.DEBUG)
|
152
134
|
except json.JSONDecodeError as e:
|
135
|
+
self._console.print("During reading file of rules raised JsonDecodeError..", level=LogLevel.TRACE)
|
153
136
|
raise RuleParsingError(f"Invalid JSON in rules file: {e}") from e
|
154
137
|
except FileNotFoundError:
|
138
|
+
self._console.print("During reading file of rules raised FileNotFound", level=LogLevel.TRACE)
|
155
139
|
raise
|
156
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
|
+
|
157
169
|
def run(self) -> bool:
|
158
170
|
"""Runs the entire validation process from start to finish.
|
159
171
|
|
@@ -174,20 +186,38 @@ class StaticValidator:
|
|
174
186
|
if not self._parse_ast_tree():
|
175
187
|
return False
|
176
188
|
|
177
|
-
|
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
|
+
)
|
178
195
|
raise
|
179
196
|
|
197
|
+
self._console.print("Starting check rules..", level=LogLevel.DEBUG)
|
180
198
|
for rule in self._rules:
|
181
199
|
if getattr(rule.config, "type", None) == "check_syntax":
|
182
200
|
continue
|
183
201
|
|
184
|
-
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
|
+
)
|
185
212
|
is_passed = rule.execute(self._ast_tree, self._source_code)
|
186
213
|
if not is_passed:
|
187
|
-
self._console.print(rule.config.message, level=LogLevel.
|
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)
|
188
216
|
self._failed_rules.append(rule.config.rule_id)
|
189
217
|
if getattr(rule.config, "is_critical", False) or self._config.stop_on_first_fail:
|
190
218
|
self._console.print("Critical rule failed. Halting validation.", level=LogLevel.WARNING)
|
191
219
|
break
|
220
|
+
else:
|
221
|
+
self._console.print(f"Rule {rule.config.rule_id} - PASS", level=LogLevel.INFO)
|
192
222
|
|
193
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)
|