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.
@@ -54,4 +54,4 @@ __all__ = [
54
54
  "RuleParsingError",
55
55
  ]
56
56
 
57
- __version__ = "0.1.3"
57
+ __version__ = "0.2.1"
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
- "-l",
42
- "--log-level",
43
+ "--log",
43
44
  type=LogLevel,
44
- choices=list(LogLevel),
45
- default=LogLevel.WARNING,
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("--silent", action="store_true", help="Suppress stdout output, show only logs.")
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.log_level)
72
- console = Console(logger, is_silent=args.silent)
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.log_level,
77
- is_silent=args.silent,
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.ERROR)
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(str(e), level=LogLevel.CRITICAL)
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.strerror}: {e.filename}", level=LogLevel.CRITICAL)
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(selector_config: dict[str, Any]) -> Selector:
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
- selector_config: The 'selector' block from a JSON rule.
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(constraint_config: dict[str, Any]) -> Constraint:
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
- constraint_config: The 'constraint' block from a JSON rule.
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
- is_silent: If True, suppresses all non-log output to stdout.
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
- is_silent: bool
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
- except (FileNotFoundError, RuleParsingError):
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(f"Executing rule: {rule.config.rule_id}", level=LogLevel.DEBUG)
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.ERROR)
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 typing import Literal
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.basicConfig(level=log_level, format=LOG_FORMAT, datefmt=DATE_FORMAT)
31
- return logging.getLogger()
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
- _is_silent (bool): A flag to suppress printing to stdout.
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
- def __init__(self, logger: logging.Logger, *, is_silent: bool = False):
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
- is_silent: If True, suppresses output to stdout. Defaults to False.
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._is_silent = is_silent
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.INFO,
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.INFO.
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
- level_str = level.value.lower() if isinstance(level, LogLevel) else level.lower()
86
- log_method = getattr(self._logger, level_str)
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._is_silent:
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)