Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions pysweb/swb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Objective: Expose SWB package entry points while preserving callable facade attributes across import order.
Author: Yi Yu (with assistance from Codex)
Created: 2026-04-19
Last updated: 2026-04-19
Last updated: 2026-10-17
Inputs: Package attribute access and submodule imports.
Outputs: Callable package facade entry points.
Usage: import pysweb.swb
Expand All @@ -21,10 +21,7 @@ def __getattr__(name):
if name == "calibrate":
return import_module("pysweb.swb.calibrate")
if name == "run":
api = import_module("pysweb.swb.api")
value = api.run
globals()[name] = value
return value
return import_module("pysweb.swb.run")
raise AttributeError(f"module 'pysweb.swb' has no attribute {name!r}")


Expand Down
8 changes: 1 addition & 7 deletions pysweb/swb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Objective: Dispatch public SWB preprocessing, calibration and simulation calls to package workflows.
Author: Yi Yu (with assistance from Codex)
Created: 2026-04-17
Last updated: 2026-10-11
Last updated: 2026-10-17
Inputs: Keyword arguments describing workflow inputs, outputs and model settings.
Outputs: Preprocessing results, calibration parameters or the simulation output path.
Usage: import pysweb.swb.api
Expand All @@ -12,7 +12,6 @@
from __future__ import annotations

from importlib import import_module
import sys

from pysweb.swb.run import run_swb_workflow

Expand Down Expand Up @@ -53,8 +52,3 @@ def run(**kwargs):
raise ValueError("Missing required inputs for SWB run")

return run_swb_workflow(**kwargs)


_PACKAGE = sys.modules.get("pysweb.swb")
if _PACKAGE is not None:
_PACKAGE.run = run
24 changes: 8 additions & 16 deletions pysweb/swb/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import argparse
import multiprocessing as mp
import sys
import types
from concurrent.futures import ProcessPoolExecutor, as_completed
from importlib import import_module
from pathlib import Path
Expand Down Expand Up @@ -806,24 +807,15 @@ def main(argv=None):
run_swb_workflow(**vars(build_parser().parse_args(argv)))


def _restore_package_run_facade() -> None:
package = sys.modules.get("pysweb.swb")
if package is None:
return
class _CallableRunModule(types.ModuleType):
def __call__(self, **kwargs):
# Import machinery assigns this module to its parent's run attribute
# after executing it. Making the module callable survives that step
# and preserves both submodule aliases and the public API validation.
return import_module("pysweb.swb.api").run(**kwargs)

api_module = sys.modules.get("pysweb.swb.api")
if api_module is not None and not hasattr(api_module, "run"):
return

# Keep `import pysweb.swb.run` working while restoring `pysweb.swb.run(...)`
# as the facade callable exposed from the package root.
if api_module is None:
package.run = import_module("pysweb.swb.api").run
else:
package.run = api_module.run


_restore_package_run_facade()
sys.modules[__name__].__class__ = _CallableRunModule


if __name__ == "__main__":
Expand Down
39 changes: 38 additions & 1 deletion tests/package/test_pysweb_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Objective: Verify the package exposes importable subpackages and callable facade entry points.
Author: Yi Yu (with assistance from Codex)
Created: 2026-04-17
Last updated: 2026-05-03
Last updated: 2026-10-17
Inputs: Package imports and direct facade calls exercised under pytest.
Outputs: Test assertions.
Usage: python -m pytest tests/package/test_pysweb_imports.py
Expand Down Expand Up @@ -113,6 +113,7 @@ def test_package_api_contracts():
[
("pysweb.swb.preprocess", "preprocess"),
("pysweb.swb.calibrate", "calibrate"),
("pysweb.swb.run", "run"),
],
)
def test_submodule_first_import_keeps_package_entry_points_callable(submodule_name, attribute_name):
Expand All @@ -131,6 +132,7 @@ def test_submodule_first_import_keeps_package_entry_points_callable(submodule_na
[
("import pysweb.swb.preprocess as preprocess_module", "preprocess_module"),
("import pysweb.swb.calibrate as calibrate_module", "calibrate_module"),
("import pysweb.swb.run as run_module", "run_module"),
],
)
def test_submodule_import_alias_remains_a_module(import_statement, module_name):
Expand All @@ -149,6 +151,7 @@ def test_submodule_import_alias_remains_a_module(import_statement, module_name):
[
("preprocess",),
("calibrate",),
("run",),
],
)
def test_api_first_import_keeps_package_entry_points_as_callable_modules(attribute_name):
Expand All @@ -168,6 +171,7 @@ def test_api_first_import_keeps_package_entry_points_as_callable_modules(attribu
("module_name", "arguments"),
[
("pysweb.swb.preprocess", ["--help"]),
("pysweb.swb.run", ["--help"]),
(
"pysweb.swb.calibrate",
["--help"],
Expand All @@ -184,3 +188,36 @@ def test_module_cli_execution_exits_cleanly(module_name, arguments):

assert result.returncode == 0
assert result.stderr == ""


@pytest.mark.parametrize("first_import", [
"from pysweb.swb.run import _parser_defaults",
"from pysweb.swb.api import run",
"import pysweb.swb; pysweb.swb.run",
])
def test_run_dispatch_and_validation_survive_fresh_import_order(first_import):
# Reproduce the production sequence in a fresh interpreter, independently
# of pytest's collection imports and earlier API attribute accesses.
script = first_import + "\n" + '''
import importlib
import inspect
import pysweb.swb
api = importlib.import_module("pysweb.swb.api")
module = importlib.import_module("pysweb.swb.run")
assert inspect.ismodule(module) and callable(module)
assert pysweb.swb.run is module
try:
pysweb.swb.run()
except ValueError as error:
assert str(error) == "Missing required inputs for SWB run"
else:
raise AssertionError("Missing-input validation was bypassed")
calls = []
api.run_swb_workflow = lambda **kwargs: calls.append(kwargs) or "accepted"
assert pysweb.swb.run(et="forcing.nc", workers=2) == "accepted"
assert module(et="forcing.nc", workers=2) == "accepted"
assert calls == [dict(et="forcing.nc", workers=2)] * 2
'''
result = subprocess.run([sys.executable, "-c", script],
capture_output=True, text=True, check=False)
assert result.returncode == 0, result.stderr
Loading