python-code-validator 0.1.2__py3-none-any.whl → 0.1.3__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.
@@ -1,17 +1,47 @@
1
1
  """A flexible framework for static validation of Python code.
2
2
 
3
- This package provides a comprehensive toolkit for statically analyzing Python source
4
- code based on a declarative set of rules defined in a JSON format. It allows
5
- for checking syntax, style, structure, and constraints without executing the code.
6
-
7
- Key components exposed by this package include:
8
- - StaticValidator: The main orchestrator for running the validation process.
9
- - AppConfig: A dataclass for configuring the validator's behavior.
10
- - ExitCode: An Enum defining exit codes for CLI operations.
11
- - Custom Exceptions: For fine-grained error handling during validation.
3
+ This package provides a comprehensive toolkit for statically analyzing Python
4
+ source code based on a declarative set of rules defined in a JSON format. It
5
+ allows for checking syntax, style, structure, and constraints without
6
+ executing the code.
7
+
8
+ The primary entry point for using this package programmatically is the
9
+ `StaticValidator` class.
10
+
11
+ Example:
12
+ A minimal example of using the validator as a library.
13
+
14
+ .. code-block:: python
15
+
16
+ from code_validator import StaticValidator, AppConfig, LogLevel
17
+ from code_validator.output import Console, setup_logging
18
+ from pathlib import Path
19
+
20
+ # Basic setup
21
+ logger = setup_logging(LogLevel.INFO)
22
+ console = Console(logger)
23
+ config = AppConfig(
24
+ solution_path=Path("path/to/solution.py"),
25
+ rules_path=Path("path/to/rules.json"),
26
+ log_level=LogLevel.INFO,
27
+ is_silent=False,
28
+ stop_on_first_fail=False
29
+ )
30
+
31
+ # Run validation
32
+ validator = StaticValidator(config, console)
33
+ is_valid = validator.run()
34
+
35
+ if is_valid:
36
+ print("Validation Passed!")
37
+
38
+ Attributes:
39
+ __version__ (str): The current version of the package.
40
+ __all__ (list[str]): The list of public objects exposed by the package.
41
+
12
42
  """
13
43
 
14
- from .config import AppConfig, ExitCode
44
+ from .config import AppConfig, ExitCode, LogLevel
15
45
  from .core import StaticValidator
16
46
  from .exceptions import RuleParsingError, ValidationFailedError
17
47
 
@@ -19,7 +49,9 @@ __all__ = [
19
49
  "StaticValidator",
20
50
  "AppConfig",
21
51
  "ExitCode",
52
+ "LogLevel",
22
53
  "ValidationFailedError",
23
54
  "RuleParsingError",
24
55
  ]
25
- __version__ = "0.1.2"
56
+
57
+ __version__ = "0.1.3"
@@ -1,8 +1,17 @@
1
- """Enables running the validator as a module.
1
+ """Enables running the validator as a package.
2
2
 
3
3
  This file allows the package to be executed directly from the command line
4
- using `python -m code_validator`. It serves as the main entry point
5
- that invokes the command-line interface logic.
4
+ using the ``-m`` flag with Python (e.g., ``python -m code_validator``). It
5
+ serves as the primary entry point that finds and invokes the command-line
6
+ interface logic defined in the `cli` module.
7
+
8
+ Example:
9
+ You can run the validator package like this from the project root:
10
+
11
+ .. code-block:: bash
12
+
13
+ python -m code_validator path/to/solution.py path/to/rules.json
14
+
6
15
  """
7
16
 
8
17
  from .cli import run_from_cli
code_validator/cli.py CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  This module is responsible for parsing command-line arguments, setting up the
4
4
  application configuration, and orchestrating the main validation workflow. It acts
5
- as the primary entry point for user interaction.
5
+ as the primary entry point for user interaction when the tool is called from
6
+ the terminal.
7
+
8
+ The main function, `run_from_cli`, handles the entire application lifecycle,
9
+ including robust top-level error handling to ensure meaningful exit codes.
6
10
  """
7
11
 
8
12
  import argparse
@@ -19,8 +23,13 @@ from .output import Console, setup_logging
19
23
  def setup_arg_parser() -> argparse.ArgumentParser:
20
24
  """Creates and configures the argument parser for the CLI.
21
25
 
26
+ This function defines all positional and optional arguments that the
27
+ `validate-code` command accepts, including their types, help messages,
28
+ and default values.
29
+
22
30
  Returns:
23
- An instance of argparse.ArgumentParser with all arguments defined.
31
+ argparse.ArgumentParser: A fully configured parser instance ready to
32
+ process command-line arguments.
24
33
  """
25
34
  parser = argparse.ArgumentParser(
26
35
  prog="validate-code",
@@ -32,7 +41,7 @@ def setup_arg_parser() -> argparse.ArgumentParser:
32
41
  "-l",
33
42
  "--log-level",
34
43
  type=LogLevel,
35
- choices=LogLevel,
44
+ choices=list(LogLevel),
36
45
  default=LogLevel.WARNING,
37
46
  help="Set the logging level (default: WARNING).",
38
47
  )
@@ -45,14 +54,20 @@ def setup_arg_parser() -> argparse.ArgumentParser:
45
54
  def run_from_cli() -> None:
46
55
  """Runs the full application lifecycle from the command line.
47
56
 
48
- This function parses arguments, initializes logging and configuration,
49
- runs the validator, and handles all top-level exceptions, exiting with an
50
- appropriate exit code.
57
+ This is the main entry point for the `validate-code` script. It performs
58
+ the following steps:
59
+ 1. Parses command-line arguments.
60
+ 2. Initializes the logger, console, and configuration.
61
+ 3. Instantiates and runs the `StaticValidator`.
62
+ 4. Handles all top-level exceptions and exits with an appropriate status code.
63
+
64
+ Raises:
65
+ SystemExit: This function will always terminate the process with an
66
+ exit code defined in the `ExitCode` enum.
51
67
  """
52
68
  parser = setup_arg_parser()
53
69
  args = parser.parse_args()
54
70
 
55
- # 1. Setup environment
56
71
  logger = setup_logging(args.log_level)
57
72
  console = Console(logger, is_silent=args.silent)
58
73
  config = AppConfig(
@@ -63,7 +78,6 @@ def run_from_cli() -> None:
63
78
  stop_on_first_fail=args.stop_on_first_fail,
64
79
  )
65
80
 
66
- # 2. Run main logic with robust error handling
67
81
  try:
68
82
  console.print(f"Starting validation for: {config.solution_path}", level=LogLevel.INFO)
69
83
  validator = StaticValidator(config, console)
@@ -1,4 +1,9 @@
1
- """Provides utility functions for working with Python's Abstract Syntax Trees (AST)."""
1
+ """Provides utility functions for working with Python's Abstract Syntax Trees (AST).
2
+
3
+ This module contains helper functions that perform common operations on AST nodes,
4
+ such as enriching the tree with parent references. These utilities are used by
5
+ various components of the validator to simplify complex tree analysis.
6
+ """
2
7
 
3
8
  import ast
4
9
 
@@ -15,7 +20,6 @@ def enrich_ast_with_parents(tree: ast.Module) -> None:
15
20
  """
16
21
  for node in ast.walk(tree):
17
22
  for child in ast.iter_child_nodes(node):
18
- # Dynamically add a reference to the parent node.
19
23
  child.parent = node
20
24
 
21
25
 
@@ -1,10 +1,11 @@
1
- """Defines the core component interfaces using Protocols.
1
+ """Defines the core component interfaces for the validator using Protocols.
2
2
 
3
3
  This module establishes the fundamental "contracts" for the main architectural
4
- components of the validator: Rules, Selectors, and Constraints. By using
5
- Protocols, we ensure that any class conforming to these interfaces can be used
6
- interchangeably by the system's factories and core engine, enabling a flexible
7
- and decoupled plugin-style architecture.
4
+ components: Rules, Selectors, and Constraints. By using `typing.Protocol`, we
5
+ ensure that any class conforming to these interfaces can be used interchangeably
6
+ by the system's factories and core engine. This enables a flexible and
7
+ decoupled plugin-style architecture, where new components can be added without
8
+ modifying the core logic.
8
9
  """
9
10
 
10
11
  import ast
@@ -1,9 +1,10 @@
1
1
  """Contains factories for creating rule, selector, and constraint objects.
2
2
 
3
3
  This module implements the Factory Method design pattern to decouple the core
4
- validator engine from the concrete implementation of rules. Factories are
5
- responsible for parsing raw dictionary configurations from JSON and instantiating
6
- the appropriate handler classes.
4
+ validator engine from the concrete implementations of its components. Factories
5
+ are responsible for parsing raw dictionary configurations from the main JSON
6
+ rules file and instantiating the appropriate handler classes from the
7
+ `rules_library`.
7
8
  """
8
9
 
9
10
  import dataclasses
@@ -1,8 +1,10 @@
1
1
  """Provides functionality to find and isolate specific scopes within an AST.
2
2
 
3
- This module contains helper functions that are used by ScopedSelectors to narrow
4
- down their search area from the entire module to a specific function, class,
5
- or method, based on the `in_scope` configuration from a JSON rule.
3
+ This module contains a key helper function, `find_scope_node`, which is used
4
+ by `ScopedSelector` instances. Its purpose is to traverse the AST and return
5
+ a specific subtree (e.g., a function body or class body) based on the
6
+ `in_scope` configuration from a JSON rule. This allows rules to be applied
7
+ with high precision to specific parts of the source code.
6
8
  """
7
9
 
8
10
  import ast
code_validator/config.py CHANGED
@@ -34,7 +34,15 @@ class LogLevel(StrEnum):
34
34
 
35
35
  @dataclass(frozen=True)
36
36
  class AppConfig:
37
- """Stores the main application configuration from CLI arguments."""
37
+ """Stores the main application configuration from CLI arguments.
38
+
39
+ Attributes:
40
+ solution_path: The file path to the Python solution to be validated.
41
+ rules_path: The file path to the JSON rules file.
42
+ log_level: The minimum logging level for console output.
43
+ is_silent: If True, suppresses all non-log output to stdout.
44
+ stop_on_first_fail: If True, halts validation after the first failed rule.
45
+ """
38
46
 
39
47
  solution_path: Path
