Skip to content

fix: Keep quotes on string members of unresolved Literal annotations#466

Open
gaoflow wants to merge 1 commit into
mkdocstrings:mainfrom
gaoflow:fix-unresolved-literal-string-quotes
Open

fix: Keep quotes on string members of unresolved Literal annotations#466
gaoflow wants to merge 1 commit into
mkdocstrings:mainfrom
gaoflow:fix-unresolved-literal-string-quotes

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

For reviewers

  • I did not use AI
  • I used AI and thoroughly reviewed every code/docs change

Description of the change

A Literal[...] whose name could not be resolved (for example used without from typing import Literal) keeps its bare name "Literal" as its canonical path. The guard in _build_subscript that enables literal_strings only matched the resolved spellings typing.Literal / typing_extensions.Literal, so for an unresolved Literal the string members were treated as forward references and re-parsed:

# unresolved Literal (no import)
x: Literal['a-b']      # -> Literal[a - b]   (parsed as a subtraction!)
y: Literal['a', 'b']   # -> Literal[a, b]    (names, not strings)

A resolved Literal keeps the quotes (Literal['a-b']), and value-position strings always keep theirs, so the two paths were inconsistent.

This adds the bare "Literal" spelling to that set. A genuinely user-defined Literal resolves to a dotted path (e.g. mod.Literal), so it is not over-matched, and forward references elsewhere (e.g. list['Foo']list[Foo]) are unaffected.

Verified:

  • Literal['a-b'], Literal['a', 'b'], Literal['hello world'] (unresolved) now round-trip with quotes intact.
  • Regression cases hold: imported Literal, typing.Literal[...], aliased import, int literals, and list['Foo'] still resolving its forward reference.
  • Added a parametrized regression test in tests/test_expressions.py (it fails before the change, passes after). test_expressions.py: 146 passed. ruff check clean (zero new warnings).

Relevant resources

A `Literal[...]` whose name could not be resolved (e.g. used without an
import) kept its bare name "Literal" as canonical path, so the guard in
`_build_subscript` that enables `literal_strings` only matched the
resolved `typing.Literal`/`typing_extensions.Literal` spellings. String
members of an unresolved `Literal` were then treated as forward
references: `Literal['a-b']` was mis-parsed as `Literal[a - b]` and
`Literal['a', 'b']` as `Literal[a, b]`.

Add the bare "Literal" spelling to the set. A user-defined `Literal`
resolves to a dotted path (e.g. `mod.Literal`), so it is not
over-matched, and forward references elsewhere (e.g. `list['Foo']`) are
unaffected.
@pawamoy

pawamoy commented Jun 21, 2026

Copy link
Copy Markdown
Member

Thanks for the PR @gaoflow.

Can you tell me why this is useful? In what case would someone use Literal without importing it?

@gaoflow

gaoflow commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. I agree that valid code should normally import it; I am not trying to encourage using Literal without an import.

The useful part here is mostly defensive/lossless parsing for source that Griffe can parse but cannot resolve completely. For example, with from __future__ import annotations, a module can still be imported even if an annotation name is missing or guarded differently, and documentation tooling may see partial/in-progress code before a type checker catches it. In that situation I think Griffe should preserve the source shape rather than rewrite string literal members into different expressions.

The current behavior changes meaning quite a bit:

x: Literal['a-b']

is rendered as Literal[a - b], and Literal['a', 'b'] as Literal[a, b]. Even if the annotation is semantically unresolved, the string members are still literal strings in the source, not forward references.

The change is intentionally narrow: it only handles the bare unresolved canonical path Literal, while a real user-defined Literal resolves to a dotted path such as mod.Literal, so it is not matched.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants