For one app that I help maintaining I noticed thatpython-memcached was used, which has not been updated in several years.
There were some effortsto transfer python-memcached to a new maintainer,but at the end that did not work out.
While an unmaintained project may currently work,there are several things to consider:
- there may be bugs which do not get fixed
- there may be security issues which do not get fixed
- it may stop working for e.g. a new Python version
So something needed to be done!
Luckily,withpymemcache there is a successor,even backed by a big company.
One of the contributors of the successor even put some effort intomaking it largely compatible.
So, as a first step I updated the app’s dependencies frompython-memcached topymemcache.
In the build configuration (e.g.setup.py,setup.cfg,pyproject.toml…) I swapped the names,and as this is an application, I also updated therequirements.txt.
Are we there yet?
Not so fast.
First, I needed to figure out what needed to be done to the imports.
The package is all about creating a connection to amemcached server,so I had to have a look at theClient class.
Forpython-memcached the import statement is:
from memcacheimport ClientHave you noticed?It is not called
from python_memcached import Client!While the package name in e.g. setup.py (and thus for PyPI)and the package name from the source code are usually the same,that is not necessary.
Forpymemcache it is… wait. There is more than oneClient class.
There ispymemcache.client.base.Client,pymemcache.client.hash.HashClient… and some more.
As the first one is meant to be able to only connect to one server,I chose the latter one.
Signature has changed
Luckily,we have a fairly strong test suite,so after updating the import statements,I knew that the test suite would drive my migration attempts.
The first problem was…
TypeError: set() got an unexpected keyword argument'min_compress_len'The signature forClient.set has changed -min_compress_len is no more.
There was not much to do,except reading the docstring ofpython-memcached’sClient.set.
I decided to just drop it,as there is no replacement.
Next one, please
TypeError: set() got an unexpected keyword argument'time'When I compared the old and the new library,I noticed that thetime argument is now calledexpire.
So I just needed to update that - for all callers!
Also,expire needs to be an int,whereastime also accepted afloat type.
No more debug
The next problem was a connection problem of the client to the server.
I usedpdbpp to have a look what was going on:
Passing[('127.0.0.1:11242', 1)] to aClient’s init (python-memcached) worked,as1 is recognized as debug flag,butpymemcache dropped that argument.
pymemcache can handle differentformats,so I used a list of strings:
['127.0.0.1:11242']Connection refused
Oh my!
The next one was really tough.
ConnectionRefusedError: [Errno 111] Connection refusedNo matter what I tried,I could not connect to the server,but only from within a test.
So, I had a hunch that this was about the test setup.
As I got stuck,I asked a colleague for help,who pointed me out to a pattern we use in the test setup.
During the test setup we try to set a value viaHashClient.set to check whether the server is up already.
Whilepython-memcached returned an int,pymemcache raises an exception when the server is not yet available,which I needed to catch now.
Forget Dead Hosts
Next one…
AttributeError: 'HashClient' object has no attribute 'forget_dead_hosts'Same as above formin_compress_len,this member has gone and after reading its docstring I decided to drop it,especially as it was only called in our test code.
The end is near
The test suite now ran with 3 failures and 1 error.
AttributeError: 'HashClient' object has no attribute 'MemcachedKeyCharacterError'Reading some old code comments, it looks like my predecessors had been bitten by a bug inpython-memcached,which allowed spaces in keys, so a regression test was written.
pymemcache decided to both move and rename the exception.
MemcachedKeyCharacterError ->MemcacheIllegalInputError
Last but not least
The last three failures had the same reason:
testtools.matchers._impl.MismatchError: b'somevalue' != 'somevalue'Oh wow! Instead of strings, we now got bytes!
I had a look at pymemcache’sdocumentationand it looks like this behavior is intended… wat?
>>> from pymemcache.client.base import Client>>> client = Client('127.0.0.1')>>> client.set('some_key', 'some_value')True>>> client.get('some_key')b'some_value'I tried to find a reason for this behavior,but without success.
I really hope a reader can shed some light into this.
Even more confusing, I found ablog post at Real Python,where the example code showed a behavior I would expect, ie string in -> string out.
So, something was wrong here.
Digging inpymemcache’s source code and its documentationI had a hunch that this had something to do with serializing and deserializing the data.
The solution was to pass in both a serializer and a deserializer when initializing aHashClient object:
from pymemcache.client.hash import HashClientfrom pymemcache.serde import python_memcache_deserializerfrom pymemcache.serde import python_memcache_serializerHashClient( servers, serializer=python_memcache_serializer, deserializer=python_memcache_deserializer)The End
I hope this migration guide will help someone out there with a similar task.
I also hope the maintainers ofpymemcache consider linking to this blog post :-)
Updates
2021-12-08
add note that
expireneeds to be anint, no longer afloatafter the deployment of the migration, we noticed a couple of problems,which boiled down to the fact,that pymemcache is less lenient to possible problemsas it only catches selected exceptions,andre-raises the rest,so you need to take care of exception handling in your code!