Skip to content

Add Ruff linting and format the codebase#83

Open
rgerum wants to merge 2 commits into
masterfrom
t3code/bcc6f755
Open

Add Ruff linting and format the codebase#83
rgerum wants to merge 2 commits into
masterfrom
t3code/bcc6f755

Conversation

@rgerum

@rgerum rgerum commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Added Ruff configuration to pyproject.toml with project-specific line length, target version, and exclude/ignore rules.
  • Added a new GitHub Actions lint job that runs ruff check and ruff format --check.
  • Reformatted a large portion of the codebase to satisfy the new linting and formatting rules.
  • Expanded and adjusted tests around example loading, stack loading, mesh behavior, and GUI usage.

Testing

  • Not run locally.
  • CI lint job now checks uvx ruff check ..
  • CI lint job now checks uvx ruff format --check ..
  • Existing test workflow remains in place for the project test suite.

Summary by CodeRabbit

  • Chores

    • Added a CI lint job and Ruff configuration to enforce consistent code style.
  • Style / Refactor

    • Large-scale code formatting and cleanup across the project for readability and maintainability.
  • Bug Fixes / Stability

    • Minor behavioral adjustments and consistency fixes that improve UI text, help content, and runtime robustness.

- Add a CI lint job for `ruff check` and `ruff format --check`
- Configure Ruff in `pyproject.toml`
- Reformat Python sources to match the new style
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Ready to act? Review this PR in Change Stack to turn feedback into patch suggestions you can inspect and refine.

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7da4582e-2652-4339-b78b-a7295571178f

📥 Commits

Reviewing files that changed from the base of the PR and between 67b43aa and c16e9be.

📒 Files selected for processing (6)
  • .github/workflows/installer.yml
  • .github/workflows/test.yml
  • pyproject.toml
  • saenopy/gui/code/syntax.py
  • test/test_download_example.py
  • test/test_use_gui.py
✅ Files skipped from review due to trivial changes (2)
  • saenopy/gui/code/syntax.py
  • test/test_download_example.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml

📝 Walkthrough

Walkthrough

Adds Ruff lint job and Ruff config; applies repository-wide formatting and refactors across GUI, exporter, core, examples, pyTFM, solver, and tests. Small functional edits appear in examples (params), multigrid tessellation mode, and exporter colorbar/scalebar behavior.

Changes

Repository-wide linting, formatting, and exporter/UI refactors

