From dd07dbb89452b70be8c7b8f19a14e6b566454976 Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Thu, 9 Jul 2026 20:29:28 +0530 Subject: [PATCH] Regex: reject trailing newline for end-anchored patterns In non-MULTILINE mode Python's $ also matches just before a single trailing newline, so Regex('^a$').validate('a\n') wrongly returned 'a' instead of raising SchemaError. Re-check the match with an extra newline appended so that quirk cannot fire, while preserving substring-matching for unanchored patterns. Fixes #307. --- schema/__init__.py | 11 ++++++++++- test_schema.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/schema/__init__.py b/schema/__init__.py index 10d6e8a..a383ba5 100644 --- a/schema/__init__.py +++ b/schema/__init__.py @@ -270,7 +270,16 @@ def validate(self, data: str, **kwargs: Any) -> str: e = self._error try: - if self._pattern.search(data): + # In non-MULTILINE mode, ``$`` also matches just before a single + # trailing newline, so e.g. ``^a$`` spuriously matches ``"a\n"``. + # When the data ends with a newline, re-check with an extra newline + # appended so that quirk cannot fire; if the match vanishes it was a + # phantom and validation must fail. ``newline`` mirrors the data type + # so byte patterns keep working. + newline = "\n" if isinstance(data, str) else b"\n" + if self._pattern.search(data) and not ( + data.endswith(newline) and not self._pattern.search(data + newline) + ): return data else: error_message = ( diff --git a/test_schema.py b/test_schema.py index 4d78456..c8158cc 100644 --- a/test_schema.py +++ b/test_schema.py @@ -178,6 +178,23 @@ def test_regex(): with SE: Regex(r"^[a-z]+$").validate("letters + spaces") == "letters + spaces" + # A trailing newline must not sneak past an end-anchored pattern: in + # non-MULTILINE mode Python's ``$`` also matches just before a single + # trailing newline, so ``^a$`` used to spuriously accept ``"a\n"``. + assert Regex(r"^a$").validate("a") == "a" + with SE: + Regex(r"^a$").validate("a\n") + with SE: + Regex(r"^a$").validate("a\n\n") + with SE: + Regex(r"^a$").validate("\na") + with SE: + Regex(r"^[a-z]+$").validate("letters\n") + # Unanchored patterns keep their substring-matching behaviour, so a value + # with a trailing newline is still accepted. + assert Regex(r"foo").validate("foo\n") == "foo\n" + assert Regex(r"^foo").validate("foo\n") == "foo\n" + # Validate dict key assert Schema({Regex(r"^foo"): str}).validate({"fookey": "value"}) == { "fookey": "value"