aboutsummaryrefslogtreecommitdiff
path: root/pw_cli/py/pw_cli/yaml_config_loader_mixin.py
diff options
context:
space:
mode:
Diffstat (limited to 'pw_cli/py/pw_cli/yaml_config_loader_mixin.py')
-rw-r--r--pw_cli/py/pw_cli/yaml_config_loader_mixin.py166
1 files changed, 166 insertions, 0 deletions
diff --git a/pw_cli/py/pw_cli/yaml_config_loader_mixin.py b/pw_cli/py/pw_cli/yaml_config_loader_mixin.py
new file mode 100644
index 000000000..4bbf47bef
--- /dev/null
+++ b/pw_cli/py/pw_cli/yaml_config_loader_mixin.py
@@ -0,0 +1,166 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Yaml config file loader mixin."""
+
+import os
+import logging
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Union
+
+import yaml
+
+_LOG = logging.getLogger(__package__)
+
+
+class MissingConfigTitle(Exception):
+ """Exception for when an existing YAML file is missing config_title."""
+
+
+class YamlConfigLoaderMixin:
+ """Yaml Config file loader mixin.
+
+ Use this mixin to load yaml file settings and save them into
+ ``self._config``. For example:
+
+ ::
+
+ class ConsolePrefs(YamlConfigLoaderMixin):
+ def __init__(self) -> None:
+ self.config_init(
+ config_section_title='pw_console',
+ project_file=Path('project_file.yaml'),
+ project_user_file=Path('project_user_file.yaml'),
+ user_file=Path('~/user_file.yaml'),
+ default_config={},
+ environment_var='PW_CONSOLE_CONFIG_FILE',
+ )
+
+ """
+
+ def config_init(
+ self,
+ config_section_title: str,
+ project_file: Optional[Union[Path, bool]] = None,
+ project_user_file: Optional[Union[Path, bool]] = None,
+ user_file: Optional[Union[Path, bool]] = None,
+ default_config: Optional[Dict[Any, Any]] = None,
+ environment_var: Optional[str] = None,
+ ) -> None:
+ """Call this to load YAML config files in order of precedence.
+
+ The following files are loaded in this order:
+ 1. project_file
+ 2. project_user_file
+ 3. user_file
+
+ Lastly, if a valid file path is specified at
+ ``os.environ[environment_var]`` then load that file overriding all
+ config options.
+
+ Args:
+ config_section_title: String name of this config section. For
+ example: ``pw_console`` or ``pw_watch``. In the YAML file this
+ is represented by a ``config_title`` key.
+
+ ::
+
+ ---
+ config_title: pw_console
+
+ project_file: Project level config file. This is intended to be a
+ file living somewhere under a project folder and is checked into
+ the repo. It serves as a base config all developers can inherit
+ from.
+ project_user_file: User's personal config file for a specific
+ project. This can be a file that lives in a project folder that
+ is git-ignored and not checked into the repo.
+ user_file: A global user based config file. This is typically a file
+ in the users home directory and settings here apply to all
+ projects.
+ default_config: A Python dict representing the base default
+ config. This dict will be applied as a starting point before
+ loading any yaml files.
+ environment_var: The name of an environment variable to check for a
+ config file. If a config file exists there it will be loaded on
+ top of the default_config ignoring project and user files.
+ """
+
+ self._config_section_title: str = config_section_title
+ self.default_config = default_config if default_config else {}
+ self.reset_config()
+
+ if project_file and isinstance(project_file, Path):
+ self.project_file = Path(
+ os.path.expandvars(str(project_file.expanduser()))
+ )
+ self.load_config_file(self.project_file)
+
+ if project_user_file and isinstance(project_user_file, Path):
+ self.project_user_file = Path(
+ os.path.expandvars(str(project_user_file.expanduser()))
+ )
+ self.load_config_file(self.project_user_file)
+
+ if user_file and isinstance(user_file, Path):
+ self.user_file = Path(
+ os.path.expandvars(str(user_file.expanduser()))
+ )
+ self.load_config_file(self.user_file)
+
+ # Check for a config file specified by an environment variable.
+ if environment_var is None:
+ return
+ environment_config = os.environ.get(environment_var, None)
+ if environment_config:
+ env_file_path = Path(environment_config)
+ if not env_file_path.is_file():
+ raise FileNotFoundError(
+ f'Cannot load config file: {env_file_path}'
+ )
+ self.reset_config()
+ self.load_config_file(env_file_path)
+
+ def _update_config(self, cfg: Dict[Any, Any]) -> None:
+ if cfg is None:
+ cfg = {}
+ self._config.update(cfg)
+
+ def reset_config(self) -> None:
+ self._config: Dict[Any, Any] = {}
+ self._update_config(self.default_config)
+
+ def _load_config_from_string( # pylint: disable=no-self-use
+ self, file_contents: str
+ ) -> List[Dict[Any, Any]]:
+ return list(yaml.safe_load_all(file_contents))
+
+ def load_config_file(self, file_path: Path) -> None:
+ if not file_path.is_file():
+ return
+
+ cfgs = self._load_config_from_string(file_path.read_text())
+
+ for cfg in cfgs:
+ if self._config_section_title in cfg:
+ self._update_config(cfg[self._config_section_title])
+
+ elif cfg.get('config_title', False) == self._config_section_title:
+ self._update_config(cfg)
+ else:
+ raise MissingConfigTitle(
+ '\n\nThe config file "{}" is missing the expected '
+ '"config_title: {}" setting.'.format(
+ str(file_path), self._config_section_title
+ )
+ )