diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2e1cedc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + +jobs: + lint: + name: ruff (lint) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - run: pip install ruff==0.15.21 + - run: ruff check src tests + + typecheck: + name: mypy (advisory) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - run: pip install mypy + # Advisory: the codebase is partially typed. Print findings but never + # fail the build (green check) until type coverage improves. Tighten by + # dropping the `|| true` once the baseline is cleaned up. + - run: mypy src/gasbench --ignore-missing-imports || true diff --git a/pyproject.toml b/pyproject.toml index 7b877c4..e9c7bde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,13 @@ dependencies = [ "open-clip-torch==3.3.0", ] +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "ruff==0.15.21", + "mypy>=1.10", +] + [project.scripts] gasbench = "gasbench.cli:main" @@ -57,6 +64,23 @@ where = ["src"] line-length = 88 target-version = ['py310'] +[tool.ruff] +line-length = 88 +target-version = "py310" +src = ["src", "tests"] + +[tool.ruff.lint] +# High-signal baseline: pyflakes (real bugs) + import/statement errors. +# Style rules (line length, naming) are intentionally left off for now to keep +# the diff focused; expand this list over time. +select = ["E4", "E7", "E9", "F"] + +[tool.mypy] +python_version = "3.10" +ignore_missing_imports = true +follow_imports = "silent" +# Advisory only for now — the codebase is partially typed. Not enforced in CI. + [tool.pytest.ini_options] testpaths = ["tests"] markers = ["slow: marks tests as slow (network I/O)"] diff --git a/src/gasbench/benchmark.py b/src/gasbench/benchmark.py index 60c41f3..26ab657 100644 --- a/src/gasbench/benchmark.py +++ b/src/gasbench/benchmark.py @@ -3,7 +3,6 @@ import json import time import uuid -import asyncio from datetime import datetime from pathlib import Path from typing import Dict, Optional @@ -181,7 +180,7 @@ async def load_model_for_benchmark( benchmark_results["validation"]["input_type"] = str(input_specs[0].type) benchmark_results["validation"]["output_shape"] = str(output_specs[0].shape) - logger.info(f"Model loaded successfully") + logger.info("Model loaded successfully") # Get execution providers providers = session.get_providers() @@ -237,7 +236,7 @@ async def execute_benchmark( if dataset_filters: logger.info(f"Dataset filters: {dataset_filters}") if modality == "image": - df = await run_image_benchmark( + await run_image_benchmark( session, input_specs, benchmark_results, @@ -264,7 +263,7 @@ async def execute_benchmark( ) benchmark_score = benchmark_results.get("image_results", {}).get("benchmark_score", 0.0) elif modality == "video": - df = await run_video_benchmark( + await run_video_benchmark( session, input_specs, benchmark_results, @@ -291,7 +290,7 @@ async def execute_benchmark( ) benchmark_score = benchmark_results.get("video_results", {}).get("benchmark_score", 0.0) elif modality == "audio": - df = await run_audio_benchmark( + await run_audio_benchmark( session, input_specs, benchmark_results, @@ -375,7 +374,7 @@ def format_benchmark_summary(benchmark_results: Dict) -> str: per_dataset = results.get("per_dataset_results", {}) if per_dataset: - lines.append(f"\nšŸ“‹ PER-DATASET RESULTS:") + lines.append("\nšŸ“‹ PER-DATASET RESULTS:") for dataset_name, dataset_results in per_dataset.items(): accuracy = dataset_results.get("accuracy", 0.0) total = dataset_results.get("total", 0) @@ -394,7 +393,7 @@ def format_benchmark_summary(benchmark_results: Dict) -> str: entry["samples"] += int(ds_stats.get("total", 0)) entry["correct"] += int(ds_stats.get("correct", 0)) if by_type: - lines.append(f"\nšŸŽ­ ACCURACY BY MEDIA TYPE:") + lines.append("\nšŸŽ­ ACCURACY BY MEDIA TYPE:") for mtype, v in by_type.items(): samples = v["samples"] correct = v["correct"] @@ -403,7 +402,7 @@ def format_benchmark_summary(benchmark_results: Dict) -> str: validation = benchmark_results.get("validation", {}) if validation: - lines.append(f"\nšŸ” MODEL VALIDATION:") + lines.append("\nšŸ” MODEL VALIDATION:") lines.append(f" Input Shape: {validation.get('input_shape', 'N/A')}") lines.append(f" Input Type: {validation.get('input_type', 'N/A')}") lines.append(f" Output Shape: {validation.get('output_shape', 'N/A')}") diff --git a/src/gasbench/benchmarks/utils/metrics.py b/src/gasbench/benchmarks/utils/metrics.py index 245d801..b9d8e55 100644 --- a/src/gasbench/benchmarks/utils/metrics.py +++ b/src/gasbench/benchmarks/utils/metrics.py @@ -117,7 +117,6 @@ def compute_sn34_score( Returns: float in [0,1]. """ - import math # Normalize MCC: [-1, 1] -> [0, 1] bin_mcc = float(self.calculate_binary_mcc()) diff --git a/src/gasbench/benchmarks/utils/pytorch_session.py b/src/gasbench/benchmarks/utils/pytorch_session.py index 3159b8b..55a0d5f 100644 --- a/src/gasbench/benchmarks/utils/pytorch_session.py +++ b/src/gasbench/benchmarks/utils/pytorch_session.py @@ -4,9 +4,9 @@ import torch import numpy as np from pathlib import Path -from typing import List, Any, Optional +from typing import List, Any -from .custom_model_loader import load_custom_model, load_model_config +from .custom_model_loader import load_custom_model from ...logger import get_logger logger = get_logger(__name__) diff --git a/src/gasbench/benchmarks/video_bench.py b/src/gasbench/benchmarks/video_bench.py index 9a8f101..f32bd5b 100644 --- a/src/gasbench/benchmarks/video_bench.py +++ b/src/gasbench/benchmarks/video_bench.py @@ -1,5 +1,4 @@ import os -import asyncio import numpy as np import traceback from typing import Dict, Optional @@ -352,8 +351,6 @@ async def run_video_benchmark( f"{dataset_config.name} ({dataset_cap} samples)" ) - dataset_skipped = 0 - try: is_gasstation = "gasstation" in dataset_config.name.lower() if skip_missing: diff --git a/src/gasbench/cli.py b/src/gasbench/cli.py index 79f24dd..7ac6184 100644 --- a/src/gasbench/cli.py +++ b/src/gasbench/cli.py @@ -101,15 +101,15 @@ def command_run(args): print(f"šŸ“¦ Using custom PyTorch model from: {model_path}") elif model_path.suffix.lower() == ".onnx": # ONNX format no longer supported for competition - print(f"Error: ONNX format is no longer supported for competition.") - print(f"Please use a safetensors model directory containing:") - print(f" - model_config.yaml") - print(f" - model.py") - print(f" - *.safetensors weights") - print(f"See docs/Safetensors.md for requirements.") + print("Error: ONNX format is no longer supported for competition.") + print("Please use a safetensors model directory containing:") + print(" - model_config.yaml") + print(" - model.py") + print(" - *.safetensors weights") + print("See docs/Safetensors.md for requirements.") return 1 else: - print(f"Error: Model must be a directory with model_config.yaml, model.py, and *.safetensors") + print("Error: Model must be a directory with model_config.yaml, model.py, and *.safetensors") print(f"Got: {model_path}") return 1 @@ -182,9 +182,9 @@ def command_run(args): f.write(format_benchmark_summary(results)) print(f"\nšŸ“Š Results saved to: {results_path}") - print(f" - JSON: results.json") - print(f" - Parquet: records.parquet") - print(f" - Summary: summary.txt") + print(" - JSON: results.json") + print(" - Parquet: records.parquet") + print(" - Summary: summary.txt") if results.get("benchmark_completed"): print("\nāœ… Benchmark completed successfully") @@ -207,7 +207,6 @@ def command_verify_cache(args): from .dataset.cache import ( scan_cache_directory, compute_cache_statistics, - verify_cache_against_configs, format_size_bytes, ) @@ -230,19 +229,19 @@ def command_verify_cache(args): by_modality, by_media_type = compute_cache_statistics(datasets) - print(f"\nšŸ“Š CACHE SUMMARY") + print("\nšŸ“Š CACHE SUMMARY") print(f"Total Datasets: {len(datasets)}") print(f"Total Samples: {total_samples:,}") print(f"Total Size: {format_size_bytes(total_size_bytes)}") - print(f"\nšŸ“ˆ BY MODALITY") + print("\nšŸ“ˆ BY MODALITY") for modality in sorted(by_modality.keys()): stats = by_modality[modality] print( f" {modality.upper():12} {stats['count']:3} datasets, {stats['samples']:7,} samples, {format_size_bytes(stats['size']):>10}" ) - print(f"\nšŸŽ­ BY MEDIA TYPE") + print("\nšŸŽ­ BY MEDIA TYPE") for mtype in sorted(by_media_type.keys()): stats = by_media_type[mtype] print( @@ -250,7 +249,7 @@ def command_verify_cache(args): ) if args.verbose: - print(f"\nšŸ“‹ DETAILED LISTING") + print("\nšŸ“‹ DETAILED LISTING") datasets_by_mod = defaultdict(list) for ds in datasets: @@ -304,7 +303,7 @@ def _verify_against_config(cached_names, cached_datasets, args, cache_dir): if missing: has_issues = True - print(f"\nāŒ MISSING DATASETS (in config but not in cache):") + print("\nāŒ MISSING DATASETS (in config but not in cache):") by_modality = defaultdict(list) for ds in expected_datasets: if ds.name in missing: @@ -318,7 +317,7 @@ def _verify_against_config(cached_names, cached_datasets, args, cache_dir): if extra: has_issues = True - print(f"\nāš ļø EXTRA DATASETS (in cache but not in config):") + print("\nāš ļø EXTRA DATASETS (in cache but not in config):") by_modality = defaultdict(list) for ds in cached_datasets: if ds["name"] in extra: diff --git a/src/gasbench/dataset/cache.py b/src/gasbench/dataset/cache.py index 0a7d370..d7bbc13 100644 --- a/src/gasbench/dataset/cache.py +++ b/src/gasbench/dataset/cache.py @@ -32,7 +32,7 @@ def check_dataset_cache( ): try: with open(dataset_info_file, "r") as f: - dataset_info = json.load(f) + json.load(f) # validate JSON is readable sample_count = len( [ diff --git a/src/gasbench/dataset/config.py b/src/gasbench/dataset/config.py index 650e837..d53197a 100644 --- a/src/gasbench/dataset/config.py +++ b/src/gasbench/dataset/config.py @@ -2,10 +2,8 @@ from typing import Dict, List, Optional, Tuple from pathlib import Path import os -import json import yaml from importlib.resources import files -from collections import defaultdict import hashlib from ..logger import get_logger @@ -404,7 +402,7 @@ def load_datasets_from_yaml(yaml_path: str) -> Dict[str, List[BenchmarkDatasetCo raise yaml.YAMLError(f"Failed to parse YAML file {yaml_path}: {e}") if not isinstance(data, dict): - raise ValueError(f"YAML file must contain a dictionary at root level") + raise ValueError("YAML file must contain a dictionary at root level") all_errors = [] result = {"image": [], "video": [], "audio": []} diff --git a/src/gasbench/dataset/download.py b/src/gasbench/dataset/download.py index 6d13897..2d21c14 100644 --- a/src/gasbench/dataset/download.py +++ b/src/gasbench/dataset/download.py @@ -21,6 +21,7 @@ import soundfile as sf import pandas as pd +import pyarrow import pyarrow.parquet as pq import requests import huggingface_hub as hf_hub @@ -223,7 +224,6 @@ def download_and_extract( f"No files found for {dataset.path} with format {dataset.source_format}" ) - src_fmt = str(getattr(dataset, "source_format", "")).lower().lstrip(".") fallback_formats = [".parquet", ".zip", ".tar", ".tar.gz"] for fallback_format in fallback_formats: if fallback_format != dataset.source_format: @@ -619,7 +619,6 @@ def _process_gasstation( # Track parquet files we've already processed if downloaded_archives is not None: - parquet_basenames = {os.path.basename(url) for url in parquet_urls} parquets_to_download = [ url for url in parquet_urls @@ -971,14 +970,18 @@ def _process_zip_or_tar( if is_zip: list_entries = archive.namelist() - get_name = lambda e: e + + def get_name(e): + return e def open_entry(e): return archive.open(e) else: list_entries = [m for m in archive.getmembers() if m.isreg()] - get_name = lambda m: m.name + + def get_name(m): + return m.name def open_entry(m): return archive.extractfile(m) @@ -1507,7 +1510,7 @@ def list_modelscope_files(repo_id, extension=None): f"Failed to list files of type {extension} in ModelScope repo {repo_id}: {e}" ) logger.error( - f"Make sure 'modelscope' package is installed: pip install modelscope" + "Make sure 'modelscope' package is installed: pip install modelscope" ) return files diff --git a/src/gasbench/dataset/iterator.py b/src/gasbench/dataset/iterator.py index c69280a..bd68643 100644 --- a/src/gasbench/dataset/iterator.py +++ b/src/gasbench/dataset/iterator.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import Optional, Dict -from PIL import Image from ..logger import get_logger from .config import BenchmarkDatasetConfig diff --git a/src/gasbench/dataset/utils/gasstation_utils.py b/src/gasbench/dataset/utils/gasstation_utils.py index 6d89ce2..50a3a9b 100644 --- a/src/gasbench/dataset/utils/gasstation_utils.py +++ b/src/gasbench/dataset/utils/gasstation_utils.py @@ -9,7 +9,7 @@ import os import json -from typing import List, Set, Optional, Dict, Tuple +from typing import List, Set, Optional, Tuple from pathlib import Path from datetime import datetime, timedelta diff --git a/src/gasbench/download_manager.py b/src/gasbench/download_manager.py index f0ef63c..24b9891 100644 --- a/src/gasbench/download_manager.py +++ b/src/gasbench/download_manager.py @@ -149,7 +149,7 @@ async def _download_dataset(self, task: DatasetTask, progress: Progress): await asyncio.sleep(0.3) progress.remove_task(task_id) - except Exception as e: + except Exception: progress.update(task_id, description=f"[red]āŒ {task.dataset.name[:45]}") await asyncio.sleep(0.3) progress.remove_task(task_id) diff --git a/src/gasbench/processing/archive.py b/src/gasbench/processing/archive.py index fc32465..198b1ad 100644 --- a/src/gasbench/processing/archive.py +++ b/src/gasbench/processing/archive.py @@ -65,7 +65,7 @@ def get_video_path(self, sample: Dict) -> Optional[str]: contents = os.listdir(cache_dir) logger.debug(f"šŸ“ Completed archive contents: {contents}") self._logged_structure_for.add(cache_dir) - except: + except Exception: pass return None @@ -157,7 +157,7 @@ def _download_and_extract_archive( with open(completion_marker, "w") as f: f.write(f"Extracted at {time.time()}") - logger.info(f"āœ… Archive extracted successfully") + logger.info("āœ… Archive extracted successfully") except Exception as extract_error: logger.error(f"āŒ Failed to extract archive: {extract_error}") @@ -205,12 +205,12 @@ def list_directory_tree(directory, prefix=""): ) else: items.append(f"{prefix}{item}") - except: + except Exception: pass return items tree = list_directory_tree(cache_dir) - logger.debug(f"šŸ“ Archive directory structure:") + logger.debug("šŸ“ Archive directory structure:") for item in tree[:20]: # Limit to first 20 items logger.debug(f" {item}") if len(tree) > 20: diff --git a/src/gasbench/processing/media.py b/src/gasbench/processing/media.py index e0d2e89..9bb8009 100644 --- a/src/gasbench/processing/media.py +++ b/src/gasbench/processing/media.py @@ -208,11 +208,11 @@ def process_video_bytes_sample( finally: try: os.unlink(temp_video_path) - except: + except Exception: pass try: vr.close() - except: + except Exception: pass except Exception as e: @@ -262,7 +262,7 @@ def process_video_frames_sample( nparr = np.frombuffer(frame_data, np.uint8) frame_array = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if frame_array is None: - logger.warning(f"Failed to decode frame bytes") + logger.warning("Failed to decode frame bytes") continue frame_array = cv2.cvtColor(frame_array, cv2.COLOR_BGR2RGB) else: diff --git a/src/gasbench/processing/preprocess_audio.py b/src/gasbench/processing/preprocess_audio.py index 9345289..5456cba 100644 --- a/src/gasbench/processing/preprocess_audio.py +++ b/src/gasbench/processing/preprocess_audio.py @@ -24,7 +24,6 @@ import os import argparse import torch -from pathlib import Path from tqdm import tqdm from ..logger import get_logger @@ -66,7 +65,7 @@ def preprocess_dataset( samples_dir = os.path.join(dataset_dir, "samples") if not os.path.exists(samples_dir): - logger.error(f"Dataset not cached. Please run 'gasbench download --modality audio' first") + logger.error("Dataset not cached. Please run 'gasbench download --modality audio' first") return False # Check if already preprocessed @@ -145,7 +144,7 @@ def preprocess_dataset( logger.warning(f"Failed to preprocess sample {sample_idx}: {e}") error_count += 1 - logger.info(f"Preprocessing complete!") + logger.info("Preprocessing complete!") logger.info(f" Processed: {processed_count}") logger.info(f" Errors: {error_count}") logger.info(f" Location: {samples_dir}") diff --git a/src/gasbench/processing/transforms.py b/src/gasbench/processing/transforms.py index 863e72d..53e2016 100644 --- a/src/gasbench/processing/transforms.py +++ b/src/gasbench/processing/transforms.py @@ -1,13 +1,9 @@ import math import random from scipy import ndimage -import torchvision.transforms as transforms -import torchvision.transforms.functional as F import numpy as np import cv2 import os -from pathlib import Path -import time from io import BytesIO from PIL import Image import torch @@ -175,7 +171,6 @@ def _h264_roundtrip_ffmpeg(video_array, crf, fps): import shutil import subprocess import tempfile - import os if shutil.which("ffmpeg") is None: return None @@ -214,7 +209,6 @@ def _h264_roundtrip_cv2(video_array, crf, fps): directly, so it is approximated through VIDEOWRITER_PROP_QUALITY (a perceptual 0-100 knob that some builds ignore). Returns (T, H, W, 3) uint8 or None.""" import tempfile - import os T, H, W, C = video_array.shape # Map CRF [18, 51] → cv2 quality [100, 0] linearly (approximate only) diff --git a/tests/unit/test_column_detection.py b/tests/unit/test_column_detection.py index 6bed619..2eaddaa 100644 --- a/tests/unit/test_column_detection.py +++ b/tests/unit/test_column_detection.py @@ -5,8 +5,6 @@ """ import pytest -from unittest.mock import Mock, patch -import pandas as pd from src.gasbench.dataset.config import BenchmarkDatasetConfig diff --git a/tests/unit/test_config_loading.py b/tests/unit/test_config_loading.py index 6662d7f..18da93a 100644 --- a/tests/unit/test_config_loading.py +++ b/tests/unit/test_config_loading.py @@ -7,7 +7,6 @@ import pytest from src.gasbench.dataset.config import ( load_benchmark_datasets_from_yaml, - BenchmarkDatasetConfig, )