diff --git a/.github/workflows/ci-auto-fix.yml b/.github/workflows/ci-auto-fix.yml new file mode 100644 index 0000000..6315ac6 --- /dev/null +++ b/.github/workflows/ci-auto-fix.yml @@ -0,0 +1,83 @@ +name: CI Auto-Fix + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +jobs: + auto_fix: + if: github.event.workflow_run.conclusion == 'failure' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install dev deps + run: python -m pip install -e '.[dev]' + + - name: Run ruff and black auto-fixes + run: | + set -e + ruff format --quiet --fix . || true + black --quiet . || true + + - name: Check for formatting changes + id: git-check + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + if [ -n "$(git status --porcelain)" ]; then echo "changed=true" >> $GITHUB_OUTPUT; else echo "changed=false" >> $GITHUB_OUTPUT; fi + + - name: Create branch and PR for formatting fixes + if: steps.git-check.outputs.changed == 'true' + env: + BRANCH_NAME: auto/ci-fix/format-${{ github.run_id }} + run: | + git checkout -b "$BRANCH_NAME" + git add -A + git commit -m "style: apply ruff/black fixes from CI auto-fix" + git push origin "$BRANCH_NAME" + # Ensure auto-fix label exists then create PR and enable auto-merge so formatting fixes are merged automatically when checks pass + gh label create "auto-fix" --color ffcc00 --description "Automated formatting fixes from CI" || true + gh pr create --title "style: apply ruff/black fixes (auto)" --body "Automated formatting fixes (ruff/black) applied by CI Auto-Fix workflow. This PR will be auto-merged when checks pass." --base main --head "$BRANCH_NAME" --label "auto-fix" --auto || true + + - name: Run tests to check if fixes pass + run: python -m pytest -q || true + + - name: Create PR on other failures (diagnostic) + if: steps.git-check.outputs.changed == 'false' + run: | + echo "Opening PR for CI failure diagnostics" + BRANCH=auto/ci-fix/failure-${{ github.run_id }} + git checkout -b "$BRANCH" + # Add a short diagnostic file for human triage + echo "CI failure triggered by workflow_run id: ${{ github.event.workflow_run.id }}" > CI_FAILURE.md + git add CI_FAILURE.md + git commit -m "ci: record CI failure for diagnostics" + git push origin "$BRANCH" + # Ensure ci-fail label exists and create a non-draft PR for triage + gh label create "ci-fail" --color ee0000 --description "CI failure requires human triage" || true + gh pr create --title "ci: investigate failing CI run #${{ github.event.workflow_run.id }}" --body "PR created automatically to investigate CI failure (workflow_run id: ${{ github.event.workflow_run.id }}). Please see workflow run logs for details." --base main --head "$BRANCH" --label "ci-fail" --web || true + + - name: Comment on original run + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const runId = context.payload.workflow_run.id + const owner = context.repo.owner + const repo = context.repo.repo + await github.rest.actions.createWorkflowRunComment({ + owner, repo, run_id: runId, body: 'CI Auto-Fix ran: formatting fixes applied or diagnostic PR created.' + }) diff --git a/app/config.py b/app/config.py index 69a3b1c..78c666c 100644 --- a/app/config.py +++ b/app/config.py @@ -155,7 +155,7 @@ class ValidationConfig: def __post_init__(self): """Initialize validation sets.""" if self.valid_group_types is None: - self.valid_group_types = {0, 1, 2, 3} + self.valid_group_types = frozenset({0, 1, 2, 3}) @dataclass diff --git a/app/logger.py b/app/logger.py index bd6a234..b8a6706 100644 --- a/app/logger.py +++ b/app/logger.py @@ -31,16 +31,9 @@ file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(formatter) logger.addHandler(file_handler) - except Exception: - # If can't create logs directory, just use console - pass - - -def get_logger(name: str = None): - """Get a logger instance. - - Parameters - ---------- + except OSError as e: + # If can't create logs directory, log a warning and continue using console + logger.warning(f"Could not create log directory '{log_dir}': {e!s}") name : str, optional Logger name (typically __name__). If None, returns root app logger. diff --git a/app/pages/ecopath.py b/app/pages/ecopath.py index bbb568b..0ba3a8c 100644 --- a/app/pages/ecopath.py +++ b/app/pages/ecopath.py @@ -732,7 +732,7 @@ def _handle_model_params_edit(): duration=5 ) - except (ValueError, TypeError) as e: + except (ValueError, TypeError): ui.notification_show( f"Invalid numeric value for {col_name}: '{new_value}'", type="error", @@ -785,7 +785,7 @@ def _handle_diet_matrix_edit(): duration=2 ) - except (ValueError, TypeError) as e: + except (ValueError, TypeError): ui.notification_show( f"Invalid numeric value for diet: '{new_value}'", type="error", @@ -952,9 +952,10 @@ def model_results_table(): # Add special styling for calculated columns (EE, GE, TL) calculated_cols = ['EE', 'GE', 'TL'] + col_positions = {c: i for i, c in enumerate(formatted_df.columns)} for col in calculated_cols: if col in formatted_df.columns: - col_idx = list(formatted_df.columns).index(col) + col_idx = col_positions[col] for row_idx in range(len(formatted_df)): is_no_data = bool(no_data_mask.iloc[row_idx][col]) if col in no_data_mask.columns else False is_stanza = bool(stanza_mask.iloc[row_idx][col]) if (stanza_mask is not None and col in stanza_mask.columns) else False diff --git a/app/pages/ecospace.py b/app/pages/ecospace.py index 0962610..594f29a 100644 --- a/app/pages/ecospace.py +++ b/app/pages/ecospace.py @@ -234,7 +234,7 @@ def create_hexagon(center_x: float, center_y: float, radius: float) -> "Polygon" angles = np.linspace(0, 2 * np.pi, 7) + np.pi / 6 # Rotate by 30 degrees x_coords = center_x + radius * np.cos(angles) y_coords = center_y + radius * np.sin(angles) - return Polygon(zip(x_coords, y_coords)) + return Polygon(zip(x_coords, y_coords, strict=True)) def ecospace_ui(): @@ -578,14 +578,14 @@ def ecospace_ui(): ) -def ecospace_server(input: Inputs, output: Outputs, session: Session, model_data: reactive.Value, sim_results: reactive.Value): +def ecospace_server(input: Inputs, _output: Outputs, _session: Session, _model_data: reactive.Value, _sim_results: reactive.Value): """Server logic for ECOSPACE page.""" # Reactive values for spatial state grid = reactive.Value(None) boundary_polygon = reactive.Value(None) # Store uploaded boundary for visualization - ecospace_params = reactive.Value(None) - spatial_results = reactive.Value(None) + ecospace_params = reactive.Value(None) # TODO: reserved for future use # noqa: F841 + spatial_results = reactive.Value(None) # TODO: reserved for future use # noqa: F841 # Load and display boundary polygon immediately on file upload @reactive.effect @@ -655,9 +655,9 @@ def load_boundary_on_upload(): # Clean up temporary directory shutil.rmtree(temp_dir, ignore_errors=True) - except Exception as e: + except (ValueError, IOError, OSError, zipfile.BadZipFile) as e: ui.notification_show( - f"Error loading boundary: {str(e)}", + f"Error loading boundary: {e!s}", type="warning", duration=5 ) @@ -838,9 +838,9 @@ def create_spatial_grid(): # Clean up temporary directory shutil.rmtree(temp_dir, ignore_errors=True) - except Exception as e: + except (ValueError, IOError, OSError, zipfile.BadZipFile) as e: ui.notification_show( - f"Error processing spatial file: {str(e)}", + f"Error processing spatial file: {e!s}", type="error", duration=6 ) @@ -849,9 +849,9 @@ def create_spatial_grid(): # Enable run button ui.update_action_button("run_spatial_sim", disabled=False) - except Exception as e: + except (ValueError, OSError) as e: ui.notification_show( - f"Error creating grid: {str(e)}", + f"Error creating grid: {e!s}", type="error", duration=5 ) @@ -929,7 +929,7 @@ def grid_plot(): folium.GeoJson( boundary_geojson, name='Boundary', - style_function=lambda x: { + style_function=lambda _x: { 'fillColor': 'red', 'color': 'red', 'weight': 2.5, @@ -971,7 +971,7 @@ def grid_plot(): folium.GeoJson( geojson_data, name='Grid Patches', - style_function=lambda x: { + style_function=lambda _x: { 'fillColor': 'lightblue', 'color': 'steelblue', 'weight': 0.5, # Thinner lines for large grids @@ -1003,7 +1003,7 @@ def grid_plot(): # Add polygon to map folium.GeoJson( geojson_data, - style_function=lambda x: { + style_function=lambda _x: { 'fillColor': 'lightblue', 'color': 'steelblue', 'weight': 1.5, @@ -1284,7 +1284,7 @@ def habitat_plot(): x, y = geom.exterior.xy color = cmap(norm(habitat[idx])) polygon = MplPolygon( - list(zip(x, y)), + list(zip(x, y, strict=True)), facecolor=color, edgecolor='darkgreen', linewidth=1.2, diff --git a/src/pypath/analysis/prebalance.py b/src/pypath/analysis/prebalance.py index 9034bee..63c72ba 100644 --- a/src/pypath/analysis/prebalance.py +++ b/src/pypath/analysis/prebalance.py @@ -46,7 +46,7 @@ def _calculate_trophic_levels(model: RpathParams) -> pd.Series: # Iteratively calculate TL for consumers # TL = 1 + weighted average of prey TLs max_iterations = 50 - for iteration in range(max_iterations): + for _iteration in range(max_iterations): tl_old = tl.copy() for i, group in enumerate(groups): @@ -313,7 +313,7 @@ def plot_biomass_vs_trophic_level( # Add group labels (sample if too many) if len(df) <= 30: - for idx, row in df.iterrows(): + for _idx, row in df.iterrows(): ax.annotate( row['Group'], (row['TL'], row['Biomass']), @@ -379,7 +379,7 @@ def plot_vital_rate_vs_trophic_level( # Add labels for interesting points if len(df) <= 20: - for idx, row in df.iterrows(): + for _idx, row in df.iterrows(): ax.annotate( row['Group'], (row['TL'], row[rate_name]),