python-code-validator 0.1.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/core.py ADDED
@@ -0,0 +1,100 @@
1
+ import ast
2
+ import json
3
+
4
+ from .components.ast_utils import enrich_ast_with_parents
5
+ from .components.definitions import Rule
6
+ from .components.factories import RuleFactory
7
+ from .config import AppConfig, LogLevel
8
+ from .exceptions import RuleParsingError
9
+ from .output import Console
10
+
11
+
12
+ class StaticValidator:
13
+ """Orchestrates the static validation process."""
14
+
15
+ def __init__(self, config: AppConfig, console: Console):
16
+ """Initializes the validator with configuration and an output handler."""
17
+ self._config = config
18
+ self._console = console
19
+ self._rule_factory = RuleFactory(self._console)
20
+ self._source_code: str = ""
21
+ self._ast_tree: ast.Module | None = None
22
+ self._validation_rules: list[Rule] = []
23
+ self._failed_rules: list[int] = []
24
+
25
+ @property
26
+ def failed_rules_id(self) -> list[int]:
27
+ """Returns a list of rule IDs that failed during the last run."""
28
+ return self._failed_rules
29
+
30
+ def _load_source_code(self) -> None:
31
+ """Loads the content of the student's solution file."""
32
+ self._console.print(f"Reading source file: {self._config.solution_path}")
33
+ try:
34
+ self._source_code = self._config.solution_path.read_text(encoding="utf-8")
35
+ except FileNotFoundError:
36
+ raise
37
+ except Exception as e:
38
+ raise RuleParsingError(f"Cannot read source file: {e}") from e
39
+
40
+ def _parse_ast_tree(self) -> bool:
41
+ """Parses the loaded source code into an AST."""
42
+ self._console.print("Parsing Abstract Syntax Tree (AST)...")
43
+ try:
44
+ self._ast_tree = ast.parse(self._source_code)
45
+ enrich_ast_with_parents(self._ast_tree)
46
+ return True
47
+ except SyntaxError as e:
48
+ # Ищем правило check_syntax, чтобы вывести его кастомное сообщение
49
+ for rule in self._validation_rules:
50
+ if getattr(rule.config, "type", None) == "check_syntax":
51
+ self._console.print(rule.config.message, level="ERROR")
52
+ self._failed_rules.append(rule.config.rule_id)
53
+ return False
54
+ # Если такого правила нет, выводим стандартное сообщение
55
+ self._console.print(f"Syntax Error found: {e}", level="ERROR")
56
+ return False
57
+
58
+ def _load_and_parse_rules(self) -> None:
59
+ """Loads and parses the JSON file with validation rules."""
60
+ self._console.print(f"Loading rules from: {self._config.rules_path}")
61
+ try:
62
+ rules_data = json.loads(self._config.rules_path.read_text(encoding="utf-8"))
63
+ raw_rules = rules_data.get("validation_rules")
64
+ if not isinstance(raw_rules, list):
65
+ raise RuleParsingError("`validation_rules` key not found or is not a list.")
66
+
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.")
69
+ except json.JSONDecodeError as e:
70
+ raise RuleParsingError(f"Invalid JSON in rules file: {e}") from e
71
+ except FileNotFoundError:
72
+ raise
73
+
74
+ def run(self) -> bool:
75
+ """Runs the entire validation process."""
76
+ try:
77
+ self._load_source_code()
78
+ self._load_and_parse_rules() # Загружаем правила до парсинга AST
79
+
80
+ if not self._parse_ast_tree():
81
+ return False
82
+
83
+ except (FileNotFoundError, RuleParsingError):
84
+ raise
85
+
86
+ for rule in self._validation_rules:
87
+ # check_syntax уже обработан в _parse_ast_tree, пропускаем его
88
+ if getattr(rule.config, "type", None) == "check_syntax":
89
+ continue
90
+
91
+ self._console.print(f"Executing rule: {rule.config.rule_id}", level=LogLevel.DEBUG)
92
+ is_passed = rule.execute(self._ast_tree, self._source_code)
93
+ if not is_passed:
94
+ self._console.print(rule.config.message, level="ERROR")
95
+ self._failed_rules.append(rule.config.rule_id)
96
+ 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")
98
+ break
99
+
100
+ return not self._failed_rules
@@ -0,0 +1,48 @@
1
+ """Defines custom exceptions for the code validator application.
2
+
3
+ These custom exception classes allow for more specific error handling and
4
+ provide clearer, more informative error messages throughout the application.
5
+ """
6
+
7
+
8
+ class CodeValidatorError(Exception):
9
+ """Base exception for all custom errors raised by this application."""
10
+
11
+ pass
12
+
13
+
14
+ class RuleParsingError(CodeValidatorError):
15
+ """Raised when a validation rule in the JSON file is malformed or invalid.
16
+
17
+ This error indicates a problem with the configuration of a rule, not with
18
+ the code being validated.
19
+
20
+ Attributes:
21
+ rule_id (int | str | None): The ID of the rule that caused the error,
22
+ if available.
23
+ """
24
+
25
+ def __init__(self, message: str, rule_id: int | str | None = None):
26
+ """Initializes the RuleParsingError.
27
+
28
+ Args:
29
+ message (str): The specific error message describing the problem.
30
+ rule_id (int | str | None): The ID of the problematic rule.
31
+ """
32
+ self.rule_id = rule_id
33
+ if rule_id:
34
+ super().__init__(f"Error parsing rule '{rule_id}': {message}")
35
+ else:
36
+ super().__init__(f"Error parsing rules file: {message}")
37
+
38
+
39
+ class ValidationFailedError(CodeValidatorError):
40
+ """Raised to signal that the source code did not pass validation.
41
+
42
+ Note:
43
+ Currently, this exception is defined but not actively raised, as the
44
+ application handles validation failure via exit codes in the CLI.
45
+ It is kept for potential future use in a library context.
46
+ """
47
+
48
+ pass
@@ -0,0 +1,90 @@
1
+ """Handles all console output and logging for the application.
2
+
3
+ This module provides a centralized way to manage user-facing messages and
4
+ internal logging. It ensures that all output is consistent and can be
5
+ controlled via configuration (e.g., log levels, silent mode).
6
+ """
7
+
8
+ import logging
9
+ import sys
10
+ from typing import Literal
11
+
12
+ from .config import LogLevel
13
+
14
+ LOG_FORMAT = "%(asctime)s | %(filename)-15s | %(funcName)-15s (%(lineno)-3s) | [%(levelname)s] - %(message)s"
15
+ DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
16
+
17
+
18
+ def setup_logging(log_level: LogLevel) -> logging.Logger:
19
+ """Configures the root logger for the application.
20
+
21
+ Sets up the basic configuration for logging, including the level,
22
+ message format, and date format.
23
+
24
+ Args:
25
+ log_level: The minimum level of logs to display.
26
+
27
+ Returns:
28
+ The configured root logger instance.
29
+ """
30
+ logging.basicConfig(level=log_level, format=LOG_FORMAT, datefmt=DATE_FORMAT)
31
+ return logging.getLogger()
32
+
33
+
34
+ class Console:
35
+ """A centralized handler for printing messages to stdout and logging.
36
+
37
+ This class abstracts all output operations, allowing for consistent
38
+ formatting and easy control over verbosity (e.g., silent mode). It ensures
39
+ that every user-facing message is also properly logged.
40
+
41
+ Attributes:
42
+ _logger (logging.Logger): The logger instance used for all log records.
43
+ _is_silent (bool): A flag to suppress printing to stdout.
44
+ _stdout (TextIO): The stream to write messages to (defaults to sys.stdout).
45
+
46
+ Example:
47
+ >>> import logging
48
+ >>> logger = logging.getLogger(__name__)
49
+ >>> console = Console(logger)
50
+ >>> console.print("This is an informational message.")
51
+ This is an informational message.
52
+ >>> console.print("This is a warning.", level="WARNING")
53
+ This is a warning.
54
+ """
55
+
56
+ def __init__(self, logger: logging.Logger, *, is_silent: bool = False):
57
+ """Initializes the Console handler.
58
+
59
+ Args:
60
+ logger: The configured logger instance to use for logging.
61
+ is_silent: If True, suppresses output to stdout. Defaults to False.
62
+ """
63
+ self._logger = logger
64
+ self._is_silent = is_silent
65
+ self._stdout = sys.stdout
66
+
67
+ def print(
68
+ self,
69
+ message: str,
70
+ *,
71
+ level: LogLevel | Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = LogLevel.INFO,
72
+ ) -> None:
73
+ """Prints a message to stdout and logs it simultaneously.
74
+
75
+ The message is always sent to the logger with the specified level. It is
76
+ printed to the configured stdout stream only if the console is not in
77
+ silent mode.
78
+
79
+ Args:
80
+ message: The string message to be displayed and logged.
81
+ level: The logging level for the message. Accepts both LogLevel
82
+ enum members and their string representations.
83
+ Defaults to LogLevel.INFO.
84
+ """
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)
88
+
89
+ if not self._is_silent:
90
+ print(message, file=self._stdout)
File without changes
@@ -0,0 +1,167 @@
1
+ # src/code_validator/rules_library/basic_rules.py
2
+
3
+ """Contains concrete implementations of executable validation rules.
4
+
5
+ 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.
8
+ """
9
+
10
+ import ast
11
+ import subprocess
12
+ import sys
13
+
14
+ from ..components.definitions import Constraint, Rule, Selector
15
+ from ..config import FullRuleConfig, ShortRuleConfig
16
+ from ..output import Console, LogLevel
17
+
18
+
19
+ class CheckSyntaxRule(Rule):
20
+ """Handles the 'check_syntax' short rule.
21
+
22
+ Note:
23
+ The actual syntax validation is performed preemptively in the core
24
+ validator engine when it calls `ast.parse()`. Therefore, this rule's
25
+ `execute` method will only be called if the syntax is already valid.
26
+ Its primary purpose is to exist so that a `check_syntax` rule can be
27
+ formally defined in the JSON configuration.
28
+ """
29
+
30
+ def __init__(self, config: ShortRuleConfig, console: Console):
31
+ """Initializes the syntax check rule handler.
32
+
33
+ Args:
34
+ config: The configuration object for this short rule.
35
+ console: The console handler for output.
36
+ """
37
+ self.config = config
38
+ self._console = console
39
+
40
+ def execute(self, tree: ast.Module | None, source_code: str | None = None) -> bool:
41
+ """Confirms that syntax is valid.
42
+
43
+ This method is guaranteed to be called only after a successful AST parsing.
44
+
45
+ Returns:
46
+ Always returns True.
47
+ """
48
+ self._console.print(f"Rule {self.config.rule_id}: Syntax is valid.", level=LogLevel.DEBUG)
49
+ return True
50
+
51
+
52
+ class CheckLinterRule(Rule):
53
+ """Handles the 'check_linter_pep8' short rule by running flake8.
54
+
55
+ This rule executes the `flake8` linter as an external subprocess on the
56
+ source code to check for style and common programming errors. It is
57
+ configurable via the 'params' field in the JSON rule.
58
+ """
59
+
60
+ def __init__(self, config: ShortRuleConfig, console: Console):
61
+ """Initializes a PEP8 linter check rule handler."""
62
+ self.config = config
63
+ self._console = console
64
+
65
+ def execute(self, tree: ast.Module | None, source_code: str | None = None) -> bool:
66
+ """Executes the flake8 linter on the source code via a subprocess.
67
+
68
+ It constructs a command-line call to `flake8`, passing the source code
69
+ via stdin. This approach ensures isolation and uses flake8's stable
70
+ CLI interface.
71
+
72
+ Args:
73
+ tree: Not used by this rule.
74
+ source_code: The raw source code string to be linted.
75
+
76
+ Returns:
77
+ True if no PEP8 violations are found, False otherwise.
78
+ """
79
+ if not source_code:
80
+ self._console.print("Source code is empty, skipping PEP8 check.", level="WARNING")
81
+ return True
82
+
83
+ self._console.print(f"Rule {self.config.rule_id}: Running flake8 linter...", level="DEBUG")
84
+
85
+ params = self.config.params
86
+ args = [sys.executable, "-m", "flake8", "-"]
87
+
88
+ if select_list := params.get("select"):
89
+ args.append(f"--select={','.join(select_list)}")
90
+ elif ignore_list := params.get("ignore"):
91
+ args.append(f"--ignore={','.join(ignore_list)}")
92
+
93
+ try:
94
+ process = subprocess.run(
95
+ args,
96
+ input=source_code,
97
+ capture_output=True,
98
+ text=True,
99
+ encoding="utf-8",
100
+ check=False,
101
+ )
102
+
103
+ if process.returncode != 0 and process.stdout:
104
+ linter_output = process.stdout.strip()
105
+ self._console.print(f"Flake8 found issues:\n{linter_output}", level="DEBUG")
106
+ return False
107
+ elif process.returncode != 0:
108
+ self._console.print(f"Flake8 exited with code {process.returncode}:\n{process.stderr}", level="ERROR")
109
+ return False
110
+
111
+ self._console.print("PEP8 check passed.", level="DEBUG")
112
+ return True
113
+ except FileNotFoundError:
114
+ self._console.print("flake8 not found. Is it installed in the venv?", level="CRITICAL")
115
+ return False
116
+ except Exception as e:
117
+ self._console.print(f"An unexpected error occurred while running flake8: {e}", level="CRITICAL")
118
+ return False
119
+
120
+
121
+ class FullRuleHandler(Rule):
122
+ """Handles a full, custom rule composed of a selector and a constraint.
123
+
124
+ This class acts as a generic executor for complex rules. It does not contain
125
+ any specific validation logic itself but instead orchestrates the interaction
126
+ between a Selector and a Constraint object.
127
+
128
+ Attributes:
129
+ config (FullRuleConfig): The dataclass object holding the rule's config.
130
+ _selector (Selector): The selector object responsible for finding nodes.
131
+ _constraint (Constraint): The constraint object for checking the nodes.
132
+ _console (Console): The console handler for logging.
133
+ """
134
+
135
+ def __init__(self, config: FullRuleConfig, selector: Selector, constraint: Constraint, console: Console):
136
+ """Initializes a full rule handler.
137
+
138
+ Args:
139
+ config: The configuration for the full rule.
140
+ selector: An initialized Selector object.
141
+ constraint: An initialized Constraint object.
142
+ console: The console handler for logging.
143
+ """
144
+ self.config = config
145
+ self._selector = selector
146
+ self._constraint = constraint
147
+ self._console = console
148
+
149
+ def execute(self, tree: ast.Module | None, source_code: str | None = None) -> bool:
150
+ """Executes the rule by running the selector and applying the constraint.
151
+
152
+ Args:
153
+ tree: The enriched AST of the source code.
154
+ source_code: Not used by this rule.
155
+
156
+ Returns:
157
+ The boolean result of applying the constraint to the selected nodes.
158
+ """
159
+ if not tree:
160
+ self._console.print("AST not available, skipping rule.", level="WARNING")
161
+ return True
162
+
163
+ self._console.print(f"Applying selector: {self._selector.__class__.__name__}", level="DEBUG")
164
+ selected_nodes = self._selector.select(tree)
165
+
166
+ self._console.print(f"Applying constraint: {self._constraint.__class__.__name__}", level="DEBUG")
167
+ return self._constraint.check(selected_nodes)
@@ -0,0 +1,257 @@
1
+ """Contains concrete implementations of all Constraint components.
2
+
3
+ Each class in this module implements the `Constraint` protocol and encapsulates
4
+ the logic for a specific condition that can be checked against a list of
5
+ AST nodes found by a Selector.
6
+ """
7
+
8
+ import ast
9
+ from typing import Any
10
+
11
+ from ..components.ast_utils import get_full_name
12
+ from ..components.definitions import Constraint
13
+
14
+
15
+ class IsRequiredConstraint(Constraint):
16
+ """Checks that at least one node was found by the selector.
17
+
18
+ This constraint is used to enforce the presence of a required language
19
+ construct. It can also check for an exact number of occurrences.
20
+
21
+ JSON Params:
22
+ count (int, optional): If provided, checks if the number of found
23
+ nodes is exactly equal to this value.
24
+ """
25
+
26
+ def __init__(self, **kwargs: Any):
27
+ """Initializes the constraint.
28
+
29
+ Args:
30
+ **kwargs: Configuration for the constraint, e.g., 'count'.
31
+ """
32
+ self.expected_count = kwargs.get("count")
33
+
34
+ def check(self, nodes: list[ast.AST]) -> bool:
35
+ """Checks if the list of nodes is not empty or matches expected count."""
36
+ if self.expected_count is not None:
37
+ return len(nodes) == self.expected_count
38
+ return len(nodes) > 0
39
+
40
+
41
+ class IsForbiddenConstraint(Constraint):
42
+ """Checks that no nodes were found by the selector.
43
+
44
+ This is the inverse of `IsRequiredConstraint` and is used to forbid certain
45
+ constructs, such as specific function calls or imports.
46
+ """
47
+
48
+ def __init__(self, **kwargs: Any):
49
+ """Initializes the constraint."""
50
+ pass
51
+
52
+ def check(self, nodes: list[ast.AST]) -> bool:
53
+ """Checks if the list of nodes is empty."""
54
+ return not nodes
55
+
56
+
57
+ class MustInheritFromConstraint(Constraint):
58
+ """Checks that a ClassDef node inherits from a specific parent class.
59
+
60
+ This constraint is designed to work with a selector that returns a single
61
+ `ast.ClassDef` node. It can resolve both simple names (e.g., `Exception`)
62
+ and attribute-based names (e.g., `arcade.Window`).
63
+
64
+ JSON Params:
65
+ parent_name (str): The expected name of the parent class.
66
+ """
67
+
68
+ def __init__(self, **kwargs: Any):
69
+ self.parent_name_to_find: str | None = kwargs.get("parent_name")
70
+
71
+ def check(self, nodes: list[ast.AST]) -> bool:
72
+ """Checks if the found class node inherits from the specified parent."""
73
+ if not self.parent_name_to_find or len(nodes) != 1:
74
+ return False
75
+
76
+ node = nodes[0]
77
+ if not isinstance(node, ast.ClassDef):
78
+ return False
79
+
80
+ for base in node.bases:
81
+ full_name = self._get_full_attribute_name(base)
82
+ if full_name == self.parent_name_to_find:
83
+ return True
84
+ return False
85
+
86
+ @staticmethod
87
+ def _get_full_attribute_name(node: ast.AST) -> str | None:
88
+ """Recursively builds the full attribute name from a base class node."""
89
+ if isinstance(node, ast.Name):
90
+ return node.id
91
+ if isinstance(node, ast.Attribute):
92
+ base = MustInheritFromConstraint._get_full_attribute_name(node.value)
93
+ return f"{base}.{node.attr}" if base else node.attr
94
+ return None
95
+
96
+
97
+ class MustBeTypeConstraint(Constraint):
98
+ """Checks the type of the value in an assignment statement.
99
+
100
+ It works for simple literals (numbers, strings, lists, etc.) and for
101
+ calls to built-in type constructors (e.g., `list()`, `dict()`).
102
+
103
+ JSON Params:
104
+ expected_type (str): The name of the type, e.g., "str", "int", "list".
105
+ """
106
+
107
+ def __init__(self, **kwargs: Any):
108
+ self.expected_type_str: str | None = kwargs.get("expected_type")
109
+ self.type_map = {
110
+ "str": str,
111
+ "int": int,
112
+ "float": float,
113
+ "list": list,
114
+ "dict": dict,
115
+ "bool": bool,
116
+ "set": set,
117
+ "tuple": tuple,
118
+ }
119
+ self.constructor_map = {t: t for t in self.type_map}
120
+
121
+ def check(self, nodes: list[ast.AST]) -> bool:
122
+ """Checks if the assigned value has the expected Python type."""
123
+ if not nodes or not self.expected_type_str:
124
+ return False
125
+ expected_py_type = self.type_map.get(self.expected_type_str)
126
+ if not expected_py_type:
127
+ return False
128
+
129
+ for node in nodes:
130
+ value_node = getattr(node, "value", None)
131
+ if value_node is None:
132
+ continue
133
+
134
+ if self._is_correct_type(value_node, expected_py_type):
135
+ continue
136
+ return False
137
+ return True
138
+
139
+ def _is_correct_type(self, value_node: ast.AST, expected_py_type: type) -> bool:
140
+ """Checks a single value node against the expected type."""
141
+ try:
142
+ assigned_value = ast.literal_eval(value_node)
143
+ if isinstance(assigned_value, expected_py_type):
144
+ return True
145
+ except (ValueError, TypeError, SyntaxError):
146
+ pass
147
+
148
+ if isinstance(value_node, ast.Call):
149
+ func_name = getattr(value_node.func, "id", None)
150
+ expected_constructor = self.constructor_map.get(self.expected_type_str)
151
+ if func_name == expected_constructor:
152
+ return True
153
+ return False
154
+
155
+
156
+ class NameMustBeInConstraint(Constraint):
157
+ """Checks if the name of a found node is in an allowed list of names.
158
+
159
+ This is useful for rules like restricting global variables to a pre-defined
160
+ set of constants.
161
+
162
+ JSON Params:
163
+ allowed_names (list[str]): A list of strings containing the allowed names.
164
+ """
165
+
166
+ def __init__(self, **kwargs: Any):
167
+ self.allowed_names = set(kwargs.get("allowed_names", []))
168
+
169
+ @staticmethod
170
+ def _get_name(node: ast.AST) -> str | None:
171
+ """Gets a name from various node types."""
172
+ if isinstance(node, (ast.Assign, ast.AnnAssign)):
173
+ target = node.targets[0] if isinstance(node, ast.Assign) else node.target
174
+ return get_full_name(target)
175
+ return getattr(node, "name", getattr(node, "id", None))
176
+
177
+ def check(self, nodes: list[ast.AST]) -> bool:
178
+ """Checks if all found node names are in the allowed set."""
179
+ for node in nodes:
180
+ name_to_check = self._get_name(node)
181
+ if name_to_check and name_to_check not in self.allowed_names:
182
+ return False
183
+ return True
184
+
185
+
186
+ class ValueMustBeInConstraint(Constraint):
187
+ """Checks if the value of a found literal node is in an allowed list.
188
+
189
+ This is primarily used to check for "magic numbers" or "magic strings",
190
+ allowing only a specific set of literal values to be present.
191
+
192
+ JSON Params:
193
+ allowed_values (list): A list of allowed literal values (e.g., [0, 1]).
194
+ """
195
+
196
+ def __init__(self, **kwargs: Any):
197
+ self.allowed_values = set(kwargs.get("allowed_values", []))
198
+
199
+ def check(self, nodes: list[ast.AST]) -> bool:
200
+ """Checks if all found literal values are in the allowed set."""
201
+ if not self.allowed_values:
202
+ return not nodes
203
+
204
+ for node in nodes:
205
+ if isinstance(node, ast.Constant):
206
+ if node.value not in self.allowed_values:
207
+ return False
208
+ else:
209
+ return False
210
+ return True
211
+
212
+
213
+ class MustHaveArgsConstraint(Constraint):
214
+ """Checks that a FunctionDef node has a specific signature.
215
+
216
+ This constraint can check for an exact number of arguments or for an
217
+ exact sequence of argument names, ignoring `self` or `cls` in methods.
218
+
219
+ JSON Params:
220
+ count (int, optional): The exact number of arguments required.
221
+ names (list[str], optional): The exact list of argument names in order.
222
+ exact_match (bool, optional): Used with `names`. If False, only checks
223
+ for presence, not for exact list match. Defaults to True.
224
+ """
225
+
226
+ def __init__(self, **kwargs: Any):
227
+ self.expected_count: int | None = kwargs.get("count")
228
+ self.expected_names: list[str] | None = kwargs.get("names")
229
+ self.exact_match: bool = kwargs.get("exact_match", True)
230
+
231
+ def check(self, nodes: list[ast.AST]) -> bool:
232
+ """Checks if the function signature matches the criteria."""
233
+ if not nodes:
234
+ return True
235
+ if not all(isinstance(node, ast.FunctionDef) for node in nodes):
236
+ return False
237
+
238
+ for node in nodes:
239
+ actual_arg_names = [arg.arg for arg in node.args.args]
240
+ if hasattr(node, "parent") and isinstance(node.parent, ast.ClassDef):
241
+ if actual_arg_names:
242
+ actual_arg_names.pop(0)
243
+
244
+ if not self._check_single_node(actual_arg_names):
245
+ return False
246
+ return True
247
+
248
+ def _check_single_node(self, actual_arg_names: list[str]) -> bool:
249
+ """Checks the argument list of a single function."""
250
+ if self.expected_names is not None:
251
+ if self.exact_match:
252
+ return actual_arg_names == self.expected_names
253
+ else:
254
+ return set(self.expected_names).issubset(set(actual_arg_names))
255
+ elif self.expected_count is not None:
256
+ return len(actual_arg_names) == self.expected_count
257
+ return False