Module cfgs
cfgs: ⚙ Serializable hierarchical dataclass settings ⚙
Also:
Simple, correct handling of config, data and cache files. Fully compliant with the XDG Base Directory Specification.
Expand source code
"""
`cfgs`: ⚙ Serializable hierarchical dataclass settings ⚙
Also:
Simple, correct handling of config, data and cache files.
Fully compliant with the XDG Base Directory Specification.
"""
from enum import Enum
from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union
import copy
import dataclasses as dc
import json
import os
import os
import sys
File = Tuple[Union[Path, str]]
_NONE = object()
class Configs:
    def diff(self, other: Any):
        assert self.__class__ is other.__class__
        result = {}
        for f in dc.fields(self):
            s, o = getattr(self, f.name), getattr(other, f.name)
            if s != o:
                if isinstance(s, Configs):
                    assert isinstance(o, Configs)
                    o = s.diff(o)
                result[f.name] = o
        return result
    def copy_from(self, **kwargs):
        for k, v in kwargs.items():
            attr = getattr(self, k)
            if isinstance(attr, Configs):
                attr.copy_from(**v)
            else:
                setattr(self, k, v)
    def load(self, *files: File):
        for f in files:
            self.copy_from(_load(f))
    def load_from_environ(
        self,
        prefix: str,
        environ: Optional[Dict] = None,
        verbose: bool = True,
    ):
        if environ is None:
            environ = os.environ
        pre = prefix.strip('_').upper() + '_'
        items = sorted(environ.items())
        items = ((k, v) for k, v in items if k.startswith(pre))
        for k, v in items:
            attr_name = k[len(pre):].lower()
            splits = list(_split_address(v, attr_name))
            if len(splits) == 1:
                parent, attr = splits[0]
                str_val = getattr(parent, attr)
                val = _string_value(k, v, str_val)
                setattr(parent, attr, val)
            elif not verbose:
                continue
            elif not splits:
                print('No configs match', k, file=sys.err)
            else:
                print('More than one config matches', k, file=sys.err)
def _split_address(parent, key):
    if key in dir(parent):
        yield parent, key
    else:
        for k in dir(parent):
            if k.startswith(key + '_'):
                k = key[len(key) + 1:]
                new_parent = getattr(parent, k)
                yield from _split_address(new_parent, k)
def _string_value(name, v, original_value):
    if original_value is None or isinstance(v, str):
        return v
    if isinstance(original_value, int):
        return int(v)
    if isinstance(original_value, float):
        return float(v)
    if isinstance(original_value, bool):
        if v.lower() in ('t', 'true'):
            return True
        if v.lower() in ('f', 'false'):
            return False
        raise ValueError(f'Cannot understand bool {name}={v}')
    if isinstance(original_value, Enum):
        return type(original_value)[v]
    return json.loads(v)
def _load(p):
    if p.suffix == '.json':
        return json.loads(p.read_text())
    if p.suffix == '.toml':
        try:
            import tomllib
            return tomllib.loads(p.read_text())
        except ImportError:
            import tomlkit
            return tomlkit.loads(p.read_text())
    if p.suffix == '.yaml':
        import yaml
        return yaml.safe_load(p.read_text())
    raise ValueError('Do not understand suffix=' + p.suffix)
_getenv = os.environ.get
_expandvars = os.path.expandvars
class App:
    """
    `cfg.App` is the main class, but it has no methods - it just holds the
    `config`, `data`, `cache` and `xdg` objects.
    """
    DEFAULT_FORMAT = 'json'
    """The default, default file format for all Apps"""
    def __init__(
        self, name, format=DEFAULT_FORMAT, read_kwds=None, write_kwds=None
    ):
        """
        Arguments:
          name: the name of the App.  `name` is used as a directory
                name so it should not contain any characters illegal in
                pathnames
          format: the format for config and data files from this App
        """
        def path(attrname):
            path = getattr(self.xdg, attrname)
            if attrname.endswith('DIRS'):
                return [os.path.join(i, self.name) for i in path.split(':')]
            return os.path.join(path, self.name)
        _check_filename(name)
        self.name = name
        """The text name of the App"""
        self.xdg = XDG()
        """A `cfg.XFG` as of when the App was constructed."""
        self.cache = Cache(path('XDG_CACHE_HOME'))
        """A `cfg.Cache` that manages cache directories"""
        if format not in FORMATS:
            raise ValueError('Unknown format', format)
        if format == 'configparser':
            self.format = ConfigparserFormat()
            """A `cfgs.Format` representing the data format."""
        else:
            self.format = Format(format, read_kwds, write_kwds)
        h, d = path('XDG_CONFIG_HOME'), path('XDG_CONFIG_DIRS')
        self.config = Directory(h, d, self.format)
        """A `cfgs.Directory` for config files"""
        h, d = path('XDG_DATA_HOME'), path('XDG_DATA_DIRS')
        self.data = Directory(h, d, self.format)
        """A `cfgs.Directory` for data files"""
class XDG:
    """
    The XDG Base Directory Spec mandates six directories for config and data
    files, caches and runtime files, with default values that can be overridden
    through environment variables.  This class takes a snapshot of these six
    directories using the current environment.
    """
    def __init__(self):
        """
        Construct the class with a snapshot of the six XDG base directories
        """
        def get(k, v):
            return _getenv(k) or _expandvars(v)
        self.XDG_CACHE_HOME = get('XDG_CACHE_HOME', '$HOME/.cache')
        """Base directory relative to which
           user-specific non-essential (cached) data should be written
        """
        self.XDG_CONFIG_DIRS = get('XDG_CONFIG_DIRS', '/etc/xdg')
        """A set of preference ordered base directories relative to which
           configuration files should be searched
        """
        self.XDG_CONFIG_HOME = get('XDG_CONFIG_HOME', '$HOME/.config')
        """Base directory relative to which user-specific
           configuration files should be written
        """
        self.XDG_DATA_DIRS = get(
            'XDG_DATA_DIRS', '/usr/local/share/:/usr/share/'
        )
        """A set of preference ordered base directories relative to which
           data files should be searched
        """
        self.XDG_DATA_HOME = get('XDG_DATA_HOME', '$HOME/.local/share')
        """Base directory relative to which user-specific
           data files should be written
        """
        self.XDG_RUNTIME_DIR = get('XDG_RUNTIME_DIR', '')
        """Base directory relative to which
           user-specific runtime files and other file objects should be placed
        """
class Directory:
    """
    An XDG directory of persistent, formatted files
    """
    def __init__(self, home, dirs, format):
        """
        Don't call this constructor directly - use either
        `cfgs.App.config` or `cfgs.App.data` instead.
        """
        self.home = home
        self.dirs = dirs
        assert not isinstance(format, str)
        self.format = format
        self.dirs.insert(0, self.home)
    def open(self, filename=None):
        """
        Open a persistent `cfg.File`.
        Arguments:
          filename: The name of the persistent file. If None,
            `filename` defaults to `cfg.App.name` plus the format suffix
          format: A string representing the file format.  If None,
             first try to guess the filename from the filename, then use
             `self.format`
        """
        if not filename:
            basename = os.path.basename(self.home)
            suffix = FORMAT_TO_SUFFIX[self.format.name]
            filename = '%s%s' % (basename, suffix)
        elif filename.startswith('/'):
            filename = filename[1:]
        return File(self.full_name(filename), self.format)
    def all_files(self, filename):
        """
        Yield all filenames matching the argument in either the home
        directory or any of the search directories
        """
        for p in self.dirs:
            full_path = os.path.join(p, filename)
            try:
                yield open(full_path) and full_path
            except IOError:
                pass
    def full_name(self, filename):
        """
        Return the full name of a file with respect to this XDG directory
        """
        return os.path.join(self.home, filename)
class File:
    """
    A formatted data or config file where you can set and get items,
    and read or write.
    """
    def __init__(self, filename, format):
        """Do not call this constructor directly but use
        `cfg.Directory.open` instead"""
        self.filename = filename
        """The full pathname to the data file"""
        self.contents = {}
        """The contents of the formatted file, read and parsed.
        This will be a `dict` for all formats except `configparser`,
        where it will be a `configparser.SafeConfigParser`.
        """
        os.makedirs(os.path.dirname(self.filename), exist_ok=True)
        self.format = format
        self.read()
    def read(self):
        """Re-read the contents from the file"""
        try:
            with open(self.filename) as fp:
                self.contents = self.format.read(fp)
        except IOError:
            self.contents = self.format.create()
        return self.contents
    def write(self):
        """Write the contents to the file"""
        with open(self.filename, 'w') as fp:
            self.format.write(self.contents, fp)
    def as_dict(self):
        """Return a deep copy of the contents as a dict"""
        return self.format.as_dict(self.contents)
    def clear(self):
        """Clear the contents without writing"""
        self.contents.clear()
    def __enter__(self):
        return self
    def __exit__(self, *args):
        self.write()
class Cache:
    """
    A class that creates caches
    """
    def __init__(self, dirname):
        """Do not call this constructor - instead use `cfgs.App.cache` """
        self.dirname = dirname
        """The full path of the root directory for all cache directories"""
    def directory(self, name='cache', cache_size=0):
        """
        Return a `cfgs.CacheDirectory`
        Arguments:
          name: The relative pathname of the cache directory
          cache_size: The number of bytes allowed in the cache.
              The default of 0 means "unlimited cache size"
        """
        name = os.path.join(self.dirname, name)
        return CacheDirectory(name, cache_size)
class CacheDirectory:
    def __init__(self, dirname, cache_size):
        """Do not call this constructor - use `cfgs.Cache.directory`"""
        self.dirname = dirname
        """The full path to this cache directory"""
        self.cache_size = cache_size
        """
        The number of bytes allowed in the cache.
        0 means "unlimited cache size"
        """
        _makedirs(self.dirname)
        self.prune()
    def open(self, filename, size_guess=0, binary=False):
        """
        Open a cached file in this directory.
        If the file already exists, it is opened for read.
        Otherwise the cache is pruned and the file is opened for write.
        Arguments:
          filename: the name of the file, relative to the cache directory
          size_guess: A guess as to how large the file will be, in bytes
          binary: if True, the file is opened in binary mode
        """
        if '/' in filename:
            raise ValueError('Subdirectories are not allowed in caches')
        bin = 'b' if binary else ''
        full = os.path.join(self.dirname, filename)
        if os.path.exists(full):
            return open(full, 'r' + bin)
        self.prune(size_guess)
        return open(full, 'w' + bin)
    def prune(self, bytes_needed=0):
        """
        Prune the cache to generate at least `bytes_needed` of free space,
        if this is possible.
        """
        if not self.cache_size:
            return
        files = os.listdir(self.dirname)
        info = {f: os.stat(os.path.join(self.dirname, f)) for f in files}
        required_size = sum(s.st_size for f, s in info.items()) + bytes_needed
        if required_size <= self.cache_size:
            return
        # Delete oldest items first
        for f, s in sorted(info.items(), key=lambda x: x[1].st_mtime):
            os.remove(os.path.join(self.dirname, f))
            required_size -= s.st_size
            if required_size <= self.cache_size:
                return
def _check_filename(filename):
    # Just a heuristic - names might pass this test and still not
    # be valid i.e. CON on Windows.
    bad_chars = _BAD_CHARS.intersection(set(filename))
    if bad_chars:
        bad_chars = ''.join(sorted(bad_chars))
        raise ValueError('Invalid characters in filename: "%s"' % bad_chars)
SUFFIX_TO_FORMAT = {
    '.cfg': 'configparser',
    '.ini': 'configparser',
    '.json': 'json',
    '.toml': 'toml',
    '.yaml': 'yaml',
    '.yml': 'yaml',
}
"""
Map file suffixes to the file format - the partial inverse
to `cfgs.FORMAT_TO_SUFFIX`
"""
FORMAT_TO_SUFFIX = {
    'configparser': '.ini',
    'json': '.json',
    'toml': '.toml',
    'yaml': '.yml',
}
"""
Map file formats to file suffix - the partial inverse
to `cfgs.SUFFIX_TO_FORMAT`
"""
FORMATS = set(SUFFIX_TO_FORMAT.values())
"""A list of all formats that `cfgs` understands."""
class Format:
    def __init__(self, format, read_kwds, write_kwds):
        self.name = format
        """The name of this format"""
        self._read_kwds = read_kwds or {}
        self._write_kwds = write_kwds or {}
        self._parser = __import__(format)
    def read(self, fp):
        """Read contents from an open file in this format"""
        load = getattr(self._parser, 'safe_load', self._parser.load)
        return load(fp, **self._read_kwds)
    def write(self, contents, fp):
        """Write contents in this format to an open file"""
        dump = getattr(self._parser, 'safe_dump', self._parser.dump)
        return dump(contents, fp, **self._write_kwds)
    def create(self):
        """Return new, empty contents"""
        return {}
    def as_dict(self, contents):
        """Convert the contents to a dict"""
        return copy.deepcopy(contents)
class ConfigparserFormat(Format):
    name = 'configparser'
    """The name of the configparser format"""
    def __init__(self):
        self._parser = __import__(self.name)
    def read(self, fp):
        """Read contents from an open file in this format"""
        contents = self.create()
        contents.readfp(fp)
        return contents
    def write(self, contents, fp):
        """Write contents in this format to an open file"""
        contents.write(fp)
    def create(self):
        """Return new, empty contents"""
        return self._parser.SafeConfigParser()
    def as_dict(self, contents):
        """Convert the contents to a dict"""
        return {k: dict(v) for k, v in contents.items()}
def _makedirs(f):  # For Python 2 compatibility
    try:
        os.makedirs(f)
    except Exception:
        pass
_BAD_CHARS = set('/\\?%*:|"<>\';')
Global variables
var FORMATS- 
A list of all formats that
cfgsunderstands. var FORMAT_TO_SUFFIX- 
Map file formats to file suffix - the partial inverse to
SUFFIX_TO_FORMAT var SUFFIX_TO_FORMAT- 
Map file suffixes to the file format - the partial inverse to
FORMAT_TO_SUFFIX 
Classes
class App (name, format='json', read_kwds=None, write_kwds=None)- 
cfg.Appis the main class, but it has no methods - it just holds theconfig,data,cacheandxdgobjects.Arguments
name: the name of the App.
nameis used as a directory name so it should not contain any characters illegal in pathnamesformat: the format for config and data files from this App
Expand source code
class App: """ `cfg.App` is the main class, but it has no methods - it just holds the `config`, `data`, `cache` and `xdg` objects. """ DEFAULT_FORMAT = 'json' """The default, default file format for all Apps""" def __init__( self, name, format=DEFAULT_FORMAT, read_kwds=None, write_kwds=None ): """ Arguments: name: the name of the App. `name` is used as a directory name so it should not contain any characters illegal in pathnames format: the format for config and data files from this App """ def path(attrname): path = getattr(self.xdg, attrname) if attrname.endswith('DIRS'): return [os.path.join(i, self.name) for i in path.split(':')] return os.path.join(path, self.name) _check_filename(name) self.name = name """The text name of the App""" self.xdg = XDG() """A `cfg.XFG` as of when the App was constructed.""" self.cache = Cache(path('XDG_CACHE_HOME')) """A `cfg.Cache` that manages cache directories""" if format not in FORMATS: raise ValueError('Unknown format', format) if format == 'configparser': self.format = ConfigparserFormat() """A `cfgs.Format` representing the data format.""" else: self.format = Format(format, read_kwds, write_kwds) h, d = path('XDG_CONFIG_HOME'), path('XDG_CONFIG_DIRS') self.config = Directory(h, d, self.format) """A `cfgs.Directory` for config files""" h, d = path('XDG_DATA_HOME'), path('XDG_DATA_DIRS') self.data = Directory(h, d, self.format) """A `cfgs.Directory` for data files"""Class variables
var DEFAULT_FORMAT- 
The default, default file format for all Apps
 
Instance variables
 class Cache (dirname)- 
A class that creates caches
Do not call this constructor - instead use
App.cacheExpand source code
class Cache: """ A class that creates caches """ def __init__(self, dirname): """Do not call this constructor - instead use `cfgs.App.cache` """ self.dirname = dirname """The full path of the root directory for all cache directories""" def directory(self, name='cache', cache_size=0): """ Return a `cfgs.CacheDirectory` Arguments: name: The relative pathname of the cache directory cache_size: The number of bytes allowed in the cache. The default of 0 means "unlimited cache size" """ name = os.path.join(self.dirname, name) return CacheDirectory(name, cache_size)Instance variables
var dirname- 
The full path of the root directory for all cache directories
 
Methods
def directory(self, name='cache', cache_size=0)- 
Return a
CacheDirectoryArguments
name: The relative pathname of the cache directory
cache_size: The number of bytes allowed in the cache. The default of 0 means "unlimited cache size"
Expand source code
def directory(self, name='cache', cache_size=0): """ Return a `cfgs.CacheDirectory` Arguments: name: The relative pathname of the cache directory cache_size: The number of bytes allowed in the cache. The default of 0 means "unlimited cache size" """ name = os.path.join(self.dirname, name) return CacheDirectory(name, cache_size) 
 class CacheDirectory (dirname, cache_size)- 
Do not call this constructor - use
Cache.directory()Expand source code
class CacheDirectory: def __init__(self, dirname, cache_size): """Do not call this constructor - use `cfgs.Cache.directory`""" self.dirname = dirname """The full path to this cache directory""" self.cache_size = cache_size """ The number of bytes allowed in the cache. 0 means "unlimited cache size" """ _makedirs(self.dirname) self.prune() def open(self, filename, size_guess=0, binary=False): """ Open a cached file in this directory. If the file already exists, it is opened for read. Otherwise the cache is pruned and the file is opened for write. Arguments: filename: the name of the file, relative to the cache directory size_guess: A guess as to how large the file will be, in bytes binary: if True, the file is opened in binary mode """ if '/' in filename: raise ValueError('Subdirectories are not allowed in caches') bin = 'b' if binary else '' full = os.path.join(self.dirname, filename) if os.path.exists(full): return open(full, 'r' + bin) self.prune(size_guess) return open(full, 'w' + bin) def prune(self, bytes_needed=0): """ Prune the cache to generate at least `bytes_needed` of free space, if this is possible. """ if not self.cache_size: return files = os.listdir(self.dirname) info = {f: os.stat(os.path.join(self.dirname, f)) for f in files} required_size = sum(s.st_size for f, s in info.items()) + bytes_needed if required_size <= self.cache_size: return # Delete oldest items first for f, s in sorted(info.items(), key=lambda x: x[1].st_mtime): os.remove(os.path.join(self.dirname, f)) required_size -= s.st_size if required_size <= self.cache_size: returnInstance variables
var cache_size- 
The number of bytes allowed in the cache. 0 means "unlimited cache size"
 var dirname- 
The full path to this cache directory
 
Methods
def open(self, filename, size_guess=0, binary=False)- 
Open a cached file in this directory.
If the file already exists, it is opened for read.
Otherwise the cache is pruned and the file is opened for write.
Arguments
filename: the name of the file, relative to the cache directory size_guess: A guess as to how large the file will be, in bytes binary: if True, the file is opened in binary mode
Expand source code
def open(self, filename, size_guess=0, binary=False): """ Open a cached file in this directory. If the file already exists, it is opened for read. Otherwise the cache is pruned and the file is opened for write. Arguments: filename: the name of the file, relative to the cache directory size_guess: A guess as to how large the file will be, in bytes binary: if True, the file is opened in binary mode """ if '/' in filename: raise ValueError('Subdirectories are not allowed in caches') bin = 'b' if binary else '' full = os.path.join(self.dirname, filename) if os.path.exists(full): return open(full, 'r' + bin) self.prune(size_guess) return open(full, 'w' + bin) def prune(self, bytes_needed=0)- 
Prune the cache to generate at least
bytes_neededof free space, if this is possible.Expand source code
def prune(self, bytes_needed=0): """ Prune the cache to generate at least `bytes_needed` of free space, if this is possible. """ if not self.cache_size: return files = os.listdir(self.dirname) info = {f: os.stat(os.path.join(self.dirname, f)) for f in files} required_size = sum(s.st_size for f, s in info.items()) + bytes_needed if required_size <= self.cache_size: return # Delete oldest items first for f, s in sorted(info.items(), key=lambda x: x[1].st_mtime): os.remove(os.path.join(self.dirname, f)) required_size -= s.st_size if required_size <= self.cache_size: return 
 class ConfigparserFormat- 
Expand source code
class ConfigparserFormat(Format): name = 'configparser' """The name of the configparser format""" def __init__(self): self._parser = __import__(self.name) def read(self, fp): """Read contents from an open file in this format""" contents = self.create() contents.readfp(fp) return contents def write(self, contents, fp): """Write contents in this format to an open file""" contents.write(fp) def create(self): """Return new, empty contents""" return self._parser.SafeConfigParser() def as_dict(self, contents): """Convert the contents to a dict""" return {k: dict(v) for k, v in contents.items()}Ancestors
Inherited members
 class Configs- 
Expand source code
class Configs: def diff(self, other: Any): assert self.__class__ is other.__class__ result = {} for f in dc.fields(self): s, o = getattr(self, f.name), getattr(other, f.name) if s != o: if isinstance(s, Configs): assert isinstance(o, Configs) o = s.diff(o) result[f.name] = o return result def copy_from(self, **kwargs): for k, v in kwargs.items(): attr = getattr(self, k) if isinstance(attr, Configs): attr.copy_from(**v) else: setattr(self, k, v) def load(self, *files: File): for f in files: self.copy_from(_load(f)) def load_from_environ( self, prefix: str, environ: Optional[Dict] = None, verbose: bool = True, ): if environ is None: environ = os.environ pre = prefix.strip('_').upper() + '_' items = sorted(environ.items()) items = ((k, v) for k, v in items if k.startswith(pre)) for k, v in items: attr_name = k[len(pre):].lower() splits = list(_split_address(v, attr_name)) if len(splits) == 1: parent, attr = splits[0] str_val = getattr(parent, attr) val = _string_value(k, v, str_val) setattr(parent, attr, val) elif not verbose: continue elif not splits: print('No configs match', k, file=sys.err) else: print('More than one config matches', k, file=sys.err)Methods
def copy_from(self, **kwargs)- 
Expand source code
def copy_from(self, **kwargs): for k, v in kwargs.items(): attr = getattr(self, k) if isinstance(attr, Configs): attr.copy_from(**v) else: setattr(self, k, v) def diff(self, other: Any)- 
Expand source code
def diff(self, other: Any): assert self.__class__ is other.__class__ result = {} for f in dc.fields(self): s, o = getattr(self, f.name), getattr(other, f.name) if s != o: if isinstance(s, Configs): assert isinstance(o, Configs) o = s.diff(o) result[f.name] = o return result def load(self, *files: Tuple[Union[pathlib.Path, str]])- 
Expand source code
def load(self, *files: File): for f in files: self.copy_from(_load(f)) def load_from_environ(self, prefix: str, environ: Optional[Dict] = None, verbose: bool = True)- 
Expand source code
def load_from_environ( self, prefix: str, environ: Optional[Dict] = None, verbose: bool = True, ): if environ is None: environ = os.environ pre = prefix.strip('_').upper() + '_' items = sorted(environ.items()) items = ((k, v) for k, v in items if k.startswith(pre)) for k, v in items: attr_name = k[len(pre):].lower() splits = list(_split_address(v, attr_name)) if len(splits) == 1: parent, attr = splits[0] str_val = getattr(parent, attr) val = _string_value(k, v, str_val) setattr(parent, attr, val) elif not verbose: continue elif not splits: print('No configs match', k, file=sys.err) else: print('More than one config matches', k, file=sys.err) 
 class Directory (home, dirs, format)- 
An XDG directory of persistent, formatted files
Don't call this constructor directly - use either
App.configorApp.datainstead.Expand source code
class Directory: """ An XDG directory of persistent, formatted files """ def __init__(self, home, dirs, format): """ Don't call this constructor directly - use either `cfgs.App.config` or `cfgs.App.data` instead. """ self.home = home self.dirs = dirs assert not isinstance(format, str) self.format = format self.dirs.insert(0, self.home) def open(self, filename=None): """ Open a persistent `cfg.File`. Arguments: filename: The name of the persistent file. If None, `filename` defaults to `cfg.App.name` plus the format suffix format: A string representing the file format. If None, first try to guess the filename from the filename, then use `self.format` """ if not filename: basename = os.path.basename(self.home) suffix = FORMAT_TO_SUFFIX[self.format.name] filename = '%s%s' % (basename, suffix) elif filename.startswith('/'): filename = filename[1:] return File(self.full_name(filename), self.format) def all_files(self, filename): """ Yield all filenames matching the argument in either the home directory or any of the search directories """ for p in self.dirs: full_path = os.path.join(p, filename) try: yield open(full_path) and full_path except IOError: pass def full_name(self, filename): """ Return the full name of a file with respect to this XDG directory """ return os.path.join(self.home, filename)Methods
def all_files(self, filename)- 
Yield all filenames matching the argument in either the home directory or any of the search directories
Expand source code
def all_files(self, filename): """ Yield all filenames matching the argument in either the home directory or any of the search directories """ for p in self.dirs: full_path = os.path.join(p, filename) try: yield open(full_path) and full_path except IOError: pass def full_name(self, filename)- 
Return the full name of a file with respect to this XDG directory
Expand source code
def full_name(self, filename): """ Return the full name of a file with respect to this XDG directory """ return os.path.join(self.home, filename) def open(self, filename=None)- 
Open a persistent
cfg.File.Arguments
filename: The name of the persistent file. If None,
filenamedefaults tocfg.App.nameplus the format suffixformat: A string representing the file format. If None, first try to guess the filename from the filename, then use
self.formatExpand source code
def open(self, filename=None): """ Open a persistent `cfg.File`. Arguments: filename: The name of the persistent file. If None, `filename` defaults to `cfg.App.name` plus the format suffix format: A string representing the file format. If None, first try to guess the filename from the filename, then use `self.format` """ if not filename: basename = os.path.basename(self.home) suffix = FORMAT_TO_SUFFIX[self.format.name] filename = '%s%s' % (basename, suffix) elif filename.startswith('/'): filename = filename[1:] return File(self.full_name(filename), self.format) 
 class File (filename, format)- 
A formatted data or config file where you can set and get items, and read or write.
Do not call this constructor directly but use
cfg.Directory.openinsteadExpand source code
class File: """ A formatted data or config file where you can set and get items, and read or write. """ def __init__(self, filename, format): """Do not call this constructor directly but use `cfg.Directory.open` instead""" self.filename = filename """The full pathname to the data file""" self.contents = {} """The contents of the formatted file, read and parsed. This will be a `dict` for all formats except `configparser`, where it will be a `configparser.SafeConfigParser`. """ os.makedirs(os.path.dirname(self.filename), exist_ok=True) self.format = format self.read() def read(self): """Re-read the contents from the file""" try: with open(self.filename) as fp: self.contents = self.format.read(fp) except IOError: self.contents = self.format.create() return self.contents def write(self): """Write the contents to the file""" with open(self.filename, 'w') as fp: self.format.write(self.contents, fp) def as_dict(self): """Return a deep copy of the contents as a dict""" return self.format.as_dict(self.contents) def clear(self): """Clear the contents without writing""" self.contents.clear() def __enter__(self): return self def __exit__(self, *args): self.write()Instance variables
var contents- 
The contents of the formatted file, read and parsed.
This will be a
dictfor all formats exceptconfigparser, where it will be aconfigparser.SafeConfigParser. var filename- 
The full pathname to the data file
 
Methods
def as_dict(self)- 
Return a deep copy of the contents as a dict
Expand source code
def as_dict(self): """Return a deep copy of the contents as a dict""" return self.format.as_dict(self.contents) def clear(self)- 
Clear the contents without writing
Expand source code
def clear(self): """Clear the contents without writing""" self.contents.clear() def read(self)- 
Re-read the contents from the file
Expand source code
def read(self): """Re-read the contents from the file""" try: with open(self.filename) as fp: self.contents = self.format.read(fp) except IOError: self.contents = self.format.create() return self.contents def write(self)- 
Write the contents to the file
Expand source code
def write(self): """Write the contents to the file""" with open(self.filename, 'w') as fp: self.format.write(self.contents, fp) 
 class Format (format, read_kwds, write_kwds)- 
Expand source code
class Format: def __init__(self, format, read_kwds, write_kwds): self.name = format """The name of this format""" self._read_kwds = read_kwds or {} self._write_kwds = write_kwds or {} self._parser = __import__(format) def read(self, fp): """Read contents from an open file in this format""" load = getattr(self._parser, 'safe_load', self._parser.load) return load(fp, **self._read_kwds) def write(self, contents, fp): """Write contents in this format to an open file""" dump = getattr(self._parser, 'safe_dump', self._parser.dump) return dump(contents, fp, **self._write_kwds) def create(self): """Return new, empty contents""" return {} def as_dict(self, contents): """Convert the contents to a dict""" return copy.deepcopy(contents)Subclasses
Instance variables
var name- 
The name of this format
 
Methods
def as_dict(self, contents)- 
Convert the contents to a dict
Expand source code
def as_dict(self, contents): """Convert the contents to a dict""" return copy.deepcopy(contents) def create(self)- 
Return new, empty contents
Expand source code
def create(self): """Return new, empty contents""" return {} def read(self, fp)- 
Read contents from an open file in this format
Expand source code
def read(self, fp): """Read contents from an open file in this format""" load = getattr(self._parser, 'safe_load', self._parser.load) return load(fp, **self._read_kwds) def write(self, contents, fp)- 
Write contents in this format to an open file
Expand source code
def write(self, contents, fp): """Write contents in this format to an open file""" dump = getattr(self._parser, 'safe_dump', self._parser.dump) return dump(contents, fp, **self._write_kwds) 
 class XDG- 
The XDG Base Directory Spec mandates six directories for config and data files, caches and runtime files, with default values that can be overridden through environment variables. This class takes a snapshot of these six directories using the current environment.
Construct the class with a snapshot of the six XDG base directories
Expand source code
class XDG: """ The XDG Base Directory Spec mandates six directories for config and data files, caches and runtime files, with default values that can be overridden through environment variables. This class takes a snapshot of these six directories using the current environment. """ def __init__(self): """ Construct the class with a snapshot of the six XDG base directories """ def get(k, v): return _getenv(k) or _expandvars(v) self.XDG_CACHE_HOME = get('XDG_CACHE_HOME', '$HOME/.cache') """Base directory relative to which user-specific non-essential (cached) data should be written """ self.XDG_CONFIG_DIRS = get('XDG_CONFIG_DIRS', '/etc/xdg') """A set of preference ordered base directories relative to which configuration files should be searched """ self.XDG_CONFIG_HOME = get('XDG_CONFIG_HOME', '$HOME/.config') """Base directory relative to which user-specific configuration files should be written """ self.XDG_DATA_DIRS = get( 'XDG_DATA_DIRS', '/usr/local/share/:/usr/share/' ) """A set of preference ordered base directories relative to which data files should be searched """ self.XDG_DATA_HOME = get('XDG_DATA_HOME', '$HOME/.local/share') """Base directory relative to which user-specific data files should be written """ self.XDG_RUNTIME_DIR = get('XDG_RUNTIME_DIR', '') """Base directory relative to which user-specific runtime files and other file objects should be placed """Instance variables
var XDG_CACHE_HOME- 
Base directory relative to which user-specific non-essential (cached) data should be written
 var XDG_CONFIG_DIRS- 
A set of preference ordered base directories relative to which configuration files should be searched
 var XDG_CONFIG_HOME- 
Base directory relative to which user-specific configuration files should be written
 var XDG_DATA_DIRS- 
A set of preference ordered base directories relative to which data files should be searched
 var XDG_DATA_HOME- 
Base directory relative to which user-specific data files should be written
 var XDG_RUNTIME_DIR- 
Base directory relative to which user-specific runtime files and other file objects should be placed