40
48
  rules_path: Path
@@ -45,7 +53,18 @@ class AppConfig:
45
53
 
46
54
  @dataclass(frozen=True)
47
55
  class SelectorConfig:
48
- """Represents the configuration for a Selector component from a JSON rule."""
56
+ """Represents the configuration for a Selector component from a JSON rule.
57
+
58
+ This dataclass captures all possible keys within the "selector" block
59
+ of a JSON validation rule.
60
+
61
+ Attributes:
62
+ type: The type of the selector to be used (e.g., "function_def").
63
+ name: A generic name parameter used by many selectors (e.g., the name
64
+ of a function, class, or module).
65
+ node_type: The AST node type name for the `ast_node` selector.
66
+ in_scope: The scope in which to apply the selector.
67
+ """
49
68
 
50
69
  type: str
51
70
  name: str | None = None
@@ -55,7 +74,21 @@ class SelectorConfig:
55
74
 
56
75
  @dataclass(frozen=True)
57
76
  class ConstraintConfig:
58
- """Represents the configuration for a Constraint component from a JSON rule."""
77
+ """Represents the configuration for a Constraint component from a JSON rule.
78
+
79
+ This dataclass captures all possible keys within the "constraint" block
80
+ of a JSON validation rule.
81
+
82
+ Attributes:
83
+ type: The type of the constraint to be applied (e.g., "is_required").
84
+ count: The exact number of nodes expected. Used by `is_required`.
85
+ parent_name: The expected parent class name. Used by `must_inherit_from`.
86
+ expected_type: The expected Python type name. Used by `must_be_type`.
87
+ allowed_names: A list of permitted names. Used by `name_must_be_in`.
88
+ allowed_values: A list of permitted literal values. Used by `value_must_be_in`.
89
+ names: A list of expected argument names. Used by `must_have_args`.
90
+ exact_match: A boolean flag for argument matching. Used by `must_have_args`.
91
+ """
59
92
 
60
93
  type: str
61
94
  count: int | None = None
@@ -69,7 +102,12 @@ class ConstraintConfig:
69
102
 
70
103
  @dataclass(frozen=True)
71
104
  class FullRuleCheck:
72
- """Represents the 'check' block within a full validation rule."""
105
+ """Represents the 'check' block within a full validation rule.
106
+
107
+ Attributes:
108
+ selector: The configuration for the selector component.
109
+ constraint: The configuration for the constraint component.
110
+ """
73
111
 
74
112
  selector: SelectorConfig
75
113
  constraint: ConstraintConfig
@@ -77,7 +115,14 @@ class FullRuleCheck:
77
115
 
78
116
  @dataclass(frozen=True)
79
117
  class ShortRuleConfig:
80
- """Represents a 'short' (pre-defined) validation rule from JSON."""
118
+ """Represents a 'short' (pre-defined) validation rule from JSON.
119
+
120
+ Attributes:
121
+ rule_id: The unique integer identifier for the rule.
122
+ type: The string identifier for the short rule (e.g., "check_syntax").
123
+ message: The error message to display if the rule fails.
124
+ params: A dictionary of optional parameters for the rule.
125
+ """
81
126
 
82
127
  rule_id: int
83
128
  type: str
@@ -87,7 +132,14 @@ class ShortRuleConfig:
87
132
 
88
133
  @dataclass(frozen=True)
89
134
  class FullRuleConfig:
90
- """Represents a 'full' (custom) validation rule with selector and constraint."""
135
+ """Represents a 'full' (custom) validation rule with a selector and constraint.
136
+
137
+ Attributes:
138
+ rule_id: The unique integer identifier for the rule.
139
+ message: The error message to display if the rule fails.
140
+ check: An object containing the selector and constraint configurations.
141
+ is_critical: If True, validation halts if this rule fails.
142
+ """
91
143
 
92
144
  rule_id: int
93
145
  message: str
code_validator/core.py CHANGED
@@ -1,3 +1,44 @@
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
 
@@ -10,25 +51,50 @@ from .output import Console
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
 
15
70
  def __init__(self, config: AppConfig, console: Console):
16
- """Initializes the validator with configuration and an output handler."""
71
+ """Initializes the StaticValidator.
72
+
73
+ Args:
74
+ config: An `AppConfig` object containing all necessary run
75
+ configurations, such as file paths and flags.
76
+ console: A `Console` object for handling all output.
77
+ """
17
78
  self._config = config
18
79
  self._console = console
19
80
  self._rule_factory = RuleFactory(self._console)
20
81
  self._source_code: str = ""
21
82
  self._ast_tree: ast.Module | None = None
22
- self._validation_rules: list[Rule] = []
83
+ self._rules: list[Rule] = []
23
84
  self._failed_rules: list[int] = []
24
85
 
25
86
  @property
26
87
  def failed_rules_id(self) -> list[int]:
27
- """Returns a list of rule IDs that failed during the last run."""
88
+ """list[int]: A list of rule IDs that failed during the last run."""
28
89
  return self._failed_rules
29
90
 
30
91
  def _load_source_code(self) -> None:
