Skip to content

Create a public API - #194

Open
FrederikSchnack wants to merge 5 commits into
upgrade_sympyfrom
public_api
Open

FrederikSchnack wants to merge 5 commits into
upgrade_sympyfrom
public_api

Conversation

@FrederikSchnack

@FrederikSchnack FrederikSchnack commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This is the final PR of this stack.

Introduce a single, explicit public API for SymPDE and reduce the amount of code and third-party functionality loaded during package imports.

User-facing code should now import supported objects from:

from sympde.api import Domain, Square, Mapping
from sympde.api import ScalarFunctionSpace, elements_of
from sympde.api import BilinearForm, LinearForm, integral

Internal SymPDE modules continue to import objects directly from their defining modules.

Closes #180
Closes #181
Closes #182

Public API changes

1. Add an explicit sympde.api namespace

A new sympde.api package provides a documented, flat namespace containing the supported user-facing objects.

The API covers:

  • symbolic constants and calculus operators;
  • domains, boundaries, interfaces, and mappings;
  • scalar and vector function spaces;
  • symbolic functions and differential operators;
  • variational forms, equations, norms, and integrals;
  • exterior-calculus objects;
  • printers and utility functions;
  • the package version.

The namespace defines an explicit __all__, making it possible to identify the supported API without relying on implementation details or wildcard imports.

Tests verify that:

  • every name in __all__ exists;
  • __all__ does not contain duplicates;
  • no unintended public names leak into the namespace;
  • exported objects are the original objects from their defining modules, rather than wrappers or duplicate definitions.

2. Remove wildcard exports from package initializers

The previous package initializers recursively imported and re-exported entire subpackages:

from .core import *
from .topology import *
from .exterior import *

Importing a small part of SymPDE could therefore load most of the library, including modules unrelated to the requested functionality.

The initializers for the following packages are now empty:

sympde
sympde.calculus
sympde.core
sympde.expr
sympde.exterior
sympde.printing
sympde.topology

This makes imports more predictable and prevents package initialization from eagerly traversing the full dependency graph.

3. Establish separate user-facing and internal import conventions

User code should use the convenient public namespace:

from sympde.api import Domain, Square, grad

Internal library code imports directly from the module that defines each object:

from sympde.topology.domain import Domain, Square
from sympde.calculus.core import grad

This separation provides a stable user-facing API while keeping internal dependencies explicit and avoiding circular imports.

Import-performance improvements

4. Avoid loading the complete library for package imports

Previously, imports such as:

import sympde
import sympde.topology.domain

executed wildcard imports from multiple package initializers. This loaded many unrelated symbolic, plotting, mapping, and expression modules.

With empty package initializers, importing a module now loads only its actual dependencies. The complete user-facing namespace is loaded only when explicitly requested through:

import sympde.api

5. Import plotting dependencies only when plotting is requested

matplotlib.pyplot was previously imported when sympde.utilities.utils was loaded, even when no plotting functionality was used.

It is now imported locally by the plotting functions:

plot_2d(...)
plot_3d(...)

This avoids Matplotlib initialization, backend selection, and configuration-directory access during unrelated SymPDE imports.

6. Import serialization dependencies only when needed

The domain module previously imported h5py, yaml, and additional NumPy functionality at module import time.

These dependencies are now imported inside the operations that require them:

domain.export(...)
Domain.from_file(...)

Creating or manipulating symbolic domains therefore no longer initializes the HDF5 and YAML stacks.

7. Remove unnecessary production imports

A large number of unused imports were removed from the core, topology, exterior-calculus, expression, and printing modules.

This includes unused:

  • NumPy imports;
  • SymPy symbols and matrix classes;
  • traversal and simplification helpers;
  • mapping and topology classes;
  • calculus operators;
  • legacy compatibility helpers.

Besides reducing import work, this makes each module’s dependency requirements easier to understand.

8. Break circular imports with local imports

The topology derivative module previously imported mapping classes at module scope, while the mapping module also depended on derivative definitions.

Mapping and Jacobian are now imported locally only in the logical-derivative path that needs them.

This removes the module-level cycle without changing the public behavior of differential operators.

Compatibility and cleanup

9. Remove the obsolete SymPy compatibility module

The previous PR replaced usages of the local compatibility helpers with maintained SymPy utilities and native Python metaclass syntax.

Once the package-level wildcard exports are removed, the remaining compatibility module is no longer part of the import surface and can be deleted.

This removes:

sympde/old_sympy_utilities.py

10. Migrate tests to the supported API

User-facing tests now import objects from sympde.api rather than relying on wildcard package exports.

This ensures that the test suite exercises the same import path recommended to downstream users.

Dedicated API tests verify:

  • package initializers remain empty;
  • the API contains only explicitly exported names;
  • API exports refer to the canonical definitions;
  • no duplicate names are present.

The API checks use direct imports and assertions rather than dynamically executing generated import statements.

11. Fix plotting of unmapped two-dimensional domains

While adding coverage for deferred plotting imports, the unmapped 2D plotting path was found to create a three-dimensional identity mapping.

It now creates the correct two-dimensional identity mapping:

IdentityMapping('Id', dim=2)

A regression test plots an unmapped square with a non-interactive Matplotlib backend.

Documentation changes

12. Document the supported public API

The README and Sphinx documentation now direct users to sympde.api.

A dedicated API page documents the complete explicit namespace:

from sympde.api import Cube, Mapping, Constant
from sympde.api import ScalarFunctionSpace, elements_of
from sympde.api import BilinearForm, LinearForm, Norm

The API documentation avoids generating duplicate cross-reference targets for objects that are also documented in their defining modules.

13. Replace obsolete notebooks with executable examples

The old notebooks and their Binder integration had become outdated and were not validated by CI.

They are replaced by focused Python examples covering:

  • a quick start;
  • analytical mappings;
  • differential calculus;
  • exterior calculus;
  • nonlinear Poisson equations;
  • tensorization;
  • variational forms and multipatch interfaces.

All examples use the new public API.

14. Execute documentation examples in CI

The documentation workflow now runs every Python example before building the Sphinx pages:

for example in doc/examples/*.py; do
    python "$example"
done

This ensures that documented imports and examples remain executable as the API evolves.

Obsolete documentation helper scripts, notebooks, notebook styles, and the Binder badge are removed.

Breaking changes

Wildcard convenience imports from package namespaces are no longer supported.

For example, user code should migrate from:

from sympde.topology import Domain, Square
from sympde.expr import BilinearForm
from sympde.calculus import grad

to:

from sympde.api import Domain, Square, BilinearForm, grad

Code that intentionally depends on internal implementation details may instead import directly from the defining module:

from sympde.topology.domain import Domain, Square
from sympde.expr.expr import BilinearForm
from sympde.calculus.core import grad

The sympde.api namespace is the recommended interface for downstream users.

Validation

The complete test suite passes:

174 passed, 2 skipped

The documentation validation also passes:

  • every documentation example executes successfully;
  • Sphinx builds successfully with warnings treated as errors;
  • the explicit API namespace is checked for missing, duplicate, or unintended exports;
  • public API objects are verified against their canonical definitions;
  • deferred plotting imports are covered with a non-interactive regression test.

@FrederikSchnack
FrederikSchnack added this pull request to stack #192 September 29, 2026 15:37
@FrederikSchnack FrederikSchnack mentioned this pull request Sep 29, 2026
Expose supported user-facing symbols from sympde.api without relying on package-level wildcard exports.
Remove package-level wildcard exports and obsolete SymPy compatibility helpers, migrate internal imports and tests to defining modules, and defer costly optional plotting and serialization imports.
Move maintained notebook material into tested documentation examples and remove obsolete tutorial helpers and notebook assets.
Describe flat public imports in the README and add the API reference and executable examples to the Sphinx navigation.

@yguclu yguclu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job!

My single big concern is using the new public interface sympde.api in the unit tests in the tests folders. I have three main reasons to believe we should not do it:

  1. The unit tests are not "public-facing", as they only exist to ensure that every library component works as intended.
  2. The unit tests should know exactly the SymPDE modules they import from, and show them to the developers.
  3. As things progress, we hope that our unit tests will approach 100% coverage of the library. This will make sympde/api/__init__.py a huge file which is not easily maintained.

If we avoid using sympde.api in the unit tests, we could restrict its imports to those needed by the projects that we know for sure to depend on SymPDE. AFAIK those are just PSYDAC, IGA-Python, and GeLaTo. This will make the file much shorter, cleaner, and easier to maintain.

Comment thread CHANGELOG.md
Comment on lines 38 to +41
### Deprecated

- Package and subpackage imports no longer re-export public objects. Replace imports such as `from sympde.topology import Square` with `from sympde.api import Square`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usual meaning of "deprecation" is a feature that is not supposed to be used any longer, typically with a runtime warning. Further, a deprecated feature usually survives one or two release cycles before being removed, but we have no time for that here.

Since an error is raised immediately in our case, I recommend moving this point (also mentioning the PR number) to the "Removed" section

Comment thread CHANGELOG.md
Comment on lines 18 to +19
- Convert the README to Markdown and expand the installation, testing, and documentation-build instructions.
- Use explicit internal imports and load optional dependencies only when needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please mention the PR number

Comment thread CHANGELOG.md
Comment on lines +9 to +10
- Add `sympde.api` as the supported flat public API.
- Add executable examples to the documentation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please mention the PR number

Comment thread sympde/__init__.py
@@ -1,7 +0,0 @@
from .version import __version__

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should we remove this line? Importing __version__ directly from the top-level of a Python package is standard; for example, try from numpy import __version__ or from sympy import __version__

Comment thread doc/conf.py
from sphinx.ext.apidoc import main as sphinx_apidoc

from sympde import __version__
from sympde.version import __version__

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not make this change. See this comment

Comment on lines +32 to +39
# Define a custom mapping by specifying its coordinate expressions.
class ComplexSquareMapping(Mapping):
"""Map ``(x1, x2)`` through the square of a complex number."""

_expressions = {
'x': 'A * (x1**2 - x2**2)',
'y': '2 * A * x1 * x2',
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring sounds a bit obscure to me. Is this how the mapping is defined?

$$ x + i y = A~(x_1 + i x_2)^2 $$

If so, I would write that explicitly

Comment thread doc/include/graph.py

@yguclu yguclu Oct 13, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find that writing the expression tree to a PNG file using SymPy's dotprint function and the Linux dot command is quite interesting. Do we have anything like that in the new documentation?

Comment on lines +58 to +62
def test_plot_unmapped_2d_domain(monkeypatch, tmp_path):
"""Plotting an unmapped square must create a 2D identity mapping."""
monkeypatch.setenv('MPLBACKEND', 'Agg')
monkeypatch.setenv('MPLCONFIGDIR', str(tmp_path))
plot_domain(Square('Omega'), draw=False)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this unit test checking?

Comment on lines +107 to +110
# Imported lazily to avoid a module cycle: mapping depends
# on the differential-operator definitions in this module.
from .mapping import Jacobian, Mapping

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of writing "module cycle", I would use the more common words "circular dependency"

Suggested change
# Imported lazily to avoid a module cycle: mapping depends
# on the differential-operator definitions in this module.
from .mapping import Jacobian, Mapping
# Imported lazily to avoid a circular dependency: mapping
# depends on the differential operators in this module.
from .mapping import Jacobian, Mapping

Comment on lines +14 to +17
def integrate(expression):
"""Integrate an expression over the problem domain."""

return integral(domain, expression)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this function? It is only used in this example, and only two times

This branch has not been deployed

No deployments
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.

Clean up and speed up library import Remove unnecessary imports from __init__.py files Avoid useless imports

2 participants