From 498c56f8a74d228a53340ddf5590e0e4714374b8 Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Thu, 25 Dec 2025 21:02:20 +0000 Subject: [PATCH] mark: treat True/False/None as literals in AST (avoid debug build crash) Fixes #27 --- src/_pytest/compat.py | 19 ++++++++++++++++++- src/_pytest/mark/expression.py | 6 +++--- testing/test_mark_expression.py | 13 +++++++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 84f9609a7db..1858fb89cfd 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -1,6 +1,7 @@ """ python version compatibility code """ +import ast import enum import functools import inspect @@ -33,7 +34,7 @@ if TYPE_CHECKING: - from typing import Type + from typing import Type # noqa: F401 (used in type string) from typing_extensions import Final @@ -60,6 +61,22 @@ class NotSetType(enum.Enum): import importlib_metadata # noqa: F401 +_LITERAL_IDENTS = { + "True": True, + "False": False, + "None": None, +} + + +def _ident_to_name(name: str) -> ast.expr: + if name in _LITERAL_IDENTS: + literal = _LITERAL_IDENTS[name] + if sys.version_info >= (3, 8): + return ast.Constant(value=literal) + return ast.NameConstant(value=literal) + return ast.Name(id=name, ctx=ast.Load()) + + def _format_args(func: Callable[..., Any]) -> str: return str(signature(func)) diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 04c73411af5..e99d5688a9a 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -27,7 +27,7 @@ import attr -from _pytest.compat import TYPE_CHECKING +from _pytest.compat import TYPE_CHECKING, _ident_to_name if TYPE_CHECKING: from typing import NoReturn @@ -129,7 +129,7 @@ def reject(self, expected: Sequence[TokenType]) -> "NoReturn": def expression(s: Scanner) -> ast.Expression: if s.accept(TokenType.EOF): - ret = ast.NameConstant(False) # type: ast.expr + ret = _ident_to_name("False") else: ret = expr(s) s.accept(TokenType.EOF, reject=True) @@ -161,7 +161,7 @@ def not_expr(s: Scanner) -> ast.expr: return ret ident = s.accept(TokenType.IDENT) if ident: - return ast.Name(ident.value, ast.Load()) + return _ident_to_name(ident.value) s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) diff --git a/testing/test_mark_expression.py b/testing/test_mark_expression.py index 335888618ad..e6f131d9d15 100644 --- a/testing/test_mark_expression.py +++ b/testing/test_mark_expression.py @@ -128,8 +128,6 @@ def test_syntax_errors(expr: str, column: int, message: str) -> None: "not[and]or", "1234+5678", "123.232", - "True", - "False", "if", "else", "while", @@ -139,6 +137,17 @@ def test_valid_idents(ident: str) -> None: assert evaluate(ident, {ident: True}.__getitem__) +@pytest.mark.parametrize( + ("expr", "expected"), + (("True", True), ("False", False), ("None", None)), +) +def test_literal_idents(expr: str, expected: object) -> None: + def _unexpected_lookup(ident: str) -> bool: + pytest.fail("unexpected lookup for {!r}".format(ident)) + + assert evaluate(expr, _unexpected_lookup) is expected + + @pytest.mark.parametrize( "ident", (