Problem
All 5 wrapped tools (pywake, floris, foxes, wayve, code_saturne) are hard dependencies in pyproject.toml. Users who only need one tool must install all five. This makes installation heavy and fragile — a broken dependency in any one tool blocks all of WIFA.
Proposed Solution
Make each tool independently installable via extras:
pip install wifa[pywake]
pip install wifa[floris]
pip install wifa[foxes]
pip install wifa[wayve]
pip install wifa[all] (installs everything, current behavior)
Core pip install wifa would only install shared dependencies (windIO, xarray).
Key insight
The API modules (pywake_api.py, floris_api.py, etc.) already use lazy imports inside functions — the tool packages are never imported at module level. This means import wifa and from wifa import run_pywake already work without the tools installed. Failures only happen when run_pywake() is called and the lazy imports execute. This makes the migration straightforward.
Note on wayve
Wayve's own setup.py declares foxes>=1.1 and py_wake as hard dependencies. So pip install wifa[wayve] will transitively install foxes and py_wake via wayve's own requirements. This is an upstream issue to address separately in the wayve project. The WIFA-level optional groups are still correct — they just can't prevent wayve from pulling in its own deps.
Implementation Plan
Step 1: Create availability-check utility
New file: wifa/_optional.py
import importlib.util
def require(package_name: str, extra_name: str) -> None:
"""Raise a clear ImportError if an optional dependency is missing."""
if importlib.util.find_spec(package_name) is None:
raise ImportError(
f"'{package_name}' is required for this functionality but is not installed. "
f"Install it with: pip install wifa[{extra_name}]"
)
Step 2: Update pyproject.toml
Move the 5 tools from dependencies to [project.optional-dependencies]. Also move mpmath into the wayve extra since it's only used by wayve_api.py.
dependencies = [
"windIO @ git+https://github.com/EUFlow/windIO.git",
"xarray>=2022.0.0,<2025",
]
[project.optional-dependencies]
pywake = ["py_wake>=2.6.5"]
foxes = ["foxes>=1.6.2"]
floris = ["floris @ git+https://github.com/lejeunemax/floris.git@windIO"]
wayve = [
"wayve @ git+https://gitlab.kuleuven.be/TFSO-software/wayve@dev_foxes",
"mpmath",
]
cs = [] # code_saturne is a subprocess, no pip dependency
all = [
"wifa[pywake]",
"wifa[foxes]",
"wifa[floris]",
"wifa[wayve]",
]
test = [
"pytest",
"pytest-cov",
"pycodestyle",
]
# dev and docs groups unchanged
Step 3: Add availability checks to each API module's run function
Add a require() call at the top of each run_* function, before any tool-specific code runs. This catches missing deps early with a clear message.
wifa/pywake_api.py — require("py_wake", "pywake") at top of run_pywake()
wifa/floris_api.py — require("floris", "floris") at top of run_floris()
wifa/foxes_api.py — require("foxes", "foxes") at top of run_foxes()
wifa/wayve_api.py — require("wayve", "wayve") at top of run_wayve()
wifa/cs_api/ — No change needed (code_saturne is a subprocess, no Python package to guard)
Step 4: Fix wayve-foxes cross-dependency
In wifa/wayve_api.py, function wake_model_setup() (line 640):
Currently line 642 unconditionally imports FoxesWakeModel from wayve. Move this import into the elif wake_tool == "foxes": branch where it's actually used, alongside the existing foxes imports. Add require("foxes", "foxes") in that branch.
Step 5: Update wifa/__init__.py
Add missing run_floris import for consistency. All API module imports are safe without the tools installed because the API modules only import tools lazily inside function bodies.
Step 6: wifa/main_api.py — No changes needed
Already safe — it imports run_* functions from the API modules (which is safe), and routes to them based on YAML config. The require() check inside each run_* function will catch missing tools at call time.
Step 7: Add test skip markers
Use pytest.importorskip() at the top of each test module. When the package isn't found, it raises pytest.skip.Exception during collection, skipping all tests in the module.
tests/test_pywake.py — pytest.importorskip("py_wake", ...)
tests/test_floris.py — pytest.importorskip("floris", ...)
tests/test_foxes.py — pytest.importorskip("foxes", ...)
tests/test_wayve.py — pytest.importorskip("wayve", ...)
tests/test_cs.py — No change needed
Files Modified
| File |
Change |
wifa/_optional.py |
NEW — require() helper |
pyproject.toml |
Move 5 tools + mpmath to optional-dependencies, add all extra |
wifa/__init__.py |
Add missing run_floris import |
wifa/pywake_api.py |
Add require("py_wake", "pywake") in run_pywake() |
wifa/floris_api.py |
Add require("floris", "floris") in run_floris() |
wifa/foxes_api.py |
Add require("foxes", "foxes") in run_foxes() |
wifa/wayve_api.py |
Add require() calls, move FoxesWakeModel import into foxes branch |
tests/test_pywake.py |
Add pytest.importorskip |
tests/test_floris.py |
Add pytest.importorskip |
tests/test_foxes.py |
Add pytest.importorskip |
tests/test_wayve.py |
Add pytest.importorskip |
Verification
- All tools installed:
pip install -e ".[all,test]" && pytest — all tests pass as before
- Individual install:
pip install -e ".[pywake,test]" && pytest tests/test_pywake.py — pywake tests pass
- Skip behavior:
pip install -e ".[test]" && pytest — all tool tests skipped with clear messages
- Error messages:
python -c "from wifa import run_pywake; run_pywake('x')" without pywake — shows clear install instructions
- Import safety:
pip install -e . && python -c "import wifa" — succeeds without any tools
Problem
All 5 wrapped tools (pywake, floris, foxes, wayve, code_saturne) are hard dependencies in
pyproject.toml. Users who only need one tool must install all five. This makes installation heavy and fragile — a broken dependency in any one tool blocks all of WIFA.Proposed Solution
Make each tool independently installable via extras:
pip install wifa[pywake]pip install wifa[floris]pip install wifa[foxes]pip install wifa[wayve]pip install wifa[all](installs everything, current behavior)Core
pip install wifawould only install shared dependencies (windIO, xarray).Key insight
The API modules (
pywake_api.py,floris_api.py, etc.) already use lazy imports inside functions — the tool packages are never imported at module level. This meansimport wifaandfrom wifa import run_pywakealready work without the tools installed. Failures only happen whenrun_pywake()is called and the lazy imports execute. This makes the migration straightforward.Note on wayve
Wayve's own
setup.pydeclaresfoxes>=1.1andpy_wakeas hard dependencies. Sopip install wifa[wayve]will transitively install foxes and py_wake via wayve's own requirements. This is an upstream issue to address separately in the wayve project. The WIFA-level optional groups are still correct — they just can't prevent wayve from pulling in its own deps.Implementation Plan
Step 1: Create availability-check utility
New file:
wifa/_optional.pyStep 2: Update
pyproject.tomlMove the 5 tools from
dependenciesto[project.optional-dependencies]. Also movempmathinto thewayveextra since it's only used bywayve_api.py.Step 3: Add availability checks to each API module's
runfunctionAdd a
require()call at the top of eachrun_*function, before any tool-specific code runs. This catches missing deps early with a clear message.wifa/pywake_api.py—require("py_wake", "pywake")at top ofrun_pywake()wifa/floris_api.py—require("floris", "floris")at top ofrun_floris()wifa/foxes_api.py—require("foxes", "foxes")at top ofrun_foxes()wifa/wayve_api.py—require("wayve", "wayve")at top ofrun_wayve()wifa/cs_api/— No change needed (code_saturne is a subprocess, no Python package to guard)Step 4: Fix wayve-foxes cross-dependency
In
wifa/wayve_api.py, functionwake_model_setup()(line 640):Currently line 642 unconditionally imports
FoxesWakeModelfrom wayve. Move this import into theelif wake_tool == "foxes":branch where it's actually used, alongside the existing foxes imports. Addrequire("foxes", "foxes")in that branch.Step 5: Update
wifa/__init__.pyAdd missing
run_florisimport for consistency. All API module imports are safe without the tools installed because the API modules only import tools lazily inside function bodies.Step 6:
wifa/main_api.py— No changes neededAlready safe — it imports
run_*functions from the API modules (which is safe), and routes to them based on YAML config. Therequire()check inside eachrun_*function will catch missing tools at call time.Step 7: Add test skip markers
Use
pytest.importorskip()at the top of each test module. When the package isn't found, it raisespytest.skip.Exceptionduring collection, skipping all tests in the module.tests/test_pywake.py—pytest.importorskip("py_wake", ...)tests/test_floris.py—pytest.importorskip("floris", ...)tests/test_foxes.py—pytest.importorskip("foxes", ...)tests/test_wayve.py—pytest.importorskip("wayve", ...)tests/test_cs.py— No change neededFiles Modified
wifa/_optional.pyrequire()helperpyproject.tomlallextrawifa/__init__.pyrun_florisimportwifa/pywake_api.pyrequire("py_wake", "pywake")inrun_pywake()wifa/floris_api.pyrequire("floris", "floris")inrun_floris()wifa/foxes_api.pyrequire("foxes", "foxes")inrun_foxes()wifa/wayve_api.pyrequire()calls, moveFoxesWakeModelimport into foxes branchtests/test_pywake.pypytest.importorskiptests/test_floris.pypytest.importorskiptests/test_foxes.pypytest.importorskiptests/test_wayve.pypytest.importorskipVerification
pip install -e ".[all,test]" && pytest— all tests pass as beforepip install -e ".[pywake,test]" && pytest tests/test_pywake.py— pywake tests passpip install -e ".[test]" && pytest— all tool tests skipped with clear messagespython -c "from wifa import run_pywake; run_pywake('x')"without pywake — shows clear install instructionspip install -e . && python -c "import wifa"— succeeds without any tools