From 40bb0d8c8b20aab700011702bb0edd16249ead09 Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Mon, 15 Jun 2026 13:58:59 -0600 Subject: [PATCH 01/13] Initial setup and linter fixes --- .gitignore | 4 +++- StandardCheck.py | 4 ++-- checkers/common_nodes.py | 18 +++++++++--------- checkers/imports.py | 8 ++++---- checkers/security.py | 4 ++-- config.py | 2 +- requirements.txt | 2 +- 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index d5e4f15..5ad7957 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -**/__pycache__ \ No newline at end of file +**/__pycache__ +venv +.venv \ No newline at end of file diff --git a/StandardCheck.py b/StandardCheck.py index 7bf9cfe..d3ae647 100644 --- a/StandardCheck.py +++ b/StandardCheck.py @@ -16,7 +16,7 @@ import checkers.complexity as complexity_module -def visit_node(node: ast.AST, file_path: str, ignore_codes: set[str], ignore_names: set[str] = None) -> list[models.StyleError]: +def visit_node(node: ast.AST, file_path: str, ignore_codes: set[str], ignore_names: set[str] = set()) -> list[models.StyleError]: """Visit an AST node and perform checks. Args: @@ -40,7 +40,7 @@ def visit_node(node: ast.AST, file_path: str, ignore_codes: set[str], ignore_nam return [] -def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] = None, config: dict[str, Any] = None) -> list[models.StyleError]: +def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] = set(), config: dict[str, Any] = {}) -> list[models.StyleError]: """Check a single Python file. Args: diff --git a/checkers/common_nodes.py b/checkers/common_nodes.py index 4416386..e976138 100644 --- a/checkers/common_nodes.py +++ b/checkers/common_nodes.py @@ -6,7 +6,7 @@ import utils.patterns as patterns_module import checkers.error_creation as error_creation_module -def check_variable(node: ast.Name, file_path: str, ignore_codes: set[str], ignore_names: set[str] = None) -> list[models.StyleError]: +def check_variable(node: ast.Name, file_path: str, ignore_codes: set[str], ignore_names: set[str] = set()) -> list[models.StyleError]: """Check variable naming. Args: @@ -18,7 +18,7 @@ def check_variable(node: ast.Name, file_path: str, ignore_codes: set[str], ignor Returns: list of style errors found """ - errors = [] + errors: list[models.StyleError] = [] ignore_names = ignore_names or set() # Skip if name should be ignored @@ -38,7 +38,7 @@ def check_variable(node: ast.Name, file_path: str, ignore_codes: set[str], ignor return errors -def check_class(node: ast.ClassDef, file_path: str, ignore_codes: set[str], ignore_names: set[str] = None) -> list[models.StyleError]: +def check_class(node: ast.ClassDef, file_path: str, ignore_codes: set[str], ignore_names: set[str] = set()) -> list[models.StyleError]: """Check class definition. Args: @@ -50,7 +50,7 @@ def check_class(node: ast.ClassDef, file_path: str, ignore_codes: set[str], igno Returns: list of style errors found """ - errors = [] + errors: list[models.StyleError] = [] ignore_names = ignore_names or set() # Skip if name should be ignored @@ -98,7 +98,7 @@ def _is_valid_class_name(name: str) -> bool: return False -def check_function(node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str, ignore_codes: set[str], ignore_names: set[str] = None) -> list[models.StyleError]: +def check_function(node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str, ignore_codes: set[str], ignore_names: set[str] = set()) -> list[models.StyleError]: """Check function definition. Args: @@ -110,7 +110,7 @@ def check_function(node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str, Returns: list of style errors found """ - errors = [] + errors: list[models.StyleError] = [] ignore_names = ignore_names or set() is_test_file = 'test' in file_path.lower() @@ -199,7 +199,7 @@ def _check_function_docstrings(node: ast.FunctionDef | ast.AsyncFunctionDef, fil Returns: list of style errors related to function docstrings """ - errors = [] + errors: list[models.StyleError] = [] # Skip docstring checks for @overload functions if _has_overload_decorator(node): @@ -230,7 +230,7 @@ def _check_docstring_format(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.C Returns: list of style errors found """ - errors = [] + errors: list[models.StyleError] = [] if not docstring: return errors summary = docstring.split('\n\n')[0].strip() @@ -264,7 +264,7 @@ def _check_function_docstring(node: ast.FunctionDef | ast.AsyncFunctionDef, file Returns: list of style errors found """ - errors = [] + errors: list[models.StyleError] = [] docstring = ast.get_docstring(node) if not docstring: diff --git a/checkers/imports.py b/checkers/imports.py index a235921..ca9345e 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -24,7 +24,7 @@ def collect_imports(tree: ast.AST) -> tuple[list[ast.Import | ast.ImportFrom], l return imports + import_froms, import_froms -def check_imports(all_imports: list[ast.AST], import_froms: list[ast.ImportFrom], used_names: set[str], file_path: Path, ignore_codes: set[str]) -> list[models.StyleError]: +def check_imports(all_imports: list[ast.Import | ast.ImportFrom], import_froms: list[ast.ImportFrom], used_names: set[str], file_path: Path, ignore_codes: set[str]) -> list[models.StyleError]: """Run all import-related checks. Args: @@ -56,7 +56,7 @@ def _check_import_order(imports: list[ast.Import | ast.ImportFrom], file_path: s Returns: list of style errors found """ - errors = [] + errors: list[models.StyleError] = [] if not imports: return errors @@ -164,7 +164,7 @@ def _check_unused_imports(imports: list[ast.Import | ast.ImportFrom], names_used try: with open(file_path, 'r', encoding='utf-8') as f: file_content = f.read() - except: + except(Exception): file_content = "" for imp in imports: @@ -217,7 +217,7 @@ def _check_unused_from_import_nodes(imp: ast.ImportFrom, names_used: set[str], f Returns: an unused import error, or None """ - errors = [] + errors: list[models.StyleError] = [] # Never flag __future__ imports as unused if imp.module == '__future__': diff --git a/checkers/security.py b/checkers/security.py index 11990ff..49fc940 100644 --- a/checkers/security.py +++ b/checkers/security.py @@ -115,7 +115,7 @@ def _check_sql_injection_call(node: ast.AST, file_path: str, ignore_codes: set[s Returns: the sql injection errors, if any """ - errors = [] + errors: list[models.StyleError] = [] if not isinstance(node, ast.Call): return errors if not (isinstance(node.func, ast.Attribute) and node.func.attr in ['execute', 'executemany']): @@ -167,7 +167,7 @@ def _check_shell_injection_call(node: ast.AST, file_path: str, ignore_codes: set Returns: the shell injection errors, if any """ - errors = [] + errors: list[models.StyleError] = [] if not isinstance(node, ast.Call): return errors is_bad_attr = isinstance(node.func, ast.Attribute) and node.func.attr in ['system', 'popen', 'spawn', 'exec'] diff --git a/config.py b/config.py index d1a8dcc..d36641b 100644 --- a/config.py +++ b/config.py @@ -48,7 +48,7 @@ def load_ignore_names() -> set[str]: Returns: Set of names to ignore in style checking """ - ignore_names = set() + ignore_names: set = set() ignore_file = Path('.standardignore') if not ignore_file.exists(): diff --git a/requirements.txt b/requirements.txt index 1855cd0..e94e123 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pathspec==0.12.1 \ No newline at end of file +pathspec ~= 1.1 \ No newline at end of file From 1929fa75e8ad06c0b6aceca86d78be90675c7b3e Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Mon, 15 Jun 2026 14:11:02 -0600 Subject: [PATCH 02/13] Fixed some odd setting of variables --- StandardCheck.py | 3 --- checkers/common_nodes.py | 3 --- checkers/complexity.py | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/StandardCheck.py b/StandardCheck.py index d3ae647..9761558 100644 --- a/StandardCheck.py +++ b/StandardCheck.py @@ -28,7 +28,6 @@ def visit_node(node: ast.AST, file_path: str, ignore_codes: set[str], ignore_nam Returns: list of style errors found """ - ignore_names = ignore_names or set() if isinstance(node, ast.ClassDef): return common_nodes_module.check_class(node, file_path, ignore_codes, ignore_names) @@ -53,8 +52,6 @@ def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] = list of style errors found """ errors = [] - ignore_names = ignore_names or set() - config = config or {} max_complexity = config.get('max_complexity', 15) max_indentation = config.get('max_indentation', 4) diff --git a/checkers/common_nodes.py b/checkers/common_nodes.py index e976138..df5c5d1 100644 --- a/checkers/common_nodes.py +++ b/checkers/common_nodes.py @@ -19,7 +19,6 @@ def check_variable(node: ast.Name, file_path: str, ignore_codes: set[str], ignor list of style errors found """ errors: list[models.StyleError] = [] - ignore_names = ignore_names or set() # Skip if name should be ignored if file_utils_module.should_ignore_name(node.id, ignore_names): @@ -51,7 +50,6 @@ def check_class(node: ast.ClassDef, file_path: str, ignore_codes: set[str], igno list of style errors found """ errors: list[models.StyleError] = [] - ignore_names = ignore_names or set() # Skip if name should be ignored if file_utils_module.should_ignore_name(node.name, ignore_names): @@ -111,7 +109,6 @@ def check_function(node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str, list of style errors found """ errors: list[models.StyleError] = [] - ignore_names = ignore_names or set() is_test_file = 'test' in file_path.lower() if file_utils_module.should_ignore_name(node.name, ignore_names): diff --git a/checkers/complexity.py b/checkers/complexity.py index 0c79ffe..3bede76 100644 --- a/checkers/complexity.py +++ b/checkers/complexity.py @@ -3,7 +3,7 @@ import models as models -def check_complexity(tree: ast.Module, content: str, file_path: str, ignore_codes: set[str], max_complexity: int = 15, max_indentation: int = 4) -> list[models.StyleError]: +def check_complexity(tree: ast.AST, content: str, file_path: str, ignore_codes: set[str], max_complexity: int = 15, max_indentation: int = 4) -> list[models.StyleError]: """Check cyclomatic complexity and indentation depth for all functions in a file. Args: From 43c876323ccaec875172ba73c5b34eee84736053 Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Mon, 15 Jun 2026 14:23:48 -0600 Subject: [PATCH 03/13] Fixed the documentation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9d2b02a..40b3645 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,12 @@ The above entries in a `.standardignore` would have the checker skip over the fo Using the `.standardignore` file specified in the section above, specific function and variable names can be skipped over on the check. -The syntax to do so is the use of the `!` before the name of the variable/function to ignore. +The syntax to do so is the use of the `name:` before the name of the variable/function to ignore. For example ```cmd -!sleep_for_retry +name: sleep_for_retry ``` The above example ignores the sleep_for_retry function when applying standards as the name is required as it is an overwrite of an outside modules functionality. From 9e85c34616e68e70024b89febda81d06079093ed Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Mon, 15 Jun 2026 14:30:17 -0600 Subject: [PATCH 04/13] Running a quick experiment on the empty brackets --- StandardCheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StandardCheck.py b/StandardCheck.py index 9761558..484f2cd 100644 --- a/StandardCheck.py +++ b/StandardCheck.py @@ -39,7 +39,7 @@ def visit_node(node: ast.AST, file_path: str, ignore_codes: set[str], ignore_nam return [] -def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] = set(), config: dict[str, Any] = {}) -> list[models.StyleError]: +def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] = set(), config: dict[str, Any] = None) -> list[models.StyleError]: """Check a single Python file. Args: From 601a13f0da14c0c40dcc4b9d94f33c8067590459 Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Mon, 15 Jun 2026 14:32:14 -0600 Subject: [PATCH 05/13] Minor type security improvment, standard check fix --- StandardCheck.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/StandardCheck.py b/StandardCheck.py index 484f2cd..538590c 100644 --- a/StandardCheck.py +++ b/StandardCheck.py @@ -39,7 +39,7 @@ def visit_node(node: ast.AST, file_path: str, ignore_codes: set[str], ignore_nam return [] -def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] = set(), config: dict[str, Any] = None) -> list[models.StyleError]: +def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] = set(), config: dict[str, Any] | None = None) -> list[models.StyleError]: """Check a single Python file. Args: @@ -51,6 +51,9 @@ def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] = Returns: list of style errors found """ + if not config: + config = {} + errors = [] max_complexity = config.get('max_complexity', 15) max_indentation = config.get('max_indentation', 4) From d569ab27734e2c3cf286b13a308b31081bb61db2 Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Mon, 15 Jun 2026 16:52:43 -0600 Subject: [PATCH 06/13] Added initial go at writing the linting check --- LintingCheck.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 10 +++++ action.yml | 12 +++++- config.py | 22 +++++++++++ requirements.txt | 3 +- 5 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 LintingCheck.py diff --git a/LintingCheck.py b/LintingCheck.py new file mode 100644 index 0000000..a169678 --- /dev/null +++ b/LintingCheck.py @@ -0,0 +1,97 @@ +from argparse import ArgumentParser +from pathlib import Path +from sys import stderr +from typing import Optional +from pathspec import PathSpec +from config import load_mypy_arguments, load_ignore_patterns +from utils.file_utils import should_ignore_file +from mypy.api import run as mypy_api_run + + +def main() -> int: + """Main entry point for the style checker. + + Returns: + Exit code (0 for success, 1 for errors found) + """ + parser = ArgumentParser(description='Modern Python style checker') + parser.add_argument('paths', nargs='*', default=['.'], help='Paths to check (default: current directory)') + parser.add_argument('--config', type=Path, help='Path to configuration file') + parser.add_argument('--ignore', action='append', help='Error codes to ignore') + parser.add_argument('--max-complexity', type=int, default=15, help='Maximum cyclomatic complexity (default: 15)') + + args = parser.parse_args() + + ignore_patterns = load_ignore_patterns() + mypy_args = load_mypy_arguments() + + all_errors: list = [] + + for path_str in args.path: + path = Path(path_str) + if path.is_file(): + if should_ignore_file(path, ignore_patterns): + continue + else: + ignored_mypy_normal_output, mypy_errors, ignored_mypy_return_value = run_mypy_on_file(path_str, mypy_args) + all_errors.extend(mypy_errors) + elif path.is_dir(): + mypy_errors = run_mypy_on_directory(path, ignore_patterns, mypy_args) + all_errors.extend(mypy_errors) + else: + print(f"Warning: Path not found: {path}", file=stderr) + + if all_errors: + all_errors.sort(key=lambda e: (e.file_path, e.line_number)) + for mypy_error in all_errors: + print(mypy_error) + print(f"\nFound {len(all_errors)} mypy errors.") + return 1 + + else: + print("Passed all mypy checks!") + return 0 + + +def run_mypy_on_directory(directory: Path, ignore_patterns: Optional[PathSpec], args: list[str] | None = None) -> list: + """Check all Python files in a directory recursively. + + Args: + directory: Directory to check + ignore_patterns: Patterns for files to ignore + + Returns: + list of style errors found + """ + errors_in_directory: list = [] + + for file_path in directory.rglob('*.py'): + if should_ignore_file(file_path, ignore_patterns): + continue + + else: + ignored_mypy_normal_output, mypy_errors, ignored_mypy_return_value = run_mypy_on_file(str(file_path), args) + errors_in_directory.extend(mypy_errors) + + return errors_in_directory + + +def run_mypy_on_file(file_path_string: str, args: list[str] | None = None) -> tuple: + """Run mypy on a single python file. + + Args: + file_path_string: The string representation of the path to the + file that should be checked + args: The arguments that should be passed into the mypy call + + Returns: + The result of calling mypy on the file + """ + args_with_file_path_at_start = [] + + if not args: + args_with_file_path_at_start = [f"{file_path_string}", "--strict"] + else: + args_with_file_path_at_start.insert(0, f"{file_path_string}") + + return mypy_api_run(args_with_file_path_at_start) \ No newline at end of file diff --git a/README.md b/README.md index 40b3645..67b3d80 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,13 @@ So, if I were trying to run it on TreeTapper, and both TreeTapper and PythonStan python "../PythonStandardAction/StandardCheck.py" ``` +## Notes about the MyPy checker step + +The MyPy checker step is going to default to running with the '--strict' call on every file in the application unless the entire file is ignored. Note that it doesn't ignore specific functions, classes, or lines since they are instead ignored with the usual "# type: ignore" comment that it otherwise uses. If you want to run this with different MyPy settings and arguments, simply add "mypyargs: [Your arguments here]" to the ".standardignore" file in the format as follows: + +```cmd +mypyargs: --ignore-missing-imports --deprecated-calls-exclude +``` + +Note that doing something like this will automatically disable the '--strict' tag in MyPy unless it is specifically included in the arguments. Regardless of if custom args are passed in or not, this step will automatically figure out the needed file path arguments so it is not necessary to add this to the beginning and will actually cause errors if you do. + diff --git a/action.yml b/action.yml index a641e86..ca7c6f6 100644 --- a/action.yml +++ b/action.yml @@ -14,7 +14,15 @@ runs: - name: Install dependencies from requirements.txt shell: bash run: pip install -r "${{ github.action_path }}/requirements.txt" + + - name: Get any missing type stubs for MyPy check + shell: bash + run: mypy --install-types --non-interactive - - name: Run Check + - name: Run Standard Check + shell: bash + run: python "${{ github.action_path }}/StandardCheck.py" + + - name: Run mypy check with args shell: bash - run: python "${{ github.action_path }}/StandardCheck.py" \ No newline at end of file + run: \ No newline at end of file diff --git a/config.py b/config.py index d36641b..6759737 100644 --- a/config.py +++ b/config.py @@ -42,6 +42,28 @@ def load_ignore_patterns() -> Optional[pathspec.PathSpec]: return pathspec.PathSpec.from_lines('gitwildmatch', patterns) +def load_mypy_arguments() -> list[str] | None: + + argument_file = Path('.standardignore') + args = [] + + if not argument_file.exists(): + return None + + with open(argument_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + + for line in lines: + line = line.strip() + if line.startswith("mypyargs:"): + args = line[9:].strip().split() + + if args: + return args + else: + return None + + def load_ignore_names() -> set[str]: """Load specific names to ignore from .standardignore file. diff --git a/requirements.txt b/requirements.txt index e94e123..1b89842 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -pathspec ~= 1.1 \ No newline at end of file +pathspec ~= 1.1 +mypy ~= 2.1 \ No newline at end of file From 6909fdb0067fc08bdedaf1a0be99175de623a857 Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Mon, 15 Jun 2026 16:54:19 -0600 Subject: [PATCH 07/13] Minor spelling update so it doesn't bother me --- LintingCheck.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LintingCheck.py b/LintingCheck.py index a169678..063929e 100644 --- a/LintingCheck.py +++ b/LintingCheck.py @@ -45,11 +45,11 @@ def main() -> int: all_errors.sort(key=lambda e: (e.file_path, e.line_number)) for mypy_error in all_errors: print(mypy_error) - print(f"\nFound {len(all_errors)} mypy errors.") + print(f"\nFound {len(all_errors)} MyPy errors.") return 1 else: - print("Passed all mypy checks!") + print("Passed all MyPy checks!") return 0 From 3510de18459aba8c30a871c91bb34f9fd8851fbf Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Tue, 23 Jun 2026 11:30:41 -0600 Subject: [PATCH 08/13] Finished fixing linting check errors --- LintingCheck.py | 17 +++++++++++------ README.md | 10 ++++++++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/LintingCheck.py b/LintingCheck.py index 063929e..4efb682 100644 --- a/LintingCheck.py +++ b/LintingCheck.py @@ -1,6 +1,6 @@ from argparse import ArgumentParser from pathlib import Path -from sys import stderr +from sys import stderr, exit from typing import Optional from pathspec import PathSpec from config import load_mypy_arguments, load_ignore_patterns @@ -14,11 +14,10 @@ def main() -> int: Returns: Exit code (0 for success, 1 for errors found) """ + print("Starting the Linting Check") + parser = ArgumentParser(description='Modern Python style checker') parser.add_argument('paths', nargs='*', default=['.'], help='Paths to check (default: current directory)') - parser.add_argument('--config', type=Path, help='Path to configuration file') - parser.add_argument('--ignore', action='append', help='Error codes to ignore') - parser.add_argument('--max-complexity', type=int, default=15, help='Maximum cyclomatic complexity (default: 15)') args = parser.parse_args() @@ -27,15 +26,17 @@ def main() -> int: all_errors: list = [] - for path_str in args.path: + for path_str in args.paths: path = Path(path_str) if path.is_file(): + print("Found a file") if should_ignore_file(path, ignore_patterns): continue else: ignored_mypy_normal_output, mypy_errors, ignored_mypy_return_value = run_mypy_on_file(path_str, mypy_args) all_errors.extend(mypy_errors) elif path.is_dir(): + print("Found a directory") mypy_errors = run_mypy_on_directory(path, ignore_patterns, mypy_args) all_errors.extend(mypy_errors) else: @@ -94,4 +95,8 @@ def run_mypy_on_file(file_path_string: str, args: list[str] | None = None) -> tu else: args_with_file_path_at_start.insert(0, f"{file_path_string}") - return mypy_api_run(args_with_file_path_at_start) \ No newline at end of file + return mypy_api_run(args_with_file_path_at_start) + + +if __name__ == '__main__': + exit(main()) \ No newline at end of file diff --git a/README.md b/README.md index 67b3d80..a14de97 100644 --- a/README.md +++ b/README.md @@ -62,18 +62,24 @@ The above example ignores the sleep_for_retry function when applying standards a ## Running locally so you don't have to wait on github actions -from the root fo the repo you're trying to check, run the following in a terminal: +From the root fo the repo you're trying to check, run the following in a terminal: ```cmd python "path-to-this-repo/StandardCheck.py" ``` -So, if I were trying to run it on TreeTapper, and both TreeTapper and PythonStandardAction were in the same folder, I would run +So, if I were trying to run it on TreeTapper, and both TreeTapper and PythonStandardAction were in the same folder, I would run: ```cmd python "../PythonStandardAction/StandardCheck.py" ``` +This can also be done for the linting step by running the same command, but replacing the `StandardCheck.py` reference with the `LintingCheck.py` referece as follows: + +```cmd +python "path-to-this-repo/LintingCheck.py" +``` + ## Notes about the MyPy checker step The MyPy checker step is going to default to running with the '--strict' call on every file in the application unless the entire file is ignored. Note that it doesn't ignore specific functions, classes, or lines since they are instead ignored with the usual "# type: ignore" comment that it otherwise uses. If you want to run this with different MyPy settings and arguments, simply add "mypyargs: [Your arguments here]" to the ".standardignore" file in the format as follows: From cc0ce73a1f6458eab681f696ed278aae55b918bc Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Tue, 23 Jun 2026 15:56:24 -0600 Subject: [PATCH 09/13] Readme updates from temp branch --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a14de97..3d7bc97 100644 --- a/README.md +++ b/README.md @@ -62,24 +62,34 @@ The above example ignores the sleep_for_retry function when applying standards a ## Running locally so you don't have to wait on github actions -From the root fo the repo you're trying to check, run the following in a terminal: +From the root of the repo you're trying to check, run the following in a terminal: ```cmd python "path-to-this-repo/StandardCheck.py" ``` -So, if I were trying to run it on TreeTapper, and both TreeTapper and PythonStandardAction were in the same folder, I would run: +The `python` command uses whatever virtual environment is currently active. Before running the checker, install this action's requirements into that environment: ```cmd +pip install -r "path-to-this-repo/requirements.txt" +``` + +So, if I were trying to run it on TreeTapper, and both TreeTapper and PythonStandardAction were in the same folder, I would activate TreeTapper's virtual environment and run: + +```cmd +pip install -r "../PythonStandardAction/requirements.txt" python "../PythonStandardAction/StandardCheck.py" ``` -This can also be done for the linting step by running the same command, but replacing the `StandardCheck.py` reference with the `LintingCheck.py` referece as follows: +This can also be done for the linting step by running the same command, but replacing the `StandardCheck.py` reference with the `LintingCheck.py` reference as follows: ```cmd -python "path-to-this-repo/LintingCheck.py" +python "../PythonStandardAction/LintingCheck.py" ``` +If the repository has an in-repo virtual environment, add it to `.standardignore` so `StandardCheck.py` and `LintingCheck.py` do not scan installed packages while walking the repository. + + ## Notes about the MyPy checker step The MyPy checker step is going to default to running with the '--strict' call on every file in the application unless the entire file is ignored. Note that it doesn't ignore specific functions, classes, or lines since they are instead ignored with the usual "# type: ignore" comment that it otherwise uses. If you want to run this with different MyPy settings and arguments, simply add "mypyargs: [Your arguments here]" to the ".standardignore" file in the format as follows: From 4bbb15fa74ec0383dd871050addec431bdf65526 Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Tue, 23 Jun 2026 16:21:23 -0600 Subject: [PATCH 10/13] Quick fix for mypy call --- LintingCheck.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/LintingCheck.py b/LintingCheck.py index 4efb682..0b3e17f 100644 --- a/LintingCheck.py +++ b/LintingCheck.py @@ -33,12 +33,12 @@ def main() -> int: if should_ignore_file(path, ignore_patterns): continue else: - ignored_mypy_normal_output, mypy_errors, ignored_mypy_return_value = run_mypy_on_file(path_str, mypy_args) - all_errors.extend(mypy_errors) + mypy_return = run_mypy_on_file(path_str, mypy_args) + all_errors.extend(mypy_return) elif path.is_dir(): print("Found a directory") - mypy_errors = run_mypy_on_directory(path, ignore_patterns, mypy_args) - all_errors.extend(mypy_errors) + mypy_return = run_mypy_on_directory(path, ignore_patterns, mypy_args) + all_errors.extend(mypy_return) else: print(f"Warning: Path not found: {path}", file=stderr) @@ -71,13 +71,13 @@ def run_mypy_on_directory(directory: Path, ignore_patterns: Optional[PathSpec], continue else: - ignored_mypy_normal_output, mypy_errors, ignored_mypy_return_value = run_mypy_on_file(str(file_path), args) + mypy_errors = run_mypy_on_file(str(file_path), args) errors_in_directory.extend(mypy_errors) return errors_in_directory -def run_mypy_on_file(file_path_string: str, args: list[str] | None = None) -> tuple: +def run_mypy_on_file(file_path_string: str, args: list[str] | None = None) -> list: """Run mypy on a single python file. Args: @@ -95,7 +95,28 @@ def run_mypy_on_file(file_path_string: str, args: list[str] | None = None) -> tu else: args_with_file_path_at_start.insert(0, f"{file_path_string}") - return mypy_api_run(args_with_file_path_at_start) + mypy_output_to_standard, mypy_output_to_error, mypy_return_value = mypy_api_run(args_with_file_path_at_start) + + # Note that despite what mypy says, it actually writes the various linting errors to the *standard* + # output, not the error output. It writes fatal errors caused by odds and ends to it's error output, + # so it's necessary to check both the standard the the error output to actually find all of the + # desired errors + + # If mypy returns with no errors, we can return an empty list of errors + if mypy_return_value == 0: + return [] + + # Otherwise we need to filter everything mypy prints so that we only have the return errors + errors_to_return = [] + for output_source in (mypy_output_to_standard, mypy_output_to_error): + for line in output_source.splitlines(): + if "error:" in line: + errors_to_return.append(line) + + return errors_to_return + + + if __name__ == '__main__': From 28fb85f618849d670a87612698bb2c6a3d148a54 Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Tue, 23 Jun 2026 16:36:02 -0600 Subject: [PATCH 11/13] standard check fixes, debugging --- LintingCheck.py | 1 - README.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/LintingCheck.py b/LintingCheck.py index 0b3e17f..04b1e41 100644 --- a/LintingCheck.py +++ b/LintingCheck.py @@ -43,7 +43,6 @@ def main() -> int: print(f"Warning: Path not found: {path}", file=stderr) if all_errors: - all_errors.sort(key=lambda e: (e.file_path, e.line_number)) for mypy_error in all_errors: print(mypy_error) print(f"\nFound {len(all_errors)} MyPy errors.") diff --git a/README.md b/README.md index 3d7bc97..3d56fec 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ pip install -r "../PythonStandardAction/requirements.txt" python "../PythonStandardAction/StandardCheck.py" ``` -This can also be done for the linting step by running the same command, but replacing the `StandardCheck.py` reference with the `LintingCheck.py` reference as follows: +This process can also be done for the linting step by running the same commands, but replacing the `StandardCheck.py` reference with the `LintingCheck.py` reference as follows: ```cmd python "../PythonStandardAction/LintingCheck.py" From c2f0e92bc2db0d7ac1a93e3ab761a1fa8bd476cf Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Tue, 23 Jun 2026 16:39:21 -0600 Subject: [PATCH 12/13] Docstring for new function --- LintingCheck.py | 1 + config.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/LintingCheck.py b/LintingCheck.py index 04b1e41..6b45c09 100644 --- a/LintingCheck.py +++ b/LintingCheck.py @@ -59,6 +59,7 @@ def run_mypy_on_directory(directory: Path, ignore_patterns: Optional[PathSpec], Args: directory: Directory to check ignore_patterns: Patterns for files to ignore + args: A list of all of the arguments to be passed into MyPy Returns: list of style errors found diff --git a/config.py b/config.py index 6759737..ef50ea6 100644 --- a/config.py +++ b/config.py @@ -43,6 +43,12 @@ def load_ignore_patterns() -> Optional[pathspec.PathSpec]: def load_mypy_arguments() -> list[str] | None: + """This function loads all of the aruments needed to override the default + ones for MyPy, if applicable, from the .standardignore file. + + Returns: + A list of arguments to pass into MyPy, or None + """ argument_file = Path('.standardignore') args = [] From ec3fa95339ddf0f32daea164f36090f755471543 Mon Sep 17 00:00:00 2001 From: Emaniacinator Date: Wed, 24 Jun 2026 14:07:37 -0600 Subject: [PATCH 13/13] Requested changes --- LintingCheck.py | 2 -- StandardCheck.py | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/LintingCheck.py b/LintingCheck.py index 6b45c09..2fb8596 100644 --- a/LintingCheck.py +++ b/LintingCheck.py @@ -29,14 +29,12 @@ def main() -> int: for path_str in args.paths: path = Path(path_str) if path.is_file(): - print("Found a file") if should_ignore_file(path, ignore_patterns): continue else: mypy_return = run_mypy_on_file(path_str, mypy_args) all_errors.extend(mypy_return) elif path.is_dir(): - print("Found a directory") mypy_return = run_mypy_on_directory(path, ignore_patterns, mypy_args) all_errors.extend(mypy_return) else: diff --git a/StandardCheck.py b/StandardCheck.py index 538590c..718fd33 100644 --- a/StandardCheck.py +++ b/StandardCheck.py @@ -16,7 +16,7 @@ import checkers.complexity as complexity_module -def visit_node(node: ast.AST, file_path: str, ignore_codes: set[str], ignore_names: set[str] = set()) -> list[models.StyleError]: +def visit_node(node: ast.AST, file_path: str, ignore_codes: set[str], ignore_names: set[str] | None = None) -> list[models.StyleError]: """Visit an AST node and perform checks. Args: @@ -28,6 +28,8 @@ def visit_node(node: ast.AST, file_path: str, ignore_codes: set[str], ignore_nam Returns: list of style errors found """ + if not ignore_names: + ignore_names = set() if isinstance(node, ast.ClassDef): return common_nodes_module.check_class(node, file_path, ignore_codes, ignore_names) @@ -39,7 +41,7 @@ def visit_node(node: ast.AST, file_path: str, ignore_codes: set[str], ignore_nam return [] -def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] = set(), config: dict[str, Any] | None = None) -> list[models.StyleError]: +def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] | None = None, config: dict[str, Any] | None = None) -> list[models.StyleError]: """Check a single Python file. Args: @@ -51,6 +53,9 @@ def check_file(file_path: Path, ignore_codes: set[str], ignore_names: set[str] = Returns: list of style errors found """ + if not ignore_names: + ignore_names = set() + if not config: config = {}