• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pyta-uoft / pyta / 19255349751

11 Nov 2025 04:51AM UTC coverage: 93.909% (-0.3%) from 94.222%
19255349751

Pull #1251

github

web-flow
Merge b610418fd into ef514f733
Pull Request #1251: Optimizing performance for 'test_examples.py'

8 of 8 new or added lines in 2 files covered. (100.0%)

16 existing lines in 7 files now uncovered.

3515 of 3743 relevant lines covered (93.91%)

17.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.43
/python_ta/checkers/invalid_name_checker.py
1
"""Checker used for identifying names that don't conform to Python naming conventions."""
2

3
from __future__ import annotations
20✔
4

5
import re
20✔
6
from typing import TYPE_CHECKING, Optional
20✔
7

8
from astroid import nodes
20✔
9
from pylint.checkers import BaseChecker, utils
20✔
10
from pylint.checkers.base.name_checker.checker import _redefines_import
20✔
11
from pylint.checkers.utils import only_required_for_messages
20✔
12

13
from python_ta.utils import _is_in_main
20✔
14

15
if TYPE_CHECKING:
16
    from pylint.lint import PyLinter
17

18
# Bad variable names.
19
BAD_NAMES = {"l", "I", "O"}
20✔
20

21
# Set a limit in name length to keep certain variable names short.
22
VAR_NAME_LENGTHS = {
20✔
23
    "module": 30,
24
    "constant": 30,
25
    "class": 30,
26
    "function": 30,
27
    "method": 30,
28
    "attribute": 30,
29
    "argument": 30,
30
    "variable": 30,
31
    "class attribute": 30,
32
    "class constant": 30,
33
    "type variable": 20,
34
    "type alias": 20,
35
}
36

37
TYPE_VAR_QNAME = frozenset(
20✔
38
    (
39
        "typing.TypeVar",
40
        "typing_extensions.TypeVar",
41
    )
42
)
43

44

45
def _is_in_snake_case(name: str) -> bool:
20✔
46
    """Returns whether `name` is in snake_case.
47

48
    `name` is in snake_case if:
49
      - `name` starts with a lowercase letter or an underscore (to denote private fields) followed
50
        by a lowercase letter,
51
      - each word is separated by an underscore, and
52
      - each word is in lowercase.
53
    """
54
    pattern = "(_?[a-z][a-z0-9_]*)$"
20✔
55

56
    return re.match(pattern, name) is not None
20✔
57

58

59
def _is_in_pascal_case(name: str) -> bool:
20✔
60
    """Returns whether `name` is in PascalCase.
61

62
    `name` is in PascalCase if:
63
      - `name` starts with an uppercase letter or an underscore (to denote private fields) followed
64
        by an uppercase letter.
65
      - each word has its first character capitalized, and
66
      - there is no whitespace, underscore, or punctuation between words.
67
    """
68
    pattern = "(_?[A-Z][a-zA-Z0-9]*)$"
20✔
69

70
    return re.match(pattern, name) is not None
20✔
71

72

73
def _is_in_upper_case_with_underscores(name: str) -> bool:
20✔
74
    """Returns whether `name` is in UPPER_CASE_WITH_UNDERSCORES.
75

76
    `name` is in `UPPER_CASE_WITH_UNDERSCORES if:
77
      - each word is in uppercase, and
78
      - words are separated by an underscore.
79
    """
80
    pattern = "(_?[A-Z][A-Z0-9_]*)$"
20✔
81

82
    return re.match(pattern, name) is not None
20✔
83

84

85
def _is_bad_name(name: str) -> str:
20✔
86
    """Returns a string detailing why `name` is a bad name.
87

88
    `name` is a bad name if it is in the pre-determined collection of "bad names".
89

90
    Returns the empty string if `name` is not a bad name."""
91
    msg = ""
20✔
92

93
    if name in BAD_NAMES:
20✔
94
        msg = (
20✔
95
            f'"{name}" is a name that should be avoided. Change to something less ambiguous '
96
            f"and/or more descriptive."
97
        )
98

99
    return msg
20✔
100

101

102
def _is_within_name_length(node_type: str, name: str) -> str:
20✔
103
    """Returns a string saying that `name` exceeds the character limit for that variable name type.
104

105
    Returns the empty string if `name` is within the name length limit."""
106
    msg = ""
20✔
107
    name_length_limit = VAR_NAME_LENGTHS[node_type]
20✔
108

109
    if len(name) > name_length_limit:
20✔
110
        msg = (
20✔
111
            f'{node_type.capitalize()} name "{name}" exceeds the limit of {name_length_limit} '
112
            f"characters."
113
        )
114

115
    return msg
20✔
116

117

118
def _ignore_name(name: str, pattern: re.Pattern) -> bool:
20✔
119
    """Returns whether name matches any of the regular expressions provided in patterns"""
120
    return pattern.pattern and pattern.match(name) is not None
20✔
121

122

123
def _check_module_name(_node_type: str, name: str) -> list[str]:
20✔
124
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
125
    module names.
126

127
    Returns an empty list if `name` is a valid module name."""
128
    error_msgs = []
20✔
129

130
    if not _is_in_snake_case(name):
20✔
131
        error_msgs.append(
20✔
132
            f'Module name "{name}" should be in snake_case format. Modules should be all-lowercase '
133
            f"names, with each name separated by underscores."
134
        )
135

136
    return error_msgs
20✔
137

138

139
def _check_const_name(node_type: str, name: str) -> list[str]:
20✔
140
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
141
    constant and class constant names.
142

143
    Returns an empty list if `name` is a valid (global or class) constant name."""
144
    error_msgs = []
20✔
145

146
    if not _is_in_upper_case_with_underscores(name):
20✔
147
        msg = (
20✔
148
            f'{node_type.capitalize()} name "{name}" should be in UPPER_CASE_WITH_UNDERSCORES format. '
149
            f"Constants should be all-uppercase words with each word separated by an "
150
            f"underscore. A single leading underscore can be used to denote a private constant."
151
        )
152
        if node_type == "class constant":
20✔
153
            msg += " A double leading underscore invokes Python's name-mangling rules."
20✔
154
        error_msgs.append(msg)
20✔
155

156
    return error_msgs
20✔
157

158

159
def _check_class_name(_node_type: str, name: str) -> list[str]:
20✔
160
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
161
    class names.
162

163
    Returns an empty list if `name` is a valid class name."""
164
    error_msgs = []
20✔
165

166
    if not _is_in_pascal_case(name):
20✔
167
        error_msgs.append(
20✔
168
            f'Class name "{name}" should be in PascalCase format. Class names should have the '
169
            f"first letter of each word capitalized with no separation between each "
170
            f"word. A single leading underscore can be used to denote a private "
171
            f"class."
172
        )
173

174
    return error_msgs
20✔
175

176

177
def _check_function_and_variable_name(node_type: str, name: str) -> list[str]:
20✔
178
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
179
    function and variable names.
180

181
    Returns an empty list if `name` is a valid function or variable name."""
182
    error_msgs = []
20✔
183

184
    if name != "_" and not _is_in_snake_case(name):
20✔
185
        error_msgs.append(
20✔
186
            f'{node_type.capitalize()} name "{name}" should be in snake_case format. '
187
            f"{node_type.capitalize()} names should be lowercase, with words "
188
            f"separated by underscores. A single leading underscore can be used to "
189
            f"denote a private {node_type}."
190
        )
191

192
    return error_msgs
20✔
193

194

195
def _check_method_and_attr_name(node_type: str, name: str) -> list[str]:
20✔
196
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
197
    method and instance or class attribute names.
198

199
    Returns an empty list if `name` is a valid method, instance, or attribute name."""
200
    error_msgs = []
20✔
201

202
    # Also consider the case of invoking Python's name mangling rules with leading dunderscores.
203
    if not (_is_in_snake_case(name) or (name.startswith("__") and _is_in_snake_case(name[2:]))):
20✔
204
        error_msgs.append(
20✔
205
            f'{node_type.capitalize()} name "{name}" should be in snake_case format. '
206
            f"{node_type.capitalize()} names should be lowercase, with words "
207
            f"separated by underscores. A single leading underscore can be used to "
208
            f"denote a private {node_type} while a double leading underscore invokes "
209
            f"Python's name-mangling rules."
210
        )
211

212
    return error_msgs
20✔
213

214

215
def _check_argument_name(_node_type: str, name: str) -> list[str]:
20✔
216
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
217
    argument names.
218

219
    Returns an empty list if `name` is a valid argument name."""
