Skip to content

refactor: define and freeze the public surface - #49

Merged
HugoFara merged 2 commits into
mainfrom
refactor/public-api-freeze
Sep 12, 2026
Merged

HugoFara merged 2 commits into
mainfrom
refactor/public-api-freeze

Conversation

@HugoFara

@HugoFara HugoFara commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

Closes #22 and #23.

The rule

A name is public when it is in a package's __all__; modules inside a package are implementation; each name has one home. The top-level package re-exports the definition path as a shortcut; every other second location is a deprecated alias.

Stated on the Deprecations page (new "What is public" section), and enforced by tests/test_public_api.py, which pins every package's __all__ name by name. The surface cannot change without a diff in that file.

Applying it to the 379 exported names

verdict count what happens
public 220 unchanged
provisional 80 unchanged; listed on the Deprecations page as exported-but-not-yet-promised (topology, solver, co-design, multi-topology synthesis, hierarchical hypergraphs, CAD export)
internal 54 leave __all__, stay importable, no warning — nothing outside the package imports them and import * is not a promise. Numba kernels, per-entity serialization halves, typing aliases, styling constants, _AnchorProxy, MutableAgent, …
duplicate 11 new deprecated alias: same object, DeprecationWarning naming the home. The eight frame/actuator/container names in dyads; optimization.Ensemble; assur.MobilityResult / StructuralAnalysis, which nothing in the package ever produced
missing — promoted: every subpackage is a top-level attribute (only five were); Ground, Crank, RRRDyad, … importable from pylinkage; mechanism.TrackerJoint and ArcDriverLink exported (leggedsnake and the editor backend were fetching them from submodules); dimensions and exceptions get an __all__

Nothing is removed. Minor-release safe by construction.

#23

README gains a "Which API should I use?" section: components / actuators / dyads / simulation is the path, with one line each for MechanismBuilder, hypergraph, assur, Mechanism. The API reference index opens with the same guidance instead of an alphabetical list; getting-started says it in one sentence. README and changelog link the policy.

Also

The second commit adds TYPE_CHECKING blocks mirroring the lazy-load tables in optimization, visualizer, solver and the top level. Without them a type checker sees lazily loaded names as object, which broke leggedsnake's mypy the moment it imported CoOptimizationConfig from the package as the rule asks.

Downstream

Both are proof the rule holds for the two real consumers.

Verification

  • Full suite with -W error::DeprecationWarning: 2552 passed. No internal path trips its own shims.
  • ruff, mypy clean.
  • leggedsnake against this branch: 622 passed, the same 5 pre-existing failures as main+main; mypy 9 → 6 errors.
  • Editor server smoke tests: 5 passed.

Closes #22 and #23.

A name is public when it is in a package's __all__; modules inside a
package are implementation; each name has one home. That rule is now
stated on the Deprecations page and enforced by tests/test_public_api.py,
which pins every package's __all__ name by name, so the surface cannot
change without a diff in that file.

Applying the rule to the 379 names that were exported:

- 54 implementation names leave __all__ but stay importable: numba
  kernels in solver, per-entity serialization halves and typing aliases
  in mechanism and synthesis, styling constants in visualizer, lookup
  helpers in assur, _AnchorProxy. No warning, since nothing outside the
  package imports them and star-import is not a promise.
- 11 sibling re-exports become deprecated aliases that warn: the eight
  frame/actuator/container names in dyads, optimization.Ensemble, and
  assur.MobilityResult / StructuralAnalysis, which nothing produced.
  Same objects, same isinstance behaviour.
- 80 names are marked provisional in the freeze: topology, solver,
  co-design, multi-topology synthesis, hierarchical hypergraphs, CAD
  export. Exported and documented, not yet promised.
- Promotions: every subpackage is a top-level attribute (only five
  were), the definition path is importable from pylinkage directly, and
  mechanism exports TrackerJoint and ArcDriverLink, which leggedsnake
  and the editor backend were fetching from submodules.

For the definition path itself, the README gains a "Which API should I
use?" section naming components / actuators / dyads / simulation and
saying in one line each what MechanismBuilder, hypergraph, assur and
Mechanism are for; the API reference index opens with the same guidance.
README and changelog link the policy.

Nothing is removed. The full suite passes with DeprecationWarning as an
error, so no internal path trips its own shims.
optimization, visualizer, solver and the top-level package load some
names on first access through a module __getattr__ that returns object.
That is invisible to a type checker: leggedsnake importing
CoOptimizationConfig from pylinkage.optimization, as the public-surface
rule now asks it to, got "object not callable" and "not valid as a
type" from mypy.

Each of those packages now carries a TYPE_CHECKING block importing the
same names eagerly. Runtime behaviour is unchanged; the block never
executes.
@HugoFara

Copy link
Copy Markdown
Owner Author

I'll merge for now, and do thorough testing before cutting the release.

@HugoFara
HugoFara merged commit d84011f into main Sep 12, 2026
9 checks passed
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.

Public API surface freeze + deprecation policy

1 participant