diff --git a/README.md b/README.md index 682d296..3e97baf 100644 --- a/README.md +++ b/README.md @@ -6,39 +6,31 @@ As a lab, certain standards have been generally adopted, but have had a hard tim ## Enforced Styles -1. Camel case is enforced on all function and variable name. +1. snake_case is enforced on all function and variable name. 2. The use of \_\_ or name mangling is disallowed. -3. Docstring argument definitions must not start with a capital letter and must not use a period at the end. To indicate additional information in a sentence like format, use semicolons to separate sections. The first letters of these sections can also not be capitalized. +3. Docstring argument definitions must start with a capital letter and must use a period at the end. To indicate additional information in a sentence like format, use semicolons to separate sections. The first letters of these sections should also be capitalized. 4. Functions are required to have a docstring. -5. Function arguments must have a type annotation. +5. Function arguments may not use mutable items(list, set, dict) as defaults, as python handles defaults stupidly. -6. Function arguments may not use mutable items(list, set, dict) as defaults, as python handles defaults stupidly. +6. Functions must specify a return type. -7. Functions must specify a return type. +7. Class names must be in pascal case (ex. ExampleClass) -8. Class names must be in pascal case (ex. ExampleClass) +8. First line of docstring must end with period -9. First line of docstring must end with period +9. Classes must have a docstring -10. Classes must have a docstring +10. Function arguments must be documented in a section started with `Args: \n` -11. Function rguments must be documented in a section started with `Args: \n` +11. Docstring argument names must be up to date with the function arguments' names -12. Docstring argument type must be up to date with the function arguments' type annotations +12. Argument defaults must be documented in the `Args: ` section in a section of their corresponding arguments saying `defaults to {default value}` -13. Docstring argument names must be up to date with the function arguments' names - -14. When specifying a docstring arguments type, if the argument has a default, the type must be followed with `, optional` - -15. Argument defaults must be documented in the `Args: ` section in a section of their corresponding arguments saying `defaults to {default value}` - -16. A function's return type must be documented in a section started with `Returns: \n` - -17. A function's return type must be exactly equal to the documented type +13. A function's return value must be documented in a section started with `Returns: \n` ## Adding action to your workflow @@ -47,7 +39,7 @@ This action is best used in the same section as the meds action. Just add it as ```github steps: - name: Follow Python Standard - uses: byuawsfhtl/PythonStandardAction@v1.1.0 + uses: byuawsfhtl/PythonStandardAction@v1.2.0 ``` ## Excluding files from the check diff --git a/checkers/imports.py b/checkers/imports.py index 77dd6a7..a235921 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -1,4 +1,5 @@ import ast +import re from pathlib import Path import models as models @@ -158,17 +159,24 @@ def _check_unused_imports(imports: list[ast.Import | ast.ImportFrom], names_used the list of unused import errors, if any """ errors = [] + + # Read file content for simple text-based checks + try: + with open(file_path, 'r', encoding='utf-8') as f: + file_content = f.read() + except: + file_content = "" for imp in imports: if isinstance(imp, ast.Import): - errors.extend(_check_unused_import_nodes(imp, names_used, file_path, ignore_codes)) + errors.extend(_check_unused_import_nodes(imp, names_used, file_path, ignore_codes, file_content)) elif isinstance(imp, ast.ImportFrom): - errors.extend(_check_unused_from_import_nodes(imp, names_used, file_path, ignore_codes)) + errors.extend(_check_unused_from_import_nodes(imp, names_used, file_path, ignore_codes, file_content)) return errors -def _check_unused_import_nodes(imp: ast.Import, names_used: set[str], file_path: str, ignore_codes: set[str]) -> list[models.StyleError]: +def _check_unused_import_nodes(imp: ast.Import, names_used: set[str], file_path: str, ignore_codes: set[str], file_content: str) -> list[models.StyleError]: """Check unused names in an ast.Import node. Args: @@ -176,22 +184,27 @@ def _check_unused_import_nodes(imp: ast.Import, names_used: set[str], file_path: names_used: the complete set of the lib names used within the file file_path: Path to file being checked ignore_codes: set of error codes to ignore + file_content: content of the file for text-based searching Returns: the list of unused import errors, if any """ errors = [] + for alias in imp.names: imported_name = alias.asname if alias.asname else alias.name.split('.')[0] - if imported_name in names_used: + + # Skip if name is used in AST or appears in file content (covers string annotations, TYPE_CHECKING, etc.) + if imported_name in names_used or re.search(rf'\b{re.escape(imported_name)}\b', file_content): continue + error = error_creation_module.create_error(imp, 'I101', f"Unused import '{imported_name}'", file_path, ignore_codes) if error: errors.append(error) return errors -def _check_unused_from_import_nodes(imp: ast.ImportFrom, names_used: set[str], file_path: str, ignore_codes: set[str]) -> list[models.StyleError]: +def _check_unused_from_import_nodes(imp: ast.ImportFrom, names_used: set[str], file_path: str, ignore_codes: set[str], file_content: str) -> list[models.StyleError]: """Check unused names in an ast.ImportFrom node. Args: @@ -199,17 +212,26 @@ def _check_unused_from_import_nodes(imp: ast.ImportFrom, names_used: set[str], f names_used: the complete set of the lib names used within the file file_path: Path to file being checked ignore_codes: set of error codes to ignore + file_content: content of the file for text-based searching Returns: an unused import error, or None """ errors = [] + + # Never flag __future__ imports as unused + if imp.module == '__future__': + return errors + for alias in imp.names: if alias.name == '*': continue # wildcard imports not checked here imported_name = alias.asname if alias.asname else alias.name - if imported_name in names_used: + + # Skip if name is used in AST or appears in file content (covers string annotations, TYPE_CHECKING, etc.) + if imported_name in names_used or re.search(rf'\b{re.escape(imported_name)}\b', file_content): continue + error = error_creation_module.create_error(imp, 'I101', f"Unused import '{imported_name}' from '{imp.module or '.'}'", file_path, ignore_codes) if error: errors.append(error) @@ -251,11 +273,23 @@ def _check_relative_imports(imports: list[ast.ImportFrom], file_path: str, ignor list of style errors found """ errors = [] + + # Check if this is an __init__.py file - allow all relative imports there + file_path_obj = Path(file_path) + is_init_file = file_path_obj.name == '__init__.py' + for imp in imports: - if imp.level <= 0: + if imp.level <= 0 or is_init_file: continue - # Relative import (starts with dots) - error = error_creation_module.create_error(imp, 'I103', f"Relative import should be absolute", file_path, ignore_codes) + + # Flag deep relative imports (..module, ...module, etc.) as potentially problematic + error = error_creation_module.create_error( + imp, + 'I103', + f"Deep relative import (level {imp.level}) should be avoided - consider using absolute imports", + file_path, + ignore_codes + ) if error: errors.append(error) return errors \ No newline at end of file