220
    error_msgs = []
20✔
221

222
    if not _is_in_snake_case(name):
20✔
223
        error_msgs.append(
×
224
            f'Argument name "{name}" should be in snake_case format. Argument names should be '
225
            f"lowercase, with words separated by underscores. A single leading "
226
            f"underscore can be used to indicate that the argument is not being used "
227
            f"but is still needed somehow."
228
        )
229

230
    return error_msgs
20✔
231

232

233
def _check_typevar_name(_node_type: str, name: str) -> list[str]:
20✔
234
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
235
    type variable names.
236

237
    Returns an empty list if `name` is a valid type variable name."""
238
    error_msgs = []
20✔
239

240
    if not _is_in_pascal_case(name):
20✔
241
        error_msgs.append(
20✔
242
            f'Type variable name "{name}" should be in PascalCase format. Type variable '
243
            f"names should have the first letter of each word capitalized with no separation "
244
            f"between each word."
245
        )
246

247
    return error_msgs
20✔
248

249

250
def _check_type_alias_name(_node_type: str, name: str) -> list[str]:
20✔
251
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
252
    type alias names.
253

254
    Returns an empty list if `name` is a valid type alias name."""
255
    error_msgs = []
16✔
256

257
    if not _is_in_pascal_case(name):
16✔
258
        error_msgs.append(
16✔
259
            f'Type alias name "{name}" should be in PascalCase format. Type alias names should '
260
            f"have the first letter of each word capitalized with no separation "
261
            f"between each word."
262
        )
263

264
    return error_msgs
16✔
265

266

267
# Map each variable name type to its corresponding check
268
NAME_CHECK = {
20✔
269
    "module": _check_module_name,
270
    "constant": _check_const_name,
271
    "class": _check_class_name,
272
    "function": _check_function_and_variable_name,
273
    "method": _check_method_and_attr_name,
274
    "attribute": _check_method_and_attr_name,
275
    "argument": _check_argument_name,
276
    "variable": _check_function_and_variable_name,
277
    "class attribute": _check_method_and_attr_name,
278
    "class constant": _check_const_name,
279
    "type variable": _check_typevar_name,
280
    "type alias": _check_type_alias_name,
281
}
282

283

284
class InvalidNameChecker(BaseChecker):
20✔
285
    """A checker class to report on names that don't conform to Python naming conventions.
286

287
    For the Python naming conventions, see https://peps.python.org/pep-0008/#naming-conventions.
288
    """
289

290
    name = "naming_convention_violation"
20✔
291
    msgs = {
20✔
292
        "C9103": (
293
            "%s",
294
            "naming-convention-violation",
295
            "Used when the name doesn't conform to standard Python naming conventions.",
296
        ),
297
        "C9104": (
298
            "%s",
299
            "module-name-violation",
300
            "Used when the name doesn't conform to standard Python naming conventions.",
301
        ),
302
    }
303
    options = (
20✔
304
        (
305
            "ignore-names",
306
            {
307
                "default": "",
308
                "type": "regexp",
309
                "metavar": "<regexp>",
310
                "help": "Ignore C9103 naming convention violation for names that exactly match the pattern",
311
            },
312
        ),
313
        (
314
            "ignore-module-names",
315
            {
316
                "default": "",
317
                "type": "regexp",
318
                "metavar": "<regexp>",
319
                "help": "Ignore C9104 module name violation for module names that exactly match the pattern",
320
            },
321
        ),
322
    )
323

324
    @only_required_for_messages("module-name-violation")
20✔
325
    def visit_module(self, node: nodes.Module) -> None:
20✔
326
        """Visit a Module node to check for any name violations.
327

328
        Snippets taken from pylint.checkers.base.name_checker.checker."""
329
        if not _ignore_name(node.name, self.linter.config.ignore_module_names):
20✔
330
            self._check_name("module", node.name.split(".")[-1], node)
20✔
331

332
    @only_required_for_messages("naming-convention-violation")
20✔
333
    def visit_classdef(self, node: nodes.ClassDef) -> None:
20✔
334
        """Visit a Class node to check for any name violations.
335

336
        Taken from pylint.checkers.base.name_checker.checker."""
