Skip to content

Commit 0503342

Browse files
authored
Merge branch 'main' into fillna
2 parents 5950e2d + 4d423c6 commit 0503342

File tree

6 files changed

+332
-377
lines changed

6 files changed

+332
-377
lines changed

docs/user-guide/pfs.qmd

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,16 @@ pfs = mikeio.PfsDocument({"MYTOOL": d})
180180
pfs
181181
```
182182

183-
Multiple targets can be achieved by providing list of dictionaries, in this way you can create a PFS file with multiple targets for the same tool.
183+
Multiple targets can be achieved by providing a list of `mikeio.PfsSection`, in this way you can create a PFS file with multiple targets for the same tool.
184184

185185
```{python}
186-
t1 = {"file_name": r"|path\file1.dfs0|"}
187-
t2 = {"file_name": r"|path\file2.dfs0|"}
188186
189-
pfs = mikeio.PfsDocument([t1, t2], names=["ATOOL", "ATOOL"])
187+
data = [
188+
mikeio.PfsSection({"ATOOL": {"file_name": r"|path\file1.dfs0|"}}),
189+
mikeio.PfsSection({"ATOOL": {"file_name": r"|path\file2.dfs0|"}}),
190+
]
191+
192+
pfs = mikeio.PfsDocument(data)
190193
pfs
191194
```
192195

mikeio/pfs/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
from __future__ import annotations
22
from pathlib import Path
3-
from typing import TextIO
43
from ._pfsdocument import PfsDocument
54
from ._pfssection import PfsNonUniqueList, PfsSection
65

76

87
def read_pfs(
9-
filename: str | Path | TextIO | dict | PfsSection,
8+
filename: str | Path,
109
encoding: str = "cp1252",
1110
unique_keywords: bool = False,
1211
) -> PfsDocument:

mikeio/pfs/_pfsdocument.py

Lines changed: 73 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import re
33
import warnings
44
from collections import Counter
5-
from collections.abc import Mapping, Sequence
5+
from collections.abc import Mapping
66
from datetime import datetime
77
from pathlib import Path
8-
from typing import Any, Callable, TextIO
8+
from typing import Any, Callable, Sequence, TextIO
99

1010
import yaml
1111

@@ -88,23 +88,24 @@ class PfsDocument(PfsSection):
8888

8989
def __init__(
9090
self,
91-
data: TextIO | PfsSection | Mapping[str | PfsSection, Any] | str | Path,
91+
data: TextIO
92+
| Mapping[str | PfsSection, Any]
93+
| Sequence[PfsSection]
94+
| str
95+
| Path,
9296
*,
9397
encoding: str = "cp1252",
94-
names: Sequence[str] | None = None,
9598
unique_keywords: bool = False,
9699
) -> None:
97100
if isinstance(data, (str, Path)) or hasattr(data, "read"):
98-
if names is not None:
99-
raise ValueError("names cannot be given as argument if input is a file")
100101
names, sections = self._read_pfs_file(data, encoding, unique_keywords) # type: ignore
101102
else:
102-
names, sections = self._parse_non_file_input(data, names)
103+
names, sections = self._parse_non_file_input(data)
103104

104105
d = self._to_nonunique_key_dict(names, sections)
105106
super().__init__(d)
106107

107-
self._ALIAS_LIST = ["_ALIAS_LIST"] # ignore these in key list
108+
self._ALIAS_LIST = set(["_ALIAS_LIST"]) # ignore these in key list
108109
if self._is_FM_engine:
109110
self._add_all_FM_aliases()
110111

@@ -204,52 +205,31 @@ def _read_pfs_file(
204205
raise FileNotFoundError(str(e))
205206
except Exception as e:
206207
raise ValueError(f"{filename} could not be parsed. " + str(e))
207-
sections = [PfsSection(list(d.values())[0]) for d in target_list] # type: ignore
208-
names = [list(d.keys())[0] for d in target_list] # type: ignore
209-
return names, sections
208+
return PfsDocument._extract_names_from_list(target_list) # type: ignore
209+
210+
@staticmethod
211+
def _extract_names_from_list(
212+
targets: Sequence[PfsSection],
213+
) -> tuple[list[str], list[PfsSection]]:
214+
names, sections = zip(
215+
*((k, PfsSection(v)) for target in targets for k, v in target.items())
216+
)
217+
return list(names), list(sections)
210218

211219
@staticmethod
212220
def _parse_non_file_input(
213-
input: (
214-
Mapping[str | PfsSection, Any]
215-
| PfsSection
216-
| Sequence[PfsSection]
217-
| Sequence[dict]
218-
),
219-
names: Sequence[str] | None = None,
220-
) -> tuple[Sequence[str], list[PfsSection]]:
221-
"""dict/PfsSection or lists of these can be parsed."""
222-
if names is None:
223-
assert isinstance(input, Mapping), "input must be a mapping"
224-
names, sections = PfsDocument._unravel_items(input.items)
225-
for sec in sections:
226-
assert isinstance(
227-
sec, Mapping
228-
), "all targets must be PfsSections/dict (no key-value pairs allowed in the root)"
229-
return names, sections
230-
231-
if isinstance(names, str):
232-
names = [names]
233-
234-
if isinstance(input, PfsSection):
235-
sections = [input]
236-
elif isinstance(input, dict):
237-
sections = [PfsSection(input)]
238-
elif isinstance(input, Sequence):
239-
if isinstance(input[0], PfsSection):
240-
sections = input # type: ignore
241-
elif isinstance(input[0], dict):
242-
sections = [PfsSection(d) for d in input]
243-
else:
244-
raise ValueError("List input must contain either dict or PfsSection")
245-
else:
246-
raise ValueError(
247-
f"Input of type ({type(input)}) could not be parsed (pfs file, dict, PfsSection, lists of dict or PfsSection)"
248-
)
249-
if len(names) != len(sections):
250-
raise ValueError(
251-
f"Length of names ({len(names)}) does not match length of target sections ({len(sections)})"
252-
)
221+
input: Mapping[str | PfsSection, Any] | Sequence[PfsSection],
222+
) -> tuple[list[str], list[PfsSection]]:
223+
if isinstance(input, Sequence):
224+
return PfsDocument._extract_names_from_list(input)
225+
226+
assert isinstance(input, Mapping), "input must be a mapping"
227+
names, sections = PfsDocument._unravel_items(input.items)
228+
for sec in sections:
229+
if not isinstance(sec, Mapping):
230+
raise ValueError(
231+
"all targets must be PfsSections/dict (no key-value pairs allowed in the root)"
232+
)
253233
return names, sections
254234

255235
@property
@@ -258,36 +238,38 @@ def _is_FM_engine(self) -> bool:
258238

259239
def _add_all_FM_aliases(self) -> None:
260240
"""create MIKE FM module aliases."""
261-
self._add_FM_alias("HD", "HYDRODYNAMIC_MODULE")
262-
self._add_FM_alias("SW", "SPECTRAL_WAVE_MODULE")
263-
self._add_FM_alias("TR", "TRANSPORT_MODULE")
264-
self._add_FM_alias("MT", "MUD_TRANSPORT_MODULE")
265-
self._add_FM_alias("EL", "ECOLAB_MODULE")
266-
self._add_FM_alias("ST", "SAND_TRANSPORT_MODULE")
267-
self._add_FM_alias("PT", "PARTICLE_TRACKING_MODULE")
268-
self._add_FM_alias("DA", "DATA_ASSIMILATION_MODULE")
241+
ALIASES = {
242+
"HD": "HYDRODYNAMIC_MODULE",
243+
"SW": "SPECTRAL_WAVE_MODULE",
244+
"TR": "TRANSPORT_MODULE",
245+
"MT": "MUD_TRANSPORT_MODULE",
246+
"EL": "ECOLAB_MODULE",
247+
"ST": "SAND_TRANSPORT_MODULE",
248+
"PT": "PARTICLE_TRACKING_MODULE",
249+
"DA": "DATA_ASSIMILATION_MODULE",
250+
}
251+
for alias, module in ALIASES.items():
252+
self._add_FM_alias(alias, module)
269253

270254
def _add_FM_alias(self, alias: str, module: str) -> None:
271255
"""Add short-hand alias for MIKE FM module, e.g. SW, but only if active!"""
272-
if hasattr(self.targets[0], module) and hasattr(
273-
self.targets[0], "MODULE_SELECTION"
274-
):
256+
target = self.targets[0]
257+
if hasattr(target, module) and hasattr(target, "MODULE_SELECTION"):
275258
mode_name = f"mode_of_{module.lower()}"
276-
mode_of = int(self.targets[0].MODULE_SELECTION.get(mode_name, 0))
259+
mode_of = int(target.MODULE_SELECTION.get(mode_name, 0))
277260
if mode_of > 0:
278-
setattr(self, alias, self.targets[0][module])
279-
self._ALIAS_LIST.append(alias)
261+
setattr(self, alias, target[module])
262+
self._ALIAS_LIST.add(alias)
280263

281264
def _pfs2yaml(
282265
self, filename: str | Path | TextIO, encoding: str | None = None
283266
) -> str:
284267
if hasattr(filename, "read"): # To read in memory strings StringIO
285268
pfsstring = filename.read()
286269
else:
287-
with open(filename, encoding=encoding) as f:
288-
pfsstring = f.read()
270+
pfsstring = Path(filename).read_text(encoding=encoding)
289271

290-
lines = pfsstring.split("\n")
272+
lines = pfsstring.splitlines()
291273

292274
output = []
293275
output.append("---")
@@ -303,7 +285,16 @@ def _pfs2yaml(
303285
def _parse_line(self, line: str, level: int = 0) -> tuple[str, int]:
304286
section_header = False
305287
s = line.strip()
306-
s = re.sub(r"\s*//.*", "", s) # remove comments
288+
parts = re.split(r'(".*?"|\'.*?\')', s) # Preserve quoted strings
289+
for i, part in enumerate(parts):
290+
if not (
291+
part.startswith('"') or part.startswith("'")
292+
): # Ignore quoted parts
293+
part = re.sub(
294+
r"\s*//.*", "", part
295+
) # Remove comments only outside quotes
296+
parts[i] = part
297+
s = "".join(parts) # Reassemble the line
307298

308299
if len(s) > 0:
309300
if s[0] == "[":
@@ -317,7 +308,7 @@ def _parse_line(self, line: str, level: int = 0) -> tuple[str, int]:
317308
if s[-1] == "]":
318309
s = s.replace("]", ":")
319310

320-
s = s.replace("//", "")
311+
# s = s.replace("//", "")
321312
s = s.replace("\t", " ")
322313

323314
if len(s) > 0 and s[0] != "!":
@@ -370,21 +361,21 @@ def _parse_token(self, token: str, context: str = "") -> str:
370361
# Example of complicated string:
371362
# '<CLOB:22,1,1,false,1,0,"",0,"",0,"",0,"",0,"",0,"",0,"",0,"",||,false>'
372363
if s.count("|") == 2 and "CLOB" not in context:
373-
parts = s.split("|")
374-
if len(parts[1]) > 1 and parts[1].count("'") > 0:
364+
prefix, content, suffix = s.split("|")
365+
if len(content) > 1 and content.count("'") > 0:
375366
# string containing single quotes that needs escaping
376367
warnings.warn(
377368
f"The string {s} contains a single quote character which will be temporarily converted to \U0001f600 . If you write back to a pfs file again it will be converted back."
378369
)
379-
parts[1] = parts[1].replace("'", "\U0001f600")
380-
s = parts[0] + "'|" + parts[1] + "|'" + parts[2]
370+
content = content.replace("'", "\U0001f600")
371+
s = f"{prefix}'|{content}|'{suffix}"
381372

382373
if len(s) > 2: # ignore foo = ''
383374
s = s.replace("''", '"')
384375

385376
return s
386377

387-
def write(self, filename: str) -> None:
378+
def write(self, filename: str | Path) -> None:
388379
"""Write object to a pfs file.
389380
390381
Parameters
@@ -399,19 +390,12 @@ def write(self, filename: str) -> None:
399390
"""
400391
from mikeio import __version__ as mikeio_version
401392

402-
# if filename is None:
403-
# return self._to_txt_lines()
404-
405-
with open(filename, "w") as f:
406-
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
407-
f.write(f"// Created : {now}\n")
408-
f.write(r"// By : MIKE IO")
409-
f.write("\n")
410-
f.write(rf"// Version : {mikeio_version}")
411-
f.write("\n\n")
412-
413-
self._write_with_func(f.write, level=0)
393+
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
394+
header = f"""// Created : {now}
395+
// By : MIKE IO
396+
// Version : {mikeio_version}
414397
398+
"""
399+
txt = header + "\n".join(self._to_txt_lines())
415400

416-
# TODO remove this alias
417-
Pfs = PfsDocument
401+
Path(filename).write_text(txt)

0 commit comments

Comments
 (0)