summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Reifschneider <jafo00@gmail.com>2023-04-17 17:12:24 -0600
committerGitHub <noreply@github.com>2023-04-17 17:12:24 -0600
commitcf51ba46eb84f7b96ffdf74a5472cab380070325 (patch)
tree06bf25c40adeb25b46e5dc3dd372254c80ca41fb
parentad8dff243d572fc3316ef5ed0557f7b568b40b12 (diff)
parent12f9bf1fb70082e4dc704cfb037e96335bddaa5f (diff)
downloadpython-memcached-cf51ba46eb84f7b96ffdf74a5472cab380070325.tar.gz
Merge branch 'master' into master
-rw-r--r--.travis.yml8
-rw-r--r--MANIFEST.in1
-rw-r--r--PKG-INFO2
-rw-r--r--SECURITY.md1
-rw-r--r--memcache.py147
-rw-r--r--setup.cfg2
-rw-r--r--setup.py14
-rw-r--r--tests/test_memcache.py24
-rw-r--r--tox.ini2
9 files changed, 117 insertions, 84 deletions
diff --git a/.travis.yml b/.travis.yml
index 47cb22e..aa98469 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,11 @@
language: python
python:
- - 2.7
- - 3.4
- - 3.5
- 3.6
+ - 3.7
+ - 3.8
+ - 3.9
+ - 3.10
+ - 3.11
- pypy
services:
- memcached
diff --git a/MANIFEST.in b/MANIFEST.in
index 612ff36..fa3264c 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -4,6 +4,7 @@ include *.txt
include ChangeLog
include MakeFile
+include PSF.LICENSE
global-exclude *.pyc
global-exclude .gitignore
diff --git a/PKG-INFO b/PKG-INFO
index 2eeccdd..4fe0224 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -4,7 +4,7 @@ Version: 1.59
Summary: A Python memcached client library.
Home-page: http://www.tummy.com/Community/software/python-memcached/
Author: Sean Reifschneider
-Author-email: jafo-memcached@tummy.com
+Author-email: jafo00@gmail.com
License: Python Software Foundation License
Description: A Python memcached client library.
Platform: UNKNOWN
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..ad77969
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1 @@
+Please report any security issues to jafo00@gmail.com
diff --git a/memcache.py b/memcache.py
index e0a2662..11da6c1 100644
--- a/memcache.py
+++ b/memcache.py
@@ -59,15 +59,11 @@ import zlib
import six
-if six.PY2:
- # With Python 2, the faster C implementation has to be imported explicitly.
- import cPickle as pickle
-else:
- import pickle
+import pickle
def cmemcache_hash(key):
- return (((binascii.crc32(key) & 0xffffffff) >> 16) & 0x7fff) or 1
+ return ((binascii.crc32(key) & 0xffffffff) >> 16) & 0x7fff
serverHashFunction = cmemcache_hash
@@ -130,7 +126,7 @@ class Client(threading.local):
@group Integers: incr, decr
@group Removal: delete, delete_multi
@sort: __init__, set_servers, forget_dead_hosts, disconnect_all,
- debuglog,\ set, set_multi, add, replace, get, get_multi,
+ debuglog, set, set_multi, add, replace, get, get_multi,
incr, decr, delete, delete_multi
"""
_FLAG_PICKLE = 1 << 0
@@ -253,12 +249,11 @@ class Client(threading.local):
return key
def _encode_cmd(self, cmd, key, headers, noreply, *args):
- cmd_bytes = cmd.encode('utf-8') if six.PY3 else cmd
+ cmd_bytes = cmd.encode('utf-8')
fullcmd = [cmd_bytes, b' ', key]
if headers:
- if six.PY3:
- headers = headers.encode('utf-8')
+ headers = headers.encode('utf-8')
fullcmd.append(b' ')
fullcmd.append(headers)
@@ -326,11 +321,13 @@ class Client(threading.local):
serverData = {}
data.append((name, serverData))
readline = s.readline
- while 1:
+ while True:
line = readline()
- if not line or line.decode('ascii').strip() == 'END':
+ if line:
+ line = line.decode('ascii')
+ if not line or line.strip() == 'END':
break
- stats = line.decode('ascii').split(' ', 2)
+ stats = line.split(' ', 2)
serverData[stats[1]] = stats[2]
return data
@@ -350,8 +347,10 @@ class Client(threading.local):
data.append((name, serverData))
s.send_cmd('stats slabs')
readline = s.readline
- while 1:
+ while True:
line = readline()
+ if line:
+ line = line.decode('ascii')
if not line or line.strip() == 'END':
break
item = line.split(' ', 2)
@@ -366,6 +365,11 @@ class Client(threading.local):
serverData[slab[0]][slab[1]] = item[2]
return data
+ def quit_all(self) -> None:
+ '''Send a "quit" command to all servers and wait for the connection to close.'''
+ for s in self.servers:
+ s.quit()
+
def get_slabs(self):
data = []
for s in self.servers:
@@ -381,7 +385,7 @@ class Client(threading.local):
data.append((name, serverData))
s.send_cmd('stats items')
readline = s.readline
- while 1:
+ while True:
line = readline()
if not line or line.strip() == 'END':
break
@@ -520,18 +524,36 @@ class Client(threading.local):
rc = 0
return rc
- def delete(self, key, time=None, noreply=False):
+ def delete(self, key, noreply=False):
'''Deletes a key from the memcache.
@return: Nonzero on success.
- @param time: number of seconds any subsequent set / update commands
- should fail. Defaults to None for no delay.
@param noreply: optional parameter instructs the server to not send the
reply.
@rtype: int
'''
- return self._deletetouch([b'DELETED', b'NOT_FOUND'], "delete", key,
- time, noreply)
+ key = self._encode_key(key)
+ if self.do_check_key:
+ self.check_key(key)
+ server, key = self._get_server(key)
+ if not server:
+ return 0
+ self._statlog('delete')
+ fullcmd = self._encode_cmd('delete', key, None, noreply)
+
+ try:
+ server.send_cmd(fullcmd)
+ if noreply:
+ return 1
+ line = server.readline()
+ if line and line.strip() in [b'DELETED', b'NOT_FOUND']:
+ return 1
+ self.debuglog('delete expected DELETED or NOT_FOUND, got: %r' % (line,))
+ except socket.error as msg:
+ if isinstance(msg, tuple):
+ msg = msg[1]
+ server.mark_dead(msg)
+ return 0
def touch(self, key, time=0, noreply=False):
'''Updates the expiration time of a key in memcache.
@@ -546,31 +568,23 @@ class Client(threading.local):
reply.
@rtype: int
'''
- return self._deletetouch([b'TOUCHED'], "touch", key, time, noreply)
-
- def _deletetouch(self, expected, cmd, key, time=0, noreply=False):
key = self._encode_key(key)
if self.do_check_key:
self.check_key(key)
server, key = self._get_server(key)
if not server:
return 0
- self._statlog(cmd)
- if time is not None:
- headers = str(time)
- else:
- headers = None
- fullcmd = self._encode_cmd(cmd, key, headers, noreply)
+ self._statlog('touch')
+ fullcmd = self._encode_cmd('touch', key, str(time), noreply)
try:
server.send_cmd(fullcmd)
if noreply:
return 1
line = server.readline()
- if line and line.strip() in expected:
+ if line and line.strip() in [b'TOUCHED']:
return 1
- self.debuglog('%s expected %s, got: %r'
- % (cmd, b' or '.join(expected), line))
+ self.debuglog('touch expected TOUCHED, got: %r' % (line,))
except socket.error as msg:
if isinstance(msg, tuple):
msg = msg[1]
@@ -796,24 +810,18 @@ class Client(threading.local):
key = self._encode_key(key)
if not isinstance(key, six.binary_type):
# set_multi supports int / long keys.
- key = str(key)
- if six.PY3:
- key = key.encode('utf8')
+ key = str(key).encode('utf8')
bytes_orig_key = key
# Gotta pre-mangle key before hashing to a
# server. Returns the mangled key.
server, key = self._get_server(
(serverhash, key_prefix + key))
-
- orig_key = orig_key[1]
else:
key = self._encode_key(orig_key)
if not isinstance(key, six.binary_type):
# set_multi supports int / long keys.
- key = str(key)
- if six.PY3:
- key = key.encode('utf8')
+ key = str(key).encode('utf8')
bytes_orig_key = key
server, key = self._get_server(key_prefix + key)
@@ -972,16 +980,7 @@ class Client(threading.local):
val = val.encode('utf-8')
elif val_type == int:
flags |= Client._FLAG_INTEGER
- val = '%d' % val
- if six.PY3:
- val = val.encode('ascii')
- # force no attempt to compress this silly string.
- min_compress_len = 0
- elif six.PY2 and isinstance(val, long): # noqa: F821
- flags |= Client._FLAG_LONG
- val = str(val)
- if six.PY3:
- val = val.encode('ascii')
+ val = ('%d' % val).encode('ascii')
# force no attempt to compress this silly string.
min_compress_len = 0
else:
@@ -1008,8 +1007,7 @@ class Client(threading.local):
val = comp_val
# silently do not store if value length exceeds maximum
- if (self.server_max_value_length != 0 and
- len(val) > self.server_max_value_length):
+ if (self.server_max_value_length != 0 and len(val) > self.server_max_value_length):
return 0
return (flags, len(val), val)
@@ -1064,7 +1062,7 @@ class Client(threading.local):
server.mark_dead(msg)
return 0
- def _get(self, cmd, key):
+ def _get(self, cmd, key, default=None):
key = self._encode_key(key)
if self.do_check_key:
self.check_key(key)
@@ -1076,7 +1074,7 @@ class Client(threading.local):
self._statlog(cmd)
try:
- cmd_bytes = cmd.encode('utf-8') if six.PY3 else cmd
+ cmd_bytes = cmd.encode('utf-8')
fullcmd = b''.join((cmd_bytes, b' ', key))
server.send_cmd(fullcmd)
rkey = flags = rlen = cas_id = None
@@ -1093,7 +1091,7 @@ class Client(threading.local):
)
if not rkey:
- return None
+ return default
try:
value = self._recv_value(server, flags, rlen)
finally:
@@ -1118,12 +1116,12 @@ class Client(threading.local):
server.mark_dead(msg)
return None
- def get(self, key):
+ def get(self, key, default=None):
'''Retrieves a key from the memcache.
@return: The value or None.
'''
- return self._get('get', key)
+ return self._get('get', key, default)
def gets(self, key):
'''Retrieves a key from the memcache. Used in conjunction with 'cas'.
@@ -1269,10 +1267,7 @@ class Client(threading.local):
elif flags & Client._FLAG_INTEGER:
val = int(buf)
elif flags & Client._FLAG_LONG:
- if six.PY3:
- val = int(buf)
- else:
- val = long(buf) # noqa: F821
+ val = int(buf)
elif flags & Client._FLAG_PICKLE:
try:
file = BytesIO(buf)
@@ -1305,8 +1300,8 @@ class Client(threading.local):
key = key[1]
if key is None:
raise Client.MemcachedKeyNoneError("Key is None")
- if key is '':
- if key_extra_len is 0:
+ if key == '':
+ if key_extra_len == 0:
raise Client.MemcachedKeyNoneError("Key is empty")
# key is empty but there is some other component to key
@@ -1315,8 +1310,7 @@ class Client(threading.local):
if not isinstance(key, six.binary_type):
raise Client.MemcachedKeyTypeError("Key must be a binary string")
- if (self.server_max_key_length != 0 and
- len(key) + key_extra_len > self.server_max_key_length):
+ if (self.server_max_key_length != 0 and len(key) + key_extra_len > self.server_max_key_length):
raise Client.MemcachedKeyLengthError(
"Key length is > %s" % self.server_max_key_length
)
@@ -1468,11 +1462,8 @@ class _Host(object):
def expect(self, text, raise_exception=False):
line = self.readline(raise_exception)
if self.debug and line != text:
- if six.PY3:
- text = text.decode('utf8')
- log_line = line.decode('utf8', 'replace')
- else:
- log_line = line
+ text = text.decode('utf8')
+ log_line = line.decode('utf8', 'replace')
self.debuglog("while expecting %r, got unexpected response %r"
% (text, log_line))
return line
@@ -1489,6 +1480,22 @@ class _Host(object):
self.buffer = buf[rlen:]
return buf[:rlen]
+ def quit(self) -> None:
+ '''Send a "quit" command to remote server and wait for connection to close.'''
+ if self.socket:
+ self.send_cmd('quit')
+
+ # We can't close the local socket until the remote end processes the quit
+ # command and sends us a FIN packet. When that happens, socket.recv()
+ # will stop blocking and return an empty string. If we try to close the
+ # socket before then, the OS will think we're initiating the connection
+ # close and will put the socket into TIME_WAIT.
+ self.socket.recv(1)
+
+ # At this point, socket should be in CLOSE_WAIT. Closing the socket should
+ # release the port back to the OS.
+ self.close_socket()
+
def flush(self):
self.send_cmd('flush_all')
self.expect(b'OK')
diff --git a/setup.cfg b/setup.cfg
index daf1ac7..170cbe2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[bdist_rpm]
release = 1
-packager = Sean Reifschneider <jafo-rpms@tummy.com>
+packager = Sean Reifschneider <jafo00@gmail.com>
requires = python-memcached
[flake8]
diff --git a/setup.py b/setup.py
index b8a0d79..d1e777a 100644
--- a/setup.py
+++ b/setup.py
@@ -11,12 +11,13 @@ setup(
version=version,
description="Pure python memcached client",
long_description=open("README.md").read(),
+ long_description_content_type="text/markdown",
author="Evan Martin",
author_email="martine@danga.com",
maintainer="Sean Reifschneider",
- maintainer_email="jafo@tummy.com",
+ maintainer_email="jafo00@gmail.com",
url="https://github.com/linsomniac/python-memcached",
- download_url=dl_url.format(version),
+ download_url="https://github.com/linsomniac/python-memcached/releases/download/{0}/python-memcached-{0}.tar.gz".format(version), # noqa
py_modules=["memcache"],
install_requires=open('requirements.txt').read().split(),
classifiers=[
@@ -28,11 +29,12 @@ setup(
"Topic :: Internet",
"Topic :: Software Development :: Libraries :: Python Modules",
"Programming Language :: Python",
- "Programming Language :: Python :: 2",
- "Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.4",
- "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
],
)
diff --git a/tests/test_memcache.py b/tests/test_memcache.py
index 40b6524..3593e03 100644
--- a/tests/test_memcache.py
+++ b/tests/test_memcache.py
@@ -4,7 +4,10 @@ from __future__ import print_function
import unittest
import zlib
-import mock
+try:
+ import unittest.mock as mock
+except ImportError:
+ import mock
from memcache import Client, _Host, SERVER_MAX_KEY_LENGTH, SERVER_MAX_VALUE_LENGTH # noqa: H301
from .utils import captured_stderr
@@ -45,12 +48,29 @@ class TestMemcache(unittest.TestCase):
self.check_setget("an_integer", 42)
self.check_setget("an_integer_2", 42, noreply=True)
+ def test_quit_all(self):
+ self.mc.quit_all()
+
def test_delete(self):
self.check_setget("long", int(1 << 30))
result = self.mc.delete("long")
self.assertEqual(result, True)
self.assertEqual(self.mc.get("long"), None)
+ def test_default(self):
+ key = "default"
+ default = object()
+ result = self.mc.get(key, default=default)
+ self.assertEqual(result, default)
+
+ self.mc.set("default", None)
+ result = self.mc.get(key, default=default)
+ self.assertIsNone(result)
+
+ self.mc.set("default", 123)
+ result = self.mc.get(key, default=default)
+ self.assertEqual(result, 123)
+
@mock.patch.object(_Host, 'send_cmd')
@mock.patch.object(_Host, 'readline')
def test_touch(self, mock_readline, mock_send_cmd):
@@ -228,7 +248,7 @@ class TestMemcache(unittest.TestCase):
self.mc.touch('key')
self.assertEqual(
output.getvalue(),
- "MemCached: touch expected %s, got: 'SET'\n" % b'TOUCHED'
+ "MemCached: touch expected %s, got: 'SET'\n" % 'TOUCHED'
)
diff --git a/tox.ini b/tox.ini
index 7d4fc52..1e2a8f0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
minversion = 1.6
-envlist = py27,py34,py35,p36,pypy,pep8
+envlist = py{36,37,38,39,310,311},pypy,pep8
skipsdist = True
[testenv]