Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/python-ta/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Extended `snapshot` to distinguish between nonlocal variables and local variables within a stack frame.
- Make `watchdog` an optional dependency; users can opt in with `pip install python-ta[watchdog]`. This affects runs of `python_ta.check_all` with the `watch` config option set to `True`.
- Added `LSPReporter`, a new reporter that outputs lint diagnostics in LSP 3.17-compliant JSON format.
- Added suggested fixes for pascal and uppercase names in `invalid_name_checker.py`

### 💫 New checkers

Expand Down
91 changes: 69 additions & 22 deletions packages/python-ta/src/python_ta/checkers/invalid_name_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def _is_in_pascal_case(name: str) -> bool:
def _is_in_upper_case_with_underscores(name: str) -> bool:
"""Returns whether `name` is in UPPER_CASE_WITH_UNDERSCORES.

`name` is in `UPPER_CASE_WITH_UNDERSCORES if:
`name` is in `UPPER_CASE_WITH_UNDERSCORES` if:
- each word is in uppercase, and
- words are separated by an underscore.
"""
Expand All @@ -82,6 +82,39 @@ def _is_in_upper_case_with_underscores(name: str) -> bool:
return re.match(pattern, name) is not None


def _parse_name(name: str) -> tuple[str, list[str] | None, str]:
"""Extracts the prefix, words, and suffix from `name`."""
name_match = re.match(r"(_*)(.*?)(_*)$", name)
if not name_match:
return "", None, ""
prefix, core, suffix = name_match.groups()
prefix = "_" if prefix else ""
if core and core[0].isdigit():
return "", None, ""
core = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", core)
core = re.sub(r"([A-Z])([A-Z][a-z])", r"\1_\2", core)

return prefix, [word for word in core.split("_") if word], suffix


def _to_pascal_case(name: str) -> str | None:
"""Returns a PascalCase version of `name`."""
prefix, words, _ = _parse_name(name)
if words is None:
return None

return prefix + "".join(word[0].upper() + word[1:] for word in words)


def _to_upper_case_with_underscores(name: str) -> str | None:
"""Returns an UPPER_CASE_WITH_UNDERSCORES version of `name`."""
prefix, words, suffix = _parse_name(name)
if words is None:
return None

return prefix + "_".join(word.upper() for word in words) + suffix


def _is_bad_name(name: str) -> str:
"""Returns a string detailing why `name` is a bad name.

Expand Down Expand Up @@ -138,16 +171,19 @@ def _check_module_name(_node_type: str, name: str) -> list[str]:

def _check_const_name(node_type: str, name: str) -> list[str]:
"""Returns a list of strings, each detailing how `name` violates Python naming conventions for
constant and class constant names.
constant and class constant names and provides a suggested correction.

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

if not _is_in_upper_case_with_underscores(name):
msg = (
f'{node_type.capitalize()} name "{name}" should be in UPPER_CASE_WITH_UNDERSCORES format. '
f"Constants should be all-uppercase words with each word separated by an "
f"underscore. A single leading underscore can be used to denote a private constant."
suggested_name = _to_upper_case_with_underscores(name)
msg = f'{node_type.capitalize()} name "{name}" should be in UPPER_CASE_WITH_UNDERSCORES format. '
if suggested_name:
msg += f'Suggested fix: "{suggested_name}". '
msg += (
"Constants should be all-uppercase words with each word separated by an "
"underscore. A single leading underscore can be used to denote a private constant."
)
if node_type == "class constant":
msg += " A double leading underscore invokes Python's name-mangling rules."
Expand All @@ -158,18 +194,21 @@ def _check_const_name(node_type: str, name: str) -> list[str]:

def _check_class_name(_node_type: str, name: str) -> list[str]:
"""Returns a list of strings, each detailing how `name` violates Python naming conventions for
class names.
class names and provides a suggested correction.

Returns an empty list if `name` is a valid class name."""
error_msgs = []

if not _is_in_pascal_case(name):
error_msgs.append(
f'Class name "{name}" should be in PascalCase format. Class names should have the '
f"first letter of each word capitalized with no separation between each "
f"word. A single leading underscore can be used to denote a private "
f"class."
suggested_name = _to_pascal_case(name)
msg = f'Class name "{name}" should be in PascalCase format. '
if suggested_name:
msg += f'Suggested fix: "{suggested_name}". '
msg += (
"Class names should have the first letter of each word capitalized with no separation "
"between each word. A single leading underscore can be used to denote a private class."
)
error_msgs.append(msg)

return error_msgs

Expand Down Expand Up @@ -232,34 +271,42 @@ def _check_argument_name(_node_type: str, name: str) -> list[str]:

def _check_typevar_name(_node_type: str, name: str) -> list[str]:
"""Returns a list of strings, each detailing how `name` violates Python naming conventions for
type variable names.
type variable names and provides a suggested correction.

Returns an empty list if `name` is a valid type variable name."""
error_msgs = []

if not _is_in_pascal_case(name):
error_msgs.append(
f'Type variable name "{name}" should be in PascalCase format. Type variable '
f"names should have the first letter of each word capitalized with no separation "
f"between each word."
suggested_name = _to_pascal_case(name)
msg = f'Type variable name "{name}" should be in PascalCase format. '
if suggested_name:
msg += f'Suggested fix: "{suggested_name}". '
msg += (
"Type variable names should have the first letter of each word "
"capitalized with no separation between each word."
)
error_msgs.append(msg)

return error_msgs


def _check_type_alias_name(_node_type: str, name: str) -> list[str]:
"""Returns a list of strings, each detailing how `name` violates Python naming conventions for
type alias names.
type alias names and provides a suggested correction.

Returns an empty list if `name` is a valid type alias name."""
error_msgs = []

if not _is_in_pascal_case(name):
error_msgs.append(
f'Type alias name "{name}" should be in PascalCase format. Type alias names should '
f"have the first letter of each word capitalized with no separation "
f"between each word."
suggested_name = _to_pascal_case(name)
msg = f'Type alias name "{name}" should be in PascalCase format. '
if suggested_name:
msg += f'Suggested fix: "{suggested_name}". '
msg += (
"Type alias names should have the first letter of each word "
"capitalized with no separation between each word."
)
error_msgs.append(msg)

return error_msgs

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
from astroid import nodes

import python_ta
from python_ta.checkers.invalid_name_checker import InvalidNameChecker
from python_ta.checkers.invalid_name_checker import (
InvalidNameChecker,
_to_pascal_case,
_to_upper_case_with_underscores,
)


class TestInvalidNameChecker(pylint.testutils.CheckerTestCase):
Expand Down Expand Up @@ -68,8 +72,9 @@ def test_const_name_violation(self) -> None:
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Constant name "{name}" should be in UPPER_CASE_WITH_UNDERSCORES format. Constants '
f"should be all-uppercase words with each word separated by an underscore. A "
f'Constant name "{name}" should be in UPPER_CASE_WITH_UNDERSCORES format. '
f'Suggested fix: "CONST_NOT_UPPER". '
f"Constants should be all-uppercase words with each word separated by an underscore. A "
f"single leading underscore can be used to denote a private constant."
)

Expand All @@ -92,8 +97,9 @@ def test_const_name_annotated_violation(self) -> None:
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Constant name "{name}" should be in UPPER_CASE_WITH_UNDERSCORES format. Constants '
f"should be all-uppercase words with each word separated by an underscore. A "
f'Constant name "{name}" should be in UPPER_CASE_WITH_UNDERSCORES format. '
f'Suggested fix: "CONST_NOT_UPPER". '
f"Constants should be all-uppercase words with each word separated by an underscore. A "
f"single leading underscore can be used to denote a private constant."
)

Expand Down Expand Up @@ -137,9 +143,10 @@ class notPascalcase:
classdef_node, *_ = mod.nodes_of_class(nodes.ClassDef)
name = classdef_node.name
msg = (
f'Class name "{name}" should be in PascalCase format. Class names should have the '
f"first letter of each word capitalized with no separation between each word. A "
f"single leading underscore can be used to denote a private class."
f'Class name "{name}" should be in PascalCase format. '
f'Suggested fix: "NotPascalcase". '
f"Class names should have the first letter of each word capitalized with no separation "
f"between each word. A single leading underscore can be used to denote a private class."
)

with self.assertAddsMessages(
Expand All @@ -162,9 +169,10 @@ class MyClass:
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Class name "{name}" should be in PascalCase format. Class names should have the '
f"first letter of each word capitalized with no separation between each word. A "
f"single leading underscore can be used to denote a private class."
f'Class name "{name}" should be in PascalCase format. '
f'Suggested fix: "SnakeCase". '
f"Class names should have the first letter of each word capitalized with no separation "
f"between each word. A single leading underscore can be used to denote a private class."
)

with self.assertAddsMessages(
Expand Down Expand Up @@ -546,6 +554,7 @@ class BadClass:
name = assignname_node.name
msg = (
f'Class constant name "{name}" should be in UPPER_CASE_WITH_UNDERSCORES format. '
f'Suggested fix: "OOGA_BOOGA". '
f"Constants should be all-uppercase words with each word separated by an "
f"underscore. A single leading underscore can be used to denote a private "
f"constant. A double leading underscore invokes Python's name-mangling rules."
Expand Down Expand Up @@ -585,9 +594,10 @@ def test_typevar_name_violation(self) -> None:
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Type variable name "{name}" should be in PascalCase format. Type variable names '
f"should have the first letter of each word capitalized with no separation between "
f"each word."
f'Type variable name "{name}" should be in PascalCase format. '
f'Suggested fix: "TypeVar". '
f"Type variable names should have the first letter of each word capitalized with "
f"no separation between each word."
)

with self.assertAddsMessages(
Expand All @@ -609,9 +619,10 @@ def test_typevar_name_tuple_violation(self) -> None:
_, assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Type variable name "{name}" should be in PascalCase format. Type variable names '
f"should have the first letter of each word capitalized with no separation between "
f"each word."
f'Type variable name "{name}" should be in PascalCase format. '
f'Suggested fix: "TypeVar". '
f"Type variable names should have the first letter of each word capitalized with "
f"no separation between each word."
)

with self.assertAddsMessages(
Expand Down Expand Up @@ -647,9 +658,10 @@ def test_typealias_name_violation(self) -> None:
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Type alias name "{name}" should be in PascalCase format. Type alias names should '
f"have the first letter of each word capitalized with no separation between each "
f"word."
f'Type alias name "{name}" should be in PascalCase format. '
f'Suggested fix: "NotPascal". '
f"Type alias names should have the first letter of each word capitalized with "
f"no separation between each word."
)

with self.assertAddsMessages(
Expand All @@ -673,9 +685,10 @@ def test_typealias_name_union_violation(self) -> None:
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Type alias name "{name}" should be in PascalCase format. Type alias names should '
f"have the first letter of each word capitalized with no separation between each "
f"word."
f'Type alias name "{name}" should be in PascalCase format. '
f'Suggested fix: "NotPascal". '
f"Type alias names should have the first letter of each word capitalized with "
f"no separation between each word."
)

with self.assertAddsMessages(
Expand All @@ -699,9 +712,10 @@ def test_typealias_name_tuple_violation(self) -> None:
_, assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Type alias name "{name}" should be in PascalCase format. Type alias names should '
f"have the first letter of each word capitalized with no separation between each "
f"word."
f'Type alias name "{name}" should be in PascalCase format. '
f'Suggested fix: "NotPascal". '
f"Type alias names should have the first letter of each word capitalized with "
f"no separation between each word."
)

with self.assertAddsMessages(
Expand Down Expand Up @@ -888,3 +902,21 @@ def test_module_name_no_snippet() -> None:
snippet = reporter.messages[file_fixture][0].snippet

assert snippet == ""


class TestNamingConventionHelpers(unittest.TestCase):
def test_to_pascal_case(self) -> None:
"""Test that names are correctly converted to PascalCase."""
self.assertEqual(_to_pascal_case("snake_case"), "SnakeCase")
self.assertEqual(_to_pascal_case("PascalCase"), "PascalCase")
self.assertEqual(_to_pascal_case("_UPPER_CASE_NAME"), "_UPPERCASENAME")
self.assertEqual(_to_pascal_case("__varName_here_"), "_VarNameHere")
self.assertEqual(_to_pascal_case("parseJSONText"), "ParseJSONText")

def test_to_uppercase_with_underscores(self) -> None:
"""Test that names are correctly converted to UPPERCASE_WITH_UNDERSCORES."""
self.assertEqual(_to_upper_case_with_underscores("snake_case"), "SNAKE_CASE")
self.assertEqual(_to_upper_case_with_underscores("PascalCase"), "PASCAL_CASE")
self.assertEqual(_to_upper_case_with_underscores("_UPPER_CASE_NAME"), "_UPPER_CASE_NAME")
self.assertEqual(_to_upper_case_with_underscores("__varName_here_"), "_VAR_NAME_HERE_")
self.assertEqual(_to_upper_case_with_underscores("parseJSONText"), "PARSE_JSON_TEXT")