From 46e0869263fcc0214674823d85a2611f4973d075 Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Tue, 2 Sep 2014 16:16:07 -0400 Subject: [PATCH 1/5] Initial version --- .travis.yml | 26 ++++ LICENSE | 45 ++++--- MANIFEST.in | 7 ++ README.md | 2 - README.rst | 64 ++++++++++ dev-requirements.txt | 6 + docs/api.rst | 3 + docs/conf.py | 30 +++++ docs/examples.rst | 31 +++++ docs/history.rst | 4 + docs/index.rst | 67 +++++++++++ setup.cfg | 12 ++ setup.py | 73 ++++++++++++ sprockets/__init__.py | 1 + sprockets/clients/__init__.py | 1 + sprockets/clients/memcached/__init__.py | 151 ++++++++++++++++++++++++ test-requirements.txt | 3 + tests.py | 54 +++++++++ 18 files changed, 554 insertions(+), 26 deletions(-) create mode 100644 .travis.yml create mode 100644 MANIFEST.in delete mode 100644 README.md create mode 100644 README.rst create mode 100644 dev-requirements.txt create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/examples.rst create mode 100644 docs/history.rst create mode 100644 docs/index.rst create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 sprockets/__init__.py create mode 100644 sprockets/clients/__init__.py create mode 100644 sprockets/clients/memcached/__init__.py create mode 100644 test-requirements.txt create mode 100644 tests.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..13a6199 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +%YAML 1.1 +--- +language: python +python: + - 2.6 + - 2.7 + - pypy + - 3.2 + - 3.3 + - 3.4 +install: + - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi + - pip install -r test-requirements.txt + - pip install -e . +script: nosetests +after_success: + - coveralls +deploy: + provider: pypi + user: sprockets + on: + python: 2.7 + tags: true + all_branches: true + password: + secure: F0sivvBoagfbkCnKBlmTEtQXccDRmpfTJSRtL2Vnk8RfsCtoPMEfx8pshuvw5Mp8m6fRxulFd8Qrg3suQjCDFdwlCGxtHricmSfW1LHxNQW/xjDBUvQpdLylv+Am/3CJXso3FZjzji9Z8z5sfBMRUID+jucjhRRbgF68yMUyMXk= diff --git a/LICENSE b/LICENSE index 99b8e75..630f26e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,28 +1,25 @@ -Copyright (c) 2014, Sprockets +Copyright (c) 2014 AWeber Communications All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the {organization} nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Sprockets nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1c854ba --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include LICENSE +include README.rst +include *requirements.txt +graft docs +graft tests.py +global-exclude __pycache__ +global-exclude *.pyc diff --git a/README.md b/README.md deleted file mode 100644 index 159a406..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -sprockets.clients.memcached -=========================== diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8b661e9 --- /dev/null +++ b/README.rst @@ -0,0 +1,64 @@ +sprockets.clients.memcached +=========================== +Memcached client wrapper that is configured via environment variables. + +|Version| |Downloads| |Status| |Coverage| |License| + +Installation +------------ +``sprockets.clients.memcached`` is available on the +`Python Package Index `_ +and can be installed via ``pip`` or ``easy_install``: + +.. code:: bash + + pip install sprockets.clients.memcached + +Documentation +------------- +https://sprocketsclientsmemcached.readthedocs.org + +Requirements +------------ +- `sprockets `_ +- `python-memcached `_ (Python 2) +- `python3-memcached `_ (Python 3) + +Example +------- +The following example sets the environment variables for connecting to +memcached on ``192.168.1.2`` and ``192.168.1.3`` and subsequently issuing a few +memcached commands: + +.. code:: python + + import os + + from sprockets.clients import memcached + + os.environ['MEMCACHED_SERVERS'] = '192.168.1.2:11211,192.168.1.3:11211' + + + client = memcached.Client() + client.set('foo', 'bar') + print(client.get('foo')) + + +Version History +--------------- +Available at https://sprocketsclientsmemcached.readthedocs.org/en/latest/history.html + +.. |Version| image:: https://badge.fury.io/py/sprockets.clients.memcached.svg? + :target: http://badge.fury.io/py/sprockets.clients.memcached + +.. |Status| image:: https://travis-ci.org/sprockets/sprockets.clients.memcached.svg?branch=master + :target: https://travis-ci.org/sprockets/sprockets.clients.memcached + +.. |Coverage| image:: https://img.shields.io/coveralls/sprockets/sprockets.clients.memcached.svg? + :target: https://coveralls.io/r/sprockets/sprockets.clients.memcached + +.. |Downloads| image:: https://pypip.in/d/sprockets.clients.memcached/badge.svg? + :target: https://pypi.python.org/pypi/sprockets.clients.memcached + +.. |License| image:: https://pypip.in/license/sprockets.clients.memcached/badge.svg? + :target: https://sprocketsclientsmemcached.readthedocs.org \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..72b4cc1 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,6 @@ +-e git://github.com/gmr/python-memcached.git@docstring-update#egg=python-memcached +-r test-requirements.txt +flake8>=2.1,<3 +sphinx>=1.2,<2 +sphinx-rtd-theme>=0.1,<1.0 +sphinxcontrib-httpdomain>=1.2,<2 diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..62d1144 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,3 @@ +.. automodule:: sprockets.clients.memcached + :members: + :inherited-members: \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..d82e084 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +import sphinx_rtd_theme + +from sprockets.clients.memcached import version_info, __version__ + +needs_sphinx = '1.0' +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.inheritance_diagram', + 'sphinxcontrib.httpdomain', +] +templates_path = [] +source_suffix = '.rst' +master_doc = 'index' +project = 'sprockets.clients.memcached' +copyright = '2014, AWeber Communications' +version = '.'.join(__version__.split('.')[0:1]) +release = __version__ +if len(version_info) > 3: + release += '-{0}'.format(str(v) for v in version_info[3:]) +exclude_patterns = [] +pygments_style = 'sphinx' +html_theme = 'sphinx_rtd_theme' +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +intersphinx_mapping = { + 'python': ('https://docs.python.org/2/', None), + 'requests': ('https://requests.readthedocs.org/en/latest/', None), + 'sprockets': ('https://sprockets.readthedocs.org/en/latest/', None), +} \ No newline at end of file diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..5237f03 --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,31 @@ +Examples +======== +The following example sets the environment variables for connecting to +memcached on ``192.168.1.2`` and ``192.168.1.3`` and subsequently issuing a few +memcached commands: + +.. code:: python + + import os + + from sprockets.clients import memcached + + os.environ['MEMCACHED_SERVERS'] = '192.168.1.2:11211,192.168.1.3:11211' + + client = memcached.Client() + client.set('foo', 'bar') + print(client.get('foo')) + +The next example uses a prefixed environment variable for configuration data: + +.. code:: python + + import os + + from sprockets.clients import memcached + + os.environ['FOO_MEMCACHED_SERVERS'] = '192.168.1.2:11211' + + client = memcached.Client('foo') + client.set('foo', 'bar') + print(client.get('foo')) diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 0000000..75e0d5a --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,4 @@ +Version History +--------------- +- 1.0.0 [2014-09-03] + - Initial release diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d59bfc0 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,67 @@ +sprockets.clients.memcached +=========================== +Memcached client wrapper that is configured via environment variables + +|Version| |Downloads| |Status| |Coverage| |License| + +Installation +------------ +``sprockets.clients.memcached`` is available on the +`Python Package Index `_ +and can be installed via ``pip`` or ``easy_install``: + +.. code:: bash + + pip install sprockets.clients.memcached + +Requirements +------------ +- `sprockets `_ +- `python-memcached `_ (Python 2) +- `python3-memcached `_ (Python 3) + +API Documentation +----------------- +.. toctree:: + :maxdepth: 2 + + api + examples + +Version History +--------------- +See :doc:`history` + +Issues +------ +Please report any issues to the Github project at `https://github.com/sprockets/sprockets.clients.memcached/issues `_ + +Source +------ +``sprockets.clients.memcached`` source is available on Github at `https://github.com/sprockets/sprockets.clients.memcached `_ + +License +------- +``sprockets.clients.memcached`` is released under the `3-Clause BSD license `_. + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +.. |Version| image:: https://badge.fury.io/py/sprockets.clients.memcached.svg? + :target: http://badge.fury.io/py/sprockets.clients.memcached + +.. |Status| image:: https://travis-ci.org/sprockets/sprockets.clients.memcached.svg?branch=master + :target: https://travis-ci.org/sprockets/sprockets.clients.memcached + +.. |Coverage| image:: https://img.shields.io/coveralls/sprockets/sprockets.clients.memcached.svg? + :target: https://coveralls.io/r/sprockets/sprockets.clients.memcached + +.. |Downloads| image:: https://pypip.in/d/sprockets.clients.memcached/badge.svg? + :target: https://pypi.python.org/pypi/sprockets.clients.memcached + +.. |License| image:: https://pypip.in/license/sprockets.clients.memcached/badge.svg? + :target: https://sprocketsclientsmemcached.readthedocs.org \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..27795d0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[build_sphinx] +all-files = 1 +source-dir = docs +build-dir = build/docs + +[nosetests] +with-coverage = 1 +cover-package = sprockets.clients.memcached +verbose = 1 + +[flake8] +exclude = build,dist,docs,env diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0b7a6f8 --- /dev/null +++ b/setup.py @@ -0,0 +1,73 @@ +import codecs +import sys + +import setuptools + + +def read_requirements_file(req_name): + requirements = [] + try: + with codecs.open(req_name, encoding='utf-8') as req_file: + for req_line in req_file: + if '#' in req_line: + req_line = req_line[0:req_line.find('#')].strip() + if req_line: + requirements.append(req_line.strip()) + except IOError: + pass + return requirements + + +install_requires = ['sprockets'] +setup_requires = read_requirements_file('setup-requirements.txt') +tests_require = read_requirements_file('test-requirements.txt') + +if sys.version_info < (2, 7): + tests_require.append('unittest2') +if sys.version_info < (3, 0): + install_requires.append('python-memcached') + tests_require.append('mock') +if sys.version_info >= (3, 0): + install_requires.append('python3-memcached') + + +setuptools.setup( + name='sprockets.clients.memcached', + version='1.0.0', + description=('Memcached client wrapper that is configured via ' + 'environment variables'), + long_description=codecs.open('README.rst', encoding='utf-8').read(), + url='https://github.com/sprockets/sprockets.clients.memcached.git', + author='AWeber Communications', + author_email='api@aweber.com', + license=codecs.open('LICENSE', encoding='utf-8').read(), + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], + packages=['sprockets', + 'sprockets.clients', + 'sprockets.clients.memcached'], + package_data={'': ['LICENSE', 'README.rst']}, + include_package_data=True, + namespace_packages=['sprockets', + 'sprockets.clients'], + install_requires=install_requires, + setup_requires=setup_requires, + tests_require=tests_require, + test_suite='nose.collector', + zip_safe=False) diff --git a/sprockets/__init__.py b/sprockets/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/sprockets/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/sprockets/clients/__init__.py b/sprockets/clients/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/sprockets/clients/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/sprockets/clients/memcached/__init__.py b/sprockets/clients/memcached/__init__.py new file mode 100644 index 0000000..bb4f4df --- /dev/null +++ b/sprockets/clients/memcached/__init__.py @@ -0,0 +1,151 @@ +""" +Memcache Client API +=================== +The memcache client API wraps the :py:class:`memcache.Client` adding +environment variable based configuration. + +Example environment variable configuration: + + ``_MEMCACHED_SERVERS = '10.0.0.1:11211:64,10.0.0.2:11211:64'`` + +""" +import logging +import os +import pickle + +import memcache + +version_info = (0, 0, 0) +__version__ = '.'.join(str(v) for v in version_info) + +LOGGER = logging.getLogger(__name__) + +DEFAULT_SERVER = '127.0.0.1:11211' + +from memcache import _DEAD_RETRY +from memcache import _SOCKET_TIMEOUT +from memcache import SERVER_MAX_KEY_LENGTH +from memcache import SERVER_MAX_VALUE_LENGTH + + +def _get_servers(prefix): + """Return the list of memcached servers from the environment variable + value, defaulting to ``DEFAULT_SERVER`` if it is not set. + + If prefix is not set, then the environment variable ``MEMCACHED_SERVERS`` + will be be used. If both ``_MEMCACHED_SERVERS`` and + ``MEMCACHED_SERVERS`` are not set, the default server value of + ``127.0.0.1`` will be returned. + + :param str prefix: The environment variable prefix + :rtype: list + + """ + key = '%s_MEMCACHED_SERVERS' % prefix if prefix else 'MEMCACHED_SERVERS' + return os.environ.get(key, DEFAULT_SERVER).split(',') + + +class Client(memcache.Client): + """Wraps :py:class:`memcache.Client`, passing in the environment + variable prefix. If prefix is set, the environment variable key is in the + format ``_MEMCACHED_SERVERS``. If the prefix is not set, the list + will attempt to be retrieved from the ``MEMCACHED_SERVERS``. If neither + environment variable is set, the default value of ``127.0.0.1:11211`` is + used. + + The per server format in the comma separated list is: + + ``[HOST]:[PORT]<:WEIGHT>`` + + Where host and port are required but weight is optional. + + :param str prefix: The environment variable prefix. Default: ``None`` + + :param int debug: Display error messages when a server can't be contacted. + Default: ``0`` + :param int pickle_protocol: number to mandate protocol used by (c)Pickle. + + :param pickler: optional override of default Pickler for subclassing + :type pickler: :py:class:`pickle.Pickler` + + :param unpickler: optional override of default Unpickler for subclassing + :type unpickler: :py:class:`pickle.Unpickler` + + :param pload: optional persistent_load function to call on pickle loading + :type pload: :py:meth:`pickle.loads` + + :param str pid: optional persistent_id function to call on pickle storing + + :param int dead_retry: Number of seconds before retrying a blacklisted + server. Default: ``30`` + + :param int socket_timeout: Timeout in seconds for all calls to a server. + Default: ``3`` + + :param server_max_key_length: Data that is larger than this will not be + sent to the server. + Default: ``SERVER_MAX_KEY_LENGTH`` + :type server_max_key_length: int + + :param server_max_value_length: Data that is larger than this will not be + sent to the server. + Default: ``SERVER_MAX_VALUE_LENGTH`` + :type server_max_value_length: int + + :param bool cache_cas: If true, cas operations will be cached. WARNING: + This cache is not expired internally, if you have + a long-running process you will need to expire it + manually via :py:meth:`Client.reset_cas`, or the + cache can grow unlimited. + Default: ``False`` + + :param int flush_on_reconnect: Optional flag which prevents a scenario + that can cause stale data to be read. If + there is more than one memcached server + and the connection to one is interrupted, + keys that mapped to that server will get + reassigned to another. If the first server + comes back, those keys will map to it + again. If it still has its data, ``get()`` + can read stale data that was overwritten + on another server. This flag is off by + default for backwards compatibility. + Default: ``0`` + + :param bool check_keys: If ``True``, the key is checked to ensure it is + the correct length and composed of the right + characters. + Default: ``True`` + + """ + def __init__(self, + prefix=None, + debug=0, + pickle_protocol=0, + pickler=pickle.Pickler, + unpickler=pickle.Unpickler, + pload=None, + pid=None, + server_max_key_length=SERVER_MAX_KEY_LENGTH, + server_max_value_length=SERVER_MAX_VALUE_LENGTH, + dead_retry=_DEAD_RETRY, + socket_timeout=_SOCKET_TIMEOUT, + cache_cas=False, + flush_on_reconnect=0, + check_keys=True): + servers = _get_servers(prefix.upper() if prefix else '') + LOGGER.debug('Connecting to %r', servers) + super(Client, self).__init__(servers, + debug, + pickle_protocol, + pickler, + unpickler, + pload, + pid, + server_max_key_length, + server_max_value_length, + dead_retry, + socket_timeout, + cache_cas, + flush_on_reconnect, + check_keys) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..20b57db --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +coverage>=3.7,<4 +coveralls>=0.4,<1 +nose>=1.3,<2 diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..55ae9af --- /dev/null +++ b/tests.py @@ -0,0 +1,54 @@ +""" +Tests for the sprockets.clients.memcached package + +""" +import mock +import os +import pickle +try: + import unittest2 as unittest +except ImportError: + import unittest + +from sprockets.clients import memcached + + +class TestGetServers(unittest.TestCase): + + def tearDown(self): + for key in ['MEMCACHED_SERVERS', 'TEST1_MEMCACHED_SERVERS']: + if key in os.environ: + del os.environ[key] + + def test_get_servers_for_prefixed_key(self): + os.environ['TEST1_MEMCACHED_SERVERS'] = '1.1.1.1:11211,1.1.1.2:11211' + self.assertListEqual(memcached._get_servers('TEST1'), + ['1.1.1.1:11211', '1.1.1.2:11211']) + + def test_get_servers_for_non_prefixed_key(self): + os.environ['MEMCACHED_SERVERS'] = '2.1.1.1:11211' + self.assertListEqual(memcached._get_servers(None), ['2.1.1.1:11211']) + + def test_get_servers_returns_default_value(self): + self.assertListEqual(memcached._get_servers(None), ['127.0.0.1:11211']) + + +class TestClientWrapsMemcacheClient(unittest.TestCase): + + @mock.patch('memcache.Client.__init__') + def test_client_super_init(self, mock_init): + memcached.Client() + mock_init.assert_called_once_with(['127.0.0.1:11211'], + 0, + 0, + pickle.Pickler, + pickle.Unpickler, + None, + None, + memcached.SERVER_MAX_KEY_LENGTH, + memcached.SERVER_MAX_VALUE_LENGTH, + memcached._DEAD_RETRY, + memcached._SOCKET_TIMEOUT, + False, + 0, + True) From aa4cb3e06bb653d22d22c5eae7cf2c04e4bece99 Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Wed, 3 Sep 2014 12:04:12 -0400 Subject: [PATCH 2/5] Add simple integration tests --- .travis.yml | 2 ++ tests.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/.travis.yml b/.travis.yml index 13a6199..38a4406 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,8 @@ install: script: nosetests after_success: - coveralls +services: + - memcached deploy: provider: pypi user: sprockets diff --git a/tests.py b/tests.py index 55ae9af..dd6bc39 100644 --- a/tests.py +++ b/tests.py @@ -52,3 +52,19 @@ class TestClientWrapsMemcacheClient(unittest.TestCase): False, 0, True) + + +class ClientIntegrationTests(unittest.TestCase): + + def setUp(self): + self.client = memcached.Client() + self.client.incr('test') + if any([s.deaduntil for s in self.client.servers]): + raise unittest.SkipTest('No memcached daemon present') + + def test_that_incr_returns_one(self): + self.assertEqual(self.client.incr('test-incr'), 1) + + def test_that_set_key_is_gettable(self): + self.client.set('foo', 'bar', 60) + self.assertEqual(self.client.get('foo'), 'bar') From 202b5c4b1a9245cf10c237788cda076fd16f59c0 Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Wed, 3 Sep 2014 13:02:39 -0400 Subject: [PATCH 3/5] Fix the incr test --- tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index dd6bc39..6eabe5f 100644 --- a/tests.py +++ b/tests.py @@ -63,7 +63,8 @@ class ClientIntegrationTests(unittest.TestCase): raise unittest.SkipTest('No memcached daemon present') def test_that_incr_returns_one(self): - self.assertEqual(self.client.incr('test-incr'), 1) + self.client.set('test-incr', 2) + self.assertEqual(self.client.incr('test-incr'), 3) def test_that_set_key_is_gettable(self): self.client.set('foo', 'bar', 60) From ac52d396425dfcb5de50dde7b19fd9ca64d540ca Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Fri, 5 Sep 2014 15:40:02 -0400 Subject: [PATCH 4/5] Simplify --- .travis.yml | 4 +- MANIFEST.in | 5 -- dev-requirements.txt | 3 +- docs/conf.py | 4 +- requirements2.txt | 1 + requirements3.txt | 1 + setup.cfg | 3 - setup.py | 23 ------ sprockets/clients/memcached/__init__.py | 99 ++----------------------- test-requirements.txt | 1 + tests.py | 18 +---- 11 files changed, 15 insertions(+), 147 deletions(-) create mode 100644 requirements2.txt create mode 100644 requirements3.txt diff --git a/.travis.yml b/.travis.yml index 38a4406..407dbd7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,10 @@ python: - 3.4 install: - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi + - if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then pip install -r requirements2.txt; fi + - if [[ $TRAVIS_PYTHON_VERSION == pypy ]]; then pip install -r requirements2.txt; fi + - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then pip install -r requirements3.txt; fi - pip install -r test-requirements.txt - - pip install -e . script: nosetests after_success: - coveralls diff --git a/MANIFEST.in b/MANIFEST.in index 1c854ba..9d5d250 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,2 @@ include LICENSE include README.rst -include *requirements.txt -graft docs -graft tests.py -global-exclude __pycache__ -global-exclude *.pyc diff --git a/dev-requirements.txt b/dev-requirements.txt index 72b4cc1..abd370c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,5 @@ +# Include this version only for documentation building purposes, normally ignored -e git://github.com/gmr/python-memcached.git@docstring-update#egg=python-memcached --r test-requirements.txt -flake8>=2.1,<3 sphinx>=1.2,<2 sphinx-rtd-theme>=0.1,<1.0 sphinxcontrib-httpdomain>=1.2,<2 diff --git a/docs/conf.py b/docs/conf.py index d82e084..a5ab4ea 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,7 +24,5 @@ pygments_style = 'sphinx' html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] intersphinx_mapping = { - 'python': ('https://docs.python.org/2/', None), - 'requests': ('https://requests.readthedocs.org/en/latest/', None), - 'sprockets': ('https://sprockets.readthedocs.org/en/latest/', None), + 'python': ('https://docs.python.org/2/', None) } \ No newline at end of file diff --git a/requirements2.txt b/requirements2.txt new file mode 100644 index 0000000..851bfd8 --- /dev/null +++ b/requirements2.txt @@ -0,0 +1 @@ +python-memcached diff --git a/requirements3.txt b/requirements3.txt new file mode 100644 index 0000000..d76721d --- /dev/null +++ b/requirements3.txt @@ -0,0 +1 @@ +python3-memcached diff --git a/setup.cfg b/setup.cfg index 27795d0..c7a6d17 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,3 @@ build-dir = build/docs with-coverage = 1 cover-package = sprockets.clients.memcached verbose = 1 - -[flake8] -exclude = build,dist,docs,env diff --git a/setup.py b/setup.py index 0b7a6f8..261d2fc 100644 --- a/setup.py +++ b/setup.py @@ -3,30 +3,10 @@ import sys import setuptools - -def read_requirements_file(req_name): - requirements = [] - try: - with codecs.open(req_name, encoding='utf-8') as req_file: - for req_line in req_file: - if '#' in req_line: - req_line = req_line[0:req_line.find('#')].strip() - if req_line: - requirements.append(req_line.strip()) - except IOError: - pass - return requirements - - install_requires = ['sprockets'] -setup_requires = read_requirements_file('setup-requirements.txt') -tests_require = read_requirements_file('test-requirements.txt') -if sys.version_info < (2, 7): - tests_require.append('unittest2') if sys.version_info < (3, 0): install_requires.append('python-memcached') - tests_require.append('mock') if sys.version_info >= (3, 0): install_requires.append('python3-memcached') @@ -67,7 +47,4 @@ setuptools.setup( namespace_packages=['sprockets', 'sprockets.clients'], install_requires=install_requires, - setup_requires=setup_requires, - tests_require=tests_require, - test_suite='nose.collector', zip_safe=False) diff --git a/sprockets/clients/memcached/__init__.py b/sprockets/clients/memcached/__init__.py index bb4f4df..1a982f3 100644 --- a/sprockets/clients/memcached/__init__.py +++ b/sprockets/clients/memcached/__init__.py @@ -1,7 +1,7 @@ """ -Memcache Client API -=================== -The memcache client API wraps the :py:class:`memcache.Client` adding +Memcached Client API +==================== +The memcached client API wraps the :py:class:`memcache.Client` adding environment variable based configuration. Example environment variable configuration: @@ -11,7 +11,6 @@ Example environment variable configuration: """ import logging import os -import pickle import memcache @@ -22,11 +21,6 @@ LOGGER = logging.getLogger(__name__) DEFAULT_SERVER = '127.0.0.1:11211' -from memcache import _DEAD_RETRY -from memcache import _SOCKET_TIMEOUT -from memcache import SERVER_MAX_KEY_LENGTH -from memcache import SERVER_MAX_VALUE_LENGTH - def _get_servers(prefix): """Return the list of memcached servers from the environment variable @@ -61,91 +55,8 @@ class Client(memcache.Client): :param str prefix: The environment variable prefix. Default: ``None`` - :param int debug: Display error messages when a server can't be contacted. - Default: ``0`` - :param int pickle_protocol: number to mandate protocol used by (c)Pickle. - - :param pickler: optional override of default Pickler for subclassing - :type pickler: :py:class:`pickle.Pickler` - - :param unpickler: optional override of default Unpickler for subclassing - :type unpickler: :py:class:`pickle.Unpickler` - - :param pload: optional persistent_load function to call on pickle loading - :type pload: :py:meth:`pickle.loads` - - :param str pid: optional persistent_id function to call on pickle storing - - :param int dead_retry: Number of seconds before retrying a blacklisted - server. Default: ``30`` - - :param int socket_timeout: Timeout in seconds for all calls to a server. - Default: ``3`` - - :param server_max_key_length: Data that is larger than this will not be - sent to the server. - Default: ``SERVER_MAX_KEY_LENGTH`` - :type server_max_key_length: int - - :param server_max_value_length: Data that is larger than this will not be - sent to the server. - Default: ``SERVER_MAX_VALUE_LENGTH`` - :type server_max_value_length: int - - :param bool cache_cas: If true, cas operations will be cached. WARNING: - This cache is not expired internally, if you have - a long-running process you will need to expire it - manually via :py:meth:`Client.reset_cas`, or the - cache can grow unlimited. - Default: ``False`` - - :param int flush_on_reconnect: Optional flag which prevents a scenario - that can cause stale data to be read. If - there is more than one memcached server - and the connection to one is interrupted, - keys that mapped to that server will get - reassigned to another. If the first server - comes back, those keys will map to it - again. If it still has its data, ``get()`` - can read stale data that was overwritten - on another server. This flag is off by - default for backwards compatibility. - Default: ``0`` - - :param bool check_keys: If ``True``, the key is checked to ensure it is - the correct length and composed of the right - characters. - Default: ``True`` - """ - def __init__(self, - prefix=None, - debug=0, - pickle_protocol=0, - pickler=pickle.Pickler, - unpickler=pickle.Unpickler, - pload=None, - pid=None, - server_max_key_length=SERVER_MAX_KEY_LENGTH, - server_max_value_length=SERVER_MAX_VALUE_LENGTH, - dead_retry=_DEAD_RETRY, - socket_timeout=_SOCKET_TIMEOUT, - cache_cas=False, - flush_on_reconnect=0, - check_keys=True): + def __init__(self, prefix=None): servers = _get_servers(prefix.upper() if prefix else '') LOGGER.debug('Connecting to %r', servers) - super(Client, self).__init__(servers, - debug, - pickle_protocol, - pickler, - unpickler, - pload, - pid, - server_max_key_length, - server_max_value_length, - dead_retry, - socket_timeout, - cache_cas, - flush_on_reconnect, - check_keys) + super(Client, self).__init__(servers) diff --git a/test-requirements.txt b/test-requirements.txt index 20b57db..6f10b7e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ coverage>=3.7,<4 coveralls>=0.4,<1 nose>=1.3,<2 +mock==1.0.1 diff --git a/tests.py b/tests.py index 6eabe5f..0fd0287 100644 --- a/tests.py +++ b/tests.py @@ -4,7 +4,6 @@ Tests for the sprockets.clients.memcached package """ import mock import os -import pickle try: import unittest2 as unittest except ImportError: @@ -38,20 +37,7 @@ class TestClientWrapsMemcacheClient(unittest.TestCase): @mock.patch('memcache.Client.__init__') def test_client_super_init(self, mock_init): memcached.Client() - mock_init.assert_called_once_with(['127.0.0.1:11211'], - 0, - 0, - pickle.Pickler, - pickle.Unpickler, - None, - None, - memcached.SERVER_MAX_KEY_LENGTH, - memcached.SERVER_MAX_VALUE_LENGTH, - memcached._DEAD_RETRY, - memcached._SOCKET_TIMEOUT, - False, - 0, - True) + mock_init.assert_called_once_with(['127.0.0.1:11211']) class ClientIntegrationTests(unittest.TestCase): @@ -62,7 +48,7 @@ class ClientIntegrationTests(unittest.TestCase): if any([s.deaduntil for s in self.client.servers]): raise unittest.SkipTest('No memcached daemon present') - def test_that_incr_returns_one(self): + def test_that_incr_returns_a_value_for_a_set_key(self): self.client.set('test-incr', 2) self.assertEqual(self.client.incr('test-incr'), 3) From 8adbda586bf65acf156338ccff9aa389bea9427d Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Fri, 5 Sep 2014 16:25:36 -0400 Subject: [PATCH 5/5] Remove the deprecated dependency --- README.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/README.rst b/README.rst index 8b661e9..973b00d 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,6 @@ https://sprocketsclientsmemcached.readthedocs.org Requirements ------------ -- `sprockets `_ - `python-memcached `_ (Python 2) - `python3-memcached `_ (Python 3)