Use case: I sometimes write programs that want to store a little state between runs, e.g., "What was the last file selected last time this program ran?" I wanted a convenient way to track this state. Python's built-in shelve
module is pretty good but I wanted a way to restrict the possible set of keys in the shelf so that I wouldn't (due to a bag) store something under some unexpected key that would never be read. While I was at it I made it so the you access values using foo.a
instead of foo['a']
, though I paid a price for this.
Questions I have about this code are:
- Is there an easier way I could have accomplished this using built-in stuff or widely used libraries?
- Did I choose the right design?
- I could have tried to inherit from one of the existing shelf classes, though the
shelve
module decides at runtime what sort of shelf you should get back fromshelve.open
. - I could have tried to use descriptors to create my own pseudo-properties, sort of like what's done here, but I don't see a need to decide which fields to include at the class, rather than instance, level
- I could have tried to inherit from one of the existing shelf classes, though the
- Is there any less gross for me to use
__getattr__
and__setattr__
(without having to store a list of fields to be handled as special)? - Did I handle the nested context managers (shelf within
RestrictedShelf
) correctly? Do I need a bunch of try/except logic in__exit__
, as in the fourth example in this answer? - Should I be defining my own exception classes, identical with
Exception
beyond the class name, as I did? - Is making _check_special_keys a class method, rather than a static method, the correct approach?
import shelve
import os
import sys
class UnexpectedKeyError(Exception):
pass
class BadSpecialKey(Exception):
pass
class RestrictedShelf(object):
_SPECIAL_KEYS = ["_filepath", "_keys", "_shelf"]
@classmethod
def _check_special_keys(cls):
for key in cls._SPECIAL_KEYS:
if not key.startswith("_"):
raise BadSpecialKey("Special key does not start with underscore: '{}'".format(key))
def __init__(self, filepath, keys):
self._filepath = filepath
self._keys = list(keys) # Make a copy in case someone modifies the original list
for key in keys:
if not isinstance(key, str):
raise TypeError("String object expected for key, {} found".format(key.__class__.__name__))
# The keys in _SPECIAL_KEYS can't be used, but let's just rule out all keys that start with underscore for good measure
if key.startswith("_"):
raise ValueError("Key illegally starts with underscore: '{}'".format(key))
def __enter__(self):
self._shelf = shelve.open(self._filepath)
for key in self._shelf.keys():
if key not in self._keys:
# This is not quite the same thing as a regular KeyError
raise UnexpectedKeyError(key, "Shelf at '{}' contains unexpected key '{}'".format(self._filepath, key))
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if sys.version_info[0] <= 2:
self._shelf.close()
else:
self._shelf.__exit__(exc_type, exc_val, exc_tb)
def __getattr__(self, key):
self.check_key(key)
return self._shelf.get(key)
def __setattr__(self, key, value):
if key in self._SPECIAL_KEYS:
# http://stackoverflow.com/a/7042247/2829764
super(RestrictedShelf, self).__setattr__(key, value)
else:
self.check_key(key)
self._shelf[key] = value
def check_key(self, key):
if key not in self._keys:
raise KeyError(key)
RestrictedShelf._check_special_keys()
if __name__ == "__main__":
with RestrictedShelf(os.path.expanduser("~/tmp/test.shelf"), "a b c".split()) as rs:
print(rs.a) # `None` the first time you run this; `10` thereafter
rs.a = 10
print(rs.a) # 10
print(rs.b) # None