From 1f611117e770618da434a564350760375a14c24f Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Wed, 21 Jan 2026 17:23:21 -0500 Subject: [PATCH 1/7] Add static code analysis to CI --- .github/workflows/build.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e40dbb73..dd827a1c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,25 @@ jobs: pip install setuptools==69.5.1 wheel pip install -r requirements.txt + # Static analysis tools + - name: Install static analysis tools + if: runner.os == 'Linux' + run: | + pip install mypy==1.8.0 + pip install flake8==7.0.0 + pip install black==24.1.1 + + - name: Run static code analysis + if: runner.os == 'Linux' + run: | + # Critical checks + mypy lean/ --ignore-missing-imports --check-untyped-defs + flake8 lean/ --select=F821 --ignore=ALL + + # Warning checks (don't fail the build) + flake8 lean/ --select=F401 --ignore=ALL --exit-zero + black --check lean/ --quiet || true + - name: Run tests run: python -m pytest -s -rs From bfde746ad5e5045e440ca71163e5a31305961a17 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Wed, 21 Jan 2026 17:37:40 -0500 Subject: [PATCH 2/7] Limit mypy CI check --- .github/workflows/build.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd827a1c..857f2ed8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,23 @@ jobs: if: runner.os == 'Linux' run: | # Critical checks - mypy lean/ --ignore-missing-imports --check-untyped-defs + mypy_output=$(mypy lean/ \ + --ignore-missing-imports \ + --check-untyped-defs \ + --show-error-codes \ + --no-error-summary 2>&1) + + missing_args=$(echo "$mypy_output" | grep -E "Missing positional argument|\[call-arg\]") + + if [ -n "$missing_args" ]; then + echo "❌ ERROR: Missing function arguments detected:" + echo "" + echo "$missing_args" + echo "" + echo "When adding/removing parameters from methods, ensure all call sites are updated." + exit 1 + fi + flake8 lean/ --select=F821 --ignore=ALL # Warning checks (don't fail the build) From 4e1b0db0945aa793d1ea985aad58273aea5829c2 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Wed, 21 Jan 2026 17:45:40 -0500 Subject: [PATCH 3/7] Fix issues with mypy --- .github/workflows/build.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 857f2ed8..37cf6859 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,14 +41,12 @@ jobs: --ignore-missing-imports \ --check-untyped-defs \ --show-error-codes \ - --no-error-summary 2>&1) + --no-error-summary 2>&1 || true) - missing_args=$(echo "$mypy_output" | grep -E "Missing positional argument|\[call-arg\]") - - if [ -n "$missing_args" ]; then - echo "❌ ERROR: Missing function arguments detected:" + if echo "$mypy_output" | grep -q -E "Missing positional argument|\[call-arg\]"; then + echo "ERROR: Missing function arguments detected:" echo "" - echo "$missing_args" + echo "$mypy_output" | grep -E "Missing positional argument|\[call-arg\]" echo "" echo "When adding/removing parameters from methods, ensure all call sites are updated." exit 1 From 31447f979ddb512268530d126fb7089508172bde Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 23 Jan 2026 00:56:45 -0500 Subject: [PATCH 4/7] Use script to improve error reporting --- .github/workflows/build.yml | 32 +------- static_analysis.py | 152 ++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 29 deletions(-) create mode 100644 static_analysis.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 37cf6859..73bbee23 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,37 +26,11 @@ jobs: pip install -r requirements.txt # Static analysis tools - - name: Install static analysis tools + - name: Static Code Analysis if: runner.os == 'Linux' run: | - pip install mypy==1.8.0 - pip install flake8==7.0.0 - pip install black==24.1.1 - - - name: Run static code analysis - if: runner.os == 'Linux' - run: | - # Critical checks - mypy_output=$(mypy lean/ \ - --ignore-missing-imports \ - --check-untyped-defs \ - --show-error-codes \ - --no-error-summary 2>&1 || true) - - if echo "$mypy_output" | grep -q -E "Missing positional argument|\[call-arg\]"; then - echo "ERROR: Missing function arguments detected:" - echo "" - echo "$mypy_output" | grep -E "Missing positional argument|\[call-arg\]" - echo "" - echo "When adding/removing parameters from methods, ensure all call sites are updated." - exit 1 - fi - - flake8 lean/ --select=F821 --ignore=ALL - - # Warning checks (don't fail the build) - flake8 lean/ --select=F401 --ignore=ALL --exit-zero - black --check lean/ --quiet || true + pip install mypy==1.15.0 flake8==7.0.0 + python static_analysis.py - name: Run tests run: python -m pytest -s -rs diff --git a/static_analysis.py b/static_analysis.py new file mode 100644 index 00000000..89f5eaf4 --- /dev/null +++ b/static_analysis.py @@ -0,0 +1,152 @@ +import subprocess +import sys + +def display_warning_summary(warnings): + print("\nWarnings:") + unused_count = sum(1 for e in warnings if e.startswith('F401:')) + if unused_count > 0: + print(f" - Unused imports: {unused_count}") + + print(" Consider addressing warnings in future updates.") + +def run_analysis(): + print("Running static analysis...") + print("=" * 60) + + all_critical_errors = [] + all_warnings = [] + + # Check for missing arguments with mypy - CRITICAL + print("\n1. Checking for missing function arguments...") + print("-" * 40) + + result = subprocess.run( + ["python", "-m", "mypy", "lean/", + "--show-error-codes", + "--no-error-summary", + "--ignore-missing-imports", + "--check-untyped-defs"], + capture_output=True, + text=True + ) + + # Filter for critical call argument mismatches + call_arg_errors = [] + + for line in (result.stdout + result.stderr).split('\n'): + if not line.strip(): + continue + + # Look for call-arg errors (this covers both "too many" and "missing" arguments) + if '[call-arg]' in line: + # Skip false positives + if any(pattern in line for pattern in + ['click.', 'subprocess.', 'Module "', 'has incompatible type "Optional', + 'validator', 'pydantic', '__call__', 'OnlyValueValidator', 'V1Validator', + 'QCParameter', 'QCBacktest']): + continue + call_arg_errors.append(line.strip()) + + # Display call argument mismatches + if call_arg_errors: + print("CRITICAL: Missing function arguments found:") + for error in call_arg_errors: + # Clean path for better display + clean_error = error.replace('/home/runner/work/lean-cli/lean-cli/', '') + print(f" {clean_error}") + + all_critical_errors.extend(call_arg_errors) + else: + print("No argument mismatch errors found") + + # Check for undefined variables with flake8 - CRITICAL + print("\n2. Checking for undefined variables...") + print("-" * 40) + + result = subprocess.run( + ["python", "-m", "flake8", "lean/", + "--select=F821", + "--ignore=ALL", + "--count"], + capture_output=True, + text=True + ) + + if result.stdout.strip() and result.stdout.strip() != "0": + detail = subprocess.run( + ["python", "-m", "flake8", "lean/", "--select=F821", "--ignore=ALL"], + capture_output=True, + text=True + ) + + undefined_errors = [e.strip() for e in detail.stdout.split('\n') if e.strip()] + print(f"CRITICAL: {len(undefined_errors)} undefined variable(s) found:") + + for error in undefined_errors: + print(f" {error}") + + all_critical_errors.extend([f"F821: {e}" for e in undefined_errors]) + else: + print("No undefined variables found") + + # Check for unused imports with flake8 - WARNING + print("\n3. Checking for unused imports...") + print("-" * 40) + + result = subprocess.run( + ["python", "-m", "flake8", "lean/", + "--select=F401", + "--ignore=ALL", + "--count", + "--exit-zero"], + capture_output=True, + text=True + ) + + if result.stdout.strip() and result.stdout.strip() != "0": + detail = subprocess.run( + ["python", "-m", "flake8", "lean/", "--select=F401", "--ignore=ALL", "--exit-zero"], + capture_output=True, + text=True + ) + + unused_imports = [e.strip() for e in detail.stdout.split('\n') if e.strip()] + if unused_imports: + print(f"WARNING: {len(unused_imports)} unused import(s) found:") + + for error in unused_imports: + print(f" {error}") + + all_warnings.extend([f"F401: {e}" for e in unused_imports]) + else: + print("No unused imports found") + else: + print("No unused imports found") + + print("\n" + "=" * 60) + + # Summary + if all_critical_errors: + total_errors = len(all_critical_errors) + print(f"BUILD FAILED: Found {total_errors} critical error(s)") + + print("\nSummary of critical errors:") + print(f" - Function call argument mismatches: {len(call_arg_errors)}") + undefined_count = sum(1 for e in all_critical_errors if e.startswith('F821:')) + print(f" - Undefined variables: {undefined_count}") + + if all_warnings: + display_warning_summary(all_warnings) + + return 1 + + if all_warnings: + print(f"BUILD PASSED with {len(all_warnings)} warning(s)") + display_warning_summary(all_warnings) + return 0 + + print("SUCCESS: All checks passed with no warnings") + return 0 + +if __name__ == "__main__": + sys.exit(run_analysis()) From 45cb3dc398b4b2fcfa3b5668142264ad92b667fb Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 23 Jan 2026 00:57:39 -0500 Subject: [PATCH 5/7] Clean up code --- lean/commands/lean.py | 1 - lean/commands/live/deploy.py | 2 +- lean/components/api/live_client.py | 3 +-- lean/components/util/compiler.py | 8 ++++---- lean/components/util/project_manager.py | 1 - lean/main.py | 2 +- lean/models/pydantic.py | 4 ++-- 7 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lean/commands/lean.py b/lean/commands/lean.py index 0de5c04d..749000bb 100644 --- a/lean/commands/lean.py +++ b/lean/commands/lean.py @@ -10,7 +10,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional from click import group, option, Context, pass_context, echo diff --git a/lean/commands/live/deploy.py b/lean/commands/live/deploy.py index de7576ed..669666bf 100644 --- a/lean/commands/live/deploy.py +++ b/lean/commands/live/deploy.py @@ -12,7 +12,7 @@ # limitations under the License. from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import List, Optional, Tuple from click import option, argument, Choice from lean.click import LeanCommand, PathParameter from lean.components.util.name_rename import rename_internal_config_to_user_friendly_format diff --git a/lean/components/api/live_client.py b/lean/components/api/live_client.py index 8dbc862e..729e2d8a 100644 --- a/lean/components/api/live_client.py +++ b/lean/components/api/live_client.py @@ -11,11 +11,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from datetime import datetime from typing import List, Optional from lean.components.api.api_client import * -from lean.models.api import QCFullLiveAlgorithm, QCLiveAlgorithmStatus, QCMinimalLiveAlgorithm, QCNotificationMethod, QCRestResponse +from lean.models.api import QCFullLiveAlgorithm, QCMinimalLiveAlgorithm, QCNotificationMethod, QCRestResponse class LiveClient: diff --git a/lean/components/util/compiler.py b/lean/components/util/compiler.py index 585c1a4d..4ec3fa62 100644 --- a/lean/components/util/compiler.py +++ b/lean/components/util/compiler.py @@ -111,13 +111,14 @@ def _compile() -> Dict[str, Any]: "mounts": [], "volumes": {} } - lean_runner.mount_project_and_library_directories(project_dir, run_options) - lean_runner.setup_language_specific_run_options(run_options, project_dir, algorithm_file, False, False) project_config = project_config_manager.get_project_config(project_dir) engine_image = cli_config_manager.get_engine_image( project_config.get("engine-image", None)) + lean_runner.mount_project_and_library_directories(project_dir, run_options) + lean_runner.setup_language_specific_run_options(run_options, project_dir, algorithm_file, False, False, engine_image) + message["result"] = docker_manager.run_image(engine_image, **run_options) temp_manager.delete_temporary_directories_when_done = False return message @@ -153,8 +154,7 @@ def _parse_python_errors(python_output: str, color_coding_required: bool) -> lis errors.append(f"{bcolors.FAIL}Build Error File: {match[0]} Line {match[1]} Column {match[2]} - {match[3]}{bcolors.ENDC}\n") else: errors.append(f"Build Error File: {match[0]} Line {match[1]} Column {match[2]} - {match[3]}\n") - - for match in re.findall(r"\*\*\* Sorry: ([^(]+) \(([^,]+), line (\d+)\)", python_output): + for match in findall(r"\*\*\* Sorry: ([^(]+) \(([^,]+), line (\d+)\)", python_output): if color_coding_required: errors.append(f"{bcolors.FAIL}Build Error File: {match[1]} Line {match[2]} Column 0 - {match[0]}{bcolors.ENDC}\n") else: diff --git a/lean/components/util/project_manager.py b/lean/components/util/project_manager.py index 60dffc12..25533217 100644 --- a/lean/components/util/project_manager.py +++ b/lean/components/util/project_manager.py @@ -366,7 +366,6 @@ def restore_csharp_project(self, csproj_file: Path, no_local: bool) -> None: """ from shutil import which from subprocess import run, STDOUT, PIPE - from lean.models.errors import MoreInfoError if no_local: return diff --git a/lean/main.py b/lean/main.py index 851cd1f2..061c721b 100644 --- a/lean/main.py +++ b/lean/main.py @@ -97,7 +97,7 @@ def main() -> None: if temp_manager.delete_temporary_directories_when_done: temp_manager.delete_temporary_directories() except Exception as exception: - from traceback import format_exc, print_exc + from traceback import format_exc from click import UsageError, Abort from requests import exceptions from io import StringIO diff --git a/lean/models/pydantic.py b/lean/models/pydantic.py index e8a8a6b8..abe5fed6 100644 --- a/lean/models/pydantic.py +++ b/lean/models/pydantic.py @@ -16,9 +16,9 @@ # We keep all this imports here, even if not used like validator, so other files can import them through this file # to avoid having to check the pydantic version in every file. # All imports should be done through this file to avoid pydantic version related errors. - from pydantic import BaseModel, ValidationError, Field, validator + from pydantic import BaseModel, ValidationError else: - from pydantic.v1 import BaseModel, ValidationError, Field, validator + from pydantic.v1 import BaseModel, ValidationError class WrappedBaseModel(BaseModel): """A version of Pydantic's BaseModel which makes the input data accessible in case of a validation error.""" From 13d8ecc6a0838adfad2f63b2d9858dd93bad1153 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 23 Jan 2026 01:00:16 -0500 Subject: [PATCH 6/7] Fix issue with mypy --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 73bbee23..a66df28e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: - name: Static Code Analysis if: runner.os == 'Linux' run: | - pip install mypy==1.15.0 flake8==7.0.0 + pip install mypy==1.14.1 flake8==7.0.0 python static_analysis.py - name: Run tests From 3ff9f954585737c87ee678771ebd2473aaa96d03 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 23 Jan 2026 01:11:49 -0500 Subject: [PATCH 7/7] Fix broken imports in pydantic --- lean/models/pydantic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lean/models/pydantic.py b/lean/models/pydantic.py index abe5fed6..e8a8a6b8 100644 --- a/lean/models/pydantic.py +++ b/lean/models/pydantic.py @@ -16,9 +16,9 @@ # We keep all this imports here, even if not used like validator, so other files can import them through this file # to avoid having to check the pydantic version in every file. # All imports should be done through this file to avoid pydantic version related errors. - from pydantic import BaseModel, ValidationError + from pydantic import BaseModel, ValidationError, Field, validator else: - from pydantic.v1 import BaseModel, ValidationError + from pydantic.v1 import BaseModel, ValidationError, Field, validator class WrappedBaseModel(BaseModel): """A version of Pydantic's BaseModel which makes the input data accessible in case of a validation error."""