Skip to content

Commit 8693177

Browse files
committed
First basic validity check
1 parent e8671c8 commit 8693177

4 files changed

Lines changed: 153 additions & 0 deletions

File tree

utils/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# OPL YAML utils
2+
3+
This folder contains utility scripts for working with the YAML format to describe problems in context of OPL. They are mainly intended to be run automatically via GitHub Actions to make collaboration easier.
4+
5+
The intended way of adding a new problem to the repository is thus as follows:
6+
7+
* Change the [new_problem.yaml](new_problem.yaml) template file to fit the new problem.
8+
* Create a PR which modifies with the changes.
9+
10+
What happens in the background then is:
11+
12+
* On PR creation and commits to the PR, the [validate_yaml.py](validate_yaml.py) script is run to check that the YAML file is valid and consistent. It is expecting the changes to be in the [new_problem.yaml](new_problem.yaml) file.
13+
* Then the PR should be reviewed manually.
14+
* When the PR is merged into the main branch, a second script runs (which doesn't exist yet), that adds the content of [new_problem.yaml](new_problem.yaml) to the [problems.yaml](../problems.yaml) file, and returns it to its previous version.
15+
16+
:alert: Note that the GitHubActions do not exist yet either, this is a WIP.
17+
18+
## validate_yaml.py
19+
20+
This script checks the new content for the following:
21+
22+
* The YAML syntax is valid and is in expected format
23+
* The required fields are present.
24+
* Specific fields are unique across the new set of problems (e.g. name)
25+
26+
:alert: Execute from root of the repository. Tested with python 3.12
27+
28+
```bash
29+
pip install -r utils/requirements.txt
30+
python utils/validate_yaml.py utils/new_problem.yaml
31+
```

utils/new_problem.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
- name: template
2+
suite/generator/single: suite
3+
objectives: '1'
4+
dimensionality: scalable
5+
variable type: continuous
6+
constraints: 'no'
7+
dynamic: 'no'
8+
noise: 'no'
9+
multimodal: 'yes'
10+
multi-fidelity: 'no'
11+
reference: ''
12+
implementation: ''
13+
source (real-world/artificial): ''
14+
textual description: 'This is a dummy template'

utils/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pyyaml

utils/validate_yaml.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import yaml
2+
import sys
3+
4+
# Define the required fields your YAML must have
5+
REQUIRED_FIELDS = [
6+
"name",
7+
"suite/generator/single",
8+
"objectives",
9+
"dimensionality",
10+
"variable type",
11+
"constraints",
12+
"dynamic",
13+
"noise",
14+
"multimodal",
15+
"multi-fidelity",
16+
"reference",
17+
"implementation",
18+
"source (real-world/artificial)",
19+
"textual description",
20+
]
21+
22+
UNIQUE_FIELDS = ["name", "reference", "implementation"]
23+
PROBLEMS_FILE = "problems.yaml"
24+
25+
26+
def read_data(filepath):
27+
try:
28+
with open(filepath, "r") as f:
29+
data = yaml.safe_load(f)
30+
return 0, data
31+
except FileNotFoundError:
32+
print(f"File not found: {filepath}")
33+
return 1, None
34+
except yaml.YAMLError as e:
35+
print(f"YAML syntax error: {e}")
36+
return 1, None
37+
38+
39+
def check_format(data):
40+
if len(data) != 1:
41+
print("YAML file should contain exactly one top-level document.")
42+
return False
43+
if not isinstance(data[0], dict):
44+
print("Top-level document should be a dictionary.")
45+
return False
46+
return True
47+
48+
49+
def check_fields(data):
50+
if len(data) != len(REQUIRED_FIELDS):
51+
print(f"YAML file should contain exactly {len(REQUIRED_FIELDS)} fields.")
52+
return False
53+
missing = [field for field in REQUIRED_FIELDS if field not in data]
54+
if missing:
55+
print(f"Missing required fields: {', '.join(missing)}")
56+
return False
57+
# Check that the name is not still template
58+
if data.get("name") == "template":
59+
print("Please change the 'name' field from 'template' to a unique name.")
60+
return False
61+
return True
62+
63+
64+
def check_novelty(data):
65+
# Load existing problems
66+
read_status, existing_data = read_data(PROBLEMS_FILE)
67+
if read_status != 0:
68+
print("Could not read existing problems for novelty check.")
69+
return False
70+
assert existing_data is not None
71+
for field in UNIQUE_FIELDS:
72+
existing_values = {
73+
entry.get(field) for entry in existing_data if isinstance(entry, dict)
74+
}
75+
if data.get(field) in existing_values:
76+
print(
77+
f"Field '{field}' with value '{data.get(field)}' already exists. Please choose a unique value."
78+
)
79+
return False
80+
return True
81+
82+
83+
def validate_yaml(filepath):
84+
status, data = read_data(filepath)
85+
if status != 0:
86+
sys.exit(1)
87+
if not check_format(data):
88+
sys.exit(1)
89+
assert data is not None and len(data) == 1
90+
new_data = data[0] # Extract the single top-level entry
91+
92+
# Check required and unique fields
93+
if not check_fields(new_data) or not check_novelty(new_data):
94+
sys.exit(1)
95+
96+
# YAML is valid if we reach this point
97+
print("YAML syntax is valid.")
98+
sys.exit(0)
99+
100+
101+
if __name__ == "__main__":
102+
if len(sys.argv) < 2:
103+
print("Usage: python validate_yaml.py <yourfile.yaml>")
104+
sys.exit(1)
105+
106+
filepath = sys.argv[1]
107+
validate_yaml(filepath)

0 commit comments

Comments
 (0)