From a5a829047726f1ac65f87750a3e758b334c60a26 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Sat, 20 Jun 2026 21:12:41 +0200 Subject: [PATCH] fix: Keep quotes on string members of unresolved Literal annotations 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. --- .../src/griffe/_internal/expressions.py | 4 ++++ packages/griffelib/tests/test_expressions.py | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/griffelib/src/griffe/_internal/expressions.py b/packages/griffelib/src/griffe/_internal/expressions.py index 4043c04b..67831a29 100644 --- a/packages/griffelib/src/griffe/_internal/expressions.py +++ b/packages/griffelib/src/griffe/_internal/expressions.py @@ -1324,6 +1324,10 @@ def _build_subscript( if isinstance(left, (ExprAttribute, ExprName)) and left.canonical_path in { "typing.Literal", "typing_extensions.Literal", + # A `Literal` that could not be resolved (e.g. used without an + # import) keeps its bare name as canonical path. Its string members + # are literal strings, not forward references, so keep their quotes. + "Literal", }: literal_strings = True slice_expr = _build( diff --git a/packages/griffelib/tests/test_expressions.py b/packages/griffelib/tests/test_expressions.py index e0b2267a..d8ea9f54 100644 --- a/packages/griffelib/tests/test_expressions.py +++ b/packages/griffelib/tests/test_expressions.py @@ -61,6 +61,26 @@ def test_full_expressions(annotation: str) -> None: assert str(module["x"].annotation) == annotation +@pytest.mark.parametrize( + "annotation", + [ + "Literal['a-b']", + "Literal['a', 'b']", + "Literal['hello world']", + ], +) +def test_unresolved_literal_keeps_string_quotes(annotation: str) -> None: + """Assert string members of an unresolved `Literal` keep their quotes. + + An unresolved `Literal` (used without an import) must still treat its + members as literal strings rather than forward references, otherwise + `Literal['a-b']` is mis-parsed as `Literal[a - b]`. + """ + code = f"x: {annotation}" + with temporary_visited_module(code) as module: + assert str(module["x"].annotation) == annotation + + def test_resolving_full_names() -> None: """Assert expressions are correctly transformed to their fully-resolved form.""" with temporary_visited_module(