an extensible multi-markup reader in less than 100 lines of python code
# (c) miraculixx, licensed as by the terms of WTFPL, http://www.wtfpl.net/txt/copying/ | |
# License: DO WHATEVER YOU WANT TO with this code. | |
# | |
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR | |
# IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED | |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. | |
# | |
from io import StringIO | |
from contextlib import contextmanager | |
import re | |
def markup(file_or_str, parsers=None, direct=True, on_error='warn', default=None, msg='could not read {}', | |
**kwargs): | |
""" | |
a safe markup file reader, accepts json and yaml, returns a dict or a default | |
Usage: | |
file_or_str = filename|file-like|markup-str | |
# try, return None if not readable, will issue a warning in the log | |
data = markup(file_or_str) | |
# try, return some other default, will issue a warning in the log | |
data = markup(file_or_str, default={}) | |
# try and fail | |
data = markup(file_or_str, on_error='fail') | |
Args: | |
file_or_str (None, str, file-like): any file-like, can be | |
any object that the parsers accept | |
parsers (list): the list of parsers, defaults to json.load, yaml.safe_load, | |
json.loads | |
direct (bool): if True returns the result, else returns markup (self). then use | |
.read() to actually read the contents | |
on_error (str): 'fail' raises a ValueError in case of error, 'warn' outputs a warning to the log, | |
and returns the default, 'silent' returns the default. Defaults to warn | |
default (obj): return the obj if the input is None or in case of on_error=warn or silent | |
**kwargs (dict): any kwargs passed on to read(), any entry that matches a parser | |
function's module name will be passed on to the parser | |
Returns: | |
data parsed or default | |
markups.exceptions contains list of exceptions raised, if any | |
""" | |
import json | |
import yaml | |
import logging | |
parsers = parsers or (json.load, yaml.safe_load, json.loads) | |
# path-like regex | |
# - \/? leading /, optional | |
# - (?P<path>\w+/?)* any path-part followed by /, repeated 0 - n times | |
# - (?P<ext>(\w*\.?\w*)+ any file.ext, at least once | |
pathlike = lambda s: re.match(r"^\/?(?P<path>\w+/?)*(?P<ext>(\w*\.?\w*))$", s) | |
@contextmanager | |
def fopen(filein, *args, **kwargs): | |
# https://stackoverflow.com/a/55032634/890242 | |
if isinstance(filein, str) and pathlike(filein): # filename | |
with open(filein, *args, **kwargs) as f: | |
yield f | |
elif isinstance(filein, str): # some other string, make a file-like | |
yield StringIO(filein) | |
else: | |
# file-like object | |
yield filein | |
throw = lambda ex: (_ for _ in ()).throw(ex) | |
exceptions = [] | |
def read(**kwargs): | |
if file_or_str is None: | |
return default | |
for fn in parsers: | |
try: | |
with fopen(file_or_str) as fin: | |
if hasattr(fin, 'seek'): | |
fin.seek(0) | |
data = fn(fin, **kwargs.get(fn.__module__, {})) | |
except Exception as e: | |
exceptions.append(e) | |
else: | |
return data | |
# nothing worked so far | |
actions = { | |
'fail': lambda: throw(ValueError("Reading {} caused exceptions {}".format(file_or_str, exceptions))), | |
'warn': lambda: logging.warning(msg.format(file_or_str)) or default, | |
'silent': lambda: default, | |
} | |
return actions[on_error]() | |
markup.read = read | |
markup.exceptions = exceptions | |
return markup.read(**kwargs) if direct else markup | |
def raises(fn, wanted_ex): | |
try: | |
fn() | |
except Exception as e: | |
assert isinstance(e, wanted_ex), "expected {}, raised {} instead".format(wanted_ex, e) | |
else: | |
raise ValueError("did not raise {}".format(wanted_ex)) | |
return True | |
if __name__ == '__main__': | |
import sys | |
from pprint import pprint | |
if len(sys.argv) > 1: | |
pprint(markup(sys.argv[1], on_error='fail')) | |
else: | |
print("testing...") | |
assert markup('foo: bar') == {'foo': 'bar'} | |
assert markup('{"foo": "bar"}') == {'foo': 'bar'} | |
assert markup(StringIO("foo: bar")) == {'foo': 'bar'} | |
#assert markup('test.txt') == {'foo': 'bar'} | |
assert raises(lambda : markup('xtest.txt', on_error='fail') == {'foo': 'bar'}, ValueError) | |
assert isinstance(markup('xtest.txt', on_error='silent', default=markup).exceptions[0], FileNotFoundError) | |
assert markup('failed: - bar') is None | |
assert markup('failed: - bar', default={}) == {} | |
assert raises(lambda : markup('failed: - bar', on_error='fail'), ValueError) | |
assert markup('failed: - bar', on_error='silent') is None | |
assert markup('failed: - bar', on_error='silent', default="nothing") == 'nothing' | |
assert markup('', default="nothing") == 'nothing' | |
assert lambda : markup('.', on_error='silent', default="nothing") == 'nothing' | |
assert lambda : markup('.', on_error='fail') | |
assert markup('foo: bar', direct=False) == markup and markup.read() == {'foo': 'bar'} | |
print("ok. use as python markup.py '<file or markup>'") |
This comment has been minimized.
This comment has been minimized.
To install this:
Then use it from your Python code
or from the command line: $ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
as seen on https://twitter.com/miraculixxs/status/1271763391921688577
If you're using this, by all means, do. If you care to reply or at least like the tweet that might just make my day :-)