Config loader
The downlink module reads its settings from a JSON file that operators edit by hand. Last week someone typed "256" in quotes, misspelt a key and set twelve retries, and the module started with the wrong settings without a word.
Write the loader that fills in what is missing, checks what is there, and lists every problem before anything starts.
Finish stage 1 to unlock this stage.
Load settings_text with json.loads(). Start config as a copy of DEFAULTS, fill in what was saved, and print every setting in name order, with the value written as JSON:
downlink_rate_kbps = "256" log_level = "INFO" log_levl = "DEBUG" retries = 12 safe_mode = "yes" timeout_s = 2.5
dict(DEFAULTS) makes the copy and update() fills it in. json.dumps(value) shows the difference between "256" and 256.
Finish stage 1 to unlock this stage.
Check every saved setting, in the order it was saved, and collect a message for each problem: an unknown name, a number setting that is not a number or is outside its range in RANGES, a safe_mode that is not true or false, a log_level that is not in LOG_LEVELS. After the config, print them all, or Config OK:
4 problems: downlink_rate_kbps: must be a number, got str retries: 12 is outside 0 to 10 safe_mode: must be true or false, got str log_levl: unknown setting
True is also an int in Python, so rule out bool first: isinstance(value, bool) or not isinstance(value, (int, float)).
Finish stage 2 to unlock this stage.
Move the work into load_config(text), which returns (config, problems) and never crashes. Text that is not valid JSON is one problem, not valid JSON: and the error's msg; JSON that is not an object, such as a list, is one problem too. In both cases the config is a copy of the defaults. Only settings that pass their check go into the config; the others keep their defaults. Run it on all three texts:
settings_text: downlink_rate_kbps: must be a number, got str retries: 12 is outside 0 to 10 safe_mode: must be true or false, got str log_levl: unknown setting good_text: Config OK: retries = 5, log_level = WARNING broken_text: not valid JSON: Expecting ',' delimiter
Hints
Modifying Dictionaries
# A mission record that changes as the mission goes on
parker = {
"name": "Parker Solar Probe",
"launched": 2018,
"orbits": 20,
"status": "cruising",
}
# Assign to a key that does not exist yet and it gets added
parker["closest_km"] = 6100000
print(parker)
# The same syntax on an existing key overwrites the old value
parker["status"] = "perihelion pass"
parker["orbits"] = 21
print("Status:", parker["status"], "after", parker["orbits"], "orbits")
# update() applies several changes in one go
parker.update({"speed_kmh": 692000, "status": "record holder"})
print("Speed:", parker["speed_kmh"], "km/h")
# pop() removes a key and hands the value back to you
old_status = parker.pop("status")
print("Removed status was:", old_status)
# del removes a key without giving anything back
del parker["launched"]
print(parker)
# setdefault() only fills in a key that is missing
parker.setdefault("agency", "NASA")
parker.setdefault("name", "Something Else")
print("Name:", parker["name"], "| Agency:", parker["agency"])
Separate arguments with commas. They're passed on every Run as sys.argv[1:].