Skip to content

Commit 61a2dc7

Browse files
authored
refactor and flesh out codec (#192)
misc cleanup, switch from strings/paths to file handles in load and write functions (more conventional), try json/toml for fun, rename "ascii" format -> "mf6" as it entails ascii files possibly referencing binary or netcdf files
1 parent 258526d commit 61a2dc7

File tree

8 files changed

+78
-42
lines changed

8 files changed

+78
-42
lines changed

docs/dev/sdd.md

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,7 @@ Input file IO is implemented in three layers:
135135

136136
The `flopy4.uio` module provides a pluggable IO framework adapted from [`astropy`](https://github.com/astropy/astropy/tree/main/astropy/io). A global `Registry` maintains mappings from `(component_class, format)` pairs to load and write functions. The `Component` base class implements user-facing `load` and `write` methods via descriptors which dispatch functions in the registry.
137137

138-
Loaders and writers can be registered for any component class and format. The registry supports inheritance: a loader/writer registered for a base class is available to all subclasses.
139-
140-
```python
141-
from flopy4.uio import DEFAULT_REGISTRY
142-
from flopy4.mf6.component import Component
143-
144-
DEFAULT_REGISTRY.register_writer(Component, "ascii", write_ascii)
145-
DEFAULT_REGISTRY.register_writer(Component, "netcdf", write_netcdf)
146-
```
147-
148-
The user may then select a format at call time, e.g. `component.write(format="netcdf")`.
138+
Loaders and writers can be registered for any component class and format. The registry supports inheritance: a loader/writer registered for a base class is available to all subclasses. The user may then select a format at call time.
149139

150140
#### Conversion
151141

flopy4/mf6/__init__.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,50 @@
1-
from flopy4.mf6 import ( # noqa: F401
2-
gwf,
3-
ims,
4-
simulation,
5-
tdis,
6-
)
7-
from flopy4.mf6.codec import dump
1+
from json import dump as dump_json
2+
from json import load as load_json
3+
from pathlib import Path
4+
5+
from tomli import load as load_toml
6+
from tomli_w import dump as dump_toml
7+
8+
from flopy4.mf6.codec import dump as dump_mf6
9+
from flopy4.mf6.codec import load as load_mf6
810
from flopy4.mf6.component import Component
9-
from flopy4.mf6.converter import COMPONENT_CONVERTER
11+
from flopy4.mf6.converter import structure, unstructure
1012
from flopy4.uio import DEFAULT_REGISTRY
1113

12-
# register io methods
13-
# TODO: call format "mf6" or something? since it might include binary files
14-
DEFAULT_REGISTRY.register_writer(
15-
Component, "ascii", lambda c: dump(COMPONENT_CONVERTER.unstructure(c), c.path)
16-
)
14+
15+
def _load_mf6(path: Path) -> Component:
16+
with open(path, "r") as fp:
17+
return structure(load_mf6(fp), path)
18+
19+
20+
def _load_json(path: Path) -> Component:
21+
with open(path, "r") as fp:
22+
return structure(load_json(fp), path)
23+
24+
25+
def _load_toml(path: Path) -> Component:
26+
with open(path, "rb") as fp:
27+
return structure(load_toml(fp), path)
28+
29+
30+
def _write_mf6(component: Component) -> None:
31+
with open(component.path, "w") as fp:
32+
dump_mf6(unstructure(component), fp)
33+
34+
35+
def _write_json(component: Component) -> None:
36+
with open(component.path, "w") as fp:
37+
dump_json(unstructure(component), fp, indent=4)
38+
39+
40+
def _write_toml(component: Component) -> None:
41+
with open(component.path, "wb") as fp:
42+
dump_toml(unstructure(component), fp)
43+
44+
45+
DEFAULT_REGISTRY.register_loader(Component, "mf6", _load_mf6)
46+
DEFAULT_REGISTRY.register_loader(Component, "json", _load_json)
47+
DEFAULT_REGISTRY.register_loader(Component, "toml", _load_toml)
48+
DEFAULT_REGISTRY.register_writer(Component, "mf6", _write_mf6)
49+
DEFAULT_REGISTRY.register_writer(Component, "json", _write_json)
50+
DEFAULT_REGISTRY.register_writer(Component, "toml", _write_toml)

flopy4/mf6/codec/reader/__init__.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
from os import PathLike
2-
from pathlib import Path
3-
from typing import Any
1+
from typing import IO, Any
42

53
from flopy4.mf6.codec.reader.parser import make_basic_parser
64
from flopy4.mf6.codec.reader.transformer import BasicTransformer
75

6+
BASIC_PARSER = make_basic_parser()
7+
BASIC_TRANSFORMER = BasicTransformer()
88

9-
def load(path: str | PathLike) -> Any:
9+
10+
def load(fp: IO[str]) -> Any:
1011
"""
1112
Load and parse an MF6 input file.
1213
@@ -20,10 +21,7 @@ def load(path: str | PathLike) -> Any:
2021
Any
2122
Parsed MF6 input file structure
2223
"""
23-
path = Path(path)
24-
with open(path, "r") as f:
25-
data = f.read()
26-
return loads(data)
24+
return loads(fp.read())
2725

2826

2927
def loads(data: str) -> Any:
@@ -41,6 +39,4 @@ def loads(data: str) -> Any:
4139
Parsed MF6 input file structure
4240
"""
4341

44-
parser = make_basic_parser()
45-
transformer = BasicTransformer()
46-
return transformer.transform(parser.parse(data))
42+
return BASIC_TRANSFORMER.transform(BASIC_PARSER.parse(data))

flopy4/mf6/codec/writer/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import sys
2-
from os import PathLike
2+
from typing import IO
33

44
import numpy as np
55
from jinja2 import Environment, PackageLoader
@@ -32,8 +32,8 @@ def dumps(data) -> str:
3232
return template.render(blocks=data)
3333

3434

35-
def dump(data, path: str | PathLike) -> None:
35+
def dump(data, fp: IO[str]) -> None:
3636
template = _JINJA_ENV.get_template(_JINJA_TEMPLATE_NAME)
3737
iterator = template.generate(blocks=data)
38-
with np.printoptions(**_PRINT_OPTIONS), open(path, "w") as f: # type: ignore
39-
f.writelines(iterator)
38+
with np.printoptions(**_PRINT_OPTIONS): # type: ignore
39+
fp.writelines(iterator)

flopy4/mf6/component.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,14 @@ class Component(ABC, MutableMapping):
8383
_load = IO(Loader) # type: ignore
8484
_write = IO(Writer) # type: ignore
8585

86-
filename: str = field(default=None)
86+
filename: str | None = field(default=None)
8787

8888
dfn: ClassVar[Dfn]
8989

9090
@property
9191
def path(self) -> Path:
9292
"""Get the path to the component's input file."""
93+
self.filename = self.filename or self.default_filename()
9394
return Path.cwd() / self.filename
9495

9596
def default_filename(self) -> str:

flopy4/mf6/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
22

3+
MF6 = "mf6"
34
FILL_DEFAULT = np.nan
45
FILL_DNODATA = 1e30

flopy4/mf6/context.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from xattree import xattree
66

77
from flopy4.mf6.component import Component
8+
from flopy4.mf6.constants import MF6
89
from flopy4.mf6.spec import field
910

1011

@@ -23,12 +24,13 @@ def __attrs_post_init__(self):
2324

2425
@property
2526
def path(self) -> Path:
27+
self.filename = self.filename or self.default_filename()
2628
return self.workspace / self.filename
2729

28-
def load(self, format="ascii"):
30+
def load(self, format=MF6):
2931
with cd(self.workspace):
3032
super().load(format=format)
3133

32-
def write(self, format="ascii"):
34+
def write(self, format=MF6):
3335
with cd(self.workspace):
3436
super().write(format=format)

flopy4/mf6/converter.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,15 @@ def final(arr):
326326
set_(a, v, nn)
327327

328328
return final(a)
329+
330+
331+
def structure(data: dict[str, Any], path: Path) -> Component:
332+
component = COMPONENT_CONVERTER.structure(data, Component)
333+
if isinstance(component, Context):
334+
component.workspace = path.parent
335+
component.filename = path.name
336+
return component
337+
338+
339+
def unstructure(component: Component) -> dict[str, Any]:
340+
return COMPONENT_CONVERTER.unstructure(component)

0 commit comments

Comments
 (0)