# User's Guide The main purpose of `FlexDict` is to allow you to work with deeply nested dictionaries with minimal amount of code. It achieves this purpose by providing an automatic nesting algorithm. It can be a dangerous feature if not used with caution. That's why, `FlexDict` provides some helper methods to prevent any unintentional side-effects. ## Setting Items When it comes to setting dictionary items, `FlexDict` provides many options. Let's start with the most *slick* way: ```python f = FlexDict() f['easily', 'create', 'deeply', 'nested', 'structures'] = 1 ``` The resulting dictionary would be: ```console {'easily':{'create':{'deeply':{'nested':{'structures': 1}}}} ``` You can directly pass instances of `list`, `tuple` or `set` instead: ```python f[['easily', 'create', 'deeply', 'nested', 'structures']] f[('easily', 'create', 'deeply', 'nested', 'structures')] f[{'easily', 'create', 'deeply', 'nested', 'structures'}] ``` You also have other options: ```python f['easily']['create']['deeply']['nested']['structures'] = 1 f.set(['easily', 'create', 'deeply', 'nested', 'structures'], 1) f.set(('easily', 'create', 'deeply', 'nested', 'structures'), 1) f.set({'easily', 'create', 'deeply', 'nested', 'structures'}, 1) ``` The resulting dictionary would be the same for all these examples. However, the `set` method provides many other features. For example, you may only want to set the dictionary items if they do not already exist: ```python f = FlexDict({'a': {'b':1}}) f.set(['a', 'b'], 2, overwrite=False) f.set(['a', 'c'], 2, overwrite=False) ``` This prevents you from overwriting existing values: ```console {'a': {'b': 1, 'c': 2}} ``` Or, if you need a counter, you can use the `increment` argument to do exactly that: ```python f = FlexDict() for i in range(20): if i % 2 == 0: f.set('Even', 1, increment=True) else: f.set('Odd', 1, increment=True) f ``` Output: ```console {'Even': 10, 'Odd': 10} ``` (Note that `overwrite` argument has no effect when `increment` is enabled.) ## Getting Items Again, `FlexDict` provides many alternative ways to access your dictionary items: ```python f = FlexDict({'key1': {'key2': {'key3': 1}}}) # 1 f['key1', 'key2', 'key3'] # 2 f['key1']['key2']['key3'] # 3 f.get(['key1', 'key2', 'key3']) f.get(('key1', 'key2', 'key3')) f.get({'key1', 'key2', 'key3'}) ``` They will all return the same result: ```console 1 ``` There is a crucial distinction between these alternatives. Whenever you use squared brackets to access an item, `FlexDict` **will automatically create the keys and fill the value with an empty** `FlexDict` **if there is no such item**: ```python f = FlexDict() f['a', 'b'] f ``` Output: ```console {'a': {'b': {}}} ``` To prevent this side-effect, `FlexDict` provides two options. First one, is the `get` method: ```python f = FlexDict() f.get(['a', 'b']), f.get(['a', 'b'], default=0), f ``` The `get` method returns the value provided with the `default` argument if the target item does not exist: ```console (None, 0, {}) ``` The other option to avoid the aformentioned side-effect is to use the recursive locking mechnasim via the `lock` method. We will cover it later in this guide. However, just to give you a taste of it, the following example is added: ```python f = FlexDict() f.lock() f['a', 'b'] ``` Output: ```console KeyError: 'a' ``` Getting the top level keys and values works just like a regular `dict`: ```python f = FlexDict({'a': 1, 'b': 2}) f.keys(), f.values() ``` The only difference you would notice is `f.values()` returns a `list` instead of `dict_values`. This is an intentional behavior since we are working with nested dictionaries: ```console (dict_keys(['a', 'b']), [1, 2]) ``` You may also want to get every key and/or value inside your `FlexDict` instance, even the nested ones. `FlexDict` can do this with recursion: ```python f = FlexDict({ 'a': { 'b': 1, 'c': { 'd': 1, 'e': { 'a': 3 } } }, 'g': 4 }) f.keys(nested=True), f.values(nested=True) ``` This allows you to check exactly what is inside your `FlexDict` instance: ```console (['a', 'b', 'c', 'd', 'e', 'a', 'g'], [1, 1, 3, 4]) ``` You can even get rid of the duplicates: ```python f.keys(nested=True, unique=True), f.values(nested=True, unique=True) ``` Note that unique items gets returned inside of a `set`: ```console ({'a', 'b', 'c', 'd', 'e', 'g'}, {1, 3, 4}) ``` If you wish, you can flatten the entire `FlexDict` instance. The `flatten` method returns a `list` of values and their respective key-paths: ```python f.flatten() ``` Output: ``` [(['a', 'b'], 1), (['a', 'c', 'd'], 1), (['a', 'c', 'e', 'a'], 3), (['g'], 4)] ``` Last but not least, if you wish to get the last item and remove it from the `FlexDict` instance, you can use the `pop` method: ```python f = FlexDict({'a': 1, 'b': 2}) f.pop(), f ``` Output: ```python ({'b': 2}, {'a': 1}) ``` ## Locking & Unlocking Automatic Nesting Like we discussed above, automatic nesting can be very dangerous in some cases. Thats why, aside from the previously mentioned workarounds, `FlexDict` provides a recursive algorithm to lock and unlock this feature: ```python f = FlexDict() f.lock() f['a'] = 1 # Normal `dict` behavior works as expected try: f['b', 'c'] = 1 # Will throw a KeyError except KeyError: f.unlock() f['b', 'c'] = 1 f ``` Output: ```console {'a': 1, 'b': {'c': 1}} ``` Each `FlexDict` instance has an attribute called `locked` which tells if it is locked. **Each nested dictionary inside a** `FlexDict` **instance is also a seperate** `FlexDict` **instance!** This means, each of them has seperate `locked` attributes. The `lock` method sets the `locked` attribute of the specified `FlexDict` instance and of all the other nested dictionaries inside of it to `True`. `unlock` method on the other hand, does the exact opposite. This means that you can create any hybrid lock structure you want (Do that with caution!): ```python f = FlexDict({'secure': {}, 'not_secure': {}}) f['secure'].lock() f.locked, f['secure'].locked, f['not_secure'].locked ``` Output: ```console (False, True, False) ``` Both `lock` and `unlock` methods provide an argument called `inplace` which allows you to create locked/unlocked copies of your `FlexDict` instances: ```python f = FlexDict() f_locked = f.lock(inplace=False) f.locked, f_locked.locked ``` Output: ```console (False, True) ``` ## Other Utility Methods You can check if your `FlexDict` instance contains (is a superset of) or inside of (is a subset of) another `dict` instance. ```python f = FlexDict({'a': {'b': 1}}) f.contains({'b': 1}), f.inside({'c': {'a': {'b': 1}}}) ``` Output: ```console (True, True) ``` `FlexDict` also allows you to easily get the length (number of keys) and size (number of keys and values) inside your dictionaries via `length` and `size` methods. They both utilize the previously mentioned `keys` and `values` methods. Hence, they can work recursively and get rid of duplicates if you wish: ```python f = FlexDict({ 'a': { 'b': 1, 'c': { 'd': 1, 'e': { 'a': 3 } } }, 'g': 4 }) # Can be used as a replacement for len() print(f'Number of keys:', f.length()) print(f'Number of keys (Recursive):', f.length(nested=True)) print(f'Number of keys (Recursive, Unique):', f.length(nested=True, unique=True)) # Saves some of your time print(f'\nNumber of items (Recursive):', f.size()) print(f'Number of items (Recursive, Unique):', f.size(unique=True)) ``` Output: ```console Number of keys: 2 Number of keys (Recursive): 7 Number of keys (Recursive, Unique): 6 Number of items (Recursive): 11 Number of items (Recursive, Unique): 9 ```