GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Incorrect format passed to `__annotate__` function · Issue #125507 · python/cpython

Incorrect format passed to __annotate__ function #125507

New issue
Copy link
New issue
Copy link
Closed as not planned
Closed as not planned
Incorrect format passed to __annotate__ function#125507
Copy link
Assignees
sobolevn
Labels
3.14bugs and security fixesbugs and security fixesstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@sobolevn
sobolevn
opened on Oct 15, 2024
Issue body actions

Bug report

Consider this example:

class My:
    def __annotate__(format):
        assert format == 2, format
        return {}

import annotationlib
annotationlib.get_annotations(My, format=2)

This will fail with:

Traceback (most recent call last):
  File "/Users/sobolev/Desktop/cpython2/ex.py", line 7, in <module>
    annotationlib.get_annotations(My, format=2)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/Users/sobolev/Desktop/cpython2/Lib/annotationlib.py", line 688, in get_annotations
    return dict(_get_dunder_annotations(obj))
                ~~~~~~~~~~~~~~~~~~~~~~~^^^^^
  File "/Users/sobolev/Desktop/cpython2/Lib/annotationlib.py", line 817, in _get_dunder_annotations
    ann = _BASE_GET_ANNOTATIONS(obj)
  File "/Users/sobolev/Desktop/cpython2/ex.py", line 3, in __annotate__
    assert format == 2, format
           ^^^^^^^^^^^
AssertionError: 1

I think that we should still pass the format in _BASE_GET_ANNOTATIONS.

I will try to fix this, since this affects my other PR: #122262

CC @JelleZijlstra

Linked PRs

  • gh-125507: Call annotate(FORWARDREF) before trying __annotations__ #125534

Activity

  1. sobolevn
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
    topic-typing
    3.14bugs and security fixesbugs and security fixes
    on Oct 15, 2024
  2. sobolevn
    self-assigned this
    on Oct 15, 2024
  3. sobolevn commented on Oct 15, 2024

    @sobolevn
    sobolevn
    on Oct 15, 2024
    MemberAuthor
    More actions

    Ok, this is not so easy. What happens here:

    1. We try to get __annotations__ first with format=VALUE via

      cpython/Lib/annotationlib.py

      Line 817 in aa18fd5

      ann = _BASE_GET_ANNOTATIONS(obj)
    2. If it fails with a NameError we actually try to call __annotate__ with format=FORWARDREF:

      cpython/Lib/annotationlib.py

      Lines 692 to 693 in aa18fd5

      # But if __annotations__ threw a NameError, we try calling __annotate__
      ann = _get_and_call_annotate(obj, format)

    My code in #122262 never had any NameErrors, so it never actually received the second call

    But the question is: is this a good API? For me it feels like a very nuanced one with a lot of corner-cases.

  4. sobolevn commented on Oct 15, 2024

    @sobolevn
    sobolevn
    on Oct 15, 2024
    MemberAuthor
    More actions

    Practical question, how can I differentiate two calls in

    cpython/Lib/dataclasses.py

    Lines 1614 to 1630 in 3b26e36

    def annotate_method(format):
    typing = sys.modules.get("typing")
    if typing is None and format == annotationlib.Format.FORWARDREF:
    typing_any = annotationlib.ForwardRef("Any", module="typing")
    return {
    ann: typing_any if t is _ANY_MARKER else t
    for ann, t in annotations.items()
    }
    from typing import Any
    ann_dict = {
    ann: Any if t is _ANY_MARKER else t
    for ann, t in annotations.items()
    }
    if format == annotationlib.Format.STRING:
    return annotationlib.annotations_to_string(ann_dict)
    return ann_dict

    1. When calling with format=VALUE the first time, so it can be later called with format=FORWARDREF
    2. When calling with format=VALUE the only time, because this is what was asked by the end user
  5. JelleZijlstra commented on Oct 15, 2024

    @JelleZijlstra
    JelleZijlstra
    on Oct 15, 2024
    Member
    More actions

    Would #124415 help here?

  6. sobolevn commented on Oct 15, 2024

    @sobolevn
    sobolevn
    on Oct 15, 2024
    MemberAuthor
    More actions

    @JelleZijlstra hm, I don't think so. Here's my simplified reproducer with #124415 applied:

    import annotationlib
    
    class My:
        def __annotate__(format):
            assert format == annotationlib.Format.FORWARDREF, format
            # Fails with: `AssertionError: 1`
            return {}
    
    annotationlib.get_annotations(My, format=annotationlib.Format.FORWARDREF)

    So, looks like it does still make the first call with format=1, even when asked to get forward refs explicitly.

  7. JelleZijlstra commented on Oct 15, 2024

    @JelleZijlstra
    JelleZijlstra
    on Oct 15, 2024
    Member
    More actions

    I understand the issue now. You want to return typing.Any in VALUE format, but in FORWARDREF format you want to avoid importing typing and return ForwardRef("typing.Any") directly.

    For FORWARDREF format, we currently try using __annotations__ directly before we call __annotate__(FORWARDREF). If we change the order in which these operations happen, we could avoid your issue. I'll work on a PR for that.

    Reacted by sobolevn
  8. JelleZijlstra commented on Oct 15, 2024

    @JelleZijlstra
    JelleZijlstra
    on Oct 15, 2024
    Member
    More actions

    Though I'm curious if @larryhastings and @carljm have opinions here: If an object has __annotations__ that can be retrieved without error, but it also has an __annotate__ that directly works with the FORWARDREF format, which should we prefer?

    At the sprint, we changed it so we preferred returning __annotations__ over evaluating __annotate__ in the fake globals environment to implement the FORWARDREF format, which makes sense to me. But if a user-written __annotate__ supports FORWARDREF directly, shouldn't we believe it?

  9. JelleZijlstra
    added a commit that references this issue on Oct 15, 2024

    pythongh-125507: Call annotate(FORWARDREF) before trying __annotations__

    74f990a
  10. carljm commented on Oct 15, 2024

    @carljm
    carljm
    on Oct 15, 2024
    Member
    More actions

    To me it seems like a useful, and sensible, invariant that if you successfully get VALUES annotations (the default), then FORWARDREFS annotations will be identical to that. FORWARDREFS is intended for cases where VALUES would fail with NameError, and the API is designed around that assumption. I would prefer not to permit objects where FORWARDREFS annotations permanently differ from VALUES annotations. So I would rather not make changes here.

    To me, the proposed approach for dataclasses seems like over-optimization. I think it's fine to always import typing -- someone is introspecting an annotation that uses typing.Any, after all. There may be other possibilities to avoid this, but I would need to look at the problem more.

    Reacted by larryhastings
  11. larryhastings commented on Oct 19, 2024

    @larryhastings
    larryhastings
    on Oct 19, 2024
    Contributor
    More actions

    What Carl said. This should be documented behavior: if __annotations__ is set, that value will be returned for VALUES or FORWARDREF formats and __annotate__ won't be called.

  12. JelleZijlstra commented on Nov 20, 2024

    @JelleZijlstra
    JelleZijlstra
    on Nov 20, 2024
    Member
    More actions

    Thanks, then let's stick with the current behavior.

  13. JelleZijlstra
    closed this as not plannedon Nov 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

  • @sobolevn
    sobolevn

Labels

3.14bugs and security fixesbugs and security fixesstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.