From c29d0e9598203a58d5c22165cb677c4b76ffca4b Mon Sep 17 00:00:00 2001 From: rachelzUT Date: Mon, 18 May 2026 14:41:25 -0400 Subject: [PATCH 1/8] modified check functions using snake_case to include a suggested fix for invalid names --- .../checkers/invalid_name_checker.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py b/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py index 90b4e5722..91ccdc480 100644 --- a/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py +++ b/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py @@ -120,17 +120,24 @@ 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: + """Converts a name to snake_case format.""" + return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower() + + 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): + suggested_name = _to_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." + f"names, with each name separated by underscores. " + f'Suggested fix: "{suggested_name}".' ) return error_msgs @@ -176,17 +183,19 @@ 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. Returns an empty list if `name` is a valid function or variable name.""" error_msgs = [] if name != "_" and not _is_in_snake_case(name): + suggested_name = _to_snake_case(name) error_msgs.append( f'{node_type.capitalize()} name "{name}" should be in snake_case format. ' 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}." + f"denote a private {node_type}. " + f'Suggested fix: "{suggested_name}".' ) return error_msgs @@ -194,19 +203,21 @@ def _check_function_and_variable_name(node_type: str, name: str) -> list[str]: 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:]))): + suggested_name = _to_snake_case(name) error_msgs.append( f'{node_type.capitalize()} name "{name}" should be in snake_case format. ' 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." + f"Python's name-mangling rules. " + f'Suggested fix: "{suggested_name}".' ) return error_msgs @@ -214,17 +225,19 @@ def _check_method_and_attr_name(node_type: str, name: str) -> list[str]: 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): + suggested_name = _to_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." + f"but is still needed somehow. " + f'Suggested fix: "{suggested_name}".' ) return error_msgs From 648fd79d24dda9d1be257da5fc27f9bed3e07c21 Mon Sep 17 00:00:00 2001 From: rachelzUT Date: Mon, 18 May 2026 14:52:52 -0400 Subject: [PATCH 2/8] updated tests for snake_case to include suggested name --- .../test_invalid_name_checker.py | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py b/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py index cc231e001..2028fa37b 100644 --- a/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py +++ b/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py @@ -199,7 +199,8 @@ def NotSnakeCase(): 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"be used to denote a private function. " + f'Suggested fix: "not_snake_case".' ) with self.assertAddsMessages( @@ -255,7 +256,8 @@ def AlsoAlsoNotSnakeCase(self): 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"Python's name-mangling rules. " + f'Suggested fix: "also_also_not_snake_case".' ) with self.assertAddsMessages( @@ -313,7 +315,8 @@ class BadClass: 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"Python's name-mangling rules. " + f'Suggested fix: "also_not_snake_case".' ) with self.assertAddsMessages( @@ -349,7 +352,8 @@ def bad(AlsoNotSnakeCase): 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"but is still needed somehow. " + f'Suggested fix: "also_not_snake_case".' ) with self.assertAddsMessages( @@ -384,7 +388,8 @@ def foo(): 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"be used to denote a private variable. " + f'Suggested fix: "why_is_this_not_in_snake_case".' ) with self.assertAddsMessages( @@ -409,7 +414,8 @@ def test_variable_name_redefined_import_violation(self) -> None: 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"be used to denote a private variable. " + f'Suggested fix: "not_snake_case".' ) with self.assertAddsMessages( @@ -432,7 +438,8 @@ def foo(): 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"be used to denote a private variable. " + f'Suggested fix: "bad_name".' ) with self.assertAddsMessages( @@ -481,7 +488,8 @@ class BadClass: 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"leading underscore invokes Python's name-mangling rules. " + f'Suggested fix: "not_snaking".' ) with self.assertAddsMessages( @@ -507,7 +515,8 @@ class BadClass: 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"leading underscore invokes Python's name-mangling rules. " + f'Suggested fix: "not_snaking".' ) with self.assertAddsMessages( @@ -813,7 +822,8 @@ 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"Modules should be all-lowercase names, with each name separated by underscores." + f"Modules should be all-lowercase names, with each name separated by underscores. " + f'Suggested fix: "invalid_module_name".' ) with self.assertAddsMessages( @@ -852,7 +862,8 @@ def NotSnakeCase(): 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"be used to denote a private function. " + f'Suggested fix: "not_snake_case".' ) with self.assertAddsMessages( From 16db55ed5636a7615056c6b59f02336a81763e87 Mon Sep 17 00:00:00 2001 From: rachelzUT Date: Mon, 18 May 2026 14:58:17 -0400 Subject: [PATCH 3/8] updated changelog --- packages/python-ta/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/python-ta/CHANGELOG.md b/packages/python-ta/CHANGELOG.md index b7cfcca9d..a764ee49f 100644 --- a/packages/python-ta/CHANGELOG.md +++ b/packages/python-ta/CHANGELOG.md @@ -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 From a6df48b961f30f730d63cd0f7dc0eb299fcd1450 Mon Sep 17 00:00:00 2001 From: rachelzUT Date: Thu, 21 May 2026 15:59:59 -0400 Subject: [PATCH 4/8] edited _to_snake_case to return None if no snake case name conversion can be made --- .../checkers/invalid_name_checker.py | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py b/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py index 91ccdc480..58a0e52e8 100644 --- a/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py +++ b/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py @@ -120,25 +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: - """Converts a name to snake_case format.""" +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 _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 and provides a suggested correction. + 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): suggested_name = _to_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. " - f'Suggested fix: "{suggested_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 @@ -183,27 +187,31 @@ 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 and provides a suggested correction. + 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): suggested_name = _to_snake_case(name) - error_msgs.append( - f'{node_type.capitalize()} name "{name}" should be in snake_case format. ' + 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}. " - f'Suggested fix: "{suggested_name}".' + 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 and provides a suggested correction. + 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 = [] @@ -211,14 +219,18 @@ def _check_method_and_attr_name(node_type: str, name: str) -> list[str]: # 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:]))): suggested_name = _to_snake_case(name) - error_msgs.append( - f'{node_type.capitalize()} name "{name}" should be in snake_case format. ' + 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. " - f'Suggested fix: "{suggested_name}".' + f"Python's name-mangling rules." ) + error_msgs.append(msg) return error_msgs @@ -232,13 +244,16 @@ def _check_argument_name(_node_type: str, name: str) -> list[str]: if not _is_in_snake_case(name): suggested_name = _to_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. " - f'Suggested fix: "{suggested_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 From a398c15c8c606c8498df8fb3ef1475c44b734790 Mon Sep 17 00:00:00 2001 From: rachelzUT Date: Thu, 21 May 2026 16:05:06 -0400 Subject: [PATCH 5/8] edited snake case tests formatting and added test case for no suggested fix --- .../test_invalid_name_checker.py | 118 +++++++++++------- 1 file changed, 71 insertions(+), 47 deletions(-) diff --git a/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py b/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py index 2028fa37b..ca0dabb1a 100644 --- a/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py +++ b/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py @@ -197,10 +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'Suggested fix: "not_snake_case".' + 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( @@ -253,11 +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'Suggested fix: "also_also_not_snake_case".' + 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( @@ -312,11 +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'Suggested fix: "also_not_snake_case".' + 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( @@ -349,11 +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'Suggested fix: "also_not_snake_case".' + 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( @@ -386,10 +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'Suggested fix: "why_is_this_not_in_snake_case".' + 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( @@ -412,10 +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'Suggested fix: "not_snake_case".' + 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( @@ -436,10 +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'Suggested fix: "bad_name".' + 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( @@ -475,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 = """ @@ -485,11 +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'Suggested fix: "not_snaking".' + 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( @@ -512,11 +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'Suggested fix: "not_snaking".' + 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( @@ -822,8 +846,8 @@ 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"Modules should be all-lowercase names, with each name separated by underscores. " - f'Suggested fix: "invalid_module_name".' + f'Suggested fix: "invalid_module_name". ' + f"Modules should be all-lowercase names, with each name separated by underscores." ) with self.assertAddsMessages( @@ -860,10 +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'Suggested fix: "not_snake_case".' + 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( From e19c477fd61fcdf6c8fd82c2fb2a8e01517b58f1 Mon Sep 17 00:00:00 2001 From: rachelzUT Date: Sat, 23 May 2026 12:29:25 -0400 Subject: [PATCH 6/8] updated changelog --- packages/python-ta/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python-ta/CHANGELOG.md b/packages/python-ta/CHANGELOG.md index a764ee49f..582761e49 100644 --- a/packages/python-ta/CHANGELOG.md +++ b/packages/python-ta/CHANGELOG.md @@ -16,7 +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. +- Updated `invalid_name_checker.py` to include a suggested fix for invalid names in checks using snake_case format. ### 💫 New checkers From 2f6eb6f44e5d10818789215212f996e72d0e056c Mon Sep 17 00:00:00 2001 From: rachelzUT Date: Sat, 23 May 2026 17:00:37 -0400 Subject: [PATCH 7/8] merged upstream master --- packages/python-ta/CHANGELOG.md | 1 + .../checkers/invalid_name_checker.py | 105 +++++++++++++----- .../test_invalid_name_checker.py | 96 +++++++++++----- 3 files changed, 147 insertions(+), 55 deletions(-) diff --git a/packages/python-ta/CHANGELOG.md b/packages/python-ta/CHANGELOG.md index 582761e49..7e6356923 100644 --- a/packages/python-ta/CHANGELOG.md +++ b/packages/python-ta/CHANGELOG.md @@ -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` - Updated `invalid_name_checker.py` to include a suggested fix for invalid names in checks using snake_case format. ### 💫 New checkers diff --git a/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py b/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py index 58a0e52e8..da7b3378c 100644 --- a/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py +++ b/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py @@ -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. """ @@ -82,6 +82,46 @@ 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 _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. @@ -120,13 +160,6 @@ 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() - - 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 and provides a suggested correction if applicable. @@ -149,16 +182,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." @@ -169,18 +205,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 @@ -260,34 +299,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 diff --git a/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py b/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py index ca0dabb1a..3f9a8955b 100644 --- a/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py +++ b/packages/python-ta/tests/test_custom_checkers/test_invalid_name_checker.py @@ -10,7 +10,12 @@ 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_snake_case, + _to_upper_case_with_underscores, +) class TestInvalidNameChecker(pylint.testutils.CheckerTestCase): @@ -68,8 +73,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." ) @@ -92,8 +98,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." ) @@ -137,9 +144,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( @@ -162,9 +170,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( @@ -579,6 +588,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." @@ -618,9 +628,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( @@ -642,9 +653,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( @@ -680,9 +692,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( @@ -706,9 +719,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( @@ -732,9 +746,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( @@ -923,3 +938,32 @@ 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") + + 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" + ) From 4f65f221ad666e6d2962efea15bd67a6915f93ff Mon Sep 17 00:00:00 2001 From: rachelzUT Date: Sat, 23 May 2026 17:10:02 -0400 Subject: [PATCH 8/8] edited check function docstrings --- .../src/python_ta/checkers/invalid_name_checker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py b/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py index da7b3378c..e1e0d71a1 100644 --- a/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py +++ b/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py @@ -162,7 +162,7 @@ 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 and provides a suggested correction if applicable. + module names and provides a suggested correction. Returns an empty list if `name` is a valid module name.""" error_msgs = [] @@ -226,7 +226,7 @@ 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 and provides a suggested correction if applicable. + function and variable names and provides a suggested correction. Returns an empty list if `name` is a valid function or variable name.""" error_msgs = [] @@ -250,7 +250,7 @@ def _check_function_and_variable_name(node_type: str, name: str) -> list[str]: 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 and provides a suggested correction if applicable. + 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 = []