I have common use cases where I want to improve python's dict and list:
AttrDict: attribute-baseddictwith better merge and deep value accessAttrList:listthat broadcasts operations to its members- Environment: reading environment files
- Configuration: loading command-line arguments and configuration files
JSend: sending JSON responses
python -m pip install attrboxAttrDict features:
-
Attribute Syntax for
dictsimilar to accessing properties in JavaScript:thing.propmeansthing["prop"]for get / set / delete. -
No
KeyError: if a key is missing, just returnNone(likedict.get()). -
Deep Indexing: use a list of keys and
intto get and set deeply nested values. This is similar to lodash.get except that only the array-like syntax is supported and you must use actualintto index acrosslistobjects. -
Deep Merge: combine two
dictobjects by extending deeply-nested keys where possible. This is different than the newdictunion operator (PEP 584).
from attrbox import AttrDict
items = AttrDict(a=1, b=[{"c": {"d": 5}}], e={"f": {"g": 7}})
items.a
# => 1
items.x is None
# => True
items.x = 10
items['x']
# => 10
items.get(["b", 0, "c", "d"])
# => 5
items <<= {"e": {"f": {"g": 20, "h": [30, 40]}}}
items.e.f.g
# => 20
items[['e', 'f', 'h', 1]]
# => 40AttrList provides member broadcast: performing operations on the list performs the operation on all the items in the list. I typically use this to achieve the Composite design pattern.
from attrbox import AttrDict, AttrList
numbers = AttrList([complex(1, 2), complex(3, 4), complex(5, 6)])
numbers.real
# => [1.0, 3.0, 5.0]
words = AttrList(["Apple", "Bat", "Cat"])
words.lower()
# => ['apple', 'bat', 'cat']
items = AttrList([AttrDict(a=1, b=2), AttrDict(a=5)])
items.a
# => [1, 5]
items.b
# => [2, None]attrbox.env is similar to python-dotenv, but uses the AttrDict ability to do deep indexing to allow for things like dotted variable names. Typically, you'll use it by calling attrbox.load_env() which will find the nearest .env file and load it into os.environ.
attrbox supports loading configuration files from .json, .toml, and .env files. By default, load_config() looks for a key imports and will recursively import those files (relative to the current file) before loading the rest of the current file (data is merged using AttrDict). This allows you to create templates or smaller configurations that build up to a larger configuration.
For CLI applications, attrbox.parse_docopt() let's you use the power of docopt with the flexibility of AttrDict. By default, --config and <config> arguments will load the file using the load_config()
"""Usage: prog.py [--help] [--version] [-c CONFIG] --file FILE
Options:
--help show this message and exit
--version show the version number and exit
-c, --config CONFIG load this configuration file (supported: .toml, .json, .env)
--file FILE the file to process
"""
def main():
args = parse_docopt(__doc__, version=__version__)
args.file # has the value of --file
if __name__ == "__main__":
main()Building on top of docopt we strip off leading dashes and convert them to underscores so that we can access the arguments as AttrDict attributes.
Read more about attrbox.config
JSend is an approximate implementation of the JSend specification that makes it easy to create standard JSON responses. The main difference is that I added an ok attribute to make it easy to tell if there was a problem (fail or error).
from attrbox import JSend
def some_function(arg1):
result = JSend() # default is "success"
if not is_good(arg1):
# fail = controlled problem
return result.fail(message="You gone messed up.")
try:
result.success(data=process(arg1))
except Exception:
# error = uncontrolled problem
return result.error(message="We have a problem.")
return resultBecause the JSend object is an AttrDict, it acts like a dict in every other respect (e.g., it is JSON-serializable).
The vendorized version of docopt-ng is also licensed under the MIT License.