31
- """Loads the content of the student's solution file."""
92
+ """Loads the content of the student's solution file into memory.
93
+
94
+ Raises:
95
+ FileNotFoundError: If the source file specified in the config does not exist.
96
+ RuleParsingError: If the source file cannot be read for any other reason.
97
+ """
32
98
  self._console.print(f"Reading source file: {self._config.solution_path}")
33
99
  try:
34
100
  self._source_code = self._config.solution_path.read_text(encoding="utf-8")
@@ -38,25 +104,42 @@ class StaticValidator:
38
104
  raise RuleParsingError(f"Cannot read source file: {e}") from e
39
105
 
40
106
  def _parse_ast_tree(self) -> bool:
41
- """Parses the loaded source code into an AST."""
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
+ """
42
117
  self._console.print("Parsing Abstract Syntax Tree (AST)...")
43
118
  try:
44
119
  self._ast_tree = ast.parse(self._source_code)
45
120
  enrich_ast_with_parents(self._ast_tree)
46
121
  return True
47
122
  except SyntaxError as e:
48
- # Ищем правило check_syntax, чтобы вывести его кастомное сообщение
49
- for rule in self._validation_rules:
123
+ for rule in self._rules:
50
124
  if getattr(rule.config, "type", None) == "check_syntax":
51
- self._console.print(rule.config.message, level="ERROR")
125
+ self._console.print(rule.config.message, level=LogLevel.ERROR)
52
126
  self._failed_rules.append(rule.config.rule_id)
53
127
  return False
54
- # Если такого правила нет, выводим стандартное сообщение
55
- self._console.print(f"Syntax Error found: {e}", level="ERROR")
128
+ self._console.print(f"Syntax Error found: {e}", level=LogLevel.ERROR)
56
129
  return False
57
130
 
58
131
  def _load_and_parse_rules(self) -> None:
59
- """Loads and parses the JSON file with validation rules."""
132
+ """Loads and parses the JSON file into executable Rule objects.
133
+
134
+ This method reads the JSON rules file, validates its basic structure,
135
+ and then uses the `RuleFactory` to instantiate a list of concrete
136
+ Rule objects.
137
+
138
+ Raises:
139
+ FileNotFoundError: If the rules file does not exist.
140
+ RuleParsingError: If the JSON is malformed or a rule configuration
141
+ is invalid.
142
+ """
60
143
  self._console.print(f"Loading rules from: {self._config.rules_path}")
61
144
  try:
62
145
  rules_data = json.loads(self._config.rules_path.read_text(encoding="utf-8"))
@@ -64,18 +147,29 @@ class StaticValidator:
64
147
  if not isinstance(raw_rules, list):
65
148
  raise RuleParsingError("`validation_rules` key not found or is not a list.")
66
149
 
67
- self._validation_rules = [self._rule_factory.create(rule) for rule in raw_rules]
68
- self._console.print(f"Successfully parsed {len(self._validation_rules)} rules.")
150
+ self._rules = [self._rule_factory.create(rule) for rule in raw_rules]
151
+ self._console.print(f"Successfully parsed {len(self._rules)} rules.")
69
152
  except json.JSONDecodeError as e:
70
153
  raise RuleParsingError(f"Invalid JSON in rules file: {e}") from e
71
154
  except FileNotFoundError:
72
155
  raise
73
156
 
74
157
  def run(self) -> bool:
75
- """Runs the entire validation process."""
158
+ """Runs the entire validation process from start to finish.
159
+
160
+ This is the main public method of the class. It orchestrates the
161
+ sequence of loading, parsing, and rule execution.
162
+
163
+ Returns:
164
+ bool: True if all validation rules passed, False otherwise.
165
+
166
+ Raises:
167
+ RuleParsingError: Propagated from loading/parsing steps.
168
+ FileNotFoundError: Propagated from loading steps.
169
+ """
76
170
  try:
77
171
  self._load_source_code()
78
- self._load_and_parse_rules() # Загружаем правила до парсинга AST
172
+ self._load_and_parse_rules()
79
173
 
80
174
  if not self._parse_ast_tree():
81
175
  return False
@@ -83,18 +177,17 @@ class StaticValidator:
83
177
  except (FileNotFoundError, RuleParsingError):
84
178
  raise
85
179
 
86
- for rule in self._validation_rules:
87
- # check_syntax уже обработан в _parse_ast_tree, пропускаем его
180
+ for rule in self._rules:
88
181
  if getattr(rule.config, "type", None) == "check_syntax":
89
182
  continue
90
183
 
91
184
  self._console.print(f"Executing rule: {rule.config.rule_id}", level=LogLevel.DEBUG)
92
185
  is_passed = rule.execute(self._ast_tree, self._source_code)
93
186
  if not is_passed:
94
- self._console.print(rule.config.message, level="ERROR")
187
+ self._console.print(rule.config.message, level=LogLevel.ERROR)
95
188
  self._failed_rules.append(rule.config.rule_id)
96
189
  if getattr(rule.config, "is_critical", False) or self._config.stop_on_first_fail:
97
- self._console.print("Critical rule failed. Halting validation.", level="WARNING")
190
+ self._console.print("Critical rule failed. Halting validation.", level=LogLevel.WARNING)
98
191
  break
99
192
 
100
193
  return not self._failed_rules
@@ -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 implements the `Rule`
7
- protocol and encapsulates the logic for a specific type of validation check.
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
@@ -77,10 +77,10 @@ class CheckLinterRule(Rule):
77
77
  True if no PEP8 violations are found, False otherwise.
78
78
  """
79
79
  if not source_code:
80
- self._console.print("Source code is empty, skipping PEP8 check.", level="WARNING")
80
+ self._console.print("Source code is empty, skipping PEP8 check.", level=LogLevel.WARNING)
81
81
  return True
82
82
 
83
- self._console.print(f"Rule {self.config.rule_id}: Running flake8 linter...", level="DEBUG")
83
+ self._console.print(f"Rule {self.config.rule_id}: Running flake8 linter...", level=LogLevel.DEBUG)
84
84
 
85
85
  params = self.config.params
86
86
  args = [sys.executable, "-m", "flake8", "-"]
@@ -102,19 +102,21 @@ class CheckLinterRule(Rule):
102
102
 
103
103
  if process.returncode != 0 and process.stdout:
104
104
  linter_output = process.stdout.strip()
105
- self._console.print(f"Flake8 found issues:\n{linter_output}", level="DEBUG")
105
+ self._console.print(f"Flake8 found issues:\n{linter_output}", level=LogLevel.DEBUG)
106
106
  return False
107
107
  elif process.returncode != 0:
108
- self._console.print(f"Flake8 exited with code {process.returncode}:\n{process.stderr}", level="ERROR")
108
+ self._console.print(
109
+ f"Flake8 exited with code {process.returncode}:\n{process.stderr}", level=LogLevel.ERROR
110
+ )
109
111
  return False
110
112
 
111
- self._console.print("PEP8 check passed.", level="DEBUG")
113
+ self._console.print("PEP8 check passed.", level=LogLevel.ERROR)
112
114
  return True
113
115
  except FileNotFoundError:
114
- self._console.print("flake8 not found. Is it installed in the venv?", level="CRITICAL")
116
+ self._console.print("flake8 not found. Is it installed in the venv?", level=LogLevel.CRITICAL)
115
117
  return False
116
118
  except Exception as e:
117
- self._console.print(f"An unexpected error occurred while running flake8: {e}", level="CRITICAL")
119
+ self._console.print(f"An unexpected error occurred while running flake8: {e}", level=LogLevel.CRITICAL)
118
120
  return False
119
121
 
120
122
 
@@ -157,11 +159,11 @@ class FullRuleHandler(Rule):
157
159
  The boolean result of applying the constraint to the selected nodes.
158
160
  """
159
161
  if not tree:
160
- self._console.print("AST not available, skipping rule.", level="WARNING")
162
+ self._console.print("AST not available, skipping rule.", level=LogLevel.WARNING)
161
163
  return True
162
164
 
163
- self._console.print(f"Applying selector: {self._selector.__class__.__name__}", level="DEBUG")
165
+ self._console.print(f"Applying selector: {self._selector.__class__.__name__}", level=LogLevel.DEBUG)
164
166
  selected_nodes = self._selector.select(tree)
165
167
 
166
- self._console.print(f"Applying constraint: {self._constraint.__class__.__name__}", level="DEBUG")
168
+ self._console.print(f"Applying constraint: {self._constraint.__class__.__name__}", level=LogLevel.DEBUG)
167
169
  return self._constraint.check(selected_nodes)
@@ -2,7 +2,11 @@
2
2
 
3
3
  Each class in this module implements the `Constraint` protocol and encapsulates
4
4
  the logic for a specific condition that can be checked against a list of
5
- AST nodes found by a Selector.
5
+ AST nodes. These classes are instantiated by the `ConstraintFactory` based on
6
+ the "constraint" block in a JSON rule.
7
+
8
+ The module also includes helper functions for processing AST nodes, which are
9
+ used internally by the constraint classes.
6
10
  """
7
11
 
8
12
  import ast
@@ -66,6 +70,12 @@ class MustInheritFromConstraint(Constraint):
66
70
  """
67
71
 
68
72
  def __init__(self, **kwargs: Any):
73
+ """Initializes the constraint.
74
+
75
+ Args:
76
+ **kwargs: Keyword arguments from the JSON rule's constraint config.
77
+ Expects `parent_name` (str) specifying the required parent class.
78
+ """
69
79
  self.parent_name_to_find: str | None = kwargs.get("parent_name")
70
80
 
71
81
  def check(self, nodes: list[ast.AST]) -> bool:
@@ -105,6 +115,12 @@ class MustBeTypeConstraint(Constraint):
105
115
  """
106
116
 
107
117
  def __init__(self, **kwargs: Any):
118
+ """Initializes the constraint.
119
+
120
+ Args:
121
+ **kwargs: Keyword arguments from the JSON rule's constraint config.
122
+ Expects `expected_type` (str) with the name of the required type.
123
+ """
108
124
  self.expected_type_str: str | None = kwargs.get("expected_type")
