Resolves Issue #219

This commit is contained in:
BJ Dierkes 2014-01-20 17:10:28 -06:00
parent f4d9d2accd
commit 949908da0a
7 changed files with 318 additions and 8 deletions

View File

@ -27,6 +27,7 @@ Features:
* :issue:`209` - Added app.debug property to allow developers to know if
`--debug` was passed at command line of via the config.
* :issue:`219` - Merged ext.memcached into mainline.
2.1.4 - Tue Oct 29, 2013 (DEVELOPMENT)
@ -43,12 +44,12 @@ Bugs:
Features:
* :issue:`190` - Merge daemon extension into core
* :issue:`190` - Merged daemon extension into core
* :issue:`194` - Added pre_argument_parsing/post_argument_parsing hooks
* :issue:`196` - Added utils.misc.wrap
* :issue:`200` - Merge ext.mustache into mainline.
* :issue:`200` - Merged ext.mustache into mainline.
* :issue:`203` - Added support for external template directory
* :issue:`207` - Support alternative 'display' name for stacked
* :issue:`207` - Added support for alternative 'display' name for stacked
controllers
Incompatible:

218
cement/ext/ext_memcached.py Normal file
View File

@ -0,0 +1,218 @@
"""Memcached Framework Extension."""
import sys
import pylibmc
from ..core import cache, handler
from ..utils.misc import minimal_logger
LOG = minimal_logger(__name__)
class MemcachedCacheHandler(cache.CementCacheHandler):
"""
This class implements the :ref:`ICache <cement.core.cache>`
interface. It provides a caching interface using the
`pylibmc <http://sendapatch.se/projects/pylibmc/>`_ library.
**Note** This extension has an external dependency on `pylibmc`. You
must include `pylibmc` in your applications dependencies as Cement
explicitly does *not* include external dependencies for optional
extensions.
**Note** This extension is not supported on Python 3, due to the fact
that `pylibmc` does not appear to support Python 3 as of yet.
Configuration:
The Memcached extension is configurable with the following config settings
under a `[cache.memcached]` section of the application configuration.
* **expire_time** - The default time in second to expire items in the
cache. Default: 0 (does not expire).
* **hosts** - List of Memcached servers.
Configurations can be passed as defaults to a CementApp:
.. code-block:: python
from cement.core import foundation, backend
from cement.utils.misc import init_defaults
defaults = init_defaults('myapp', 'cache.memcached')
defaults['cache.memcached']['expire_time'] = 0
defaults['cache.memcached']['hosts'] = ['127.0.0.1']
app = foundation.CementApp('myapp',
config_defaults=defaults,
cache_handler='memcached',
)
Additionally, an application configuration file might have a section like
the following:
.. code-block:: text
[cache.memcached]
# time in seconds that an item in the cache will expire
expire_time = 3600
# comma seperated list of memcached servers
hosts = 127.0.0.1, cache.example.com
Usage:
.. code-block:: python
from cement.core import foundation
from cement.utils.misc import init_defaults
defaults = init_defaults('myapp', 'memcached')
defaults['cache.memcached']['expire_time'] = 300 # seconds
defaults['cache.memcached']['hosts'] = ['127.0.0.1']
class MyApp(foundation.CementApp):
class Meta:
label = 'myapp'
config_defaults = defaults
extensions = ['memcached']
cache_handler = 'memcached'
app = MyApp()
try:
app.setup()
app.run()
# Set a cached value
app.cache.set('my_key', 'my value')
# Get a cached value
app.cache.get('my_key')
# Delete a cached value
app.cache.delete('my_key')
# Delete the entire cache
app.cache.purge()
finally:
app.close()
"""
class Meta:
interface = cache.ICache
label = 'memcached'
config_defaults = dict(
hosts=['127.0.0.1'],
expire_time=0,
)
def __init__(self, *args, **kw):
super(MemcachedCacheHandler, self).__init__(*args, **kw)
self.mc = None
def _setup(self, *args, **kw):
super(MemcachedCacheHandler, self)._setup(*args, **kw)
self._fix_hosts()
self.mc = pylibmc.Client(self._config('hosts'))
def _fix_hosts(self):
"""
Useful to fix up the hosts configuration (i.e. convert a
comma-separated string into a list). This function does not return
anything, however it is expected to set the `hosts` value of the
`[cache.memcached]` section (which is what this extension reads for
it's host configution).
:returns: None
"""
hosts = self._config('hosts')
fixed_hosts = []
if type(hosts) == str:
parts = hosts.split(',')
for part in parts:
fixed_hosts.append(part.strip())
elif type(hosts) == list:
fixed_hosts = hosts
self.app.config.set(self._meta.config_section, 'hosts', fixed_hosts)
def get(self, key, fallback=None, **kw):
"""
Get a value from the cache. Any additional keyword arguments will be
passed directly to `pylibmc` get function.
:param key: The key of the item in the cache to get.
:param fallback: The value to return if the item is not found in the
cache.
:returns: The value of the item in the cache, or the `fallback` value.
"""
LOG.debug("getting cache value using key '%s'" % key)
res = self.mc.get(key, **kw)
if res is None:
return fallback
else:
return res
def _config(self, key):
"""
This is a simple wrapper, and is equivalent to:
`self.app.config.get('cache.memcached', <key>)`.
:param key: The key to get a config value from the 'cache.memcached'
config section.
:returns: The value of the given key.
"""
return self.app.config.get(self._meta.config_section, key)
def set(self, key, value, time=None, **kw):
"""
Set a value in the cache for the given `key`. Any additional
keyword arguments will be passed directly to the `pylibmc` set
function.
:param key: The key of the item in the cache to set.
:param value: The value of the item to set.
:param time: The expiration time (in seconds) to keep the item cached.
Defaults to `expire_time` as defined in the applications
configuration.
:returns: None
"""
if time is None:
time = int(self._config('expire_time'))
self.mc.set(key, value, time=time, **kw)
def delete(self, key, **kw):
"""
Delete an item from the cache for the given `key`. Any additional
keyword arguments will be passed directly to the `pylibmc` delete
function.
:param key: The key to delete from the cache.
:returns: None
"""
self.mc.delete(key, **kw)
def purge(self, **kw):
"""
Purge the entire cache, all keys and values will be lost. Any
additional keyword arguments will be passed directly to the
`pylibmc` flush_all() function.
:returns: None
"""
self.mc.flush_all(**kw)
def load():
handler.register(MemcachedCacheHandler)

