-
Notifications
You must be signed in to change notification settings - Fork 140
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
Patch treq so public APIs don't swallow any unexpected kwargs. #251
Conversation
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`.
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.
Not that I have any authority, just noticed this quickly as I was glancing around here.
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.
@altendky Do you want some authority? 😉 |
@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. |
Thank you for fixing the lint @glyph, I had a very busy weekend.
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. |
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.
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.
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. |
(Sorry about that. Tabbed one too far.) |
…EU/treq into evilham-dont-use-kwargs-request
…equest-001 Alternative approach with less kwargs muddling
src/treq/client.py
Outdated
"Unsupported arguments: [", | ||
",".join(kwargs.keys()), | ||
"]"]), | ||
category=DeprecationWarning, stacklevel=2) |
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.
:] 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)
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.
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.
800b987
to
68df904
Compare
@@ -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): |
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.
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]) |
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.
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.
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.
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]) |
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.
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') |
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.
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): |
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.
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) |
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.
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.
Hi @evilham ! It would be great to get this done; any chance you could:
Thanks! |
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. |
PR following short conversation on IRC:
^^^^ 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.