| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- """Manager to read and modify config data in JSON files."""
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- from __future__ import annotations
- import copy
- import errno
- import glob
- import json
- import os
- import typing as t
- from traitlets.config import LoggingConfigurable
- from traitlets.traitlets import Bool, Unicode
- StrDict = dict[str, t.Any]
- def recursive_update(target: StrDict, new: StrDict) -> None:
- """Recursively update one dictionary using another.
- None values will delete their keys.
- """
- for k, v in new.items():
- if isinstance(v, dict):
- if k not in target:
- target[k] = {}
- recursive_update(target[k], v)
- if not target[k]:
- # Prune empty subdicts
- del target[k]
- elif v is None:
- target.pop(k, None)
- else:
- target[k] = v
- def remove_defaults(data: StrDict, defaults: StrDict) -> None:
- """Recursively remove items from dict that are already in defaults"""
- # copy the iterator, since data will be modified
- for key, value in list(data.items()):
- if key in defaults:
- if isinstance(value, dict):
- remove_defaults(data[key], defaults[key])
- if not data[key]: # prune empty subdicts
- del data[key]
- elif value == defaults[key]:
- del data[key]
- class BaseJSONConfigManager(LoggingConfigurable):
- """General JSON config manager
- Deals with persisting/storing config in a json file with optionally
- default values in a {section_name}.d directory.
- """
- config_dir = Unicode(".")
- read_directory = Bool(True)
- def ensure_config_dir_exists(self) -> None:
- """Will try to create the config_dir directory."""
- try:
- os.makedirs(self.config_dir, 0o755)
- except OSError as e:
- if e.errno != errno.EEXIST:
- raise
- def file_name(self, section_name: str) -> str:
- """Returns the json filename for the section_name: {config_dir}/{section_name}.json"""
- return os.path.join(self.config_dir, section_name + ".json")
- def directory(self, section_name: str) -> str:
- """Returns the directory name for the section name: {config_dir}/{section_name}.d"""
- return os.path.join(self.config_dir, section_name + ".d")
- def get(self, section_name: str, include_root: bool = True) -> dict[str, t.Any]:
- """Retrieve the config data for the specified section.
- Returns the data as a dictionary, or an empty dictionary if the file
- doesn't exist.
- When include_root is False, it will not read the root .json file,
- effectively returning the default values.
- """
- paths = [self.file_name(section_name)] if include_root else []
- if self.read_directory:
- pattern = os.path.join(self.directory(section_name), "*.json")
- # These json files should be processed first so that the
- # {section_name}.json take precedence.
- # The idea behind this is that installing a Python package may
- # put a json file somewhere in the a .d directory, while the
- # .json file is probably a user configuration.
- paths = sorted(glob.glob(pattern)) + paths
- self.log.debug(
- "Paths used for configuration of %s: \n\t%s",
- section_name,
- "\n\t".join(paths),
- )
- data: dict[str, t.Any] = {}
- for path in paths:
- if os.path.isfile(path) and os.path.getsize(path):
- with open(path, encoding="utf-8") as f:
- try:
- recursive_update(data, json.load(f))
- except json.decoder.JSONDecodeError:
- self.log.warning("Invalid JSON in %s, skipping", path)
- return data
- def set(self, section_name: str, data: t.Any) -> None:
- """Store the given config data."""
- filename = self.file_name(section_name)
- self.ensure_config_dir_exists()
- if self.read_directory:
- # we will modify data in place, so make a copy
- data = copy.deepcopy(data)
- defaults = self.get(section_name, include_root=False)
- remove_defaults(data, defaults)
- # Generate the JSON up front, since it could raise an exception,
- # in order to avoid writing half-finished corrupted data to disk.
- json_content = json.dumps(data, indent=2)
- with open(filename, "w", encoding="utf-8") as f:
- f.write(json_content)
- def update(self, section_name: str, new_data: t.Any) -> dict[str, t.Any]:
- """Modify the config section by recursively updating it with new_data.
- Returns the modified config data as a dictionary.
- """
- data = self.get(section_name)
- recursive_update(data, new_data)
- self.set(section_name, data)
- return data
|