vibefpmgui.build_configs

Build configurations: named sets of Build options for a project, kept in a JSON file beside its fpm.toml (FILE_NAME unless another is opened), so they can be shared (e.g. committed with the project).

The file::

    {
      "vibefpm": 1,
      "configurations": [
        {"name": "release (ifx)", "profile": "release", "features": [],
         "compiler": "ifx", "flags": "", "target": "", "extra_args": "",
         "program_args": "", "parallel_tests": 4, "coverage": false,
         "verbose": false}
      ]
    }

A configuration has every Build option but the fpm executable (a path on this computer, kept in the app's settings). An option left out of a configuration has its default; unknown keys are kept as they are.

Beside the configurations, "new_configuration" is New configuration's Build options (those not saved as one): a configuration without a name. Then "options" (one set, for every configuration in it) are the GUI's for the project: see vibefpmgui.project_options.

ConfigFile is a project's file as the app has it: read whole (a file that can't be read whole isn't used, nor written over), and written a part at a time, the rest kept as it is.

  1"""Build configurations: named sets of Build options for a project, kept in a
  2JSON file beside its fpm.toml (`FILE_NAME` unless another is opened), so
  3they can be shared (e.g. committed with the project).
  4
  5The file::
  6```json
  7    {
  8      "vibefpm": 1,
  9      "configurations": [
 10        {"name": "release (ifx)", "profile": "release", "features": [],
 11         "compiler": "ifx", "flags": "", "target": "", "extra_args": "",
 12         "program_args": "", "parallel_tests": 4, "coverage": false,
 13         "verbose": false}
 14      ]
 15    }
 16```
 17
 18A configuration has every Build option but the fpm executable (a path on
 19this computer, kept in the app's settings). An option left out of a
 20configuration has its default; unknown keys are kept as they are.
 21
 22Beside the configurations, "new_configuration" is New configuration's Build
 23options (those not saved as one): a configuration without a name. Then
 24"options" (one set, for every configuration in it) are the GUI's for the
 25project: see `vibefpmgui.project_options`.
 26
 27`ConfigFile` is a project's file as the app has it: read whole (a file that
 28can't be read whole isn't used, nor written over), and written a part at a
 29time, the rest kept as it is.
 30"""
 31
 32from __future__ import annotations
 33
 34import json
 35import os
 36import tempfile
 37from dataclasses import asdict, dataclass, field, fields
 38from pathlib import Path
 39
 40FILE_NAME = "vibefpm.json"
 41FORMAT = 1  # the file's "vibefpm" version
 42DEFAULT_PROFILE = "(default)"  # (as the Profile list shows it; "" in the file)
 43
 44
 45class ConfigError(Exception):
 46    """A configuration file that can't be read (the message says why)."""
 47
 48
 49@dataclass
 50class Configuration:
 51    name: str
 52    profile: str = DEFAULT_PROFILE
 53    features: list[str] = field(default_factory=list)
 54    compiler: str = ""
 55    flags: str = ""
 56    target: str = ""
 57    extra_args: str = ""
 58    program_args: str = ""
 59    parallel_tests: int = 1
 60    coverage: bool = False
 61    verbose: bool = False
 62
 63    def to_json(self, named: bool = True) -> dict:
 64        """As the file has it (``named`` False: New configuration's, without a name)."""
 65        data = asdict(self)
 66        data["profile"] = "" if self.profile == DEFAULT_PROFILE else self.profile
 67        if not named:
 68            del data["name"]
 69        return data
 70
 71    @classmethod
 72    def from_json(cls, data: dict, name: str | None = None) -> Configuration:
 73        """A configuration from the file (a value of the wrong type: its
 74        default). ``name``: one without a name in the file (New configuration's)."""
 75        if name is None:
 76            name = data.get("name")
 77            if not isinstance(name, str) or not name.strip():
 78                raise ConfigError("A configuration has no name.")
 79        config = cls(name.strip())
 80        for f in fields(cls):
 81            if f.name == "name" or f.name not in data:
 82                continue
 83            value, default = data[f.name], getattr(config, f.name)
 84            if isinstance(default, bool):
 85                ok = isinstance(value, bool)
 86            elif isinstance(default, int):
 87                ok = isinstance(value, int) and not isinstance(value, bool) and value >= 1
 88            elif isinstance(default, list):
 89                ok = isinstance(value, list) and all(isinstance(v, str) for v in value)
 90            else:
 91                ok = isinstance(value, str)
 92            if ok:
 93                setattr(config, f.name, list(value) if isinstance(value, list) else value)
 94        if not config.profile:
 95            config.profile = DEFAULT_PROFILE
 96        return config
 97
 98
 99def default_path(manifest: Path) -> Path:
100    """Where a project's configurations go unless another file was opened."""
101    return Path(manifest).parent / FILE_NAME
102
103
104def manifest_for(path: Path) -> Path | None:
105    """The fpm.toml a configuration file belongs to (beside it), if there is one."""
106    manifest = Path(path).parent / "fpm.toml"
107    return manifest if manifest.is_file() else None
108
109
110def _read(path: Path) -> dict:
111    try:
112        data = json.loads(Path(path).read_text(encoding="utf-8"))
113    except OSError as exc:
114        raise ConfigError(f"Can't read {path}: {exc.strerror or exc}") from exc
115    except (ValueError, UnicodeDecodeError) as exc:
116        raise ConfigError(f"{Path(path).name} isn't valid JSON: {exc}") from exc
117    if not isinstance(data, dict) or not isinstance(data.get("configurations", []), list):
118        raise ConfigError(f"{Path(path).name} isn't a VibeFPM configuration file "
119                          "(no \"configurations\" list).")
120    return data
121
122
123def load(path: Path) -> list[Configuration]:
124    """The configurations in a file, in its order (none if it doesn't exist)."""
125    if not Path(path).exists():
126        return []
127    configs, seen = [], set()
128    for item in _read(path).get("configurations", []):
129        if not isinstance(item, dict):
130            raise ConfigError(f"{Path(path).name}: a configuration isn't an object.")
131        config = Configuration.from_json(item)
132        if config.name in seen:
133            raise ConfigError(f"{Path(path).name}: two configurations are named “{config.name}”.")
134        seen.add(config.name)
135        configs.append(config)
136    return configs
137
138
139def _write(path: Path, data: dict):
140    """Write the file whole or not at all (a temporary file, then renamed)."""
141    path = Path(path)
142    text = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
143    handle, temporary = tempfile.mkstemp(prefix=".vibefpm-", suffix=".json", dir=path.parent)
144    try:
145        with os.fdopen(handle, "w", encoding="utf-8", newline="\n") as f:
146            f.write(text)
147        os.replace(temporary, path)
148    except BaseException:
149        Path(temporary).unlink(missing_ok=True)
150        raise
151
152
153def _with_configurations(path: Path, change) -> list[Configuration]:
154    """Read the file (or start one), let ``change`` edit its list of
155    configurations (as JSON objects), and write it back; the result. Other
156    keys, and keys of a configuration this app doesn't know, are kept; a
157    file that can't be read whole isn't written over (ConfigError)."""
158    load(path)  # (every configuration checked first)
159    data = _read(path) if Path(path).exists() else {}
160    items = [item for item in data.get("configurations", []) if isinstance(item, dict)]
161    change(items)
162    data = {"vibefpm": data.get("vibefpm", FORMAT), **{k: v for k, v in data.items() if k != "vibefpm"},
163            "configurations": items}
164    _write(path, data)
165    return load(path)
166
167
168def save(path: Path, config: Configuration) -> list[Configuration]:
169    """Add a configuration to the file, or replace the one with its name."""
170    def change(items: list[dict]):
171        new = config.to_json()
172        for i, item in enumerate(items):
173            if item.get("name") == config.name:
174                items[i] = {**item, **new}
175                return
176        items.append(new)
177    return _with_configurations(path, change)
178
179
180def remove(path: Path, name: str) -> list[Configuration]:
181    """Take the configuration named ``name`` out of the file."""
182    return _with_configurations(path, lambda items: items.__setitem__(
183        slice(None), [item for item in items if item.get("name") != name]))
184
185
186# Build options a file's "options" had before they went to "new_configuration"
187# (the app's settings keys for them): moved there when it's read.
188_OLD_OPTION_FIELDS = {"profile": "profile", "compiler": "compiler", "flags": "flags", "extra": "extra_args",
189                      "target": "target", "program_args": "program_args", "verbose": "verbose",
190                      "coverage": "coverage", "test_jobs": "parallel_tests", "features": "features"}
191
192
193def _object(data: dict, key: str, path: Path) -> dict | None:
194    value = data.get(key)
195    if value is not None and not isinstance(value, dict):
196        raise ConfigError(f"{Path(path).name}: \"{key}\" isn't an object.")
197    return value
198
199
200_NONE = object()  # (an option not in the file)
201
202
203class ConfigFile:
204    """A project's configuration file, as the app has it: ``configurations``,
205    New configuration's Build options (``new_configuration``: None if the
206    file has none) and the GUI's ``options`` (None: there's no file, or it
207    can't be read, ``error`` saying why; then none of it is used, and it
208    isn't written over)."""
209
210    def __init__(self, path: Path):
211        self.path = Path(path)
212        self.configurations: list[Configuration] = []
213        self.new_configuration: Configuration | None = None
214        self.options: dict | None = None
215        self.error = ""
216        self._written: tuple | None = None  # (options, new configuration) as the file has them
217
218    def exists(self) -> bool:
219        return self.path.is_file()
220
221    def _load(self) -> tuple[list[Configuration], dict | None, dict | None]:
222        """Everything in the file, checked (ConfigError if any of it can't be read)."""
223        if not self.exists():
224            return [], None, None
225        configurations = load(self.path)
226        data = _read(self.path)
227        return configurations, _object(data, "options", self.path), _object(data, "new_configuration", self.path)
228
229    def read(self) -> ConfigFile:
230        """Read it all (the file's options and New configuration replace those had)."""
231        try:
232            configurations, options, new = self._load()
233        except ConfigError as exc:
234            self.configurations, self.options, self.new_configuration = [], None, None
235            self.error, self._written = str(exc), None
236            return self
237        self.configurations, self.error = configurations, ""
238        self._take_gui(options, new)
239        return self
240
241    def _take_gui(self, options: dict | None, new: dict | None):
242        self._written = (options, new) if self.exists() else None
243        self.options = dict(options or {}) if self.exists() else None
244        new = dict(new or {})
245        for key, name in _OLD_OPTION_FIELDS.items():  # (from a file written before: moved)
246            if self.options is not None and key in self.options:
247                new.setdefault(name, self.options.pop(key))
248        self.new_configuration = Configuration.from_json(new, name="") if new else None
249
250    def reread_configurations(self):
251        """Its configurations read again (the project reloaded: the file may
252        have changed; its options and New configuration are the window's
253        until they're written, or it's saved in the editor: see reread)."""
254        try:
255            self.configurations, self.error = self._load()[0], ""
256        except ConfigError as exc:
257            self.configurations, self.error = [], str(exc)
258
259    def reread(self) -> tuple[set[str], bool]:
260        """Read it all again (it was saved in the editor): (the options
261        changed in it since it was last read or written, whether New
262        configuration was). Those changed are taken (``options`` and
263        ``new_configuration``: the file's)."""
264        try:
265            configurations, options, new = self._load()
266        except ConfigError as exc:
267            self.configurations, self.error = [], str(exc)
268            return set(), False
269        self.configurations, self.error = configurations, ""
270        if not self.exists():
271            return set(), False
272        before, before_new = self._written or (None, None)
273        before, options_now = before or {}, options or {}
274        changed = {key for key in {*before, *options_now} if before.get(key, _NONE) != options_now.get(key, _NONE)}
275        new_changed = (new or {}) != (before_new or {})
276        if changed or new_changed:
277            self._take_gui(options, new)
278        return changed, new_changed
279
280    def start_options(self):
281        """It was just made (the first configuration saved): it holds the
282        project's options from now on (none yet)."""
283        if self.options is None and self.exists() and not self.error:
284            self.options, self._written = {}, ({}, None)
285
286    def _check_writable(self):
287        if self.error and self.exists():  # (not written over: fix it, or move it away)
288            raise ConfigError(self.error)
289
290    def save(self, config: Configuration):
291        """Add a configuration, or replace the one with its name (OSError, ConfigError)."""
292        self._check_writable()
293        self.configurations = save(self.path, config)
294
295    def remove(self, name: str):
296        """Take a configuration out (OSError, ConfigError)."""
297        self._check_writable()
298        self.configurations = remove(self.path, name)
299
300    def write_options(self, options: dict, new: Configuration) -> bool:
301        """Write the options and New configuration, if they changed (True:
302        written; OSError, ConfigError)."""
303        if self.options is None or self.error:
304            return False
305        written = (options, new.to_json(named=False))
306        if written == self._written:
307            return False
308        save_options(self.path, *written)
309        self.options, self.new_configuration, self._written = dict(options), new, written
310        return True
311
312
313def save_options(path: Path, options: dict, new_configuration: dict):
314    """Put ``options`` and New configuration's Build options in the file
315    (which must exist and be readable), its configurations and anything else
316    in it kept, in the order it's read: the configurations, New
317    configuration, the options."""
318    load(path)  # (every configuration checked first: a file that can't be read isn't written over)
319    data = {k: v for k, v in _read(path).items() if k not in ("new_configuration", "options")}
320    data["new_configuration"] = new_configuration
321    data["options"] = options
322    _write(path, data)
FILE_NAME = 'vibefpm.json'
FORMAT = 1
DEFAULT_PROFILE = '(default)'
class ConfigError(builtins.Exception):
46class ConfigError(Exception):
47    """A configuration file that can't be read (the message says why)."""

A configuration file that can't be read (the message says why).

@dataclass
class Configuration:
50@dataclass
51class Configuration:
52    name: str
53    profile: str = DEFAULT_PROFILE
54    features: list[str] = field(default_factory=list)
55    compiler: str = ""
56    flags: str = ""
57    target: str = ""
58    extra_args: str = ""
59    program_args: str = ""
60    parallel_tests: int = 1
61    coverage: bool = False
62    verbose: bool = False
63
64    def to_json(self, named: bool = True) -> dict:
65        """As the file has it (``named`` False: New configuration's, without a name)."""
66        data = asdict(self)
67        data["profile"] = "" if self.profile == DEFAULT_PROFILE else self.profile
68        if not named:
69            del data["name"]
70        return data
71
72    @classmethod
73    def from_json(cls, data: dict, name: str | None = None) -> Configuration:
74        """A configuration from the file (a value of the wrong type: its
75        default). ``name``: one without a name in the file (New configuration's)."""
76        if name is None:
77            name = data.get("name")
78            if not isinstance(name, str) or not name.strip():
79                raise ConfigError("A configuration has no name.")
80        config = cls(name.strip())
81        for f in fields(cls):
82            if f.name == "name" or f.name not in data:
83                continue
84            value, default = data[f.name], getattr(config, f.name)
85            if isinstance(default, bool):
86                ok = isinstance(value, bool)
87            elif isinstance(default, int):
88                ok = isinstance(value, int) and not isinstance(value, bool) and value >= 1
89            elif isinstance(default, list):
90                ok = isinstance(value, list) and all(isinstance(v, str) for v in value)
91            else:
92                ok = isinstance(value, str)
93            if ok:
94                setattr(config, f.name, list(value) if isinstance(value, list) else value)
95        if not config.profile:
96            config.profile = DEFAULT_PROFILE
97        return config
Configuration( name: str, profile: str = '(default)', features: list[str] = <factory>, compiler: str = '', flags: str = '', target: str = '', extra_args: str = '', program_args: str = '', parallel_tests: int = 1, coverage: bool = False, verbose: bool = False)
name: str
profile: str = '(default)'
features: list[str]
compiler: str = ''
flags: str = ''
target: str = ''
extra_args: str = ''
program_args: str = ''
parallel_tests: int = 1
coverage: bool = False
verbose: bool = False
def to_json(self, named: bool = True) -> dict:
64    def to_json(self, named: bool = True) -> dict:
65        """As the file has it (``named`` False: New configuration's, without a name)."""
66        data = asdict(self)
67        data["profile"] = "" if self.profile == DEFAULT_PROFILE else self.profile
68        if not named:
69            del data["name"]
70        return data

As the file has it (named False: New configuration's, without a name).

@classmethod
def from_json( cls, data: dict, name: str | None = None) -> Configuration:
72    @classmethod
73    def from_json(cls, data: dict, name: str | None = None) -> Configuration:
74        """A configuration from the file (a value of the wrong type: its
75        default). ``name``: one without a name in the file (New configuration's)."""
76        if name is None:
77            name = data.get("name")
78            if not isinstance(name, str) or not name.strip():
79                raise ConfigError("A configuration has no name.")
80        config = cls(name.strip())
81        for f in fields(cls):
82            if f.name == "name" or f.name not in data:
83                continue
84            value, default = data[f.name], getattr(config, f.name)
85            if isinstance(default, bool):
86                ok = isinstance(value, bool)
87            elif isinstance(default, int):
88                ok = isinstance(value, int) and not isinstance(value, bool) and value >= 1
89            elif isinstance(default, list):
90                ok = isinstance(value, list) and all(isinstance(v, str) for v in value)
91            else:
92                ok = isinstance(value, str)
93            if ok:
94                setattr(config, f.name, list(value) if isinstance(value, list) else value)
95        if not config.profile:
96            config.profile = DEFAULT_PROFILE
97        return config

A configuration from the file (a value of the wrong type: its default). name: one without a name in the file (New configuration's).

def default_path(manifest: pathlib.Path) -> pathlib.Path:
100def default_path(manifest: Path) -> Path:
101    """Where a project's configurations go unless another file was opened."""
102    return Path(manifest).parent / FILE_NAME

Where a project's configurations go unless another file was opened.

def manifest_for(path: pathlib.Path) -> pathlib.Path | None:
105def manifest_for(path: Path) -> Path | None:
106    """The fpm.toml a configuration file belongs to (beside it), if there is one."""
107    manifest = Path(path).parent / "fpm.toml"
108    return manifest if manifest.is_file() else None

The fpm.toml a configuration file belongs to (beside it), if there is one.

def load(path: pathlib.Path) -> list[Configuration]:
124def load(path: Path) -> list[Configuration]:
125    """The configurations in a file, in its order (none if it doesn't exist)."""
126    if not Path(path).exists():
127        return []
128    configs, seen = [], set()
129    for item in _read(path).get("configurations", []):
130        if not isinstance(item, dict):
131            raise ConfigError(f"{Path(path).name}: a configuration isn't an object.")
132        config = Configuration.from_json(item)
133        if config.name in seen:
134            raise ConfigError(f"{Path(path).name}: two configurations are named “{config.name}”.")
135        seen.add(config.name)
136        configs.append(config)
137    return configs

The configurations in a file, in its order (none if it doesn't exist).

def save( path: pathlib.Path, config: Configuration) -> list[Configuration]:
169def save(path: Path, config: Configuration) -> list[Configuration]:
170    """Add a configuration to the file, or replace the one with its name."""
171    def change(items: list[dict]):
172        new = config.to_json()
173        for i, item in enumerate(items):
174            if item.get("name") == config.name:
175                items[i] = {**item, **new}
176                return
177        items.append(new)
178    return _with_configurations(path, change)

Add a configuration to the file, or replace the one with its name.

def remove( path: pathlib.Path, name: str) -> list[Configuration]:
181def remove(path: Path, name: str) -> list[Configuration]:
182    """Take the configuration named ``name`` out of the file."""
183    return _with_configurations(path, lambda items: items.__setitem__(
184        slice(None), [item for item in items if item.get("name") != name]))

Take the configuration named name out of the file.

class ConfigFile:
204class ConfigFile:
205    """A project's configuration file, as the app has it: ``configurations``,
206    New configuration's Build options (``new_configuration``: None if the
207    file has none) and the GUI's ``options`` (None: there's no file, or it
208    can't be read, ``error`` saying why; then none of it is used, and it
209    isn't written over)."""
210
211    def __init__(self, path: Path):
212        self.path = Path(path)
213        self.configurations: list[Configuration] = []
214        self.new_configuration: Configuration | None = None
215        self.options: dict | None = None
216        self.error = ""
217        self._written: tuple | None = None  # (options, new configuration) as the file has them
218
219    def exists(self) -> bool:
220        return self.path.is_file()
221
222    def _load(self) -> tuple[list[Configuration], dict | None, dict | None]:
223        """Everything in the file, checked (ConfigError if any of it can't be read)."""
224        if not self.exists():
225            return [], None, None
226        configurations = load(self.path)
227        data = _read(self.path)
228        return configurations, _object(data, "options", self.path), _object(data, "new_configuration", self.path)
229
230    def read(self) -> ConfigFile:
231        """Read it all (the file's options and New configuration replace those had)."""
232        try:
233            configurations, options, new = self._load()
234        except ConfigError as exc:
235            self.configurations, self.options, self.new_configuration = [], None, None
236            self.error, self._written = str(exc), None
237            return self
238        self.configurations, self.error = configurations, ""
239        self._take_gui(options, new)
240        return self
241
242    def _take_gui(self, options: dict | None, new: dict | None):
243        self._written = (options, new) if self.exists() else None
244        self.options = dict(options or {}) if self.exists() else None
245        new = dict(new or {})
246        for key, name in _OLD_OPTION_FIELDS.items():  # (from a file written before: moved)
247            if self.options is not None and key in self.options:
248                new.setdefault(name, self.options.pop(key))
249        self.new_configuration = Configuration.from_json(new, name="") if new else None
250
251    def reread_configurations(self):
252        """Its configurations read again (the project reloaded: the file may
253        have changed; its options and New configuration are the window's
254        until they're written, or it's saved in the editor: see reread)."""
255        try:
256            self.configurations, self.error = self._load()[0], ""
257        except ConfigError as exc:
258            self.configurations, self.error = [], str(exc)
259
260    def reread(self) -> tuple[set[str], bool]:
261        """Read it all again (it was saved in the editor): (the options
262        changed in it since it was last read or written, whether New
263        configuration was). Those changed are taken (``options`` and
264        ``new_configuration``: the file's)."""
265        try:
266            configurations, options, new = self._load()
267        except ConfigError as exc:
268            self.configurations, self.error = [], str(exc)
269            return set(), False
270        self.configurations, self.error = configurations, ""
271        if not self.exists():
272            return set(), False
273        before, before_new = self._written or (None, None)
274        before, options_now = before or {}, options or {}
275        changed = {key for key in {*before, *options_now} if before.get(key, _NONE) != options_now.get(key, _NONE)}
276        new_changed = (new or {}) != (before_new or {})
277        if changed or new_changed:
278            self._take_gui(options, new)
279        return changed, new_changed
280
281    def start_options(self):
282        """It was just made (the first configuration saved): it holds the
283        project's options from now on (none yet)."""
284        if self.options is None and self.exists() and not self.error:
285            self.options, self._written = {}, ({}, None)
286
287    def _check_writable(self):
288        if self.error and self.exists():  # (not written over: fix it, or move it away)
289            raise ConfigError(self.error)
290
291    def save(self, config: Configuration):
292        """Add a configuration, or replace the one with its name (OSError, ConfigError)."""
293        self._check_writable()
294        self.configurations = save(self.path, config)
295
296    def remove(self, name: str):
297        """Take a configuration out (OSError, ConfigError)."""
298        self._check_writable()
299        self.configurations = remove(self.path, name)
300
301    def write_options(self, options: dict, new: Configuration) -> bool:
302        """Write the options and New configuration, if they changed (True:
303        written; OSError, ConfigError)."""
304        if self.options is None or self.error:
305            return False
306        written = (options, new.to_json(named=False))
307        if written == self._written:
308            return False
309        save_options(self.path, *written)
310        self.options, self.new_configuration, self._written = dict(options), new, written
311        return True

A project's configuration file, as the app has it: configurations, New configuration's Build options (new_configuration: None if the file has none) and the GUI's options (None: there's no file, or it can't be read, error saying why; then none of it is used, and it isn't written over).

ConfigFile(path: pathlib.Path)
211    def __init__(self, path: Path):
212        self.path = Path(path)
213        self.configurations: list[Configuration] = []
214        self.new_configuration: Configuration | None = None
215        self.options: dict | None = None
216        self.error = ""
217        self._written: tuple | None = None  # (options, new configuration) as the file has them
path
configurations: list[Configuration]
new_configuration: Configuration | None
options: dict | None
error
def exists(self) -> bool:
219    def exists(self) -> bool:
220        return self.path.is_file()
def read(self) -> ConfigFile:
230    def read(self) -> ConfigFile:
231        """Read it all (the file's options and New configuration replace those had)."""
232        try:
233            configurations, options, new = self._load()
234        except ConfigError as exc:
235            self.configurations, self.options, self.new_configuration = [], None, None
236            self.error, self._written = str(exc), None
237            return self
238        self.configurations, self.error = configurations, ""
239        self._take_gui(options, new)
240        return self

Read it all (the file's options and New configuration replace those had).

def reread_configurations(self):
251    def reread_configurations(self):
252        """Its configurations read again (the project reloaded: the file may
253        have changed; its options and New configuration are the window's
254        until they're written, or it's saved in the editor: see reread)."""
255        try:
256            self.configurations, self.error = self._load()[0], ""
257        except ConfigError as exc:
258            self.configurations, self.error = [], str(exc)

Its configurations read again (the project reloaded: the file may have changed; its options and New configuration are the window's until they're written, or it's saved in the editor: see reread).

def reread(self) -> tuple[set[str], bool]:
260    def reread(self) -> tuple[set[str], bool]:
261        """Read it all again (it was saved in the editor): (the options
262        changed in it since it was last read or written, whether New
263        configuration was). Those changed are taken (``options`` and
264        ``new_configuration``: the file's)."""
265        try:
266            configurations, options, new = self._load()
267        except ConfigError as exc:
268            self.configurations, self.error = [], str(exc)
269            return set(), False
270        self.configurations, self.error = configurations, ""
271        if not self.exists():
272            return set(), False
273        before, before_new = self._written or (None, None)
274        before, options_now = before or {}, options or {}
275        changed = {key for key in {*before, *options_now} if before.get(key, _NONE) != options_now.get(key, _NONE)}
276        new_changed = (new or {}) != (before_new or {})
277        if changed or new_changed:
278            self._take_gui(options, new)
279        return changed, new_changed

Read it all again (it was saved in the editor): (the options changed in it since it was last read or written, whether New configuration was). Those changed are taken (options and new_configuration: the file's).

def start_options(self):
281    def start_options(self):
282        """It was just made (the first configuration saved): it holds the
283        project's options from now on (none yet)."""
284        if self.options is None and self.exists() and not self.error:
285            self.options, self._written = {}, ({}, None)

It was just made (the first configuration saved): it holds the project's options from now on (none yet).

def save(self, config: Configuration):
291    def save(self, config: Configuration):
292        """Add a configuration, or replace the one with its name (OSError, ConfigError)."""
293        self._check_writable()
294        self.configurations = save(self.path, config)

Add a configuration, or replace the one with its name (OSError, ConfigError).

def remove(self, name: str):
296    def remove(self, name: str):
297        """Take a configuration out (OSError, ConfigError)."""
298        self._check_writable()
299        self.configurations = remove(self.path, name)

Take a configuration out (OSError, ConfigError).

def write_options(self, options: dict, new: Configuration) -> bool:
301    def write_options(self, options: dict, new: Configuration) -> bool:
302        """Write the options and New configuration, if they changed (True:
303        written; OSError, ConfigError)."""
304        if self.options is None or self.error:
305            return False
306        written = (options, new.to_json(named=False))
307        if written == self._written:
308            return False
309        save_options(self.path, *written)
310        self.options, self.new_configuration, self._written = dict(options), new, written
311        return True

Write the options and New configuration, if they changed (True: written; OSError, ConfigError).

def save_options(path: pathlib.Path, options: dict, new_configuration: dict):
314def save_options(path: Path, options: dict, new_configuration: dict):
315    """Put ``options`` and New configuration's Build options in the file
316    (which must exist and be readable), its configurations and anything else
317    in it kept, in the order it's read: the configurations, New
318    configuration, the options."""
319    load(path)  # (every configuration checked first: a file that can't be read isn't written over)
320    data = {k: v for k, v in _read(path).items() if k not in ("new_configuration", "options")}
321    data["new_configuration"] = new_configuration
322    data["options"] = options
323    _write(path, data)

Put options and New configuration's Build options in the file (which must exist and be readable), its configurations and anything else in it kept, in the order it's read: the configurations, New configuration, the options.