Skip to content

Make all wrapped tools (pywake, floris, foxes, wayve, code_saturne) optional dependencies #50

@bjarketol

Description

@bjarketol

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.pyrequire("py_wake", "pywake") at top of run_pywake()
  • wifa/floris_api.pyrequire("floris", "floris") at top of run_floris()
  • wifa/foxes_api.pyrequire("foxes", "foxes") at top of run_foxes()
  • wifa/wayve_api.pyrequire("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.pypytest.importorskip("py_wake", ...)
  • tests/test_floris.pypytest.importorskip("floris", ...)
  • tests/test_foxes.pypytest.importorskip("foxes", ...)
  • tests/test_wayve.pypytest.importorskip("wayve", ...)
  • tests/test_cs.py — No change needed

Files Modified

File Change
wifa/_optional.py NEWrequire() 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

  1. All tools installed: pip install -e ".[all,test]" && pytest — all tests pass as before
  2. Individual install: pip install -e ".[pywake,test]" && pytest tests/test_pywake.py — pywake tests pass
  3. Skip behavior: pip install -e ".[test]" && pytest — all tool tests skipped with clear messages
  4. Error messages: python -c "from wifa import run_pywake; run_pywake('x')" without pywake — shows clear install instructions
  5. Import safety: pip install -e . && python -c "import wifa" — succeeds without any tools

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions