BUG: accept the callable parachute triggers the docstring promises - #1103
Conversation
The `or` sat inside the isinstance call rather than beside it:
isinstance(member, (str, int, float) or callable(member))
A non-empty type tuple is truthy, so the expression short-circuited to the
tuple and callable(member) was never evaluated. The check reduced to
isinstance(member, (str, int, float)), and a callable is none of those.
Parachute takes a callable trigger and Flight calls it, and the docstring three
lines above says "a list of callables, string 'apogee' or ints/floats". Only
the stochastic wrapper refused one. The two non-callable forms passed
throughout, which is why the tests never caught it.
Left as an assert to match the other fourteen in these two modules, and because
raising a different type would break anyone catching AssertionError.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1103 +/- ##
===========================================
+ Coverage 82.18% 82.57% +0.39%
===========================================
Files 122 128 +6
Lines 16355 16564 +209
===========================================
+ Hits 13441 13678 +237
+ Misses 2914 2886 -28 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Moving the `or` let callables through, but left four things the check should never have accepted. `["banana"]` passed here and then `Parachute` raised ValueError, so the wrapper only moved the failure to create time. `[True]` is worse: bool is an int, so it went through as a height of one metre. `[]` passed because `all([])` is True. And `numbers.Real` replaces `(int, float)`, which took numpy.float64 because it subclasses float and refused numpy.int64 because it subclasses neither. The check is raised rather than asserted. `python -O` strips an assert outright, and this is the only thing between those triggers and a Parachute that either refuses them later or misreads them. Still an AssertionError, so nothing that catches it has to change. The docstring claimed a tuple form that was never implemented; it now describes what the code does. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
|
All four taken, in Strings. Booleans. This is the one I would call a real bug rather than a loose check. Empty list. Numeric types. While checking the above I found
The docstring. It promised "a tuple that will be further validated in the StochasticModel class". No tuple has ever reached that path; the check has always required a list. Rewritten to say what the code does, and Tests21 now. Five mutations, one per rule, each taking only its own case:
There is also the end-to-end case that was missing: the callable now has to survive Local: ruff clean, |
Widening the height check to numbers.Real was the same mistake as accepting "banana": Parachute checks isinstance(trigger, (int, float)), so numpy.float64 passes because it subclasses float and numpy.int64 raises ValueError because it subclasses neither. Letting them through here only moved the failure to create_object. Back to (int, float), matching that check rather than improving on it, and the test that asserted numpy.int64 was accepted now asserts both ends refuse it. The asymmetry is Parachute's rather than this wrapper's and is worth fixing there, where widening the check would not strand anything downstream. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
|
Good catch, and it is the same mistake I spent the last round criticising. Fixed in I widened the height check to So The test made it worse. Back to
Putting it back to The asymmetry itself is real and is Local: ruff clean, |
The trigger tests built a 14-element state. Flight passes 13: x y z vx vy vz e0 e1 e2 e3 wx wy wz. Only y[5] is read, so both worked, but the test is there to document the contract and was documenting it wrongly. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
|
This was true, and it is what Twelve of fourteen agree. The two that do not are What is left is the asymmetry inside One smaller thing from the same round, in |
By hand, because the automation cannot run on a pull request from a fork (RocketPy-Team#1101). No documented API breaks, but the observable behaviour does: an invalid string, an empty list or a boolean trigger now fails during StochasticParachute validation rather than later in Parachute construction, or silently becoming a one-metre height trigger. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
|
Added the changelog entry here rather than waiting on the automation, which cannot run on a fork pull request until #1101 is fixed. Head is Your wording on the scope is better than mine, so the entry follows it: no documented API break, but an invalid string, an empty list or a boolean now fails during |
Gui's point on the review. `python -O` strips an assert, and this is what keeps a non-sampler out of the model, so it has to be a raise. Same shape as RocketPy-Team#1103, which took the identical route for the parachute triggers. AssertionError is kept rather than swapped for TypeError, because the docstring on develop already documents it and a caller catching it should keep working. Two tests. One is the behaviour; the other runs a child interpreter under -O, since that is the mechanism and the plain test passes either way. Note this module carries thirteen more asserts on develop, none of them mine. Happy to send them separately if you want the same treatment there. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
BUG: give each CustomSampler its own stream instead of the model's seed Every CustomSampler on a model was reset with the model's own seed, so two samplers backed by default_rng started from the same state and drew the same underlying deviate. Each input now gets a stream keyed by its name, and samplers that share one generator declare a seed_group so the group is seeded once between them. Merged manually rather than through the button: the CHANGELOG conflicted with #1103, and the resolution keeps both entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses #1095. Not
Closes, because the keyword only fires when a pull request targets the default branch and this targetsdevelop.Pull request type
Checklist
ruff check/ruff format --check/pylint rocketpy/ tests/ docs/) has passed locallypylintexits 0,pytest tests/unit tests/integrationis 2058 passed, 44 skipped.Current behavior
StochasticParachute._validate_triggerrefused the callable triggers its own docstring promised:The
orsat inside theisinstancecall.(str, int, float)is a non-empty tuple and therefore truthy, so the expression short-circuited to the tuple andcallable(member)was never evaluated. The check reduced toisinstance(member, (str, int, float)), and a callable is none of those.Fixing that turned up four more things the check should never have accepted:
New behavior
The check now accepts exactly what
Parachuteaccepts, and nothing else:(int, float)rather thannumbers.Real, deliberately.Parachuteuses that exact check, so widening here would letnumpy.int64past validation and intocreate_object, which is the same defect as accepting"banana": not permissiveness, but moving the failure somewhere less obvious.boolis excluded becauseParachutewould otherwise readTrueas one metre without complaint.The whole contract, checked against both sides in one test:
The last row is the one deliberate gap, in the safe direction.
The check is also raised from an
ifrather than asserted. It stays anAssertionError, so nothing catching it has to change, butpython -Ostrips anassertoutright and this is the only thing between those triggers and aParachutethat either refuses them later or misreads them.The docstring promised a tuple form that was never implemented, and now describes what the code does.
Tests
Twenty-five. Five mutations, one per rule, each taking only its own case:
["banana"][True][](int, float)widened tonumbers.Realnumpy.int64raiseback toassert-OtestThere is also the end-to-end case that was missing: a callable has to survive
create_objectand be what gets called, rather than merely be accepted by the constructor.Breaking change
It accepts callables, which the docstring already promised, and refuses four forms that either failed later or were silently misread.
Additional information
Parachute's own numeric check is asymmetric:numpy.float64passes because it subclassesfloat,numpy.int64does not because it subclasses neither, andTrueis taken as a height. This follows that contract rather than trying to improve on it, and the asymmetry is filed separately as #1106, where widening would not strand anything downstream.Found while reviewing #1054, and unrelated to it.