diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4b7a9a94..ba7925a8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -458,6 +458,7 @@ jobs: needs: [set-os, prepare-nonhindcast-cache] runs-on: ubuntu-latest strategy: + fail-fast: false matrix: module: [wave, tidal, river, dolfyn, power, loads, mooring, acoustics, utils] @@ -478,15 +479,17 @@ jobs: path: ~/.cache/mhkit - name: Install system dependencies + if: matrix.module != 'river' || matrix.module != 'power' || matrix.module != 'utils' || matrix.module != 'loads' run: sudo apt-get install -y libhdf5-dev libnetcdf-dev - name: Install MHKiT with optional dependency run: | python -m pip install --upgrade pip - pip install "mhkit[${{ matrix.module }}]" + pip install -e ".[${{ matrix.module }}]" pip install pytest - name: Reinstall h5py and netCDF4 with system libraries + if: matrix.module != 'river' || matrix.module != 'power' || matrix.module != 'utils' || matrix.module != 'loads' run: "pip install --force-reinstall --no-binary=:all: h5py netCDF4" - name: Run tests for ${{ matrix.module }} diff --git a/mhkit/wave/__init__.py b/mhkit/wave/__init__.py index f84c667c..7fbaf30b 100644 --- a/mhkit/wave/__init__.py +++ b/mhkit/wave/__init__.py @@ -1,5 +1,37 @@ +import importlib + from mhkit.wave import resource -from mhkit.wave import io from mhkit.wave import graphics from mhkit.wave import performance from mhkit.wave import contours + + +def __getattr__(name): + """ + Lazy load the wave.io submodule using PEP 562 module-level __getattr__. + + This defers importing heavy wave.io dependencies (rex, netCDF4, etc,) until + they are actually accessed, improving import time for users who don't need + all wave submodules, and avoiding import errors for users who have specified + module level installs that need wave module functions, but not wave.io functions. + """ + if name == "io": + # This uses importlib.import_module() here, not "from mhkit.wave import io" + # because when Python executes getattr(), it looks for 'io' as an attribute of + # mhkit.wave. At this point in the module loading code 'io' doesn't exist yet and + # Python calls __getattr__('io') again. This triggers the same "from" statement, + # which calls __getattr__('io') again yielding a RecursionError. + # + # To fix this uses importlib.import_module("mhkit.wave.io") which loads the module directly + # using the absolute path without doing attribute lookup on the parent. + # + # The statement "from mhkit.wave import io" is equivalent to: + # io = getattr(mhkit.wave, 'io') + # + io = importlib.import_module("mhkit.wave.io") + + # Cache the module so subsequent accesses bypass __getattr__ entirely + globals()[name] = io + return io + + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/pyproject.toml b/pyproject.toml index 7623b9ed..c0c7df26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "xarray>=2024.6.0", "matplotlib>=3.9.1", "pecos>=0.3.0", + "requests", ] [project.optional-dependencies] @@ -38,20 +39,17 @@ wave = [ "pytz", "NREL-rex>=0.2.63", "beautifulsoup4", - "requests", "bottleneck", "lxml" ] tidal = [ "netCDF4>=1.7.1.post1", - "requests", "bottleneck" ] river = [ "netCDF4>=1.7.1.post1", - "requests", "bottleneck", ] @@ -66,7 +64,9 @@ power = [ ] loads = [ - "fatpack" + "fatpack", + "statsmodels>=0.14.2", + "scikit-learn>=1.5.1", ] mooring = [