View File

@ -0,0 +1,10 @@
.. _cement.ext.ext_memcached:
:mod:`cement.ext.ext_memcached`
-------------------------------
.. automodule:: cement.ext.ext_memcached
:members:
:undoc-members:
:private-members:
:show-inheritance:

View File

@ -8,7 +8,7 @@ Cement Core Modules
.. toctree::
:maxdepth: 1
core/arg
core/backend
core/cache
@ -32,12 +32,12 @@ Cement Utility Modules
.. toctree::
:maxdepth: 1
utils/fs
utils/shell
utils/misc
utils/test
.. _api-ext:
Cement Extension Modules
@ -45,12 +45,13 @@ Cement Extension Modules
.. toctree::
:maxdepth: 1
ext/ext_argparse
ext/ext_configparser
ext/ext_daemon
ext/ext_json
ext/ext_logging
ext/ext_memcached
ext/ext_mustache
ext/ext_nulloutput
ext/ext_plugin

View File

@ -6,4 +6,5 @@ pep8
autopep8
# Required for optional extensions
pystache
pystache
pylibmc

View File

@ -8,6 +8,7 @@ cover-inclusive=1
cover-erase=1
cover-html=1
cover-html-dir=coverage_report/
where=tests/
[build_sphinx]
source-dir = doc/source

View File

@ -0,0 +1,78 @@
"""Tests for cement.ext.ext_memcached."""
import sys
from time import sleep
from random import random
from cement.core import handler
from cement.utils import test
from cement.utils.misc import init_defaults
if sys.version_info[0] < 3:
import pylibmc
else:
raise test.SkipTest('pylibmc does not support Python 3') # pragma: no cover
class MemcachedExtTestCase(test.CementTestCase):
def setUp(self):
self.key = "cement-tests-random-key-%s" % random()
defaults = init_defaults('tests', 'cache.memcached')
defaults['cache.memcached']['hosts'] = '127.0.0.1, localhost'
self.app = self.make_app('tests',
config_defaults=defaults,
extensions=['memcached'],
cache_handler='memcached',
)
self.app.setup()
def tearDown(self):
self.app.cache.delete(self.key)
def test_memcache_list_type_config(self):
defaults = init_defaults('tests', 'cache.memcached')
defaults['cache.memcached']['hosts'] = ['127.0.0.1', 'localhost']
self.app = self.make_app('tests',
config_defaults=defaults,
extensions=['memcached'],
cache_handler='memcached',
)
self.app.setup()
self.eq(self.app.config.get('cache.memcached', 'hosts'),
['127.0.0.1', 'localhost'])
def test_memcache_str_type_config(self):
defaults = init_defaults('tests', 'cache.memcached')
defaults['cache.memcached']['hosts'] = '127.0.0.1, localhost'
self.app = self.make_app('tests',
config_defaults=defaults,
extensions=['memcached'],
cache_handler='memcached',
)
self.app.setup()
self.eq(self.app.config.get('cache.memcached', 'hosts'),
['127.0.0.1', 'localhost'])
def test_memcached_set(self):
self.app.cache.set(self.key, 1001)
self.eq(self.app.cache.get(self.key), 1001)
def test_memcached_get(self):
# get empty value
self.app.cache.delete(self.key)
self.eq(self.app.cache.get(self.key), None)
# get empty value with fallback
self.eq(self.app.cache.get(self.key, 1234), 1234)
def test_memcached_delete(self):
self.app.cache.delete(self.key)
def test_memcached_purge(self):
self.app.cache.set(self.key, 1002)
self.app.cache.purge()
self.eq(self.app.cache.get(self.key), None)
def test_memcache_expire(self):
self.app.cache.set(self.key, 1003, time=2)
sleep(3)
self.eq(self.app.cache.get(self.key), None)