One table for each value that you can change, and the file that keeps your changes.
Each value that a person can change is a row in one table, SETTINGS. A row has a group, a name, a label, one line of help, a type, a default and, for a number, a range. The engine, the web app and the Settings page all read this table. Thus each value has one place.
Your changes go in settings.json in cfg_dir(). The code reads a value when it uses it, so a change works at once, with no restart. A saved value outside its range gets the nearest end of the range. A saved value of the wrong type gets the default. A bad file never stops the app.
This module imports only the standard library and fastcore. Each engine module reads its values from here, so the import must stay light.
The groups
The Settings page shows the groups in this order. Each group has a label and one lead line.
The table
add puts one row in SETTINGS. The name is section.key: the section of settings.json and the key in it. The type comes from the default. more puts the row under the closed “More” part of its group. store='profile' marks a value that the vault keeps in your profile, not this file. hi can be the name of another row. Then that value is the top of the range. ui=False marks a value that its group shows with its own controls. reset=False keeps a value when you use the defaults of its group: the theme, and the speech engine and model.
Add the setting name to SETTINGS and return its row.
The Look group keeps its own controls: the theme swatches and the mode. The Learning group holds the three values of your profile.
The Walks group sets the size of a walk and of a side walk.
The Talks group sets how a talk runs: the questions, the time, the vote and the room for phones.
The Checks and grading group sets the tries, the grade of a shaky pass, and when an idea counts as learned.
The Review group sets the time to the next review. A daily limit on review is new: 0 means no limit, as before.
The Links group sets how much of a link the tutor gets. The pages read by default can not be more than the page cap.
The Research group sets the web search for a step with no source. The tutor searches the web for the idea, reads the best pages, and quotes one passage word for word. When research is off, the tutor does no web search, and the step says that the text is in the tutor’s words.
The Code runner group sets the time limits of a run. The kill time and the CPU limit follow the total time: total + 2 s and total + 1 s.
The Speech to text group keeps the engine and the model, and the pauses at which the mic cuts what you say.
The Models group has the default model and one model for each task. An empty value uses the default model. The tasks after extract borrow the model of an older task when they have none: BORROWS says which. The reply caps are in tokens.
The Lists group sets how many items show in the lists of the app. All of it is under “More”.
Each group except Prompts has rows, and each row is in a group. The Prompts group lists the prompts of prompts.PROMPTS.
assertset(GROUPS) - {'prompts'} == {r.group for r in SETTINGS.values()}assertall(r.kind !='int'or r.lo <= r.dflt for r in SETTINGS.values())len(SETTINGS)
The file
DFLT_SETTINGS is the default of each section: the rows that the file keeps, the saved prompt texts and the models that the Settings page downloaded. settings() gives the saved settings over the defaults. A section that is not a dict in the file is ignored, and a top-level key that is not known is dropped.
save_settings writes the file: first a temporary file with its own name, then a rename, so a crash never leaves half a file. A lock keeps two changes from the web app apart. It does not keep a value that is the default. update_settings changes keys in one section.
fit makes a saved value fit its row. A number outside the range gets the nearest end. A value of the wrong type gets the default. A whole number can come as 12.0 from a form; it stays a whole number. A choice must be one of the choices.
set_val saves a value made to fit its row, and returns it. reset puts back the default of one row, and reset_group of each row in a group. changed is true when a value is not its default. clamped gives the value in the file when it was outside the range, so the page can say so.
Save the value v of the setting name, made to fit its row. Return the value in use.
A value saved out of range is saved as the end of the range. reset and reset_group put back the defaults.
test_eq(set_val('walks.steps', 99), 30)test_eq(raw('walks.steps'), 30)assert changed('walks.steps') andnot changed('walks.note')reset('walks.steps')test_eq(val('walks.steps'), 12)set_val('checks.tries', 5); set_val('checks.near', 8)test_eq(sorted(reset_group('checks')), ['checks.near', 'checks.tries'])test_eq((val('checks.tries'), val('checks.near')), (3, 7))update_settings('walks', steps=50)test_eq(clamped('walks.steps'), 50)# a number typed by hand as text works, and text that is not a number gives the defaultupdate_settings('tokens', hint='900', grade='many')test_eq((val('tokens.hint'), val('tokens.grade')), (900, 2048))# a value that the profile keeps has no reset heretest_fail(lambda: reset('learning.pace'), contains='profile')save_settings(dict(prompts={}, downloaded=[]))
What stays fixed
Some values keep you safe, so they have no setting: the closed process of the code runner (no network, the sandbox profile, the audit hook, python -I, sandbox=True), the link checks for local addresses, plain HTTP, the link kinds, the HTML allowlist, the check of a pane address, the local-only hosts, the test allowlist, the grade scale and the depth ladder. The room for phones keeps its safety limits fixed too: the bind rule, the join code, the rate limits, the body size and the headers. FIXED names them, and no row may have one of these words in its name.
assertnot [n for n in SETTINGS for w in FIXED if w in n]