From af3d569ee986133feccb311762f927acaa7f57ff Mon Sep 17 00:00:00 2001 From: whut Date: Fri, 7 Oct 2022 17:39:51 -0500 Subject: [PATCH] itbegins.xcf --- .gitignore | 160 +++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 13 ++++ README.md | 22 +++++++ pyproject.toml | 21 ++++++ src/config-test.yaml | 13 ++++ src/confthing/__init__.py | 70 ++++++++++++++++++++ src/confthing/confthing.py | 69 +++++++++++++++++++ src/test.py | 17 +++++ 8 files changed, 385 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/config-test.yaml create mode 100644 src/confthing/__init__.py create mode 100644 src/confthing/confthing.py create mode 100755 src/test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68bc17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c93f45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3beac25 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +A really simple library for accessing dictionary items using paths. Meant to make working with configuration files easier. + +## quick-n-dirty documentalation +The class you want is `confthing.Config(data, delimiter = "/", defaults = {}, requirements = [], **misc_options)`. +- `data` is the dict you want to wrap the class around. +- `delimiter` is whatever you want to use as a path delimiter. +- `defaults` is a dict whose keys are paths and values are the default values for each path. +- `requirements` is a list of paths that must be defined, otherwise MissingRequirementError is raised. +- Miscellaneous options are as follows: + - `strict_subscript`: when True, enables strict mode when getting items via subscript. Default is False. + - `clobber_subscript`: when True, enables clobber mode when setting items via subscript. Default is False. + +### Methods + +#### `get(path, fallback = None, strict = False)` +Tries to return the value at the specified path, otherwise returns the fallback value (or raises PathNotFoundError if `strict` is True). + +#### `set(path, value, clobber = False)` +Tries to set the value at the specified path. If it encounters an item that isn't a dict, it'll raise NotDictError if `clobber` is False, or it'll replace that with an empty dict. + +#### Subscripting +Config objects are subscriptable, calling `get` and `set` as appropriate. The two `subscript` options control what `strict` and `clobber` are set to when subscripting. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d7fdc05 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "confthing" +version = "0.1" +authors = [ + { name="whut", email="watewhut@aaathats3as.com" }, +] +description = "Allows working with dicts using path strings" +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: Other/Proprietary License", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://git.lain.church/whut/confthing" \ No newline at end of file diff --git a/src/config-test.yaml b/src/config-test.yaml new file mode 100644 index 0000000..d8ce603 --- /dev/null +++ b/src/config-test.yaml @@ -0,0 +1,13 @@ +alpha: + aleph: + a: 12345 + b: abcde + c: ["i","ro","ha"] + bet: 123 + gimel: "abc" + dalet: + a: + b: + c: + d: "hey" + diff --git a/src/confthing/__init__.py b/src/confthing/__init__.py new file mode 100644 index 0000000..e381dd5 --- /dev/null +++ b/src/confthing/__init__.py @@ -0,0 +1,70 @@ +class PathError(Exception): + def __init__(self, path): + self.path = path + super().__init__(self.path) + +class PathNotFoundError(PathError): + pass + +class MissingRequirementError(PathError): + pass + +class NotDictError(PathError): + pass + +class Config: + def __init__(self, data, delimiter = "/", defaults = {}, requirements = [], **misc_options): + self.data = data + self.delimiter = delimiter + self.strict_subscript = misc_options.get("strict_subscript", False) + self.clobber_subscript = misc_options.get("clobber_subscript", False) + + for r in requirements: + if not self.get(r): raise MissingRequirementError(r) + for d in defaults: + try: + self.get(d, strict = True) + except PathNotFoundError: + self.set(d, defaults[d]) + + def split_path(self, path): + return path.split(self.delimiter) + + def get(self, path, fallback = None, strict = False): + elementsAll = self.split_path(path) + + def g(elems = elementsAll, obj = self.data): + if not elems or not elems[0] or type(obj) != dict: return obj + head, tail = elems[0], elems[1:] + if strict and head not in obj: + raise PathNotFoundError(path) + return g(tail, obj.get(head, fallback)) + + return g() + + def set(self, path, value, clobber = False): + elementsAll = self.split_path(path) + + def s(elems = elementsAll, obj = self.data): + head, tail = elems[0], elems[1:] + + # YANDEV MODE ACTIVATE!!! + if not tail: + obj[head] = value + else: + if head not in obj or type(obj[head]) != dict: + if clobber: + obj[head] = {} + else: + raise NotDictError(self.delimiter.join(elementsAll[:-len(tail)])) + + s(tail, obj[head]) + + s() + + def __getitem__(self, path): + return self.get(path, self.strict_subscript) + + def __setitem__(self, path, value): + self.set(path, value, self.clobber_subscript) + diff --git a/src/confthing/confthing.py b/src/confthing/confthing.py new file mode 100644 index 0000000..a68ea3b --- /dev/null +++ b/src/confthing/confthing.py @@ -0,0 +1,69 @@ +import yaml + +class PathError(Exception): + def __init__(self, path): + self.path = path + super().__init__(self.path) + +class PathNotFoundError(PathError): + pass + +class MissingRequirementError(PathError): + pass + +class NotDictError(PathError): + pass + +class Config: + def __init__(self, data, delimiter = "/", defaults = {}, requirements = [], **misc_options): + self.data = yaml.safe_load(open(data)) if type(data) != dict else data + self.delimiter = delimiter + self.strict_subscript = misc_options.get("strict_subscript", False) + self.clobber_subscript = misc_options.get("clobber_subscript", False) + + for r in requirements: + if not self.get(r): raise MissingRequirementError(r) + for d in defaults: + if not self.get(d): self.set(d, defaults[d]) + + def split_path(self, path): + return path.split(self.delimiter) + + def get(self, path, fallback = None, strict = False): + elementsAll = self.split_path(path) + + def g(elems = elementsAll, obj = self.data): + if not elems or not elems[0] or type(obj) != dict: return obj + head, tail = elems[0], elems[1:] + if strict and head not in obj: + raise PathNotFoundError(path) + return g(tail, obj.get(head, fallback)) + + return g() + + def set(self, path, value, clobber = False): + elementsAll = self.split_path(path) + + def s(elems = elementsAll, obj = self.data): + head, tail = elems[0], elems[1:] + + # YANDEV MODE ACTIVATE!!! + if not tail: + obj[head] = value + else: + if head not in obj or type(obj[head]) != dict: + if clobber: + obj[head] = {} + else: + raise NotDictError(self.delimiter.join(elementsAll[:-len(tail)])) + + s(tail, obj[head]) + + s() + + def __getitem__(self, path): + return self.get(path, self.strict_subscript) + + def __setitem__(self, path, value): + self.set(path, value, self.clobber_subscript) + diff --git a/src/test.py b/src/test.py new file mode 100755 index 0000000..10d0a28 --- /dev/null +++ b/src/test.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +import confthing, yaml + +conf = confthing.Config(yaml.safe_load(open("config-test.yaml")), defaults = { + "alpha/aleph/llll": 1337 +}, requirements = [ + "alpha/bet" +], clobber_subscript = True) + +test = lambda n: print("\"%s\"\n\t%s\n" % (n, conf[n])) + +test("") +test("alpha/aleph") +conf["alpha/aleph"] = {"i": "now set to a string"} +test("alpha/aleph/i") +conf["alpha/aleph/i/one/two/three"] = 12345 +test("alpha/aleph/i")