Skip to content

fix(deps): make pyex installable as a bare dependency#139

Open
ivarvong wants to merge 3 commits into
mainfrom
pyex-consumable
Open

fix(deps): make pyex installable as a bare dependency#139
ivarvong wants to merge 3 commits into
mainfrom
pyex-consumable

Conversation

@ivarvong

Copy link
Copy Markdown
Owner

The bug

pyex referenced three deps in lib/ that a fresh consumer wouldn't have, so {:pyex, "~> x"} failed to compile for anyone. It only worked in-repo because the deps were present transitively. Found while composing pyex with another library — and it's a release blocker (#60 ships something that doesn't install).

dep how lib/ used it fix
decimal pervasive in the numeric tower, never declared real runtime dependency (it's core, not optional)
postgrex (sql) sql.ex pattern-matches %Postgrex.Error{} — a hard compile-time struct requirement wrap the module in if Code.ensure_loaded?(Postgrex)
explorer (pandas) scattered Explorer.Series/DataFrame calls + .t() specs + struct patterns across core modules @compile {:no_warn_undefined, …} for calls, term() for specs, is_struct(x, Explorer.Series) guards for patterns (a runtime atom check — no compile-time struct), producer module wrapped in Code.ensure_loaded?(Explorer)

postgrex/explorer stay optional: true, so the core library carries no heavy native deps. import sql / import pandas now raise a clean Python ImportError when the backend isn't installed (Pyex.Stdlib.fetch/1 and module_names/0 degrade gracefully) instead of crashing.

The idiomatic patterns (for the record)

  • Core dep → just declare it (decimal).
  • Optional dep with struct refs / patternsif Code.ensure_loaded?(Dep) around the module (struct expansion is a hard compile-time requirement that nothing else can defer).
  • Optional dep with scattered function calls@compile {:no_warn_undefined, [Dep.Mod]}.
  • Optional struct in a case/headis_struct(x, Dep.Struct) guard (takes the module as a runtime atom; compiles when absent).
  • Graceful runtimeCode.ensure_loaded?/function_exported? at the dispatch boundary.

Proof it works — and stays working

scripts/consumer_smoke.sh + a new consumer-smoke CI job: a throwaway project depending on pyex and nothing else compiles and runs —

==> resolving + compiling pyex as a bare dependency
==> asserting behavior with no optional backends present
consumer smoke: OK — pyex compiles + runs with no optional deps

It asserts Explorer/Postgrex are genuinely absent, core runs, and import pandas/import sql%Pyex.Error{kind: :import}. This is the regression class pyex's own build (where the optional deps are present) can't catch.

Gate

Full suite (6185) + Dialyzer green with the deps present; consumer-smoke green without them. mix format ✅.

🤖 Generated with Claude Code

https://claude.ai/code/session_019NokzcR7BiAigPgC78zpk9

ivarvong and others added 3 commits June 30, 2026 09:13
pyex referenced three deps in lib/ that a fresh consumer wouldn't have, so
`{:pyex, "~> x"}` failed to compile for anyone — it only worked in-repo because
the deps were present transitively. Found while composing pyex with another
library; it's also a release blocker.

  - decimal: used pervasively in the numeric tower but never declared. Now a
    real runtime dependency (it isn't optional — it's core).
  - postgrex (the `sql` backend): lib/pyex/stdlib/sql.ex pattern-matches
    %Postgrex.Error{} structs — a hard compile-time requirement. The module is
    now wrapped in `if Code.ensure_loaded?(Postgrex)`.
  - explorer (the `pandas` backend): scattered Explorer.Series/DataFrame calls,
    type specs, and struct patterns across core modules. Fixed idiomatically:
    `@compile {:no_warn_undefined, ...}` for the calls, `term()` for the specs,
    `is_struct(x, Explorer.Series)` guards (a runtime atom check, no compile-time
    struct) for the patterns, and the producer module wrapped in
    `Code.ensure_loaded?(Explorer)`.

postgrex/explorer stay `optional: true`: `import sql`/`import pandas` now raise
a clean Python ImportError when the backend isn't installed (Pyex.Stdlib.fetch/1
and module_names/0 degrade gracefully), so the core library carries no heavy
native deps.

Proven by `scripts/consumer_smoke.sh` + a `consumer-smoke` CI job: a throwaway
project depending on pyex and nothing else compiles and runs, with the optional
features degrading to ImportError — the regression class pyex's own build
(where the optional deps ARE present) cannot catch.

Full suite (6185) + Dialyzer green with the deps present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NokzcR7BiAigPgC78zpk9
Capture why pandas/sql are optional and how to add another optional backend,
citing the idiomatic shapes (the same ones :explorer uses for its own optional
:nx — @compile {:no_warn_undefined} + is_struct guards), with the consumer-smoke
CI job as the regression guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NokzcR7BiAigPgC78zpk9
A path dep ships the whole working tree, so it can't catch a compile-time file
(an @external_resource, a data dir) left out of `package/0`'s `:files`. Verified
the gap exists in principle, then closed it: consumer_smoke.sh now runs
`mix hex.build`, unpacks the package, and compiles *that* as a bare dependency —
the true "installs from hex" gate. Still asserts core runs and pandas/sql
degrade to ImportError with no optional backends present.
(The package was already complete — 145 files, incl. the spreadsheet.py
@external_resource — so this is the guard, not a fix.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NokzcR7BiAigPgC78zpk9
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