Skip to content

Commit 6888e41

Browse files
malmans2jdha
andauthored
Implement from_namelist method (#43)
* Introduce namelists in the accessor * Untested, but the main implementation should be in good shape * use class variable introduced * add tests * accessor now handles 999999 * Update accessor.py Adding error handle when importing namelist * introduce _check_namelist_entries * typo * tidy up namelist check * add type hints * minor semplification * add more checks * let Zco handle checks * deprecate ldbletanh * fix comment * better error print * decorate __call__ * clean utils * add tests * use None * ready for review * fix doc and ci * better docs * avoid numpy with type hint bug Co-authored-by: jdha <jdha@users.noreply.github.com>
1 parent 006b9f3 commit 6888e41

17 files changed

Lines changed: 463 additions & 102 deletions

.github/workflows/ci.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,26 @@ jobs:
6868
- name: Run Tests
6969
shell: bash -l {0}
7070
run: pytest --no-cov
71+
72+
bare-environment:
73+
name: bare-environment
74+
runs-on: ubuntu-latest
75+
steps:
76+
- uses: actions/checkout@v2
77+
- uses: conda-incubator/setup-miniconda@v2
78+
with:
79+
environment-file: ci/bare-environment.yml
80+
activate-environment: pydomcfg_test_bare
81+
auto-update-conda: false
82+
miniforge-variant: Mambaforge
83+
use-mamba: true
84+
85+
- name: Set up conda environment
86+
shell: bash -l {0}
87+
run: |
88+
python -m pip install -e .
89+
conda list
90+
91+
- name: Run Tests
92+
shell: bash -l {0}
93+
run: pytest --no-cov

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ repos:
4141
hooks:
4242
- id: mypy
4343
exclude: docs
44-
additional_dependencies: [xarray, types-pkg_resources]
44+
additional_dependencies: [xarray, types-pkg_resources, numpy!=1.21.0]
4545

4646
- repo: https://github.com/PyCQA/doc8
4747
rev: 0.9.0a1

ci/bare-environment.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: pydomcfg_test_bare
2+
channels:
3+
- conda-forge
4+
dependencies:
5+
- pooch
6+
- pytest-cov
7+
- pytest
8+
- pytest-xdist
9+
- xarray

ci/environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ dependencies:
77
- pytest
88
- pytest-xdist
99
- xarray
10+
- f90nml

ci/upstream-dev-env.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ dependencies:
99
- pip
1010
- pip:
1111
- git+https://github.com/pydata/xarray.git
12+
- git+https://github.com/marshallward/f90nml.git

docs/developers/whats-new.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
.. currentmodule:: pydomcfg
1+
.. currentmodule:: xarray
22

33
What's New
44
----------
55

66
Unreleased
77
==========
88

9+
- Introduced :py:meth:`Dataset.domcfg.from_namelist` to use
10+
``NEMO DOMAINcfg`` namelists (:pr:`43`)
911
- Introduced the ``domcfg`` accessor (:pr:`36`)
1012
- Added :py:meth:`~pydomcfg.tests.bathymetry.Bathymetry.sea_mount`
1113
useful to generate classic sea mount test case. (:pr:`17`)

docs/users/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Methods
2121
:toctree: generated/
2222
:template: autosummary/accessor_method.rst
2323

24+
Dataset.domcfg.from_namelist
2425
Dataset.domcfg.zco
2526

2627
Domzgr

docs/users/installing.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Required dependencies
99
- `xarray <http://xarray.pydata.org/>`_
1010
- `numpy <http://www.numpy.org/>`_
1111

12+
Optional dependencies
13+
---------------------
14+
15+
- `f90nml <https://f90nml.readthedocs.io/>`_
16+
1217
Instructions
1318
------------
1419

@@ -18,5 +23,5 @@ The best way to install all dependencies is to use `conda <http://conda.io/>`_.
1823

1924
.. code-block:: sh
2025
21-
conda install -c conda-forge xarray pip
26+
conda install -c conda-forge xarray f90nml pip
2227
pip install git+https://github.com/pyNEMO/pyDOMCFG.git

pydomcfg/accessor.py

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
1-
from typing import Any, Callable, TypeVar, cast
1+
import inspect
2+
import warnings
3+
from collections import ChainMap
4+
from functools import wraps
5+
from pathlib import Path
6+
from typing import IO, Any, Callable, TypeVar, Union, cast
27

38
import xarray as xr
49
from xarray import Dataset
510

611
from .domzgr.zco import Zco
712

13+
try:
14+
import f90nml
15+
16+
HAS_F90NML = True
17+
except ImportError:
18+
HAS_F90NML = False
19+
820
F = TypeVar("F", bound=Callable[..., Any])
21+
ZGR_MAPPER = {
22+
"ln_zco": Zco,
23+
# "ln_zps": TODO,
24+
# "ln_sco": TODO
25+
}
926

1027

1128
def _jpk_check(func: F) -> F:
1229
"""
1330
Decorator to raise an error if jpk was not set
1431
"""
1532

33+
@wraps(func)
1634
def wrapper(self, *args, **kwargs):
1735
if not self.jpk:
1836
raise ValueError(
@@ -29,7 +47,9 @@ class Accessor:
2947
def __init__(self, xarray_obj: Dataset):
3048
self._obj = xarray_obj
3149
self._jpk = 0
50+
self._nml_ref_path = ""
3251

52+
# Set attributes
3353
@property
3454
def jpk(self) -> int:
3555
"""
@@ -43,12 +63,112 @@ def jpk(self) -> int:
4363

4464
@jpk.setter
4565
def jpk(self, value: int):
46-
if value <= 0:
47-
raise ValueError("`jpk` MUST be > 0")
66+
if value < 0:
67+
raise ValueError("`jpk` MUST be >= 0 (use 0 to unset jpk)")
4868
self._jpk = value
4969

70+
@property
71+
def nml_ref_path(self) -> str:
72+
"""
73+
Path to reference namelist.
74+
75+
Returns
76+
-------
77+
str
78+
"""
79+
return self._nml_ref_path
80+
81+
@nml_ref_path.setter
82+
def nml_ref_path(self, value: str):
83+
self._nml_ref_path = value
84+
85+
# domzgr methods
86+
# TODO:
87+
# I think the process of creating the public API and doc
88+
# can be further automatized, but let's not put too much effort into it
89+
# until we settle on the back-end structure:
90+
# See: https://github.com/pyNEMO/pyDOMCFG/issues/45
5091
@_jpk_check
5192
def zco(self, *args: Any, **kwargs: Any) -> Dataset:
52-
return Zco(self._obj, self._jpk)(*args, **kwargs)
93+
name = inspect.stack()[0][3]
94+
return ZGR_MAPPER["ln_" + name](self._obj, self._jpk)(*args, **kwargs)
5395

5496
zco.__doc__ = Zco.__call__.__doc__
97+
98+
# Emulate NEMO DOMAINcfg tools
99+
def from_namelist(self, nml_cfg_path_or_io: Union[str, Path, IO[str]]) -> Dataset:
100+
"""
101+
Auto-populate pydomcfg parameters using NEMO DOMAINcfg namelists.
102+
103+
Parameters
104+
----------
105+
nml_cfg_path_or_io: str, Path, IO
106+
Path pointing to a namelist_cfg,
107+
or namelist_cfg previously opened with open()
108+
109+
Returns
110+
-------
111+
Dataset
112+
"""
113+
114+
nml_chained = self._namelist_parser(nml_cfg_path_or_io)
115+
zgr_initialized, kwargs = self._get_zgr_initialized_and_kwargs(nml_chained)
116+
return zgr_initialized(**kwargs)
117+
118+
def _namelist_parser(
119+
self, nml_cfg_path_or_io: Union[str, Path, IO[str]]
120+
) -> ChainMap:
121+
"""Parse namelists using f90nml, chaining all namblocks"""
122+
123+
if not HAS_F90NML:
124+
raise ImportError(
125+
"`f90nml` MUST be installed to use `obj.domcfg.from_namelist()`"
126+
)
127+
128+
if not self.nml_ref_path:
129+
raise ValueError(
130+
"Set `nml_ref_path` before calling `obj.domcfg.from_namelist()`"
131+
" For example: obj.domcfg.nml_ref_path = 'path/to/nml_ref'"
132+
)
133+
134+
if self.jpk:
135+
warnings.warn(
136+
"`obj.domcfg.jpk` is ignored. `jpk` is inferred from the namelists."
137+
)
138+
139+
# Read namelists: cfg overrides ref
140+
nml_cfg = f90nml.read(nml_cfg_path_or_io)
141+
nml = f90nml.patch(self.nml_ref_path, nml_cfg)
142+
143+
return ChainMap(*nml.todict().values())
144+
145+
def _get_zgr_initialized_and_kwargs(self, nml_chained: ChainMap):
146+
147+
# TODO: Add return type hint when abstraction in base class is implemented
148+
149+
# Pick the appropriate class
150+
zgr_classes = [
151+
value for key, value in ZGR_MAPPER.items() if nml_chained.get(key)
152+
]
153+
if len(zgr_classes) != 1:
154+
raise ValueError(
155+
"One and only one of the following variables MUST be `.true.`:"
156+
f" {tuple(ZGR_MAPPER)}"
157+
)
158+
zgr_class = zgr_classes[0]
159+
160+
# Compatibility with NEMO DOMAINcfg
161+
if nml_chained.get("ldbletanh") is False:
162+
for pp in ["ppa2", "ppkth2", "ppacr2"]:
163+
nml_chained[pp] = None
164+
165+
# Get kwargs, converting 999_999 to None
166+
parameters = list(inspect.signature(zgr_class.__call__).parameters)
167+
parameters.remove("self")
168+
kwargs = {
169+
key: None if nml_chained[key] == 999_999 else nml_chained[key]
170+
for key in parameters
171+
if key in nml_chained
172+
}
173+
174+
return zgr_class(self._obj, nml_chained["jpkdta"]), kwargs

0 commit comments

Comments
 (0)