From 82aa6e6f6bae183cece62fe42528c270714483da Mon Sep 17 00:00:00 2001 From: Bastien Gerard Date: Sun, 16 Aug 2026 15:52:09 +0200 Subject: [PATCH 1/3] Fix minor stuff before 1.0 --- README.rst | 61 ++++++++++++++++++++++------------------ docs/changelog.rst | 17 ++++++----- mongoengine/__init__.py | 2 +- setup.cfg | 3 ++ tests/test_common.py | 20 +++++++++++++ tests/test_connection.py | 3 +- tests/utils.py | 2 +- 7 files changed, 68 insertions(+), 40 deletions(-) diff --git a/README.rst b/README.rst index 4d21b4c0c..3ea73d256 100644 --- a/README.rst +++ b/README.rst @@ -3,8 +3,8 @@ MongoEngine =========== :Info: MongoEngine is an ORM-like layer on top of PyMongo. :Repository: https://github.com/MongoEngine/mongoengine -:Author: Harry Marr (http://github.com/hmarr) -:Maintainer: Bastien Gerard (http://github.com/bagerard) +:Author: Harry Marr (https://github.com/hmarr) +:Maintainer: Bastien Gerard (https://github.com/bagerard) .. image:: https://github.com/MongoEngine/mongoengine/actions/workflows/github-actions.yml/badge.svg?branch=master :target: https://github.com/MongoEngine/mongoengine/actions @@ -13,23 +13,24 @@ MongoEngine :target: https://coveralls.io/github/MongoEngine/mongoengine?branch=master .. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/ambv/black + :target: https://github.com/psf/black .. image:: https://pepy.tech/badge/mongoengine/month :target: https://pepy.tech/project/mongoengine .. image:: https://img.shields.io/pypi/v/mongoengine.svg - :target: https://pypi.python.org/pypi/mongoengine + :target: https://pypi.org/project/mongoengine/ .. image:: https://readthedocs.org/projects/mongoengine-odm/badge/?version=latest - :target: https://readthedocs.org/projects/mongoengine-odm/builds/ + :target: https://mongoengine-odm.readthedocs.io/ + ⚠️ **Warning:** ``mongoengine.org`` is no longer controlled by the MongoEngine project and appears to be an expired domain takeover. The official MongoEngine project is maintained on `GitHub `_ and documented on -`ReadTheDocs `_. +`Read the Docs `_. About ===== @@ -44,31 +45,30 @@ Supported MongoDB Versions MongoEngine is currently tested against MongoDB v4.4, v5.0, v6.0, v7.0 and v8.0. Future versions should be supported as well, but aren't actively tested at the moment. Make sure to open an issue or submit a pull request if you -experience any problems with a more recent MongoDB versions. +experience any problems with more recent MongoDB versions. Installation ============ -We recommend the use of `virtualenv `_ and of -`pip `_. You can then use ``python -m pip install -U mongoengine``. -You may also have `setuptools `_ -and thus you can use ``easy_install -U mongoengine``. Another option is -`pipenv `_. You can then use ``pipenv install mongoengine`` -to both create the virtual environment and install the package. Otherwise, you can -download the source from `GitHub `_ and -run ``python setup.py install``. +MongoEngine requires Python 3.10 or newer. Install it from PyPI with: + +.. code-block:: console + + $ python -m pip install -U mongoengine -The support for Python2 was dropped with MongoEngine 0.20.0 +To install a source checkout, run ``python -m pip install .`` from the +repository root. Dependencies ============ -All of the dependencies can easily be installed via `python -m pip `_. -At the very least, you'll need these two packages to use MongoEngine: +MongoEngine requires: -- pymongo>=3.12 +- PyMongo >=3.12,<5.0 + +The following optional packages enable additional functionality: If you utilize a ``DateTimeField``, you might also use a more flexible date parser: -- dateutil>=2.1.0 +- python-dateutil >=2.1.0 If you need to use an ``ImageField`` or ``ImageGridFsProxy``: @@ -107,7 +107,7 @@ Some simple examples of what MongoEngine code looks like: >>> post1.save() # Create a link-based post - >>> post2 = LinkPost(title='MongoEngine Docs', url='hmarr.com/mongoengine') + >>> post2 = LinkPost(title='Example Docs', url='https://example.com/') >>> post2.tags = ['mongoengine', 'documentation'] >>> post2.save() @@ -120,7 +120,7 @@ Some simple examples of what MongoEngine code looks like: ... print('Link:', post.url) ... - # Count all blog posts and its subtypes + # Count all blog posts and their subtypes >>> BlogPost.objects.count() 2 >>> TextPost.objects.count() @@ -136,8 +136,13 @@ Some simple examples of what MongoEngine code looks like: Tests ===== -To run the test suite, ensure you are running a local instance of MongoDB on -the standard port and have ``pytest`` installed. Then, run ``pytest tests/``. +To run the test suite, ensure MongoDB is running on the standard port, then +install the package with its test dependencies and run pytest: + +.. code-block:: console + + $ python -m pip install -e ".[test]" + $ pytest tests/ To run the test suite on every supported Python and PyMongo version, you can use ``tox``. You'll need to make sure you have each supported Python version @@ -152,10 +157,10 @@ installed in your environment and then: Community ========= -- `MongoEngine Users mailing list - `_ -- `MongoEngine Developers mailing list - `_ +- Ask usage questions on `Stack Overflow + `_. +- Report confirmed bugs on `GitHub Issues + `_. Contributing ============ diff --git a/docs/changelog.rst b/docs/changelog.rst index b9135b218..ca9ad6c80 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,10 +7,9 @@ Changelog Development =========== - (Fill this out as you fix issues and develop your features). -- Add a warning that ``mongoengine.org`` is no longer controlled by the MongoEngine - project and appears to be an expired domain takeover. -- Fix querying GenericReferenceField with __in operator #2886 -- Fix Document.compare_indexes() not working correctly for text indexes on multiple fields #2612 + +Changes in 1.0.0 +=========== - Add support for transaction through run_in_transaction (kudos to juannyG for this) #2569 Some considerations: @@ -18,6 +17,10 @@ Development - make sure to read https://www.mongodb.com/docs/manual/core/transactions-in-applications/#callback-api-vs-core-api - run_in_transaction context manager relies on Pymongo coreAPI, it will retry automatically in case of ``UnknownTransactionCommitResult`` but not ``TransientTransactionError`` exceptions - Using .count() in a transaction will always use Collection.count_document (as estimated_document_count is not supported in transactions) +- Add a warning that ``mongoengine.org`` is no longer controlled by the MongoEngine + project and appears to be an expired domain takeover. +- Fix querying GenericReferenceField with __in operator #2886 +- Fix Document.compare_indexes() not working correctly for text indexes on multiple fields #2612 - BREAKING CHANGE: wrap _document_registry (normally not used by end users) with _DocumentRegistry which acts as a singleton to access the registry - Log a warning in case users creates multiple Document classes with the same name as it can lead to unexpected behavior #1778 - Fix use of $geoNear or $collStats in aggregate #2493 @@ -32,11 +35,7 @@ Development Changes in 0.29.3 ================= -- Remove mentions of mongoengine[dot]org has we no longer own it #2905 - -Changes in 0.29.2 -================= -- fixes relted with +- Remove mentions of mongoengine[dot]org as we no longer own it #2905 Changes in 0.29.1 ================= diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index bda28f1ee..f7c70e7b2 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -29,7 +29,7 @@ ) -VERSION = (0, 29, 3) +VERSION = (1, 0, 0) def get_version(): diff --git a/setup.cfg b/setup.cfg index aa965c8f8..cadc59a1b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,9 @@ max-complexity=47 # Limits the discovery to tests directory # avoids that it runs for instance the benchmark testpaths = tests +filterwarnings = + ignore:No uuidRepresentation is specified!:DeprecationWarning + ignore:Multiple Document classes named .* were registered:UserWarning [isort] known_first_party = mongoengine,tests diff --git a/tests/test_common.py b/tests/test_common.py index d4122863c..1c2c89e7e 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,6 +1,7 @@ import pytest from mongoengine import Document +from mongoengine.base import _DocumentRegistry, common as base_common from mongoengine.common import _import_class @@ -12,3 +13,22 @@ def test__import_class(self): def test__import_class_raise_if_not_known(self): with pytest.raises(ValueError): _import_class("UnknownClass") + + def test_document_registry_warns_for_same_class_name_from_different_modules( + self, monkeypatch + ): + monkeypatch.setattr(base_common, "_document_registry", {}) + first = type( + "DuplicateDocument", + (), + {"_class_name": "DuplicateDocument", "__module__": "first"}, + ) + second = type( + "DuplicateDocument", + (), + {"_class_name": "DuplicateDocument", "__module__": "second"}, + ) + + _DocumentRegistry.register(first) + with pytest.warns(UserWarning, match="Multiple Document classes named"): + _DocumentRegistry.register(second) diff --git a/tests/test_connection.py b/tests/test_connection.py index c7343539d..b41ef5826 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -719,7 +719,8 @@ def test_connect_uri_uuidrepresentation_set_both_arg_and_uri_arg_prevail(self): def test_connect_uri_uuidrepresentation_default_to_pythonlegacy(self): # To be changed soon to unspecified rand = random_str() - tmp_conn = connect(alias=rand, db=rand) + with pytest.warns(DeprecationWarning, match="No uuidRepresentation"): + tmp_conn = connect(alias=rand, db=rand) assert ( tmp_conn.options.codec_options.uuid_representation == pymongo.common._UUID_REPRESENTATIONS["pythonLegacy"] diff --git a/tests/utils.py b/tests/utils.py index 73623661b..5fc0a1c77 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -23,7 +23,7 @@ class MongoDBTestCase(unittest.TestCase): @classmethod def setUpClass(cls): disconnect_all() - cls._connection = connect(db=MONGO_TEST_DB) + cls._connection = connect(db=MONGO_TEST_DB, uuidRepresentation="pythonLegacy") cls._connection.drop_database(MONGO_TEST_DB) cls.db = get_db() From 51c3d1a181e0f1a68e35c748a7104ce10fc98263 Mon Sep 17 00:00:00 2001 From: Bastien Gerard Date: Sun, 16 Aug 2026 16:04:57 +0200 Subject: [PATCH 2/3] Fix warnings in test suites + utcnow ones --- docs/guide/defining-documents.rst | 10 +++++----- docs/guide/signals.rst | 7 +++---- mongoengine/common.py | 7 +++++++ mongoengine/fields.py | 6 ++++-- tests/document/test_instance.py | 5 +++-- tests/document/test_timeseries_collection.py | 11 ++++++----- tests/fields/test_complex_datetime_field.py | 7 ++++--- tests/fields/test_datetime_field.py | 18 ++++++++++++++++-- tests/fields/test_fields.py | 13 +++++++------ tests/queryset/test_queryset.py | 7 +++---- tests/test_common.py | 12 +++++++++++- 11 files changed, 69 insertions(+), 34 deletions(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 75e10d81e..4dcb53397 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -18,11 +18,11 @@ To define a schema for a document, create a class that inherits from objects** as class attributes to the document class:: from mongoengine import * - import datetime + from mongoengine.common import utcnow_naive class Page(Document): title = StringField(max_length=200, required=True) - date_modified = DateTimeField(default=datetime.datetime.utcnow) + date_modified = DateTimeField(default=utcnow_naive) As BSON (the binary format for storing data in mongodb) is order dependent, documents are serialized based on their field order. @@ -274,7 +274,7 @@ store; in this situation a :class:`~mongoengine.fields.DictField` is appropriate user = ReferenceField(User) answers = DictField() - survey_response = SurveyResponse(date=datetime.utcnow(), user=request.user) + survey_response = SurveyResponse(date=utcnow_naive(), user=request.user) response_form = ResponseForm(request.POST) survey_response.answers = response_form.cleaned_data() survey_response.save() @@ -689,7 +689,7 @@ collection after a given period. See the official documentation for more information. A common usecase might be session data:: class Session(Document): - created = DateTimeField(default=datetime.utcnow) + created = DateTimeField(default=utcnow_naive) meta = { 'indexes': [ {'fields': ['created'], 'expireAfterSeconds': 3600} @@ -812,7 +812,7 @@ the class name in every documents. When a document is loaded, MongoEngine checks it's :attr:`_cls` attribute and use that class to construct the instance.:: Page(title='a funky title').save() - DatedPage(title='another title', date=datetime.utcnow()).save() + DatedPage(title='another title', date=utcnow_naive()).save() print(Page.objects().count()) # 2 print(DatedPage.objects().count()) # 1 diff --git a/docs/guide/signals.rst b/docs/guide/signals.rst index e52146102..b49a787f5 100644 --- a/docs/guide/signals.rst +++ b/docs/guide/signals.rst @@ -74,13 +74,12 @@ Attaching Events After writing a handler function like the following:: import logging - from datetime import datetime - from mongoengine import * from mongoengine import signals + from mongoengine.common import utcnow_naive def update_modified(sender, document): - document.modified = datetime.utcnow() + document.modified = utcnow_naive() You attach the event handler to your :class:`~mongoengine.Document` or :class:`~mongoengine.EmbeddedDocument` subclass:: @@ -139,7 +138,7 @@ cleaner looking while still allowing manual execution of the callback:: @handler(signals.pre_save) def update_modified(sender, document): - document.modified = datetime.utcnow() + document.modified = utcnow_naive() @update_modified.apply class Record(Document): diff --git a/mongoengine/common.py b/mongoengine/common.py index 640384ec0..c1dad3958 100644 --- a/mongoengine/common.py +++ b/mongoengine/common.py @@ -1,7 +1,14 @@ +from datetime import datetime, timezone + _class_registry_cache = {} _field_list_cache = [] +def utcnow_naive() -> datetime: + """Return the current UTC datetime without timezone information.""" + return datetime.now(timezone.utc).replace(tzinfo=None) + + def _import_class(cls_name): """Cache mechanism for imports. diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 980098dfb..bfcfd01ce 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -527,7 +527,8 @@ class DateTimeField(BaseField): installed you can utilise it to convert varying types of date formats into valid python datetime objects. - Note: To default the field to the current datetime, use: DateTimeField(default=datetime.utcnow) + Note: To default the field to the current datetime, use: + DateTimeField(default=utcnow_naive), imported from mongoengine.common. Note: Microseconds are rounded to the nearest millisecond. Pre UTC microsecond support is effectively broken. @@ -631,7 +632,8 @@ class ComplexDateTimeField(StringField): The `,` as the separator can be easily modified by passing the `separator` keyword when initializing the field. - Note: To default the field to the current datetime, use: DateTimeField(default=datetime.utcnow) + Note: To default the field to the current datetime, use: + DateTimeField(default=utcnow_naive), imported from mongoengine.common. """ def __init__(self, separator=",", **kwargs): diff --git a/tests/document/test_instance.py b/tests/document/test_instance.py index b7bf2179c..c501f46de 100644 --- a/tests/document/test_instance.py +++ b/tests/document/test_instance.py @@ -15,6 +15,7 @@ from mongoengine import * from mongoengine import signals from mongoengine.base import _DocumentRegistry +from mongoengine.common import utcnow_naive from mongoengine.connection import get_db from mongoengine.context_managers import query_counter, switch_db from mongoengine.errors import ( @@ -3540,7 +3541,7 @@ class Test(Document): def test_default_values_dont_get_override_upon_save_when_only_is_used(self): class Person(Document): - created_on = DateTimeField(default=lambda: datetime.utcnow()) + created_on = DateTimeField(default=utcnow_naive) name = StringField() p = Person(name="alon") @@ -3554,7 +3555,7 @@ class Person(Document): assert orig_created_on == p3.created_on class Person(Document): - created_on = DateTimeField(default=lambda: datetime.utcnow()) + created_on = DateTimeField(default=utcnow_naive) name = StringField() height = IntField(default=189) diff --git a/tests/document/test_timeseries_collection.py b/tests/document/test_timeseries_collection.py index 46d209b6a..87cc8adc8 100644 --- a/tests/document/test_timeseries_collection.py +++ b/tests/document/test_timeseries_collection.py @@ -1,6 +1,6 @@ import time import unittest -from datetime import datetime, timedelta +from datetime import timedelta from mongoengine import ( DateTimeField, @@ -10,6 +10,7 @@ connect, get_db, ) +from mongoengine.common import utcnow_naive from mongoengine.connection import disconnect from tests.utils import requires_mongodb_gte_50 @@ -84,7 +85,7 @@ def test_insert_document_into_timeseries_collection(self): assert collection_name in self.db.list_collection_names() # Insert a document and ensure it was inserted - self.SensorData(timestamp=datetime.utcnow(), temperature=23.4).save() + self.SensorData(timestamp=utcnow_naive(), temperature=23.4).save() assert collection.count_documents({}) == 1 @requires_mongodb_gte_50 @@ -98,7 +99,7 @@ def test_timeseries_expiration(self): assert options.get("timeseries", {}) is not None assert options["expireAfterSeconds"] == 1 - self.SensorData(timestamp=datetime.utcnow(), temperature=23.4).save() + self.SensorData(timestamp=utcnow_naive(), temperature=23.4).save() assert collection.count_documents({}) == 1 @@ -144,7 +145,7 @@ def test_timeseries_data_insertion_order(self): self.SensorData._get_collection() # Insert documents out of order - now = datetime.utcnow() + now = utcnow_naive() self.SensorData(timestamp=now, temperature=23.4).save() self.SensorData(timestamp=now - timedelta(seconds=5), temperature=22.0).save() self.SensorData(timestamp=now + timedelta(seconds=5), temperature=24.0).save() @@ -164,7 +165,7 @@ def test_timeseries_query_by_time_range(self): self.SensorData._get_collection_name() self.SensorData._get_collection() - now = datetime.utcnow() + now = utcnow_naive() self.SensorData(timestamp=now - timedelta(seconds=10), temperature=22.0).save() self.SensorData(timestamp=now - timedelta(seconds=5), temperature=23.0).save() self.SensorData(timestamp=now, temperature=24.0).save() diff --git a/tests/fields/test_complex_datetime_field.py b/tests/fields/test_complex_datetime_field.py index 205fb22f6..ba9cb5b8a 100644 --- a/tests/fields/test_complex_datetime_field.py +++ b/tests/fields/test_complex_datetime_field.py @@ -6,6 +6,7 @@ import pytest from mongoengine import * +from mongoengine.common import utcnow_naive from tests.utils import MongoDBTestCase @@ -163,7 +164,7 @@ class Log(Document): assert fetched_log.timestamp is None def test_default_static_value(self): - NOW = datetime.datetime.utcnow() + NOW = utcnow_naive() class Log(Document): timestamp = ComplexDateTimeField(default=NOW) @@ -178,10 +179,10 @@ class Log(Document): assert fetched_log.timestamp == NOW def test_default_callable(self): - NOW = datetime.datetime.utcnow() + NOW = utcnow_naive() class Log(Document): - timestamp = ComplexDateTimeField(default=datetime.datetime.utcnow) + timestamp = ComplexDateTimeField(default=utcnow_naive) Log.drop_collection() diff --git a/tests/fields/test_datetime_field.py b/tests/fields/test_datetime_field.py index d04f39b04..5197a8890 100644 --- a/tests/fields/test_datetime_field.py +++ b/tests/fields/test_datetime_field.py @@ -4,6 +4,7 @@ from mongoengine import * from mongoengine import connection +from mongoengine.common import utcnow_naive from tests.utils import MongoDBTestCase, get_as_pymongo try: @@ -45,9 +46,9 @@ def test_default_value_utcnow(self): """ class Person(Document): - created = DateTimeField(default=dt.datetime.utcnow) + created = DateTimeField(default=utcnow_naive) - utcnow = dt.datetime.utcnow() + utcnow = utcnow_naive() person = Person() person.validate() person_created_t0 = person.created @@ -55,6 +56,19 @@ class Person(Document): assert person_created_t0 == person.created # make sure it does not change assert person._data["created"] == person.created + def test_aware_datetime_is_stored_as_naive_utc(self): + class LogEntry(Document): + time = DateTimeField() + + LogEntry.drop_collection() + aware_datetime = dt.datetime( + 2026, 8, 16, 12, tzinfo=dt.timezone(dt.timedelta(hours=2)) + ) + + LogEntry(time=aware_datetime).save() + + assert LogEntry.objects.get().time == dt.datetime(2026, 8, 16, 10) + def test_set_using_callable(self): # Weird feature but it's there for a while so let's make sure we don't break it class Person(Document): diff --git a/tests/fields/test_fields.py b/tests/fields/test_fields.py index c58f8a842..f1bab2545 100644 --- a/tests/fields/test_fields.py +++ b/tests/fields/test_fields.py @@ -35,6 +35,7 @@ ) from mongoengine.base import BaseField, EmbeddedDocumentList from mongoengine.base.fields import _no_dereference_for_fields +from mongoengine.common import utcnow_naive from mongoengine.errors import DeprecatedError from tests.utils import MongoDBTestCase @@ -89,7 +90,7 @@ class Person(Document): name = StringField() age = IntField(default=30, required=False) userid = StringField(default=lambda: "test", required=True) - created = DateTimeField(default=datetime.datetime.utcnow) + created = DateTimeField(default=utcnow_naive) day = DateField(default=datetime.date.today) person = Person(name="Ross") @@ -169,7 +170,7 @@ class Person(Document): name = StringField() age = IntField(default=30, required=False) userid = StringField(default=lambda: "test", required=True) - created = DateTimeField(default=datetime.datetime.utcnow) + created = DateTimeField(default=utcnow_naive) # Trying setting values to None person = Person(name=None, age=None, userid=None, created=None) @@ -203,7 +204,7 @@ class Person(Document): name = StringField() age = IntField(default=30, required=False) userid = StringField(default=lambda: "test", required=True) - created = DateTimeField(default=datetime.datetime.utcnow) + created = DateTimeField(default=utcnow_naive) person = Person() person.name = None @@ -271,7 +272,7 @@ class Person(Document): name = StringField() age = IntField(default=30, required=False) userid = StringField(default=lambda: "test", required=True) - created = DateTimeField(default=datetime.datetime.utcnow) + created = DateTimeField(default=utcnow_naive) person = Person( name="Ross", @@ -336,7 +337,7 @@ class HandleNoneFields(Document): doc.str_fld = "spam ham egg" doc.int_fld = 42 doc.flt_fld = 4.2 - doc.com_dt_fld = datetime.datetime.utcnow() + doc.com_dt_fld = utcnow_naive() doc.save() res = HandleNoneFields.objects(id=doc.id).update( @@ -372,7 +373,7 @@ class HandleNoneFields(Document): doc.str_fld = "spam ham egg" doc.int_fld = 42 doc.flt_fld = 4.2 - doc.comp_dt_fld = datetime.datetime.utcnow() + doc.comp_dt_fld = utcnow_naive() doc.save() # Unset all the fields diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index d9a9d8842..14895dd1b 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -10,6 +10,7 @@ from pymongo.results import UpdateResult from mongoengine import * +from mongoengine.common import utcnow_naive from mongoengine.connection import get_db from mongoengine.context_managers import query_counter, switch_db from mongoengine.errors import InvalidQueryError @@ -1553,9 +1554,7 @@ class BlogPost(Document): meta = {"ordering": ["-published_date"]} - BlogPost.objects.create( - title="whatever", published_date=datetime.datetime.utcnow() - ) + BlogPost.objects.create(title="whatever", published_date=utcnow_naive()) with db_ops_tracker() as q: BlogPost.objects.get(title="whatever") @@ -3114,7 +3113,7 @@ class Link(Document): Link.drop_collection() - now = datetime.datetime.utcnow() + now = utcnow_naive() # Note: Test data taken from a custom Reddit homepage on # Fri, 12 Feb 2010 14:36:00 -0600. Link ordering should diff --git a/tests/test_common.py b/tests/test_common.py index 1c2c89e7e..25f943414 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,11 +1,21 @@ +from datetime import datetime, timezone + import pytest -from mongoengine import Document +from mongoengine import Document, common from mongoengine.base import _DocumentRegistry, common as base_common from mongoengine.common import _import_class class TestCommon: + def test_utcnow_naive_returns_current_naive_utc_datetime(self): + before = datetime.now(timezone.utc).replace(tzinfo=None) + result = common.utcnow_naive() + after = datetime.now(timezone.utc).replace(tzinfo=None) + + assert result.tzinfo is None + assert before <= result <= after + def test__import_class(self): doc_cls = _import_class("Document") assert doc_cls is Document From 628e01f89d934f63b1bbc56e0b7378a03d8a56db Mon Sep 17 00:00:00 2001 From: Bastien Gerard Date: Wed, 19 Aug 2026 18:32:14 +0200 Subject: [PATCH 3/3] Improve situation with utcnow --- .gitignore | 1 + docs/guide/defining-documents.rst | 21 +++++++++++++++----- docs/guide/signals.rst | 7 ++++--- mongoengine/common.py | 7 ------- mongoengine/fields.py | 15 ++++++++++---- tests/document/test_instance.py | 7 +++---- tests/document/test_timeseries_collection.py | 11 +++++----- tests/fields/test_complex_datetime_field.py | 3 +-- tests/fields/test_datetime_field.py | 5 ++--- tests/fields/test_fields.py | 19 ++++++++++++------ tests/queryset/test_queryset.py | 8 +++++--- tests/test_common.py | 12 +---------- tests/test_utils.py | 11 ++++++++++ tests/utils.py | 6 ++++++ 14 files changed, 79 insertions(+), 54 deletions(-) diff --git a/.gitignore b/.gitignore index 16633baef..e9da9d690 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ env/ htmlcov/ venv venv3 +.python-version diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 4dcb53397..d1af1c53d 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -17,12 +17,15 @@ To define a schema for a document, create a class that inherits from :class:`~mongoengine.Document`. Fields are specified by adding **field objects** as class attributes to the document class:: + from datetime import datetime, timezone + from mongoengine import * - from mongoengine.common import utcnow_naive class Page(Document): title = StringField(max_length=200, required=True) - date_modified = DateTimeField(default=utcnow_naive) + date_modified = DateTimeField( + default=lambda: datetime.now(timezone.utc) + ) As BSON (the binary format for storing data in mongodb) is order dependent, documents are serialized based on their field order. @@ -274,7 +277,10 @@ store; in this situation a :class:`~mongoengine.fields.DictField` is appropriate user = ReferenceField(User) answers = DictField() - survey_response = SurveyResponse(date=utcnow_naive(), user=request.user) + survey_response = SurveyResponse( + date=datetime.now(timezone.utc), + user=request.user, + ) response_form = ResponseForm(request.POST) survey_response.answers = response_form.cleaned_data() survey_response.save() @@ -689,7 +695,9 @@ collection after a given period. See the official documentation for more information. A common usecase might be session data:: class Session(Document): - created = DateTimeField(default=utcnow_naive) + created = DateTimeField( + default=lambda: datetime.now(timezone.utc) + ) meta = { 'indexes': [ {'fields': ['created'], 'expireAfterSeconds': 3600} @@ -812,7 +820,10 @@ the class name in every documents. When a document is loaded, MongoEngine checks it's :attr:`_cls` attribute and use that class to construct the instance.:: Page(title='a funky title').save() - DatedPage(title='another title', date=utcnow_naive()).save() + DatedPage( + title='another title', + date=datetime.now(timezone.utc), + ).save() print(Page.objects().count()) # 2 print(DatedPage.objects().count()) # 1 diff --git a/docs/guide/signals.rst b/docs/guide/signals.rst index b49a787f5..166c88b94 100644 --- a/docs/guide/signals.rst +++ b/docs/guide/signals.rst @@ -74,12 +74,13 @@ Attaching Events After writing a handler function like the following:: import logging + from datetime import datetime, timezone + from mongoengine import * from mongoengine import signals - from mongoengine.common import utcnow_naive def update_modified(sender, document): - document.modified = utcnow_naive() + document.modified = datetime.now(timezone.utc) You attach the event handler to your :class:`~mongoengine.Document` or :class:`~mongoengine.EmbeddedDocument` subclass:: @@ -138,7 +139,7 @@ cleaner looking while still allowing manual execution of the callback:: @handler(signals.pre_save) def update_modified(sender, document): - document.modified = utcnow_naive() + document.modified = datetime.now(timezone.utc) @update_modified.apply class Record(Document): diff --git a/mongoengine/common.py b/mongoengine/common.py index c1dad3958..640384ec0 100644 --- a/mongoengine/common.py +++ b/mongoengine/common.py @@ -1,14 +1,7 @@ -from datetime import datetime, timezone - _class_registry_cache = {} _field_list_cache = [] -def utcnow_naive() -> datetime: - """Return the current UTC datetime without timezone information.""" - return datetime.now(timezone.utc).replace(tzinfo=None) - - def _import_class(cls_name): """Cache mechanism for imports. diff --git a/mongoengine/fields.py b/mongoengine/fields.py index bfcfd01ce..0f5ee5402 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -527,8 +527,15 @@ class DateTimeField(BaseField): installed you can utilise it to convert varying types of date formats into valid python datetime objects. - Note: To default the field to the current datetime, use: - DateTimeField(default=utcnow_naive), imported from mongoengine.common. + Note: To default the field to the current datetime, use + ``DateTimeField(default=lambda: datetime.now(timezone.utc))`` + + .. warning:: + + PyMongo converts timezone-aware datetimes to UTC before storing them. + The original timezone and UTC offset are not preserved. By default, + datetimes are returned as naive UTC values; use ``tz_aware=True`` on + the connection to receive timezone-aware UTC values. Note: Microseconds are rounded to the nearest millisecond. Pre UTC microsecond support is effectively broken. @@ -632,8 +639,8 @@ class ComplexDateTimeField(StringField): The `,` as the separator can be easily modified by passing the `separator` keyword when initializing the field. - Note: To default the field to the current datetime, use: - DateTimeField(default=utcnow_naive), imported from mongoengine.common. + Note: To default the field to the current datetime, use + ``DateTimeField(default=lambda: datetime.now(timezone.utc))`` """ def __init__(self, separator=",", **kwargs): diff --git a/tests/document/test_instance.py b/tests/document/test_instance.py index c501f46de..5428ad45f 100644 --- a/tests/document/test_instance.py +++ b/tests/document/test_instance.py @@ -4,7 +4,7 @@ import unittest import uuid import weakref -from datetime import datetime +from datetime import datetime, timezone from unittest.mock import Mock import bson @@ -15,7 +15,6 @@ from mongoengine import * from mongoengine import signals from mongoengine.base import _DocumentRegistry -from mongoengine.common import utcnow_naive from mongoengine.connection import get_db from mongoengine.context_managers import query_counter, switch_db from mongoengine.errors import ( @@ -3541,7 +3540,7 @@ class Test(Document): def test_default_values_dont_get_override_upon_save_when_only_is_used(self): class Person(Document): - created_on = DateTimeField(default=utcnow_naive) + created_on = DateTimeField(default=lambda: datetime.now(timezone.utc)) name = StringField() p = Person(name="alon") @@ -3555,7 +3554,7 @@ class Person(Document): assert orig_created_on == p3.created_on class Person(Document): - created_on = DateTimeField(default=utcnow_naive) + created_on = DateTimeField(default=lambda: datetime.now(timezone.utc)) name = StringField() height = IntField(default=189) diff --git a/tests/document/test_timeseries_collection.py b/tests/document/test_timeseries_collection.py index 87cc8adc8..6422c5d98 100644 --- a/tests/document/test_timeseries_collection.py +++ b/tests/document/test_timeseries_collection.py @@ -1,6 +1,6 @@ import time import unittest -from datetime import timedelta +from datetime import datetime, timedelta, timezone from mongoengine import ( DateTimeField, @@ -10,7 +10,6 @@ connect, get_db, ) -from mongoengine.common import utcnow_naive from mongoengine.connection import disconnect from tests.utils import requires_mongodb_gte_50 @@ -85,7 +84,7 @@ def test_insert_document_into_timeseries_collection(self): assert collection_name in self.db.list_collection_names() # Insert a document and ensure it was inserted - self.SensorData(timestamp=utcnow_naive(), temperature=23.4).save() + self.SensorData(timestamp=datetime.now(timezone.utc), temperature=23.4).save() assert collection.count_documents({}) == 1 @requires_mongodb_gte_50 @@ -99,7 +98,7 @@ def test_timeseries_expiration(self): assert options.get("timeseries", {}) is not None assert options["expireAfterSeconds"] == 1 - self.SensorData(timestamp=utcnow_naive(), temperature=23.4).save() + self.SensorData(timestamp=datetime.now(timezone.utc), temperature=23.4).save() assert collection.count_documents({}) == 1 @@ -145,7 +144,7 @@ def test_timeseries_data_insertion_order(self): self.SensorData._get_collection() # Insert documents out of order - now = utcnow_naive() + now = datetime.now(timezone.utc) self.SensorData(timestamp=now, temperature=23.4).save() self.SensorData(timestamp=now - timedelta(seconds=5), temperature=22.0).save() self.SensorData(timestamp=now + timedelta(seconds=5), temperature=24.0).save() @@ -165,7 +164,7 @@ def test_timeseries_query_by_time_range(self): self.SensorData._get_collection_name() self.SensorData._get_collection() - now = utcnow_naive() + now = datetime.now(timezone.utc) self.SensorData(timestamp=now - timedelta(seconds=10), temperature=22.0).save() self.SensorData(timestamp=now - timedelta(seconds=5), temperature=23.0).save() self.SensorData(timestamp=now, temperature=24.0).save() diff --git a/tests/fields/test_complex_datetime_field.py b/tests/fields/test_complex_datetime_field.py index ba9cb5b8a..e248981b7 100644 --- a/tests/fields/test_complex_datetime_field.py +++ b/tests/fields/test_complex_datetime_field.py @@ -6,8 +6,7 @@ import pytest from mongoengine import * -from mongoengine.common import utcnow_naive -from tests.utils import MongoDBTestCase +from tests.utils import MongoDBTestCase, utcnow_naive class ComplexDateTimeFieldTest(MongoDBTestCase): diff --git a/tests/fields/test_datetime_field.py b/tests/fields/test_datetime_field.py index 5197a8890..b8d227912 100644 --- a/tests/fields/test_datetime_field.py +++ b/tests/fields/test_datetime_field.py @@ -4,7 +4,6 @@ from mongoengine import * from mongoengine import connection -from mongoengine.common import utcnow_naive from tests.utils import MongoDBTestCase, get_as_pymongo try: @@ -46,9 +45,9 @@ def test_default_value_utcnow(self): """ class Person(Document): - created = DateTimeField(default=utcnow_naive) + created = DateTimeField(default=lambda: dt.datetime.now(dt.timezone.utc)) - utcnow = utcnow_naive() + utcnow = dt.datetime.now(dt.timezone.utc) person = Person() person.validate() person_created_t0 = person.created diff --git a/tests/fields/test_fields.py b/tests/fields/test_fields.py index f1bab2545..e6f0f7590 100644 --- a/tests/fields/test_fields.py +++ b/tests/fields/test_fields.py @@ -35,9 +35,8 @@ ) from mongoengine.base import BaseField, EmbeddedDocumentList from mongoengine.base.fields import _no_dereference_for_fields -from mongoengine.common import utcnow_naive from mongoengine.errors import DeprecatedError -from tests.utils import MongoDBTestCase +from tests.utils import MongoDBTestCase, utcnow_naive class TestField(MongoDBTestCase): @@ -90,7 +89,9 @@ class Person(Document): name = StringField() age = IntField(default=30, required=False) userid = StringField(default=lambda: "test", required=True) - created = DateTimeField(default=utcnow_naive) + created = DateTimeField( + default=lambda: datetime.datetime.now(datetime.timezone.utc) + ) day = DateField(default=datetime.date.today) person = Person(name="Ross") @@ -170,7 +171,9 @@ class Person(Document): name = StringField() age = IntField(default=30, required=False) userid = StringField(default=lambda: "test", required=True) - created = DateTimeField(default=utcnow_naive) + created = DateTimeField( + default=lambda: datetime.datetime.now(datetime.timezone.utc) + ) # Trying setting values to None person = Person(name=None, age=None, userid=None, created=None) @@ -204,7 +207,9 @@ class Person(Document): name = StringField() age = IntField(default=30, required=False) userid = StringField(default=lambda: "test", required=True) - created = DateTimeField(default=utcnow_naive) + created = DateTimeField( + default=lambda: datetime.datetime.now(datetime.timezone.utc) + ) person = Person() person.name = None @@ -272,7 +277,9 @@ class Person(Document): name = StringField() age = IntField(default=30, required=False) userid = StringField(default=lambda: "test", required=True) - created = DateTimeField(default=utcnow_naive) + created = DateTimeField( + default=lambda: datetime.datetime.now(datetime.timezone.utc) + ) person = Person( name="Ross", diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index 14895dd1b..d64384670 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -10,7 +10,6 @@ from pymongo.results import UpdateResult from mongoengine import * -from mongoengine.common import utcnow_naive from mongoengine.connection import get_db from mongoengine.context_managers import query_counter, switch_db from mongoengine.errors import InvalidQueryError @@ -1554,7 +1553,10 @@ class BlogPost(Document): meta = {"ordering": ["-published_date"]} - BlogPost.objects.create(title="whatever", published_date=utcnow_naive()) + BlogPost.objects.create( + title="whatever", + published_date=datetime.datetime.now(datetime.timezone.utc), + ) with db_ops_tracker() as q: BlogPost.objects.get(title="whatever") @@ -3113,7 +3115,7 @@ class Link(Document): Link.drop_collection() - now = utcnow_naive() + now = datetime.datetime.now(datetime.timezone.utc) # Note: Test data taken from a custom Reddit homepage on # Fri, 12 Feb 2010 14:36:00 -0600. Link ordering should diff --git a/tests/test_common.py b/tests/test_common.py index 25f943414..1c2c89e7e 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,21 +1,11 @@ -from datetime import datetime, timezone - import pytest -from mongoengine import Document, common +from mongoengine import Document from mongoengine.base import _DocumentRegistry, common as base_common from mongoengine.common import _import_class class TestCommon: - def test_utcnow_naive_returns_current_naive_utc_datetime(self): - before = datetime.now(timezone.utc).replace(tzinfo=None) - result = common.utcnow_naive() - after = datetime.now(timezone.utc).replace(tzinfo=None) - - assert result.tzinfo is None - assert before <= result <= after - def test__import_class(self): doc_cls = _import_class("Document") assert doc_cls is Document diff --git a/tests/test_utils.py b/tests/test_utils.py index a97645aec..f3b4c91d0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,12 +1,23 @@ import re +from datetime import datetime, timezone import pytest from mongoengine.base.utils import LazyRegexCompiler +from tests.utils import utcnow_naive signal_output = [] +def test_utcnow_naive__called__returns_current_naive_utc_datetime(): + before = datetime.now(timezone.utc).replace(tzinfo=None) + result = utcnow_naive() + after = datetime.now(timezone.utc).replace(tzinfo=None) + + assert result.tzinfo is None + assert before <= result <= after + + class TestLazyRegexCompiler: def test_lazy_regex_compiler_verify_laziness_of_descriptor(self): class UserEmail: diff --git a/tests/utils.py b/tests/utils.py index 5fc0a1c77..daee1395e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,7 @@ import functools import operator import unittest +from datetime import datetime, timezone import pymongo import pytest @@ -15,6 +16,11 @@ MONGO_TEST_DB = "mongoenginetest" # standard name for the test database +def utcnow_naive() -> datetime: + """Return the current UTC datetime without timezone information.""" + return datetime.now(timezone.utc).replace(tzinfo=None) + + class MongoDBTestCase(unittest.TestCase): """Base class for tests that need a mongodb connection It ensures that the db is clean at the beginning and dropped at the end automatically