Skip to content
Merged

yep #42

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
32 changes: 12 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
52 changes: 43 additions & 9 deletions checkers/imports.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ast
import re
from pathlib import Path

import models as models
Expand Down Expand Up @@ -158,58 +159,79 @@ 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:
imp: the import within the file
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:
imp: the import within the file
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)
Expand Down Expand Up @@ -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