Skip to content

Holistic review round 2: fix all findings, release 1.7.1#18

Merged
wlaur merged 13 commits into
mainfrom
fix/holistic-review-1.7.1
Jul 11, 2026
Merged

Holistic review round 2: fix all findings, release 1.7.1#18
wlaur merged 13 commits into
mainfrom
fix/holistic-review-1.7.1

Conversation

@wlaur

@wlaur wlaur commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Patch release fixing everything surfaced by the second holistic pre-release review (10 findings: 1 major, 3 minor, 6 nits — all reproduced at runtime before fixing). One commit per finding.

Fixes

Major

  • Raise the typeguard floor to 4.1.3 — typeguard 4.1.0–4.1.2 cannot be imported on Python 3.14 (they use ast.Str, removed in 3.14), so the declared floor >=4.1 was uninstallable on a supported Python. Bisected empirically: 4.1.3 is the first working release.
  • Run the dependency-floors job on 3.13 and 3.14 — the floors job ran on 3.13 only, which is exactly why the typeguard floor slipped through: a pure-Python wheel installs on any Python, so --only-binary cannot catch a floor that resolves everywhere but only works on some interpreters. The 3.14 leg was pre-verified locally: resolves typeguard 4.1.3 / pydantic 2.12.0 / pydantic-settings 2.0.0, and the installed suite passes (770 passed, 10 skipped).

