diff --git a/packages/python-ta/CHANGELOG.md b/packages/python-ta/CHANGELOG.md index 3a2c2443a..7e6356923 100644 --- a/packages/python-ta/CHANGELOG.md +++ b/packages/python-ta/CHANGELOG.md @@ -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 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 f7032b4ea..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 @@ -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. @@ -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 @@ -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 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 15fb90e30..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 @@ -13,6 +13,7 @@ from python_ta.checkers.invalid_name_checker import ( InvalidNameChecker, _to_pascal_case, + _to_snake_case, _to_upper_case_with_underscores, ) @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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 = """ @@ -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( @@ -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( @@ -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." ) @@ -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( @@ -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" + )