324 lines
8.7 KiB
Python
324 lines
8.7 KiB
Python
|
#!/usr/bin/env python
|
||
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
"""
|
||
|
| This file is part of the web2py Web Framework
|
||
|
| Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
|
||
|
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
|
||
|
|
||
|
Provides:
|
||
|
|
||
|
- List; like list but returns None instead of IndexOutOfBounds
|
||
|
- Storage; like dictionary allowing also for `obj.foo` for `obj['foo']`
|
||
|
"""
|
||
|
|
||
|
from pydal.contrib import portalocker
|
||
|
from gluon._compat import copyreg, pickle, PY2
|
||
|
|
||
|
__all__ = ['List', 'Storage', 'Settings', 'Messages',
|
||
|
'StorageList', 'load_storage', 'save_storage']
|
||
|
|
||
|
DEFAULT = lambda: 0
|
||
|
|
||
|
|
||
|
class Storage(dict):
|
||
|
"""
|
||
|
A Storage object is like a dictionary except `obj.foo` can be used
|
||
|
in addition to `obj['foo']`, and setting obj.foo = None deletes item foo.
|
||
|
|
||
|
Example::
|
||
|
|
||
|
>>> o = Storage(a=1)
|
||
|
>>> print o.a
|
||
|
1
|
||
|
|
||
|
>>> o['a']
|
||
|
1
|
||
|
|
||
|
>>> o.a = 2
|
||
|
>>> print o['a']
|
||
|
2
|
||
|
|
||
|
>>> del o.a
|
||
|
>>> print o.a
|
||
|
None
|
||
|
|
||
|
"""
|
||
|
__slots__ = ()
|
||
|
__setattr__ = dict.__setitem__
|
||
|
__delattr__ = dict.__delitem__
|
||
|
__getitem__ = dict.get
|
||
|
__getattr__ = dict.get
|
||
|
__getnewargs__ = lambda self: getattr(dict,self).__getnewargs__(self)
|
||
|
__repr__ = lambda self: '<Storage %s>' % dict.__repr__(self)
|
||
|
# http://stackoverflow.com/questions/5247250/why-does-pickle-getstate-accept-as-a-return-value-the-very-instance-it-requi
|
||
|
__getstate__ = lambda self: None
|
||
|
__copy__ = lambda self: Storage(self)
|
||
|
|
||
|
def getlist(self, key):
|
||
|
"""
|
||
|
Returns a Storage value as a list.
|
||
|
|
||
|
If the value is a list it will be returned as-is.
|
||
|
If object is None, an empty list will be returned.
|
||
|
Otherwise, `[value]` will be returned.
|
||
|
|
||
|
Example output for a query string of `?x=abc&y=abc&y=def`::
|
||
|
|
||
|
>>> request = Storage()
|
||
|
>>> request.vars = Storage()
|
||
|
>>> request.vars.x = 'abc'
|
||
|
>>> request.vars.y = ['abc', 'def']
|
||
|
>>> request.vars.getlist('x')
|
||
|
['abc']
|
||
|
>>> request.vars.getlist('y')
|
||
|
['abc', 'def']
|
||
|
>>> request.vars.getlist('z')
|
||
|
[]
|
||
|
|
||
|
"""
|
||
|
value = self.get(key, [])
|
||
|
if value is None or isinstance(value, (list, tuple)):
|
||
|
return value
|
||
|
else:
|
||
|
return [value]
|
||
|
|
||
|
def getfirst(self, key, default=None):
|
||
|
"""
|
||
|
Returns the first value of a list or the value itself when given a
|
||
|
`request.vars` style key.
|
||
|
|
||
|
If the value is a list, its first item will be returned;
|
||
|
otherwise, the value will be returned as-is.
|
||
|
|
||
|
Example output for a query string of `?x=abc&y=abc&y=def`::
|
||
|
|
||
|
>>> request = Storage()
|
||
|
>>> request.vars = Storage()
|
||
|
>>> request.vars.x = 'abc'
|
||
|
>>> request.vars.y = ['abc', 'def']
|
||
|
>>> request.vars.getfirst('x')
|
||
|
'abc'
|
||
|
>>> request.vars.getfirst('y')
|
||
|
'abc'
|
||
|
>>> request.vars.getfirst('z')
|
||
|
|
||
|
"""
|
||
|
values = self.getlist(key)
|
||
|
return values[0] if values else default
|
||
|
|
||
|
def getlast(self, key, default=None):
|
||
|
"""
|
||
|
Returns the last value of a list or value itself when given a
|
||
|
`request.vars` style key.
|
||
|
|
||
|
If the value is a list, the last item will be returned;
|
||
|
otherwise, the value will be returned as-is.
|
||
|
|
||
|
Simulated output with a query string of `?x=abc&y=abc&y=def`::
|
||
|
|
||
|
>>> request = Storage()
|
||
|
>>> request.vars = Storage()
|
||
|
>>> request.vars.x = 'abc'
|
||
|
>>> request.vars.y = ['abc', 'def']
|
||
|
>>> request.vars.getlast('x')
|
||
|
'abc'
|
||
|
>>> request.vars.getlast('y')
|
||
|
'def'
|
||
|
>>> request.vars.getlast('z')
|
||
|
|
||
|
"""
|
||
|
values = self.getlist(key)
|
||
|
return values[-1] if values else default
|
||
|
|
||
|
|
||
|
def pickle_storage(s):
|
||
|
return Storage, (dict(s),)
|
||
|
|
||
|
copyreg.pickle(Storage, pickle_storage)
|
||
|
if PY2:
|
||
|
PICKABLE = (str, int, long, float, bool, list, dict, tuple, set)
|
||
|
else:
|
||
|
PICKABLE = (str, int, float, bool, list, dict, tuple, set)
|
||
|
|
||
|
class StorageList(Storage):
|
||
|
"""
|
||
|
Behaves like Storage but missing elements defaults to [] instead of None
|
||
|
"""
|
||
|
def __getitem__(self, key):
|
||
|
return self.__getattr__(key)
|
||
|
|
||
|
def __getattr__(self, key):
|
||
|
if key in self:
|
||
|
return self.get(key)
|
||
|
else:
|
||
|
r = []
|
||
|
self[key] = r
|
||
|
return r
|
||
|
|
||
|
|
||
|
def load_storage(filename):
|
||
|
fp = None
|
||
|
try:
|
||
|
fp = portalocker.LockedFile(filename, 'rb')
|
||
|
storage = pickle.load(fp)
|
||
|
finally:
|
||
|
if fp:
|
||
|
fp.close()
|
||
|
return Storage(storage)
|
||
|
|
||
|
|
||
|
def save_storage(storage, filename):
|
||
|
fp = None
|
||
|
try:
|
||
|
fp = portalocker.LockedFile(filename, 'wb')
|
||
|
pickle.dump(dict(storage), fp)
|
||
|
finally:
|
||
|
if fp:
|
||
|
fp.close()
|
||
|
|
||
|
|
||
|
class Settings(Storage):
|
||
|
def __setattr__(self, key, value):
|
||
|
if key != 'lock_keys' and self['lock_keys'] and key not in self:
|
||
|
raise SyntaxError('setting key \'%s\' does not exist' % key)
|
||
|
if key != 'lock_values' and self['lock_values']:
|
||
|
raise SyntaxError('setting value cannot be changed: %s' % key)
|
||
|
self[key] = value
|
||
|
|
||
|
|
||
|
class Messages(Settings):
|
||
|
def __init__(self, T):
|
||
|
Storage.__init__(self, T=T)
|
||
|
|
||
|
def __getattr__(self, key):
|
||
|
value = self[key]
|
||
|
if isinstance(value, str):
|
||
|
return self.T(value)
|
||
|
return value
|
||
|
|
||
|
|
||
|
class FastStorage(dict):
|
||
|
"""
|
||
|
Eventually this should replace class Storage but causes memory leak
|
||
|
because of http://bugs.python.org/issue1469629
|
||
|
|
||
|
>>> s = FastStorage()
|
||
|
>>> s.a = 1
|
||
|
>>> s.a
|
||
|
1
|
||
|
>>> s['a']
|
||
|
1
|
||
|
>>> s.b
|
||
|
>>> s['b']
|
||
|
>>> s['b']=2
|
||
|
>>> s['b']
|
||
|
2
|
||
|
>>> s.b
|
||
|
2
|
||
|
>>> isinstance(s,dict)
|
||
|
True
|
||
|
>>> dict(s)
|
||
|
{'a': 1, 'b': 2}
|
||
|
>>> dict(FastStorage(s))
|
||
|
{'a': 1, 'b': 2}
|
||
|
>>> import pickle
|
||
|
>>> s = pickle.loads(pickle.dumps(s))
|
||
|
>>> dict(s)
|
||
|
{'a': 1, 'b': 2}
|
||
|
>>> del s.b
|
||
|
>>> del s.a
|
||
|
>>> s.a
|
||
|
>>> s.b
|
||
|
>>> s['a']
|
||
|
>>> s['b']
|
||
|
|
||
|
"""
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
dict.__init__(self, *args, **kwargs)
|
||
|
self.__dict__ = self
|
||
|
|
||
|
def __getattr__(self, key):
|
||
|
return getattr(self, key) if key in self else None
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
return dict.get(self, key, None)
|
||
|
|
||
|
def copy(self):
|
||
|
self.__dict__ = {}
|
||
|
s = FastStorage(self)
|
||
|
self.__dict__ = self
|
||
|
return s
|
||
|
|
||
|
def __repr__(self):
|
||
|
return '<Storage %s>' % dict.__repr__(self)
|
||
|
|
||
|
def __getstate__(self):
|
||
|
return dict(self)
|
||
|
|
||
|
def __setstate__(self, sdict):
|
||
|
dict.__init__(self, sdict)
|
||
|
self.__dict__ = self
|
||
|
|
||
|
def update(self, *args, **kwargs):
|
||
|
dict.__init__(self, *args, **kwargs)
|
||
|
self.__dict__ = self
|
||
|
|
||
|
|
||
|
class List(list):
|
||
|
|
||
|
"""
|
||
|
Like a regular python list but callable.
|
||
|
When a(i) is called if i is out of bounds returns None
|
||
|
instead of `IndexError`.
|
||
|
"""
|
||
|
|
||
|
def __call__(self, i, default=DEFAULT, cast=None, otherwise=None):
|
||
|
"""Allows to use a special syntax for fast-check of
|
||
|
`request.args()` validity.
|
||
|
:params:
|
||
|
i: index
|
||
|
default: use this value if arg not found
|
||
|
cast: type cast
|
||
|
otherwise:
|
||
|
will be executed when:
|
||
|
- casts fail
|
||
|
- value not found, dont have default and otherwise is
|
||
|
especified
|
||
|
can be:
|
||
|
- None: results in a 404
|
||
|
- str: redirect to this address
|
||
|
- callable: calls the function (nothing is passed)
|
||
|
Example:
|
||
|
You can use::
|
||
|
request.args(0,default=0,cast=int,otherwise='http://error_url')
|
||
|
request.args(0,default=0,cast=int,otherwise=lambda:...)
|
||
|
"""
|
||
|
n = len(self)
|
||
|
if 0 <= i < n or -n <= i < 0:
|
||
|
value = self[i]
|
||
|
elif default is DEFAULT:
|
||
|
value = None
|
||
|
else:
|
||
|
value, cast, otherwise = default, False, False
|
||
|
try:
|
||
|
if cast:
|
||
|
value = cast(value)
|
||
|
if not value and otherwise:
|
||
|
raise ValueError('Otherwise will raised.')
|
||
|
except (ValueError, TypeError):
|
||
|
from gluon.http import HTTP, redirect
|
||
|
if otherwise is None:
|
||
|
raise HTTP(404)
|
||
|
elif isinstance(otherwise, str):
|
||
|
redirect(otherwise)
|
||
|
elif callable(otherwise):
|
||
|
return otherwise()
|
||
|
else:
|
||
|
raise RuntimeError("invalid otherwise")
|
||
|
return value
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
import doctest
|
||
|
doctest.testmod()
|