-
Notifications
You must be signed in to change notification settings - Fork 185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mock connection error #197
Conversation
…ead of throwing an error
# Conflicts: # fakeredis.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this looks like a reasonable approach. One thing that I think could be improved is to remove the need to put a decorator on every method. The constructor already (optionally) applies a transformation to every method (_patch_responses
), so I'd prefer to do something similar (and maybe have a common function to decide whether an instance member is a command, so that it can be extended later).
A metaclass might be the most Pythonic solution and has the advantage that it patches the class rather than every instance, but it may be a bit of a pain because the syntax changes between Python 2 and 3. I'd also be happy if it was just implemented the same way as _patch_responses
.
The unit test is certainly thorough, but may be overkill and create more work to maintain as new commands are added. A few different cases (e.g. a simple command, commands that have a few of the existing decorators, a command that involves multiple databases, a transaction-related command etc) should be sufficient. If you prefer to keep it as is I won't object. @jamesls do you have an opinion here?
It might also be nice to have a public interface to toggle the _connected
flag (which could just be removing the _
to make the flag public) so that people can write tests similar to the one you wrote for test_rename
.
fakeredis.py
Outdated
@@ -331,6 +331,17 @@ def release(self): | |||
self.redis.delete(self.name) | |||
|
|||
|
|||
def check_conn(func): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this as an internal function, so should have an underscore prefix.
@bmerry Yeah I realized working through it that a decorator on every function was becoming a mentally laborious and I had wished there there was a sole underlying function that everything called so I could simply put the logic in it like the actual Redis implementation. I had not noticed the technique used for |
If we go the # Pass in the decorator
def _patch_responses(obj, decorator):
for attr_name in dir(obj):
attr = getattr(obj, attr_name)
if not callable(attr) or attr_name.startswith('_'):
continue
func = decorator(attr)
setattr(obj, attr_name, func)
def _check_conn(func):
"""Used to mock connection errors"""
@functools.wraps(func)
def func_wrapper(*args, **kwargs):
if not func.__self__.connected:
raise redis.ConnectionError
return func(*args, **kwargs)
return func_wrapper
class FakeStrictRedis(object):
# deleted for brevity
def __init__(self, db=0, charset='utf-8', errors='strict',
decode_responses=False, singleton=True, connected=True, **kwargs):
# deleted for brevity
self._decode_responses = decode_responses
self.connected = connected
if decode_responses:
_patch_responses(self, _make_decode_func)
if not connected:
_patch_responses(self, _check_conn) |
Sure, that sounds sensible. |
…rough _patch_responses
Also think you are right on the testing but I wanted to at least make sure I had them all covered which was nice to have in order to test after removing the decorator on every method. We could, and I'm not sure this is a terribly great testing practice, call all the functions through introspection like how def test_simple_calls(self):
params = {
'set': ['name', 2]
}
for attr_name in dir(self.redis):
attr = getattr(self.redis, attr_name)
if not callable(attr) or attr_name.startswith('_'):
continue
with self.assertRaises(redis.ConnectionError):
attr(*params[attr_name]) |
Okay, we can leave the tests as is for now - trying to do it by introspection sounds yucky. We can always remove them later if they become annoying to maintain. Can you perhaps write a section in the README to describe this feature? |
fakeredis.py
Outdated
_patch_responses(self) | ||
_patch_responses(self, _make_decode_func) | ||
|
||
if not connected: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be done unconditionally? Otherwise you get the odd behaviour that you can toggle connected
on and off, but only if it was initially set to false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I believe you are correct. I'll implement that change.
fakeredis.py
Outdated
@functools.wraps(func) | ||
def func_wrapper(*args, **kwargs): | ||
if not args[0]._connected: | ||
if not func.__self__.connected: | ||
raise redis.ConnectionError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also pass a message with the ConnectionError e.g. "fakeredis is emulating a connection error".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do. Kinda been thinking that needed a message.
I'm not entirely sure why but it doesn't appear the |
@bmerry Nix that question on the documentation. I was able to resolve it. |
Thanks for all the updates. I'm going to be away for about a week, so I'll take a fresh look when I'm back. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good. The one question I have is about how FakePubSub should work e.g. whether it should also have a connected
attribute and fake connection errors when calling functions like psubscribe
. I haven't experimented to see what happens in redis-py in this case.
Yeah I'll have to look over |
Here is the underlying function on the |
Sounds reasonable to me. |
fakeredis.py
Outdated
@@ -2231,11 +2231,13 @@ class FakePubSub(object): | |||
PATTERN_MESSAGE_TYPES = ['psubscribe', 'punsubscribe'] | |||
LISTEN_DELAY = 0.1 # delay between listen loops (seconds) | |||
|
|||
def __init__(self, decode_responses=False, *args, **kwargs): | |||
def __init__(self, connected=True, decode_responses=False, *args, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather put connected
after decode_responses
, so that it doesn't break existing code that passes decode_responses as a positional parameter.
test_fakeredis.py
Outdated
class TestPubSubConnected(unittest.TestCase): | ||
|
||
def setUp(self): | ||
self.redis = fakeredis.FakePubSub(connected=False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather call this variable self.pubsub
since it is not an instance of a Redis object.
test_fakeredis.py
Outdated
@@ -4403,5 +4403,42 @@ def test_hscan_iter(self): | |||
self.redis.hscan_iter('name') | |||
|
|||
|
|||
class TestPubSubConnected(unittest.TestCase): | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency with other classes, remove this blank line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the work on this! I'm going to merge it.
Alright sweet. First OSS contrib. |
In reference to #102
Created a decorator to be added to top level methods that will throw a
ConnectionError
whenconnect=False
is passed in constructor.