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/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,59 @@ 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,
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.INFO.
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
- level_str = level.value.lower() if isinstance(level, LogLevel) else level.lower()
86
- log_method = getattr(self._logger, level_str)
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._is_silent:
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.DEBUG)
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 flake8 linter...", level=LogLevel.DEBUG)
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.DEBUG)
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.ERROR)
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.DEBUG)
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.DEBUG)
173
+ self._console.print(f"Applying constraint: {self._constraint.__class__.__name__}", level=LogLevel.TRACE)
169
174
  return self._constraint.check(selected_nodes)