@@ -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/ |
@@ -0,0 +1,13 @@ | |||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE | |||
Version 2, December 2004 | |||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> | |||
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. |
@@ -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. |
@@ -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" |
@@ -0,0 +1,13 @@ | |||
alpha: | |||
aleph: | |||
a: 12345 | |||
b: abcde | |||
c: ["i","ro","ha"] | |||
bet: 123 | |||
gimel: "abc" | |||
dalet: | |||
a: | |||
b: | |||
c: | |||
d: "hey" | |||
@@ -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) | |||
@@ -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) | |||
@@ -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") |