Skip to content

Commit 033b6a1

Browse files
fix for dict support
1 parent 5bbd13b commit 033b6a1

File tree

2 files changed

+89
-27
lines changed

2 files changed

+89
-27
lines changed

pyamlo/config.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,28 @@ def load_config(
2323
config = merge_all_sources(sources, security_policy)
2424
config = process_overrides(config, overrides, use_cli)
2525
return Resolver(security_policy=security_policy).resolve(config)
26+
27+
28+
class Loader:
29+
"""A class to load and resolve configuration files."""
30+
31+
def __init__(
32+
self, security_policy: SecurityPolicy = SecurityPolicy(restrictive=False)
33+
):
34+
self.security_policy = security_policy
35+
36+
def load(
37+
self,
38+
source: Union[
39+
str, Path, IO[str], dict, Sequence[Union[str, Path, IO[str], dict]]
40+
],
41+
overrides: Optional[list[str]] = None,
42+
):
43+
sources = get_sources(source)
44+
config = merge_all_sources(sources, self.security_policy)
45+
config = process_overrides(config, overrides, False)
46+
return config
47+
48+
def resolve(self, config: dict) -> dict:
49+
"""Resolve a given configuration."""
50+
return Resolver(security_policy=self.security_policy).resolve(config)

pyamlo/sources.py

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,74 @@
44

55
from pyamlo.merge import deep_merge
66
from pyamlo.include import process_includes, set_base_paths
7-
from pyamlo.tags import ConfigLoader, CallSpec, ExtendSpec, PatchSpec
7+
from pyamlo.tags import ConfigLoader
88
from pyamlo.security import SecurityPolicy
99

1010

1111
def _process_dict_tags(data: Any, security_policy: SecurityPolicy) -> Any:
12-
"""Process YAML-style tags in dictionary data."""
13-
if isinstance(data, dict):
14-
return {k: _process_dict_tags(v, security_policy) for k, v in data.items()}
15-
elif isinstance(data, list):
16-
return [_process_dict_tags(item, security_policy) for item in data]
17-
elif isinstance(data, str):
18-
# Check for YAML tags in string values
19-
if data.startswith("!@"):
20-
# Object instantiation tag
21-
parts = data[2:].strip().split(None, 1)
22-
path = parts[0]
23-
args = [parts[1]] if len(parts) > 1 else []
24-
return CallSpec(path, args, {}, is_interpolated=False)
25-
elif data.startswith("!env "):
26-
# Environment variable tag
27-
var = data[5:].strip()
28-
security_policy.check_env_var(var)
29-
val = os.environ.get(var)
30-
if val is None:
31-
raise ValueError(f"Environment variable '{var}' not set")
32-
return val
33-
elif data == "!extend":
34-
return ExtendSpec([])
35-
elif data == "!patch":
36-
return PatchSpec({})
37-
return data
12+
"""Process dictionary data - handle both raw dicts and dicts with YAML tag strings."""
13+
14+
def has_yaml_tags(obj):
15+
"""Check if the data structure contains YAML tag-like strings."""
16+
if isinstance(obj, dict):
17+
return any(has_yaml_tags(v) for v in obj.values())
18+
elif isinstance(obj, list):
19+
return any(has_yaml_tags(item) for item in obj)
20+
elif isinstance(obj, str):
21+
return obj.startswith('!')
22+
return False
23+
24+
# If the dict contains no YAML tag strings, it's already fully resolved
25+
if not has_yaml_tags(data):
26+
return data
27+
28+
# Otherwise, process YAML tags using the YAML stream approach (simpler than direct parsing)
29+
from io import StringIO
30+
import yaml
31+
32+
def dict_to_yaml_text(obj, indent=0):
33+
"""Convert dict to YAML text without escaping tags."""
34+
def format_value(val):
35+
if isinstance(val, str):
36+
# Don't quote strings that are YAML tags
37+
if val.startswith('!'):
38+
return val
39+
# Quote strings to preserve them as strings
40+
dumped = yaml.safe_dump(val, default_flow_style=True).strip()
41+
# Remove document separators
42+
if dumped.endswith('\n...'):
43+
dumped = dumped[:-4]
44+
elif dumped.endswith('...'):
45+
dumped = dumped[:-3]
46+
return dumped
47+
return str(val)
48+
49+
if isinstance(obj, dict):
50+
lines = []
51+
for key, value in obj.items():
52+
if isinstance(value, dict):
53+
lines.append(" " * indent + f"{key}:")
54+
lines.append(dict_to_yaml_text(value, indent + 1))
55+
elif isinstance(value, list):
56+
lines.append(" " * indent + f"{key}:")
57+
for item in value:
58+
if isinstance(item, dict):
59+
lines.append(" " * (indent + 1) + "-")
60+
lines.append(dict_to_yaml_text(item, indent + 2))
61+
else:
62+
lines.append(" " * (indent + 1) + f"- {format_value(item)}")
63+
else:
64+
lines.append(" " * indent + f"{key}: {format_value(value)}")
65+
return '\n'.join(lines)
66+
return str(obj)
67+
68+
# Convert to YAML text and parse with ConfigLoader
69+
yaml_text = dict_to_yaml_text(data)
70+
loader = ConfigLoader(StringIO(yaml_text), security_policy=security_policy)
71+
try:
72+
return loader.get_single_data()
73+
finally:
74+
loader.dispose()
3875

3976

4077
def _load_source(

0 commit comments

Comments
 (0)