337
        if not _ignore_name(node.name, self.linter.config.ignore_names):
20✔
338
            self._check_name("class", node.name, node)
20✔
339

340
    @only_required_for_messages("naming-convention-violation")
20✔
341
    def visit_functiondef(self, node: nodes.FunctionDef) -> None:
20✔
342
        """Visit a FunctionDef node to check for any name violations.
343

344
        Snippets taken from pylint.checkers.base.name_checker.checker."""
345
        if node.is_method():
20✔
346
            if utils.overrides_a_method(node.parent.frame(future=True), node.name):
20✔
UNCOV
347
                return
×
348

349
        if not _ignore_name(node.name, self.linter.config.ignore_names):
20✔
350
            self._check_name("method" if node.is_method() else "function", node.name, node)
20✔
351

352
    visit_asyncfunctiondef = visit_functiondef
20✔
353

354
    @only_required_for_messages("naming-convention-violation")
20✔
355
    def visit_assignname(self, node: nodes.AssignName) -> None:
20✔
356
        """Visit an AssignName node to check for any name violations.
357

358
        Taken from pylint.checkers.base.name_checker.checker."""
359
        # Do not check this node if included in the ignore-names option
360
        if _ignore_name(node.name, self.linter.config.ignore_names):
20✔
361
            return
20✔
362

363
        frame = node.frame()
20✔
364
        assign_type = node.assign_type()
20✔
365

366
        # Check names defined in comprehensions
367
        if isinstance(assign_type, nodes.Comprehension):
20✔
368
            self._check_name("variable", node.name, node)
20✔
369

370
        # Check names defined as function arguments.
371
        elif isinstance(assign_type, nodes.Arguments):
20✔
372
            self._check_name("argument", node.name, node)
20✔
373

374
        # Check names defined in module scope
375
        elif isinstance(frame, nodes.Module) and not _is_in_main(node):
20✔
376
            # Check names defined in Assign nodes
377
            if isinstance(assign_type, nodes.Assign):
20✔
378
                inferred_assign_type = utils.safe_infer(assign_type.value)
20✔
379

380
                # Check TypeVar's and TypeAliases assigned alone or in tuple assignment
381
                if isinstance(node.parent, nodes.Assign):
20✔
382
                    if self._assigns_typevar(assign_type.value):
20✔
383
                        self._check_name("type variable", assign_type.targets[0].name, node)
20✔
384
                        return
20✔
385
                    if self._assigns_typealias(assign_type.value):
20✔
386
                        self._check_name("type alias", assign_type.targets[0].name, node)
×
387
                        return
×
388

389
                if (
20✔
390
                    isinstance(node.parent, nodes.Tuple)
391
                    and isinstance(assign_type.value, nodes.Tuple)
392
                    # protect against unbalanced tuple unpacking
393
                    and node.parent.elts.index(node) < len(assign_type.value.elts)
394
                ):
UNCOV
395
                    assigner = assign_type.value.elts[node.parent.elts.index(node)]
×
UNCOV
396
                    if self._assigns_typevar(assigner):
×
397
                        self._check_name(
×
398
                            "type variable",
399
                            assign_type.targets[0].elts[node.parent.elts.index(node)].name,
400
                            node,
401
                        )
402
                        return
×
UNCOV
403
                    if self._assigns_typealias(assigner):
×
404
                        self._check_name(
×
405
                            "type alias",
406
                            assign_type.targets[0].elts[node.parent.elts.index(node)].name,
407
                            node,
408
                        )
409
                        return
×
410

411
                # Check classes (TypeVar's are classes so they need to be excluded first)
412
                elif isinstance(inferred_assign_type, nodes.ClassDef):
20✔
413
                    self._check_name("class", node.name, node)
×
414

415
                # Don't emit if the name redefines an import in an ImportError except handler.
416
                elif not _redefines_import(node):
20✔
417
                    self._check_name("constant", node.name, node)
20✔
418
                else:
419
                    self._check_name("variable", node.name, node)
×
420

421
            # Check names defined in AnnAssign nodes
422
            elif isinstance(assign_type, nodes.AnnAssign):
20✔
423
                if utils.is_assign_name_annotated_with(node, "Final"):
16✔
424
                    self._check_name("constant", node.name, node)
