Hello community, here is the log from the commit of package python-pymemcache for openSUSE:Factory checked in at 2019-09-11 10:36:44 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pymemcache (Old) and /work/SRC/openSUSE:Factory/.python-pymemcache.new.7948 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pymemcache" Wed Sep 11 10:36:44 2019 rev:4 rq:729846 version:2.2.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pymemcache/python-pymemcache.changes 2019-03-19 10:03:17.559785759 +0100 +++ /work/SRC/openSUSE:Factory/.python-pymemcache.new.7948/python-pymemcache.changes 2019-09-11 10:36:46.543272143 +0200 @@ -1,0 +2,14 @@ +Tue Sep 10 11:58:40 UTC 2019 - Tomáš Chvátal <tchva...@suse.com> + +- Update to 2.2.2: + * Fix long_description string in Python packaging. + * Fix flags when setting multiple differently-typed values at once. + * Use setup.cfg metadata instead setup.py config to generate package. + * Add default_noreply parameter to HashClient. + * Add encoding parameter to Client constructors (defaults to ascii). + * Add flags parameter to write operation methods. + * Handle unicode key values in MockMemcacheClient correctly. + * Improve ASCII encoding failure exception. + * Fix setup.py dependency on six already being installed. + +------------------------------------------------------------------- Old: ---- pymemcache-2.1.1.tar.gz New: ---- pymemcache-2.2.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pymemcache.spec ++++++ --- /var/tmp/diff_new_pack.zjHKWo/_old 2019-09-11 10:36:47.771271786 +0200 +++ /var/tmp/diff_new_pack.zjHKWo/_new 2019-09-11 10:36:47.807271777 +0200 @@ -19,25 +19,25 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-pymemcache -Version: 2.1.1 +Version: 2.2.2 Release: 0 Summary: A pure Python memcached client License: Apache-2.0 Group: Development/Languages/Python -Url: https://github.com/Pinterest/pymemcache +URL: https://github.com/Pinterest/pymemcache Source: https://files.pythonhosted.org/packages/source/p/pymemcache/pymemcache-%{version}.tar.gz BuildRequires: %{python_module mock} BuildRequires: %{python_module pytest} BuildRequires: %{python_module setuptools} BuildRequires: %{python_module six} +BuildRequires: fdupes +BuildRequires: python-rpm-macros +Requires: python-six +BuildArch: noarch %ifpython2 BuildRequires: python2-future Requires: python2-future %endif -BuildRequires: python-rpm-macros -Requires: python-six -BuildArch: noarch - %python_subpackages %description @@ -59,12 +59,12 @@ %install %python_install +%python_expand %fdupes %{buildroot}%{$python_sitelib} %check %python_exec setup.py test %files %{python_files} -%defattr(-,root,root,-) %license LICENSE.txt %doc README.rst %{python_sitelib}/* ++++++ pymemcache-2.1.1.tar.gz -> pymemcache-2.2.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/ChangeLog.rst new/pymemcache-2.2.2/ChangeLog.rst --- old/pymemcache-2.1.1/ChangeLog.rst 2019-01-28 18:34:10.000000000 +0100 +++ new/pymemcache-2.2.2/ChangeLog.rst 2019-08-06 21:06:33.000000000 +0200 @@ -1,5 +1,23 @@ -Change Log -========== +Changelog +========= + +New in version 2.2.2 +-------------------- +* Fix ``long_description`` string in Python packaging. + +New in version 2.2.1 +-------------------- +* Fix ``flags`` when setting multiple differently-typed values at once. + +New in version 2.2.0 +-------------------- +* Drop official support for Python 3.4. +* Use ``setup.cfg`` metadata instead ``setup.py`` config to generate package. +* Add ``default_noreply`` parameter to ``HashClient``. +* Add ``encoding`` parameter to ``Client`` constructors (defaults to ``ascii``). +* Add ``flags`` parameter to write operation methods. +* Handle unicode key values in ``MockMemcacheClient`` correctly. +* Improve ASCII encoding failure exception. New in version 2.1.1 -------------------- @@ -58,8 +76,8 @@ New in version 1.4.0 -------------------- -* Unicode keys support. It is now possible to pass the flag `allow_unicode_keys` when creating the clients, thanks @jogo! -* Fixed a bug where PooledClient wasn't following `default_noreply` arg set on init, thanks @kols! +* Unicode keys support. It is now possible to pass the flag ``allow_unicode_keys`` when creating the clients, thanks @jogo! +* Fixed a bug where PooledClient wasn't following ``default_noreply`` arg set on init, thanks @kols! * Improved documentation New in version 1.3.8 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/MANIFEST.in new/pymemcache-2.2.2/MANIFEST.in --- old/pymemcache-2.1.1/MANIFEST.in 2018-09-07 17:11:46.000000000 +0200 +++ new/pymemcache-2.2.2/MANIFEST.in 2019-08-06 21:06:33.000000000 +0200 @@ -2,4 +2,3 @@ recursive-include pymemcache *.py global-exclude *.pyc global-exclude *.pyo - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/PKG-INFO new/pymemcache-2.2.2/PKG-INFO --- old/pymemcache-2.1.1/PKG-INFO 2019-01-28 18:34:49.000000000 +0100 +++ new/pymemcache-2.2.2/PKG-INFO 2019-08-06 21:07:47.000000000 +0200 @@ -1,8 +1,8 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: pymemcache -Version: 2.1.1 -Summary: A comprehensive, fast, pure Python memcached client -Home-page: https://github.com/Pinterest/pymemcache +Version: 2.2.2 +Summary: "A comprehensive, fast, pure Python memcached client" +Home-page: https://github.com/pinterest/pymemcache Author: Charles Gordon Author-email: char...@pinterest.com License: Apache License 2.0 @@ -56,6 +56,13 @@ See the documentation here: https://pymemcache.readthedocs.io/en/latest/ + Django + ------ + + If you're planning on using pymemcache with Django, you might be interested in + `django-pymemcache <https://github.com/django-pymemcache/django-pymemcache>`_. + It provides a Django cache backend that is built on pymemcache. + Comparison with Other Libraries =============================== @@ -133,8 +140,26 @@ Are you really excited about open-source? Or great software engineering? Pinterest is `hiring <https://careers.pinterest.com/>`_! - Change Log - ========== + Changelog + ========= + + New in version 2.2.2 + -------------------- + * Fix ``long_description`` string in Python packaging. + + New in version 2.2.1 + -------------------- + * Fix ``flags`` when setting multiple differently-typed values at once. + + New in version 2.2.0 + -------------------- + * Drop official support for Python 3.4. + * Use ``setup.cfg`` metadata instead ``setup.py`` config to generate package. + * Add ``default_noreply`` parameter to ``HashClient``. + * Add ``encoding`` parameter to ``Client`` constructors (defaults to ``ascii``). + * Add ``flags`` parameter to write operation methods. + * Handle unicode key values in ``MockMemcacheClient`` correctly. + * Improve ASCII encoding failure exception. New in version 2.1.1 -------------------- @@ -193,8 +218,8 @@ New in version 1.4.0 -------------------- - * Unicode keys support. It is now possible to pass the flag `allow_unicode_keys` when creating the clients, thanks @jogo! - * Fixed a bug where PooledClient wasn't following `default_noreply` arg set on init, thanks @kols! + * Unicode keys support. It is now possible to pass the flag ``allow_unicode_keys`` when creating the clients, thanks @jogo! + * Fixed a bug where PooledClient wasn't following ``default_noreply`` arg set on init, thanks @kols! * Improved documentation New in version 1.3.8 @@ -245,13 +270,14 @@ -------------------- * Introduced PooledClient a thread-safe pool of clients +Keywords: memcache,client,database Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: Apache Software License Classifier: Topic :: Database +Description-Content-Type: text/x-rst diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/README.rst new/pymemcache-2.2.2/README.rst --- old/pymemcache-2.1.1/README.rst 2019-01-08 16:29:27.000000000 +0100 +++ new/pymemcache-2.2.2/README.rst 2019-04-20 01:32:18.000000000 +0200 @@ -48,6 +48,13 @@ See the documentation here: https://pymemcache.readthedocs.io/en/latest/ +Django +------ + +If you're planning on using pymemcache with Django, you might be interested in +`django-pymemcache <https://github.com/django-pymemcache/django-pymemcache>`_. +It provides a Django cache backend that is built on pymemcache. + Comparison with Other Libraries =============================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/pymemcache/__init__.py new/pymemcache-2.2.2/pymemcache/__init__.py --- old/pymemcache-2.1.1/pymemcache/__init__.py 2019-01-28 18:34:10.000000000 +0100 +++ new/pymemcache-2.2.2/pymemcache/__init__.py 2019-08-06 21:06:33.000000000 +0200 @@ -1,4 +1,4 @@ -__version__ = '2.1.1' +__version__ = '2.2.2' from pymemcache.client.base import Client # noqa from pymemcache.client.base import PooledClient # noqa diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/pymemcache/client/base.py new/pymemcache-2.2.2/pymemcache/client/base.py --- old/pymemcache-2.1.1/pymemcache/client/base.py 2019-01-08 18:10:49.000000000 +0100 +++ new/pymemcache-2.2.2/pymemcache/client/base.py 2019-08-05 21:52:32.000000000 +0200 @@ -93,18 +93,18 @@ else: key = key.encode('ascii') except (UnicodeEncodeError, UnicodeDecodeError): - raise MemcacheIllegalInputError("Non-ASCII key: '%r'" % key) + raise MemcacheIllegalInputError("Non-ASCII key: %r" % key) key = key_prefix + key parts = key.split() if len(key) > 250: - raise MemcacheIllegalInputError("Key is too long: '%r'" % key) + raise MemcacheIllegalInputError("Key is too long: %r" % key) # second statement catches leading or trailing whitespace elif len(parts) > 1 or parts[0] != key: - raise MemcacheIllegalInputError("Key contains whitespace: '%r'" % key) + raise MemcacheIllegalInputError("Key contains whitespace: %r" % key) elif b'\00' in key: - raise MemcacheIllegalInputError("Key contains null: '%r'" % key) + raise MemcacheIllegalInputError("Key contains null: %r" % key) return key @@ -175,19 +175,27 @@ raise Exception("Unknown flags for value: {1}".format(flags)) + .. note:: + + Most write operations allow the caller to provide a ``flags`` value to + support advanced interaction with the server. This will **override** the + "flags" value returned by the serializer and should therefore only be + used when you have a complete understanding of how the value should be + serialized, stored, and deserialized. + *Error Handling* All of the methods in this class that talk to memcached can throw one of the following exceptions: - * MemcacheUnknownCommandError - * MemcacheClientError - * MemcacheServerError - * MemcacheUnknownError - * MemcacheUnexpectedCloseError - * MemcacheIllegalInputError - * socket.timeout - * socket.error + * :class:`pymemcache.exceptions.MemcacheUnknownCommandError` + * :class:`pymemcache.exceptions.MemcacheClientError` + * :class:`pymemcache.exceptions.MemcacheServerError` + * :class:`pymemcache.exceptions.MemcacheUnknownError` + * :class:`pymemcache.exceptions.MemcacheUnexpectedCloseError` + * :class:`pymemcache.exceptions.MemcacheIllegalInputError` + * :class:`socket.timeout` + * :class:`socket.error` Instances of this class maintain a persistent connection to memcached which is terminated when any of these exceptions are raised. The next @@ -206,7 +214,8 @@ socket_module=socket, key_prefix=b'', default_noreply=True, - allow_unicode_keys=False): + allow_unicode_keys=False, + encoding='ascii'): """ Constructor. @@ -233,6 +242,7 @@ store commands (except from cas, incr, and decr, which default to False). allow_unicode_keys: bool, support unicode (utf8) keys + encoding: optional str, controls data encoding (defaults to 'ascii'). Notes: The constructor does not make a connection to memcached. The first @@ -254,6 +264,7 @@ self.key_prefix = key_prefix self.default_noreply = default_noreply self.allow_unicode_keys = allow_unicode_keys + self.encoding = encoding def check_key(self, key): """Checks key and add key_prefix.""" @@ -293,7 +304,7 @@ finally: self.sock = None - def set(self, key, value, expire=0, noreply=None): + def set(self, key, value, expire=0, noreply=None, flags=None): """ The memcached "set" command. @@ -304,6 +315,8 @@ from the cache, or zero for no expiry (the default). noreply: optional bool, True to not wait for the reply (defaults to self.default_noreply). + flags: optional int, arbitrary bit field used for server-specific + flags Returns: If no exception is raised, always returns True. If an exception is @@ -312,9 +325,10 @@ """ if noreply is None: noreply = self.default_noreply - return self._store_cmd(b'set', {key: value}, expire, noreply)[key] + return self._store_cmd(b'set', {key: value}, expire, noreply, + flags=flags)[key] - def set_many(self, values, expire=0, noreply=None): + def set_many(self, values, expire=0, noreply=None, flags=None): """ A convenience function for setting multiple values. @@ -325,6 +339,8 @@ from the cache, or zero for no expiry (the default). noreply: optional bool, True to not wait for the reply (defaults to self.default_noreply). + flags: optional int, arbitrary bit field used for server-specific + flags Returns: Returns a list of keys that failed to be inserted. @@ -332,12 +348,12 @@ """ if noreply is None: noreply = self.default_noreply - result = self._store_cmd(b'set', values, expire, noreply) + result = self._store_cmd(b'set', values, expire, noreply, flags=flags) return [k for k, v in six.iteritems(result) if not v] set_multi = set_many - def add(self, key, value, expire=0, noreply=None): + def add(self, key, value, expire=0, noreply=None, flags=None): """ The memcached "add" command. @@ -348,6 +364,8 @@ from the cache, or zero for no expiry (the default). noreply: optional bool, True to not wait for the reply (defaults to self.default_noreply). + flags: optional int, arbitrary bit field used for server-specific + flags Returns: If noreply is True, the return value is always True. Otherwise the @@ -356,9 +374,10 @@ """ if noreply is None: noreply = self.default_noreply - return self._store_cmd(b'add', {key: value}, expire, noreply)[key] + return self._store_cmd(b'add', {key: value}, expire, noreply, + flags=flags)[key] - def replace(self, key, value, expire=0, noreply=None): + def replace(self, key, value, expire=0, noreply=None, flags=None): """ The memcached "replace" command. @@ -369,6 +388,8 @@ from the cache, or zero for no expiry (the default). noreply: optional bool, True to not wait for the reply (defaults to self.default_noreply). + flags: optional int, arbitrary bit field used for server-specific + flags Returns: If noreply is True, always returns True. Otherwise returns True if @@ -377,9 +398,10 @@ """ if noreply is None: noreply = self.default_noreply - return self._store_cmd(b'replace', {key: value}, expire, noreply)[key] + return self._store_cmd(b'replace', {key: value}, expire, noreply, + flags=flags)[key] - def append(self, key, value, expire=0, noreply=None): + def append(self, key, value, expire=0, noreply=None, flags=None): """ The memcached "append" command. @@ -390,15 +412,18 @@ from the cache, or zero for no expiry (the default). noreply: optional bool, True to not wait for the reply (defaults to self.default_noreply). + flags: optional int, arbitrary bit field used for server-specific + flags Returns: True. """ if noreply is None: noreply = self.default_noreply - return self._store_cmd(b'append', {key: value}, expire, noreply)[key] + return self._store_cmd(b'append', {key: value}, expire, noreply, + flags=flags)[key] - def prepend(self, key, value, expire=0, noreply=None): + def prepend(self, key, value, expire=0, noreply=None, flags=None): """ The memcached "prepend" command. @@ -409,15 +434,18 @@ from the cache, or zero for no expiry (the default). noreply: optional bool, True to not wait for the reply (defaults to self.default_noreply). + flags: optional int, arbitrary bit field used for server-specific + flags Returns: True. """ if noreply is None: noreply = self.default_noreply - return self._store_cmd(b'prepend', {key: value}, expire, noreply)[key] + return self._store_cmd(b'prepend', {key: value}, expire, noreply, + flags=flags)[key] - def cas(self, key, value, cas, expire=0, noreply=False): + def cas(self, key, value, cas, expire=0, noreply=False, flags=None): """ The memcached "cas" command. @@ -428,13 +456,16 @@ expire: optional int, number of seconds until the item is expired from the cache, or zero for no expiry (the default). noreply: optional bool, False to wait for the reply (the default). + flags: optional int, arbitrary bit field used for server-specific + flags Returns: If noreply is True, always returns True. Otherwise returns None if the key didn't exist, False if it existed but had a different cas value and True if it existed and was changed. """ - return self._store_cmd(b'cas', {key: value}, expire, noreply, cas)[key] + return self._store_cmd(b'cas', {key: value}, expire, noreply, + flags=flags, cas=cas)[key] def get(self, key, default=None): """ @@ -571,7 +602,7 @@ value of the key, or None if the key wasn't found. """ key = self.check_key(key) - cmd = b'incr ' + key + b' ' + six.text_type(value).encode('ascii') + cmd = b'incr ' + key + b' ' + six.text_type(value).encode(self.encoding) if noreply: cmd += b' noreply' cmd += b'\r\n' @@ -596,7 +627,7 @@ value of the key, or None if the key wasn't found. """ key = self.check_key(key) - cmd = b'decr ' + key + b' ' + six.text_type(value).encode('ascii') + cmd = b'decr ' + key + b' ' + six.text_type(value).encode(self.encoding) if noreply: cmd += b' noreply' cmd += b'\r\n' @@ -625,7 +656,9 @@ if noreply is None: noreply = self.default_noreply key = self.check_key(key) - cmd = b'touch ' + key + b' ' + six.text_type(expire).encode('ascii') + cmd = ( + b'touch ' + key + b' ' + six.text_type(expire).encode(self.encoding) + ) if noreply: cmd += b' noreply' cmd += b'\r\n' @@ -706,7 +739,7 @@ """ if noreply is None: noreply = self.default_noreply - cmd = b'flush_all ' + six.text_type(delay).encode('ascii') + cmd = b'flush_all ' + six.text_type(delay).encode(self.encoding) if noreply: cmd += b' noreply' cmd += b'\r\n' @@ -739,6 +772,31 @@ error = line[line.find(b' ') + 1:] raise MemcacheServerError(error) + def _extract_value(self, expect_cas, line, buf, remapped_keys, + prefixed_keys): + """ + This function is abstracted from _fetch_cmd to support different ways + of value extraction. In order to use this feature, _extract_value needs + to be overriden in the subclass. + """ + if expect_cas: + _, key, flags, size, cas = line.split() + else: + try: + _, key, flags, size = line.split() + except Exception as e: + raise ValueError("Unable to parse line %s: %s" % (line, e)) + + buf, value = _readvalue(self.sock, buf, int(size)) + key = remapped_keys[key] + if self.deserializer: + value = self.deserializer(key, value, int(flags)) + + if expect_cas: + return key, (value, cas), buf + else: + return key, value, buf + def _fetch_cmd(self, name, keys, expect_cas): prefixed_keys = [self.check_key(k) for k in keys] remapped_keys = dict(zip(prefixed_keys, keys)) @@ -760,25 +818,10 @@ if line == b'END' or line == b'OK': return result elif line.startswith(b'VALUE'): - if expect_cas: - _, key, flags, size, cas = line.split() - else: - try: - _, key, flags, size = line.split() - except Exception as e: - raise ValueError("Unable to parse line %s: %s" - % (line, str(e))) - - buf, value = _readvalue(self.sock, buf, int(size)) - key = remapped_keys[key] - - if self.deserializer: - value = self.deserializer(key, value, int(flags)) - - if expect_cas: - result[key] = (value, cas) - else: - result[key] = value + key, value, buf = self._extract_value(expect_cas, line, buf, + remapped_keys, + prefixed_keys) + result[key] = value elif name == b'stats' and line.startswith(b'STAT'): key_value = line.split() result[key_value[1]] = key_value[2] @@ -794,7 +837,7 @@ return {} raise - def _store_cmd(self, name, values, expire, noreply, cas=None): + def _store_cmd(self, name, values, expire, noreply, flags=None, cas=None): cmds = [] keys = [] @@ -803,7 +846,7 @@ extra += b' ' + cas if noreply: extra += b' noreply' - expire = six.text_type(expire).encode('ascii') + expire = six.text_type(expire).encode(self.encoding) for key, data in six.iteritems(values): # must be able to reliably map responses back to the original order @@ -811,20 +854,26 @@ key = self.check_key(key) if self.serializer: - data, flags = self.serializer(key, data) + data, data_flags = self.serializer(key, data) else: - flags = 0 + data_flags = 0 + + # If 'flags' was explicitly provided, it overrides the value + # returned by the serializer. + if flags is not None: + data_flags = flags if not isinstance(data, six.binary_type): try: - data = six.text_type(data).encode('ascii') + data = six.text_type(data).encode(self.encoding) except UnicodeEncodeError as e: - raise MemcacheIllegalInputError(str(e)) + raise MemcacheIllegalInputError( + "Data values must be binary-safe: %s" % e) cmds.append(name + b' ' + key + b' ' + - six.text_type(flags).encode('ascii') + + six.text_type(data_flags).encode(self.encoding) + b' ' + expire + - b' ' + six.text_type(len(data)).encode('ascii') + + b' ' + six.text_type(len(data)).encode(self.encoding) + extra + b'\r\n' + data + b'\r\n') if self.sock is None: @@ -920,7 +969,8 @@ max_pool_size=None, lock_generator=None, default_noreply=True, - allow_unicode_keys=False): + allow_unicode_keys=False, + encoding='ascii'): self.server = server self.serializer = serializer self.deserializer = deserializer @@ -941,6 +991,7 @@ after_remove=lambda client: client.close(), max_size=max_pool_size, lock_generator=lock_generator) + self.encoding = encoding def check_key(self, key): """Checks key and add key_prefix.""" @@ -966,33 +1017,38 @@ def close(self): self.client_pool.clear() - def set(self, key, value, expire=0, noreply=None): + def set(self, key, value, expire=0, noreply=None, flags=None): with self.client_pool.get_and_release(destroy_on_fail=True) as client: - return client.set(key, value, expire=expire, noreply=noreply) + return client.set(key, value, expire=expire, noreply=noreply, + flags=flags) - def set_many(self, values, expire=0, noreply=None): + def set_many(self, values, expire=0, noreply=None, flags=None): with self.client_pool.get_and_release(destroy_on_fail=True) as client: - failed = client.set_many(values, expire=expire, noreply=noreply) + failed = client.set_many(values, expire=expire, noreply=noreply, + flags=flags) return failed set_multi = set_many - def replace(self, key, value, expire=0, noreply=None): + def replace(self, key, value, expire=0, noreply=None, flags=None): with self.client_pool.get_and_release(destroy_on_fail=True) as client: - return client.replace(key, value, expire=expire, noreply=noreply) + return client.replace(key, value, expire=expire, noreply=noreply, + flags=flags) - def append(self, key, value, expire=0, noreply=None): + def append(self, key, value, expire=0, noreply=None, flags=None): with self.client_pool.get_and_release(destroy_on_fail=True) as client: - return client.append(key, value, expire=expire, noreply=noreply) + return client.append(key, value, expire=expire, noreply=noreply, + flags=flags) - def prepend(self, key, value, expire=0, noreply=None): + def prepend(self, key, value, expire=0, noreply=None, flags=None): with self.client_pool.get_and_release(destroy_on_fail=True) as client: - return client.prepend(key, value, expire=expire, noreply=noreply) + return client.prepend(key, value, expire=expire, noreply=noreply, + flags=flags) - def cas(self, key, value, cas, expire=0, noreply=False): + def cas(self, key, value, cas, expire=0, noreply=False, flags=None): with self.client_pool.get_and_release(destroy_on_fail=True) as client: return client.cas(key, value, cas, - expire=expire, noreply=noreply) + expire=expire, noreply=noreply, flags=flags) def get(self, key, default=None): with self.client_pool.get_and_release(destroy_on_fail=True) as client: @@ -1046,9 +1102,10 @@ delete_multi = delete_many - def add(self, key, value, expire=0, noreply=None): + def add(self, key, value, expire=0, noreply=None, flags=None): with self.client_pool.get_and_release(destroy_on_fail=True) as client: - return client.add(key, value, expire=expire, noreply=noreply) + return client.add(key, value, expire=expire, noreply=noreply, + flags=flags) def incr(self, key, value, noreply=False): with self.client_pool.get_and_release(destroy_on_fail=True) as client: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/pymemcache/client/hash.py new/pymemcache-2.2.2/pymemcache/client/hash.py --- old/pymemcache-2.1.1/pymemcache/client/hash.py 2019-01-08 16:29:27.000000000 +0100 +++ new/pymemcache-2.2.2/pymemcache/client/hash.py 2019-06-08 00:30:13.000000000 +0200 @@ -33,7 +33,9 @@ dead_timeout=60, use_pooling=False, ignore_exc=False, - allow_unicode_keys=False + allow_unicode_keys=False, + default_noreply=True, + encoding='ascii' ): """ Constructor. @@ -54,6 +56,7 @@ attempts. dead_timeout (float): Time in seconds before attempting to add a node back in the pool. + encoding: optional str, controls data encoding (defaults to 'ascii'). Further arguments are interpreted as for :py:class:`.Client` constructor. @@ -81,6 +84,7 @@ 'serializer': serializer, 'deserializer': deserializer, 'allow_unicode_keys': allow_unicode_keys, + 'default_noreply': default_noreply, } if use_pooling is True: @@ -91,6 +95,7 @@ for server, port in servers: self.add_server(server, port) + self.encoding = encoding def add_server(self, server, port): key = '%s:%s' % (server, port) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/pymemcache/test/test_client.py new/pymemcache-2.2.2/pymemcache/test/test_client.py --- old/pymemcache-2.1.1/pymemcache/test/test_client.py 2019-01-08 18:10:49.000000000 +0100 +++ new/pymemcache-2.2.2/pymemcache/test/test_client.py 2019-08-05 21:52:32.000000000 +0200 @@ -23,6 +23,7 @@ import mock import socket import unittest + import pytest from pymemcache.client.base import PooledClient, Client @@ -114,6 +115,13 @@ return getattr(socket, name) +class CustomizedClient(Client): + + def _extract_value(self, expect_cas, line, buf, remapped_keys, + prefixed_keys): + return b'key', b'value', b'END\r\n' + + @pytest.mark.unit() class ClientTestMixin(object): def make_client(self, mock_socket_values, **kwargs): @@ -126,16 +134,47 @@ setattr, client, "sock", sock)) return client + def make_customized_client(self, mock_socket_values, **kwargs): + client = CustomizedClient(None, **kwargs) + # mock out client._connect() rather than hard-settting client.sock to + # ensure methods are checking whether self.sock is None before + # attempting to use it + sock = MockSocket(list(mock_socket_values)) + client._connect = mock.Mock(side_effect=functools.partial( + setattr, client, "sock", sock)) + return client + def test_set_success(self): client = self.make_client([b'STORED\r\n']) result = client.set(b'key', b'value', noreply=False) assert result is True + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.set(b'key', b'value', noreply=False) + assert result is True + + # unit test for set operation with parameter flags + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.set(b'key', b'value', noreply=False, flags=0x00000030) + assert result is True + def test_set_future(self): client = self.make_client([b'STORED\r\n']) result = client.set(newbytes(b'key'), newbytes(b'value'), noreply=False) assert result is True + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.set(newbytes(b'key'), newbytes(b'value'), noreply=False) + assert result is True + + # unit test for set operation with parameter flags + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.set(newbytes(b'key'), newbytes(b'value'), noreply=False, + flags=0x00000030) + assert result is True + def test_set_unicode_key(self): client = self.make_client([b'']) @@ -196,26 +235,54 @@ result = client.set(b'key', b'value', noreply=True) assert result is True + # unit test for encoding passed in __init__() + client = self.make_client([], encoding='utf-8') + result = client.set(b'key', b'value', noreply=True) + assert result is True + def test_set_many_success(self): client = self.make_client([b'STORED\r\n']) result = client.set_many({b'key': b'value'}, noreply=False) assert result == [] + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.set_many({b'key': b'value'}, noreply=False) + assert result == [] + def test_set_multi_success(self): # Should just map to set_many client = self.make_client([b'STORED\r\n']) result = client.set_multi({b'key': b'value'}, noreply=False) assert result == [] + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.set_multi({b'key': b'value'}, noreply=False) + assert result == [] + def test_add_stored(self): client = self.make_client([b'STORED\r', b'\n']) result = client.add(b'key', b'value', noreply=False) assert result is True + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r', b'\n'], encoding='utf-8') + result = client.add(b'key', b'value', noreply=False) + assert result is True + def test_add_not_stored(self): client = self.make_client([b'STORED\r', b'\n', b'NOT_', b'STOR', b'ED', b'\r\n']) + client.add(b'key', b'value', noreply=False) result = client.add(b'key', b'value', noreply=False) + assert result is False + + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r', b'\n', + b'NOT_', b'STOR', b'ED', b'\r\n'], + encoding='utf-8') + client.add(b'key', b'value', noreply=False) result = client.add(b'key', b'value', noreply=False) assert result is False @@ -224,17 +291,36 @@ result = client.get(b'key') assert result is None + # Unit test for customized client (override _extract_value) + client = self.make_customized_client([b'END\r\n']) + result = client.get(b'key') + assert result is None + def test_get_not_found_default(self): client = self.make_client([b'END\r\n']) result = client.get(b'key', default='foobar') - assert result is 'foobar' + assert result == 'foobar' + + # Unit test for customized client (override _extract_value) + client = self.make_customized_client([b'END\r\n']) + result = client.get(b'key', default='foobar') + assert result == 'foobar' def test_get_found(self): client = self.make_client([ b'STORED\r\n', b'VALUE key 0 5\r\nvalue\r\nEND\r\n', ]) - result = client.set(b'key', b'value', noreply=False) + client.set(b'key', b'value', noreply=False) + result = client.get(b'key') + assert result == b'value' + + # Unit test for customized client (override _extract_value) + client = self.make_customized_client([ + b'STORED\r\n', + b'VALUE key 0 5\r\nvalue\r\nEND\r\n', + ]) + client.set(b'key', b'value', noreply=False) result = client.get(b'key') assert result == b'value' @@ -253,7 +339,7 @@ b'STORED\r\n', b'VALUE key1 0 6\r\nvalue1\r\nEND\r\n', ]) - result = client.set(b'key1', b'value1', noreply=False) + client.set(b'key1', b'value1', noreply=False) result = client.get_many([b'key1', b'key2']) assert result == {b'key1': b'value1'} @@ -264,8 +350,8 @@ b'VALUE key1 0 6\r\nvalue1\r\n', b'VALUE key2 0 6\r\nvalue2\r\nEND\r\n', ]) - result = client.set(b'key1', b'value1', noreply=False) - result = client.set(b'key2', b'value2', noreply=False) + client.set(b'key1', b'value1', noreply=False) + client.set(b'key2', b'value2', noreply=False) result = client.get_many([b'key1', b'key2']) assert result == {b'key1': b'value1', b'key2': b'value2'} @@ -285,7 +371,7 @@ def test_delete_found(self): client = self.make_client([b'STORED\r', b'\n', b'DELETED\r\n']) - result = client.add(b'key', b'value', noreply=False) + client.add(b'key', b'value', noreply=False) result = client.delete(b'key', noreply=False) assert result is True @@ -306,7 +392,7 @@ def test_delete_many_found(self): client = self.make_client([b'STORED\r', b'\n', b'DELETED\r\n']) - result = client.add(b'key', b'value', noreply=False) + client.add(b'key', b'value', noreply=False) result = client.delete_many([b'key'], noreply=False) assert result is True @@ -316,7 +402,7 @@ b'DELETED\r\n', b'NOT_FOUND\r\n' ]) - result = client.add(b'key', b'value', noreply=False) + client.add(b'key', b'value', noreply=False) result = client.delete_many([b'key', b'key2'], noreply=False) assert result is True @@ -326,7 +412,7 @@ b'DELETED\r\n', b'NOT_FOUND\r\n' ]) - result = client.add(b'key', b'value', noreply=False) + client.add(b'key', b'value', noreply=False) result = client.delete_multi([b'key', b'key2'], noreply=False) assert result is True @@ -370,26 +456,51 @@ result = client.append(b'key', b'value', noreply=False) assert result is True + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.append(b'key', b'value', noreply=False) + assert result is True + def test_prepend_stored(self): client = self.make_client([b'STORED\r\n']) result = client.prepend(b'key', b'value', noreply=False) assert result is True + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.prepend(b'key', b'value', noreply=False) + assert result is True + def test_cas_stored(self): client = self.make_client([b'STORED\r\n']) result = client.cas(b'key', b'value', b'cas', noreply=False) assert result is True + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.cas(b'key', b'value', b'cas', noreply=False) + assert result is True + def test_cas_exists(self): client = self.make_client([b'EXISTS\r\n']) result = client.cas(b'key', b'value', b'cas', noreply=False) assert result is False + # unit test for encoding passed in __init__() + client = self.make_client([b'EXISTS\r\n'], encoding='utf-8') + result = client.cas(b'key', b'value', b'cas', noreply=False) + assert result is False + def test_cas_not_found(self): client = self.make_client([b'NOT_FOUND\r\n']) result = client.cas(b'key', b'value', b'cas', noreply=False) assert result is None + # unit test for encoding passed in __init__() + client = self.make_client([b'NOT_FOUND\r\n'], encoding='utf-8') + result = client.cas(b'key', b'value', b'cas', noreply=False) + assert result is None + def test_cr_nl_boundaries(self): client = self.make_client([b'VALUE key1 0 6\r', b'\nvalue1\r\n' @@ -538,11 +649,21 @@ result = client.replace(b'key', b'value', noreply=False) assert result is True + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.replace(b'key', b'value', noreply=False) + assert result is True + def test_replace_not_stored(self): client = self.make_client([b'NOT_STORED\r\n']) result = client.replace(b'key', b'value', noreply=False) assert result is False + # unit test for encoding passed in __init__ + client = self.make_client([b'NOT_STORED\r\n'], encoding='utf-8') + result = client.replace(b'key', b'value', noreply=False) + assert result is False + def test_serialization(self): def _ser(key, value): return json.dumps(value), 0 @@ -553,6 +674,40 @@ b'set key 0 0 10 noreply\r\n{"c": "d"}\r\n' ] + def test_serialization_flags(self): + def _ser(key, value): + return value, 1 if isinstance(value, int) else 0 + + client = self.make_client( + [b'STORED\r\n', b'STORED\r\n'], serializer=_ser) + client.set_many( + collections.OrderedDict([(b'a', b's'), (b'b', 0)]), noreply=False) + assert client.sock.send_bufs == [ + b'set a 0 0 1\r\ns\r\nset b 1 0 1\r\n0\r\n' + ] + + def test_serialization_overridden_flags(self): + def _ser(key, value): + return value, 1 if isinstance(value, int) else 0 + + client = self.make_client( + [b'STORED\r\n', b'STORED\r\n'], serializer=_ser) + client.set_many( + collections.OrderedDict([(b'a', b's'), (b'b', 0)]), + noreply=False, flags=5) + assert client.sock.send_bufs == [ + b'set a 5 0 1\r\ns\r\nset b 5 0 1\r\n0\r\n' + ] + + def test_explicit_flags(self): + client = self.make_client([b'STORED\r\n', b'STORED\r\n']) + client.set_many( + collections.OrderedDict([(b'a', b's'), (b'b', 0)]), + noreply=False, flags=5) + assert client.sock.send_bufs == [ + b'set a 5 0 1\r\ns\r\nset b 5 0 1\r\n0\r\n' + ] + def test_set_socket_handling(self): client = self.make_client([b'STORED\r\n']) result = client.set(b'key', b'value', noreply=False) @@ -649,11 +804,31 @@ assert client.sock.closed is False assert len(client.sock.send_bufs) == 1 + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r\n'], encoding='utf-8') + result = client.set_many({b'key': b'value'}, noreply=False) + assert result == [] + assert client.sock.closed is False + assert len(client.sock.send_bufs) == 1 + def test_set_many_exception(self): client = self.make_client([b'STORED\r\n', Exception('fail')]) def _set(): client.set_many({b'key': b'value', b'other': b'value'}, + noreply=False) + + with pytest.raises(Exception): + _set() + + assert client.sock is None + + # unit test for encoding passed in __init__() + client = self.make_client([b'STORED\r\n', Exception('fail')], + encoding='utf-8') + + def _set(): + client.set_many({b'key': b'value', b'other': b'value'}, noreply=False) with pytest.raises(Exception): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/pymemcache/test/test_utils.py new/pymemcache-2.2.2/pymemcache/test/test_utils.py --- old/pymemcache-2.1.1/pymemcache/test/test_utils.py 2017-12-18 16:48:03.000000000 +0100 +++ new/pymemcache-2.2.2/pymemcache/test/test_utils.py 2019-05-07 19:37:31.000000000 +0200 @@ -14,6 +14,15 @@ @pytest.mark.unit() +def test_get_set_unicide_key(): + client = MockMemcacheClient() + assert client.get(u"hello") is None + + client.set(b"hello", 12) + assert client.get(u"hello") == 12 + + +@pytest.mark.unit() def test_get_set_non_ascii_value(): client = MockMemcacheClient() assert client.get(b"hello") is None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/pymemcache/test/utils.py new/pymemcache-2.2.2/pymemcache/test/utils.py --- old/pymemcache-2.1.1/pymemcache/test/utils.py 2019-01-08 16:29:27.000000000 +0100 +++ new/pymemcache-2.2.2/pymemcache/test/utils.py 2019-07-05 19:11:21.000000000 +0200 @@ -27,7 +27,8 @@ no_delay=False, ignore_exc=False, default_noreply=True, - allow_unicode_keys=False): + allow_unicode_keys=False, + encoding='ascii'): self._contents = {} @@ -41,11 +42,10 @@ self.timeout = timeout self.no_delay = no_delay self.ignore_exc = ignore_exc + self.encoding = encoding def get(self, key, default=None): if not self.allow_unicode_keys: - if isinstance(key, six.text_type): - raise MemcacheIllegalInputError(key) if isinstance(key, six.string_types): try: if isinstance(key, bytes): @@ -77,24 +77,20 @@ get_multi = get_many - def set(self, key, value, expire=0, noreply=True): + def set(self, key, value, expire=0, noreply=True, flags=0): if not self.allow_unicode_keys: - if isinstance(key, six.text_type): - raise MemcacheIllegalInputError(key) if isinstance(key, six.string_types): try: if isinstance(key, bytes): - key = key.decode().encode('ascii') + key = key.decode().encode() else: - key = key.encode('ascii') + key = key.encode(self.encoding) except (UnicodeEncodeError, UnicodeDecodeError): raise MemcacheIllegalInputError - if isinstance(value, six.text_type): - raise MemcacheIllegalInputError(value) if (isinstance(value, six.string_types) and not isinstance(value, six.binary_type)): try: - value = value.encode('ascii') + value = value.encode(self.encoding) except (UnicodeEncodeError, UnicodeDecodeError): raise MemcacheIllegalInputError diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/pymemcache.egg-info/PKG-INFO new/pymemcache-2.2.2/pymemcache.egg-info/PKG-INFO --- old/pymemcache-2.1.1/pymemcache.egg-info/PKG-INFO 2019-01-28 18:34:49.000000000 +0100 +++ new/pymemcache-2.2.2/pymemcache.egg-info/PKG-INFO 2019-08-06 21:07:47.000000000 +0200 @@ -1,8 +1,8 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: pymemcache -Version: 2.1.1 -Summary: A comprehensive, fast, pure Python memcached client -Home-page: https://github.com/Pinterest/pymemcache +Version: 2.2.2 +Summary: "A comprehensive, fast, pure Python memcached client" +Home-page: https://github.com/pinterest/pymemcache Author: Charles Gordon Author-email: char...@pinterest.com License: Apache License 2.0 @@ -56,6 +56,13 @@ See the documentation here: https://pymemcache.readthedocs.io/en/latest/ + Django + ------ + + If you're planning on using pymemcache with Django, you might be interested in + `django-pymemcache <https://github.com/django-pymemcache/django-pymemcache>`_. + It provides a Django cache backend that is built on pymemcache. + Comparison with Other Libraries =============================== @@ -133,8 +140,26 @@ Are you really excited about open-source? Or great software engineering? Pinterest is `hiring <https://careers.pinterest.com/>`_! - Change Log - ========== + Changelog + ========= + + New in version 2.2.2 + -------------------- + * Fix ``long_description`` string in Python packaging. + + New in version 2.2.1 + -------------------- + * Fix ``flags`` when setting multiple differently-typed values at once. + + New in version 2.2.0 + -------------------- + * Drop official support for Python 3.4. + * Use ``setup.cfg`` metadata instead ``setup.py`` config to generate package. + * Add ``default_noreply`` parameter to ``HashClient``. + * Add ``encoding`` parameter to ``Client`` constructors (defaults to ``ascii``). + * Add ``flags`` parameter to write operation methods. + * Handle unicode key values in ``MockMemcacheClient`` correctly. + * Improve ASCII encoding failure exception. New in version 2.1.1 -------------------- @@ -193,8 +218,8 @@ New in version 1.4.0 -------------------- - * Unicode keys support. It is now possible to pass the flag `allow_unicode_keys` when creating the clients, thanks @jogo! - * Fixed a bug where PooledClient wasn't following `default_noreply` arg set on init, thanks @kols! + * Unicode keys support. It is now possible to pass the flag ``allow_unicode_keys`` when creating the clients, thanks @jogo! + * Fixed a bug where PooledClient wasn't following ``default_noreply`` arg set on init, thanks @kols! * Improved documentation New in version 1.3.8 @@ -245,13 +270,14 @@ -------------------- * Introduced PooledClient a thread-safe pool of clients +Keywords: memcache,client,database Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: Apache Software License Classifier: Topic :: Database +Description-Content-Type: text/x-rst diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/setup.cfg new/pymemcache-2.2.2/setup.cfg --- old/pymemcache-2.1.1/setup.cfg 2019-01-28 18:34:49.000000000 +0100 +++ new/pymemcache-2.2.2/setup.cfg 2019-08-06 21:07:47.000000000 +0200 @@ -1,3 +1,31 @@ +[metadata] +name = pymemcache +version = attr: pymemcache.__version__ +author = Charles Gordon +author_email = char...@pinterest.com +description = "A comprehensive, fast, pure Python memcached client" +long_description = file: README.rst, ChangeLog.rst +long_description_content_type = text/x-rst +license = Apache License 2.0 +url = https://github.com/pinterest/pymemcache +keywords = memcache, client, database +classifiers = + Programming Language :: Python + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: Implementation :: PyPy + License :: OSI Approved :: Apache Software License + Topic :: Database + +[options] +setup_requires = + six +install_requires = + six +packages = find: + [bdist_wheel] universal = true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pymemcache-2.1.1/setup.py new/pymemcache-2.2.2/setup.py --- old/pymemcache-2.1.1/setup.py 2019-01-28 18:34:10.000000000 +0100 +++ new/pymemcache-2.2.2/setup.py 2019-02-22 00:25:57.000000000 +0100 @@ -1,46 +1,5 @@ #!/usr/bin/env python -import os -import re +from setuptools import setup -from setuptools import setup, find_packages - - -def read(path): - return open(os.path.join(os.path.dirname(__file__), path)).read() - - -def read_version(path): - match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", read(path), re.M) - if match: - return match.group(1) - raise RuntimeError("Unable to find __version__ in %s." % path) - - -readme = read('README.rst') -changelog = read('ChangeLog.rst') -version = read_version('pymemcache/__init__.py') - -setup( - name='pymemcache', - version=version, - author='Charles Gordon', - author_email='char...@pinterest.com', - packages=find_packages(), - install_requires=['six'], - description='A comprehensive, fast, pure Python memcached client', - long_description=readme + '\n' + changelog, - license='Apache License 2.0', - url='https://github.com/Pinterest/pymemcache', - classifiers=[ - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: Implementation :: PyPy', - 'License :: OSI Approved :: Apache Software License', - 'Topic :: Database', - ], -) +setup()