# (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\w+/?)* any path-part followed by /, repeated 0 - n times # - (?P(\w*\.?\w*)+ any file.ext, at least once pathlike = lambda s: re.match(r"^\/?(?P\w+/?)*(?P(\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 ''")