Skip to content

functools.update_wrapper breaks the signature of functools.partial objects #90917

Open

Description

@larryhastings
BPO 46761
Nosy @ncoghlan, @larryhastings, @1st1, @isidentical, @ofey404
PRs
  • gh-90917: handle functools.partial before follow_wrapper_chains in inspect.signature() #31529
  • Files
  • update_wrapper.breaks.partial.signature.test.py
  • update_wrapper.breaks.partial.signature.check.wrapped.py
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2022-02-15.18:06:14.844>
    labels = ['type-bug', '3.8', '3.9', '3.10', '3.11', '3.7', 'library']
    title = 'functools.update_wrapper breaks the signature of functools.partial objects'
    updated_at = <Date 2022-03-26.12:36:12.144>
    user = 'https://github.com/larryhastings'

    bugs.python.org fields:

    activity = <Date 2022-03-26.12:36:12.144>
    actor = 'ncoghlan'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['Library (Lib)']
    creation = <Date 2022-02-15.18:06:14.844>
    creator = 'larry'
    dependencies = []
    files = ['50625', '50639']
    hgrepos = []
    issue_num = 46761
    keywords = ['patch']
    message_count = 21.0
    messages = ['413299', '413557', '413688', '413778', '413814', '413833', '413876', '413896', '414175', '414176', '414638', '414890', '414937', '415024', '415025', '416023', '416024', '416025', '416028', '416031', '416060']
    nosy_count = 7.0
    nosy_names = ['ping', 'ncoghlan', 'larry', 'grahamd', 'yselivanov', 'BTaskaya', 'ofey404']
    pr_nums = ['31529']
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue46761'
    versions = ['Python 3.7', 'Python 3.8', 'Python 3.9', 'Python 3.10', 'Python 3.11']

    Activity

    1. larryhastings commented on Feb 15, 2022

      @larryhastings
      ContributorAuthor

      It's considered good hygiene to use functools.update_wrapper() to make your wrapped functions look like the original. However, when using functools.partial() to pre-supply arguments to a function, if you then call functools.update_wrapper() to update that partial object, inspect.signature() returns the *original* function's signature, not the *wrapped* function's signature.

      To be precise: if you wrap a function with functools.partial() to pre-provide arguments, then immediately call inspect.signature() on that partial object, it returns the correct signature with the pre-filled parameters removed. If you then call functools.update_wrapper() to update the partial from the original function, inspect.signature() now returns the *wrong* signature.

      I looked into it a little. The specific thing changing inspect.signature()'s behavior is the '__wrapped__' attribute added by functools.update_wrapper(). By default inspect.signature() will unwrap partial objects, but only if it has a '__wrapped__' attribute.

      This all looks pretty deliberate. And it seems like there was some thought given to this wrinkle; inspect.signature() takes a "follow_wrapper_chains" parameter the user can supply to control this behavior. But the default is True, meaning that by default it unwraps partial objects if they have a '__wrapped__'.

      I admit I don't have any context for this. Why do we want inspect.signature() to return the wrong signature by default?

    2. added
      3.11only security fixes
      stdlibStandard Library Python modules in the Lib/ directory
      type-bugAn unexpected behavior, bug, or error
      on Feb 15, 2022
    3. 13 remaining items

    4. larryhastings commented on Mar 13, 2022

      @larryhastings
      ContributorAuthor

      You make a good point. I filed a separate bug (bpo-46846) suggesting that partial objects should set their own annotations and signature. I agree that objects performing such magic should take care of these details themselves, rather than requiring the inspect module to have workarounds based on deep knowledge of these other modules' inner workings.

    5. larryhastings commented on Mar 25, 2022

      @larryhastings
      ContributorAuthor

      I heard back from both Samuel Colvin (Pydantic) and Sebastián Ramírez (FastAPI). They said neither of them use update_wrapper with partial objects. They also took a peek in a bunch of other projects (FastAPI, Typer, SQLModel, Asyncer, SQLAlchemy, Trio, and AnyIO) and nobody was doing it.

      So honestly it seems like nobody (but me!) calls update_wrapper on partial objects, and we can just fix it.

      Graham, any final thoughts before we start pulling levers and merging PRs? For now I just want to fix this bug. I'm in favor of re-engineering the relevant objects so they write their own __signature__ objects, so inspect.Signature doesn't have to understand the internals of objects from other modules. But maybe we save that for another day.

    6. grahamd commented on Mar 25, 2022

      grahamdmannequin
      Mannequin

      It is Django I would worry about and look at closely as they do stuff with decorators on instance methods that uses partials.

      https://github.com/django/django/blob/7119f40c9881666b6f9b5cf7df09ee1d21cc8344/django/utils/decorators.py#L43

          def _wrapper(self, *args, **kwargs):
              # bound_method has the signature that 'decorator' expects i.e. no
              # 'self' argument, but it's a closure over self so it can call
              # 'func'. Also, wrap method.__get__() in a function because new
              # attributes can't be set on bound method objects, only on functions.
              bound_method = wraps(method)(partial(method.__get__(self, type(self))))
              for dec in decorators:
                  bound_method = dec(bound_method)
              return bound_method(*args, **kwargs)
      
    7. grahamd commented on Mar 25, 2022

      grahamdmannequin
      Mannequin
    8. larryhastings commented on Mar 26, 2022

      @larryhastings
      ContributorAuthor

      Ooh, good one. I don't know anybody in the Django project to contact though. Anybody have any leads?

    9. grahamd commented on Mar 26, 2022

      grahamdmannequin
      Mannequin

      These days I have no idea who is active on Django.

    10. ncoghlan commented on Mar 26, 2022

      @ncoghlan
      Contributor

      On the historical front, wraps & update_wrapper were only designed to handle true wrapper functions (i.e. those that don't change the calling signature).

      For anything else (including partial), I considered it unlikely that the doc string would still be accurate, let alone any of the other metadata, so I didn't worry about supporting them.

      That sais, if it's practical to make the results of combining the two less broken then I agree it would make sense to do so.

    11. transferred this issue fromon Apr 10, 2022
    12. ofey404 commented on Aug 23, 2022

      @ofey404
      Contributor

      For now I just want to fix this bug. I'm in favor of re-engineering the relevant objects so they write their own signature objects, so inspect.Signature doesn't have to understand the internals of objects from other modules. But maybe we save that for another day.

      Does this problem get fixed now? @larryhastings

      Sorry for disappearing for so long, but in April I was trapped in Shanghai lockdown and totally forgot this issue…

      Ooh, good one. I don't know anybody in the Django project to contact though. Anybody have any leads?

      If this problem is not fixed now, maybe I can post something in Django’s mailing list, trying to come into contact with them?

    13. picnixz commented on Oct 10, 2025

      @picnixz
      Member

      Please block the above user (see the edit) @python/organization-owners

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      stdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions