Replace bare except clauses with specific exceptions - #126
Conversation
Part of PharmaPy-org#11 (finding 2). Narrows the three bare excepts in the package: - ThermoModule.ParseDatabase: catch ValueError/TypeError around the float-array conversion, preserving the keep-as-list fallback for non-numeric property fields. - StatsModule.bootstrap_params: catch Exception so a failed optimization still records a NaN row, but KeyboardInterrupt/SystemExit propagate and users can abort long bootstrap runs. - Crystallizers _BaseCryst: catch ImportError and warn when jac_type='AD' is requested but jax is not importable, instead of silently falling back to numpy.
bernalde
left a comment
There was a problem hiding this comment.
Thank you for your contribution to our package!
I left a small comment in the testing function that should be addressed
|
|
||
|
|
||
| def test_batch_cryst_warns_when_ad_requested_without_jax(monkeypatch): | ||
| BatchCryst = _import_batch_cryst(monkeypatch) |
There was a problem hiding this comment.
We would prefer to avoid monkeypatch tests
There was a problem hiding this comment.
Addressed in 6c54354. The local fake Assimulo module tree was removed in favor of tests/assimulo_helpers.py. The only remaining monkeypatch is the optional Explicit_Problem boundary needed by the core lane; its rationale is documented, and the real set_ode_problem routing is exercised.
bernalde
left a comment
There was a problem hiding this comment.
Reviewed current head b6ab056 against the current master tip as a clean merge result. Narrowing ParseDatabase to conversion errors is appropriate, and the new KeyboardInterrupt/JAX-warning regressions are genuinely red on the PR's merge base. Two blocking source issues remain.
The existing test-design thread also remains relevant: after updating onto current master, avoid duplicating the optional-Assimulo import stub. tests/assimulo_helpers.py is now the shared boundary helper; reuse it if the unit test still needs import isolation, or move this check to the Assimulo-enabled lane.
Blocking issues: 2.
Nonblocking issues: 1.
Questions: none.
Validation:
- Static safety pass over this external contribution: no workflow, dependency-install, subprocess, network, secret, or execution-hook changes.
- Local and GitHub changed-file sets match: four files.
- Clean merge into current
mastercompleted without conflicts. - Solver-free Python 3.11: targeted tests
5 passed; core lane66 passed, 5 skipped, 6 deselected. The five skips are tests that explicitly require optional Assimulo. - Solver-enabled Python 3.11: core lane
66 passed, 15 deselected. - Base regression probe: the KeyboardInterrupt and missing-JAX warning tests both fail against the merge base as intended.
- CRLF-aware diff check passed; the apparent default whitespace warnings are existing CRLF endings.
- Current PR-head GitHub checks: Core tests and Assimulo integration tests both passed.
Merge readiness: not ready. I would not merge this until the blocking issues above are addressed.
| _BaseCryst.np = np | ||
| except: | ||
| pass | ||
| except ImportError: |
There was a problem hiding this comment.
Blocking: this handler says it is falling back to NumPy, but it leaves jac_type='AD'. set_ode_problem(eval_sens=True, ...) then selects self.jac_states_ad and self.jac_params_ad, neither of which exists, so the supposedly recovered object fails with AttributeError. Either reject unavailable AD immediately with a clear exception, or switch to a real supported mode such as finite_diff; then extend the regression through set_ode_problem so it proves the fallback is operational rather than only that construction succeeds.
There was a problem hiding this comment.
Addressed in 6c54354. Because the AD callbacks are not implemented regardless of JAX availability, construction now normalizes AD to finite_diff, and the unreachable missing-callback branch was removed. The regression calls set_ode_problem and asserts the numerical Jacobian and sensitivity handoff.
There was a problem hiding this comment.
Strengthened in 9ab73e9. The regression now explicitly asserts jac_params_fn == jac_params_numerical in addition to the state-Jacobian and rhs_sens handoffs. I replaced the production assignment with None in a disposable copy; the test then failed at this exact assertion, which confirms it now guards the complete finite-difference sensitivity setup.
| verbose=False, store_iter=False, | ||
| optim_options=self.inst.optim_options) | ||
| except: | ||
| except Exception: |
There was a problem hiding this comment.
Blocking: Exception is still broad enough to swallow ordinary programming failures such as AttributeError and replace them with a NaN row plus a generic print, which preserves the debuggability problem from #11. Catch only the optimizer's documented failure exceptions (or introduce a dedicated optimization-failure exception), retain the cause in a warning/log message, and add a regression showing that an unrelated programming error propagates.
There was a problem hiding this comment.
Addressed in 6c54354. bootstrap_params now catches only numpy.linalg.LinAlgError, emits a warning containing the sample index and original diagnostic, and records that row as NaN. A separate regression proves AttributeError propagates; the interrupt regression remains.
Mazhar331
left a comment
There was a problem hiding this comment.
Second maintainer pass on b6ab056. I deliberately skipped everything already covered by @bernalde's reviews — the jac_type='AD' fallback leaving an unusable object, the breadth of except Exception in bootstrap_params, and the duplicated optional-Assimulo import stub all still stand and I have nothing to add to them. Everything below is new.
Good starting point: the two behavioral regressions really are red on the merge base, and an AST scan confirms zero bare except: handlers remain anywhere in the repository, not just in PharmaPy/.
Blocking: the ThermoModule narrowing has no test at all
Anchored inline at PharmaPy/ThermoModule.py:82. Both new ParseDatabase tests pass unchanged against the merge base (c7a38a4), because a bare except: also catches ValueError. I put the bug back — reverted line 82 to except: at this head and re-ran the file — and all 5 tests still passed, which means nothing in this PR guards the third of its three changes. That is a missing test for changed behavior, the same gap @bernalde asked you to close for StatsModule.
You do not need a monkeypatch for this one. A JSON field whose value overflows float64 is a real input that exercises the difference:
db = {"water": {"mw": 10 ** 400}, "ethanol": {"mw": 46}}- merge base: returns
dd_arrays["mw"]as a plainlist, silently - this head: raises
OverflowError: int too large to convert to float
Assert that with pytest.raises(OverflowError) and the narrowing is genuinely pinned. While you are there, TypeError is in the tuple but no test reaches it; the only input I found that produces it is a nested dict value, which ParseDatabase already unwraps one level up — either cover it or drop it from the tuple.
Nonblocking: the new warning points at PharmaPy internals, not the caller
Anchored inline at PharmaPy/Crystallizers.py:126. warnings.warn defaults to stacklevel=1, so the emitted warning reads Crystallizers.py:126: RuntimeWarning: ... — it names the library line rather than the user's BatchCryst(...) call, which is the location the user can act on. Add stacklevel=2.
Nonblocking: the new test asserts on unrestored process-global state
Anchored inline at tests/test_exception_handling.py:124. _BaseCryst.np is a class attribute that jac_type='AD' rebinds for the whole process and nothing ever restores. With a jax module importable, I observed:
before: _BaseCryst.np is numpy -> True
after one BatchCryst(jac_type='AD'): _BaseCryst.np is numpy -> False
a later BatchCryst() with jac_type=None: type(b).np is numpy -> False
then re-running this test's scenario: type(c).np is numpy -> False
So the assertion holds today only because jax is absent from CI; in a jax-enabled environment it fails depending on what ran earlier in the session, for reasons unrelated to the handler under test. Pin it with monkeypatch.setattr(_BaseCryst, "np", np) before constructing.
The last line of that trace is also worth noting for the fallback question @bernalde raised: the message says "Falling back to numpy", but the handler performs no fallback — it just declines to rebind. Whatever a previous AD instance left on the class is what the object keeps using.
Nonblocking: test file does not follow the repo's documentation convention
Recent test modules here (tests/test_drying_latent_heat_factor.py, for example) carry a module docstring plus a one-line docstring per test stating the regression contract. tests/test_exception_handling.py has neither, and its name groups three unrelated modules by mechanism rather than by the module under test. A module docstring and one line per test naming which bare-except each case guards would bring it in line.
Question: is this better ordered behind #135?
Open PR #135 ("Make optional Assimulo imports lazy") states it keeps the solver-backed model modules importable without Assimulo and removes the temporary fake sys.modules Assimulo package tree. If that lands first, _stub_assimulo_modules/_import_batch_cryst here can be deleted outright rather than reworked onto tests/assimulo_helpers.py. That may be a cheaper resolution to the stub thread than either option currently on the table — worth a decision before you spend effort refactoring the helper.
Blocking issues: 1 (plus the 2 already open from @bernalde).
Nonblocking issues: 3.
Questions: 1.
Validation (Python 3.11, conda env with Assimulo installed and jax absent):
- Changed-file sets from
git diff --name-only $(git merge-base origin/master HEAD) HEADandgh pr diff --name-onlymatch: four files. - PR head: full suite
59 passed;tests/test_exception_handling.py5 passed. - Merge result: merged current
master(8c7d14d) into the head with no conflicts; full suite90 passed. - Base regression probe at
c7a38a4:2 failed, 3 passed— the KeyboardInterrupt and JAX-warning tests are red as intended, and bothParseDatabasetests are green, which is the blocking finding above. - Mutation probe: reverting
ThermoModule.py:82to a bareexcept:at this head leaves all 5 tests passing. ParseDatabaserun over all 13 JSON databases shipped in the repo: identical outcomes at base and head. The four files that raise (minimum_modeling_objects.json,pfr_test_constructor_kwargs.json,compounds_lomustine.json,physical_property_descriptors.json) fail identically on both sides, from code outside thetryblock — pre-existing, not this PR.- AST scan for bare
excepthandlers across every.pyin the repo: none remain. (flake8is not installed in this environment, so this stands in for the--select=E722claim.) - Static safety pass: no workflow, packaging, subprocess, network, or secret-touching changes.
- GitHub checks on this head: Core tests SUCCESS, Assimulo integration tests SUCCESS.
Merge readiness: not ready. I would not merge this until the blocking issues above are addressed.
| props = np.array(props, dtype=float) | ||
| except: | ||
| pass | ||
| except (ValueError, TypeError): |
There was a problem hiding this comment.
Blocking: nothing in this PR guards this change. Both new ParseDatabase tests pass unchanged against the merge base c7a38a4, because a bare except: also catches ValueError. I put the bug back — reverted this line to except: at this head and re-ran tests/test_exception_handling.py — and all 5 tests still passed. That is the same missing-regression gap already flagged for StatsModule.
A monkeypatch is not needed here. A JSON value that overflows float64 is a real input that separates the two behaviors:
db = {"water": {"mw": 10 ** 400}, "ethanol": {"mw": 46}}- merge base: silently returns
dd_arrays["mw"]as a plainlist - this head: raises
OverflowError: int too large to convert to float
Assert that with pytest.raises(OverflowError) and the narrowing is pinned.
Separately, TypeError is in the tuple but no test reaches it. The only input I found that produces it is a nested dict value, which ParseDatabase already unwraps a few lines above — either cover it or drop it from the tuple.
There was a problem hiding this comment.
Addressed in 6c54354. The handler now catches only ValueError; TypeError was dropped. Real JSON fixtures cover both float64 overflow and a malformed nested value, and restoring a bare except: makes both regressions fail as intended.
| except: | ||
| pass | ||
| except ImportError: | ||
| warnings.warn( |
There was a problem hiding this comment.
Nonblocking: warnings.warn defaults to stacklevel=1, so this emits Crystallizers.py:126: RuntimeWarning: ... — it names the library line rather than the user's BatchCryst(...) call, which is the location they can actually act on. Add stacklevel=2.
There was a problem hiding this comment.
Addressed in 6c54354. This uses stacklevel=3 because the warning is emitted in _BaseCryst.__init__, one frame below the public subclass constructor; 3 is what points to the user's BatchCryst(...) call. The regression asserts the warning filename is the caller test file.
| crystallizer = BatchCryst(target_comp="solute", jac_type="AD") | ||
|
|
||
| assert crystallizer.jac_type == "AD" | ||
| assert type(crystallizer).np is np |
There was a problem hiding this comment.
Nonblocking: this asserts on process-global state that nothing restores. _BaseCryst.np is a class attribute rebound by jac_type='AD' for the lifetime of the process. With a jax module importable I observed:
before: _BaseCryst.np is numpy -> True
after one BatchCryst(jac_type='AD'): _BaseCryst.np is numpy -> False
a later BatchCryst() with jac_type=None: type(b).np is numpy -> False
then re-running this test's scenario: type(c).np is numpy -> False
So this assertion holds today only because jax is absent from CI; in a jax-enabled environment it fails depending on what ran earlier in the session, for reasons unrelated to the handler under test. Pin it with monkeypatch.setattr(_BaseCryst, "np", np) before constructing.
The last line is also relevant to the fallback discussion above: the warning says "Falling back to numpy", but the handler performs no fallback — it only declines to rebind, so whatever a previous AD instance left on the class is what the object keeps using.
There was a problem hiding this comment.
Addressed in 6c54354 by removing the JAX class-attribute rebinding entirely. _BaseCryst.np now remains NumPy regardless of prior instances, and AD requests take the explicit finite-difference path before set_ode_problem is built.
| pytestmark = pytest.mark.unit | ||
|
|
||
|
|
||
| def _stub_assimulo_modules(monkeypatch): |
There was a problem hiding this comment.
Please provide numpy documentation for all function modifications/additions, including tests
There was a problem hiding this comment.
Addressed in 6c54354. The test module, helper classes, helper methods, nested optimizer doubles, and tests now have NumPy-style docstrings. The modified production functions/methods also document parameters, returns, warnings/errors, shapes, physical units, and bases.
| @@ -0,0 +1,124 @@ | |||
| import json | |||
There was a problem hiding this comment.
Provide a brief description of the test file. Kindly avoid monkeypatches unless necessary, and provide appropriate reasons (existence of related issues, long execution time, etc.) in the PR body/comments when using
There was a problem hiding this comment.
Addressed in 6c54354. The module docstring summarizes all three regression areas and explains the optional-boundary test design. The duplicate monkeypatch helper is removed; the shared helper and narrow Explicit_Problem substitution are retained only to keep this handoff regression in the Assimulo-free core lane, with solver execution left to the Assimulo integration lane.
|
The original review batch was addressed in @AshleyAHuang — I pushed the one-line follow-up directly to your branch because maintainer edits are enabled. I can revert it if you would prefer to take the change yourself. Commit pushed:
The AD fallback regression now checks every callback configured by the real
This closes the remaining coverage gap in the original operational-fallback thread. In a disposable copy I replaced the production parameter-Jacobian assignment with Verification at
No review request was declined. The original handler narrowing, shared Assimulo helper, documentation, and warning-location fixes remain intact. GitHub reports |
Mazhar331
left a comment
There was a problem hiding this comment.
Went through 6c54354. I checked the fixes against the code rather than the summary — pulled the head tree into a scratch copy, ran the suite there, and put each original defect back to confirm the new tests actually catch it. The earlier round is fully resolved; one new thing came out of the stacklevel change that I would like tightened before this goes in.
Previous items
ThermoModule narrowing had no test. Closed. The handler is except ValueError only, TypeError is dropped, and the propagation is pinned instead of the tuple. Restoring the bare except: reddens test_parse_database_propagates_float_overflow and ..._malformed_nested_values, so the change is now genuinely guarded.
AD fallback left an unusable object. Closed, and the chosen fix is better than what I suggested — normalizing to finite_diff at construction and deleting the unreachable elif self.jac_type == 'AD' branch removes the failure mode instead of documenting around it. Deleting the jac_type = 'finite_diff' line reddens the regression, which now runs through set_ode_problem and asserts problem.jac / problem.rhs_sens.
except Exception was too broad. Closed. Widening it back to Exception reddens the new AttributeError test, and restoring the original bare except: reddens three tests including the KeyboardInterrupt one — so strengthening the assertions did not quietly cost the original guarantee, which was my main worry with a rewrite this size. The rationale also checks out against the code: LevMarq.py imports solve and inv from numpy.linalg and uses them at lines 101 and 156, so LinAlgError is the right thing to treat as recoverable.
_BaseCryst.np global. Closed, again better than my suggestion — removing the JAX rebinding entirely means there is no global left to restore.
Docstrings and test description. Closed. The module docstring covers all three regression areas, the helpers and test doubles are documented, and the production docstrings carry units and bases. I spot-checked the ones that are easy to get wrong: vol_ht really is threaded through the constructors without ever being stored, and the energy balances recompute it locally at 1570, 1894 and 2216 — so "retained for constructor compatibility, subclasses derive it internally" is accurate rather than a guess.
Monkeypatch reduction and #135. Closed. The local fake Assimulo tree is gone, tests/assimulo_helpers.py is confirmed present on master, and the remaining Explicit_Problem substitution is the thing that lets the test assert the real set_ode_problem routing, so it is earning its keep. Agreed that #135 is no longer a dependency.
New: stacklevel=3 is right for two of the three public crystallizers
stacklevel=3 is correct reasoning for BatchCryst and MSMPR, which call _BaseCryst.__init__ directly. But SemibatchCryst subclasses MSMPR, so its chain is one frame deeper and the warning lands on our own code instead of the caller:
BatchCryst -> caller.py:12
MSMPR -> caller.py:12
SemibatchCryst -> Crystallizers.py:2097
Crystallizers.py:2097 is the super().__init__ call inside SemibatchCryst.__init__, which is exactly the kind of location the original comment was about. The new filename assertion only exercises BatchCryst, so nothing catches this.
A fixed stacklevel cannot be correct for a hierarchy of varying depth, so I would rather not just bump the number. The cheapest honest fix is to parameterize the existing warning test over BatchCryst, MSMPR and SemibatchCryst — that makes the gap visible — and then either emit the warning from the public constructors, or resolve the caller frame instead of hard-coding a count. Whatever you pick, the test should cover all three.
Two smaller notes
_BaseCryst.np = np is now vestigial. With the rebinding gone, nothing in Crystallizers.py reads self.np or .np; the only remaining consumer is the test's own assert type(crystallizer).np is np, which pins a value no code path uses. Not worth expanding this PR for — it is a natural pickup for the dead-code task in #11 — but the assertion is currently guarding nothing.
The LinAlgError rationale is scoped to the LM path. bootstrap_params forwards self.inst.opt_method, and optimize_fn also accepts 'IPOPT'. A cyipopt-side numerical failure is not a LinAlgError, so under that method one bad sample now aborts the whole bootstrap run rather than recording a NaN row. That may be the intended strictness, but the Notes currently reason only about Levenberg–Marquardt, so a sentence covering the other method would keep the docstring honest.
What I ran
Scratch copy of the head tree, verified byte-identical to git show 6c543549:<path>. Focused file 8 passed; full suite with Assimulo 3.4.3 installed 95 passed, which matches the 80 + 15 split in your summary. Six mutations, each reverting one fix, all red on the intended test only and green again after restoring: bare except: in ThermoModule (2 failed), except Exception in StatsModule (1), bare except: in StatsModule (3), dropping the AD → finite_diff normalization (1), and stacklevel 3→2 and 3→1 (1 each — so the 3 is pinned, not assumed). Line endings are clean: the three CRLF-tracked production files stayed pure CRLF and the new LF-only test file matches every other file under tests/. Core tests, Assimulo integration, and the locked pixi install on Ubuntu and Windows are all green at this head.
Where this leaves it
Everything from the previous round holds up, and the mutation evidence is genuinely reassuring — a failing test there is the outcome I was looking for. I am holding off on merging only for the SemibatchCryst stacklevel gap: the fix is incomplete for one of the three public entry points, and the test that was added to pin it does not cover that path, so it would go in looking verified when it is not. Once the warning test covers all three classes and points at the caller in each, this is good to go from my side. The two smaller notes are yours to take or leave.
`stacklevel=3` was correct only for `BatchCryst` and `MSMPR`, which call `_BaseCryst.__init__` directly. `SemibatchCryst` subclasses `MSMPR`, so its chain is one frame deeper and the warning was attributed to `Crystallizers.py:2097` -- the `super().__init__` call inside `SemibatchCryst.__init__` -- rather than to the caller. No fixed stacklevel can be correct across a hierarchy of varying depth, so resolve it instead: `_caller_stacklevel` walks outward to the first frame not executing in this module. Frames are matched on module globals rather than `__file__` so relative and absolute import paths behave the same. Parameterize the warning-location test over all three public crystallizers and assert the exact construction line, not only the file, since a stacklevel short by one frame still lands inside `Crystallizers.py`. Pinning a fixed value now reddens the suite for every choice: 3 fails `SemibatchCryst` only, 4 fails `BatchCryst` and `MSMPR`, and 2 fails all four cases. Also drop the `type(crystallizer).np is np` assertion, which pinned a value no code path reads once the JAX rebinding was removed, and record in `StatsModule.bootstrap_params` that the `LinAlgError` recovery is scoped to `opt_method='LM'`; `optimize_fn` also accepts `'IPOPT'`, whose failures surface from cyipopt and abort the run instead of recording a NaN row.
|
Addressed the review at @AshleyAHuang — heads up that I pushed this commit directly to your branch rather than leaving it for you, since maintainer edits are enabled on the PR. Happy to revert it if you would rather take the fix yourself; the reasoning is all below either way. Thanks for the work on this one — the mutation evidence in the last round was what made it easy to review. Commit pushed:
|
stacklevel |
Result |
|---|---|
| 3 (the value under review) | 1 failed — SemibatchCryst only |
| 4 | 3 failed — BatchCryst and MSMPR |
| 2 | 4 failed — all cases |
_caller_stacklevel() |
11 passed |
The stacklevel=3 row is the gap you found, now caught by the test rather than by review.
The two smaller notes — both taken
_BaseCryst.np assertion guards nothing.
I removed assert type(crystallizer).np is np from the test. You were right that it pins a value no code path reads once the JAX rebinding is gone, and a vacuous assertion is worse than none. I left _BaseCryst.np = np in production as you suggested — removing it is dead-code work that belongs with #11 rather than here, and it is now genuinely unreferenced rather than half-pinned by a test.
LinAlgError rationale is scoped to LM.
Verified against the code rather than taking it on trust: optimize_fn declares method : {'LM', 'IPOPT'}, the 'IPOPT' branch dispatches to minimize_ipopt through cyipopt, and bootstrap_params forwards self.inst.opt_method unchanged. So a cyipopt-side numerical failure is indeed not a LinAlgError and aborts the run. The Notes section now says the recovery is scoped to opt_method='LM' and states what happens under IPOPT. No behavior change — this documents the existing strictness rather than widening the handler, which would need its own regression and a decision about whether aborting is actually wrong.
Verification
All at 0c8bb31, with the Assimulo 3.4.3 environment for the solver lane:
| Command | Result |
|---|---|
pytest tests/test_exception_handling.py -q (both environments) |
11 passed (was 8) |
pytest tests/ -q with Assimulo |
98 passed (was 95) |
pytest tests/ -m "not assimulo" -q |
83 passed, 5 skipped, 6 deselected |
pytest tests/ -m assimulo -q |
15 passed, 83 deselected |
| new tests in the Assimulo-free lane | 3 passed, 8 deselected |
The three new cases run in the core lane too, so the gap is covered where CI actually exercises it, not only in a solver environment.
Line endings held: Crystallizers.py and StatsModule.py are CRLF-tracked, so I applied those edits at byte level and confirmed both files stay pure CRLF with no bare-LF lines; the LF-only test file is unchanged in that respect. git diff --check with core.whitespace=cr-at-eol is clean. ruff reports the same 12 pre-existing findings as the merge base — all legacy unused imports, unused locals, and type() comparisons in Crystallizers.py, none on changed lines — and black --check passes on the test file. Current-head CI: core tests, Assimulo integration, and locked pixi install on Ubuntu and Windows all pass.
I updated the four test counts in the PR body, which the added cases made stale.
Nothing is resolved from my side, and the decision stays CHANGES_REQUESTED until @Mazhar331 re-reviews.
Part of #11 (finding 2), following the issue's suggestion to split the hygiene work into separate small PRs — this one only removes the package's three bare
except:clauses and makes each replacement operational.ThermoModule.ParseDatabase— catches onlyValueErroraround float-array conversion, preserving the established list fallback for non-numeric properties (for example CAS identifiers) while allowing malformed structured values (TypeError) and float-range failures (OverflowError) to retain their diagnostics.StatsModule.bootstrap_params— catches onlynumpy.linalg.LinAlgError, the documented failure from the package's Levenberg-Marquardtsolve/invoperations. A singular bootstrap sample records a NaN row and emits aRuntimeWarningcontaining the sample index and original diagnostic; programming errors,KeyboardInterrupt, andSystemExitpropagate.Crystallizers._BaseCryst— the requestedjac_type='AD'mode cannot be used because its Jacobian callbacks are not implemented. Construction now warns at the user's call site and normalizes that request to the supportedfinite_diffmode; the dead JAX import/rebinding path and unreachable missing-callback branch are removed. The regression continues throughset_ode_problemand asserts the numerical Jacobian and sensitivity handoff.The focused regression module now has NumPy-style documentation and units/bases for its scientific fixtures. It reuses
tests/assimulo_helpers.py; its remaining monkeypatch is limited to the optional Assimulo import/problem boundary so the realBatchCryst.set_ode_problemrouting remains exercised in the core lane.Mutation checks independently restore each defect class: a bare
except:swallows both overflow and malformed-value errors, broadexcept Exceptionswallows the programming error, and leavingjac_type='AD'makes the fallback regression fail.flake8 --select=E722 PharmaPy tests/test_exception_handling.pyreports zero E722 violations.Local verification on Python 3.11:
pytest tests/ -m "not assimulo"— 83 passed, 5 skipped, 6 deselected.pytest tests/ -m assimulo— 15 passed, 83 deselected.pytest tests/test_exception_handling.py -q— 11 passed.pytest --collect-only -q— 89 tests collected.StatsModule.pyandThermoModule.py. The remaining 12 Ruff findings inCrystallizers.pyare unchanged findings already present onmasterand remain outside this exception-handling PR.