Module emanate.config
Emanate's configuration module.
emanate.config
defines Emanate's defaults, along with helpers for working
with configuration objects, loading them from JSON files, and dealing with
relative paths.
Expand source code
"""Emanate's configuration module.
`emanate.config` defines Emanate's defaults, along with helpers for working
with configuration objects, loading them from JSON files, and dealing with
relative paths.
"""
import functools
import json
from pathlib import Path
from collections.abc import Iterable
CONFIG_PATHS = ('destination', 'source', 'ignore')
class Config(dict):
"""Simple wrapper around dict, allowing accessing values as attributes."""
def __getattr__(self, name):
"""Provide the contents of self as attributes."""
if name not in self:
raise AttributeError(
f"{type(self).__name__!r} object has no attribute {name!r}",
)
return self[name]
def copy(self):
"""Return a new Config, with the same contents as self."""
return Config(self)
@classmethod
def defaults(cls, src):
"""Return Emanate's default configuration.
Config.defaults() resolves the default using the value
of Path.home() at the time it was called.
"""
return cls({
'confirm': True,
'destination': Path.home(),
'ignore': frozenset((
"*~",
".*~",
".*.sw?",
"emanate.json",
"*/emanate.json",
".emanate",
".*.emanate",
".git/",
".gitignore",
".gitmodules",
"__pycache__/",
)),
}).resolve(src.absolute())
def resolve(self, rel_to):
"""Convert path to absolute pathlib.Path objects.
Returns a new Config object, similar to its input, with all
paths attributes converted to `pathlib` objects, and relative paths
resolved relatively to `relative_to`.
"""
assert isinstance(rel_to, Path)
assert rel_to.is_absolute()
result = self.copy()
for key in CONFIG_PATHS:
if key not in result:
continue
if isinstance(result[key], (str, Path)):
result[key] = rel_to / Path(result[key]).expanduser()
elif isinstance(result[key], Iterable):
result[key] = [rel_to / Path(p).expanduser() for p in result[key]]
return result
def merge(*configs, strict_resolve=True): # pylint: disable=no-method-argument
"""Merge several Config objects.
Later configurations override previous ones,
and the `ignore` attributes are merged (according to set union).
"""
def _merge_one(config, other):
assert isinstance(config, Config)
assert isinstance(other, Config)
assert config.resolved
if strict_resolve and not other.resolved:
raise ValueError("Merging a non-resolved configuration")
config = config.copy()
for key, value in other.items():
if value is None:
continue
if key == 'ignore':
config[key] = config.get(key, frozenset()).union(value)
else:
config[key] = value
return config
return functools.reduce(_merge_one, filter(None, configs), Config())
@classmethod
def from_json(cls, path):
"""Load an Emanate configuration from a file.
Takes a `pathlib.Path` object designating a JSON configuration file,
loads it, and resolve paths relative to the file.
"""
assert isinstance(path, Path)
with path.open() as file:
return cls(json.load(file)).resolve(path.parent.resolve())
@property
def resolved(self):
"""Check that all path options in a configuration object are absolute."""
for key in CONFIG_PATHS:
if key not in self:
continue
if isinstance(self[key], Path):
return self[key].is_absolute()
if isinstance(self[key], Iterable):
for path in self[key]:
if not isinstance(path, Path):
raise TypeError(
f"Configuration key '{key}' should contain Paths, "
f"got a '{type(path).__name__}': '{path!r}'"
)
if not path.is_absolute():
return False
raise TypeError(
f"Configuration key '{key}' should be a (list of) Path(s), "
f"got a '{type(key).__name__}': '{self[key]!r}'"
)
return True
Classes
class Config (*args, **kwargs)
-
Simple wrapper around dict, allowing accessing values as attributes.
Expand source code
class Config(dict): """Simple wrapper around dict, allowing accessing values as attributes.""" def __getattr__(self, name): """Provide the contents of self as attributes.""" if name not in self: raise AttributeError( f"{type(self).__name__!r} object has no attribute {name!r}", ) return self[name] def copy(self): """Return a new Config, with the same contents as self.""" return Config(self) @classmethod def defaults(cls, src): """Return Emanate's default configuration. Config.defaults() resolves the default using the value of Path.home() at the time it was called. """ return cls({ 'confirm': True, 'destination': Path.home(), 'ignore': frozenset(( "*~", ".*~", ".*.sw?", "emanate.json", "*/emanate.json", ".emanate", ".*.emanate", ".git/", ".gitignore", ".gitmodules", "__pycache__/", )), }).resolve(src.absolute()) def resolve(self, rel_to): """Convert path to absolute pathlib.Path objects. Returns a new Config object, similar to its input, with all paths attributes converted to `pathlib` objects, and relative paths resolved relatively to `relative_to`. """ assert isinstance(rel_to, Path) assert rel_to.is_absolute() result = self.copy() for key in CONFIG_PATHS: if key not in result: continue if isinstance(result[key], (str, Path)): result[key] = rel_to / Path(result[key]).expanduser() elif isinstance(result[key], Iterable): result[key] = [rel_to / Path(p).expanduser() for p in result[key]] return result def merge(*configs, strict_resolve=True): # pylint: disable=no-method-argument """Merge several Config objects. Later configurations override previous ones, and the `ignore` attributes are merged (according to set union). """ def _merge_one(config, other): assert isinstance(config, Config) assert isinstance(other, Config) assert config.resolved if strict_resolve and not other.resolved: raise ValueError("Merging a non-resolved configuration") config = config.copy() for key, value in other.items(): if value is None: continue if key == 'ignore': config[key] = config.get(key, frozenset()).union(value) else: config[key] = value return config return functools.reduce(_merge_one, filter(None, configs), Config()) @classmethod def from_json(cls, path): """Load an Emanate configuration from a file. Takes a `pathlib.Path` object designating a JSON configuration file, loads it, and resolve paths relative to the file. """ assert isinstance(path, Path) with path.open() as file: return cls(json.load(file)).resolve(path.parent.resolve()) @property def resolved(self): """Check that all path options in a configuration object are absolute.""" for key in CONFIG_PATHS: if key not in self: continue if isinstance(self[key], Path): return self[key].is_absolute() if isinstance(self[key], Iterable): for path in self[key]: if not isinstance(path, Path): raise TypeError( f"Configuration key '{key}' should contain Paths, " f"got a '{type(path).__name__}': '{path!r}'" ) if not path.is_absolute(): return False raise TypeError( f"Configuration key '{key}' should be a (list of) Path(s), " f"got a '{type(key).__name__}': '{self[key]!r}'" ) return True
Ancestors
- builtins.dict
Static methods
def defaults(src)
-
Return Emanate's default configuration.
Config.defaults() resolves the default using the value of Path.home() at the time it was called.
Expand source code
@classmethod def defaults(cls, src): """Return Emanate's default configuration. Config.defaults() resolves the default using the value of Path.home() at the time it was called. """ return cls({ 'confirm': True, 'destination': Path.home(), 'ignore': frozenset(( "*~", ".*~", ".*.sw?", "emanate.json", "*/emanate.json", ".emanate", ".*.emanate", ".git/", ".gitignore", ".gitmodules", "__pycache__/", )), }).resolve(src.absolute())
def from_json(path)
-
Load an Emanate configuration from a file.
Takes a
pathlib.Path
object designating a JSON configuration file, loads it, and resolve paths relative to the file.Expand source code
@classmethod def from_json(cls, path): """Load an Emanate configuration from a file. Takes a `pathlib.Path` object designating a JSON configuration file, loads it, and resolve paths relative to the file. """ assert isinstance(path, Path) with path.open() as file: return cls(json.load(file)).resolve(path.parent.resolve())
Instance variables
var resolved
-
Check that all path options in a configuration object are absolute.
Expand source code
@property def resolved(self): """Check that all path options in a configuration object are absolute.""" for key in CONFIG_PATHS: if key not in self: continue if isinstance(self[key], Path): return self[key].is_absolute() if isinstance(self[key], Iterable): for path in self[key]: if not isinstance(path, Path): raise TypeError( f"Configuration key '{key}' should contain Paths, " f"got a '{type(path).__name__}': '{path!r}'" ) if not path.is_absolute(): return False raise TypeError( f"Configuration key '{key}' should be a (list of) Path(s), " f"got a '{type(key).__name__}': '{self[key]!r}'" ) return True
Methods
def copy(self)
-
Return a new Config, with the same contents as self.
Expand source code
def copy(self): """Return a new Config, with the same contents as self.""" return Config(self)
def merge(*configs, strict_resolve=True)
-
Merge several Config objects.
Later configurations override previous ones, and the
ignore
attributes are merged (according to set union).Expand source code
def merge(*configs, strict_resolve=True): # pylint: disable=no-method-argument """Merge several Config objects. Later configurations override previous ones, and the `ignore` attributes are merged (according to set union). """ def _merge_one(config, other): assert isinstance(config, Config) assert isinstance(other, Config) assert config.resolved if strict_resolve and not other.resolved: raise ValueError("Merging a non-resolved configuration") config = config.copy() for key, value in other.items(): if value is None: continue if key == 'ignore': config[key] = config.get(key, frozenset()).union(value) else: config[key] = value return config return functools.reduce(_merge_one, filter(None, configs), Config())
def resolve(self, rel_to)
-
Convert path to absolute pathlib.Path objects.
Returns a new Config object, similar to its input, with all paths attributes converted to
pathlib
objects, and relative paths resolved relatively torelative_to
.Expand source code
def resolve(self, rel_to): """Convert path to absolute pathlib.Path objects. Returns a new Config object, similar to its input, with all paths attributes converted to `pathlib` objects, and relative paths resolved relatively to `relative_to`. """ assert isinstance(rel_to, Path) assert rel_to.is_absolute() result = self.copy() for key in CONFIG_PATHS: if key not in result: continue if isinstance(result[key], (str, Path)): result[key] = rel_to / Path(result[key]).expanduser() elif isinstance(result[key], Iterable): result[key] = [rel_to / Path(p).expanduser() for p in result[key]] return result