Skip to content

Commit d9757bb

Browse files
committed
Parse items in value lists. Make parser for variables.
1 parent b521ed7 commit d9757bb

File tree

4 files changed

+171
-61
lines changed

4 files changed

+171
-61
lines changed

src/dj_toml_settings/toml_parser.py

Lines changed: 41 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
from dateutil import parser as dateparser
1010
from typeguard import typechecked
1111

12-
from dj_toml_settings.value_parsers.dict_value_parsers import (
13-
EnvValueParser,
14-
InsertValueParser,
15-
NoneValueParser,
16-
PathValueParser,
17-
TypeValueParser,
18-
ValueValueParser,
12+
from dj_toml_settings.value_parsers.dict_parsers import (
13+
EnvParser,
14+
InsertParser,
15+
NoneParser,
16+
PathParser,
17+
TypeParser,
18+
ValueParser,
1919
)
20+
from dj_toml_settings.value_parsers.str_parsers import VariableParser
2021

2122
logger = logging.getLogger(__name__)
2223

@@ -65,14 +66,14 @@ def parse_file(self):
6566
for key, value in toml_data.items():
6667
logger.debug(f"tool.django: Update '{key}' with '{value}'")
6768

68-
self.data.update(self.parse_key_value(key, value))
69+
self.data.update({key: self.parse_value(key, value)})
6970

7071
# Add settings from `tool.django.apps.*`
7172
for apps_name, apps_value in apps_data.items():
7273
for app_key, app_value in apps_value.items():
7374
logger.debug(f"tool.django.apps.{apps_name}: Update '{app_key}' with '{app_value}'")
7475

75-
self.data.update(self.parse_key_value(app_key, app_value))
76+
self.data.update({app_key: self.parse_value(app_key, app_value)})
7677

7778
# Add settings from `tool.django.envs.*` if it matches the `ENVIRONMENT` env variable
7879
if environment_env_variable := os.getenv("ENVIRONMENT"):
@@ -81,7 +82,7 @@ def parse_file(self):
8182
for env_key, env_value in envs_value.items():
8283
logger.debug(f"tool.django.envs.{envs_name}: Update '{env_key}' with '{env_value}'")
8384

84-
self.data.update(self.parse_key_value(env_key, env_value))
85+
self.data.update({env_key: self.parse_value(env_key, env_value)})
8586

8687
return self.data
8788

@@ -101,7 +102,7 @@ def get_data(self) -> dict:
101102
return data.get("tool", {}).get("django", {}) or {}
102103

103104
@typechecked
104-
def parse_key_value(self, key: str, value: Any) -> dict:
105+
def parse_value(self, key: Any, value: Any) -> Any:
105106
"""Handle special cases for `value`.
106107
107108
Special cases:
@@ -116,13 +117,33 @@ def parse_key_value(self, key: str, value: Any) -> dict:
116117
- `datetime`
117118
"""
118119

119-
if isinstance(value, dict):
120-
type_parser = TypeValueParser(data=self.data, value=value)
121-
env_parser = EnvValueParser(data=self.data, value=value)
122-
path_parser = PathValueParser(data=self.data, value=value, path=self.path)
123-
value_parser = ValueValueParser(data=self.data, value=value)
124-
none_parser = NoneValueParser(data=self.data, value=value)
125-
insert_parser = InsertValueParser(data=self.data, value=value, data_key=key)
120+
if isinstance(value, list):
121+
processed_list = []
122+
123+
for item in value:
124+
processed_item = self.parse_value(key, item)
125+
processed_list.append(processed_item)
126+
127+
value = processed_list
128+
elif isinstance(value, dict):
129+
# Process nested dictionaries
130+
processed_dict = {}
131+
132+
for k, v in value.items():
133+
if isinstance(v, dict):
134+
processed_dict.update({k: self.parse_value(key, v)})
135+
else:
136+
processed_dict[k] = v
137+
138+
if processed_dict:
139+
value = processed_dict
140+
141+
type_parser = TypeParser(data=self.data, value=value)
142+
env_parser = EnvParser(data=self.data, value=value)
143+
path_parser = PathParser(data=self.data, value=value, path=self.path)
144+
value_parser = ValueParser(data=self.data, value=value)
145+
none_parser = NoneParser(data=self.data, value=value)
146+
insert_parser = InsertParser(data=self.data, value=value, data_key=key)
126147

127148
# Check for a match for all specials (except $type)
128149
for parser in [env_parser, path_parser, value_parser, insert_parser, none_parser]:
@@ -134,42 +155,8 @@ def parse_key_value(self, key: str, value: Any) -> dict:
134155
if type_parser.match():
135156
value = type_parser.parse(value)
136157
elif isinstance(value, str):
137-
# Handle variable substitution
138-
for match in re.finditer(r"\$\{\w+\}", value):
139-
data_key = value[match.start() : match.end()][2:-1]
140-
141-
if variable := self.data.get(data_key):
142-
if isinstance(variable, Path):
143-
path_str = combine_bookends(value, match, variable)
144-
145-
value = Path(path_str)
146-
elif callable(variable):
147-
value = variable
148-
elif isinstance(variable, int):
149-
value = combine_bookends(value, match, variable)
150-
151-
try:
152-
value = int(value)
153-
except Exception: # noqa: S110
154-
pass
155-
elif isinstance(variable, float):
156-
value = combine_bookends(value, match, variable)
157-
158-
try:
159-
value = float(value)
160-
except Exception: # noqa: S110
161-
pass
162-
elif isinstance(variable, list):
163-
value = variable
164-
elif isinstance(variable, dict):
165-
value = variable
166-
elif isinstance(variable, datetime):
167-
value = dateparser.isoparse(str(variable))
168-
else:
169-
value = value.replace(match.string, str(variable))
170-
else:
171-
logger.warning(f"Missing variable substitution {value}")
158+
value = VariableParser(data=self.data, value=value).parse()
172159
elif isinstance(value, datetime):
173160
value = dateparser.isoparse(str(value))
174161

175-
return {key: value}
162+
return value

src/dj_toml_settings/value_parsers/dict_value_parsers.py renamed to src/dj_toml_settings/value_parsers/dict_parsers.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from dj_toml_settings.exceptions import InvalidActionError
88

99

10-
class DictValueParser:
10+
class DictParser:
1111
data: dict
1212
value: str
1313

@@ -43,7 +43,7 @@ def parse(self):
4343
raise NotImplementedError("parse() not implemented")
4444

4545

46-
class EnvValueParser(DictValueParser):
46+
class EnvParser(DictParser):
4747
key: str = "env"
4848

4949
def parse(self) -> Any:
@@ -56,7 +56,7 @@ def parse(self) -> Any:
5656
return value
5757

5858

59-
class PathValueParser(DictValueParser):
59+
class PathParser(DictParser):
6060
key: str = "path"
6161

6262
def __init__(self, data: dict, value: str, path: Path):
@@ -83,14 +83,14 @@ def resolve_file_name(self) -> Path:
8383
return (current_path / self.file_name).resolve()
8484

8585

86-
class ValueValueParser(DictValueParser):
86+
class ValueParser(DictParser):
8787
key = "value"
8888

8989
def parse(self) -> Any:
9090
return self.value[self.key]
9191

9292

93-
class InsertValueParser(DictValueParser):
93+
class InsertParser(DictParser):
9494
key = "insert"
9595

9696
def __init__(self, data: dict, value: str, data_key: str):
@@ -113,7 +113,7 @@ def parse(self) -> Any:
113113
return insert_data
114114

115115

116-
class NoneValueParser(DictValueParser):
116+
class NoneParser(DictParser):
117117
key = "none"
118118

119119
def match(self) -> bool:
@@ -123,7 +123,7 @@ def parse(self) -> Any:
123123
return None
124124

125125

126-
class TypeValueParser(DictValueParser):
126+
class TypeParser(DictParser):
127127
key = "type"
128128

129129
def parse(self, resolved_value: Any) -> Any:
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import logging
2+
import re
3+
from datetime import datetime
4+
from pathlib import Path
5+
from typing import Any
6+
7+
from dateutil import parser as dateparser
8+
from typeguard import typechecked
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
@typechecked
14+
def combine_bookends(original: str, match: re.Match, middle: Any) -> str:
15+
"""Get the beginning of the original string before the match, and the
16+
end of the string after the match and smush the replaced value in between
17+
them to generate a new string.
18+
"""
19+
20+
start_idx = match.start()
21+
start = original[:start_idx]
22+
23+
end_idx = match.end()
24+
ending = original[end_idx:]
25+
26+
return start + str(middle) + ending
27+
28+
29+
class VariableParser:
30+
data: dict
31+
value: str
32+
33+
def __init__(self, data: dict, value: str):
34+
self.data = data
35+
self.value = value
36+
37+
def parse(self) -> Any:
38+
value = self.value
39+
40+
for match in re.finditer(r"\$\{\w+\}", value):
41+
data_key = value[match.start() : match.end()][2:-1]
42+
43+
if variable := self.data.get(data_key):
44+
if isinstance(variable, Path):
45+
path_str = combine_bookends(value, match, variable)
46+
47+
value = Path(path_str)
48+
elif callable(variable):
49+
value = variable
50+
elif isinstance(variable, int):
51+
value = combine_bookends(value, match, variable)
52+
53+
try:
54+
value = int(value)
55+
except Exception: # noqa: S110
56+
pass
57+
elif isinstance(variable, float):
58+
value = combine_bookends(value, match, variable)
59+
60+
try:
61+
value = float(value)
62+
except Exception: # noqa: S110
63+
pass
64+
elif isinstance(variable, list):
65+
value = variable
66+
elif isinstance(variable, dict):
67+
value = variable
68+
elif isinstance(variable, datetime):
69+
value = dateparser.isoparse(str(variable))
70+
else:
71+
value = value.replace(match.string, str(variable))
72+
else:
73+
logger.warning(f"Missing variable substitution {value}")
74+
75+
return value

tests/test_toml_parser/test_parse_file.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,54 @@ def test_env(tmp_path, monkeypatch):
182182
assert expected == actual
183183

184184

185+
def test_env_in_nested_dict(tmp_path, monkeypatch):
186+
monkeypatch.setenv("SOME_VAR", "blob")
187+
188+
expected = {"SOMETHING": {"MORE": "blob"}}
189+
190+
path = tmp_path / "pyproject.toml"
191+
path.write_text("""
192+
[tool.django]
193+
SOMETHING = { MORE = { $env = "SOME_VAR" } }
194+
""")
195+
196+
actual = Parser(path).parse_file()
197+
198+
assert expected == actual
199+
200+
201+
def test_env_in_list(tmp_path, monkeypatch):
202+
monkeypatch.setenv("SOME_VAR", "blob")
203+
204+
expected = {"SOMETHING": ["blob"]}
205+
206+
path = tmp_path / "pyproject.toml"
207+
path.write_text("""
208+
[tool.django]
209+
SOMETHING = [{ "$env" = "SOME_VAR"}]
210+
""")
211+
212+
actual = Parser(path).parse_file()
213+
214+
assert expected == actual
215+
216+
217+
def test_variable_in_list(tmp_path):
218+
expected = {"SOMETHING": ["blob"], "SOME_VAR": "blob"}
219+
220+
path = tmp_path / "pyproject.toml"
221+
path.write_text("""
222+
[tool.django]
223+
SOME_VAR = "blob"
224+
SOMETHING = ["${SOME_VAR}"]
225+
""")
226+
227+
actual = Parser(path).parse_file()
228+
print(actual)
229+
230+
assert expected == actual
231+
232+
185233
def test_env_quoted_key(tmp_path, monkeypatch):
186234
monkeypatch.setenv("SOME_VAR", "blob")
187235

0 commit comments

Comments
 (0)