Skip to content

Commit 526483a

Browse files
committed
feature: Add config parsing
1 parent 64f78ab commit 526483a

File tree

9 files changed

+578
-13
lines changed

9 files changed

+578
-13
lines changed

pyproject.toml

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ description = "Simulation of radio skies to create astrophysical data sets"
44
readme = "README.rst"
55
requires-python = ">=3.11"
66
license = { text = "MIT" }
7-
authors = [
8-
{ name = "radionets Developers" },
9-
]
7+
authors = [{ name = "radionets Developers" }]
108
maintainers = [
119
{ name = "Kevin Schmitz", email = "kevin2.schmiz@tu-dortmund.de" },
1210
{ name = "Christian Arauner", email = "christian.arauner@tu-dortmund.de" },
@@ -49,6 +47,7 @@ radiosim = "radiosim.tools.cli:main"
4947

5048
[project.optional-dependencies]
5149
torch = ["torch", "torchvision"]
50+
ppdisks = ["fargopy", "tomllib", "tomli-w"]
5251

5352
[dependency-groups]
5453
dev = [
@@ -94,11 +93,7 @@ requires = ["hatch-vcs", "hatchling"]
9493
build-backend = "hatchling.build"
9594

9695
[tool.coverage.run]
97-
omit = [
98-
"docs/*",
99-
"src/radiosim/_version.py",
100-
"src/radiosim/version.py",
101-
]
96+
omit = ["docs/*", "src/radiosim/_version.py", "src/radiosim/version.py"]
10297

10398
[tool.coverage.xml]
10499
output = "coverage.xml"
@@ -125,12 +120,12 @@ extend-exclude = ["tests"]
125120

126121
[tool.ruff.lint]
127122
extend-select = [
128-
"I", # isort
129-
"E", # pycodestyle
130-
"F", # Pyflakes
123+
"I", # isort
124+
"E", # pycodestyle
125+
"F", # Pyflakes
131126
"UP", # pyupgrade
132-
"B", # flake8-bugbear
133-
"SIM", # flake8-simplify
127+
"B", # flake8-bugbear
128+
"SIM", # flake8-simplify
134129
]
135130
ignore = ["B905", "UP038"]
136131
fixable = ["ALL"]

radiosim/ppdisk/__init__.py

Whitespace-only changes.

radiosim/ppdisk/config/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .fargopy import FargopyConfiguration
2+
from .parser import Parser
3+
from .toml import TOMLConfiguration
4+
from .variables import Variables
5+
6+
__all__ = ["TOMLConfiguration", "FargopyConfiguration", "Variables", "Parser"]

radiosim/ppdisk/config/fargo.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import warnings
2+
from dataclasses import dataclass
3+
from pathlib import Path
4+
5+
import numpy as np
6+
7+
from radiosim.ppdisk.config import Parser, Variables
8+
9+
__all__ = ["FargoParameterConfig", "FargoParameterEntry"]
10+
11+
12+
@dataclass
13+
class FargoParameterEntry:
14+
key: str
15+
value: object
16+
comment: str
17+
18+
def get_line(self, max_key_len: int, max_value_len: int):
19+
return (
20+
f"{self.key:<{max_key_len + 2}}{self.value:<{max_value_len + 2}}"
21+
f"{self.comment if self.comment is not None else ''}\n"
22+
)
23+
24+
25+
class FargoParameterConfig:
26+
def __init__(self, setup: str, autosave: bool = False):
27+
self._path: Path = Variables.get("FARGO_ROOT") / f"setups/{setup}/{setup}.par"
28+
self._autosave: bool = autosave
29+
30+
if not self._path.exists():
31+
raise NameError(f"The given setup '{setup}' does not exist!")
32+
33+
self._parameters: dict[str, FargoParameterEntry] = dict()
34+
35+
self.load()
36+
if self._parameters["Setup"].value != setup:
37+
warnings.warn(
38+
"The given setup name exists but the 'Setup' parameter in the"
39+
" config gives a different name. A missmatch might lead to "
40+
"execution problems.",
41+
stacklevel=1,
42+
)
43+
44+
def _get_entries(self) -> list[FargoParameterEntry]:
45+
values = []
46+
for _key, value in self._parameters.items():
47+
if isinstance(value, dict):
48+
values.extend(list(value.values()))
49+
else:
50+
values.append(value)
51+
52+
return values
53+
54+
def load(self) -> None:
55+
with open(self._path) as file:
56+
lines = file.readlines()
57+
58+
current_category = None
59+
for line in lines:
60+
if line.strip() == "":
61+
continue
62+
63+
if line.startswith("### ") and "[" in line and "]" in line:
64+
current_category = (
65+
line.removeprefix("### ").split("[")[1].split("]")[0]
66+
)
67+
self._parameters[current_category] = dict()
68+
continue
69+
70+
components = line.split()
71+
72+
if len(components) < 2:
73+
continue
74+
75+
entry = FargoParameterEntry(
76+
key=components[0],
77+
value=Parser().parse(components[1]),
78+
comment=None if len(components) == 2 else " ".join(components[2:]),
79+
)
80+
81+
if current_category is None:
82+
self._parameters[components[0]] = entry
83+
else:
84+
self._parameters[current_category][components[0]] = entry
85+
86+
def save(self) -> None:
87+
with open(self._path) as file:
88+
old_content = file.read()
89+
with open(self._path, "w") as file:
90+
try:
91+
key_lens = []
92+
value_lens = []
93+
for entry in self._get_entries():
94+
key_lens.append(len(str(entry.key)))
95+
value_lens.append(len(str(entry.value)))
96+
97+
max_key_len = np.max(key_lens)
98+
max_value_len = np.max(value_lens)
99+
100+
lines = []
101+
102+
for key, entry in self._parameters.items():
103+
if isinstance(entry, dict):
104+
lines.append("\n")
105+
lines.append(f"### [{key}]\n")
106+
lines.append("\n")
107+
108+
for _subkey, subentry in self._parameters[key].items():
109+
lines.append(
110+
subentry.get_line(
111+
max_key_len=max_key_len, max_value_len=max_value_len
112+
)
113+
)
114+
else:
115+
lines.append(
116+
entry.get_line(
117+
max_key_len=max_key_len, max_value_len=max_value_len
118+
)
119+
)
120+
121+
file.writelines(lines)
122+
except Exception as e:
123+
warnings.warn(
124+
"An error occured while saving. Rolling back configuration files.",
125+
stacklevel=1,
126+
)
127+
file.write(old_content)
128+
raise e
129+
130+
def __getitem__(self, key: str) -> FargoParameterEntry:
131+
key_components = key.split(".")
132+
133+
match len(key_components):
134+
case 1:
135+
return self._parameters[key_components[0]]
136+
case 2:
137+
return self._parameters[key_components[0]][key_components[1]]
138+
case _:
139+
if len(key_components) > 2:
140+
raise KeyError(
141+
"The maximum depth of a config key is 2 (catgeory -> entry)!"
142+
)
143+
144+
def __setitem__(self, key: str, value: object) -> None:
145+
key_components = key.split(".")
146+
147+
match len(key_components):
148+
case 1:
149+
if isinstance(value, dict):
150+
self._parameters[key_components[0]] = value
151+
return None
152+
if isinstance(value, FargoParameterEntry):
153+
self._parameters[key_components[0]] = value
154+
elif isinstance(
155+
self._parameters[key_components[0]], FargoParameterEntry
156+
):
157+
self._parameters[key_components[0]].value = value
158+
else:
159+
raise TypeError(
160+
"Values at root level must either be a dict or a valid entry!"
161+
)
162+
case 2:
163+
if isinstance(value, FargoParameterEntry):
164+
self._parameters[key_components[0]][key_components[1]] = value
165+
elif isinstance(
166+
self._parameters[key_components[0]][key_components[1]],
167+
FargoParameterEntry,
168+
):
169+
self._parameters[key_components[0]][key_components[1]].value = value
170+
else:
171+
raise TypeError(
172+
"This key does not point to a valid entry! Enter an instance "
173+
"of a 'FargoParameterEntry'"
174+
)
175+
176+
case _:
177+
if len(key_components) > 2:
178+
raise KeyError(
179+
"The maximum depth of a config key is 2 (catgeory -> entry)!"
180+
)
181+
182+
if self._autosave:
183+
self.save()

radiosim/ppdisk/config/fargopy.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import shutil
2+
import subprocess
3+
from pathlib import Path
4+
5+
6+
def _parse_fpc_value(value: object) -> str:
7+
match value:
8+
case str():
9+
return f'"{value}"'
10+
case float() | int() | bool():
11+
return value
12+
case _:
13+
raise TypeError(
14+
f"The value '{value}' is not parsable for the fargopy config!"
15+
)
16+
17+
18+
class FargopyConfiguration:
19+
def __init__(self):
20+
self.path: Path = Path("~/.fargopy/fargopyrc").expanduser()
21+
22+
if not self.exists():
23+
self.reset()
24+
25+
def exists(self) -> bool:
26+
return self.path.is_file()
27+
28+
def get_content(self) -> dict:
29+
fargopyrc = dict()
30+
with open(self.path) as file:
31+
exec(file.read(), dict(), fargopyrc)
32+
return fargopyrc
33+
34+
def reset(self) -> None:
35+
try:
36+
import fargopy as fp
37+
38+
fp.initialize(options="configure")
39+
except Exception:
40+
print(
41+
"The fargopy configuration seems to be corrupted or is gone. "
42+
"Re-running initial fargopy configuration. This could take a moment..."
43+
)
44+
45+
if self.exists() or self.path.parent.exists():
46+
shutil.rmtree(self.path.parent)
47+
48+
subprocess.run(["ifargopy"], shell=True)
49+
50+
print("Finished regeneration.")
51+
52+
self["FP_FARGO3D_CLONECMD"] = "git clone https://github.com/FARGO3D/fargo3d.git"
53+
54+
def __getitem__(self, i: str):
55+
return self.get_content()[i]
56+
57+
def __setitem__(self, key: str, value: object):
58+
_ = self[key]
59+
with open(self.path) as f:
60+
content = f.readlines()
61+
62+
with open(self.path, "w") as f:
63+
for i in range(len(content)):
64+
if content[i].startswith(key):
65+
content[i] = f"{key} = {_parse_fpc_value(value)}\n"
66+
f.writelines(content)

radiosim/ppdisk/config/parser.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
__all__ = ["Parser"]
2+
3+
4+
class Parser:
5+
def __init__(self, bool_true: str = "yes", bool_false: str = "no"):
6+
self._type_parsers = [
7+
BoolParser(true_val=bool_true, false_val=bool_false),
8+
TypeParser(parse_type=int),
9+
TypeParser(parse_type=float),
10+
TypeParser(parse_type=str),
11+
]
12+
13+
def parse(self, value: str):
14+
for parser in self._type_parsers:
15+
try:
16+
return parser.parse(value=value)
17+
except Exception:
18+
continue
19+
20+
21+
class BoolParser:
22+
def __init__(self, true_val: str, false_val: str):
23+
self._true_val: str = true_val
24+
self._false_val: str = false_val
25+
26+
def parse(self, value: str) -> bool:
27+
true_check = value == self._true_val
28+
false_check = value == self._false_val
29+
30+
if true_check:
31+
return True
32+
if false_check:
33+
return False
34+
35+
if not true_check and not false_check:
36+
raise ValueError(
37+
f"Invalid boolean value: '{value}'! "
38+
f"Valid values: {self._true_val} (True) or {self._false_val} (False)"
39+
)
40+
41+
42+
class TypeParser:
43+
def __init__(self, parse_type: type):
44+
self._parse_type = parse_type
45+
46+
def parse(self, value: str):
47+
return self._parse_type(value)

0 commit comments

Comments
 (0)