HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Dictionary with restricted keys

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
withdictionaryrestrictedkeys

Problem

I'm currently building some software that works with systemd unit files, and I've been trying to improve how we construct the unit files.

This is all running in Python 3.4

This first block is the base classes I'm building things up with.

```
class LimitedDict(dict):
"""
Sub Class of the dictionary object that restricts the allowed keys.
The allowed keys are set when creating the object.

>>> a = LimitedDict(allowed_keys=['foo'], foo='bar')
>>> a == {'foo': 'bar'}
True

>>> ok_keywords = {'foo': 'bar'}
>>> a = LimitedDict(allowed_keys=['foo'], **ok_keywords)
>>> a == {'foo': 'bar'}
True

>>> LimitedDict(allowed_keys=['foo'], foo='bar', fail='true')
Traceback (most recent call last):
File "", line 1, in
File "", line 13, in __init__
KeyError: "key: 'fail' not in allowed keys: '['foo']'"

>>> not_ok_keywords = {'foo': 'bar', 'fail': 'oops'}
>>> LimitedDict(allowed_keys=['foo'], **not_ok_keywords)
Traceback (most recent call last):
File "", line 1, in
File "", line 13, in __init__
KeyError: "key: 'fail' not in allowed keys: '['foo']'"

>>> a = LimitedDict(allowed_keys=['foo'], foo='bar')
>>> a['fail'] = 'true'
Traceback (most recent call last):
...
KeyError: "key: 'fail' not in allowed keys: '['foo']'"
"""
_allowed_keys = list()

def __init__(self, allowed_keys, **kwargs):
if not isinstance(allowed_keys, list):
AttributeError("'allowed_keys' must be a list")
self._allowed_keys = allowed_keys
for key in kwargs.keys():
if key not in self._allowed_keys:
raise KeyError("key: '" + str(key) + "' not in allowed keys: '" + str(self._allowed_keys) + "'")
self[key] = kwargs[key]

def __setitem__(self, key, val):
if key not in self._allowed_keys:
raise KeyError("key: '" + str(key) + "' not in allowed keys: '" + str(self._allowed_keys) + "'")
di

Solution

To re-factor this code firstly I'd recommend using packages instead of nested classes here, so create a package named Sections and create two more packages named Unit and Services inside of it, you can also move the dictionary definitions inside of this package say in a file named dicts.py and each of those inner packages then can import the required dict(s) if required.

The structure will look something like this:

$ tree Sections/
Sections/
├── dicts.py
├── __init__.py
├── Service
│   └── __init__.py
└── Unit
└── __init__.py


Here to use the nested dot notation Sections.Unit.Description we import the required items from each of the individual packages to Sections's __init__.py file:

$ cat Sections/__init__.py
from .Unit import After, Description
from .Service import ExecStartPre, ExecStart, ExecStartPost, ExecStopPre,\
ExecStop, ExecStopPost, TimeoutStartSec


Similarly the contents of Unit/__init__.py and Services/__init__.py are:

$ cat Sections/Unit/__init__.py
from ..dicts import MightyMorphingMetaMagic

class Description(MightyMorphingMetaMagic):
pass

class After(MightyMorphingMetaMagic):
pass

class Require(MightyMorphingMetaMagic):
pass

$ cat Sections/Service/__init__.py
from ..dicts import MightyMorphingMetaMagic

class TimeoutStartSec(MightyMorphingMetaMagic):
pass

class ExecStartPre(MightyMorphingMetaMagic):
pass

class ExecStart(MightyMorphingMetaMagic):
pass

class ExecStartPost(MightyMorphingMetaMagic):
pass

class ExecStopPre(MightyMorphingMetaMagic):
pass

class ExecStop(MightyMorphingMetaMagic):
pass

class ExecStopPost(MightyMorphingMetaMagic):
pass


Now in your main script you can simply import Sections(considering this packages lies in the module search path):

import Sections

unit_json = [

Sections.Unit.Description(value=str(self.description)),
Sections.Service.ExecStartPre(value=str(self.execstartpre)),
Sections.Service.ExecStart(value=str(self.execstart)),
Sections.Service.ExecStop(value=str(self.execstop))
]


Now to accommodate this package based version we will have to make some changes:

-
We can't use __qualname__ now, an alternative is to use the __module__ argument of the class object.

-
Instead of caller.__class__ to get instance's class we can use type(caller). In new-style classes both do the same thing.

-
Instead of using a list for _allowed_keys it's better to use a set as it stores only unique items and provides O(1) lookups.

-
We can allow all types of iterables to be passed as _allowed_keys instead of just list, we can check this using an abstract base class collections.Iterable.

-
As allowed_keys is kind of compulsory argument here, better make it a key-word only argument with default value of None, this will also help in separating it from normal kwargs.

-
Use string formatting instead of str() calls and +, plus using 'str(some_key)' can be confusing to the user when some_key is 1 or '1'. A better way is to use the repr representation of the object, in new-style string formatting we can obtain it using {!r}.

-
While checking for a key in a dict simply use if key in dict, the .keys() call on the dict is unnecessary and will take O(N) time in Python 2.

-
To get the keys of a dictionary use list(some_dict) instead list(some_dict.keys()), this works as is in both Python 2 and 3. But again we should better pass the keys in the form of a set whenever possible.

-
Use for key, value in some_dict.items(): if you want key as well corresponding value while iterating over a dict.

-
I noticed that you are calculating super_kwargs inside __init__ of each class that too from the kwargs passed to that class. I'd say why not define _allowed_key in that class itself, and the call super().__init__(allowed_keys=self._allowed_keys, **kwargs). Now the baseclass LimitedDict will handle the rest.

-
To support the super().__init__ I've used the same signature in each class's __init__ method.

-
I cannot think of a way to use a metaclass here myself, so I moved build_option_dict inside of the class.

-
if not kwargs['value']: raise KeyError doesn't make much sense, better store the value in some variable and if the key is missing Python will raise an error itself.

Now my dicts.py looks like(removed doctests to save some space):

`from collections import Iterable

class LimitedDict(dict):

_allowed_keys = set()

def __init__(self, *, allowed_keys=None, **kwargs):

if not isinstance(allowed_keys, Iterable):
AttributeError("'allowed_keys' must be an iterable")
self._allowed_keys = allowed_keys
for key, value in kwargs.items():
if key not in self._allowed_keys:
raise KeyError("key: {!r} not in allowed keys: {!r}".format(key,
self._allowed

Context

StackExchange Code Review Q#81794, answer score: 2

Revisions (0)

No revisions yet.