Skip to content
1 change: 1 addition & 0 deletions packages/python-ta/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- 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`
- Updated `invalid_name_checker.py` to include a suggested fix for invalid names in checks using snake_case format.

### 💫 New checkers

Expand Down
62 changes: 45 additions & 17 deletions packages/python-ta/src/python_ta/checkers/invalid_name_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ def _to_upper_case_with_underscores(name: str) -> str | None:
return prefix + "_".join(word.upper() for word in words) + suffix


def _to_snake_case(name: str) -> str | None:
"""Returns name converted to snake_case format or None if no valid suggestion can be made."""
if not re.match(r"_?[A-Za-z]", name):
return None
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower()


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

Expand Down Expand Up @@ -155,16 +162,20 @@ def _ignore_name(name: str, pattern: re.Pattern) -> bool:

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

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

if not _is_in_snake_case(name):
error_msgs.append(
f'Module name "{name}" should be in snake_case format. Modules should be all-lowercase '
f"names, with each name separated by underscores."
)
suggested_name = _to_snake_case(name)
msg = f'Module name "{name}" should be in snake_case format. '

if suggested_name:
msg += f'Suggested fix: "{suggested_name}". '

msg += f"Modules should be all-lowercase names, with each name separated by underscores."
error_msgs.append(msg)

return error_msgs

Expand Down Expand Up @@ -215,56 +226,73 @@ class names and provides a suggested correction.

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

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

if name != "_" and not _is_in_snake_case(name):
error_msgs.append(
f'{node_type.capitalize()} name "{name}" should be in snake_case format. '
suggested_name = _to_snake_case(name)
msg = f'{node_type.capitalize()} name "{name}" should be in snake_case format. '

if suggested_name:
msg += f'Suggested fix: "{suggested_name}". '

msg += (
f"{node_type.capitalize()} names should be lowercase, with words "
f"separated by underscores. A single leading underscore can be used to "
f"denote a private {node_type}."
)
error_msgs.append(msg)

return error_msgs


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

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

# Also consider the case of invoking Python's name mangling rules with leading dunderscores.
if not (_is_in_snake_case(name) or (name.startswith("__") and _is_in_snake_case(name[2:]))):
error_msgs.append(
f'{node_type.capitalize()} name "{name}" should be in snake_case format. '
suggested_name = _to_snake_case(name)
msg = f'{node_type.capitalize()} name "{name}" should be in snake_case format. '

if suggested_name:
msg += f'Suggested fix: "{suggested_name}". '

msg += (
f"{node_type.capitalize()} names should be lowercase, with words "
f"separated by underscores. A single leading underscore can be used to "
f"denote a private {node_type} while a double leading underscore invokes "
f"Python's name-mangling rules."
)
error_msgs.append(msg)

return error_msgs


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

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

if not _is_in_snake_case(name):
error_msgs.append(
f'Argument name "{name}" should be in snake_case format. Argument names should be '
f"lowercase, with words separated by underscores. A single leading "
f"underscore can be used to indicate that the argument is not being used "
f"but is still needed somehow."
suggested_name = _to_snake_case(name)
msg = f'Argument name "{name}" should be in snake_case format. '
if suggested_name:
msg += f'Suggested fix: "{suggested_name}". '

msg += (
f"Argument names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to indicate that the argument is "
f"not being used but is still needed somehow."
)
error_msgs.append(msg)

return error_msgs

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from python_ta.checkers.invalid_name_checker import (
InvalidNameChecker,
_to_pascal_case,
_to_snake_case,
_to_upper_case_with_underscores,
)

Expand Down Expand Up @@ -205,9 +206,10 @@ def NotSnakeCase():
functiondef_node, *_ = mod.nodes_of_class(nodes.FunctionDef)
name = functiondef_node.name
msg = (
f'Function name "{name}" should be in snake_case format. Function names should be '
f"lowercase, with words separated by underscores. A single leading underscore can "
f"be used to denote a private function."
f'Function name "{name}" should be in snake_case format. '
f'Suggested fix: "not_snake_case". '
f"Function names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to denote a private function."
)

with self.assertAddsMessages(
Expand Down Expand Up @@ -260,10 +262,11 @@ def AlsoAlsoNotSnakeCase(self):
functiondef_node, *_ = mod.nodes_of_class(nodes.FunctionDef)
name = functiondef_node.name
msg = (
f'Method name "{name}" should be in snake_case format. Method names should be '
f"lowercase, with words separated by underscores. A single leading underscore can "
f"be used to denote a private method while a double leading underscore invokes "
f"Python's name-mangling rules."
f'Method name "{name}" should be in snake_case format. '
f'Suggested fix: "also_also_not_snake_case". '
f"Method names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to denote a private method while "
f"a double leading underscore invokes Python's name-mangling rules."
)

with self.assertAddsMessages(
Expand Down Expand Up @@ -318,10 +321,11 @@ class BadClass:
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Attribute name "{name}" should be in snake_case format. Attribute names should be '
f"lowercase, with words separated by underscores. A single leading underscore can "
f"be used to denote a private attribute while a double leading underscore invokes "
f"Python's name-mangling rules."
f'Attribute name "{name}" should be in snake_case format. '
f'Suggested fix: "also_not_snake_case". '
f"Attribute names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to denote a private attribute while "
f"a double leading underscore invokes Python's name-mangling rules."
)

with self.assertAddsMessages(
Expand Down Expand Up @@ -354,10 +358,11 @@ def bad(AlsoNotSnakeCase):
argument_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = argument_node.name
msg = (
f'Argument name "{name}" should be in snake_case format. Argument names should be '
f"lowercase, with words separated by underscores. A single leading "
f"underscore can be used to indicate that the argument is not being used "
f"but is still needed somehow."
f'Argument name "{name}" should be in snake_case format. '
f'Suggested fix: "also_not_snake_case". '
f"Argument names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to indicate that the argument is "
f"not being used but is still needed somehow."
)

with self.assertAddsMessages(
Expand Down Expand Up @@ -390,9 +395,10 @@ def foo():
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Variable name "{name}" should be in snake_case format. Variable names should be '
f"lowercase, with words separated by underscores. A single leading underscore can "
f"be used to denote a private variable."
f'Variable name "{name}" should be in snake_case format. '
f'Suggested fix: "why_is_this_not_in_snake_case". '
f"Variable names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to denote a private variable."
)

with self.assertAddsMessages(
Expand All @@ -415,9 +421,10 @@ def test_variable_name_redefined_import_violation(self) -> None:
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Variable name "{name}" should be in snake_case format. Variable names should be '
f"lowercase, with words separated by underscores. A single leading underscore can "
f"be used to denote a private variable."
f'Variable name "{name}" should be in snake_case format. '
f'Suggested fix: "not_snake_case". '
f"Variable names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to denote a private variable."
)

with self.assertAddsMessages(
Expand All @@ -438,9 +445,10 @@ def foo():
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Variable name "{name}" should be in snake_case format. Variable names should be '
f"lowercase, with words separated by underscores. A single leading underscore can "
f"be used to denote a private variable."
f'Variable name "{name}" should be in snake_case format. '
f'Suggested fix: "bad_name". '
f"Variable names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to denote a private variable."
)

with self.assertAddsMessages(
Expand Down Expand Up @@ -476,6 +484,30 @@ def test_variable_name_underscore(self) -> None:
with self.assertNoMessages():
self.checker.visit_assignname(assignname_node)

def test_variable_name_first_char_violation(self) -> None:
"""Test that the checker correctly reports a variable name that starts with a non-letter character
and does not provide a suggested fix."""
src = """
def f():
_9bad_name = 10
"""
mod = astroid.parse(src)
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Variable name "{name}" should be in snake_case format. '
f"Variable names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to denote a private variable."
)

with self.assertAddsMessages(
pylint.testutils.MessageTest(
msg_id="naming-convention-violation", node=assignname_node, args=msg
),
ignore_position=True,
):
self.checker.visit_assignname(assignname_node)

def test_class_attribute_name_violation(self) -> None:
"""Test that the checker correctly reports an invalid class attribute name."""
src = """
Expand All @@ -486,10 +518,11 @@ class BadClass:
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Class attribute name "{name}" should be in snake_case format. Class attribute names '
f"should be lowercase, with words separated by underscores. A single leading "
f"underscore can be used to denote a private class attribute while a double "
f"leading underscore invokes Python's name-mangling rules."
f'Class attribute name "{name}" should be in snake_case format. '
f'Suggested fix: "not_snaking". '
f"Class attribute names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to denote a private class attribute while "
f"a double leading underscore invokes Python's name-mangling rules."
)

with self.assertAddsMessages(
Expand All @@ -512,10 +545,11 @@ class BadClass:
assignname_node, *_ = mod.nodes_of_class(nodes.AssignName)
name = assignname_node.name
msg = (
f'Class attribute name "{name}" should be in snake_case format. Class attribute names '
f"should be lowercase, with words separated by underscores. A single leading "
f"underscore can be used to denote a private class attribute while a double "
f"leading underscore invokes Python's name-mangling rules."
f'Class attribute name "{name}" should be in snake_case format. '
f'Suggested fix: "not_snaking". '
f"Class attribute names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to denote a private class attribute while "
f"a double leading underscore invokes Python's name-mangling rules."
)

with self.assertAddsMessages(
Expand Down Expand Up @@ -827,6 +861,7 @@ def test_default_ignore_module_names_invalid(self):
module_node.name = "InvalidModuleName"
msg = (
f'Module name "{module_node.name}" should be in snake_case format. '
f'Suggested fix: "invalid_module_name". '
f"Modules should be all-lowercase names, with each name separated by underscores."
)

Expand Down Expand Up @@ -864,9 +899,10 @@ def NotSnakeCase():
functiondef_node, *_ = mod.nodes_of_class(nodes.FunctionDef)
name = functiondef_node.name
msg = (
f'Function name "{name}" should be in snake_case format. Function names should be '
f"lowercase, with words separated by underscores. A single leading underscore can "
f"be used to denote a private function."
f'Function name "{name}" should be in snake_case format. '
f'Suggested fix: "not_snake_case". '
f"Function names should be lowercase, with words separated by underscores. "
f"A single leading underscore can be used to denote a private function."
)

with self.assertAddsMessages(
Expand Down Expand Up @@ -920,3 +956,14 @@ def test_to_uppercase_with_underscores(self) -> None:
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")

def test_to_snake_case(self) -> None:
"""Test that names are correctly converted to snake_case."""
self.assertEqual(_to_snake_case("snake_case"), "snake_case")
self.assertEqual(_to_snake_case("PascalCase"), "pascal_case")
self.assertEqual(_to_snake_case("UPPER_CASE_NAME"), "upper_case_name")
self.assertEqual(_to_snake_case("_MIXED_CaseName"), "_mixed_case_name")
self.assertEqual(_to_snake_case("_5first_char_non_letter"), None)
self.assertEqual(
_to_snake_case("_name_with_num_not_first_10"), "_name_with_num_not_first_10"
)