Created
January 24, 2021 21:57
-
-
Save WaylonWalker/a008d993db6e80eff793e46eac9c2806 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Standard Config. | |
A module to load tooling config from a users project space. | |
One day this may become a package, for now its a gist. | |
Inspired from frustrations that some tools have a tool.ini, .tool.ini, | |
setup.cfg, or pyproject.toml. Some allow for global configs, some don't. Some | |
properly follow the users home directory, others end up in a weird temp | |
directory. Windows home directory is only more confusing. Some will even | |
respect the users `$XDG_HOME` directory. | |
This file is for any project that can be configured in plain text such as `ini` | |
or `toml` and not requiring a .py file. Just name your tool and let users put | |
config where it makes sense to them, no need to figure out resolution order. | |
## Usage: | |
``` python | |
from standard_config import load | |
# Retrieve any overrides from the user | |
overrides = {'setting': True} | |
config = load('my_tool', overrides) | |
``` | |
## Resolution Order | |
* First global file with a tool key | |
* First local file with a tool key | |
* Environment variables prefixed with `TOOL` | |
* Overrides | |
### Tool Specific Ini files | |
Ini file formats must include a `<tool>` key. | |
``` ini | |
[my_tool] | |
setting = True | |
``` | |
### pyproject.toml | |
Toml files must include a `tool.<tool>` key | |
``` toml | |
[tool.my_tool] | |
setting = True | |
``` | |
### setup.cfg | |
setup.cfg files must include a `tool:<tool>` key | |
``` ini | |
[tool:my_tool] | |
setting = True | |
``` | |
### global files to consider | |
* <home>/tool.ini | |
* <home>/.tool | |
* <home>/.tool.ini | |
* <home>/.config/tool.ini | |
* <home>/.config/.tool | |
* <home>/.config/.tool.ini | |
### local files to consider | |
* <project_home>/tool.ini | |
* <project_home>/.tool | |
* <project_home>/.tool.ini | |
* <project_home>/pyproject.toml | |
* <project_home>/setup.cfg | |
""" | |
import os | |
from pathlib import Path | |
from typing import Dict, List, Union | |
import anyconfig | |
# path_spec_type = List[Dict[str, Union[Path, str, List[str]]]] | |
path_spec_type = List | |
def _get_global_path_specs(tool: str) -> path_spec_type: | |
""" | |
Generate a list of standard pathspecs for global config files. | |
Args: | |
tool (str): name of the tool to configure | |
""" | |
try: | |
home = Path(os.environ["XDG_HOME"]) | |
except KeyError: | |
home = Path.home() | |
return [ | |
{"path_specs": home / f"{tool}.ini", "ac_parser": "ini", "keys": [tool]}, | |
{"path_specs": home / f".{tool}", "ac_parser": "ini", "keys": [tool]}, | |
{"path_specs": home / f".{tool}.ini", "ac_parser": "ini", "keys": [tool]}, | |
{ | |
"path_specs": home / ".config" / f"{tool}.ini", | |
"ac_parser": "ini", | |
"keys": [tool], | |
}, | |
{ | |
"path_specs": home / ".config" / f".{tool}", | |
"ac_parser": "ini", | |
"keys": [tool], | |
}, | |
{ | |
"path_specs": home / ".config" / f".{tool}.ini", | |
"ac_parser": "ini", | |
"keys": [tool], | |
}, | |
] | |
def _get_local_path_specs(tool: str, project_home: Union[str, Path]) -> path_spec_type: | |
""" | |
Generate a list of standard pathspecs for local, project directory config files. | |
Args: | |
tool (str): name of the tool to configure | |
""" | |
return [ | |
{ | |
"path_specs": Path(project_home) / f"{tool}.ini", | |
"ac_parser": "ini", | |
"keys": [tool], | |
}, | |
{ | |
"path_specs": Path(project_home) / f".{tool}", | |
"ac_parser": "ini", | |
"keys": [tool], | |
}, | |
{ | |
"path_specs": Path(project_home) / f".{tool}.ini", | |
"ac_parser": "ini", | |
"keys": [tool], | |
}, | |
{ | |
"path_specs": Path(project_home) / f"pyproject.toml", | |
"ac_parser": "toml", | |
"keys": ["tool", tool], | |
}, | |
{ | |
"path_specs": Path(project_home) / f"setup.cfg", | |
"ac_parser": "ini", | |
"keys": [f"tool.{tool}"], | |
}, | |
] | |
def _get_attrs(attrs: list, config: Dict) -> Dict: | |
"""Get nested config data from a list of keys. | |
specifically written for pyproject.toml which needs to get `tool` then `<tool>` | |
""" | |
for attr in attrs: | |
config = config[attr] | |
return config | |
def _load_files(config_path_specs: path_spec_type) -> Dict: | |
"""Use anyconfig to load config files stopping at the first one that exists. | |
config_path_specs (list): a list of pathspecs and keys to load | |
""" | |
for file in config_path_specs: | |
try: | |
config = anyconfig.load(**file) | |
except FileNotFoundError: | |
# ignore missing files | |
continue | |
try: | |
return _get_attrs(file["keys"], config) | |
except KeyError: | |
# ignore incorrect keys | |
continue | |
return {} | |
def _load_env(tool: str) -> Dict: | |
"""Load config from environment variables. | |
Args: | |
tool (str): name of the tool to configure | |
""" | |
vars = [var for var in os.environ.keys() if var.startswith(tool.upper())] | |
return { | |
var.lower().strip(tool.lower()).strip("_").strip("-"): os.environ[var] | |
for var in vars | |
} | |
def load(tool: str, project_home: Union[Path, str] = ".", overrides: Dict = {}) -> Dict: | |
"""Load tool config from standard config files. | |
Resolution Order | |
* First global file with a tool key | |
* First local file with a tool key | |
* Environment variables prefixed with `TOOL` | |
* Overrides | |
Args: | |
tool (str): name of the tool to configure | |
""" | |
global_config = _load_files(_get_global_path_specs(tool)) | |
local_config = _load_files(_get_local_path_specs(tool, project_home)) | |
env_config = _load_env(tool) | |
return {**global_config, **local_config, **env_config, **overrides} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment