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

Patch treq so public APIs don't swallow any unexpected kwargs. #251

Closed

Conversation

evilham
Copy link

@evilham evilham commented Jul 29, 2019

PR following short conversation on IRC:

exarkun: ugh.  `treq.get`, etc all take **kwargs and silently ignore unrecognized arguments? :(
evilham: yup: https://github.com/twisted/treq/blob/master/src/treq/client.py#L144
only method and url are required, everything else is either used or ignored
exarkun: Bad.
evilham: maybe :-D yes, it shouldn't be too hard to patch, I agree I want the thing to explode in my face if I mistype `unbuffered` as `unbuffreed` instead of causing the whole system to swap and trash to death

^^^^ This has actually happened at some point :-) and is just awkward.

I consider this to be WIP because at the moment I have to stop and therefore can't add tests for "blow up in my face if passed unknown arguments", but since it shouldn't decrease coverage, is understandable and current tests pass, maybe if it passes review it can be fit for merging.

Notice that I had to change an internal API for this tow ork, shouldn't be too much of a problem I guess.

Up until now it was easy to, for example mistype `unbuffered=True` as
`unbuffreed=True` which could have dare consequences :-).

In general it sounds like complaining about unexpected data is a safer bet than
just ignoring it.
Also: Documented the files argument to `treq.request` which was only documented
in the actual code for `treq.client.HTTPClient.request`.
Copy link
Member

@altendky altendky left a comment

Choose a reason for hiding this comment

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

Not that I have any authority, just noticed this quickly as I was glancing around here.

src/treq/client.py Outdated Show resolved Hide resolved
src/treq/client.py Show resolved Hide resolved
evilham and others added 4 commits July 30, 2019 14:47
requests.cookies.merge_cookies already checks and converts the cookies
parameter, so there was no need for treq to do so.
Some code relies on the returned cookies object being defined properly and based
off the CookieJar.
This change ensures all tests pass again.
@glyph
Copy link
Member

glyph commented Aug 5, 2019

@altendky Do you want some authority? 😉

@altendky
Copy link
Member

altendky commented Aug 5, 2019

@glyph if someone sees fit to give me authority I'll try to live up to it... here mostly I fell off the map because pycharm wouldn't let me set a breakpoint without totally trashing the trial run.

@evilham
Copy link
Author

evilham commented Aug 6, 2019

Thank you for fixing the lint @glyph, I had a very busy weekend.

Notice coverage decreases because previously fully tested code is now shorter :-/.

FWIW, I think @altendky's review was very thorough and it's the kind of review that makes the process of contributing to twisted(+ related projects) somewhat easy to do with confidence that things won't utterly break suddenly because of something missed in a rush.

Copy link
Member

@altendky altendky left a comment

Choose a reason for hiding this comment

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

Overall I'm not sure how this fits into backwards compatibility. The point of the change is exactly to break in cases where previously treq did not break. This seems good but it also hazards breaking existing code using treq. I'll go read up on treq/twisted policies here, though I expect someone else should just weigh in on the matter.

src/treq/api.py Outdated Show resolved Hide resolved
src/treq/client.py Outdated Show resolved Hide resolved
src/treq/api.py Outdated Show resolved Hide resolved
@glyph
Copy link
Member

glyph commented Aug 6, 2019

This seems good but it also hazards breaking existing code using treq. I'll go read up on treq/twisted policies here, though I expect someone else should just weigh in on the matter.

the relevant policy is https://twisted.readthedocs.io/en/twisted-19.7.0/core/development/policy/compatibility-policy.html and I'll save you a moment reading through it: the thing to do here is to emit a warning for a few releases before raising an exception. so let's do that here.

src/treq/client.py Outdated Show resolved Hide resolved
@twm twm closed this Aug 8, 2019
@twm twm reopened this Aug 8, 2019
@twm
Copy link
Contributor

twm commented Aug 8, 2019

(Sorry about that. Tabbed one too far.)

@evilham evilham changed the title Patch treq so public APIs don't swallow any unexpected kwargs. [stuck until 18.6 + 2 releases] Patch treq so public APIs don't swallow any unexpected kwargs. Aug 8, 2019
@evilham evilham changed the title [stuck until 18.6 + 2 releases] Patch treq so public APIs don't swallow any unexpected kwargs. Patch treq so public APIs don't swallow any unexpected kwargs. Aug 8, 2019
"Unsupported arguments: [",
",".join(kwargs.keys()),
"]"]),
category=DeprecationWarning, stacklevel=2)
Copy link
Member

@altendky altendky Aug 8, 2019

Choose a reason for hiding this comment

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

:] Listing the unsupported arguments is a nice touch for sure.

:[ When called directly we need stacklevel=2 but when called through treq.request() etc I think you would want stacklevel=3? I don't know how important this is but we could handle it via a silly stack traversal until we got out of treq. At least then it would be isolated here and not require passing params etc. I was curious to practice so I made up this... I tend to think it's kind of icky and this issue may not be worth addressing but... the result is nice. Hopefully the try/finally would successfully make the worst case be that you get a warning but don't know quite where from.

Py2: https://repl.it/@altendky/GrizzledWatchfulPostgres-5
Py3: https://repl.it/@altendky/GrotesqueAnnualTag-6

import inspect
import warnings

top_level_package = __name__.partition('.')[0]

def warn(*args, **kwargs):
    try:
        if 'stacklevel' not in kwargs:
            stack = inspect.stack()

            for stack_level, frame_info in enumerate(stack, 1):
                module_name = inspect.getmodule(frame_info[0]).__name__
                if not module_name.startswith(top_level_package + '.'):
                    break
            else:
                stack_level = 3
            
            kwargs['stacklevel'] = stack_level
    finally:
        kwargs.setdefault('stacklevel', 3)
        warnings.warn(*args, **kwargs)

Copy link
Author

Choose a reason for hiding this comment

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

Nice! I think I'd like to see this in twisted (if not python itself, twisted is a good first step).

I added it to treq._util with some "improvements", I hope you agree that makes the code more generic :-).

Amongst other things, I noticed that if the code lived somewhere outside of the top_level_package, it'd break the loop on the first frame, also, I think it has a few usability goodies.

Since you caught the difference between request and get, I went ahead and added a test for all supported methods.

Thanks to @altendky, who came up with the gist of treq._utils.warn, which
dinamically sets the stacklevel.

This is adds a handy API that could be useful in twisted and other projects,
while being fully compatible with the standard library function.
@evilham evilham force-pushed the evilham-dont-use-kwargs-request branch from 800b987 to 68df904 Compare August 8, 2019 22:50
@@ -45,3 +48,50 @@ def default_pool(reactor, pool, persistent):
set_global_pool(HTTPConnectionPool(reactor, persistent=True))

return get_global_pool()


def warn(*args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

The try/finally was very intentional. This is doing some goofy stuff, which maybe you tidied up a bit, but what we really don't want to have happen is have our attempt to warn someone of a future exception... turn into another exception without the warning. Speaking of which, I probably should have had an except Exception: pass to avoid any exception caused by this inspection ever.

Defaults to 1, which would match the place where this was called from.
"""
# Default to top-level package
namespace = kwargs.pop('namespace', __name__.partition('.')[0])
Copy link
Member

Choose a reason for hiding this comment

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

The default could be gotten by grabbing the calling stack frame's module's name, if it has one. We're already inspecting like we just don't care, why not a bit more.

Copy link
Member

Choose a reason for hiding this comment

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

In so far as it needs to support other packages using it, perhaps it should get a class. Any code that uses it can create an instance which holds the desired top level package name and then call that instance instead of having to reinspect or repass the top level package. With it only needing to be passed once it's not a hassle to anyone to just require it be specified and never guess. Then we can disregard my last comment and have a tad less inspection.

Defaults to 1, which would match the place where this was called from.
"""
# Default to top-level package
namespace = kwargs.pop('namespace', __name__.partition('.')[0])
Copy link
Member

Choose a reason for hiding this comment

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

In so far as it needs to support other packages using it, perhaps it should get a class. Any code that uses it can create an instance which holds the desired top level package name and then call that instance instead of having to reinspect or repass the top level package. With it only needing to be passed once it's not a hassle to anyone to just require it be specified and never guess. Then we can disregard my last comment and have a tad less inspection.

def test_request_unknown_arguments(self):
self._check_unknown_arguments('request',
self.test_request_unknown_arguments,
'GET', 'www.http://example.com')
Copy link
Member

Choose a reason for hiding this comment

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

Is this intentionally not 'http://www.example.com'?

@@ -45,3 +48,50 @@ def default_pool(reactor, pool, persistent):
set_global_pool(HTTPConnectionPool(reactor, persistent=True))

return get_global_pool()


def warn(*args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to use functools.wraps or wrapt or... If we do that can we have extra args? I certainly have had places I should have used them but have never really learned them.

"""
# Default to top-level package
namespace = kwargs.pop('namespace', __name__.partition('.')[0])
default_stacklevel = kwargs.pop('default_stacklevel', 1)
Copy link
Member

Choose a reason for hiding this comment

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

Good golly this review is scattered... I should just stop for now. You are claiming the namespace (should this be top_level_package? even your comment says that) and default_stacklevel parameters. Perhaps there should be tests verifying that they are not valid for warnings.warn(). Just in case they get added someday. Or maybe that's excessive. I'm not sure offhand how to write them without just duplicating the names which hazards the test and this function getting out of sync.

@glyph
Copy link
Member

glyph commented Oct 3, 2019

Hi @evilham !

It would be great to get this done; any chance you could:

  • address some of the reviews
  • placate coveralls
  • resolve the conflicts?

Thanks!

@glyph
Copy link
Member

glyph commented Aug 4, 2020

Since this branch has unresolved conflicts and has now celebrated its first birthday, I'm closing it. The bug is still real though so I've opened #287 to track that. Anyone with time to follow up on the feedback above, please feel free to resubmit.

@glyph glyph closed this Aug 4, 2020
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

4 participants