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

Event hooks #1215

Closed
wants to merge 7 commits into from
Closed

Event hooks #1215

wants to merge 7 commits into from

Conversation

tomchristie
Copy link
Member

@tomchristie tomchristie commented Aug 24, 2020

Closes #790, based on the design outlined in #790 (comment)

I've narrowed this down to only request and response initially. I think that auth and redirect also make sense, but let's treat that separately. (I want to be careful, since it could be a bit fiddly making sure that raised exceptions within those hooks don't leave responses open.)

Also worth noting that we're not exposing the EventHooks data structure as public API. Event hooks are always set using a regular dictionary, but have smart behaviour when the property is updated, so that they always return lists of installed handlers, but can be set either from a list or from a single callable.

For example:

client = httpx.Client()
assert dict(client.event_hooks) == {'request': [], 'response': []}

client.event_hooks['request'] = some_callable
assert dict(client.event_hooks) == {'request': [some_callable], 'response': []}

client.event_hooks['request'].append(some_other_callable)
assert dict(client.event_hooks) == {'request': [some_callable, some_other_callable], 'response': []}

del client.event_hooks['request']
assert dict(client.event_hooks) == {'request': [], 'response': []}

Rendered docs:

Screenshot 2020-08-24 at 15 28 19

@tomchristie tomchristie added the enhancement New feature or request label Aug 24, 2020
@tomchristie tomchristie requested a review from a team August 24, 2020 14:36
Copy link
Member

@j178 j178 left a comment

Choose a reason for hiding this comment

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

💯 👍

@@ -35,6 +35,7 @@ def request(
verify: VerifyTypes = True,
cert: CertTypes = None,
trust_env: bool = True,
event_hooks: dict = None,
Copy link
Member

@florimondmanca florimondmanca Aug 29, 2020

Choose a reason for hiding this comment

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

I think I'd like us to prefer a stricter typing here, and a more straightforward style that requires using a list of callables:

client = httpx.Client(event_hooks={"request": [on_request], "response": [on_response])

It would make it clearer that client.event_hooks is really just always a dict of lists, rather than "a dict of sometimes a callable, sometimes a list of callables, depending on what you've set" (not even sure that's what this PR intends, actually?).

Hence this suggestion (which would need to be applied across _client.py as well):

Suggested change
event_hooks: dict = None,
event_hooks: Dict[str, List[Callable]] = None,

@@ -410,6 +411,48 @@ def __repr__(self) -> str:
)


class EventHooks(MutableMapping):
Copy link
Member

Choose a reason for hiding this comment

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

I think if we apply my suggestion of using a plain Dict[str, List[Callable]] always, we can mostly drop this custom data structure, correct?

I'm also not sure we need to enforce that the only possible keys be request and response. Currently we do this with a bit of a surprising behavior ("don't actually store items for unknown keys"), while I think making it so that event_hooks behaves just like a normal dict would be beneficial.

So perhaps we could replace this with a simple factory helper…

def create_event_hooks(initial: Dict[str, List[Callable]] = None) -> Dict[str, List[Callable]]):
    event_hooks = {"request": [], "response": []}
    event_hooks.update(initial or {})
    return event_hooks

@@ -166,3 +166,59 @@ async def test_100_continue(server):

assert response.status_code == 200
assert response.content == data


@pytest.mark.usefixtures("async_environment")
Copy link
Member

Choose a reason for hiding this comment

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

What do we think of putting these tests (and those for the sync client) in a dedicated tests/client/test_event_hooks.py file?



def test_client_base_url():
client = AsyncClient()
client = Client()
Copy link
Member

Choose a reason for hiding this comment

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

I think these changes belong to a separate PR? Or most likely, this PR isn't up to date with master. :-)

pass # pragma: nocover

client = Client()
client.event_hooks = {"request": on_request} # type: ignore
Copy link
Member

Choose a reason for hiding this comment

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

Another reason why I'm advocating for a straightforward "dict of lists of callables" is that we wouldn't need any # type: ignore

Suggested change
client.event_hooks = {"request": on_request} # type: ignore
client.event_hooks = {"request": [on_request]}

pass # pragma: nocover


def test_event_hooks_init():
Copy link
Member

Choose a reason for hiding this comment

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

These tests would be simplified if we adopted a simple create_event_hooks() helper that returns a plain dict.

@tomchristie tomchristie mentioned this pull request Sep 2, 2020
@tomchristie tomchristie marked this pull request as draft September 2, 2020 10:31
@florimondmanca florimondmanca mentioned this pull request Sep 6, 2020
@florimondmanca florimondmanca deleted the event-hooks branch September 14, 2020 16:17
@Dustyposa
Copy link

Dustyposa commented Oct 13, 2021

How can we get the response body in response hook function?
Like this:

async def raise_with_response(cls, res: Response) -> None:
        if res.status_code >= 300:
            # await res.aread(). # if no this code, will occurs an error
            print(res.text). # error occurs: httpx.ResponseNotRead: Attempted to access streaming response content, without having called `read()`.

For read text, I need run await res.aread(). Has any other way todo this?
And I shouldn't do this in event_hooks ? What the best way to handle response with response text?
Thanks in advance.

@tomchristie
Copy link
Member Author

I'm not sure what the question is?

If you want to access the response body inside an event hook, then as you've said, you need to call .read()/.aread(), yes.

@Dustyposa
Copy link

I'm not sure what the question is?

If you want to access the response body inside an event hook, then as you've said, you need to call .read()/.aread(), yes.

Thank you, got it.
I just want to know whether should I handle response.text in event_hook or use other way, which one is best way.

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

Successfully merging this pull request may close these issues.

Support for Event Hooks
4 participants