Skip to content

fix(analyzer): support member access on Annotated alias values - #11691

Open
Henry Su (hsusul) wants to merge 3 commits into
microsoft:mainfrom
hsusul:fix/annotated-alias-member-access
Open

Henry Su (hsusul) wants to merge 3 commits into
microsoft:mainfrom
hsusul:fix/annotated-alias-member-access

Conversation

@hsusul

Copy link
Copy Markdown
Contributor

Closes #11538.

Summary of Changes

At runtime, typing._AnnotatedAlias.__getattr__ delegates attribute access to __origin__ (the underlying annotated type).

In getTypeOfMemberAccessWithBaseType:

  • Preserved baseType.props?.typeForm as baseTypeForm before stripping special forms.
  • When member lookup on an Annotated special form produces an error or no member, falls back to evaluating member access against the underlying typeForm.
  • Added sample test annotated10.py and test suite entry in typeEvaluator7.test.ts verifying member access on an Annotated alias value.

@bschnurr

Bill Schnurr (bschnurr) commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

🔒 Automated review in progress — Bill Schnurr (@bschnurr) is auto-reviewing this PR.

@bschnurr

Copy link
Copy Markdown
Member

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

Result: could-not-verify

Summary: No tests executed because the verification container could not start: no trusted sandbox image is configured for `microsoft/pyright`. The PR adds the targeted `Annotated10` Jest case covering member access through an `Annotated` alias. Verification is therefore blocked by the environment.

Test runs: 2 not run

  • ⚠️ Not run | Dependency and test discovery | git status --short && git diff --name-status upstream/main...HEAD && git diff --stat upstream/main...HEAD && if [ -d packages/pyright-internal/node_modules ]; then echo DEPS_PRESENT; else echo DEPS_MISSING; fi && node -e "const p=require('./packages/pyright-internal/package.json'); console.log(JSON.stringify(p.scripts))"
  • ⚠️ Not run | Annotated10 | node ./node_modules/jest/bin/jest.js typeEvaluator7.test -t Annotated10 --runInBand
⚠️ Dependency and test discovery diagnostic output
Container verification could not start: no trusted sandbox image is configured for microsoft/pyright.
⚠️ Annotated10 diagnostic output
Not attempted after the verification container failed to start.

@bschnurr Bill Schnurr (bschnurr) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved via Review Center.

@bschnurr Bill Schnurr (bschnurr) added the review-auto:approved Automated review: no blocking findings (approval posted). label Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved via Review Center.

@heejaechang

Heejae Chang (heejaechang) commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

🔴 Pyright CLI QA — 348b77a5 — verdict: red

Automated CLI checks compared this change with its base revision.
10 focused check(s) completed.

Needs review: reportAttributeAccessIssue no longer reported

The base reports reportAttributeAccessIssue for this example; the PR does not. Is this change intended?

Reproduction and observed results

Create these files in an empty directory:

main.py

from typing import Annotated, ClassVar, LiteralString, TypeGuard

class C:
    value: ClassVar[int] = 1

# These origins are typing objects, not the classes their annotations describe.
Text = Annotated[LiteralString, "metadata"]
Guard = Annotated[TypeGuard[C], "metadata"]
ClassValue = Annotated[ClassVar[C], "metadata"]

# Each read must be rejected: each raises AttributeError at runtime.
reveal_type(Text.upper)
reveal_type(Guard.bit_length)
reveal_type(ClassValue.value)

pyrightconfig.json

{
  "include": [
    "main.py"
  ],
  "typeCheckingMode": "basic",
  "pythonVersion": "3.13"
}

runtime_check.py

"""Check that typing-special-form origins do not provide class attributes."""
from typing import Annotated, ClassVar, LiteralString, TypeGuard

class C:
    value: ClassVar[int] = 1

for origin, member in (
    (LiteralString, 'upper'),
    (TypeGuard[C], 'bit_length'),
    (ClassVar[C], 'value'),
):
    alias = Annotated[origin, 'metadata']
    try:
        getattr(alias, member)
    except AttributeError:
        print(f'{origin}: {member} raises AttributeError as expected')
    else:
        raise AssertionError(f'{origin} unexpectedly exposes {member}')

From that directory, run each built Pyright revision:

node <pyright-cli-entrypoint> --outputjson -p .

Base 7464296a7cec3ff1db73222f2598bfb51e6d7b23

{
  "exit_code": 1,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "error",
      "rule": "reportAttributeAccessIssue",
      "message": "Cannot access attribute \"bit_length\" for class \"Annotated\"\n  Attribute \"bit_length\" is unknown",
      "range": {
        "start": [
          12,
          18
        ],
        "end": [
          12,
          28
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "error",
      "rule": "reportAttributeAccessIssue",
      "message": "Cannot access attribute \"upper\" for class \"Annotated\"\n  Attribute \"upper\" is unknown",
      "range": {
        "start": [
          11,
          17
        ],
        "end": [
          11,
          22
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "error",
      "rule": "reportAttributeAccessIssue",
      "message": "Cannot access attribute \"value\" for class \"Annotated\"\n  Attribute \"value\" is unknown",
      "range": {
        "start": [
          13,
          23
        ],
        "end": [
          13,
          28
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"ClassValue.value\" is \"Unknown\"",
      "range": {
        "start": [
          13,
          12
        ],
        "end": [
          13,
          28
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"Guard.bit_length\" is \"Unknown\"",
      "range": {
        "start": [
          12,
          12
        ],
        "end": [
          12,
          28
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"Text.upper\" is \"Unknown\"",
      "range": {
        "start": [
          11,
          12
        ],
        "end": [
          11,
          22
        ]
      }
    }
  ]
}

PR 348b77a5c0f54d199341d5ac80980f99b9d53e68

{
  "exit_code": 0,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"ClassValue.value\" is \"int\"",
      "range": {
        "start": [
          13,
          12
        ],
        "end": [
          13,
          28
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"Guard.bit_length\" is \"(self: TypeGuard[C]) -> int\"",
      "range": {
        "start": [
          12,
          12
        ],
        "end": [
          12,
          28
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"Text.upper\" is \"Overload[(self: LiteralString) -> LiteralString, (self: LiteralString) -> str]\"",
      "range": {
        "start": [
          11,
          12
        ],
        "end": [
          11,
          22
        ]
      }
    }
  ]
}

Diagnostic ranges use zero-based line and character positions.

@hsusul

Copy link
Copy Markdown
Contributor Author

Not intended. All five QA examples were regressions, fixed in f7c9b2f. The fallback now runs only for reads of non-dunder names on an implicit alias whose Annotated origin is a plain class. These cases are no longer delegated:

  • forward-reference, Literal, TypeAliasType, and PEP 695 origins
  • PEP 695 aliases of Annotated
  • dunder names
  • del
  • variables that only hold an alias (the IntAlias if flag else TextAlias case)

Assignment reports a single error again. annotated11.py covers each QA example and fails without the fix. annotated10.py still passes. typeEvaluator and checker suites: 1292 passed.

At runtime, typing._AnnotatedAlias delegates attribute access to
__origin__ (the annotated type). When member access on an Annotated
special form fails, fall back to evaluating member access on its
underlying typeForm.
Evaluate the fallback against the instantiable form of the annotated class
so methods stay unbound, and skip the fallback for non-class origins such as
unions, which do not expose their members' attributes at runtime.
…ed alias forwards

Limit the fallback to reads of non-dunder names on an implicit alias whose
origin is a plain class. Forward references, literals and TypeAliasType
origins, PEP 695 aliases, deletion, and values that merely hold an alias
are no longer delegated, and assignments no longer report a second error.
@hsusul
Henry Su (hsusul) force-pushed the fix/annotated-alias-member-access branch from f7c9b2f to 348b77a Compare October 23, 2026 21:01

This branch has not been deployed

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

Labels

review-auto:approved Automated review: no blocking findings (approval posted).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Member access on an Annotated alias value is rejected (unlike __getitem__, cf #8916)

3 participants