×
425
                elif self._assigns_typealias(assign_type.annotation):
16✔
426
                    self._check_name("type alias", node.name, node)
16✔
427

428
        # Check names defined in function scopes
429
        elif isinstance(frame, nodes.FunctionDef):
20✔
430
            # global introduced variable aren't in the function locals
431
            if node.name in frame and node.name not in frame.argnames():
20✔
432
                if not _redefines_import(node):
20✔
433
                    self._check_name("variable", node.name, node)
20✔
434

435
        # Check names defined in class scopes
436
        elif isinstance(frame, nodes.ClassDef):
20✔
437
            if not list(frame.local_attr_ancestors(node.name)):
20✔
438
                for ancestor in frame.ancestors():
20✔
439
                    if utils.is_enum(ancestor) or utils.is_assign_name_annotated_with(
20✔
440
                        node, "Final"
441
                    ):
442
                        self._check_name("class constant", node.name, node)
20✔
443
                        break
20✔
444
                    elif utils.is_assign_name_annotated_with(node, "ClassVar"):
20✔
445
                        self._check_name("class attribute", node.name, node)
20✔
446
                        break
20✔
447
                    elif isinstance(node.parent, nodes.AnnAssign):
20✔
448
                        self._check_name("attribute", node.name, node)
20✔
449
                        break
20✔
450
                else:
UNCOV
451
                    self._check_name("class attribute", node.name, node)
×
452

453
    def _check_name(self, node_type: str, name: str, node: nodes.NodeNG) -> None:
20✔
454
        """Check for a name that violates Python naming conventions."""
455
        name_check = NAME_CHECK[node_type]
20✔
456
        error_msgs = name_check(node_type, name)
20✔
457

458
        bad_name_msg = _is_bad_name(name)
20✔
459
        if bad_name_msg:
20✔
460
            error_msgs.append(bad_name_msg)
20✔
461

462
        name_length_msg = _is_within_name_length(node_type, name)
20✔
463
        if name_length_msg:
20✔
464
            error_msgs.append(name_length_msg)
20✔
465

466
        msg_id = "naming-convention-violation" if node_type != "module" else "module-name-violation"
20✔
467
        line_no = 1 if node_type == "module" else None
20✔
468

469
        for msg in error_msgs:
20✔
470
            self.add_message(msgid=msg_id, node=node, args=msg, line=line_no)
20✔
471

472
    @staticmethod
20✔
473
    def _assigns_typevar(node: Optional[nodes.NodeNG]) -> bool:
20✔
474
        """Check if a node is assigning a TypeVar.
475

476
        Taken from pylint.checkers.base.name_checker.checker."""
477
        if isinstance(node, nodes.Call):
20✔
478
            inferred = utils.safe_infer(node.func)
20✔
479
            if isinstance(inferred, nodes.ClassDef) and inferred.qname() in TYPE_VAR_QNAME:
20✔
480
                return True
20✔
481
        return False
20✔
482

483
    @staticmethod
20✔
484
    def _assigns_typealias(node: Optional[nodes.NodeNG]) -> bool:
20✔
485
        """Check if a node is assigning a TypeAlias.
486

487
        Taken from pylint.checkers.base.name_checker.checker."""
488
        inferred = utils.safe_infer(node)
20✔
489
        if isinstance(inferred, nodes.ClassDef):
20✔
490
            qname = inferred.qname()
8✔
491
            if qname == "typing.TypeAlias":
8✔
492
                return True
8✔
UNCOV
493
            if qname == ".Union":
×
494
                # Union is a special case because it can be used as a type alias
495
                # or as a type annotation. We only want to check the former.
496
                assert node is not None
×
497
                return not isinstance(node.parent, nodes.AnnAssign)
×
498
        elif isinstance(inferred, nodes.FunctionDef):
20✔
499
            # TODO: when py3.12 is minimum, remove this condition
500
            # TypeAlias became a class in python 3.12
501
            if inferred.qname() == "typing.TypeAlias":
8✔
502
                return True
8✔
503
        return False
20✔
504

505

506
def register(linter: PyLinter) -> None:
20✔
507
    """Required method to auto-register this checker to the linter"""
508
    linter.register_checker(InvalidNameChecker(linter))
20✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc