From 079b4da93ea336b5bcc801cfd64c310aa7f8ddee Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Fri, 6 Jan 2023 12:20:22 +0100 Subject: config early port (test still fails) --- tagit/config/__init__.py | 25 +++ tagit/config/loader.py | 83 +++++++ tagit/config/schema.py | 283 ++++++++++++++++++++++++ tagit/config/settings.json | 70 ++++++ tagit/config/settings.py | 473 ++++++++++++++++++++++++++++++++++++++++ tagit/config/types.py | 273 +++++++++++++++++++++++ tagit/config/user-defaults.json | 142 ++++++++++++ tagit/config/utils.py | 104 +++++++++ 8 files changed, 1453 insertions(+) create mode 100644 tagit/config/__init__.py create mode 100644 tagit/config/loader.py create mode 100644 tagit/config/schema.py create mode 100644 tagit/config/settings.json create mode 100644 tagit/config/settings.py create mode 100644 tagit/config/types.py create mode 100644 tagit/config/user-defaults.json create mode 100644 tagit/config/utils.py (limited to 'tagit/config') diff --git a/tagit/config/__init__.py b/tagit/config/__init__.py new file mode 100644 index 0000000..c9edb15 --- /dev/null +++ b/tagit/config/__init__.py @@ -0,0 +1,25 @@ +"""Configuration system. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# inner-module imports +from . import loader +from . import utils +from .loader import TAGITRC, DEFAULT_USER_CONFIG +from .schema import schema, declare, declare_title +from .settings import Settings, ConfigError +from .types import * + +# exports +__all__: typing.Sequence[str] = ( + 'Settings', + 'TAGITRC', + 'schema', + ) + +## EOF ## diff --git a/tagit/config/loader.py b/tagit/config/loader.py new file mode 100644 index 0000000..489b063 --- /dev/null +++ b/tagit/config/loader.py @@ -0,0 +1,83 @@ +"""High-level config loading. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import logging +import os +import shutil +import typing + +# inner-module imports +from .settings import Settings + +# constants + +TAGITRC = '.tagitrc' + +DEFAULT_USER_CONFIG = os.path.join(os.path.dirname(__file__), 'user-defaults.json') + +SETTINGS_PATH = [ + # user home + os.path.expanduser(os.path.join('~', TAGITRC)), + # installation directory + '/usr/share/tagit/settings', + '/usr/share/tagit/keybindings', + # module defaults + os.path.join(os.path.dirname(__file__), 'settings.json'), + ] + +# exports +__all__: typing.Sequence[str] = ( + 'load_settings', + ) + + +## code ## + +logger = logging.getLogger(__name__) + +def load_settings(path=None, verbose=0): + """Load application settings. + The settings are loaded from the specified *path* and from all default + search paths (see *SETTINGS_PATH*). More specific locations overwrite + less specific ones. Every config key comes with a default value that + applies if it is not specified in the config files. + """ + verbose = max(0, verbose) + + # build searchpaths + searchpaths = [] + searchpaths += [path] if path is not None else [] + searchpaths += SETTINGS_PATH + + # create default user config on first start + first_start = False + user_config = os.path.expanduser(os.path.join('~', TAGITRC)) + if os.path.exists(DEFAULT_USER_CONFIG) and not os.path.exists(user_config): + first_start = True + shutil.copy(DEFAULT_USER_CONFIG, user_config) + + # scan searchpaths + cfg = Settings() + for path in searchpaths[::-1]: + if verbose > 0 or cfg('session', 'verbose') > 0: + print(f'Loading settings from {path}') + + if path is not None and os.path.exists(path): + try: + cfg.update(Settings.Open(path, clear_defaults=False)) + + except TypeError as e: # schema violation + logger.critical(f'Encountered a config error while loading {path}: {e}') + raise e + + # update verbosity from argument + cfg.set(('session', 'verbose'), max(cfg('session', 'verbose'), verbose)) + # set first start according to previous user config existence + cfg.set(('session', 'first_start'), first_start) + return cfg + +## EOF ## diff --git a/tagit/config/schema.py b/tagit/config/schema.py new file mode 100644 index 0000000..7f1c17a --- /dev/null +++ b/tagit/config/schema.py @@ -0,0 +1,283 @@ +"""Definition of a configuration schema. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import abc +import logging +import typing + +# tagit imports +from tagit.utils import errors, fst + +# exports +__all__: typing.Sequence[str] = ( + 'ConfigSchema', + 'declare', + 'declare_title', + ) + + +## code ## + +logger = logging.getLogger(__name__) + +class IncompatibleTypes(Exception): + """Raised if a config key is declared multiple times with incompatible signatures.""" + pass + +class ConfigSchema(abc.Collection, abc.Callable, abc.Hashable): + """The config schema registers types, defaults, and documentation for + configuration keys. The specification of a config key can be accessed in + dict-style (schema[key]) or Settings-style (schema(key)). A global schema + is instantiated to be used by tagit modules to declare their config keys. + + In addition to config keys, the class supports titles for documentation + of configuration sections (essentially any part of a config key that has + no value assigned to it). + """ + def __init__(self): + self.config = dict() + self.titles = dict() + + ## interfaces + + def __hash__(self): + return hash((type(self), + tuple(sorted([(hash(k), hash(v)) for k, v in self.config.items()], key=fst)), + tuple(sorted([(hash(k), hash(v)) for k, v in self.titles.items()], key=fst)))) + + def __call__(self, *key): + """Return the definition of a *key*.""" + return self.config[key] + + def __getitem__(self, key): + """Return the definition of a *key*.""" + return self.config[key] + + def __contains__(self, key): + """Return True if the config key *key* was declared.""" + return key in self.config + + def __iter__(self): + """Iterate over all declared config keys.""" + return iter(self.config) + + def __len__(self): + """Return the number of declared config keys.""" + return len(self.config) + + def keys(self, titles=False): + """Return an iterator over all declared config keys. + If *titles* is True, also return the declared title keys. + """ + if titles: + return iter(set(self.config.keys()) | set(self.titles.keys())) + else: + return self.config.keys() + + ## titles extras + + def is_title(self, key): + """Return True if the *key* matches a section.""" + return key in self.titles and key not in self.config + + def get_title(self, key): + """Return the section title of *key*.""" + return self.titles[key] + + ## declaration interface + + def declare(self, key, type, default, + module=None, title=None, description=None, example=None): + """Declare a configuration key. + + A key cannot be declared multiple times unless it has the same type + annotation and default value. + + :param:`key` Configuration key as tuple + :param:`type` Value type definition + :param:`default` Default value + :param:`module` Declaring module + :param:`title` Reader friendly name + :param:`description` Verbose description of its effect + :param:`example` Usage example + + """ + if len(key) == 0: + raise errors.ProgrammingError('the config key must contain at least one item.') + + # FIXME: can't have a rule for a subkey + # e.g. ('session', ): String() and ('session', 'verbose'): Int() + key = tuple(key) + if key in self.config: + # declaration exists, check compatibility + if self.config[key].type == type and \ + self.config[key].default == default: + # types are compatible, set/overwrite values + self.config[key].modules = module + self.config[key].title = title + self.config[key].description = description + self.config[key].example = example + logger.warning(f'config schema: potentially overwriting key {key}') + else: + raise IncompatibleTypes(f'declaration of {key} violates a previous declaration') + + elif type.check(default): + self.config[key] = ConfigKey(key, type, default, module, title, description, example) + + else: + raise errors.ProgrammingError('default value violates value type specification') + + def declare_title(self, key, module, title, description=None): + """Declare a config section title. Section titles are only used for + documentation purposes. + + :param:`key` Configuration key as tuple + :param:`module` Declaring module + :param:`title` Reader friendly name + :param:`description` Verbose description of its effect + + """ + if len(key) == 0: + raise errors.ProgrammingError('the config key must contain at least one item.') + + key = tuple(key) + if key in self.titles: + self.titles[key].title = title + self.titles[key].modules = module + self.titles[key].description = description + logger.warn(f'config schema: potentially overwriting title {key}') + else: + self.titles[key] = ConfigTitle(key, title, module, description) + + +class ConfigTitle(abc.Hashable): + """Title and description of a config key. Used for documentation.""" + def __init__(self, key, title=None, module=None, description=None): + self._key = key + self._title = title + self._description = description + self._modules = {module} if module is not None else set() + + def __repr__(self): + return f'ConfigTitle({self.key}, {self.title})' + + def __eq__(self, other): + return isinstance(other, type(self)) and self._key == other._key + + def __hash__(self): + return hash((type(self), self._key)) + + @property + def branch(self): + """Return the branch.""" + return self._key[:-1] + + @property + def leaf(self): + """Return the leaf.""" + return self._key[-1] + + @property + def key(self): + """Return the key.""" + return self._key + + @property + def title(self): + """Return the key's title.""" + return self._title if self._title is not None else self.leaf + + @title.setter + def title(self, title): + """Overwrite the key's title.""" + if title is not None and title != '': + self._title = title + + @property + def description(self): + """Return the key's description.""" + return self._description if self._description is not None else '' + + @description.setter + def description(self, description): + """Overwrite the key's description.""" + if description is not None and description != '': + self._description = description + + @property + def modules(self): + """Return the module names that declared the key.""" + return self._modules + + @modules.setter + def modules(self, module): + """Add another declaring module.""" + if module is not None and module != '': + self._modules.add(module) + + +class ConfigKey(ConfigTitle): + """Define the type and default value of a configuration key.""" + def __init__(self, key, type, default, module=None, title=None, + description=None, example=None): + super(ConfigKey, self).__init__(key, title, module, description) + self._type = type + self._default = default + self._examples = {example} if example is not None else set() + + def __repr__(self): + return f'ConfigKey({self.key}, {self.type}, {self.default})' + + def __eq__(self, other): + return super(ConfigKey, self).__eq__(other) and \ + self._type == other._type and \ + self._default == other._default + + def __hash__(self): + return hash((super(ConfigKey, self).__hash__(), self._type, self._default)) + + def check(self, value): + """Return True if *value* adheres to the key's type specification.""" + return self.type.check(value) + + def backtrack(self, value): + """Return True if *value* matches the key's type, raises a TypeError otherwise.""" + self.type.backtrack(value, '.'.join(self.key)) + return True + + @property + def default(self): + """Return the default value.""" + return self._default + + @property + def type(self): + """Return the type definition.""" + return self._type + + @property + def example(self): + """Return an example value.""" + return ', '.join(self._examples) if len(self._examples) else self.type.example + + @example.setter + def example(self, example): + """Add more examples for the key.""" + if example is not None and example != '': + self._examples.add(example) + +## global instance + +schema = ConfigSchema() + +def declare(*args, **kwargs): + schema.declare(*args, **kwargs) + +def declare_title(*args, **kwargs): + schema.declare_title(*args, **kwargs) + +## EOF ## diff --git a/tagit/config/settings.json b/tagit/config/settings.json new file mode 100644 index 0000000..e0bb3cf --- /dev/null +++ b/tagit/config/settings.json @@ -0,0 +1,70 @@ +{ + "ui": { + "standalone": { + "keytriggers": [ + "ClipboardCopy", + "ClipboardPaste", + "CreateGroup", + "DissolveGroup", + "AddToGroup", + "MoveCursorUp", + "MoveCursorDown", + "MoveCursorLeft", + "MoveCursorRight", + "MoveCursorLast", + "MoveCursorFirst", + "NextPage", + "PreviousPage", + "ScrollDown", + "ScrollUp", + "ZoomIn", + "ZoomOut", + "Select", + "SelectAll", + "SelectNone", + "SelectMulti", + "SelectRange", + "AddToken", + "GoBack", + "GoForth", + "SearchByAddressOnce", + "AddTag", + "EditTag", + "OpenGroup", + "RepresentGroup", + "Search", + "ShowSelected", + "RemoveSelected", + "OpenExternal", + "ShowHelp" + ], + "buttondocks": { + "filter": [ + "AddToken", + "GoBack", + "GoForth" + ], + "navigation_left": [ + "MoveCursorFirst", + "PreviousPage", + "ScrollUp" + ], + "navigation_right": [ + "ScrollDown", + "NextPage", + "MoveCursorLast" + ], + "status": [ + "RotateLeft", + "DeleteObject", + "RotateRight" + ] + }, + "context": { + "root": [ + "CloseSessionAndExit" + ] + } + } + } +} diff --git a/tagit/config/settings.py b/tagit/config/settings.py new file mode 100644 index 0000000..21ab594 --- /dev/null +++ b/tagit/config/settings.py @@ -0,0 +1,473 @@ +"""Configuration storage. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import abc +from copy import deepcopy +import io +import json +import os +import typing + +# tagit imports +from tagit.utils import errors, fst, is_list + +# inner-module imports +from . import types +from .schema import schema as global_schema +from .utils import key_starts_with, superkey_of, subkey_of + +# constants +INDENT = 4 + +# exports +__all__: typing.Sequence[str] = ( + 'Settings', + ) + + +## code ## + +class ConfigError(TypeError): pass + +class Settings(abc.MutableMapping, abc.Hashable, abc.Callable): + """Access and modify config keys in a dict-like manner. + + It's assumed that the schema might not be available for all config + elements. That's because it might be declared in code that is + not yet or will never be loaded. In such cases, any value is accepted. + If the schema is known, however, it is enforced. + """ + + ## construction + + def __init__(self, schema=None, prefix=None, data=None): + self.prefix = tuple(prefix) if prefix is not None else tuple() + self.schema = schema if schema is not None else global_schema + self.config = data if data is not None else dict() + + @classmethod + def Open(cls, source, schema=None, on_error='raise', clear_defaults=True): + schema = schema if schema is not None else global_schema + config_path = '' + # load source + if isinstance(source, dict): # dictionary + config = source + elif isinstance(source, str): # path or serialized + if os.path.exists(source): + config_path = os.path.realpath(source) + with open(source, 'r') as ifile: + config = json.load(ifile) + else: + config = json.loads(source) + elif isinstance(source, io.TextIOBase): # opened file + config = json.load(source) + else: + raise TypeError('expected dict, path, or file-like') + + # flatten and verify + data = None + if len(config) > 0: + data = cls.flatten_tree(config, schema, on_error=on_error) + if clear_defaults: + # filter defaults + data = {key: value + for key, value in data.items() + if key not in schema or value != schema[key].default} + + data['session', 'paths', 'config'] = config_path + return cls(schema=schema, data=data) + + def update(self, other): + for key, value in other.config.items(): + self.set(key, value) + return self + + def rebase(self, schema=None, on_error='raise', clear_defaults=True): + """Re-align the config with the current schema. + Should be done if the schema changes *after* the Settings was initialized. + Can also be used to enforce a new schema on the current config. + + Be aware that calling rebase will disconnect Settings instances from + each other. For example, this affects non-leaf key retrieval via + get such as cfg('session') + """ + schema = self.schema if schema is None else schema + # unroll + tree = dict() + for key, value in self.config.items(): + path, leaf = list(key[:-1]), key[-1] + # navigate through the path + branch = tree + while len(path): + curr = path.pop(0) + if curr not in branch: + branch[curr] = dict() + branch = branch[curr] + + branch[leaf] = value + + # flatten the unrolled config + flat = self.flatten_tree(tree, schema, on_error=on_error) + # remove defaults + if clear_defaults: + flat = {key: value + for key, value in flat.items() + if key not in schema or value != schema[key].default} + # set new schema and config + self.config = flat + self.schema = schema + + ## comparison + + def __eq__(self, other): + return isinstance(other, Settings) and \ + self.schema == other.schema and \ + self.config == other.config and \ + self.prefix == other.prefix + + def __hash__(self): + return hash((type(self), + self.prefix, + hash(self.schema), + hash(tuple(sorted(self.config.items(), key=fst))))) + + def __str__(self): + return str({k: v for k, v in self.config.items() if key_starts_with(k, self.prefix)}) + + def __repr__(self): + prefix = ','.join(self.prefix) + size_self = len([key for key in self.config if key_starts_with(key, self.prefix)]) + size_all = len(self) + return f'Settings(prefix=({prefix}), keys={size_self}, len={size_all})' + + ## conversion + + def clone(self): + return Settings(schema=self.schema, prefix=self.prefix, data=deepcopy(self.config)) + + @staticmethod + def flatten_tree(hdict, schema, on_error='raise', prefix=tuple()): + """Flattens a hierarchical dictionary by using schema information. + Returns a flat list of config keys and their values. + + If an invalid type was found and on_error is 'raise, a TypeError is raised. + Otherwise the invalid key is ignored. + """ + if len(hdict) == 0: + # not in schema, or passed the check + return {prefix: dict()} + + flat = dict() + for sub in hdict: + try: + key = prefix + (sub, ) + # check schema first, to preserve dict types + if key in schema and schema[key].backtrack(hdict[sub]): + # accept the value (also defaults!) + flat[key] = hdict[sub] + + elif isinstance(hdict[sub], dict): + flat.update(Settings.flatten_tree(hdict[sub], schema, on_error, key)) + + elif any(key_starts_with(k, key) for k in schema): + subkeys = [k[len(key):] for k in schema if key_starts_with(k, key)] + subkeys = ','.join('.'.join(k) for k in subkeys) + raise ConfigError( + f'found value {hdict[sub]} in {key}, expected keys ({subkeys})') + + else: + # terminal but not in schema; accept + flat[key] = hdict[sub] + + except TypeError as e: + if on_error == 'raise': + raise e + + return flat + + def to_tree(self, defaults=False): + """Return a nested dictionary with all config values. + If *defaults*, the schema defaults are included. + """ + tree = dict() + source = set(self.config.keys()) + if defaults: + source |= set(self.schema.keys()) + + for key in source: + if not key_starts_with(key, self.prefix): + continue + value = self.get(*key[len(self.prefix):]) + path, leaf = list(key[:-1]), key[-1] + path = path[len(self.prefix):] + # navigate through the path + branch = tree + while len(path): + curr = path.pop(0) + if curr not in branch: + branch[curr] = dict() + branch = branch[curr] + + branch[leaf] = value + + return tree + + def file_connected(self): + """Return True if the config is backed by a file.""" + return self('session', 'paths', 'config') is not None and \ + self('session', 'paths', 'config') != '' + + def save(self, uri=None): + """Save changes to a file at *uri*.""" + # pick defaults + uri = uri if uri is not None else self('session', 'paths', 'config') + if uri is None or uri == '': + raise ValueError('config saving requires a valid uri') + + # convert to tree + config = self.to_tree(defaults=False) + + # save to file + if isinstance(uri, io.TextIOBase): + json.dump(config, uri, indent=INDENT) + else: + with open(uri, 'w') as ofile: + json.dump(config, ofile, indent=INDENT) + + def diff(self, other): + """Return a config that includes only the keys which differ from *other*.""" + # keys in self that differ from other + config = {key: value + for key, value in self.config.items() + if key not in other.config or value != other.config[key] + } + # keys in other that differ from default + config.update({key: self.schema[key].default + for key, value in other.config.items() + if key not in self.config and \ + key in self.schema and \ + value != self.schema[key].default + }) + + return Settings(schema=self.schema, prefix=self.prefix, data=deepcopy(config)) + + + ## getting + + def __getitem__(self, key): + """Alias for *get*.""" + if is_list(key): + return self.get(*key) + else: + return self.get(key) + + def __call__(self, *key, default=None): + """Alias for *get*.""" + return self.get(*key, default=default) + + def get(self, *key, default=None): + key = self.prefix + key + + # known leaf + if key in self.config: + value = self.config[key] + if key in self.schema: + if self.schema[key].check(value): + return value + elif default is not None and self.schema[key].check(default): + return default + else: + return self.schema[key].default + else: + return value + + # unknown leaf + if key in self.schema: + if default is not None and self.schema[key].check(default): + return default + else: + return self.schema[key].default + + # branch + if any(key_starts_with(sub, key) for sub in self.config): + return Settings(schema=self.schema, prefix=key, data=self.config) + elif any(key_starts_with(sub, key) for sub in self.schema): + return Settings(schema=self.schema, prefix=key, data=self.config) + + if default is not None: + return default + + raise KeyError(key) + + ## checking + + def __contains__(self, key): + """Alias for *has*.""" + return self.has(*key) + + def has(self, *key): + key = self.prefix + key + + if key in self.config: + # key is a known leaf + return True + elif key in self.schema: + # key is an unknown leaf + return True + else: + # key might be a branch + for sub in self.config: + if key_starts_with(sub, key): + return True + + for sub in self.schema: + if key_starts_with(sub, key): + return True + + return False + + ## setting + + def __setitem__(self, key, value): + """Alias for *set*.""" + if is_list(key): + self.set(key, value) + else: + self.set((key, ), value) + + def set(self, key, value): + key = self.prefix + key + + if key in self.schema and self.schema[key].backtrack(value): + if self.schema[key].default != value: + self.config[key] = value + elif key in self.config: # value is default + # reset value to default, remove from config + del self.config[key] + # else: value was default but not present, ignore + + elif key in self.config: + self.config[key] = value + + elif isinstance(value, dict) and len(value) > 0: + # flatten value and set its items individually + subtree = self.flatten_tree(value, self.schema, prefix=key) + for subkey, subval in subtree.items(): + # defaults will be filtered by set + self.set(subkey[len(self.prefix):], subval) + + elif superkey_of(key, self.schema) or subkey_of(key, self.schema): + # schema violation in another branch + conflicts = {'.'.join(sub) + for sub in self.schema + if key_starts_with(sub, key) or key_starts_with(key, sub)} + raise ConfigError(f'{key} conflicts with schema keys {",".join(conflicts)}') + + elif superkey_of(key, self.config): + # it's allowed to overwrite dict-like config values + # Example: + # having defined session.paths.preview.files = 'somewhere' + # it's allowed to set session.paths.preview = {} + # It's admissible iff: + # * the value is an empty dict + # * no subkey is in the schema (already checked in the case above) + if value == dict(): + self.unset(*key) + self.config[key] = value + else: + conflicts = {'.'.join(sub) + for sub in self.schema + if key_starts_with(sub, key) or key_starts_with(key, sub)} + raise ConfigError(f'{key} conflicts with config keys {",".join(conflicts)}') + + elif subkey_of(key, self.config): + # it's allowed to overwrite dict-like config values + # Example: + # having defined session.paths.preview = {} + # it's allowed to set session.paths.preview.files = 'somewhere' + # It's admissible iff: + # * the superkey is an empty dict + # * no subkey of the superkey is in the schema + sups = [sup for sup in self.config if key_starts_with(key, sup)] + if len(sups) != 1: + # there can only be one super-key + raise errors.ProgrammingError(f'expected one superkey, found {len(sups)}') + + sup = sups[0] + if self.config[sup] == dict() and \ + len({sub for sub in self.schema if key_starts_with(sup, sub)}) == 0: + del self.config[sup] + self.config[key] = value + else: + # already have a superkey in the config that cannot be overwritten + conflicts = '.'.join(sup) + raise ConfigError(f'{key} conflicts with config keys {conflicts}') + + else: + self.config[key] = value + + return self + + ## removal + + def __delitem__(self, key): + """Alias for *unset*.""" + if is_list(key): + self.unset(*key) + else: + self.unset(key) + + def unset(self, *key): + key = self.prefix + key + + if key in self.config: + # key is a leaf + del self.config[key] + else: + # key might be a branch + subs = [sub for sub in self.config if key_starts_with(sub, key)] + for sub in subs: + del self.config[sub] + + return self + + ## iteration + + def __iter__(self): + """Alias for *keys*.""" + return self.keys() + + def keys(self): + for key in set(self.config.keys()) | set(self.schema.keys()): + if key_starts_with(key, self.prefix): + yield key[len(self.prefix):] + + def items(self): + for key in self.keys(): + yield key, self.get(*key) + + ## properties + + def __len__(self): + return len(list(self.keys())) + + +## config ## + +global_schema.declare(('session', 'verbose'), types.Unsigned(), 0, + __name__, 'Verbosity', 'Print additional information in various places of the application.') + +global_schema.declare(('session', 'debug'), types.Bool(), False, + __name__, 'Debug mode', 'Enable debug output and debug behaviour. Should be set to false in a productive environment.') + +global_schema.declare(('session', 'paths', 'config'), types.Path(), '', + __name__, 'Config path', "The path of the session's main configuration file. Is set automatically and for internal use only.") + +global_schema.declare(('storage', 'config', 'write_through'), types.Bool(), True, + __name__, 'Write-through', "Write the config to its file whenever it changes") + +## EOF ## diff --git a/tagit/config/types.py b/tagit/config/types.py new file mode 100644 index 0000000..3dc3d38 --- /dev/null +++ b/tagit/config/types.py @@ -0,0 +1,273 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# tagit imports +from tagit.utils import errors, is_list + +# exports +__all__: typing.Sequence[str] = ( + 'ConfigTypeError', + # types + 'Any', + 'Bool', + 'Dict', + 'Enum', + 'Float', + 'Int', + 'Keybind', + 'List', + 'Numeric', + 'Path', + 'String', + 'Unsigned', + ) + +# TODO: Bounded int or range? (specify lo/hi bounds) +# TODO: File vs. Dir; existence condition? + +## code ## + +# base class + +class ConfigTypeError(TypeError): + """Raised if a type inconsistency is detected.""" + pass + +class ConfigType(object): + """A config type defines a constraint over admissible values in order to + perform a basic verification of user-entered config values. + """ + + # example values + example = '' + + # type description + description = '' + + def __str__(self): + return f'{type(self).__name__}' + + def __repr__(self): + return f'{type(self).__name__}()' + + def __eq__(self, other): + return isinstance(other, type(self)) + + def __hash__(self): + return hash(type(self)) + + def check(self, value): + """Return True if the *value* matches the type.""" + try: + self.backtrack(value, '') + return True + except ConfigTypeError: + return False + + def backtrack(self, value, key): + """Check *value* for errors. + Raises a ConfigTypeError with a detailed message if an inconsistency is detected. + """ + errors.abstract() + +# generic types + +class Any(ConfigType): + example = '1, "a", [1,2,"a"]' + description = 'Any type' + + def backtrack(self, value, key): + # accepts anything + pass + + +class Bool(ConfigType): + example = 'True, False' + description = 'Boolean' + + def backtrack(self, value, key): + if not isinstance(value, bool): + raise ConfigTypeError(f'found {value} in {key}, expected a boolean') + + +class Keybind(ConfigType): + example = '[("a", ["ctrl"], [])]' + description = 'A list of (key, required modifiers, excluded modifiers)-triples' + + def backtrack(self, value, key): + if not is_list(value): + raise ConfigTypeError(f'found {type(value)} in {key}, expected a list of bindings') + + modifiers = {'shift', 'alt', 'ctrl', 'cmd', 'altgr', 'rest', 'all'} + for idx, itm in enumerate(value): + if not is_list(itm) or len(itm) != 3: + raise ConfigTypeError(f'found {itm} in {key}[{idx}], expected a list of three') + + char, inc, exc = itm + if not isinstance(char, str) and \ + not isinstance(char, int) and \ + not isinstance(char, float): + raise ConfigTypeError( + f'found {char} in {key}[{idx}], expected a character or number') + if not is_list(inc) or not set(inc).issubset(modifiers): + mods = ','.join(modifiers) + raise ConfigTypeError(f'found {inc} in {key}[{idx}], expected some of ({mods})') + if not is_list(exc) or not set(exc).issubset(modifiers): + mods = ','.join(modifiers) + raise ConfigTypeError(f'found {exc} in {key}[{idx}], expected some of ({mods})') + + +# numeric types + +class Numeric(ConfigType): + pass + + +class Int(Numeric): + example = '-8, -1, 0, 1, 3' + description = 'Integer number' + + def backtrack(self, value, key): + if not isinstance(value, int): + raise ConfigTypeError(f'found {value} in {key}, expected an integer') + + +class Unsigned(Int): + example = '0, 1, 13, 32' + description = 'Non-negative integer number, including zero' + + def __str__(self): + return 'Unsigned int' + + def backtrack(self, value, key): + if not isinstance(value, int) or value < 0: + raise ConfigTypeError(f'found {value} in {key}, expeced an integer of at least zero') + + +class Float(Numeric): + example = '1.2, 3.4, 5, 6' + description = 'Integer or Decimal number' + + def backtrack(self, value, key): + if not isinstance(value, float) and not isinstance(value, int): + raise ConfigTypeError(f'found {value} in {key}, expected a number') + + +# string types + +class String(ConfigType): + example = '"hello world", "", "foobar"' + description = 'String' + + def backtrack(self, value, key): + if not isinstance(value, str): + raise ConfigTypeError(f'found {value} in {key}, expected a string') + + +class Path(String): + example = '"/tmp", "Pictures/trip", "~/.tagitrc"' + description = 'String, compliant with file system paths' + + +# compound types + +class Enum(ConfigType): + description = 'One out of a predefined set of values' + + @property + def example(self): + return ', '.join(str(o) for o in list(self.options)[:3]) + + def __init__(self, *options): + self.options = set(options[0] if len(options) == 1 and is_list(options[0]) else options) + + def __eq__(self, other): + return super(Enum, self).__eq__(other) and \ + self.options == other.options + + def __hash__(self): + return hash((super(Enum, self).__hash__(), tuple(self.options))) + + def __str__(self): + options = ', '.join(str(itm) for itm in self.options) + return f'One out of ({options})' + + def __repr__(self): + return f'{type(self).__name__}([{self.options}])' + + def backtrack(self, value, key): + try: + if value not in self.options: + raise Exception() + except Exception: + options = ','.join(str(itm) for itm in self.options) + raise ConfigTypeError(f'found {value} in {key}, expected one out of ({options})') + + +class List(ConfigType): + description = 'List of values' + + @property + def example(self): + return f'[{self.item_type.example}]' + + def __init__(self, item_type): + self.item_type = item_type + + def __eq__(self, other): + return super(List, self).__eq__(other) and \ + self.item_type == other.item_type + + def __hash__(self): + return hash((super(List, self).__hash__(), hash(self.item_type))) + + def __str__(self): + return f'List of {str(self.item_type)}' + + def __repr__(self): + return f'{type(self).__name__}({self.item_type})' + + def backtrack(self, value, key): + if not isinstance(value, list) and not isinstance(value, tuple): + raise ConfigTypeError(f'found {type(value)} in {key}, expected list') + for item in value: + self.item_type.backtrack(item, key) + + +class Dict(ConfigType): + example = '{"hello": "world"}; {"hello": 3}; {"hello": [1, 2, 3]}' + description = 'Map of keys/values' + + def __init__(self, key_type, value_type): + self.key_type = key_type + self.value_type = value_type + + def __eq__(self, other): + return super(Dict, self).__eq__(other) and \ + self.key_type == other.key_type and \ + self.value_type == other.value_type + + def __hash__(self): + return hash((super(Dict, self).__hash__(), hash(self.key_type), hash(self.value_type))) + + def __str__(self): + return f'Dict from {self.key_type} to {self.value_type}' + + + def __repr__(self): + return f'{type(self).__name__}({self.key_type}, {self.value_type})' + + def backtrack(self, value, key): + if not isinstance(value, dict): + raise ConfigTypeError(f'found {type(value)} in {key}, expected a dict') + for subkey, subval in value.items(): + self.key_type.backtrack(subkey, str(key) + '.' + str(subkey)) + self.value_type.backtrack(subval, str(key) + '.' + str(subkey)) + +## EOF ## diff --git a/tagit/config/user-defaults.json b/tagit/config/user-defaults.json new file mode 100644 index 0000000..b76ef2b --- /dev/null +++ b/tagit/config/user-defaults.json @@ -0,0 +1,142 @@ +{ + "session": { + "first_start": false, + "paths": { + "searchlog": "~/.tagit.log" + } + }, + "storage": { + "index": { + "preview_size": [ + 50, + 200, + 400 + ] + } + }, + "ui": { + "standalone": { + "window_size": "1024x768", + "browser": { + "maxrows": 8, + "maxcols": 8 + }, + "buttondocks": { + "sidebar_left": [ + "Menu", + "ShowDashboard", + "AddTag", + "EditTag", + "CreateGroup", + "DissolveGroup", + "SelectAll", + "SelectNone", + "SelectInvert", + "SelectAdditive", + "SelectSubtractive", + "SelectSingle", + "SelectMulti", + "SelectRange" + ] + }, + "tabs": { + "max": 2 + }, + "tiledocks": { + "dashboard": { + "Buttons": { + "buttons": [ + "ShowBrowsing", + "CreateSession", + "CreateTempSession", + "LoadSession", + "ReloadSession", + "ImportObjects", + "SaveSession", + "SaveSessionAs", + "ItemExport", + "UpdateSelectedObjects", + "SyncSelectedObjects", + "ShowHelp", + "ShowSettings" + ] + }, + "LibSummary": {}, + "Hints": {}, + "TagHistogram": {}, + "Tagcloud": {}, + "Searchtree": {} + }, + "sidebar_right": { + "Info": {}, + "CursorTags": {}, + "Venn": {} + } + }, + "search": { + "sort_blacklist": [ + "entity", + "flash", + "latitude", + "longitude", + "mime", + "author", + "camera", + "attributes" + ] + }, + "context": { + "app": [ + "ShowSettings", + "ShowHelp", + "ShowConsole" + ], + "session": [ + "SaveSession", + "SaveSessionAs", + "ItemExport", + "ImportObjects" + ], + "search": [ + "ShowSelected", + "RemoveSelected" + ], + "browser": [ + "ZoomIn", + "ZoomOut" + ], + "select": [ + "SelectAll", + "SelectNone", + "SelectInvert", + "SelectSingle", + "SelectMulti", + "SelectRange", + "SelectAdditive", + "SelectSubtractive" + ], + "clipboard": [ + "ClipboardCopy", + "ClipboardPaste" + ], + "tagging": [ + "AddTag", + "EditTag", + "SetRank1", + "SetRank3", + "SetRank5" + ], + "grouping": [ + "CreateGroup", + "DissolveGroup", + "AddToGroup", + "RepresentGroup", + "RemoveFromGroup" + ], + "root": [ + "CloseSessionAndExit" + ] + } + } + } +} diff --git a/tagit/config/utils.py b/tagit/config/utils.py new file mode 100644 index 0000000..948f53a --- /dev/null +++ b/tagit/config/utils.py @@ -0,0 +1,104 @@ +"""Configuration system utilities. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# tagit imports +from tagit.utils import import_all + +# inner-module imports +from . import types + +# exports +__all__: typing.Sequence[str] = ( + 'key_starts_with', + 'schema_key_sort', + 'schema_to_rst', + 'subkey_of', + 'superkey_of', + ) + + +## code ## + +def key_starts_with(key, prefix): + """Return whether a config *key* starts with (is a subclass of) *prefix*.""" + return key[:len(prefix)] == prefix + +def subkey_of(key, pool): + """Return True if *key* is at a lower level than some key in *pool*. + Example: session.debug is a subkey of session. + """ + for sup in pool: + if key_starts_with(key, sup): + return True + return False + +def superkey_of(key, pool): + """Return True if *key* is at a higher level than some key in *pool*. + Example: session is a superkey of session.debug. + """ + for sub in pool: + if key_starts_with(sub, key): + return True + return False + +def schema_key_sort(schema): + """Return a comparison function for sorting schema config or title keys. + To be used in sorted or sort as key function. + + >>> sorted(schema.keys(titles=True), key=schema_keys_sort(schema)) + + """ + def cmp(key): + """Return an unambiguous representation of schema config or title keys.""" + return ('.'.join(key[:-1]) + '..' + key[-1]) \ + if not schema.is_title(key) \ + else ('.'.join(key) + '..') + + return cmp + +def schema_to_rst(schema, print_modules=False, no_import=False): + """Creates a documentation page in ReST of the config schema. + Calling this method with *no_import* set to False imports all + tagit submodules. + """ + # import all modules + if not no_import: + import tagit + import_all(tagit, exclude={'.*\.external'}) + + header = '=-^~"' + + known_sections = set() + for key in sorted(schema.keys(titles=True), key=schema_key_sort(schema)): + # print headings + for idx, sec in enumerate(key): + heading = '.'.join(key[:idx+1]) + if heading not in known_sections: + print('') + print(heading) + print(header[idx] * len(heading)) + known_sections.add(heading) + + if schema.is_title(key): + print(schema.get_title(key).description + '\n') + + else: + print(schema[key].description + '\n') + + print(f':Format: {str(schema[key].type)} ({schema[key].example})') + print(f':Default: {schema[key].default}') + + if isinstance(schema[key].type, types.Enum): + print(f':Options: {schema[key].type.options}') + + if print_modules: + modules = ', '.join(f'`{str(m)}`_' for m in schema[key].modules) + print(f':Modules: {modules}') + +## EOF ## -- cgit v1.2.3 From ceaaef069d8ffda23fce320ce66c86e0226f1046 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Fri, 6 Jan 2023 17:40:25 +0100 Subject: first startup, empty screen --- tagit/config/settings.json | 48 ---------------------------------------------- 1 file changed, 48 deletions(-) (limited to 'tagit/config') diff --git a/tagit/config/settings.json b/tagit/config/settings.json index e0bb3cf..8fd4754 100644 --- a/tagit/config/settings.json +++ b/tagit/config/settings.json @@ -2,67 +2,19 @@ "ui": { "standalone": { "keytriggers": [ - "ClipboardCopy", - "ClipboardPaste", - "CreateGroup", - "DissolveGroup", - "AddToGroup", - "MoveCursorUp", - "MoveCursorDown", - "MoveCursorLeft", - "MoveCursorRight", - "MoveCursorLast", - "MoveCursorFirst", - "NextPage", - "PreviousPage", - "ScrollDown", - "ScrollUp", - "ZoomIn", - "ZoomOut", - "Select", - "SelectAll", - "SelectNone", - "SelectMulti", - "SelectRange", - "AddToken", - "GoBack", - "GoForth", - "SearchByAddressOnce", - "AddTag", - "EditTag", - "OpenGroup", - "RepresentGroup", - "Search", - "ShowSelected", - "RemoveSelected", - "OpenExternal", - "ShowHelp" ], "buttondocks": { "filter": [ - "AddToken", - "GoBack", - "GoForth" ], "navigation_left": [ - "MoveCursorFirst", - "PreviousPage", - "ScrollUp" ], "navigation_right": [ - "ScrollDown", - "NextPage", - "MoveCursorLast" ], "status": [ - "RotateLeft", - "DeleteObject", - "RotateRight" ] }, "context": { "root": [ - "CloseSessionAndExit" ] } } -- cgit v1.2.3 From 1a8d8f8a37e78f48da88dd69e785234d822425ed Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Fri, 6 Jan 2023 22:55:36 +0100 Subject: load from config, switch to browsing --- tagit/config/loader.py | 4 +- tagit/config/port-config.yaml | 114 ++++++++++++++++++++++++++++++++++++++++ tagit/config/settings.py | 12 +++-- tagit/config/user-defaults.yaml | 112 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 tagit/config/port-config.yaml create mode 100644 tagit/config/user-defaults.yaml (limited to 'tagit/config') diff --git a/tagit/config/loader.py b/tagit/config/loader.py index 489b063..87ac328 100644 --- a/tagit/config/loader.py +++ b/tagit/config/loader.py @@ -17,7 +17,7 @@ from .settings import Settings TAGITRC = '.tagitrc' -DEFAULT_USER_CONFIG = os.path.join(os.path.dirname(__file__), 'user-defaults.json') +DEFAULT_USER_CONFIG = os.path.join(os.path.dirname(__file__), 'user-defaults.yaml') SETTINGS_PATH = [ # user home @@ -26,7 +26,7 @@ SETTINGS_PATH = [ '/usr/share/tagit/settings', '/usr/share/tagit/keybindings', # module defaults - os.path.join(os.path.dirname(__file__), 'settings.json'), + os.path.join(os.path.dirname(__file__), 'settings.yaml'), ] # exports diff --git a/tagit/config/port-config.yaml b/tagit/config/port-config.yaml new file mode 100644 index 0000000..038dc07 --- /dev/null +++ b/tagit/config/port-config.yaml @@ -0,0 +1,114 @@ +session: + first_start: false + paths: + searchlog: ~/.tagit.log +storage: + index: + preview_size: + - 50 + - 200 + - 400 +ui: + standalone: + plane: browsing + browser: + maxcols: 8 + maxrows: 8 + buttondocks: + sidebar_left: [] + #- Menu + #- ShowDashboard + #- AddTag + #- EditTag + #- CreateGroup + #- DissolveGroup + #- SelectAll + #- SelectNone + #- SelectInvert + #- SelectAdditive + #- SelectSubtractive + #- SelectSingle + #- SelectMulti + #- SelectRange + context: + app: + - ShowSettings + - ShowHelp + - ShowConsole + - ShowBrowsing + # browser: + # - ZoomIn + # - ZoomOut + # clipboard: + # - ClipboardCopy + # - ClipboardPaste + # grouping: + # - CreateGroup + # - DissolveGroup + # - AddToGroup + # - RepresentGroup + # - RemoveFromGroup + # root: + # - CloseSessionAndExit + # search: + # - ShowSelected + # - RemoveSelected + # select: + # - SelectAll + # - SelectNone + # - SelectInvert + # - SelectSingle + # - SelectMulti + # - SelectRange + # - SelectAdditive + # - SelectSubtractive + # session: + # - SaveSession + # - SaveSessionAs + # - ItemExport + # - ImportObjects + # tagging: + # - AddTag + # - EditTag + # - SetRank1 + # - SetRank3 + # - SetRank5 + search: + sort_blacklist: + - entity + - flash + - latitude + - longitude + - mime + - author + - camera + - attributes + tabs: + max: 2 + tiledocks: + dashboard: {} + #Buttons: + # buttons: + # - ShowBrowsing + # - CreateSession + # - CreateTempSession + # - LoadSession + # - ReloadSession + # - ImportObjects + # - SaveSession + # - SaveSessionAs + # - ItemExport + # - UpdateSelectedObjects + # - SyncSelectedObjects + # - ShowHelp + # - ShowSettings + #Hints: {} + #LibSummary: {} + #Searchtree: {} + #TagHistogram: {} + #Tagcloud: {} + sidebar_right: {} + #CursorTags: {} + #Info: {} + #Venn: {} + window_size: 1024x768 diff --git a/tagit/config/settings.py b/tagit/config/settings.py index 21ab594..190268c 100644 --- a/tagit/config/settings.py +++ b/tagit/config/settings.py @@ -12,6 +12,9 @@ import json import os import typing +# external imports +import yaml # FIXME: mb/port/convenicence + # tagit imports from tagit.utils import errors, fst, is_list @@ -60,11 +63,14 @@ class Settings(abc.MutableMapping, abc.Hashable, abc.Callable): if os.path.exists(source): config_path = os.path.realpath(source) with open(source, 'r') as ifile: - config = json.load(ifile) + #config = json.load(ifile) + config = yaml.safe_load(ifile) # FIXME: mb/port/convenicence else: - config = json.loads(source) + #config = json.loads(source) + config = yaml.safe_load(source) # FIXME: mb/port/convenicence elif isinstance(source, io.TextIOBase): # opened file - config = json.load(source) + #config = json.load(source) + config = yaml.safe_load(source) # FIXME: mb/port/convenicence else: raise TypeError('expected dict, path, or file-like') diff --git a/tagit/config/user-defaults.yaml b/tagit/config/user-defaults.yaml new file mode 100644 index 0000000..b7a70c4 --- /dev/null +++ b/tagit/config/user-defaults.yaml @@ -0,0 +1,112 @@ +session: + first_start: false + paths: + searchlog: ~/.tagit.log +storage: + index: + preview_size: + - 50 + - 200 + - 400 +ui: + standalone: + browser: + maxcols: 8 + maxrows: 8 + buttondocks: + sidebar_left: + - Menu + - ShowDashboard + - AddTag + - EditTag + - CreateGroup + - DissolveGroup + - SelectAll + - SelectNone + - SelectInvert + - SelectAdditive + - SelectSubtractive + - SelectSingle + - SelectMulti + - SelectRange + context: + app: + - ShowSettings + - ShowHelp + - ShowConsole + browser: + - ZoomIn + - ZoomOut + clipboard: + - ClipboardCopy + - ClipboardPaste + grouping: + - CreateGroup + - DissolveGroup + - AddToGroup + - RepresentGroup + - RemoveFromGroup + root: + - CloseSessionAndExit + search: + - ShowSelected + - RemoveSelected + select: + - SelectAll + - SelectNone + - SelectInvert + - SelectSingle + - SelectMulti + - SelectRange + - SelectAdditive + - SelectSubtractive + session: + - SaveSession + - SaveSessionAs + - ItemExport + - ImportObjects + tagging: + - AddTag + - EditTag + - SetRank1 + - SetRank3 + - SetRank5 + search: + sort_blacklist: + - entity + - flash + - latitude + - longitude + - mime + - author + - camera + - attributes + tabs: + max: 2 + tiledocks: + dashboard: + Buttons: + buttons: + - ShowBrowsing + - CreateSession + - CreateTempSession + - LoadSession + - ReloadSession + - ImportObjects + - SaveSession + - SaveSessionAs + - ItemExport + - UpdateSelectedObjects + - SyncSelectedObjects + - ShowHelp + - ShowSettings + Hints: {} + LibSummary: {} + Searchtree: {} + TagHistogram: {} + Tagcloud: {} + sidebar_right: + CursorTags: {} + Info: {} + Venn: {} + window_size: 1024x768 -- cgit v1.2.3 From 6b6495b8f5b3bfd8fbd4caf56a44424df070e813 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 7 Jan 2023 15:53:20 +0100 Subject: removed tabs --- tagit/config/port-config.yaml | 5 +++-- tagit/config/user-defaults.json | 3 --- tagit/config/user-defaults.yaml | 2 -- 3 files changed, 3 insertions(+), 7 deletions(-) (limited to 'tagit/config') diff --git a/tagit/config/port-config.yaml b/tagit/config/port-config.yaml index 038dc07..d05a498 100644 --- a/tagit/config/port-config.yaml +++ b/tagit/config/port-config.yaml @@ -83,8 +83,9 @@ ui: - author - camera - attributes - tabs: - max: 2 + # FIXME: mb/port/tabs + #tabs: + # max: 2 tiledocks: dashboard: {} #Buttons: diff --git a/tagit/config/user-defaults.json b/tagit/config/user-defaults.json index b76ef2b..5a39311 100644 --- a/tagit/config/user-defaults.json +++ b/tagit/config/user-defaults.json @@ -39,9 +39,6 @@ "SelectRange" ] }, - "tabs": { - "max": 2 - }, "tiledocks": { "dashboard": { "Buttons": { diff --git a/tagit/config/user-defaults.yaml b/tagit/config/user-defaults.yaml index b7a70c4..62b8ed6 100644 --- a/tagit/config/user-defaults.yaml +++ b/tagit/config/user-defaults.yaml @@ -81,8 +81,6 @@ ui: - author - camera - attributes - tabs: - max: 2 tiledocks: dashboard: Buttons: -- cgit v1.2.3 From 37f1ac3f456b6677d9ce14274f3987ccda752f03 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 7 Jan 2023 16:26:26 +0100 Subject: browser and misc actions --- tagit/config/port-config.yaml | 115 ------------------------------------------ 1 file changed, 115 deletions(-) delete mode 100644 tagit/config/port-config.yaml (limited to 'tagit/config') diff --git a/tagit/config/port-config.yaml b/tagit/config/port-config.yaml deleted file mode 100644 index d05a498..0000000 --- a/tagit/config/port-config.yaml +++ /dev/null @@ -1,115 +0,0 @@ -session: - first_start: false - paths: - searchlog: ~/.tagit.log -storage: - index: - preview_size: - - 50 - - 200 - - 400 -ui: - standalone: - plane: browsing - browser: - maxcols: 8 - maxrows: 8 - buttondocks: - sidebar_left: [] - #- Menu - #- ShowDashboard - #- AddTag - #- EditTag - #- CreateGroup - #- DissolveGroup - #- SelectAll - #- SelectNone - #- SelectInvert - #- SelectAdditive - #- SelectSubtractive - #- SelectSingle - #- SelectMulti - #- SelectRange - context: - app: - - ShowSettings - - ShowHelp - - ShowConsole - - ShowBrowsing - # browser: - # - ZoomIn - # - ZoomOut - # clipboard: - # - ClipboardCopy - # - ClipboardPaste - # grouping: - # - CreateGroup - # - DissolveGroup - # - AddToGroup - # - RepresentGroup - # - RemoveFromGroup - # root: - # - CloseSessionAndExit - # search: - # - ShowSelected - # - RemoveSelected - # select: - # - SelectAll - # - SelectNone - # - SelectInvert - # - SelectSingle - # - SelectMulti - # - SelectRange - # - SelectAdditive - # - SelectSubtractive - # session: - # - SaveSession - # - SaveSessionAs - # - ItemExport - # - ImportObjects - # tagging: - # - AddTag - # - EditTag - # - SetRank1 - # - SetRank3 - # - SetRank5 - search: - sort_blacklist: - - entity - - flash - - latitude - - longitude - - mime - - author - - camera - - attributes - # FIXME: mb/port/tabs - #tabs: - # max: 2 - tiledocks: - dashboard: {} - #Buttons: - # buttons: - # - ShowBrowsing - # - CreateSession - # - CreateTempSession - # - LoadSession - # - ReloadSession - # - ImportObjects - # - SaveSession - # - SaveSessionAs - # - ItemExport - # - UpdateSelectedObjects - # - SyncSelectedObjects - # - ShowHelp - # - ShowSettings - #Hints: {} - #LibSummary: {} - #Searchtree: {} - #TagHistogram: {} - #Tagcloud: {} - sidebar_right: {} - #CursorTags: {} - #Info: {} - #Venn: {} - window_size: 1024x768 -- cgit v1.2.3 From 8f2f697f7ed52b7e1c7a17411b2de526b6490691 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 17:18:43 +0100 Subject: removed save and sync functionality since they no longer apply --- tagit/config/user-defaults.json | 5 ----- tagit/config/user-defaults.yaml | 5 ----- 2 files changed, 10 deletions(-) (limited to 'tagit/config') diff --git a/tagit/config/user-defaults.json b/tagit/config/user-defaults.json index 5a39311..1ff7ab4 100644 --- a/tagit/config/user-defaults.json +++ b/tagit/config/user-defaults.json @@ -49,11 +49,8 @@ "LoadSession", "ReloadSession", "ImportObjects", - "SaveSession", - "SaveSessionAs", "ItemExport", "UpdateSelectedObjects", - "SyncSelectedObjects", "ShowHelp", "ShowSettings" ] @@ -89,8 +86,6 @@ "ShowConsole" ], "session": [ - "SaveSession", - "SaveSessionAs", "ItemExport", "ImportObjects" ], diff --git a/tagit/config/user-defaults.yaml b/tagit/config/user-defaults.yaml index 62b8ed6..3ec23d0 100644 --- a/tagit/config/user-defaults.yaml +++ b/tagit/config/user-defaults.yaml @@ -61,8 +61,6 @@ ui: - SelectAdditive - SelectSubtractive session: - - SaveSession - - SaveSessionAs - ItemExport - ImportObjects tagging: @@ -91,11 +89,8 @@ ui: - LoadSession - ReloadSession - ImportObjects - - SaveSession - - SaveSessionAs - ItemExport - UpdateSelectedObjects - - SyncSelectedObjects - ShowHelp - ShowSettings Hints: {} -- cgit v1.2.3 From 9bdf4d104a299577634061bcf698d1c9e5708cce Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 4 Mar 2023 15:58:43 +0100 Subject: config loading --- tagit/config/loader.py | 4 -- tagit/config/settings.json | 22 ------- tagit/config/settings.yaml | 10 +++ tagit/config/user-defaults.json | 134 ---------------------------------------- tagit/config/user-defaults.yaml | 130 +++++++++++++++++++------------------- 5 files changed, 75 insertions(+), 225 deletions(-) delete mode 100644 tagit/config/settings.json create mode 100644 tagit/config/settings.yaml delete mode 100644 tagit/config/user-defaults.json (limited to 'tagit/config') diff --git a/tagit/config/loader.py b/tagit/config/loader.py index 87ac328..47a51fa 100644 --- a/tagit/config/loader.py +++ b/tagit/config/loader.py @@ -54,10 +54,8 @@ def load_settings(path=None, verbose=0): searchpaths += SETTINGS_PATH # create default user config on first start - first_start = False user_config = os.path.expanduser(os.path.join('~', TAGITRC)) if os.path.exists(DEFAULT_USER_CONFIG) and not os.path.exists(user_config): - first_start = True shutil.copy(DEFAULT_USER_CONFIG, user_config) # scan searchpaths @@ -76,8 +74,6 @@ def load_settings(path=None, verbose=0): # update verbosity from argument cfg.set(('session', 'verbose'), max(cfg('session', 'verbose'), verbose)) - # set first start according to previous user config existence - cfg.set(('session', 'first_start'), first_start) return cfg ## EOF ## diff --git a/tagit/config/settings.json b/tagit/config/settings.json deleted file mode 100644 index 8fd4754..0000000 --- a/tagit/config/settings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "ui": { - "standalone": { - "keytriggers": [ - ], - "buttondocks": { - "filter": [ - ], - "navigation_left": [ - ], - "navigation_right": [ - ], - "status": [ - ] - }, - "context": { - "root": [ - ] - } - } - } -} diff --git a/tagit/config/settings.yaml b/tagit/config/settings.yaml new file mode 100644 index 0000000..e9264d4 --- /dev/null +++ b/tagit/config/settings.yaml @@ -0,0 +1,10 @@ +ui: + standalone: + keytriggers: [] + buttondocks: + navigation_left: [] + navigation_right: [] + filter: [] + status: [] + context: + root: [] diff --git a/tagit/config/user-defaults.json b/tagit/config/user-defaults.json deleted file mode 100644 index 1ff7ab4..0000000 --- a/tagit/config/user-defaults.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "session": { - "first_start": false, - "paths": { - "searchlog": "~/.tagit.log" - } - }, - "storage": { - "index": { - "preview_size": [ - 50, - 200, - 400 - ] - } - }, - "ui": { - "standalone": { - "window_size": "1024x768", - "browser": { - "maxrows": 8, - "maxcols": 8 - }, - "buttondocks": { - "sidebar_left": [ - "Menu", - "ShowDashboard", - "AddTag", - "EditTag", - "CreateGroup", - "DissolveGroup", - "SelectAll", - "SelectNone", - "SelectInvert", - "SelectAdditive", - "SelectSubtractive", - "SelectSingle", - "SelectMulti", - "SelectRange" - ] - }, - "tiledocks": { - "dashboard": { - "Buttons": { - "buttons": [ - "ShowBrowsing", - "CreateSession", - "CreateTempSession", - "LoadSession", - "ReloadSession", - "ImportObjects", - "ItemExport", - "UpdateSelectedObjects", - "ShowHelp", - "ShowSettings" - ] - }, - "LibSummary": {}, - "Hints": {}, - "TagHistogram": {}, - "Tagcloud": {}, - "Searchtree": {} - }, - "sidebar_right": { - "Info": {}, - "CursorTags": {}, - "Venn": {} - } - }, - "search": { - "sort_blacklist": [ - "entity", - "flash", - "latitude", - "longitude", - "mime", - "author", - "camera", - "attributes" - ] - }, - "context": { - "app": [ - "ShowSettings", - "ShowHelp", - "ShowConsole" - ], - "session": [ - "ItemExport", - "ImportObjects" - ], - "search": [ - "ShowSelected", - "RemoveSelected" - ], - "browser": [ - "ZoomIn", - "ZoomOut" - ], - "select": [ - "SelectAll", - "SelectNone", - "SelectInvert", - "SelectSingle", - "SelectMulti", - "SelectRange", - "SelectAdditive", - "SelectSubtractive" - ], - "clipboard": [ - "ClipboardCopy", - "ClipboardPaste" - ], - "tagging": [ - "AddTag", - "EditTag", - "SetRank1", - "SetRank3", - "SetRank5" - ], - "grouping": [ - "CreateGroup", - "DissolveGroup", - "AddToGroup", - "RepresentGroup", - "RemoveFromGroup" - ], - "root": [ - "CloseSessionAndExit" - ] - } - } - } -} diff --git a/tagit/config/user-defaults.yaml b/tagit/config/user-defaults.yaml index 3ec23d0..6ec51c6 100644 --- a/tagit/config/user-defaults.yaml +++ b/tagit/config/user-defaults.yaml @@ -1,56 +1,93 @@ session: - first_start: false - paths: - searchlog: ~/.tagit.log -storage: - index: - preview_size: - - 50 - - 200 - - 400 + bsfs: + Graph: + backend: + SparqlStore: {} + user: 'http://example.com/me' + script: + - Search ui: standalone: + window_size: [1440, 810] + #maximize: True + keytriggers: + - MoveCursorUp + - MoveCursorDown + - MoveCursorLeft + - MoveCursorRight + - MoveCursorLast + - MoveCursorFirst + - NextPage + - PreviousPage + - ScrollDown + - ScrollUp + - ZoomIn + - ZoomOut + - Select + - SelectAll + - SelectNone + - SelectMulti + - SelectRange + - AddToken + - GoBack + - GoForth + - AddTag + - EditTag + - Search + - ShowSelected + - RemoveSelected browser: + cols: 4 + rows: 3 maxcols: 8 maxrows: 8 buttondocks: + navigation_left: + - MoveCursorFirst + - PreviousPage + - ScrollUp + navigation_right: + - ScrollDown + - NextPage + - MoveCursorLast + filter: + - GoBack + - GoForth + status: + - ZoomIn + - ZoomOut sidebar_left: - - Menu - - ShowDashboard - AddTag - EditTag - - CreateGroup - - DissolveGroup + - ShowSelected + - RemoveSelected - SelectAll - SelectNone - SelectInvert - - SelectAdditive - - SelectSubtractive - SelectSingle - - SelectMulti - SelectRange + - SelectMulti + - SelectAdditive + - SelectSubtractive context: app: - - ShowSettings - ShowHelp - ShowConsole + - ShowSettings browser: - ZoomIn - ZoomOut - clipboard: - - ClipboardCopy - - ClipboardPaste - grouping: - - CreateGroup - - DissolveGroup - - AddToGroup - - RepresentGroup - - RemoveFromGroup - root: - - CloseSessionAndExit + - MoveCursorFirst + - PreviousPage + - ScrollUp + - ScrollDown + - NextPage + - MoveCursorLast search: - ShowSelected - RemoveSelected + - GoForth + - GoBack select: - SelectAll - SelectNone @@ -60,46 +97,9 @@ ui: - SelectRange - SelectAdditive - SelectSubtractive - session: - - ItemExport - - ImportObjects tagging: - AddTag - EditTag - - SetRank1 - - SetRank3 - - SetRank5 - search: - sort_blacklist: - - entity - - flash - - latitude - - longitude - - mime - - author - - camera - - attributes tiledocks: - dashboard: - Buttons: - buttons: - - ShowBrowsing - - CreateSession - - CreateTempSession - - LoadSession - - ReloadSession - - ImportObjects - - ItemExport - - UpdateSelectedObjects - - ShowHelp - - ShowSettings - Hints: {} - LibSummary: {} - Searchtree: {} - TagHistogram: {} - Tagcloud: {} sidebar_right: - CursorTags: {} Info: {} - Venn: {} - window_size: 1024x768 -- cgit v1.2.3 From 1f916fe4ef821d38af20057b6d1386c155663105 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 4 Mar 2023 16:23:47 +0100 Subject: cleanup --- tagit/config/user-defaults.yaml | 2 ++ 1 file changed, 2 insertions(+) (limited to 'tagit/config') diff --git a/tagit/config/user-defaults.yaml b/tagit/config/user-defaults.yaml index 6ec51c6..447e10f 100644 --- a/tagit/config/user-defaults.yaml +++ b/tagit/config/user-defaults.yaml @@ -84,6 +84,8 @@ ui: - NextPage - MoveCursorLast search: + - AddToken + - SortOrder - ShowSelected - RemoveSelected - GoForth -- cgit v1.2.3