Skip to content

Commit 0728e88

Browse files
simmsajmcvey3akeeste
authored
Fix pip module tests (#423)
Status: Ready This fixes test failures related to module loading using `pip install "mhkit[<module>]"` syntax. There are some modules that have interdependencies with other modules, i.e., loads module needs wave module code that are causing test failures. The root cause of test failures is specific packages that are not specified in pyproject.toml for a specific module, but are called when the tests are run. The fix is to add lazy loading for the `wave.io` module and reconfigure the modules in pyproject.toml to capture the a more correct dependency list derived from running the tests. Also the command that runs these tests was changed from `pip install -e "mhkit[<module>]" .` to `pip install -e ".[<module>]"` because the tests fail with `-e "mhkit"` but succeed with `-e "."`. I suspect that this changes the way mhkit is installed, but I don't know what the specific differences are. --------- Co-authored-by: jmcvey3 <53623232+jmcvey3@users.noreply.github.com> Co-authored-by: akeeste <akeeste@sandia.gov>
1 parent 9fbc192 commit 0728e88

3 files changed

Lines changed: 41 additions & 6 deletions

File tree

.github/workflows/main.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ jobs:
458458
needs: [set-os, prepare-nonhindcast-cache]
459459
runs-on: ubuntu-latest
460460
strategy:
461+
fail-fast: false
461462
matrix:
462463
module:
463464
[wave, tidal, river, dolfyn, power, loads, mooring, acoustics, utils]
@@ -478,15 +479,17 @@ jobs:
478479
path: ~/.cache/mhkit
479480

480481
- name: Install system dependencies
482+
if: matrix.module != 'river' || matrix.module != 'power' || matrix.module != 'utils' || matrix.module != 'loads'
481483
run: sudo apt-get install -y libhdf5-dev libnetcdf-dev
482484

483485
- name: Install MHKiT with optional dependency
484486
run: |
485487
python -m pip install --upgrade pip
486-
pip install "mhkit[${{ matrix.module }}]"
488+
pip install -e ".[${{ matrix.module }}]"
487489
pip install pytest
488490
489491
- name: Reinstall h5py and netCDF4 with system libraries
492+
if: matrix.module != 'river' || matrix.module != 'power' || matrix.module != 'utils' || matrix.module != 'loads'
490493
run: "pip install --force-reinstall --no-binary=:all: h5py netCDF4"
491494

492495
- name: Run tests for ${{ matrix.module }}

mhkit/wave/__init__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
1+
import importlib
2+
13
from mhkit.wave import resource
2-
from mhkit.wave import io
34
from mhkit.wave import graphics
45
from mhkit.wave import performance
56
from mhkit.wave import contours
7+
8+
9+
def __getattr__(name):
10+
"""
11+
Lazy load the wave.io submodule using PEP 562 module-level __getattr__.
12+
13+
This defers importing heavy wave.io dependencies (rex, netCDF4, etc,) until
14+
they are actually accessed, improving import time for users who don't need
15+
all wave submodules, and avoiding import errors for users who have specified
16+
module level installs that need wave module functions, but not wave.io functions.
17+
"""
18+
if name == "io":
19+
# This uses importlib.import_module() here, not "from mhkit.wave import io"
20+
# because when Python executes getattr(), it looks for 'io' as an attribute of
21+
# mhkit.wave. At this point in the module loading code 'io' doesn't exist yet and
22+
# Python calls __getattr__('io') again. This triggers the same "from" statement,
23+
# which calls __getattr__('io') again yielding a RecursionError.
24+
#
25+
# To fix this uses importlib.import_module("mhkit.wave.io") which loads the module directly
26+
# using the absolute path without doing attribute lookup on the parent.
27+
#
28+
# The statement "from mhkit.wave import io" is equivalent to:
29+
# io = getattr(mhkit.wave, 'io')
30+
#
31+
io = importlib.import_module("mhkit.wave.io")
32+
33+
# Cache the module so subsequent accesses bypass __getattr__ entirely
34+
globals()[name] = io
35+
return io
36+
37+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies = [
2727
"xarray>=2024.6.0",
2828
"matplotlib>=3.9.1",
2929
"pecos>=0.3.0",
30+
"requests",
3031
]
3132

3233
[project.optional-dependencies]
@@ -38,20 +39,17 @@ wave = [
3839
"pytz",
3940
"NREL-rex>=0.2.63",
4041
"beautifulsoup4",
41-
"requests",
4242
"bottleneck",
4343
"lxml"
4444
]
4545

4646
tidal = [
4747
"netCDF4>=1.7.1.post1",
48-
"requests",
4948
"bottleneck"
5049
]
5150

5251
river = [
5352
"netCDF4>=1.7.1.post1",
54-
"requests",
5553
"bottleneck",
5654
]
5755

@@ -66,7 +64,9 @@ power = [
6664
]
6765

6866
loads = [
69-
"fatpack"
67+
"fatpack",
68+
"statsmodels>=0.14.2",
69+
"scikit-learn>=1.5.1",
7070
]
7171

7272
mooring = [

0 commit comments

Comments
 (0)