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.
- Updated `invalid_name_checker.py` to include a suggested fix for invalid names in its messages.

### 💫 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 @@ -120,18 +120,29 @@ def _ignore_name(name: str, pattern: re.Pattern) -> bool:
return pattern.pattern and pattern.match(name) is not None


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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the _is_in_snake_case method, the regex also requires that the name begin with a letter (a-z). However, the current suggested approach will not correctly return a word that is in snake case.

In general I think it's hard to generate a good suggestion if the variable doesn't start with a letter already.

So I recommend (1) modifying the return type to str | None, returning None when no valid suggestion can be generated, and (2) modifying the check functions below to only display the suggested fix if one is returned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@david-yz-liu Thanks for the feedback! I modified _to_snake_case to return None if name does not start with a letter or a underscore followed by a letter. I also added a test case to verify that no suggested fix is given in this case.



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 if applicable.

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 @@ -176,56 +187,73 @@ class names.

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 if applicable.

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 if applicable.

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 @@ -197,9 +197,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 @@ -252,10 +253,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 @@ -310,10 +312,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 @@ -346,10 +349,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 @@ -382,9 +386,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 @@ -407,9 +412,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 @@ -430,9 +436,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 @@ -468,6 +475,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 @@ -478,10 +509,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 @@ -504,10 +536,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 @@ -813,6 +846,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 @@ -850,9 +884,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