Skip to content

add stdlib queue.Queue compatible API - #7

Merged
philipbl merged 27 commits into
philipbl:masterfrom
Kriechi:queue-api
Dec 6, 2016
Merged

philipbl merged 27 commits into
philipbl:masterfrom
Kriechi:queue-api

Conversation

@Kriechi

@Kriechi Kriechi commented Nov 14, 2016 •

Copy link
Copy Markdown
Contributor

This PR adds new functions to reach API-compatibility with the Python stdlib queue.Queue.
I'll leave bumping the major version number to @philipbl.

fixes #2

ToDo:

  • add tests for new API function
  • add timeout handling to get and put
  • restructure API to Python stdlib queue
  • add maxsize checks to handle full queues
  • make use of queue.Empty and queue.Full exceptions
  • 100% test coverage
  • add documentation

@philipbl

Copy link
Copy Markdown
Owner

Why did you switch to py.test?

@Kriechi

Kriechi commented Nov 15, 2016

Copy link
Copy Markdown
Contributor Author

You already invoked the tests using py.test:

- "py.test -v --cov persistent_queue"

So I though it would be nice to fully embrace the py.test goodness(tm). 😄

What do you think in general of the proposed API additions?

@philipbl philipbl mentioned this pull request Nov 15, 2016
@philipbl

Copy link
Copy Markdown
Owner

The API changes seem good!

I think we could remove some redundant methods. For example, instead of having a push method, we could use put, but have it handle multiple values, similar to how push currently works. Instead of having a pop method, we could use the get method, but change it to take an optional value for the number of items to get. These changes would still be Queue compatible but remove the extra methods (and possibly confusing terms: pop vs get, push vs put).

@philipbl

Copy link
Copy Markdown
Owner

Could you separate the changes to the tests into another PR?

@Kriechi

Kriechi commented Nov 16, 2016

Copy link
Copy Markdown
Contributor Author

Sure! I didn't want to mess with your existing API too much (semver and all...)
But if you are open to bumping up to 2.0 or something, I would be happy to make the stdlib API "the default". Should I open another PR - or should I integrate it here?

@Kriechi

Kriechi commented Nov 16, 2016 •

Copy link
Copy Markdown
Contributor Author

Could you separate the changes to the tests into another PR?

Done.

@coveralls

coveralls commented Nov 16, 2016 •

Copy link
Copy Markdown

Coverage Status

Coverage decreased (-9.4%) to 90.61% when pulling 3bf0c05 on Kriechi:queue-api into e319e5e on philipbl:master.

@philipbl

Copy link
Copy Markdown
Owner

Yeah, let's bump it to version 2. I think it would be better to have the API resemble something that people are familiar with.

You can make those changes as part of this PR. What ever is easiest.

@Kriechi

Kriechi commented Nov 17, 2016 •

Copy link
Copy Markdown
Contributor Author

Ok - I just pushed most of the refactoring into this PR. I might have gone overboard and changed a lot of things. If you have any objections to the specifics parts, I will try to extract it.

  • use tox to have a nice test matrix
  • use flake8 as linter
  • use modern package layout with __init__.py
  • implement stdlib-queue-compatible API
  • restructure class and expand doc-comments
  • fix a potential bug with self.path, use os.abspath to get a full filename
  • mark all internal members as private with self._foobar underscore
  • merged / reimplemented python2.7 support from master

Two tests are still missing - then we have 100% coverage again.

@Kriechi Kriechi changed the title [WIP] add stdlib queue.Queue compatible API add stdlib queue.Queue compatible API Nov 18, 2016
@Kriechi

Kriechi commented Nov 18, 2016

Copy link
Copy Markdown
Contributor Author

@philipbl all done!
Let me know if you are happy with the API, then I will start working on the README / documentation.

@philipbl

Copy link
Copy Markdown
Owner

Sorry for the slow response. I really like it!

I have a few comments, but it's too late tonight and I want to look at the code again in the morning.

Thanks for all of your work on this!

@philipbl philipbl left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few comments for you. Let me know what you think.

Comment thread persistent_queue/persistent_queue.py Outdated
if timeout <= 0:
raise queue.Empty
self._put_event.clear()
elif not partial and items == 1:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should happen if items > 1? For example, queue.get(block=False, items=2).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this part is undefined at the moment. What would you expect it to do? Should it raise if we try to get more items than are available, or just return everything that is available?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably doesn't matter as long as it is consistent and documented.

I think when you peek (partial=True), the data that is available should be given, even if more was requested. When you get (partial=False), an exception should be raised if there is not enough items in the queue, no matter how many items are being requested.

So I would change lines 130-132 to be:

elif not partial and self._length < items:
    raise queue.Empty

Comment thread persistent_queue/persistent_queue.py Outdated
if timeout is not None:
target = time.time() + timeout
while self._length < items:
if self._put_event.wait(timeout) is not True:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I don't completely understand, but the logic doesn't seem to be right. First, shouldn't the not True be changed to True? Second, if wait returns True, then we need to check the length again before we see if the timeout has expired and raise an exception. Something like:

while self._length < items:
    if self._put_event.wait(timeout) is True:
        if timeout is not None:
            timeout = target - time.time()
            if self._length < items and timeout <= 0:
                raise queue.Empty
    self._put_event.clear()

Third, we need something to handle the case where wait returns False:

if self._put_event.wait(timeout) is True:
    ...
else:
    raise queue.Empty

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is within the if block: condition - so we should not raise a queue.Empty unless a timeout value is given.

If timeout == None, the code looks like this (simplified):

            while self._length < items:
                self._put_event.wait(None) # only returns True
                self._put_event.clear()

If timeout is a number, the code looks like this (simplified)
target is going to be e.g. 5 seconds in the future.

            while self._length < items:
                if self._put_event.wait(5) is not True:
                        timeout = target - time.time()
                        if timeout <= 0:  # only raise if timeout was set and has expired
                            raise queue.Empty
                # else: the event obviously returned True, which means a new item was pushed
                # so we just clear the event, and let the while-condition decode what to do next
                self._put_event.clear()

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't

if self._put_event.wait(timeout) is not True:

be simplified to

if self._put_event.wait(timeout) is False:

?

Maybe I am understanding Event.wait incorrectly. Doesn't it return False if the wait call times out? Assuming that a timeout value is given, this branch

if self._put_event.wait(timeout) is not True:

will only be taken if wait has timed out. Since wait has timed out, we can just raise queue.Empty, we don't need to do the rest of the logic you have. It is only when wait returns True (because something was added the the queue so the wait didn't time out) that you need to adjust timeout to a new value, as long as there's still not enough items in the queue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - this is probably a bit weirdly written. As far as I understood the API docs, it returns True if the event got set. But in the case of a timeout, the return value is not defined (I assume it is and will be False forever. Do you want me to clean that up and make it more intuitive?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I think the behavior is incorrect, not unintuitive. Here is what I think the behavior should be

if block:
    if timeout is not None:
        target = time.time() + timeout
    while self._length < items:
        if self._put_event.wait(timeout) is False:
            # Nothing was added to the queue and timeout expired
            # This will never happen if timeout is None
            raise queue.Empty

        # Something was added to the queue
        # Update timeout, if necessary
        if timeout is not None:
            timeout = target - time.time()
            if timeout <= 0:
                raise queue.Empty

        self._put_event.clear()
elif not partial and items == 1:
    if self._length < items:
        raise queue.Empty

What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - you are right!

One minor thing:
I think we can even remove this block - since if the timeout would be negative, the wait should have already returned False.

if timeout <= 0:
    raise queue.Empty

What do you think?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment thread persistent_queue/persistent_queue.py Outdated
with self._all_tasks_done:
unfinished = self._unfinished_tasks - items
if unfinished <= 0:
if unfinished < 0:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a style thing, but I don't like the nested checks on unfinished. How about something like:

if unfinished < 0:
    raise ValueError('task_done() called too many times')
elif unfinished == 0:
    self._all_tasks_done.notify_all()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok - I will change it.
FYI: I just copied this block of code straight out of lib/python3.5/queue.py from the Python stdlib.

Comment thread persistent_queue/persistent_queue.py Outdated
flush_limit: below this filesize, flush() is a no-op.
"""
if filename is None:
raise ValueError("filename must be set, e.g. '/my/path/important.queue'")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why you didn't make filename a required parameter? I know it is a break from the Queue API, but isn't this practically the same thing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right, this is probably a valid point to break API-compatibility.
However, I'm not sure how to implement this - because putting filename as a first argument might lead to confusion, since the std-API expects an integer as API...
Or should we just verify that filename is a str?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, we don't need to check to make sure filename is a str. We don't do that for the rest of the parameters and os.path.abspath(filename) will fail if filename is an int. I don't feel strongly about this though, so if you want to add the check, go ahead.

Here is what I think the signature should be:

def __init__(self, filename, maxsize=0, dumps=pickle.dumps, loads=pickle.loads, flush_limit=1048576):

Comment thread persistent_queue/persistent_queue.py Outdated
if timeout is not None:
target = time.time() + timeout
while self._length + len(items) > self.maxsize:
if self._get_event.wait(timeout) is not True:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comments for _peek. I think they apply here as well.

@Kriechi

Kriechi commented Dec 1, 2016

Copy link
Copy Markdown
Contributor Author

I just pushed a few new commits that should address all your comments.

@philipbl
philipbl merged commit a2f19d2 into philipbl:master Dec 6, 2016
@Kriechi
Kriechi deleted the queue-api branch December 6, 2016 19:30
@Kriechi

Kriechi commented Dec 6, 2016

Copy link
Copy Markdown
Contributor Author

Yeah! Thanks! 👍 🎉 👏

@philipbl philipbl added this to the Version 2 milestone Dec 6, 2016
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.

Support timeout for blocking peek/pop

3 participants