Skip to content

Commit f177463

Browse files
Check of cycles in configuration files.
1 parent 8200720 commit f177463

File tree

4 files changed

+109
-29
lines changed

4 files changed

+109
-29
lines changed

pyaml/configuration/fileloader.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,43 +15,49 @@
1515

1616
logger = logging.getLogger(__name__)
1717

18-
#TODO
19-
#Implement cycle detection in case of wrong yaml/json link that creates a cycle
18+
accepted_suffixes = [".yaml", ".yml", ".json"]
2019

21-
def load(filename:str) -> Union[dict,list]:
20+
def load(filename:str, paths_stack:list=None) -> Union[dict,list]:
2221
"""Load recursively a configuration setup"""
2322
if filename.endswith(".yaml") or filename.endswith(".yml"):
24-
l = YAMLLoader(filename)
23+
l = YAMLLoader(filename, paths_stack)
2524
elif filename.endswith(".json"):
26-
l = JSONLoader(filename)
25+
l = JSONLoader(filename, paths_stack)
2726
else:
2827
raise PyAMLException(f"{filename} File format not supported (only .yaml .yml or .json)")
29-
return l.load(filename)
28+
return l.load()
29+
30+
# Expand condition
31+
def hasToExpand(value):
32+
return isinstance(value,str) and any(value.endswith(suffix) for suffix in accepted_suffixes)
33+
3034

3135
# Loader base class (nested files expansion)
3236
class Loader:
3337

34-
def __init__(self, filename:str):
35-
self.suffixes = []
38+
def __init__(self, filename:str, parent_path_stack:list):
3639
self.path:Path = get_root_folder() / filename
40+
self.files_stack = []
41+
if parent_path_stack:
42+
if any(self.path.samefile(parent_path) for parent_path in parent_path_stack):
43+
raise PyAMLException(f"A cycle has been detected: {parent_path_stack}")
44+
self.files_stack.extend(parent_path_stack)
45+
self.files_stack.append(self.path)
3746

38-
# Expand condition
39-
def hasToExpand(self,value):
40-
return isinstance(value,str) and any(value.endswith(suffix) for suffix in self.suffixes)
4147

4248
# Recursively expand a dict
4349
def expand_dict(self,d:dict):
4450
for key, value in d.items():
45-
if self.hasToExpand(value):
46-
d[key] = load(value)
51+
if hasToExpand(value):
52+
d[key] = load(value, self.files_stack)
4753
else:
4854
self.expand(value)
4955

5056
# Recursively expand a list
5157
def expand_list(self,l:list):
5258
for idx,value in enumerate(l):
53-
if self.hasToExpand(value):
54-
l[idx] = load(value)
59+
if hasToExpand(value):
60+
l[idx] = load(value, self.files_stack)
5561
else:
5662
self.expand(value)
5763

@@ -65,7 +71,7 @@ def expand(self,obj: Union[dict,list]):
6571

6672
# Load a file
6773
def load(self) -> Union[dict,list]:
68-
raise Exception(str(self.path) + ": load() method not implemented")
74+
raise PyAMLException(str(self.path) + ": load() method not implemented")
6975

7076
class SafeLineLoader(SafeLoader):
7177

@@ -93,22 +99,24 @@ def construct_mapping(self, node, deep=False):
9399

94100
# YAML loader
95101
class YAMLLoader(Loader):
96-
97-
def load(self,fileName:str) -> Union[dict,list]:
98-
self.path:Path = get_root_folder() / fileName
99-
self.suffixes = [".yaml",".yml"]
102+
def __init__(self, filename: str, parent_paths_stack:list):
103+
super().__init__(filename, parent_paths_stack)
104+
105+
def load(self) -> Union[dict,list]:
106+
logger.log(logging.DEBUG, f"Loading YAML file '{self.path}'")
100107
with open(self.path) as file:
101108
try:
102109
return self.expand(yaml.load(file,Loader=SafeLineLoader))
103110
except yaml.YAMLError as e:
104-
raise Exception(self.path + ": " + str(e))
111+
raise PyAMLException(str(self.path) + ": " + str(e)) from e
105112

106113
# JSON loader
107114
class JSONLoader(Loader):
115+
def __init__(self, filename: str, parent_paths_stack:list):
116+
super().__init__(filename, parent_paths_stack)
108117

109-
def load(self,fileName:str) -> Union[dict,list]:
118+
def load(self) -> Union[dict,list]:
110119
logger.log(logging.DEBUG, f"Loading JSON file '{self.path}'")
111-
self.suffixes = [".json"]
112120
with open(self.path) as file:
113121
try:
114122
return self.expand(json.load(file))

tests/config/bad_conf_cycles.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"type": "pyaml.pyaml",
3+
"instruments":[
4+
{
5+
"type": "pyaml.instrument",
6+
"name": "sr",
7+
"energy": 6e9,
8+
"simulators": ["../config/bad_conf_cycles.json"],
9+
"data_folder": "/data/store",
10+
"arrays": [
11+
{
12+
"type": "pyaml.arrays.hcorrector",
13+
"name": "HCORR",
14+
"elements": [
15+
"SH1A-C01-H",
16+
"SH1A-C02-H"
17+
]
18+
},
19+
{
20+
"type": "pyaml.arrays.vcorrector",
21+
"name": "VCORR",
22+
"elements": ["SH1A-C01-V", "SH1A-C02-V"]
23+
}
24+
],
25+
"devices": [
26+
"sr/quadrupoles/QF1AC01.yaml",
27+
"sr/correctors/SH1AC01.yaml",
28+
"sr/correctors/SH1AC02.yaml"
29+
]
30+
}
31+
]
32+
}

tests/config/bad_conf_cycles.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
type: pyaml.pyaml
2+
instruments:
3+
- type: pyaml.instrument
4+
name: sr
5+
energy: 6e9
6+
simulators:
7+
- type: pyaml.lattice.simulator
8+
lattice: sr/lattices/ebs.mat
9+
name: design
10+
data_folder: /data/store
11+
arrays:
12+
- type: pyaml.arrays.hcorrector
13+
name: HCORR
14+
elements:
15+
- SH1A-C01-H
16+
- SH1A-C02-H
17+
- type: pyaml.arrays.vcorrector
18+
name: VCORR
19+
elements:
20+
- SH1A-C01-V
21+
- SH1A-C02-V
22+
devices:
23+
- ../config/bad_conf_cycles.yml # Cycle here
24+
- sr/quadrupoles/QF1AC01.yaml
25+
- sr/correctors/SH1AC01.yaml
26+
- sr/correctors/SH1AC02.yaml

tests/test_factory.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pytest
2-
from pyaml import PyAMLConfigException
2+
from pyaml import PyAMLConfigException, PyAMLException
33
from pyaml.configuration.factory import Factory
44
from pyaml.pyaml import PyAML, pyaml
55
from tests.conftest import MockElement
@@ -28,9 +28,23 @@ def test_factory_with_custom_strategy():
2828
assert obj.name == "custom_injected"
2929

3030

31-
def test_error_location():
32-
31+
@pytest.mark.parametrize("test_file", [
32+
"tests/config/bad_conf.yml",
33+
])
34+
def test_error_location(test_file):
3335
with pytest.raises(PyAMLConfigException) as exc:
34-
ml: PyAML = pyaml("tests/config/bad_conf.yml")
35-
36-
assert "at line 7, column 9" in str(exc.value)
36+
ml: PyAML = pyaml(test_file)
37+
print(str(exc.value))
38+
test_file_names = test_file.split("/")
39+
test_file_name = test_file_names[len(test_file_names)-1]
40+
assert f"{test_file_name} at line 7, column 9" in str(exc.value)
41+
42+
@pytest.mark.parametrize("test_file", [
43+
"tests/config/bad_conf_cycles.yml",
44+
"tests/config/bad_conf_cycles.json",
45+
])
46+
def test_error_cycles(test_file):
47+
with pytest.raises(PyAMLException) as exc:
48+
ml: PyAML = pyaml(test_file)
49+
50+
assert "A cycle has been detected" in str(exc.value)

0 commit comments

Comments
 (0)