From 4c6b75d3c9cea30a548aa8942e20ae507db53834 Mon Sep 17 00:00:00 2001 From: gersmann Date: Fri, 2 Jan 2026 12:12:16 +0100 Subject: [PATCH 1/2] feat: support timezones in type coercion --- mgqpy/utils.py | 19 +++++++++++++------ tests/test_python_types.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/mgqpy/utils.py b/mgqpy/utils.py index b629d7a..9156df3 100644 --- a/mgqpy/utils.py +++ b/mgqpy/utils.py @@ -17,10 +17,14 @@ def coerce(a: Any, b: Any) -> tuple[Any, Any]: match (a, b): # one is date/datetime, other is str - case (datetime.date() | datetime.datetime(), str()): - return a, _try_date(b, type(a)) - case (str(), datetime.date() | datetime.datetime()): - return _try_date(a, type(b)), b + case (datetime.datetime() as dt, str()): + return dt, _try_date(b, datetime.datetime, tzinfo=dt.tzinfo) + case (datetime.date() as d, str()): + return d, _try_date(b, datetime.date) + case (str(), datetime.datetime() as dt): + return _try_date(a, datetime.datetime, tzinfo=dt.tzinfo), dt + case (str(), datetime.date() as d): + return _try_date(a, datetime.date), d # one is decimal case (decimal.Decimal(), _): @@ -40,12 +44,15 @@ def coerce(a: Any, b: Any) -> tuple[Any, Any]: # Datetime ↔︎ ISO 8601 string -def _try_date(s: str, target): +def _try_date(s: str, target, *, tzinfo: datetime.tzinfo | None = None): """Attempt to convert *val* to target date or datetime""" try: if target is datetime.date: return datetime.date.fromisoformat(s) - return datetime.datetime.fromisoformat(s) + dt = datetime.datetime.fromisoformat(s) + if tzinfo is not None and dt.tzinfo is None: + return dt.replace(tzinfo=tzinfo) + return dt except ValueError: # Not a valid ISO format – leave as string so normal # comparison semantics apply (and tests expecting a failure diff --git a/tests/test_python_types.py b/tests/test_python_types.py index 1a4c56f..65cc98e 100644 --- a/tests/test_python_types.py +++ b/tests/test_python_types.py @@ -27,6 +27,16 @@ {"ts": {"$eq": "2025-07-31T12:00:00"}}, True, ), + ( + {"ts": _dt.datetime(2025, 7, 31, 0, 0, 0)}, + {"ts": {"$eq": "2025-07-31"}}, + True, + ), + ( + {"ts": _dt.datetime(2025, 7, 31, 12, 0, 0)}, + {"ts": {"$gt": "2025-07-31"}}, + True, + ), ( {"ts": _dt.datetime(2025, 7, 31, 12, 0, 0)}, {"ts": {"$gt": "2025-07-30T23:59:59"}}, @@ -53,6 +63,7 @@ def test_datetime_with_timezone(): assert Query({"ts": {"$eq": "2025-07-31T10:00:00+00:00"}}).test({"ts": aware}) assert Query({"ts": {"$eq": aware}}).test({"ts": "2025-07-31T10:00:00+00:00"}) + assert Query({"ts": {"$gt": "2025-07-31"}}).test({"ts": aware}) @pytest.fixture() From cf1b69c020d905a05463f59ce3f4868e70ceedd5 Mon Sep 17 00:00:00 2001 From: gersmann Date: Fri, 2 Jan 2026 13:26:56 +0100 Subject: [PATCH 2/2] chore: coverage --- tests/test_python_types.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_python_types.py b/tests/test_python_types.py index 65cc98e..67fbb42 100644 --- a/tests/test_python_types.py +++ b/tests/test_python_types.py @@ -85,6 +85,10 @@ def fruit_basket_dict(): {"fruits.0.harvested": {"$eq": "2024-08-01"}}, True, ), + ( + {"fruits.0.harvested": {"$eq": _dt.date(2024, 8, 1)}}, + True, + ), ( {"fruits.1.type": {"$eq": "aggregate"}}, True, @@ -107,6 +111,12 @@ def test_date_coercion(fruit_basket_dict, query, expected): assert Query(query).test(fruit_basket_dict) is expected +def test_date_filter_string_doc(): + assert Query({"harvested": {"$eq": _dt.date(2024, 8, 1)}}).test( + {"harvested": "2024-08-01"} + ) + + @pytest.mark.parametrize( "doc, query, expected", [