Minor

  • Derive the non-strict ordering operators from the strict ones — on polars containers, nan >= nan returned True (polars' total order calls NaN equal to NaN) while == said False, breaking the documented ge ⟺ gt ∨ eq consistency. >=/<= are now derived as strict ∨ eq from the same tolerant __eq__. Re-fuzzed ~172k value pairs per operator across all four magnitude containers; (nan, nan) added to the cross-container test matrix.
  • Reject Series-vs-Expr magnitude comparisons like the other mixed containers — this pair bypassed the mixed-container guard and fell through to exact (non-tolerant) comparison. Now raises with an Expr-appropriate hint (evaluate the Expr side, or lift the other side with pl.lit).
  • Reject a NaN density when convert_volume_mass gets a Polars input — a NaN rho sentinel would flow into the Series/Expr result as NaN, violating the library-wide "missing = null, never NaN" convention for polars results. Now rejected with a message pointing at the null spelling.

Nits

  • Point the __hash__ tolerance note at the (rtol, atol) predicate — the comment referenced np.isclose, which is not the predicate in use.
  • Attribute the arithmetic inference to the operator overloads in the README — the overview bullet claimed Quantity.__new__ overloads cover * / ** inference; they are separate arithmetic-operator overloads.
  • State the actual versioning policy instead of claiming semver (README + usage docs).
  • List water in the coolprop README file overview.
  • Refresh the polars-expr-coolprop notebook from a clean top-to-bottom run — sequential execution counts, stale stdout removed, profiler display output (which embedded machine paths) dropped.
  • Document the public module constants on the API reference — attribute docstrings for all nine encomp.coolprop constants plus EAGER_PLUGIN_MIN_SIZE, CONSTANTS, SETTINGS, CUSTOM_DIMENSIONS. This surfaced a latent docs bug: a comment line starting with # type: in units.py was parsed by sphinx's ModuleAnalyzer as a PEP 484 type comment, silently dropping the attribute docs for the whole module. UNIT_REGISTRY is deliberately left undocumented — autodoc's signature formatter crashes on its UnitRegistry[Any] annotation under -W.

Release chores

  • Bump to 1.7.1 (pyproject + uv.lock).
  • Serve the README logo from main instead of the release tag, so the version lives only in pyproject.toml.

Gates (after the final commit)

  • pyright: 0 errors · pyrefly: 0 errors (46 suppressed) · ruff check + format: clean
  • pytest: 957 passed (was 948; new tests cover the three minor fixes)
  • sphinx -T -W --keep-going (RTD parity): build succeeded, constants render on the API page

🤖 Generated with Claude Code

wlaur and others added 13 commits July 10, 2026 15:42
typeguard 4.1.0-4.1.2 do `from ast import Str` at import time; ast.Str was
removed in Python 3.14 (still present in 3.13), so on 3.14 those releases --
and therefore `import encomp` -- fail with an opaque ImportError. All three
ship py3-none-any wheels whose Requires-Python allows 3.14 and none is
yanked, so a lowest-bounds resolution selects 4.1.0 on 3.14. Bisected
empirically: 4.1.3 is the first release that imports on 3.14, and encomp
works end-to-end with it (isinstance_types, the registered Quantity
typeguard checker, self_check).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fold previously combined pint's RAW >= / <= with the tolerant equality
(`raw_ge | equal`). polars' total order calls NaN equal to NaN, so its raw
`nan >= nan` is True while encomp's __eq__ says False -- on Series/Expr
magnitudes `nan >= nan` and `nan <= nan` answered True with ==, > and <
all False, breaking the documented `ge == (gt or eq)` decomposition within
one container (the float path asserts the opposite result explicitly).

Evaluate only the strict pint operator and derive the non-strict result as
`(strict & ~equal) | equal`. For every non-NaN input this is equivalent to
the previous fold (ties, +-inf and null propagation included, verified by
fuzzing ~172k pairs per operator across all four containers); for
nan-vs-nan it now answers False everywhere. The deliberate NaN-vs-number
ordering divergence between the numpy and polars worlds is unchanged.

nan-vs-nan joins _COMPARISON_PAIRS, which asserts container-independence
for every operator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The job resolved and ran the suite on 3.13 only. A pure-Python wheel
installs on any Python, so --only-binary cannot stop a floor that resolves
on both supported Pythons but is broken on one: typeguard 4.1.0 (the
previous floor) imports ast.Str, which 3.14 removed -- the job passed on
3.13 while `import encomp` failed on 3.14. Cover the oldest and newest
supported Python so a version-dependent floor fails in CI.

Verified locally for the 3.14 leg: the lowest resolution picks
typeguard==4.1.3 / pydantic==2.12.0 / pydantic-settings==2.0.0, and the
installed suite passes (770 passed, 10 skipped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ainers

The mixed-container guard covered only numpy-vs-polars. A pl.Series
quantity compared against a pl.Expr quantity fell through to polars'
raw operator, which lifts the Series into an Expr literal and compares
EXACTLY -- silently skipping the (rtol, atol) tolerance every sanctioned
path applies (within-tolerance operands answered False), with a length
mismatch surfacing only at collect() time as a ShapeError pointing into
the plan. The pair is outside the typed API exactly like numpy-vs-polars,
and the guard's stated principle is to refuse instead of picking silently.

Treat any two DIFFERENT magnitude containers as mixed, and point the error
at the right escape hatch: .astype() conversion for eager containers, or
evaluating / explicitly pl.lit()-lifting when a pl.Expr is involved (an
Expr has no data for .astype to convert).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A missing density yields a missing result at that position: NaN for
float/numpy magnitudes, null for a Polars Series. A scalar NaN density (or
an ndarray density with NaN rows) paired with a pl.Series input slipped
between the container-specific guards and produced a Series result carrying
NaN -- the sentinel a polars magnitude never uses, and the exact leak the
Series-density branch rejects with 'use null for a missing density'.

A float/ndarray density has no null spelling, so with a Polars input a NaN
density cannot express 'missing': reject it with the same pointer at a
null-carrying pl.Series. NaN densities remain the valid missing sentinel
for float and numpy inputs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The note cited np.isclose, whose asymmetric predicate is precisely what
the module-level _is_close exists to avoid; name the tolerance pair
instead of an implementation the equality operator does not use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…EADME

The overview bullet credited Quantity.__new__ for the * / ` / ** result
dimensionalities; __new__ overloads encode the unit literals, and the
combinations are __mul__/__truediv__/__pow__ overloads, as the type-system
section further down already states.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The versioning sections promised semantic versioning for documented public
APIs; the release practice keeps those APIs stable where possible and
announces any breaking change in the GitHub release notes, without
reserving such changes for major versions. Say that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Files section named fluid and humid_air as the package's public API;
water is documented in the same README's API list above.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The committed state was an out-of-order interactive-session snapshot:
execution counts 19, 20, 39, 21, ..., a stale 'extension is already
loaded' stream in the setup cell, and no outputs for the %%pyinstrument
cell. Re-execute sequentially (counts 1-14) and drop the pyinstrument
profile display from the committed outputs -- it embeds machine-local
filesystem paths -- keeping the cell's DataFrame result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
automodule :members: only renders module data that carries an attribute
docstring, so the exported constants (BACKENDS, FLUID_PARAMS, WATER_NAME,
PHASE_IGNORING_BACKENDS, EAGER_PLUGIN_MIN_SIZE, CONSTANTS, SETTINGS,
CUSTOM_DIMENSIONS, ...) were invisible on the rendered API page. Give each
one a docstring.

In encomp.units this alone was not enough: the rtol/atol comment wrapped
onto a line starting with '# type:', which sphinx's source analyzer parses
as a PEP 484 type comment -- the resulting parse failure silently dropped
the attribute docstrings of the whole module. Rewrap the comment and leave
a note, since nothing else guards against that.

UNIT_REGISTRY stays undocumented data: autodoc's signature formatter fails
on its UnitRegistry[Any] value annotation, which would break the -W docs
build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Patch release covering the holistic-review fixes: the typeguard floor
raise, the ordering-operator derivation, the Series-vs-Expr comparison
guard, the convert_volume_mass NaN-density rejection, and the docs and
notebook refreshes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The raw.githubusercontent.com URL embedded the release tag, which made
the README a second place that had to be edited on every version bump.
Pointing it at main keeps the version in pyproject.toml only, and the
absolute URL still renders on PyPI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wlaur wlaur merged commit 48fb8d8 into main Jul 11, 2026
19 checks passed
@wlaur wlaur deleted the fix/holistic-review-1.7.1 branch July 11, 2026 08:05
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