109
125
  self.type_map = {
110
126
  "str": str,
@@ -164,6 +180,12 @@ class NameMustBeInConstraint(Constraint):
164
180
  """
165
181
 
166
182
  def __init__(self, **kwargs: Any):
183
+ """Initializes the constraint.
184
+
185
+ Args:
186
+ **kwargs: Keyword arguments from the JSON rule's constraint config.
187
+ Expects `allowed_names` (list[str]) containing the valid names.
188
+ """
167
189
  self.allowed_names = set(kwargs.get("allowed_names", []))
168
190
 
169
191
  @staticmethod
@@ -194,6 +216,12 @@ class ValueMustBeInConstraint(Constraint):
194
216
  """
195
217
 
196
218
  def __init__(self, **kwargs: Any):
219
+ """Initializes the constraint.
220
+
221
+ Args:
222
+ **kwargs: Keyword arguments from the JSON rule's constraint config.
223
+ Expects `allowed_values` (list) containing the valid literal values.
224
+ """
197
225
  self.allowed_values = set(kwargs.get("allowed_values", []))
198
226
 
199
227
  def check(self, nodes: list[ast.AST]) -> bool:
@@ -224,6 +252,13 @@ class MustHaveArgsConstraint(Constraint):
224
252
  """
225
253
 
226
254
  def __init__(self, **kwargs: Any):
255
+ """Initializes the constraint.
256
+
257
+ Args:
258
+ **kwargs: Keyword arguments from the JSON rule's constraint config.
259
+ Can accept `count` (int), `names` (list[str]), and
260
+ `exact_match` (bool).
261
+ """
227
262
  self.expected_count: int | None = kwargs.get("count")
228
263
  self.expected_names: list[str] | None = kwargs.get("names")
229
264
  self.exact_match: bool = kwargs.get("exact_match", True)
@@ -3,7 +3,8 @@
3
3
  Each class in this module implements the `Selector` protocol and is responsible
4
4
  for finding and returning specific types of nodes from an Abstract Syntax Tree.
5
5
  They use `ast.walk` to traverse the tree and can be constrained to specific
6
- scopes using the ScopedSelector base class.
6
+ scopes via the `ScopedSelector` base class, which uses the `scope_handler`.
7
+ These classes are instantiated by the `SelectorFactory`.
7
8
  """
8
9
 
9
10
  import ast
@@ -27,11 +28,11 @@ class ScopedSelector(Selector):
27
28
  """
28
29
 
29
30
  def __init__(self, **kwargs: Any):
30
- """Initializes the ScopedSelector.
31
+ """Initializes the ScopedSelector base class.
31
32
 
32
33
  Args:
33
- **kwargs: Keyword arguments containing the scope configuration.
34
- Expects `in_scope` key.
34
+ **kwargs: Keyword arguments passed from a subclass constructor.
35
+ It extracts the `in_scope` configuration.
35
36
  """
36
37
  self.in_scope_config = kwargs.get("in_scope")
37
38
 
@@ -67,6 +68,12 @@ class FunctionDefSelector(ScopedSelector):
67
68
  """
68
69
 
69
70
  def __init__(self, **kwargs: Any):
71
+ """Initializes the FunctionDefSelector.
72
+
73
+ Args:
74
+ **kwargs: Keyword arguments from the JSON rule's selector config.
75
+ Expects `name` (str) and optionally `in_scope` (dict).
76
+ """
70
77
  super().__init__(**kwargs)
71
78
  self.name_to_find = kwargs.get("name")
72
79
 
@@ -118,6 +125,12 @@ class ImportStatementSelector(ScopedSelector):
118
125
  """
119
126
 
120
127
  def __init__(self, **kwargs: Any):
128
+ """Initializes the ImportStatementSelector.
129
+
130
+ Args:
131
+ **kwargs: Keyword arguments from the JSON rule's selector config.
132
+ Expects `name` (str) for the module name and optionally `in_scope`.
133
+ """
121
134
  super().__init__(**kwargs)
122
135
  self.module_name_to_find = kwargs.get("name")
123
136
 
@@ -188,6 +201,12 @@ class AssignmentSelector(ScopedSelector):
188
201
  """
189
202
 
190
203
  def __init__(self, **kwargs: Any):
204
+ """Initializes the AssignmentSelector.
205
+
206
+ Args:
207
+ **kwargs: Keyword arguments from the JSON rule's selector config.
208
+ Expects `name` (str) for the assignment target and optionally `in_scope`.
209
+ """
191
210
  super().__init__(**kwargs)
192
211
  self.target_name_to_find = kwargs.get("name")
193
212
 
@@ -221,6 +240,13 @@ class UsageSelector(ScopedSelector):
221
240
  """
222
241
 
223
242
  def __init__(self, **kwargs: Any):
243
+ """Initializes the UsageSelector.
244
+
245
+ Args:
246
+ **kwargs: Keyword arguments from the JSON rule's selector config.
247
+ Expects `name` (str) for the variable/attribute being used and
248
+ optionally `in_scope`.
249
+ """
224
250
  super().__init__(**kwargs)
225
251
  self.variable_name_to_find = kwargs.get("name")
226
252
 
@@ -248,10 +274,30 @@ class LiteralSelector(ScopedSelector):
248
274
  """
249
275
 
250
276
  def __init__(self, **kwargs: Any):
277
+ """Initializes the LiteralSelector.
278
+
279
+ Args:
280
+ **kwargs: Keyword arguments from the JSON rule's selector config.
281
+ Expects `name` (str) to be "string" or "number" and optionally
282
+ `in_scope`.
283
+ """
251
284
  super().__init__(**kwargs)
252
- self.literal_type = kwargs.get("name") # 'name' - это наш унифицированный ключ
285
+ self.literal_type = kwargs.get("name")
253
286
 
254
287
  def select(self, tree: ast.Module) -> list[ast.AST]:
288
+ """Finds all ast.Constant nodes that match the type criteria.
289
+
290
+ This method traverses the given AST (or a sub-tree defined by `in_scope`)
291
+ and collects all number or string literals. It contains special logic
292
+ to intelligently ignore nodes that are likely to be docstrings or parts
293
+ of f-strings to avoid false positives.
294
+
295
+ Args:
296
+ tree: The root of the AST (the module object) to be searched.
297
+
298
+ Returns:
299
+ A list of `ast.Constant` nodes matching the criteria.
300
+ """
255
301
  search_tree = self._get_search_tree(tree)
256
302
  if not search_tree:
257
303
  return []
@@ -295,6 +341,12 @@ class AstNodeSelector(ScopedSelector):
295
341
  """
296
342
 
297
343
  def __init__(self, **kwargs: Any):
344
+ """Initializes the AstNodeSelector.
345
+
346
+ Args:
347
+ **kwargs: Keyword arguments from the JSON rule's selector config.
348
+ Expects `node_type` (str or list[str]) and optionally `in_scope`.
349
+ """
298
350
  super().__init__(**kwargs)
299
351
  node_type_arg = kwargs.get("node_type")
300
352
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-code-validator
3
- Version: 0.1.2
4
- Summary: A flexible framework for static validation of Python code based on JSON rules.
3
+ Version: 0.1.3
4
+ Summary: A flexible, AST-based framework for static validation of Python code using declarative JSON rules.
5
5
  Author-email: Qu1nel <covach.qn@gmail.com>
6
6
  License: MIT License
7
7
 
@@ -28,22 +28,27 @@ License: MIT License
28
28
  Project-URL: Homepage, https://github.com/Qu1nel/PythonCodeValidator
29
29
  Project-URL: Documentation, https://pythoncodevalidator.readthedocs.io/en/latest/
30
30
  Project-URL: Bug Tracker, https://github.com/Qu1nel/PythonCodeValidator/issues
31
- Keywords: validation,linter,static analysis,testing,education
32
- Classifier: Development Status :: 3 - Alpha
31
+ Keywords: validation,linter,static analysis,testing,education,ast
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: Intended Audience :: Education
33
35
  Classifier: Programming Language :: Python :: 3
34
36
  Classifier: Programming Language :: Python :: 3.11
35
37
  Classifier: Programming Language :: Python :: 3.12
36
38
  Classifier: License :: OSI Approved :: MIT License
37
39
  Classifier: Operating System :: OS Independent
40
+ Classifier: Topic :: Software Development :: Quality Assurance
41
+ Classifier: Topic :: Software Development :: Testing
42
+ Classifier: Topic :: Education
38
43
  Requires-Python: >=3.11
39
44
  Description-Content-Type: text/markdown
40
45
  License-File: LICENSE
46
+ Requires-Dist: flake8>=7.0.0
41
47
  Provides-Extra: dev
42
48
  Requires-Dist: ruff>=0.4.0; extra == "dev"
43
- Requires-Dist: flake8>=7.0.0; extra == "dev"
44
49
  Requires-Dist: build; extra == "dev"
45
50
  Requires-Dist: twine; extra == "dev"
46
- Requires-Dist: coverage>=7.5.0; extra == "dev"
51
+ Requires-Dist: coverage[toml]>=7.5.0; extra == "dev"
47
52
  Provides-Extra: docs
48
53
  Requires-Dist: sphinx>=7.0.0; extra == "docs"
49
54
  Requires-Dist: furo; extra == "docs"
@@ -0,0 +1,22 @@
1
+ code_validator/__init__.py,sha256=3eCuRsidy-Tc4QASdz7mwRIiGYOumb9uPxY3ob85HBs,1721
2
+ code_validator/__main__.py,sha256=Z41EoJqX03AI11gnku_Iwt6rP8SPUkYuxwN7P51qgLc,600
3
+ code_validator/cli.py,sha256=PtPxlgKdbAAEKDpI1Xk7bZ5CKVYr9efIlOgh45Igjxk,4119
4
+ code_validator/config.py,sha256=qLuGsFPn61RS97clV8zHd43jb9EBIBwBzl1jrSKLD2w,5052
5
+ code_validator/core.py,sha256=n4aP_ohE_9qoX9lqbmPSvMZouxomWxauFiwgjzA1_ys,8003
6
+ code_validator/exceptions.py,sha256=XkiRNQ25FWJkjS2wBaUaKQcEL5WF9tN_HSV3tqJwDcE,1627
7
+ code_validator/output.py,sha256=VRJLGwm6X9i8SnovosNrHu96ueZXz9GIvUQXy4xDtHw,3304
8
+ code_validator/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ code_validator/components/ast_utils.py,sha256=v0N3F58oxNMg7bUY6-8x0AeoLsxrzrp_bLTGNVr5EMU,1589
10
+ code_validator/components/definitions.py,sha256=9WEXQ_i-S4Vega6HqOdjreZE_1cgJPC7guZodTKtzhc,3208
11
+ code_validator/components/factories.py,sha256=0-U--7pekt-fIvqb_WesWh_ehKCvJ7ROObx5GU-yjGc,10492
12
+ code_validator/components/scope_handler.py,sha256=q4cFK_YzoYvAqPkw9wPPxgPe-aO0kgy6gk0ETWduj-U,2593
13
+ code_validator/rules_library/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ code_validator/rules_library/basic_rules.py,sha256=VYm6t_En0t6D42vp6vtCr5XV6-M4xuPupX8Hz6-eveI,6858
15
+ code_validator/rules_library/constraint_logic.py,sha256=CRnzMgLR17Ja2d4M3jWBO-xLLPB6Vpk7iusGMoyJ17s,10932
16
+ code_validator/rules_library/selector_nodes.py,sha256=hWzvar1tvnkEFReCFLSw5oTK9HETNmdNVcDD3_57Iyg,14306
17
+ python_code_validator-0.1.3.dist-info/licenses/LICENSE,sha256=Lq69RwIO4Dge7OsjgAamJfYSDq2DWI2yzVYI1VX1s6c,1089
18
+ python_code_validator-0.1.3.dist-info/METADATA,sha256=NlDRdvgugKe3hWC-Z8q1h7_lrqZBJ4Jpd6YbjD3nuVU,12081
19
+ python_code_validator-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ python_code_validator-0.1.3.dist-info/entry_points.txt,sha256=pw_HijiZyPxokVJHStTkGCwheTjukDomdk81JyHzv74,66
21
+ python_code_validator-0.1.3.dist-info/top_level.txt,sha256=yowMDfABI5oqgW3hhTdec_7UHGeprkvc2BnqRzNbI5w,15
22
+ python_code_validator-0.1.3.dist-info/RECORD,,
@@ -1,22 +0,0 @@
1
- code_validator/__init__.py,sha256=etZcfEkc_LFR8gqYPeH6hitwZ5-HS2WKSjl6LW9M8PY,961
2
- code_validator/__main__.py,sha256=c7P8Lz3EuwYRHarTEp_DExMUauod9X42p6aTZkpvi10,330
3
- code_validator/cli.py,sha256=fLbSP_4ABZGZTH6-L4oAXKVRTW-OVMdpcZgT9F4ibtY,3466
4
- code_validator/config.py,sha256=kTqD8-SFbtUQ9oif-TylqNFPadTq5JSGBv84cI0R8dY,2657
5
- code_validator/core.py,sha256=4sPdlunmODfNpDrP9QH2jIrGFVLFlo_r8MAoB4_63vo,4560
6
- code_validator/exceptions.py,sha256=XkiRNQ25FWJkjS2wBaUaKQcEL5WF9tN_HSV3tqJwDcE,1627
7
- code_validator/output.py,sha256=VRJLGwm6X9i8SnovosNrHu96ueZXz9GIvUQXy4xDtHw,3304
8
- code_validator/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- code_validator/components/ast_utils.py,sha256=7DzyKmLekj_qtuZcS8BrcWXqQXVEPPQ_rwh3BmpIk9o,1412
10
- code_validator/components/definitions.py,sha256=cFbKL7UZYHqWjz2gJCmZ6fU_ZdQ5L_h1tlQBYkxSO7Q,3126
11
- code_validator/components/factories.py,sha256=qrQotS7lyqIGhoNGRvSNQKy6p9YJKQC7YaPi4eCtpww,10436
12
- code_validator/components/scope_handler.py,sha256=vb4pCE-4DyxGkRYGeW2JWxP2IiZH3uRRfReo0IBgM5Y,2459
13
- code_validator/rules_library/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- code_validator/rules_library/basic_rules.py,sha256=0K45Ed8yjy-pXDZpI1Xvgj94zvaFz-PucFRz__X4uRI,6652
15
- code_validator/rules_library/constraint_logic.py,sha256=X95g0673hoZey3LGCBH6AscbIuHYq_aY9WRKIkJhOnk,9525
16
- code_validator/rules_library/selector_nodes.py,sha256=JxhUBXS95Dad_60Ut-8XkW2MFM-7bkeRb_yk7vXlNPE,12200
17
- python_code_validator-0.1.2.dist-info/licenses/LICENSE,sha256=Lq69RwIO4Dge7OsjgAamJfYSDq2DWI2yzVYI1VX1s6c,1089
18
- python_code_validator-0.1.2.dist-info/METADATA,sha256=lYCstaPpPsj2KDOze-aYZFkr_NDi3Cy8UAgUCJHB2Mo,11829
19
- python_code_validator-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- python_code_validator-0.1.2.dist-info/entry_points.txt,sha256=pw_HijiZyPxokVJHStTkGCwheTjukDomdk81JyHzv74,66
21
- python_code_validator-0.1.2.dist-info/top_level.txt,sha256=yowMDfABI5oqgW3hhTdec_7UHGeprkvc2BnqRzNbI5w,15
22
- python_code_validator-0.1.2.dist-info/RECORD,,