Layer / File(s) Summary
CI lint and Ruff config
.github/workflows/test.yml, pyproject.toml
Adds a lint GitHub Actions job that runs ruff and adds Ruff configuration in pyproject.toml.
Examples and small config tweaks
saenopy/examples.py
Reformatted example definitions; changed BrightfieldTFM.material_parameters key from d_sds and updated solve_parameters.alpha from 1e10 to 1e11.
Multigrid tessellation change
saenopy/multigrid_helper.py
Refactor/tidying plus removal of previously appended tetrahedra in the tesselation_mode == "6" branch (mode no longer appends those T entries).
Exporter rendering and colorbar/scalebar refactors
saenopy/gui/solver/modules/exporter/*
Major reformat and restructuring of 2D/3D rendering paths, arrow generation, colorbar/scalebar placement and tick labeling (now uses explicit normalization and SI-prefix labeling).
Fiber viewer caching & intensity fix
saenopy/gui/solver/modules/exporter/FiberViewer.py
Adds module-level caches registry and fixes scale_intensity() to compute rescale range correctly and return single rescaled image path.
Numerous GUI formatting and API-preserving changes
saenopy/gui/**, saenopy/gui/tfm2d/**
Widespread reformatting of widget construction, signal wiring, and multi-line argument formatting; no API signature changes except normalized defaults/typing in a few PipelineModule/ParameterMapping/PipelineModule variants.
Core solver and save/load refactors
saenopy/solver.py, saenopy/result_file.py, saenopy/saveable.py
Large formatting and typing cleanup with some signature reformatting; migrations in Result.from_dict are preserved and reformatted.
pyTFM and numerical cleanup
saenopy/pyTFM/*
Formatting and minor expression normalization changes (e.g., exponent style), no algorithmic changes.
Tests & scripts formatting
test/*, saenopy/gui/tmp/*
Formatting and structural rewrites for clarity; no changed test assertions or behaviors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A rabbit nibbles at trailing whitespace,
Hops through functions with a gentle pace.
I tidy commas, quotes, and lines anew,
Ruff paw-prints tidy CI’s queue.
Hop—build passes, I munch a carrot or two. 🥕

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/bcc6f755

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
saenopy/gui/solver/modules/exporter/FiberViewer.py (2)

24-38: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Global cache has no eviction and can grow indefinitely.

caches keeps entries forever and is keyed by id(data). This can leak memory over long sessions and risks stale cache reuse if an object ID is recycled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/gui/solver/modules/exporter/FiberViewer.py` around lines 24 - 38, The
global caches dict keyed by id(data) in get_cache (and the Cache placeholder
class) can grow unbounded and risk stale reuse on id reuse; replace the manual
id-based dict with a weak-reference mapping (e.g., use weakref.WeakKeyDictionary
or WeakValueDictionary) keyed by the ndarray object itself so entries are
removed automatically when the array is garbage-collected, update get_cache to
look up and store cache objects in that weak map instead of using id(data), and
ensure any creation logic still returns a Cache instance when needed (symbols:
caches, Cache, get_cache, id(data)).

155-177: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix invalid default for process_stack intensity range.

scale_intensity() does tuple(range); with the current range=range default, omitting range passes the built-in range type, which will raise at runtime. Set a 2-number default instead (e.g., (0.0, 1.0)).

🐛 Proposed fix
 def process_stack(
     stack,
     channel,
@@
-    range=range,
+    range=(0.0, 1.0),
@@
 ):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/gui/solver/modules/exporter/FiberViewer.py` around lines 155 - 177,
The default argument for process_stack is wrong: change the parameter named
range (used by scale_intensity) from the built-in type to a numeric 2-tuple
(e.g., range=(0.0, 1.0)) so that scale_intensity(tuple(range)) receives a valid
pair; update the function signature in process_stack and any tests or callers
relying on the old default if needed, and ensure references to scale_intensity,
process_stack, and the range parameter are consistent.
🧹 Nitpick comments (7)
pyproject.toml (1)

84-84: ⚖️ Poor tradeoff

Consider incrementally removing E722 (bare except:) from the Ruff ignore list

File: pyproject.toml
Lines: 84-84

Snippet showing the final state of code at these lines:

"E722",

Ruff is configured to ignore E722, but ruff check --select E722 --statistics . reports 4 bare except: instances in saenopy/gui/common/ (QExtendedGraphicsView.py:324, QtShortCuts.py:1423, QtShortCuts.py:1458, QtShortCuts.py:1561). Bare except: catches SystemExit and KeyboardInterrupt, which can hinder debugging and graceful shutdown—replace with targeted exceptions (e.g., except Exception: or more specific types) as you remove the ignore incrementally (note: there are additional bare except: in unused/, which is excluded by config).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 84, Ruff is currently ignoring E722 (bare except:)
which masks four occurrences in saenopy/gui/common — replace each bare except:
in QExtendedGraphicsView.py (around line 324) and QtShortCuts.py (around lines
1423, 1458, 1561) with a more specific handler (preferably except Exception: or
an even narrower exception type relevant to the try block), run unit/interactive
tests and ruff (ruff check --select E722 --statistics .) to confirm no remaining
instances, and once all occurrences are fixed remove "E722" from the ignore list
in pyproject.toml so the linter enforces it going forward.
saenopy/gui/common/sigmoid_widget.py (1)

214-218: ⚡ Quick win

Remove unreachable branch in get_range().

Line 215 returns immediately, so Lines 216-218 are dead and the effective API is always normalized [minx, maxx]. Please delete the unreachable return (or invert the logic if mapped values are intended).

♻️ Proposed cleanup
 def get_range(self):
     return self.minx, self.maxx
-    return self.original_min + (self.original_max - self.original_min) * self.minx, self.original_min + (
-        self.original_max - self.original_min
-    ) * self.maxx
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/gui/common/sigmoid_widget.py` around lines 214 - 218, The get_range()
function currently has an early return that makes the mapped-value branch
unreachable; remove the immediate "return self.minx, self.maxx" and keep the
computed mapped return using original_min/original_max so get_range() returns
(original_min + (original_max - original_min)*minx, original_min + (original_max
- original_min)*maxx); ensure you update any callers if the API was intended to
stay normalized instead (alternatively invert this change if normalized values
are the desired API).
saenopy/pyTFM/calculate_stress_imports/mask_interpolation.py (1)

31-31: ⚡ Quick win

Remove debug print statement.

The print(iter) statement appears to be leftover debug code and should be removed or replaced with proper logging.

🧹 Suggested fix
-    print(iter)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/pyTFM/calculate_stress_imports/mask_interpolation.py` at line 31,
Remove the leftover debug print call print(iter) in mask_interpolation.py:
delete the print(iter) statement (or replace it with a suitable logger call
using the project's logging facility if runtime info is required) so there are
no stray prints; locate the occurrence by searching for the literal print(iter)
or references to the iter variable in the mask interpolation function and update
accordingly.
saenopy/gui/tmp/render_results2.py (1)

7-95: ⚡ Quick win

Remove or rename the first params block to avoid dead configuration.

Line 96 fully overwrites Line 7’s dictionary, so the first block is unreachable configuration and makes active settings ambiguous.

Also applies to: 96-185

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/gui/tmp/render_results2.py` around lines 7 - 95, The file defines the
dictionary named params twice (the initial params block and a later params that
overwrites it), leaving the first block dead; remove or rename the first params
block (or merge its intended keys into the later params dictionary) so there is
a single authoritative params variable used by the rest of render_results2.py,
and if you rename it update all references to params accordingly (search for the
symbol params to locate usages).
saenopy/gui/tfm2d/modules/draw.py (1)

120-121: ⚡ Quick win

Remove redundant import inside method.

The import numpy as np at line 120 is redundant since numpy is already imported at module level (line 1). Method-level imports are non-idiomatic and waste time on each method call.

🧹 Proposed fix
         draw.ellipse((x1 - size // 2, y1 - size // 2, x1 + size // 2, y1 + size // 2), fill=color)

-        import numpy as np
-
         im = np.asarray(self.full_image)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/gui/tfm2d/modules/draw.py` around lines 120 - 121, Remove the
redundant method-level "import numpy as np" statement in
saenopy/gui/tfm2d/modules/draw.py; instead rely on the existing module-level
numpy import (np) already present at the top of the file, so delete the
in-method import line and ensure all uses inside that function reference the
module-level "np" identifier.
saenopy/gui/orientation/modules/result.py (2)

63-91: 💤 Low value

Unusual TypedDict definition pattern.

Same issue as with GlobalResult above - the empty class definition is immediately shadowed by the functional TypedDict reassignment.

♻️ Suggested refactor to standard TypedDict pattern

Apply the same refactor as suggested for GlobalResult to use either purely class-based or purely functional syntax.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/gui/orientation/modules/result.py` around lines 63 - 91, The
DistanceResult TypedDict is currently defined with an empty class then
immediately reassigned via the TypedDict(...) functional call; remove the
redundant empty class definition and choose a single consistent pattern (either
convert the functional call into a class-based TypedDict class named
DistanceResult with the listed keys as annotations, or keep the functional
TypedDict(...) and delete the empty "class DistanceResult(TypedDict): pass");
update references if needed to the symbol DistanceResult so code uses the chosen
definition; mirror the same refactor you applied for GlobalResult to maintain
consistency.

44-60: 💤 Low value

Unusual TypedDict definition pattern.

The code defines an empty GlobalResult class, then immediately reassigns it with a TypedDict constructor. This pattern is functionally correct but unconventional:

class GlobalResult(TypedDict):
    pass

GlobalResult = TypedDict("GlobalResult", {...})

The more idiomatic approach would be either:

  • Use the class-based syntax with annotations directly
  • Use only the functional TypedDict(...) syntax without the empty class

This pattern may have been generated by auto-formatting and could confuse readers.

♻️ Suggested refactor to standard TypedDict pattern

Option 1 (class-based, preferred for complex types):

-class GlobalResult(TypedDict):
-    pass
-
-
-GlobalResult = TypedDict(
-    "GlobalResult",
-    {
+class GlobalResult(TypedDict):
         "Mean Coherency": float,
         "Mean Coherency (weighted by intensity)": float,
         ...
-    },
-)
+    }

Option 2 (functional, if dynamic construction is needed):

-class GlobalResult(TypedDict):
-    pass
-
-
 GlobalResult = TypedDict(
     "GlobalResult",
     {
         ...
     },
 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/gui/orientation/modules/result.py` around lines 44 - 60, The current
pattern creates an empty class GlobalResult(TypedDict) and then reassigns
GlobalResult via TypedDict(...); remove the redundant empty class and use a
single, idiomatic definition for the TypedDict: either replace both lines with a
class-based TypedDict named GlobalResult containing annotated keys ("Mean
Coherency", "Mean Angle", etc.) mapped to float, or keep only the functional
assignment GlobalResult = TypedDict("GlobalResult", {...}); ensure the final
symbol GlobalResult refers to a single TypedDict definition and adjust imports
if necessary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/test.yml:
- Line 16: Add credential persistence hardening to the checkout step: update the
GitHub Actions checkout step (uses: actions/checkout@v4) to set
persist-credentials: false so the GITHUB_TOKEN is not kept available to
subsequent steps; ensure the lint job does not rely on persisted credentials
after updating the checkout configuration.
- Around line 16-17: The workflow currently references actions by mutable tags
actions/checkout@v4 and astral-sh/setup-uv@v5; replace those tag refs with the
corresponding immutable commit SHAs for the exact releases (pin the usages of
actions/checkout and astral-sh/setup-uv to their commit hashes), verifying the
commit SHA for the desired v4 and v5 releases before updating the workflow so
each uses the full commit hash instead of the semantic tag.
- Around line 12-19: The lint job currently uses default GitHub Actions
permissions; update the "lint" job definition to explicitly set minimal
permissions (e.g., set permissions.contents: read) to enforce least-privilege
for the job named "lint" that runs the uvx ruff steps; add the permissions block
under the lint job (adjacent to runs-on/timeout-minutes) so only read access to
repository contents is granted.

In `@pyproject.toml`:
- Line 90: pyproject.toml currently globally ignores Ruff error code F821 which
is masking 51 undefined-name issues; remove "F821" from [tool.ruff.lint].ignore
and then fix the undefined references such as BatchEvaluate, StackSelector,
get_stacks_spheroid, and settings (e.g., import the correct modules, add missing
definitions, or qualify names) in the affected files like
saenopy/gui/orientation/modules/Detection.py; if a name is legitimately dynamic,
add a targeted per-file-ignores or inline `# noqa: F821` with a comment
explaining why instead of a global ignore.

In `@saenopy/examples.py`:
- Line 211: The example data for BrightfieldTFM uses the wrong material
parameter key "ds" which causes a KeyError when the solver expects
material_parameters["d_s"]; update the example dictionary in BrightfieldTFM to
use "d_s": 0.034 (or add a compatibility mapping) so SemiAffineFiberMaterial
construction reads the correct key from material_parameters.

In `@saenopy/gui/common/patch_lifreader.py`:
- Line 31: Remove the debugging print statement that spams logs in the recursive
XML parser: delete the
print(item.findall("./Data/Image/ImageDescription/Dimensions/DimensionDescription"))
call in patch_lifreader.py (the recursive traversal that iterates over item and
calls findall). If retaining info is needed for debugging, replace it with a
logger.debug(...) call instead of print so output is controllable via logging
level.

In `@saenopy/gui/solver/modules/exporter/ExporterRender2D.py`:
- Line 625: The direct index into factor_prefix (factor_prefix[power +
base_power]) can raise KeyError for computed exponents; change both occurrences
to safely look up the prefix using factor_prefix.get(power + base_power, "") (or
check membership with "if (power + base_power) in factor_prefix") and only
prepend the prefix when present, ensuring variables power, base_power and
factor_prefix in ExporterRender2D are used and both lines (the current
assignment and the similar one at the later occurrence) are updated accordingly.
- Around line 258-259: The code in ExporterRender2D.py normalizes map_im using
(vmax - vmin) which can be zero; update the normalization before the lines that
compute map_im = map_im / (vmax - vmin) and map_im = plt.get_cmap(cmap)(map_im)
to guard against division by zero (e.g., compute a safe denom = vmax - vmin and
if denom == 0 set denom = 1 or set map_im to zeros/ones as appropriate), then
perform normalization and optionally clamp values into [0,1] (using np.clip)
before calling plt.get_cmap(cmap) so color mapping remains stable when vmax ==
vmin.

In `@saenopy/gui/solver/modules/exporter/ExporterRender3D.py`:
- Around line 315-317: The scalar bar is being removed unconditionally; update
the code around plotter.remove_scalar_bar() to check params["colorbar"]["hide"]
first (i.e., call plotter.remove_scalar_bar() only when
params["colorbar"]["hide"] is True) so the 3D exporter honors the colorbar.hide
flag; look for the call to plotter.remove_scalar_bar() in ExporterRender3D and
gate it with an if params["colorbar"]["hide"]: condition.

In `@saenopy/gui/solver/modules/showVectorField.py`:
- Around line 179-181: Guard the denominator used to auto-scale "factor" in
showVectorField (the expression using np.nanmax(point_cloud[name + "_mag2"])
when computing factor) against 0 or NaN: compute denom =
np.nanmax(point_cloud[name + "_mag2"]), check with np.isfinite(denom) and denom
!= 0, and only apply factor = factor * norm_stack_size / denom when valid;
otherwise set factor to a safe default (e.g., 0 or leave unchanged) or skip
scaling so rendering remains stable. Ensure you reference the variables factor
and point_cloud[name + "_mag2"] and replace the direct division with this
guarded logic inside the same block where factor is computed.

In `@saenopy/gui/spheroid/modules/ForceCalculator.py`:
- Around line 79-86: process() is reading the live widget value
(str(self.lookup_table.value())) instead of the mapped parameter, which can
cause execution to use a different lookup table than the persisted value; change
process to pass force_parameters["lookup_table"] (converted to str if needed)
into jf.force.reconstruct_gui in place of str(self.lookup_table.value()),
ensuring the function uses the scheduled/exported lookup_table value rather than
the current widget state.

In `@saenopy/gui/spheroid/modules/LookupTable.py`:
- Around line 255-260: The run() method is still constructing the output path
from self.output (the input folder) even though you added the
QtShortCuts.QInputFilename widget self.lookup_table; update run() (and the
similar block around the other save usage at the 299-307 area) to use
self.lookup_table.value() as the destination path when present (falling back to
the previous behavior only if lookup_table.value() is empty), and ensure any
file writing uses that exact path (create parent dirs if needed) so
dialog.lookup_table.value() matches where the file is actually saved.

In `@saenopy/gui/tmp/render_results2.py`:
- Around line 337-341: The code normalizes y and then accesses np.where(y <
0.05)[0][0] without safety checks; add guards in the block around the
normalization and threshold lookup (the y = y / y[0], (l,) = plt.plot(...),
index_max = np.where(...)[0][0], plt.axvline(...) sequence): first ensure y is
non-empty and y[0] is not zero before dividing (skip plotting or handle
zero-case), and after computing idxs = np.where(y < 0.05)[0] check if idxs.size
> 0 before using idxs[0] (if empty, either skip drawing the vertical line or
choose a fallback index such as -1), so plt.axvline is only called with a valid
x[index_max].

In `@saenopy/gui/tmp/render_surface_comparison.py`:
- Around line 87-91: Protect the normalization and threshold lookup around the y
array to avoid division-by-zero and IndexError: before doing y = y / y[0] in the
block containing plt.plot(...) check if y.size>0 and y[0] != 0 (or fall back to
a safe normalization, e.g. skip normalization or use np.nan/1.0) and after
plotting, replace index_max = np.where(y < 0.05)[0][0] with a guarded search
that checks if any(y < 0.05) and only computes/plots the axvline when found
(otherwise skip or use a fallback index); refer to the y normalization line and
the index_max / plt.axvline usage to locate the edits.

In `@saenopy/macro.py`:
- Line 364: The subplot index calculation using plt.subplot(1, subplot_count, 1
+ (len(plots_stretch) > 0)) can produce an out-of-range index when subplot_count
== 1 because (len(plots_stretch) > 0) evaluates to True (1) and yields 2; change
the expression to only add the offset when there is more than one subplot.
Replace that call with logic that computes index = 1 + (1 if (len(plots_stretch)
> 0 and subplot_count > 1) else 0) and pass that index into plt.subplot,
referencing the same symbols plt.subplot, subplot_count, and plots_stretch.

In `@saenopy/pyTFM/calculate_stress_imports/mask_interpolation.py`:
- Line 29: The iteration count calculation in mask_interpolation.py erroneously
duplicates the x-axis ratio; update the expression that computes iter (the line
using np.max([...]) inside the mask interpolation routine) to compare
mask.shape[0] / dims[0] and mask.shape[1] / dims[1] so both height and width
scaling are considered (keep the ceil and * 5 safety factor intact). Ensure you
only change the second term from mask.shape[0] / dims[0] to mask.shape[1] /
dims[1] so the upscaling iteration count correctly reflects both axes.

In `@saenopy/solver.py`:
- Around line 1344-1350: The distance calculation is using
result.solvers[0].mesh instead of the current solver, so replace references to
result.solvers[0].mesh with result.solvers[i].mesh when computing dist_to_cell
(the np.min / np.linalg.norm expression that uses mesh.nodes and yxz), then keep
assigning result.solvers[i].mesh.cell_boundary_mask = included; ensure you use
the same element_size, yxz and i variables already in scope and that
result.solvers[i].mesh exists before computing.

In `@saenopy/unused/macro.py`:
- Line 591: The plt.subplot call uses 1 + (len(plots_stretch) > 0) which yields
2 when plots_stretch is non-empty (because True == 1) and can select a
non-existent subplot when subplot_count == 1; update the index expression in the
plt.subplot call (the one using subplot_count and plots_stretch) to ensure the
chosen index is clamped to the available subplot_count (for example, use min(1 +
int(len(plots_stretch) > 0), subplot_count) or equivalent) so the index never
exceeds subplot_count.

---

Outside diff comments:
In `@saenopy/gui/solver/modules/exporter/FiberViewer.py`:
- Around line 24-38: The global caches dict keyed by id(data) in get_cache (and
the Cache placeholder class) can grow unbounded and risk stale reuse on id
reuse; replace the manual id-based dict with a weak-reference mapping (e.g., use
weakref.WeakKeyDictionary or WeakValueDictionary) keyed by the ndarray object
itself so entries are removed automatically when the array is garbage-collected,
update get_cache to look up and store cache objects in that weak map instead of
using id(data), and ensure any creation logic still returns a Cache instance
when needed (symbols: caches, Cache, get_cache, id(data)).
- Around line 155-177: The default argument for process_stack is wrong: change
the parameter named range (used by scale_intensity) from the built-in type to a
numeric 2-tuple (e.g., range=(0.0, 1.0)) so that scale_intensity(tuple(range))
receives a valid pair; update the function signature in process_stack and any
tests or callers relying on the old default if needed, and ensure references to
scale_intensity, process_stack, and the range parameter are consistent.

---

Nitpick comments:
In `@pyproject.toml`:
- Line 84: Ruff is currently ignoring E722 (bare except:) which masks four
occurrences in saenopy/gui/common — replace each bare except: in
QExtendedGraphicsView.py (around line 324) and QtShortCuts.py (around lines
1423, 1458, 1561) with a more specific handler (preferably except Exception: or
an even narrower exception type relevant to the try block), run unit/interactive
tests and ruff (ruff check --select E722 --statistics .) to confirm no remaining
instances, and once all occurrences are fixed remove "E722" from the ignore list
in pyproject.toml so the linter enforces it going forward.

In `@saenopy/gui/common/sigmoid_widget.py`:
- Around line 214-218: The get_range() function currently has an early return
that makes the mapped-value branch unreachable; remove the immediate "return
self.minx, self.maxx" and keep the computed mapped return using
original_min/original_max so get_range() returns (original_min + (original_max -
original_min)*minx, original_min + (original_max - original_min)*maxx); ensure
you update any callers if the API was intended to stay normalized instead
(alternatively invert this change if normalized values are the desired API).

In `@saenopy/gui/orientation/modules/result.py`:
- Around line 63-91: The DistanceResult TypedDict is currently defined with an
empty class then immediately reassigned via the TypedDict(...) functional call;
remove the redundant empty class definition and choose a single consistent
pattern (either convert the functional call into a class-based TypedDict class
named DistanceResult with the listed keys as annotations, or keep the functional
TypedDict(...) and delete the empty "class DistanceResult(TypedDict): pass");
update references if needed to the symbol DistanceResult so code uses the chosen
definition; mirror the same refactor you applied for GlobalResult to maintain
consistency.
- Around line 44-60: The current pattern creates an empty class
GlobalResult(TypedDict) and then reassigns GlobalResult via TypedDict(...);
remove the redundant empty class and use a single, idiomatic definition for the
TypedDict: either replace both lines with a class-based TypedDict named
GlobalResult containing annotated keys ("Mean Coherency", "Mean Angle", etc.)
mapped to float, or keep only the functional assignment GlobalResult =
TypedDict("GlobalResult", {...}); ensure the final symbol GlobalResult refers to
a single TypedDict definition and adjust imports if necessary.

In `@saenopy/gui/tfm2d/modules/draw.py`:
- Around line 120-121: Remove the redundant method-level "import numpy as np"
statement in saenopy/gui/tfm2d/modules/draw.py; instead rely on the existing
module-level numpy import (np) already present at the top of the file, so delete
the in-method import line and ensure all uses inside that function reference the
module-level "np" identifier.

In `@saenopy/gui/tmp/render_results2.py`:
- Around line 7-95: The file defines the dictionary named params twice (the
initial params block and a later params that overwrites it), leaving the first
block dead; remove or rename the first params block (or merge its intended keys
into the later params dictionary) so there is a single authoritative params
variable used by the rest of render_results2.py, and if you rename it update all
references to params accordingly (search for the symbol params to locate
usages).

In `@saenopy/pyTFM/calculate_stress_imports/mask_interpolation.py`:
- Line 31: Remove the leftover debug print call print(iter) in
mask_interpolation.py: delete the print(iter) statement (or replace it with a
suitable logger call using the project's logging facility if runtime info is
required) so there are no stray prints; locate the occurrence by searching for
the literal print(iter) or references to the iter variable in the mask
interpolation function and update accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 70f02b39-e054-4dab-9068-471b8fc4c333

📥 Commits

Reviewing files that changed from the base of the PR and between ecf6092 and 67b43aa.

📒 Files selected for processing (133)
  • .github/workflows/test.yml
  • pyproject.toml
  • saenopy/__init__.py
  • saenopy/add_source.py
  • saenopy/conjugate_gradient.py
  • saenopy/examples.py
  • saenopy/export_html.py
  • saenopy/get_deformations.py
  • saenopy/gui/code/code_editor.py
  • saenopy/gui/code/gui_code.py
  • saenopy/gui/code/script_file.py
  • saenopy/gui/code/syntax.py
  • saenopy/gui/code_evluate_normal.py
  • saenopy/gui/common/AddFilesDialog.py
  • saenopy/gui/common/BatchEvaluateBase.py
  • saenopy/gui/common/ModuleColorBar.py
  • saenopy/gui/common/ModuleScaleBar.py
  • saenopy/gui/common/PipelineModule.py
  • saenopy/gui/common/PlottingWindowBase.py
  • saenopy/gui/common/QExtendedGraphicsView.py
  • saenopy/gui/common/QTimeSlider.py
  • saenopy/gui/common/QtShortCuts.py
  • saenopy/gui/common/TabModule.py
  • saenopy/gui/common/code_export.py
  • saenopy/gui/common/gui_classes.py
  • saenopy/gui/common/lif_reader.py
  • saenopy/gui/common/patch_lifreader.py
  • saenopy/gui/common/resources.py
  • saenopy/gui/common/sigmoid_widget.py
  • saenopy/gui/common/stack_preview.py
  • saenopy/gui/common/stack_selector.py
  • saenopy/gui/common/stack_selector_crop.py
  • saenopy/gui/common/stack_selector_leica.py
  • saenopy/gui/common/stack_selector_tif.py
  • saenopy/gui/example_creator.py
  • saenopy/gui/gui_master.py
  • saenopy/gui/material_fit/gui_fit.py
  • saenopy/gui/orientation/analyze/PlottingWindow.py
  • saenopy/gui/orientation/gui_orientation.py
  • saenopy/gui/orientation/modules/AddFilesDialog.py
  • saenopy/gui/orientation/modules/BatchEvaluate.py
  • saenopy/gui/orientation/modules/Detection.py
  • saenopy/gui/orientation/modules/Segmentation.py
  • saenopy/gui/orientation/modules/TabAngle.py
  • saenopy/gui/orientation/modules/TabCellOutline.py
  • saenopy/gui/orientation/modules/TabCoherence.py
  • saenopy/gui/orientation/modules/TabOrientationField.py
  • saenopy/gui/orientation/modules/result.py
  • saenopy/gui/solver/analyze/PlottingWindow.py
  • saenopy/gui/solver/gui_solver.py
  • saenopy/gui/solver/modules/BatchEvaluate.py
  • saenopy/gui/solver/modules/DeformationDetector.py
  • saenopy/gui/solver/modules/Initializer.py
  • saenopy/gui/solver/modules/MeshCreator.py
  • saenopy/gui/solver/modules/Regularizer.py
  • saenopy/gui/solver/modules/TabFittedDeformations.py
  • saenopy/gui/solver/modules/TabForces.py
  • saenopy/gui/solver/modules/TabPiv.py
  • saenopy/gui/solver/modules/TabResultView.py
  • saenopy/gui/solver/modules/TabStackDisplay.py
  • saenopy/gui/solver/modules/TabTargetDeformations.py
  • saenopy/gui/solver/modules/VTK_Toolbar.py
  • saenopy/gui/solver/modules/exporter/ExportRenderCommon.py
  • saenopy/gui/solver/modules/exporter/Exporter.py
  • saenopy/gui/solver/modules/exporter/ExporterRender2D.py
  • saenopy/gui/solver/modules/exporter/ExporterRender3D.py
  • saenopy/gui/solver/modules/exporter/FiberViewer.py
  • saenopy/gui/solver/modules/load_measurement_dialog.py
  • saenopy/gui/solver/modules/path_editor.py
  • saenopy/gui/solver/modules/showVectorField.py
  • saenopy/gui/spheroid/analyze/PlottingWindow.py
  • saenopy/gui/spheroid/gui_deformation_spheroid.py
  • saenopy/gui/spheroid/modules/AddFilesDialog.py
  • saenopy/gui/spheroid/modules/BatchEvaluate.py
  • saenopy/gui/spheroid/modules/DeformationDetector.py
  • saenopy/gui/spheroid/modules/ForceCalculator.py
  • saenopy/gui/spheroid/modules/ListWidget.py
  • saenopy/gui/spheroid/modules/LookupTable.py
  • saenopy/gui/spheroid/modules/MatplotlibWidget.py
  • saenopy/gui/spheroid/modules/QSlider.py
  • saenopy/gui/spheroid/modules/helper.py
  • saenopy/gui/spheroid/modules/path_editor.py
  • saenopy/gui/spheroid/modules/result.py
  • saenopy/gui/tfm2d/analyze/PlottingWindow.py
  • saenopy/gui/tfm2d/gui_2d.py
  • saenopy/gui/tfm2d/modules/BatchEvaluate.py
  • saenopy/gui/tfm2d/modules/CalculateDisplacements.py
  • saenopy/gui/tfm2d/modules/CalculateForceGeneration.py
  • saenopy/gui/tfm2d/modules/CalculateForces.py
  • saenopy/gui/tfm2d/modules/CalculateStress.py
  • saenopy/gui/tfm2d/modules/DisplayCellImage.py
  • saenopy/gui/tfm2d/modules/DisplayDeformed.py
  • saenopy/gui/tfm2d/modules/PipelineModule.py
  • saenopy/gui/tfm2d/modules/TabDeformations.py
  • saenopy/gui/tfm2d/modules/TabForces.py
  • saenopy/gui/tfm2d/modules/TabStress.py
  • saenopy/gui/tfm2d/modules/draw.py
  • saenopy/gui/tfm2d/modules/load_measurement_dialog.py
  • saenopy/gui/tfm2d/modules/path_editor.py
  • saenopy/gui/tfm2d/modules/result.py
  • saenopy/gui/tmp/render_results2.py
  • saenopy/gui/tmp/render_surface_comparison.py
  • saenopy/macro.py
  • saenopy/materials.py
  • saenopy/mesh.py
  • saenopy/multigrid_helper.py
  • saenopy/pyTFM/TFM_functions.py
  • saenopy/pyTFM/TFM_tractions.py
  • saenopy/pyTFM/calculate_deformation.py
  • saenopy/pyTFM/calculate_forces.py
  • saenopy/pyTFM/calculate_strain_energy.py
  • saenopy/pyTFM/calculate_stress.py
  • saenopy/pyTFM/calculate_stress_imports/find_borders.py
  • saenopy/pyTFM/calculate_stress_imports/graph_theory_for_cell_boundaries.py
  • saenopy/pyTFM/calculate_stress_imports/grid_setup_solids_py.py
  • saenopy/pyTFM/calculate_stress_imports/mask_interpolation.py
  • saenopy/pyTFM/calculate_stress_imports/stress_functions.py
  • saenopy/pyTFM/correct_stage_drift.py
  • saenopy/pyTFM/plotting.py
  • saenopy/result_file.py
  • saenopy/saveable.py
  • saenopy/solver.py
  • saenopy/stack.py
  • saenopy/unused/macro.py
  • test/interactive_material.py
  • test/interactive_material2.py
  • test/mock_dir.py
  • test/test_download_example.py
  • test/test_load_stack.py
  • test/test_material.py
  • test/test_mesh.py
  • test/test_non_regularize.py
  • test/test_use_gui.py

Comment on lines +12 to +19
lint:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uvx ruff check .
- run: uvx ruff format --check .

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add explicit permissions to follow least-privilege principle.

The lint job inherits default permissions, which are broader than necessary. Restrict to read-only contents access.

🔒 Recommended fix
 lint:
   runs-on: ubuntu-latest
   timeout-minutes: 5
+  permissions:
+    contents: read
   steps:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lint:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uvx ruff check .
- run: uvx ruff format --check .
lint:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uvx ruff check .
- run: uvx ruff format --check .
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 16-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 12-19: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)


[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 17-17: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 12 - 19, The lint job currently uses
default GitHub Actions permissions; update the "lint" job definition to
explicitly set minimal permissions (e.g., set permissions.contents: read) to
enforce least-privilege for the job named "lint" that runs the uvx ruff steps;
add the permissions block under the lint job (adjacent to
runs-on/timeout-minutes) so only read access to repository contents is granted.

runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider disabling credential persistence for defense-in-depth.

While this lint job doesn't use credentials, setting persist-credentials: false prevents the GITHUB_TOKEN from being accessible to subsequent steps as a security best practice.

🔒 Optional hardening
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 16-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml at line 16, Add credential persistence hardening
to the checkout step: update the GitHub Actions checkout step (uses:
actions/checkout@v4) to set persist-credentials: false so the GITHUB_TOKEN is
not kept available to subsequent steps; ensure the lint job does not rely on
persisted credentials after updating the checkout configuration.

Comment on lines +16 to +17
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin action references to commit hashes for supply-chain security.

Both actions use semantic version tags rather than commit hashes, creating a supply-chain risk since tags can theoretically be moved. Best practice is to pin to immutable commit hashes.

🔒 Recommended fix
-      - uses: actions/checkout@v4
-      - uses: astral-sh/setup-uv@v5
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
+      - uses: astral-sh/setup-uv@f0ec0a0267ad3f8c0311dfa8cecbddbe9e4d4204  # v5.3.0

Note: Verify the latest commit hashes for these versions at the time of merge.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: astral-sh/setup-uv@f0ec0a0267ad3f8c0311dfa8cecbddbe9e4d4204 # v5.3.0
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 16-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 17-17: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 16 - 17, The workflow currently
references actions by mutable tags actions/checkout@v4 and
astral-sh/setup-uv@v5; replace those tag refs with the corresponding immutable
commit SHAs for the exact releases (pin the usages of actions/checkout and
astral-sh/setup-uv to their commit hashes), verifying the commit SHA for the
desired v4 and v5 releases before updating the workflow so each uses the full
commit hash instead of the semantic tag.

Comment thread pyproject.toml
"F401",
"F541",
"F811",
"F821",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for undefined name violations that are currently being ignored

# Create temporary config without F821 in ignore list
uvx ruff check --select F821 --statistics .

Repository: rgerum/saenopy

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect Ruff config around the mentioned lines
echo "== pyproject.toml (Ruff section near line 90) =="
python3 - <<'PY'
import itertools
path="pyproject.toml"
start=60
end=110
with open(path,'r',encoding='utf-8') as f:
    for i,line in enumerate(f,1):
        if start <= i <= end:
            print(f"{i:4d}:{line.rstrip()}")
        if i>end: break
PY

# 2) Check whether ruff is available in this environment
echo "== ruff availability =="
( ruff --version ) 2>/dev/null || echo "ruff not found on PATH"
( python3 -m ruff --version ) 2>/dev/null || echo "python -m ruff not available"

# 3) Try running F821-only checks
echo "== running: ruff check --select F821 --statistics . =="
if command -v ruff >/dev/null 2>&1; then
  ruff check --select F821 --statistics .
else
  python3 -m ruff check --select F821 --statistics .
fi

Repository: rgerum/saenopy

Length of output: 874


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== running: ruff check --statistics . (default config) =="
ruff check --statistics . || true

echo
echo "== F821 file counts =="
ruff check --select F821 --statistics . --output-format=concise 2>/dev/null | head -n 50 || true

echo
echo "== sample F821 errors (first 30) =="
ruff check --select F821 . --output-format=full 2>/dev/null | head -n 120 || true

Repository: rgerum/saenopy

Length of output: 5863


Stop globally ignoring Ruff F821—it’s currently masking real undefined-name findings.

pyproject.toml globally ignores F821 ([tool.ruff.lint].ignore includes "F821"), and yet ruff check --select F821 reports 51 F821 undefined-name errors—e.g., undefined BatchEvaluate/StackSelector type refs and (more concerning) undefined get_stacks_spheroid and settings in saenopy/gui/orientation/modules/Detection.py.

Remove F821 from the global ignore and fix the reported undefined names; if any are truly unavoidable (e.g., dynamic imports), use targeted # noqa: F821 / per-file-ignores with documentation rather than a blanket ignore.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 90, pyproject.toml currently globally ignores Ruff
error code F821 which is masking 51 undefined-name issues; remove "F821" from
[tool.ruff.lint].ignore and then fix the undefined references such as
BatchEvaluate, StackSelector, get_stacks_spheroid, and settings (e.g., import
the correct modules, add missing definitions, or qualify names) in the affected
files like saenopy/gui/orientation/modules/Detection.py; if a name is
legitimately dynamic, add a targeted per-file-ignores or inline `# noqa: F821`
with a comment explaining why instead of a global ignore.

Comment thread saenopy/examples.py
"k": 6062.0,
"d_0": 0.0025,
"lambda_s": 0.0804,
"ds": 0.034,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check which material parameter key name is used in materials module and other examples

# Check the materials module for expected parameter names
echo "=== Checking materials module for parameter definitions ==="
rg -n -C3 "class.*Material|def __init__.*d_s|def __init__.*ds" saenopy/materials.py

# Check other examples for consistency
echo -e "\n=== Checking other example configs for d_s vs ds usage ==="
rg -n "\"d_s\"|'d_s'|\"ds\"|'ds'" saenopy/examples.py

# Check if any code references the old key name
echo -e "\n=== Checking for references to material_parameters\[\"d_s\"\] ==="
rg -n "material_parameters\[.*d_s" --type py

Repository: rgerum/saenopy

Length of output: 1892


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for material parameter access using key \"ds\" ==="
rg -n "material_parameters\[['\"]ds['\"]\]|material_parameters\.get\(['\"]ds['\"]\)" -S . --type py

echo -e "\n=== Search for any usage of string literal \"ds\" alongside \"d_s\" ==="
rg -n "\"ds\"|'ds'|\"d_s\"|'d_s'" saenopy -S --type py

echo -e "\n=== Search for mapping/normalization from \"ds\" to d_s ==="
rg -n "ds\s*[:=]\s*|['\"]ds['\"]\s*(\)|\]|\}|,)|replace\(|lower\(|upper\(|normalize" saenopy -S --type py

Repository: rgerum/saenopy

Length of output: 118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Occurrences of string literal \"ds\" in saenopy (python only) ==="
rg -n "\"ds\"|'ds'" saenopy -S --type py

echo -e "\n=== Occurrences of parameter key \"d_s\" usage (python only) ==="
rg -n "material_parameters\[['\"]d_s['\"]\]|material_parameters\.get\(['\"]d_s['\"]\)" saenopy -S --type py

echo -e "\n=== Inspect SemiAffineFiberMaterial signature / assignments in saenopy/materials.py ==="
python3 - <<'PY'
import itertools, pathlib, re
p = pathlib.Path("saenopy/materials.py")
lines = p.read_text().splitlines()
for i,l in enumerate(lines, start=1):
    if "class SemiAffineFiberMaterial" in l:
        start=i-5
        end=i+80
        for j in range(start, end+1):
            if 1<=j<=len(lines):
                print(f"{j}:{lines[j-1]}")
        break
PY

Repository: rgerum/saenopy

Length of output: 4639


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Where material model is constructed from material_parameters (search) ==="
rg -n "SemiAffineFiberMaterial\(|LinearMaterial\(|material_parameters\s*=" -S saenopy --type py

echo -e "\n=== Search for places that reference material_parameters dict contents ==="
rg -n "material_parameters\[" -S saenopy --type py

echo -e "\n=== Inspect result_file.py conversion from old key \"ds\" to \"d_s\" ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("saenopy/result_file.py")
lines=p.read_text().splitlines()
for i,l in enumerate(lines, start=1):
    if 'old="ds"' in l or "new=\"d_s\"" in l:
        start=max(1,i-15); end=min(len(lines), i+15)
        for j in range(start,end+1):
            print(f"{j}:{lines[j-1]}")
        print("-----")
PY

echo -e "\n=== Search for any normalization of ds->d_s outside result_file.py ==="
rg -n "old=\"ds\"|new=\"d_s\"|ds\".*d_s|d_s.*ds|normalize.*ds|\\bds\\b.*\\bd_s\\b" -S saenopy --type py

Repository: rgerum/saenopy

Length of output: 8519


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Context around saenopy/examples.py:211 ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("saenopy/examples.py")
lines=p.read_text().splitlines()
for i in range(190, 225):
    if 1 <= i <= len(lines):
        print(f"{i}:{lines[i-1]}")
PY

echo -e "\n=== Inspect saenopy/gui/code_evluate_normal.py material_parameters construction ==="
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("saenopy/gui/code_evluate_normal.py")
lines=p.read_text().splitlines()
# print around the "material_parameters =" block
for idx,l in enumerate(lines, start=1):
    if re.search(r"\bmaterial_parameters\s*=\s*{", l):
        start=max(1, idx-5); end=min(len(lines), idx+25)
        for i in range(start,end+1):
            print(f"{i}:{lines[i-1]}")
        break
PY

echo -e "\n=== Inspect Regularizer.py where it reads material_parameters keys ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("saenopy/gui/solver/modules/Regularizer.py")
lines=p.read_text().splitlines()
for target in [300,360,370,400]:
    start=max(1,target-5); end=min(len(lines), target+20)
    print(f"\n--- around line {target} ---")
    for i in range(start,end+1):
        print(f"{i}:{lines[i-1]}")
PY

Repository: rgerum/saenopy

Length of output: 8141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find where saenopy/examples.py is imported/used ==="
rg -n "import saenopy\.examples|from saenopy\.examples import|saenopy\.examples" -S saenopy --type py

echo -e "\n=== Find where example[\"material_parameters\"] is accessed ==="
rg -n "example\[['\"]material_parameters['\"]\]|\\bmaterial_parameters\\b\\s*=\\s*example\\[|\\bmaterial_parameters\\b\\s*=\\s*.*example\\[" -S saenopy --type py

echo -e "\n=== Find where downloaded example dicts are wired into solver Regularizer ==="
rg -n "material_parameters\\b.*example|example.*material_parameters" -S saenopy --type py

Repository: rgerum/saenopy

Length of output: 1296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Inspect saenopy/gui/solver/modules/BatchEvaluate.py around example material_parameters ==="
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("saenopy/gui/solver/modules/BatchEvaluate.py")
lines=p.read_text().splitlines()
for i,l in enumerate(lines, start=1):
    if 'data.material_parameters = example["material_parameters"]' in l:
        start=max(1,i-30); end=min(len(lines), i+80)
        for j in range(start,end+1):
            print(f"{j}:{lines[j-1]}")
        break
PY

echo -e "\n=== Search for material_parameters usage in BatchEvaluate.py ==="
rg -n "material_parameters" saenopy/gui/solver/modules/BatchEvaluate.py --type py

Repository: rgerum/saenopy

Length of output: 2160


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for get_examples implementation and any ds->d_s normalization in saenopy/examples.py ==="
rg -n "def get_examples|get_examples\(" saenopy/examples.py --type py

echo -e "\n=== List all occurrences of \"ds\" in saenopy/examples.py with context ==="
rg -n -C2 "\"ds\"|'ds'" saenopy/examples.py --type py

echo -e "\n=== Look for any replace/rename mapping between ds and d_s in saenopy/examples.py ==="
rg -n "ds.*d_s|d_s.*ds|rename.*ds|replace.*ds|material_parameters.*ds" saenopy/examples.py --type py

echo -e "\n=== Inspect get_examples function block (approx) ==="
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("saenopy/examples.py")
lines=p.read_text().splitlines()
# find first def get_examples occurrence
idx=None
for i,l in enumerate(lines, start=1):
    if re.match(r'\s*def\s+get_examples\s*\(', l):
        idx=i; break
if idx is None:
    # fallback: any get_examples_*
    for i,l in enumerate(lines, start=1):
        if 'def get_examples' in l:
            idx=i; break
if idx:
    start=max(1,idx-10); end=min(len(lines), idx+200)
    for j in range(start,end+1):
        print(f"{j}:{lines[j-1]}")
else:
    print("No get_examples definition found")
PY

Repository: rgerum/saenopy

Length of output: 603


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for get_examples implementation and any ds->d_s normalization in saenopy/examples.py ==="
rg -n "def get_examples|get_examples\(" saenopy/examples.py --type py

echo -e "\n=== List all occurrences of \"ds\" in saenopy/examples.py with context ==="
rg -n -C2 "\"ds\"|'ds'" saenopy/examples.py --type py

echo -e "\n=== Look for any replace/rename mapping between ds and d_s in saenopy/examples.py ==="
rg -n "ds.*d_s|d_s.*ds|rename.*ds|replace.*ds|material_parameters.*ds" saenopy/examples.py --type py

echo -e "\n=== Inspect get_examples function block (approx) ==="
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("saenopy/examples.py")
lines=p.read_text().splitlines()
idx=None
for i,l in enumerate(lines, start=1):
    if re.match(r'\s*def\s+get_examples\s*\(', l):
        idx=i; break
if idx is None:
    for i,l in enumerate(lines, start=1):
        if 'def get_examples' in l:
            idx=i; break
if idx:
    start=max(1,idx-10); end=min(len(lines), idx+200)
    for j in range(start,end+1):
        print(f"{j}:{lines[j-1]}")
else:
    print("No get_examples definition found")
PY

Repository: rgerum/saenopy

Length of output: 603


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Inspect saenopy/examples.py: get_examples() block (around line 76) ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("saenopy/examples.py")
lines=p.read_text().splitlines()
start=40; end=160
for i in range(start, min(end, len(lines))+1):
    print(f"{i}:{lines[i-1]}")
PY

echo -e "\n=== Inspect saenopy/examples.py: section containing material_parameters with \"ds\" (around line 180-230) ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("saenopy/examples.py")
lines=p.read_text().splitlines()
start=160; end=235
for i in range(start, min(end, len(lines))+1):
    print(f"{i}:{lines[i-1]}")
PY

Repository: rgerum/saenopy

Length of output: 9700


Fix inconsistent material parameter key in example data (d_s vs ds).

saenopy/examples.py uses "ds": 0.034, in the BrightfieldTFM example, while the solver expects material_parameters["d_s"] when constructing SemiAffineFiberMaterial (and examples are passed through without normalization), so loading that example will raise a KeyError.

"ds": 0.034,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/examples.py` at line 211, The example data for BrightfieldTFM uses
the wrong material parameter key "ds" which causes a KeyError when the solver
expects material_parameters["d_s"]; update the example dictionary in
BrightfieldTFM to use "d_s": 0.034 (or add a compatibility mapping) so
SemiAffineFiberMaterial construction reads the correct key from
material_parameters.

Comment on lines +87 to +91
y = y / y[0]
(l,) = plt.plot(x * 1e6, y * 100, "-", label=label)
index_max = np.where(y < 0.05)[0][0]
print(x[index_max] * 1e6)
plt.axvline(x[index_max] * 1e6, color=l.get_color(), lw=0.8, linestyle="--")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard normalization/threshold lookup to avoid runtime crashes.

Line 87 and Line 89 can fail on valid edge inputs (y[0] == 0 or no values below 0.05), causing division by zero or IndexError.

Proposed fix
-        y = y / y[0]
-        (l,) = plt.plot(x * 1e6, y * 100, "-", label=label)
-        index_max = np.where(y < 0.05)[0][0]
-        print(x[index_max] * 1e6)
-        plt.axvline(x[index_max] * 1e6, color=l.get_color(), lw=0.8, linestyle="--")
+        if y.size == 0 or y[0] <= 0:
+            return None
+        y = y / y[0]
+        (l,) = plt.plot(x * 1e6, y * 100, "-", label=label)
+        below = np.where(y < 0.05)[0]
+        if below.size:
+            index_max = below[0]
+            print(x[index_max] * 1e6)
+            plt.axvline(x[index_max] * 1e6, color=l.get_color(), lw=0.8, linestyle="--")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
y = y / y[0]
(l,) = plt.plot(x * 1e6, y * 100, "-", label=label)
index_max = np.where(y < 0.05)[0][0]
print(x[index_max] * 1e6)
plt.axvline(x[index_max] * 1e6, color=l.get_color(), lw=0.8, linestyle="--")
if y.size == 0 or y[0] <= 0:
return None
y = y / y[0]
(l,) = plt.plot(x * 1e6, y * 100, "-", label=label)
below = np.where(y < 0.05)[0]
if below.size:
index_max = below[0]
print(x[index_max] * 1e6)
plt.axvline(x[index_max] * 1e6, color=l.get_color(), lw=0.8, linestyle="--")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/gui/tmp/render_surface_comparison.py` around lines 87 - 91, Protect
the normalization and threshold lookup around the y array to avoid
division-by-zero and IndexError: before doing y = y / y[0] in the block
containing plt.plot(...) check if y.size>0 and y[0] != 0 (or fall back to a safe
normalization, e.g. skip normalization or use np.nan/1.0) and after plotting,
replace index_max = np.where(y < 0.05)[0][0] with a guarded search that checks
if any(y < 0.05) and only computes/plots the axvline when found (otherwise skip
or use a fallback index); refer to the y normalization line and the index_max /
plt.axvline usage to locate the edits.

Comment thread saenopy/macro.py
plt.ylabel("stress")
if len(plots_stretch):
plt.subplot(1, subplot_count, 1+(len(plots_stretch)>0))
plt.subplot(1, subplot_count, 1 + (len(plots_stretch) > 0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix subplot index calculation for stretch-only runs.

Line 364 computes subplot index from plots_stretch, which becomes 2 when subplot_count == 1, causing an invalid subplot selection.

Proposed fix
-            plt.subplot(1, subplot_count, 1 + (len(plots_stretch) > 0))
+            plt.subplot(1, subplot_count, 1 + (len(plots_shear) > 0))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
plt.subplot(1, subplot_count, 1 + (len(plots_stretch) > 0))
plt.subplot(1, subplot_count, 1 + (len(plots_shear) > 0))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/macro.py` at line 364, The subplot index calculation using
plt.subplot(1, subplot_count, 1 + (len(plots_stretch) > 0)) can produce an
out-of-range index when subplot_count == 1 because (len(plots_stretch) > 0)
evaluates to True (1) and yields 2; change the expression to only add the offset
when there is more than one subplot. Replace that call with logic that computes
index = 1 + (1 if (len(plots_stretch) > 0 and subplot_count > 1) else 0) and
pass that index into plt.subplot, referencing the same symbols plt.subplot,
subplot_count, and plots_stretch.

iter = int(
np.ceil(np.max([mask.shape[0] / dims[0], mask.shape[0] / dims[0]])) * 5
) # times 5 is safety factor
iter = int(np.ceil(np.max([mask.shape[0] / dims[0], mask.shape[0] / dims[0]])) * 5) # times 5 is safety factor

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the function usage and any related tests
rg -n "mask_interpolation" --type=py -C3

Repository: rgerum/saenopy

Length of output: 5734


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact code around the referenced line
sed -n '1,120p' saenopy/pyTFM/calculate_stress_imports/mask_interpolation.py | nl -ba | sed -n '20,60p'

# Check for the duplicated ratio pattern anywhere else
rg -n "mask\.shape\[0\]\s*/\s*dims\[0\].*mask\.shape\[0\]\s*/\s*dims\[0\]" saenopy/pyTFM || true
rg -n "np\.max\(\[mask\.shape\[0\]\s*/\s*dims\[0\]" saenopy/pyTFM || true

# Search for any tests mentioning mask_interpolation / interpolation behavior
rg -n "mask_interpolation" --type=py -S . || true

Repository: rgerum/saenopy

Length of output: 101


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="saenopy/pyTFM/calculate_stress_imports/mask_interpolation.py"

echo "---- code around referenced line ----"
python3 - <<'PY'
import itertools
path="saenopy/pyTFM/calculate_stress_imports/mask_interpolation.py"
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if 10 <= i <= 60:
            print(f"{i:4d}:{line.rstrip()}")
PY

echo "---- search for the duplication pattern ----"
rg -n "mask\.shape\[0\]\s*/\s*dims\[0\].*mask\.shape\[0\]\s*/\s*dims\[0\]" saenopy/pyTFM || true
rg -n "np\.max\(\[mask\.shape\[0\]\s*/\s*dims\[0\]" saenopy/pyTFM || true

echo "---- search for any tests referencing mask_interpolation ----"
rg -n "mask_interpolation" -S . || true

Repository: rgerum/saenopy

Length of output: 3013


Fix duplicated dimension ratio in mask interpolation iteration calculation

In saenopy/pyTFM/calculate_stress_imports/mask_interpolation.py (line 29), np.max([mask.shape[0] / dims[0], mask.shape[0] / dims[0]]) repeats the same value, so iter ignores the y-axis scaling; the second term should use mask.shape[1] / dims[1] to compare both dimensions for the upscaling case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/pyTFM/calculate_stress_imports/mask_interpolation.py` at line 29, The
iteration count calculation in mask_interpolation.py erroneously duplicates the
x-axis ratio; update the expression that computes iter (the line using
np.max([...]) inside the mask interpolation routine) to compare mask.shape[0] /
dims[0] and mask.shape[1] / dims[1] so both height and width scaling are
considered (keep the ceil and * 5 safety factor intact). Ensure you only change
the second term from mask.shape[0] / dims[0] to mask.shape[1] / dims[1] so the
upscaling iteration count correctly reflects both axes.

Comment thread saenopy/solver.py
Comment on lines +1344 to 1350
dist_to_cell = np.min(
np.linalg.norm(result.solvers[0].mesh.nodes[:, :, None] - yxz[None, :, :], axis=1),
axis=1,
)
included = dist_to_cell < element_size / 2

result.solvers[i].mesh.cell_boundary_mask = included

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the current solver index when computing cell-boundary distances.

Line 1344 hardcodes result.solvers[0] while Line 1350 writes to result.solvers[i], so multi-stack results get masks from the wrong mesh.

Proposed fix
-        dist_to_cell = np.min(
-            np.linalg.norm(result.solvers[0].mesh.nodes[:, :, None] - yxz[None, :, :], axis=1),
-            axis=1,
-        )
+        dist_to_cell = np.min(
+            np.linalg.norm(result.solvers[i].mesh.nodes[:, :, None] - yxz[None, :, :], axis=1),
+            axis=1,
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dist_to_cell = np.min(
np.linalg.norm(result.solvers[0].mesh.nodes[:, :, None] - yxz[None, :, :], axis=1),
axis=1,
)
included = dist_to_cell < element_size / 2
result.solvers[i].mesh.cell_boundary_mask = included
dist_to_cell = np.min(
np.linalg.norm(result.solvers[i].mesh.nodes[:, :, None] - yxz[None, :, :], axis=1),
axis=1,
)
included = dist_to_cell < element_size / 2
result.solvers[i].mesh.cell_boundary_mask = included
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/solver.py` around lines 1344 - 1350, The distance calculation is
using result.solvers[0].mesh instead of the current solver, so replace
references to result.solvers[0].mesh with result.solvers[i].mesh when computing
dist_to_cell (the np.min / np.linalg.norm expression that uses mesh.nodes and
yxz), then keep assigning result.solvers[i].mesh.cell_boundary_mask = included;
ensure you use the same element_size, yxz and i variables already in scope and
that result.solvers[i].mesh exists before computing.

Comment thread saenopy/unused/macro.py
plt.ylabel("stress")
if len(plots_stretch):
plt.subplot(1, subplot_count, 1+(len(plots_stretch)>0))
plt.subplot(1, subplot_count, 1 + (len(plots_stretch) > 0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix subplot index for stretch-only plotting path.

Line 591 uses 1 + (len(plots_stretch) > 0), which selects subplot 2 even when only one subplot exists.

Proposed fix
-            plt.subplot(1, subplot_count, 1 + (len(plots_stretch) > 0))
+            plt.subplot(1, subplot_count, 1 + (len(plots_shear) > 0))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@saenopy/unused/macro.py` at line 591, The plt.subplot call uses 1 +
(len(plots_stretch) > 0) which yields 2 when plots_stretch is non-empty (because
True == 1) and can select a non-existent subplot when subplot_count == 1; update
the index expression in the plt.subplot call (the one using subplot_count and
plots_stretch) to ensure the chosen index is clamped to the available
subplot_count (for example, use min(1 + int(len(plots_stretch) > 0),
subplot_count) or equivalent) so the index never exceeds subplot_count.

# Conflicts:
#	pyproject.toml
#	saenopy/gui/code/syntax.py
#	test/test_use_gui.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/installer.yml:
- Around line 14-17: Replace mutable action tags with immutable commit SHAs for
the referenced steps: pin actions/checkout@v6, astral-sh/setup-uv@v7, and
actions/upload-artifact@v4 to their respective commit SHAs in the
`.github/workflows/installer.yml` workflow; additionally, in each checkout step
that uses actions/checkout (the steps currently using actions/checkout@v6), add
the with: persist-credentials: false setting to the checkout step configuration
to prevent credential propagation. Locate the steps by the exact uses: tokens
(actions/checkout@v6, astral-sh/setup-uv@v7, actions/upload-artifact@v4) and
update their tags to specific commit SHAs and add persist-credentials: false
under the checkout steps' with block.

In @.github/workflows/test.yml:
- Around line 27-28: Replace mutable action tags with their corresponding
immutable commit SHAs: locate the three uses: "actions/checkout@v6",
"pyvista/setup-headless-display-action@v3", and "astral-sh/setup-uv@v7" in the
test job and update each to use the full commit SHA (e.g.,
actions/checkout@<full-sha>) fetched from the action's repository (or the
release commit you want to pin). Ensure you replace only the tag portion after
the @ for each uses: entry and verify the workflow still passes by running the
CI; keep a short comment noting the pinned SHA and why it was chosen.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7da4582e-2652-4339-b78b-a7295571178f

📥 Commits

Reviewing files that changed from the base of the PR and between 67b43aa and c16e9be.

📒 Files selected for processing (6)
  • .github/workflows/installer.yml
  • .github/workflows/test.yml
  • pyproject.toml
  • saenopy/gui/code/syntax.py
  • test/test_download_example.py
  • test/test_use_gui.py
✅ Files skipped from review due to trivial changes (2)
  • saenopy/gui/code/syntax.py
  • test/test_download_example.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/installer.yml:
- Around line 14-17: Replace mutable action tags with immutable commit SHAs for
the referenced steps: pin actions/checkout@v6, astral-sh/setup-uv@v7, and
actions/upload-artifact@v4 to their respective commit SHAs in the
`.github/workflows/installer.yml` workflow; additionally, in each checkout step
that uses actions/checkout (the steps currently using actions/checkout@v6), add
the with: persist-credentials: false setting to the checkout step configuration
to prevent credential propagation. Locate the steps by the exact uses: tokens
(actions/checkout@v6, astral-sh/setup-uv@v7, actions/upload-artifact@v4) and
update their tags to specific commit SHAs and add persist-credentials: false
under the checkout steps' with block.

In @.github/workflows/test.yml:
- Around line 27-28: Replace mutable action tags with their corresponding
immutable commit SHAs: locate the three uses: "actions/checkout@v6",
"pyvista/setup-headless-display-action@v3", and "astral-sh/setup-uv@v7" in the
test job and update each to use the full commit SHA (e.g.,
actions/checkout@<full-sha>) fetched from the action's repository (or the
release commit you want to pin). Ensure you replace only the tag portion after
the @ for each uses: entry and verify the workflow still passes by running the
CI; keep a short comment noting the pinned SHA and why it was chosen.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7da4582e-2652-4339-b78b-a7295571178f

📥 Commits

Reviewing files that changed from the base of the PR and between 67b43aa and c16e9be.

📒 Files selected for processing (6)
  • .github/workflows/installer.yml
  • .github/workflows/test.yml
  • pyproject.toml
  • saenopy/gui/code/syntax.py
  • test/test_download_example.py
  • test/test_use_gui.py
✅ Files skipped from review due to trivial changes (2)
  • saenopy/gui/code/syntax.py
  • test/test_download_example.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
🛑 Comments failed to post (2)
.github/workflows/installer.yml (1)

14-17: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all workflow actions are pinned to full commit SHAs
rg -nP '^\s*-\s*uses:\s*[^@]+@([0-9a-f]{40}|.+)$' .github/workflows/*.yml -C1
echo "---- Non-SHA pinned uses ----"
rg -nP '^\s*-\s*uses:\s*[^@]+@(?!(?:[0-9a-f]{40})$).+' .github/workflows/*.yml

Repository: rgerum/saenopy

Length of output: 2449


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/installer.yml"
echo "=== installer.yml (head 140 lines with numbers) ==="
nl -ba "$FILE" | sed -n '1,160p'

echo
echo "=== Targeted search for setup-uv and persist-credentials in installer.yml ==="
rg -n "setup-uv|persist-credentials|checkout@|upload-artifact@" "$FILE" || true

Repository: rgerum/saenopy

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/installer.yml"
echo "=== installer.yml: lines 1-130 (with numbers) ==="
cat -n "$FILE" | sed -n '1,130p'

echo
echo "=== installer.yml: targeted search ==="
rg -n "setup-uv|persist-credentials|uses:\s*actions/checkout@|uses:\s*astral-sh/setup-uv@|upload-artifact@" "$FILE" || true

Repository: rgerum/saenopy

Length of output: 4877


🏁 Script executed:

rg -n "persist-credentials" .github/workflows/installer.yml || true

Repository: rgerum/saenopy

Length of output: 40


Harden GitHub Actions supply chain and reduce checkout credential exposure in release installer workflow.

In .github/workflows/installer.yml, these uses: entries are pinned only to mutable tags—pin them to immutable commit SHAs:

  • actions/checkout@v6 (lines 14, 53, 90)
  • astral-sh/setup-uv@v7 (lines 17, 56, 93)
  • actions/upload-artifact@v4 (lines 37, 75, 115)

Also, checkout steps at lines 14/53/90 should set with: persist-credentials: false to reduce token/credential exposure to later steps.

Suggested pattern
-    - uses: actions/checkout@v6
+    - uses: actions/checkout@<commit-sha> # v6
+      with:
+        persist-credentials: false

-    - name: Install uv
-      uses: astral-sh/setup-uv@v7
+    - name: Install uv
+      uses: astral-sh/setup-uv@<commit-sha> # v7
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 14-14: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 14-14: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 17-17: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 17-17: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): this step

(cache-poisoning)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/installer.yml around lines 14 - 17, Replace mutable action
tags with immutable commit SHAs for the referenced steps: pin
actions/checkout@v6, astral-sh/setup-uv@v7, and actions/upload-artifact@v4 to
their respective commit SHAs in the `.github/workflows/installer.yml` workflow;
additionally, in each checkout step that uses actions/checkout (the steps
currently using actions/checkout@v6), add the with: persist-credentials: false
setting to the checkout step configuration to prevent credential propagation.
Locate the steps by the exact uses: tokens (actions/checkout@v6,
astral-sh/setup-uv@v7, actions/upload-artifact@v4) and update their tags to
specific commit SHAs and add persist-credentials: false under the checkout
steps' with block.
.github/workflows/test.yml (1)

27-28: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect all workflow action refs and list non-SHA pins
rg -nP '^\s*-\s*uses:\s*[^@]+@(?!(?:[0-9a-f]{40})$).+' .github/workflows/test.yml -C1

Repository: rgerum/saenopy

Length of output: 310


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== .github/workflows/test.yml (numbered) =="
nl -ba .github/workflows/test.yml | sed -n '1,120p'

echo
echo "== All uses: lines with numbers =="
rg -n '^\s*-\s*uses:' .github/workflows/test.yml

Repository: rgerum/saenopy

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== .github/workflows/test.yml (numbered) =="
cat -n .github/workflows/test.yml | sed -n '1,140p'

echo
echo "== All uses: lines with numbers =="
rg -n '^\s*-\s*uses:' .github/workflows/test.yml

Repository: rgerum/saenopy

Length of output: 1777


Pin GitHub Actions in test job to immutable commit SHAs.

In .github/workflows/test.yml, the test job uses mutable action tags:

  • actions/checkout@v6 (line 27)
  • pyvista/setup-headless-display-action@v3 (line 28)
  • astral-sh/setup-uv@v7 (line 33)

Pin each uses: reference to a full commit SHA to prevent tag-retargeting supply-chain risk.

Suggested pattern
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@<commit-sha> # v6

-      - uses: pyvista/setup-headless-display-action@v3
+      - uses: pyvista/setup-headless-display-action@<commit-sha> # v3

-      - name: Install uv
-        uses: astral-sh/setup-uv@v7
+      - name: Install uv
+        uses: astral-sh/setup-uv@<commit-sha> # v7
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 27-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 27-27: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 28-28: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 27 - 28, Replace mutable action tags
with their corresponding immutable commit SHAs: locate the three uses:
"actions/checkout@v6", "pyvista/setup-headless-display-action@v3", and
"astral-sh/setup-uv@v7" in the test job and update each to use the full commit
SHA (e.g., actions/checkout@<full-sha>) fetched from the action's repository (or
the release commit you want to pin). Ensure you replace only the tag portion
after the @ for each uses: entry and verify the workflow still passes by running
the CI; keep a short comment noting the pinned SHA and why it was chosen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant