Skip to content
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

Merged
merged 25 commits into from
Jul 27, 2018
Merged

Conversation

Amertz08
Copy link
Contributor

@Amertz08 Amertz08 commented Jul 7, 2018

In reference to #102

Created a decorator to be added to top level methods that will throw a ConnectionError when connect=False is passed in constructor.

@coveralls
Copy link

coveralls commented Jul 7, 2018

Coverage Status

Coverage increased (+0.01%) to 98.479% when pulling 28cae30 on Amertz08:102-mock-connection-error into 5bd2f1b on jamesls:master.

Copy link
Collaborator

@bmerry bmerry left a 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):
Copy link
Collaborator

@bmerry bmerry Jul 10, 2018

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.

@Amertz08
Copy link
Contributor Author

@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 _patch_response and that does look like it might be the best solution to reduce the usage of the decorator.

@Amertz08
Copy link
Contributor Author

Amertz08 commented Jul 10, 2018

If we go the _patch_responses route may I suggest the following modification.

# 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)

@bmerry
Copy link
Collaborator

bmerry commented Jul 10, 2018

Sure, that sounds sensible.

@Amertz08
Copy link
Contributor Author

Amertz08 commented Jul 10, 2018

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 _patch_responses works but we'd likely have to print the method out on failure in order to know which one we are actually calling. Also not entirely sure how'd we go about determining the parameters to pass to the methods outside of creating a dictionary where the keys are the method name and the value is a list of the parameters. Which would still require updates for each time a method is added and is kinda convoluted. Just brainstorming.

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])

@bmerry
Copy link
Collaborator

bmerry commented Jul 11, 2018

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:
Copy link
Collaborator

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.

Copy link
Contributor Author

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
Copy link
Collaborator

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".

Copy link
Contributor Author

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.

@Amertz08
Copy link
Contributor Author

I'm not entirely sure why but it doesn't appear the README is properly rendering the additions I've made. I can see the short text paragraph I wrote but not the code example.

@Amertz08
Copy link
Contributor Author

@bmerry Nix that question on the documentation. I was able to resolve it.

@bmerry
Copy link
Collaborator

bmerry commented Jul 12, 2018

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.

Copy link
Collaborator

@bmerry bmerry left a 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.

@Amertz08
Copy link
Contributor Author

Yeah I'll have to look over redis-py and see how it is implemented and get that in if needed.

@Amertz08
Copy link
Contributor Author

Amertz08 commented Jul 18, 2018

Here is the underlying function on the PubSub class. Basically the same implementation as the standard Redis class. So maybe we implement it in the same manner as we did with FakeStrictRedis

@bmerry
Copy link
Collaborator

bmerry commented Jul 20, 2018

Here is the underlying function on the PubSub class. Basically the same implementation as the standard Redis class. So maybe we implement it in the same manner as we did with FakeStrictRedis

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):
Copy link
Collaborator

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.

class TestPubSubConnected(unittest.TestCase):

def setUp(self):
self.redis = fakeredis.FakePubSub(connected=False)
Copy link
Collaborator

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.

@@ -4403,5 +4403,42 @@ def test_hscan_iter(self):
self.redis.hscan_iter('name')


class TestPubSubConnected(unittest.TestCase):

Copy link
Collaborator

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.

Copy link
Collaborator

@bmerry bmerry left a 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.

@bmerry bmerry merged commit 70d4910 into jamesls:master Jul 27, 2018
@Amertz08
Copy link
Contributor Author

Alright sweet. First OSS contrib.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants