[issue41232] Python `functools.wraps` doesn't deal with defaults correctly

2021-05-03 Thread Thor Whalen


Thor Whalen  added the comment:

On the surface, seems like a fair design to me: Back-compatible yet solves
this misalignment that bugged me (literally).
It would also force the documentation of `functools.wraps` to mention this
"trap", through describing the `signature_changed` flag.
As for the `__wrapped_with_changed_signature__`; yes, it's terrible. But I
have no better. At least, it's very descriptive!

On Sat, May 1, 2021 at 9:52 PM Jelle Zijlstra 
wrote:

>
> Jelle Zijlstra  added the comment:
>
> We could add a new argument to `@functools.wraps()` to differentiate
> between a wrapper with the same signature and one with a different
> signature.
>
> Here's a possible design:
> * functools.wraps adds a new keyword-only argument signature_changed. It
> defaults to False for backward compatibility.
> * If signature_changed is True:
>   * __annotations__ are not copied
>   * __wrapped__ is not set on the wrapping function. Instead, we set a new
> attribute __wrapped_with_changed_signature__ (that's a pretty terrible
> name, open to suggestions). This will make inspect.signature not look at
> the wrapped function.
>
> --
>
> ___
> Python tracker 
> <https://bugs.python.org/issue41232>
> ___
>

--

___
Python tracker 
<https://bugs.python.org/issue41232>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue40374] collections.abc docs table: Mapping missing __reversed__

2020-04-23 Thread Thor Whalen


New submission from Thor Whalen :

`Mapping.__reversed__` exists, but is not listed in the table: 
https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes

https://github.com/python/cpython/blob/master/Doc/library/collections.abc.rst

--
assignee: docs@python
components: Documentation
messages: 367127
nosy: Thor Whalen2, docs@python, rhettinger, stutzbach
priority: normal
pull_requests: 19001
severity: normal
status: open
title: collections.abc docs table: Mapping missing __reversed__
versions: Python 3.8

___
Python tracker 
<https://bugs.python.org/issue40374>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue40374] collections.abc docs table: Mapping missing __reversed__

2020-04-24 Thread Thor Whalen


Thor Whalen  added the comment:

Dennis,

Thanks for the (very complete) explanation. I induced to err because of the
following confusing facts:
```
from collections.abc import Mapping, Sequence
import sys
if sys.version_info <= (2, 7):
assert '__reversed__' not in dir(Mapping)
elif sys.version_info >= (3, 7):
assert not isinstance(Mapping, Sequence)
assert '__reversed__' in dir(Mapping)
assert list(reversed(dict.fromkeys('abc'))) == ['c', 'b', 'a']
# + no mention of Mapping.__reversed__ in the docs
```
Indeed, I didn't look at the code. Thanks for that.

On Thu, Apr 23, 2020 at 5:28 PM Dennis Sweeney 
wrote:

>
> Dennis Sweeney  added the comment:
>
> > `Mapping.__reversed__` exists
>
> While ``'__reversed__' in dir(Mapping)`` is true, that unfortunately does
> not mean that it is a real callable method:
>
>
> from collections.abc import Mapping
> class Map(Mapping):
> def __getitem__(self, x):
> if x == 42:
> return 17
> raise KeyError
> def __len__(self, x):
> return 1
> def __iter__(self):
> yield 42
>
> >>> m = Map()
> >>> reversed(m)
> Traceback (most recent call last):
> ...
> TypeError: 'Map' object is not reversible
>
> In the code for Mapping, ``__reversed__`` is explicitly defined as None
> [1] so that calling ``reversed(custom_mapping)`` doesn't accidentally fall
> back on the sequence protocol, which would mean starting at
> len(custom_mapping)-1 and calling __getitem__ on each index [2], which is
> certainly not desirable for arbitrary mappings.
>
> I don't think there is a reasonable way, given arbitrary implementations
> of __len__, __iter__, and __getitem__, to have a mixin reversed iterator.
> If someone wants their mapping to have a __reversed__ method, they should
> define it themselves.
>
> [1]
> https://github.com/python/cpython/blob/master/Lib/_collections_abc.py#L707
> [2]
> https://docs.python.org/3/reference/datamodel.html?highlight=__reversed__#object.__reversed__
>
> --
> nosy: +Dennis Sweeney
>
> ___
> Python tracker 
> <https://bugs.python.org/issue40374>
> ___
>

--
nosy: +Thor Whalen

___
Python tracker 
<https://bugs.python.org/issue40374>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue41232] Python `functools.wraps` doesn't deal with defaults correctly

2020-07-07 Thread Thor Whalen


New submission from Thor Whalen :

# PROBLEM

When using `functools.wraps`, the signature claims one set of defaults, but the 
(wrapped) function uses the original (wrappee) defaults.

Why might that be the desirable default?

# PROPOSED SOLUTION
Adding '__defaults__',  '__kwdefaults__' to `WRAPPER_ASSIGNMENTS` so that 
actual defaults can be consistent with signature defaults.

# Code Demo

```python
from functools import wraps
from inspect import signature

def g(a: float, b=10):
return a * b

def f(a: int, b=1):
return a * b

assert f(3) == 3

f = wraps(g)(f)

assert str(signature(f)) == '(a: float, b=10)'  # signature says that b 
defaults to 10
assert f.__defaults__ == (1,)  # ... but it's a lie!
assert f(3) == 3 != g(3) == 30  # ... and one that can lead to problems!
```

Why is this so? Because `functools.wraps` updates the `__signature__` 
(including annotations and defaults), `__annotations__`, but not 
`__defaults__`, which python apparently looks to in order to assign defaults.

One solution is to politely ask wraps to include these defaults.

```python
from functools import wraps, WRAPPER_ASSIGNMENTS, partial

my_wraps = partial(wraps, assigned=(list(WRAPPER_ASSIGNMENTS) + 
['__defaults__', '__kwdefaults__']))

def g(a: float, b=10):
return a * b

def f(a: int, b=1):
return a * b

assert f(3) == 3

f = my_wraps(g)(f)

assert f(3) == 30 == g(3)
assert f.__defaults__ == (10,)  # ... because now got g defaults!
```

Wouldn't it be better to get this out of the box?

When would I want the defaults that are actually used be different than those 
mentioned in the signature?



--
messages: 373254
nosy: Thor Whalen2
priority: normal
severity: normal
status: open
title: Python `functools.wraps` doesn't deal with defaults correctly
type: enhancement
versions: Python 3.8

___
Python tracker 
<https://bugs.python.org/issue41232>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue41232] Python `functools.wraps` doesn't deal with defaults correctly

2020-07-07 Thread Thor Whalen


Thor Whalen  added the comment:

Posted to stackoverflow to gather opinions about the issue: 
https://stackoverflow.com/questions/62782230/python-functools-wraps-doesnt-deal-with-defaults-correctly

Also made GitHub PR: https://github.com/python/cpython/pull/21379

--
keywords: +patch
message_count: 1.0 -> 2.0
pull_requests: +20523
stage:  -> patch review
pull_request: https://github.com/python/cpython/pull/21379

___
Python tracker 
<https://bugs.python.org/issue41232>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue41232] Python `functools.wraps` doesn't deal with defaults correctly

2020-07-07 Thread Thor Whalen


Thor Whalen  added the comment:

Further, note that even with the additional '__defaults__', and 
'__kwdefaults__', `functools.wraps` breaks when keyword only arguments involved:

```
from functools import wraps, WRAPPER_ASSIGNMENTS, partial

# First, I need to add `__defaults__` and `__kwdefaults__` to wraps, because 
they don't come for free...
my_wraps = partial(wraps, assigned=(list(WRAPPER_ASSIGNMENTS) + 
['__defaults__', '__kwdefaults__']))

def g(a: float, b=10):
return a * b

def f(a: int,  *, b=1):
return a * b

# all is well (for now)...
assert f(3) == 3
assert g(3) == 30
```

This:
```
my_wraps(g)(f)(3)
```
raises TypeError (missing required positional argument 'b'), expected

Note that `wraps(g)(f)(3)` doesn't throw a TypeError, but the output is not 
consistent with the signature (inspect.signature(wraps(g)(f)) is (a: float, 
b=10), so 3 should be multiplied by 10). This is because __defaults__ wasn't 
updated. See for example, that third-party from boltons.funcutils import wraps 
works as expected. And so do (the very popular) wrapt and decorator packages. 
Boltons works for wraps(f)(g), but not wraps(g)(f) in my example. 

See: 
https://stackoverflow.com/questions/62782709/pythons-functools-wraps-breaks-when-keyword-only-arguments-involved

--

___
Python tracker 
<https://bugs.python.org/issue41232>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue41232] Python `functools.wraps` doesn't deal with defaults correctly

2020-08-04 Thread Thor Whalen


Thor Whalen  added the comment:

Hi Terry, sorry for the later reply.

Is this a bugfix? Well, I'm not sure what you would call a bug. Can't one
always redefine a bug to be a feature, and visa versa?

I would definitely say that the behavior (seeing one default in the
signature, but a different one actually taking effect) is probably not a
good one -- as this could lead to very hard to find... bugs. It seems in
fact that third party "fix your decorators" packages such as `wrapt` and
`boltons.funcutils` agree, since their implementation of `wraps` doesn't
have this... "misaligned-by-default feature" that `functools.wraps` does.

Unless I'm missing something, my guess of why `functools.wraps` doesn't
include what I put in my pull request is that it breaks some tests. But I
had a look at the failing test and it seems that it is the test that is
"wrong" (i.e. tests for a behavior that really shouldn't be the default).

See comment:
https://github.com/python/cpython/pull/21379#issuecomment-655661983

The question is: Is there a lot of code out there that depends on this
misaligned behavior. My guess is not.

On Fri, Jul 10, 2020 at 9:58 PM Terry J. Reedy 
wrote:

>
> Terry J. Reedy  added the comment:
>
> Is this actually a bugfix?
>
> --
> nosy: +terry.reedy
> versions: +Python 3.10 -Python 3.8
>
> ___
> Python tracker 
> <https://bugs.python.org/issue41232>
> ___
>

--
nosy: +Thor Whalen

___
Python tracker 
<https://bugs.python.org/issue41232>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue41232] Python `functools.wraps` doesn't deal with defaults correctly

2020-08-05 Thread Thor Whalen


Thor Whalen  added the comment:

You are the guardians of the great python, so we can leave it at that if
you want. That said for posterity, I'll offer a defense.

The same "the tools does what it does, and if you need something else, use
another tool" argument could have been applied to vote against adding
`__annotations__` etc. back when it was lacking.
If there were clear use cases for having signature defaults be different
from actual defaults, I'd also agree with the argument.
If it were a complicated fix, I'd also agree with you.

But it's a simple fix that would help avoiding unintended misalignments.

I may be missing something, but the only positive reasons I see for keeping
it the way it is are: (1) backcompatibility safety, and (2) not having to
change the (in my opinion incorrect) tests.

If it is kept as such, I think a documentation warning would go a long way
in making users avoid the trap I myself fell into.

On Wed, Aug 5, 2020 at 8:51 AM Dominic Davis-Foster 
wrote:

>
> Dominic Davis-Foster  added the comment:
>
> To me your examples seem like a misuse of functools.wraps. IIUC its
> purpose is to make a wrapper that accepts solely *args and **kwargs appear
> to be the wrapped function; it doesn't seem to be intended to be used when
> the wrapper takes arguments of different types or that have different
> default values.
>
> I can't think of a situation where you'd use wraps with a decorator that
> doesn't take just *args and **kwargs. That's not to say there aren't
> occasions where you'd want to to that, just that wraps isn't the right tool.
>
> --
> nosy: +domdfcoding
>
> ___
> Python tracker 
> <https://bugs.python.org/issue41232>
> ___
>

--

___
Python tracker 
<https://bugs.python.org/issue41232>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com