From ff83bb1473dc3dd0e2010e27944089b5e4873ab1 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:15:04 +0530 Subject: [PATCH 1/5] fix: preserve docnames matching scientific notation in get_safe_filters --- frappe/tests/test_client.py | 9 +++++++++ frappe/tests/test_utils.py | 18 ++++++++++++++++++ frappe/utils/__init__.py | 6 ++---- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/frappe/tests/test_client.py b/frappe/tests/test_client.py index 24bc03f9c3..a40e1ce5d7 100644 --- a/frappe/tests/test_client.py +++ b/frappe/tests/test_client.py @@ -291,3 +291,12 @@ class TestClient(IntegrationTestCase): # cleanup for doc in docs: frappe.delete_doc("Note", doc) + + def test_get_value_with_scientific_notation_docname(self): + from frappe.client import get_value + + tag = frappe.get_doc({"doctype": "Tag", "name": "3E002"}).insert(ignore_if_duplicate=True) + try: + self.assertEqual(get_value("Tag", ["name"], "3E002"), {"name": "3E002"}) + finally: + tag.delete() diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 3093fba8b0..9ab94d4e6b 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -32,6 +32,7 @@ from frappe.utils import ( get_file_timestamp, get_gravatar, get_link_to_report, + get_safe_filters, get_site_info, get_sites, get_url, @@ -352,6 +353,23 @@ class TestFilters(IntegrationTestCase): link = get_link_to_report(name="ToDo", filters=filters) self.assertIn('creation=["between",["2024-01-01","2024-12-31"]]', link) + def test_get_safe_filters_preserves_scientific_notation_docnames(self): + self.assertEqual(get_safe_filters("3E002"), "3E002") + self.assertEqual(get_safe_filters("1E5"), "1E5") + self.assertEqual(get_safe_filters("2e10"), "2e10") + self.assertEqual(get_safe_filters("1.5"), "1.5") + self.assertEqual(get_safe_filters("Infinity"), "Infinity") + self.assertEqual(get_safe_filters("NaN"), "NaN") + + def test_get_safe_filters_still_parses_json(self): + self.assertEqual(get_safe_filters('{"name": "ABC"}'), {"name": "ABC"}) + self.assertEqual(get_safe_filters('[["name", "=", "ABC"]]'), [["name", "=", "ABC"]]) + + def test_get_safe_filters_passes_through_non_strings(self): + self.assertEqual(get_safe_filters({"name": "ABC"}), {"name": "ABC"}) + self.assertEqual(get_safe_filters([["name", "=", "ABC"]]), [["name", "=", "ABC"]]) + self.assertIsNone(get_safe_filters(None)) + class TestMoney(IntegrationTestCase): def test_money_in_words(self): diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 68ca5c7f51..0ec51ca9b0 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -904,12 +904,10 @@ def call(fn, *args, **kwargs): def get_safe_filters(filters): + if not isinstance(filters, str) or not filters or filters[0] not in "{[": + return filters try: filters = orjson.loads(filters) - - if isinstance(filters, int | float): - filters = frappe.as_unicode(filters) - except (TypeError, ValueError): # filters are not passed, not json pass From 46fd36d5429de28aed4ff73405e8951e3f86b1ae Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:31:19 +0530 Subject: [PATCH 2/5] refactor: simplify get_safe_filters guard for readability --- frappe/utils/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 0ec51ca9b0..122368c1b9 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -904,14 +904,12 @@ def call(fn, *args, **kwargs): def get_safe_filters(filters): - if not isinstance(filters, str) or not filters or filters[0] not in "{[": - return filters - try: - filters = orjson.loads(filters) - except (TypeError, ValueError): - # filters are not passed, not json - pass - + if isinstance(filters, str) and filters and filters[0] in "{[": + try: + return orjson.loads(filters) + except (TypeError, ValueError): + # filters are not passed, not json + pass return filters From 470964015eb72d7e4eef2a59f649e5d0943e66a8 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:10:13 +0530 Subject: [PATCH 3/5] fix: also parse JSON-encoded scalar strings in get_safe_filters --- frappe/tests/test_utils.py | 2 ++ frappe/utils/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 9ab94d4e6b..2677f7d18f 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -364,6 +364,8 @@ class TestFilters(IntegrationTestCase): def test_get_safe_filters_still_parses_json(self): self.assertEqual(get_safe_filters('{"name": "ABC"}'), {"name": "ABC"}) self.assertEqual(get_safe_filters('[["name", "=", "ABC"]]'), [["name", "=", "ABC"]]) + # FrappeClient encodes scalar filters via frappe.as_json — must still unwrap + self.assertEqual(get_safe_filters('"ABC"'), "ABC") def test_get_safe_filters_passes_through_non_strings(self): self.assertEqual(get_safe_filters({"name": "ABC"}), {"name": "ABC"}) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 122368c1b9..d20a754114 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -904,7 +904,7 @@ def call(fn, *args, **kwargs): def get_safe_filters(filters): - if isinstance(filters, str) and filters and filters[0] in "{[": + if isinstance(filters, str) and filters and filters[0] in '{["': try: return orjson.loads(filters) except (TypeError, ValueError): From 8ade4ce27dfb4ff84a2b9fc2d89f7c44164fa91b Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:28:55 +0530 Subject: [PATCH 4/5] fix: preserve numeric-shaped docnames without losing JSON parsing of scalars --- frappe/tests/test_utils.py | 3 +++ frappe/utils/__init__.py | 17 ++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 2677f7d18f..579109d11c 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -366,6 +366,9 @@ class TestFilters(IntegrationTestCase): self.assertEqual(get_safe_filters('[["name", "=", "ABC"]]'), [["name", "=", "ABC"]]) # FrappeClient encodes scalar filters via frappe.as_json — must still unwrap self.assertEqual(get_safe_filters('"ABC"'), "ABC") + self.assertIsNone(get_safe_filters("null")) + self.assertIs(get_safe_filters("true"), True) + self.assertIs(get_safe_filters("false"), False) def test_get_safe_filters_passes_through_non_strings(self): self.assertEqual(get_safe_filters({"name": "ABC"}), {"name": "ABC"}) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index d20a754114..3f3b88b2f1 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -904,13 +904,16 @@ def call(fn, *args, **kwargs): def get_safe_filters(filters): - if isinstance(filters, str) and filters and filters[0] in '{["': - try: - return orjson.loads(filters) - except (TypeError, ValueError): - # filters are not passed, not json - pass - return filters + try: + parsed = orjson.loads(filters) + except (TypeError, ValueError): + # not a string, or not valid json + return filters + # numeric JSON is ambiguous: docnames like "3E002" parse as floats and + # would be corrupted by stringifying back, so keep the original string + if isinstance(parsed, int | float) and not isinstance(parsed, bool): + return filters + return parsed def create_batch(iterable: Iterable, size: int) -> Generator[Iterable]: From ec8b3d9187cefd2e6c3e80a833c0a3aa952e1417 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:34:14 +0530 Subject: [PATCH 5/5] test: shorten safe filters test names --- frappe/tests/test_client.py | 2 +- frappe/tests/test_utils.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/tests/test_client.py b/frappe/tests/test_client.py index a40e1ce5d7..6cdb7b0b9e 100644 --- a/frappe/tests/test_client.py +++ b/frappe/tests/test_client.py @@ -292,7 +292,7 @@ class TestClient(IntegrationTestCase): for doc in docs: frappe.delete_doc("Note", doc) - def test_get_value_with_scientific_notation_docname(self): + def test_get_value_scientific_notation_docname(self): from frappe.client import get_value tag = frappe.get_doc({"doctype": "Tag", "name": "3E002"}).insert(ignore_if_duplicate=True) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 579109d11c..9b8d160399 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -353,7 +353,7 @@ class TestFilters(IntegrationTestCase): link = get_link_to_report(name="ToDo", filters=filters) self.assertIn('creation=["between",["2024-01-01","2024-12-31"]]', link) - def test_get_safe_filters_preserves_scientific_notation_docnames(self): + def test_safe_filters_scientific_notation(self): self.assertEqual(get_safe_filters("3E002"), "3E002") self.assertEqual(get_safe_filters("1E5"), "1E5") self.assertEqual(get_safe_filters("2e10"), "2e10") @@ -361,7 +361,7 @@ class TestFilters(IntegrationTestCase): self.assertEqual(get_safe_filters("Infinity"), "Infinity") self.assertEqual(get_safe_filters("NaN"), "NaN") - def test_get_safe_filters_still_parses_json(self): + def test_safe_filters_json(self): self.assertEqual(get_safe_filters('{"name": "ABC"}'), {"name": "ABC"}) self.assertEqual(get_safe_filters('[["name", "=", "ABC"]]'), [["name", "=", "ABC"]]) # FrappeClient encodes scalar filters via frappe.as_json — must still unwrap @@ -370,7 +370,7 @@ class TestFilters(IntegrationTestCase): self.assertIs(get_safe_filters("true"), True) self.assertIs(get_safe_filters("false"), False) - def test_get_safe_filters_passes_through_non_strings(self): + def test_safe_filters_non_string(self): self.assertEqual(get_safe_filters({"name": "ABC"}), {"name": "ABC"}) self.assertEqual(get_safe_filters([["name", "=", "ABC"]]), [["name", "=", "ABC"]]) self.assertIsNone(get_safe_filters(None))