refactor: one lazy import left, and it is the only real cycle - #117
Conversation
|
Warning Review limit reached
Next review available in: 41 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (18)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The package had eight in-function `linopy_yaml` imports. One broke a genuine cycle; the other seven broke nothing and made that one impossible to spot. `piecewise.py` and `lowering.py` imported each other lazily, and piecewise reached for a *private* name — `lowering._lower_expr` — using it as a predicate and throwing the result away. The dependency is real and its direction is right: a formulation sits above lowering, and it needs to know whether a link expression is in the streaming subset. So lowering now offers `check_core_subset()` for exactly that, piecewise imports it at module level, and only the reverse edge stays lazy — documented, and now the sole entry in `DELIBERATE_LAZY_IMPORTS`. That guard turned out to be load-bearing and untested. Removing it, `p ** 2` and `p * p` in a `piecewise:` link are still refused — but by lowering, on the *expanded* declarations, so the message names `cost_curve_link0`, which the user never wrote. The guard exists to keep the error pointing at the block and link index in the file. Two tests now pin that. The other six were leftovers: `resolution` imported `errors`, `where_parser` and `helpers` in-function while already importing them at the top, and `dimensions` and `validation` deferred imports of modules with no path back. All hoisted. `test_lazy_intra_package_imports_are_all_declared` fails on any undeclared one from here, and also on a stale declaration — so the list cannot rot in either direction. `tidy_sources` moves to a new `sources.py`. It was in `lowering.py`, where it shared no code and no concept with anything: lowering turns an AST into a plan and touches no data, that function touches only data and knows nothing about expressions. They were together because `api.build` calls them on consecutive lines. 311 tests pass, 3 of them new. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1f864a3 to
e15b6f1
Compare
The package had eight in-function
linopy_yamlimports. One broke a genuine cycle. The other seven broke nothing — and made that one impossible to spot.The one real cycle went through a private name
piecewise.pyandlowering.pyimported each other lazily, and piecewise reached forlowering._lower_expr, using it as a predicate and discarding the result:The dependency is real and its direction is right: a formulation sits above lowering, and it needs to know whether a link expression is in the streaming subset. So lowering now offers a public name for exactly that —
— piecewise imports it at module level, and only the reverse edge stays lazy. It is now the sole entry in
DELIBERATE_LAZY_IMPORTS, with the reason written down.That guard was load-bearing, and untested
I checked whether it could just go, by removing the call. With it gone,
p ** 2andp * pin apiecewise:link are still refused — by lowering, on the expanded declarations:cost_curve_link0is a declaration the formulation generated. The user wrotecost_curve, link 0. The guard exists to keep the message pointing at the file. Two tests now pin that, so the next person to find this "redundant" call has the reason in front of them.The other six were leftovers
resolution.pyimportederrors,where_parserandhelpersin-function while already importing them at the top;dimensions.pyandvalidation.pydeferred imports of modules with no path back. All hoisted.A new architecture test keeps it that way, and fails in both directions — an undeclared in-function import, or a declaration for an import that no longer exists:
ARCHITECTURE.mdgains this as hard rule 0, since it is the mechanical form of "the layers are ordered".It earned itself on the rebase. #104 landed while this was open and added a ninth in-function import —
piecewise.py→relational.arrow. The test failed and named it. It turned out to be hoistable (arrow.pyimports pyarrow lazily itself, so module level costs nothing), so that is one more import gone rather than a second declaration.tidy_sources→sources.pyRebased onto #104's Arrow rewrite of that function, so
sources.pycarries the new body, not the pandas one. It was inlowering.py, sharing no code and no concept with anything there: lowering turns an AST into a plan and touches no data; that function touches only data and knows nothing about expressions. They were in one file becauseapi.buildcalls them on consecutive lines, which is not a reason.Verification
Against current
main(eae4d40):uv run pytest— 311 passed, 1 xfailed (3 new) ·ruff checkandruff format --checkclean ·pyrefly check0 errors.🤖 Generated with Claude Code