Dotty Dict¶
Dotty Dict is a wrapper around builtin dictionary. Provides quick access to deeply nested keys and values with dot notation. Dotty Dict expose dictionary public API as proxy to dict implemented underneath and should work with all dict-like objects which are instances of Mapping.
Dotty-Dict¶
Info: | Dictionary wrapper for quick access to deeply nested keys. |
---|---|
Author: | Pawel Zadrozny @pawelzny <pawel.zny@gmail.com> |
Features¶
- Simple wrapper around python dictionary and dict like objects
- Two wrappers with the same dict are considered equal
- Access to deeply nested keys with dot notation:
dot['deeply.nested.key']
- Create, read, update and delete nested keys of any length
- Expose all dictionary methods like
.get
,.pop
,.keys
and other - Access dicts in lists by index
dot['parents.0.first_name']
- key=value caching to speed up lookups and low down memory consumption
- support for setting value in multidimensional lists
- support for accessing lists with slices
Installation¶
pip install dotty-dict
Documentation¶
- Full documentation: http://dotty-dict.readthedocs.io
- Public API: http://dotty-dict.readthedocs.io/en/latest/api.html
- Examples and usage ideas: http://dotty-dict.readthedocs.io/en/latest/examples.html
TODO¶
Waiting for your feature requests ;)
Quick Example¶
Create new dotty using factory function.
>>> from dotty_dict import dotty
>>> dot = dotty({'plain': {'old': {'python': 'dictionary'}}})
>>> dot['plain.old']
{'python': 'dictionary'}
You can start with empty dotty
>>> from dotty_dict import dotty
>>> dot = dotty()
>>> dot['very.deeply.nested.thing'] = 'spam'
>>> dot
Dotty(dictionary={'very': {'deeply': {'nested': {'thing': 'spam'}}}}, separator='.', esc_char='\\')
>>> dot['very.deeply.spam'] = 'indeed'
>>> dot
Dotty(dictionary={'very': {'deeply': {'nested': {'thing': 'spam'}, 'spam': 'indeed'}}}, separator='.', esc_char='\\')
>>> del dot['very.deeply.nested']
>>> dot
Dotty(dictionary={'very': {'deeply': {'spam': 'indeed'}}}, separator='.', esc_char='\\')
>>> dot.get('very.not_existing.key')
None
NOTE: Using integer in dictionary keys will be treated as embedded list index.
Limitations¶
In some very rare cases dotty may not work properly.
- When nested dictionary has two keys of different type, but with the same value. In that case dotty will return dict or list under random key with passed value.
- Keys in dictionary may not contain dots. If you need to use dots, please specify dotty with custom separator.
- Nested keys may not be bool type. Bool type keys are only supported when calling keys with type defined value (e.g. dot[True], dot[False]).
Examples¶
Yes, I know it’s dangerous to follow code examples. Usually examples aren’t in sync with real source code.
But I found a solution … I hope!
Note
See also
Look at Public API for more details.
Basics¶
The easiest way to use Dotty dict is with function factory. Factory takes only one, optional dictionary as argument.
If leaved empty, factory function will create new, empty dictionary.
Wrap existing dict¶
from dotty_dict import dotty
data = {'status': 'ok', 'code': 200, 'data': {'timestamp': 1525018224,
'payload': []}}
data = dotty(data)
assert data['data.timestamp'] == 1525018224
Create new dotty¶
from dotty_dict import dotty
data = dotty()
data['status'] = 'ok'
data['data.timestamp'] = 1525018224
data['data.fancy.deeply.nested.key.for'] = 'fun'
assert data == {'status': 'ok',
'data': {
'timestamp': 1525018224,
'fancy': {
'deeply': {
'nested': {
'key': {
'for': 'fun',
},
},
},
},
}}
Builtin methods¶
Dotty exposes all native to dict, builtin methods. Only change is made to method which uses key as input to accept dot notation.
from dotty_dict import dotty
dot = dotty({'status': 'ok',
'data': {
'timestamp': 1525018224,
'fancy': {
'deeply': {
'nested': {
'key': {
'for': 'fun',
},
},
},
},
}})
# get value, return None if not exist
assert dot.get('data.payload') is None
# pop key
assert dot.pop('data.fancy.deeply.nested.key') == {'for': 'fun'}
# get value and set new value if not exist
assert dot.setdefault('data.payload', []) == []
assert 'payload' in dot['data']
# check what changed
assert dot == {'status': 'ok',
'data': {
'timestamp': 1525018224,
'fancy': {
'deeply': {
'nested': {},
},
},
'payload': [],
}}
# get keys
assert sorted(dot.keys()) == ['data', 'status']
Advanced¶
Lets simulate more real scenario. API requests and responses are often very complex
with many deeply nested keys. And when you need to check one of them it may
looks like: res.get('data', {}).get('service', {}).get('status', {}).get('current', False)
.
It’s awful! All this empty dictionary fallback to dig in for current status!
Make API request¶
In this scenario we will send post request to create new user with superuser privileges. Below there is example response as dictionary, and then the way to check granted privileges.
def make_request(payload):
"""Fake request for example purpose.
:param dict payload: Example payload
:return dict: Example response
"""
return {
'status': {
'code': 200,
'msg': 'User created',
},
'data': {
'user': {
'id': 123,
'personal': {
'name': 'Arnold',
'email': 'arnold@dotty.dict',
},
'privileges': {
'granted': ['login', 'guest', 'superuser'],
'denied': ['admin'],
'history': {
'actions': [
['superuser granted', '2018-04-29T17:08:48'],
['login granted', '2018-04-29T17:08:48'],
['guest granted', '2018-04-29T17:08:48'],
['created', '2018-04-29T17:08:48'],
['signup_submit', '2018-04-29T17:08:47'],
],
},
},
},
},
}
from dotty_dict import dotty
request = dotty()
request['request.data.payload'] = {'name': 'Arnold',
'email': 'arnold@dotty.dict',
'type': 'superuser'}
request['request.data.headers'] = {'content_type': 'application/json'}
request['request.url'] = 'http://127.0.0.1/api/user/create'
response = dotty(make_request(request.to_dict()))
assert response['status.code'] == 200
assert 'superuser' in response['data.user.privileges.granted']
Access dict with embedded lists¶
This scenario shows how to access subfield in a list.
from dotty_dict import dotty
# dotty supports embedded lists
# WARNING!
# Dotty used to support lists only with dotty_l.
# This feature is depreciated and was removed - now lists have native support.
# If you need old functionality pass additional flag 'no_list' to dotty
dot = dotty({
'annotations': [
{'label': 'app', 'value': 'webapi'},
{'label': 'role', 'value': 'admin'},
],
'spec': {
'containers': [
['gpu', 'tensorflow', 'ML'],
['cpu', 'webserver', 'sql'],
]
}
})
assert dot['annotations.0.label'] == 'app'
assert dot['annotations.0.value'] == 'webapi'
assert dot['annotations.1.label'] == 'role'
assert dot['annotations.1.value'] == 'admin'
assert dot['spec.containers.0.0'] == 'gpu'
assert dot['spec.containers.0.1'] == 'tensorflow'
assert dot['spec.containers.0.2'] == 'ML'
assert dot['spec.containers.1.0'] == 'cpu'
assert dot['spec.containers.1.1'] == 'webserver'
assert dot['spec.containers.1.2'] == 'sql'
Access multiple fields with list slices¶
This scenario shows how to access multiple subfields in a list of dicts.
from dotty_dict import dotty
# dotty supports standard Python slices for lists
dot = dotty({
'annotations': [
{'label': 'app', 'value': 'webapi'},
{'label': 'role', 'value': 'admin'},
{'label': 'service', 'value': 'mail'},
{'label': 'database', 'value': 'postgres'}
],
})
assert dot['annotations.:.label'] == ['app', 'role', 'service', 'database']
assert dot['annotations.:2.label'] == ['app', 'role']
assert dot['annotations.2:.label'] == ['service', 'database']
assert dot['annotations.::2.label'] == ['app', 'service']
Access numeric fields as dict keys¶
This scenario shows how to access numeric keys which should not be treated as list indices.
from dotty_dict import dotty
# For special use cases dotty supports dictionary key only access
# With additional flag no_list passed to dotty
# all digits and slices will be treated as string keys
dot = dotty({
'special': {
'1': 'one',
':': 'colon',
'2:': 'two colons'
}
})
assert dot['special.1'] == 'one'
assert dot['special.:'] == 'colon'
assert dot['special.2:'] == 'two colons'
Escape character¶
In some cases we want to preserve dot in key name and do not treat it as keys separator. It can by done with escape character.
from dotty_dict import dotty
dot = dotty({
'deep': {
'key': 'value',
},
'key.with.dot': {
'deeper': 'other value',
},
})
# how to access deeper value?
assert dot[r'key\.with\.dot.deeper'] == 'other value'
Escape the escape character¶
What if escape character should be preserved as integral key name, but it happens to be placed right before separator character?
The answer is: Escape the escape character.
Warning
Be careful because backslashes in Python require special treatment.
from dotty_dict import dotty
dot = dotty({
'deep': {
'key': 'value',
},
'key.with_backslash\\': { # backslash at the end of key
'deeper': 'other value',
},
})
# escape first dot and escape the escape character before second dot
assert dot[r'key\.with_backslash\\.deeper'] == 'other value'
Customization¶
By default Dotty uses dot as keys separator and backslash as escape character. In special occasions you may want to use different set of chars.
Customization require using Dotty class directly instead of factory function.
Custom separator¶
In fact any valid string can be used as separator.
from dotty_dict import Dotty
dot = Dotty({'deep': {'deeper': {'harder': 'faster'}}}, separator='$', esc_char='\\')
assert dot['deep$deeper$harder'] == 'faster'
Custom escape char¶
As separator, escape character can be any valid string not only single character.
from dotty_dict import Dotty
dot = Dotty({'deep.deeper': {'harder': 'faster'}}, separator='.', esc_char='#')
assert dot['deep#.deeper.harder'] == 'faster'
Public API¶
See also
Check out Examples derived from real and fully tested source code.
-
dotty_dict.dotty_dict.
dotty
(dictionary=None, no_list=False)[source]¶ Factory function for Dotty class.
Create Dotty wrapper around existing or new dictionary.
Parameters: - dictionary (dict) – Any dictionary or dict-like object
- no_list (bool) – If set to True then numeric keys will NOT be converted to list indices
Returns: Dotty instance
-
class
dotty_dict.dotty_dict.
Dotty
(dictionary, separator='.', esc_char='\', no_list=False)[source]¶ Dictionary and dict-like objects wrapper.
Dotty wraps dictionary and provides proxy for quick accessing to deeply nested keys and values using dot notation.
Dot notation can be customize in special cases. Let’s say dot character has special meaning, and you want to use other character for accessing deep keys.
Dotty does not copy original dictionary but it operates on it. All changes made in original dictionary are reflected in dotty wrapped dict and vice versa.
Parameters: - dictionary (dict) – Any dictionary or dict-like object
- separator (str) – Character used to chain deep access.
- esc_char (str) – Escape character for separator.
- no_list (bool) – If set to True then numeric keys will NOT be converted to list indices
-
clear
()¶ Removes all elements from dotty dict.
-
items
()¶ Returns generator of dotty dict’s (key, value) tuple pairs.
-
keys
()¶ Returns generator of dotty dict’s keys.
-
values
()¶ Returns generator of dotty dict’s values.
-
update
(dict2)¶ Adds dictionary dict2’s key-values pairs to dotty dict.
-
static
fromkeys
(seq, value=None)[source]¶ Create a new dictionary with keys from seq and values set to value.
New created dictionary is wrapped in Dotty.
Parameters: - seq – Sequence of elements which is to be used as keys for the new dictionary
- value – Value which is set to each element of the dictionary
Returns: Dotty instance
-
get
(key, default=None)[source]¶ Get value from deep key or default if key does not exist.
This method match 1:1 with dict .get method except that it accepts deeply nested key with dot notation.
Parameters: - key (str) – Single key or chain of keys
- default (Any) – Default value if deep key does not exist
Returns: Any or default value
-
pop
(key, default=None)[source]¶ Pop key from Dotty.
This method match 1:1 with dict .pop method except that it accepts deeply nested key with dot notation.
Parameters: - key (str) – Single key or chain of keys
- default (Any) – If default is provided will be returned
Raises: KeyError – If key does not exist and default has not been provided
Returns: Any or default value
-
static
set_list_index
(data, index, value)[source]¶ Set value in list at specified index. All the values before target index should stay unchanged or be filled with None. :param data: List where value should be set :param index: String or Int of target index :param value: Target value to put under index
-
setdefault
(key, default=None)[source]¶ Get key value if exist otherwise set default value under given key and return its value.
This method match 1:1 with dict .setdefault method except that it accepts deeply nested key with dot notation.
Parameters: - key (str) – Single key or chain of keys
- default (Any) – Default value for not existing key
Returns: Value under given key or default
Credits¶
Development¶
- Pawel Zadrozny @pawelzny <pawel.zny@gmail.com>
Contributors¶
- Linus Groh @linusg
- Andreas Motl @amotl
- Aneesh Devasthale @aneeshd16
- Szymon Piotr Krasuski @Dysproz
Read more how to contribute on Contributing.
Contributing¶
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
You can contribute in many ways:
Types of Contributions¶
Report Bugs¶
Report bugs at https://github.com/pawelzny/dotty_dict/issues
If you are reporting a bug, please include:
- Your operating system name and version.
- Any details about your local setup that might be helpful in troubleshooting.
- Detailed steps to reproduce the bug.
Fix Bugs¶
Look through the GitHub issues for bugs. Anything tagged with “bug” is open to whoever wants to implement it.
Implement Features¶
Look through the GitHub issues for features. Anything tagged with “feature” is open to whoever wants to implement it.
Write Documentation¶
authentication could always use more documentation, whether as part of the official authentication docs, in docstrings, or even on the web in blog posts, articles, and such.
Submit Feedback¶
The best way to send feedback is to file an issue at https://github.com/pawelzny/dotty_dict/issues
If you are proposing a feature:
- Explain in detail how it would work.
- Keep the scope as narrow as possible, to make it easier to implement.
- Remember that this is a volunteer-driven project, and that contributions are welcome :)
Get Started!¶
Ready to contribute? Here’s how to set up dotty_dict for local development.
Fork the dotty_dict repo on GitHub.
Clone your fork locally:
$ git clone git@github.com:your_name_here/dotty_dict.git
Install your local copy into a virtualenv. This is how you set up your fork for local development:
$ cd dotty_dict/ $ make install
or if you don’t have ‘make’, do it manually:
$ cd dotty_dict/
$ pip install poetry==1.1.14
$ poetry install --no-root
Create a branch for local development:
$ git checkout -b name-of-your-bugfix-or-feature
Now you can introduce your changes locally.
When you’re done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:
$ make test-all
or if you don’t have ‘make’, run tox directly:
$ poetry run tox --skip-missing-interpreters
Commit your changes and push your branch to GitHub:
$ git add . $ git commit -m "Your detailed description of your changes." $ git push origin HEAD
Submit a pull request through the GitHub website.
Pull Request Guidelines¶
Before you submit a pull request, check that it meets these guidelines:
The pull request should include tests.
If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst.
The pull request should work for: Python >=3.5,<4.0 and for >=PyPy3.8-7.3.9.
Check https://circleci.com/gh/pawelzny/dotty_dict and make sure that the tests pass for all supported Python versions.
LICENSE¶
MIT License
Copyright (c) 2017, Pawel Zadrozny
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.