Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ A simple synchronous example:

class BlogPost(Document):
title = StringField(required=True, max_length=200)
posted = DateTimeField(default=datetime.datetime.utcnow)
posted = DateTimeField(default=lambda: datetime.datetime.now(datetime.UTC))
tags = ListField(StringField(max_length=50))

post = BlogPost(
Expand Down
2 changes: 1 addition & 1 deletion docs/apireference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Fields
.. autoclass:: mongoengine.fields.BooleanField
.. autoclass:: mongoengine.fields.DateTimeField
.. autoclass:: mongoengine.fields.ComplexDateTimeField
.. autoclass:: mongoengine.fields.ZonedDateTimeField
.. autoclass:: mongoengine.fields.AwareDateTimeField
.. autoclass:: mongoengine.fields.EmbeddedDocumentField
.. autoclass:: mongoengine.fields.GenericEmbeddedDocumentField
.. autoclass:: mongoengine.fields.DynamicField
Expand Down
2 changes: 1 addition & 1 deletion docs/code/tumblelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class LinkPost(Post):


async def run_async_tumblelog():
await async_connect("tumblelog")
async_connect("tumblelog")

await Post.adrop_collection()

Expand Down
10 changes: 5 additions & 5 deletions docs/guide/connecting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function. The first argument is the name of the database to connect to::
The asynchronous alternative is :func:`~mongoengine.async_connect`::

from mongoengine import async_connect
await async_connect('project1')
async_connect('project1')

By default, MongoEngine assumes that the :program:`mongod` instance is running
on **localhost** on port **27017**.
Expand Down Expand Up @@ -55,7 +55,7 @@ The asynchronous alternative is as follows::

# Connects to 'my_db' database by authenticating
# with given credentials against that same database
await async_connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db?authSource=my_db")
async_connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db?authSource=my_db")

The URI string can also be used to configure advanced parameters like ssl, replicaSet, etc. For more
information or example about URI string, you can refer to the `official doc <https://www.mongodb.com/docs/manual/reference/connection-string/>`_::
Expand All @@ -79,7 +79,7 @@ and :attr:`authentication_source` arguments should be provided::

The asynchronous alternative is as follows::

await async_connect('my_db', username='my_user', password='my_password', authentication_source='admin')
async_connect('my_db', username='my_user', password='my_password', authentication_source='admin')

The set of attributes that :func:`~mongoengine.connect` recognizes includes but is not limited to:
:attr:`host`, :attr:`port`, :attr:`read_preference`, :attr:`username`, :attr:`password`, :attr:`authentication_source`, :attr:`authentication_mechanism`,
Expand Down Expand Up @@ -171,11 +171,11 @@ connection globally::
The asynchronous alternative is :func:`~mongoengine.async_disconnect`::

from mongoengine import async_connect, async_disconnect
await async_connect('a_db', alias='db1')
async_connect('a_db', alias='db1')

await async_disconnect(alias='db1')

await async_connect('another_db', alias='db1')
async_connect('another_db', alias='db1')

.. note:: Calling :func:`~mongoengine.disconnect` without argument
will disconnect the "default" connection
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/defining-documents.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ objects** as class attributes to the document class::

class Page(Document):
title = StringField(max_length=200, required=True)
date_modified = DateTimeField(default=datetime.datetime.utcnow)
date_modified = DateTimeField(default=lambda: datetime.datetime.now(datetime.UTC))

As BSON (the binary format for storing data in mongodb) is order dependent,
documents are serialized based on their field order.
Expand Down Expand Up @@ -82,7 +82,7 @@ are as follows:
* :class:`~mongoengine.fields.BooleanField`
* :class:`~mongoengine.fields.ComplexDateTimeField`
* :class:`~mongoengine.fields.DateTimeField`
* :class:`~mongoengine.fields.ZonedDateTimeField`
* :class:`~mongoengine.fields.AwareDateTimeField`
* :class:`~mongoengine.fields.DecimalField`
* :class:`~mongoengine.fields.DictField`
* :class:`~mongoengine.fields.DynamicField`
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/logging-monitoring.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ The following snippet provides a basic logging of all command events:
# The asynchronous alternative is as follows:

async def async_logging_example():
await async_connect()
async_connect()

log.info('GO ASYNC!')

Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The asynchronous alternative is :func:`~mongoengine.async_connect`::

from mongoengine import async_connect

await async_connect('tumblelog')
async_connect('tumblelog')

There are lots of options for connecting to MongoDB, for more information about
them see the :ref:`guide-connecting` guide.
Expand Down
27 changes: 11 additions & 16 deletions mongoengine/asynchronous/connection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pymongo import AsyncMongoClient, ReadPreference
from pymongo.asynchronous import uri_parser
from pymongo.synchronous import uri_parser
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.common import _UUID_REPRESENTATIONS
from pymongo.driver_info import DriverInfo
Expand Down Expand Up @@ -31,7 +31,7 @@
_dbs = {}


async def _async_get_connection_settings(
def _async_get_connection_settings(
db=None,
name=None,
host=None,
Expand Down Expand Up @@ -74,7 +74,7 @@ async def _async_get_connection_settings(
resolved_hosts.append(entity)
continue

uri_info = await uri_parser.parse_uri(entity)
uri_info = uri_parser.parse_uri(entity)
resolved_hosts.append(entity)

# override DB name from URI if provided
Expand Down Expand Up @@ -122,7 +122,7 @@ async def _async_get_connection_settings(
return conn_settings


async def async_register_connection(
def async_register_connection(
alias,
db=None,
name=None,
Expand Down Expand Up @@ -158,7 +158,7 @@ async def async_register_connection(
for example, maxpoolsize, tz_aware, etc. See the documentation
for pymongo's `MongoClient` for a full list.
"""
conn_settings = await _async_get_connection_settings(
conn_settings = _async_get_connection_settings(
db=db,
name=name,
host=host,
Expand Down Expand Up @@ -218,13 +218,8 @@ def _create_connection(alias, mongo_client_class, **connection_settings):
raise ConnectionFailure(f"Cannot connect to database {alias} :\n{e}")


async def async_get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
def async_get_connection(alias=DEFAULT_CONNECTION_NAME):
"""Return a connection with a given alias."""

# Connect to the database if not already connected
if reconnect:
await async_disconnect(alias)

# If the requested alias already exists in the _connections list, return
# it immediately.
if alias in _connections and isinstance(_connections[alias], AsyncMongoClient):
Expand Down Expand Up @@ -313,15 +308,15 @@ async def async_get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False) -> AsyncD
await async_disconnect(alias)

if alias not in _dbs or not isinstance(_dbs[alias], AsyncDatabase):
conn = await async_get_connection(alias)
conn = async_get_connection(alias)
conn_settings = _connection_settings[alias]
db = conn[conn_settings["name"]]
# Authenticate if necessary
_dbs[alias] = db
return _dbs[alias]


async def async_connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
def async_connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
"""Connect to the database specified by the 'db' argument.

Connection settings may be provided here as well if the database is not
Expand All @@ -339,14 +334,14 @@ async def async_connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
"""
if alias in _connections:
prev_conn_setting = _connection_settings[alias]
new_conn_settings = await _async_get_connection_settings(db, **kwargs)
new_conn_settings = _async_get_connection_settings(db, **kwargs)
if new_conn_settings != prev_conn_setting:
err_msg = (
"A different connection with alias `{}` was already "
"registered. Use async_disconnect() first"
).format(alias)
raise ConnectionFailure(err_msg)
else:
await async_register_connection(alias, db, **kwargs)
async_register_connection(alias, db, **kwargs)

return await async_get_connection(alias)
return async_get_connection(alias)
2 changes: 1 addition & 1 deletion mongoengine/context_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ def __exit__(self, exc_type, exc, tb):
# Async context manager
# ------------------------------------------------------------------
async def __aenter__(self):
conn = await async_get_connection(self.alias)
conn = async_get_connection(self.alias)

self._async_session_cm = conn.start_session(**self.session_kwargs)
self._async_session = await self._async_session_cm.__aenter__()
Expand Down
2 changes: 1 addition & 1 deletion mongoengine/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ async def _aget_timeseries_collection(cls):
db, include_system_collections=True
):
collection = db[collection_name]
collection.options()
await collection.options()
return collection

opts = {"expireAfterSeconds": timeseries_opts.pop("expireAfterSeconds", None)}
Expand Down
80 changes: 44 additions & 36 deletions mongoengine/fields/datetime/aware_datetime_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class Event (Document):
meta = {'indexes': ['start_time.tz']} # Explicit nested field
"""

AVAILABLE_TIMEZONES = available_timezones() # Compute this once, CPU intensive call

def __init__(self, *args, **kwargs):
"""
Initialize AwareDateTimeField.
Expand Down Expand Up @@ -188,30 +190,35 @@ def to_python(self, value):
return None

if isinstance(value, datetime.datetime):
# Already a datetime object
return value

if isinstance(value, dict) and "utc" in value and "tz" in value:
# Stored format: {"utc": datetime, "tz": "Asia/Kolkata"}
utc_dt = value["utc"]
tz_name = value["tz"]
if not (isinstance(value, dict) and "utc" in value and "tz" in value):
return None

utc_dt = value["utc"]
tz_name = value["tz"]

if not isinstance(utc_dt, datetime.datetime):
return None
if not isinstance(utc_dt, datetime.datetime):
return None

# Ensure UTC datetime is timezone-aware
if utc_dt.tzinfo is None:
utc_dt = utc_dt.replace(tzinfo=UTC)
try:
tz = ZoneInfo(tz_name)
except Exception:
return utc_dt.replace(tzinfo=UTC) if utc_dt.tzinfo is None else utc_dt

# Convert from UTC to original timezone
# Prefer the ISO string: it preserves microseconds and the exact UTC offset.
iso_str = value.get("iso")
if isinstance(iso_str, str):
try:
tz = ZoneInfo(tz_name)
return utc_dt.astimezone(tz)
except Exception:
# If timezone is invalid, return UTC
return utc_dt
tz_aware_dt = datetime.datetime.fromisoformat(iso_str)
if tz_aware_dt.tzinfo is None:
tz_aware_dt = tz_aware_dt.replace(tzinfo=UTC)
return tz_aware_dt.astimezone(tz)
except (ValueError, TypeError):
pass # fall through to UTC path

return None
# Fallback for documents stored before the iso field was added
return utc_dt.astimezone(tz)

def to_mongo(self, value):
"""Convert Python datetime to MongoDB storage format."""
Expand All @@ -234,37 +241,30 @@ def to_mongo(self, value):
"Use datetime.now(ZoneInfo('Asia/Kolkata')) or similar."
)

# Get timezone name
# Resolve the IANA timezone name from the tzinfo object
tz_name = None
if hasattr(value.tzinfo, "key"):
# pytz timezone
# pytz: zone.key == "Asia/Kolkata"
tz_name = value.tzinfo.key
elif hasattr(value.tzinfo, "tzname"):
# Could be ZoneInfo or other
tz_name_str = value.tzinfo.tzname(value)
# For ZoneInfo, try to get the actual zone name
if hasattr(value.tzinfo, "__str__"):
# ZoneInfo's __str__ returns the zone name
zone_str = str(value.tzinfo)
# ZoneInfo zones are in available_timezones
if zone_str in available_timezones():
tz_name = zone_str
else:
tz_name = tz_name_str
else:
# ZoneInfo: str(zone) == "Asia/Kolkata"
zone_str = str(value.tzinfo)
if zone_str in self.AVAILABLE_TIMEZONES:
tz_name = zone_str
else:
tz_name = tz_name_str
# Last resort: tzname() — covers datetime.timezone.utc → "UTC"
tz_name = value.tzinfo.tzname(value)

if not tz_name:
self.error(
"Could not determine timezone name. "
"Use ZoneInfo('Asia/Kolkata') or pytz.timezone('Asia/Kolkata')"
)

# Convert to UTC for storage
utc_dt = value.astimezone(UTC)

return {
"utc": utc_dt,
"utc": value.astimezone(UTC),
# ISO string preserves the original UTC offset and microseconds exactly
"iso": value.isoformat(),
"tz": tz_name,
}

Expand Down Expand Up @@ -296,6 +296,14 @@ def lookup_member(self, member_name):
field.db_field = "utc"
field.name = member_name
return field
elif member_name == "iso":
from mongoengine.fields.string import StringField

# Return field type for nested iso string
field = StringField()
field.db_field = "iso"
field.name = member_name
return field
elif member_name == "tz":
from mongoengine.fields.string import StringField

Expand Down
2 changes: 1 addition & 1 deletion mongoengine/mongodb_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async def async_get_mongodb_version(alias: str = DEFAULT_CONNECTION_NAME):
cached = _VERSION_CACHE.get(alias)
if cached is not None:
return cached
conn = await async_get_connection(alias=alias)
conn = async_get_connection(alias=alias)
version = tuple((await conn.server_info())["versionArray"][:2])
_VERSION_CACHE[alias] = version
return version
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ test = [
"pillow (>=12.2)",
"tox (>=4.54)",
"tox-uv>=1.35.2",
"mongomock>=4.3.0"
]

[project.urls]
Expand Down
11 changes: 4 additions & 7 deletions tests/asynchronous/all_warnings/test_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,23 @@
top level and called first by the test suite.
"""

import unittest
import warnings

from mongoengine import *
from mongoengine.base.common import _document_registry
from tests.asynchronous.utils import reset_async_connections
from tests.utils import MONGO_TEST_DB
from tests.asynchronous.utils import MongoDBAsyncTestCase


class TestAllWarnings(unittest.IsolatedAsyncioTestCase):
class TestAllWarnings(MongoDBAsyncTestCase):
async def asyncSetUp(self):
await async_connect(db=MONGO_TEST_DB)
self.warning_list = []
self.showwarning_default = warnings.showwarning
warnings.showwarning = self.append_to_warning_list
await super().asyncSetUp()

async def asyncTearDown(self):
warnings.showwarning = self.showwarning_default
await async_disconnect_all()
await reset_async_connections()
await super().asyncTearDown()

def append_to_warning_list(self, message, category, *args):
self.warning_list.append({"message": message, "category": category})
Expand Down
Loading
Loading