From 60d07615277fb36f3363b3e125ff2b7b769445a8 Mon Sep 17 00:00:00 2001 From: Jib Date: Tue, 19 Dec 2023 18:42:23 -0500 Subject: [PATCH] PYTHON-2884: Replaced SON usage in all internal classes and commands (#1426) --- bson/raw_bson.py | 11 ++-- doc/changelog.rst | 2 + gridfs/grid_file.py | 5 +- pymongo/aggregation.py | 3 +- pymongo/auth.py | 97 +++++++++++++++-------------------- pymongo/auth_aws.py | 17 +++---- pymongo/auth_oidc.py | 12 ++--- pymongo/bulk.py | 25 ++++----- pymongo/client_session.py | 3 +- pymongo/collection.py | 45 +++++++++-------- pymongo/cursor.py | 21 +++----- pymongo/database.py | 18 ++++--- pymongo/encryption.py | 9 ++-- pymongo/helpers.py | 5 +- pymongo/message.py | 78 +++++++++++++---------------- pymongo/mongo_client.py | 7 ++- pymongo/operations.py | 9 ++-- pymongo/pool.py | 103 ++++++++++++++++---------------------- test/test_collection.py | 4 +- test/test_cursor.py | 3 +- 20 files changed, 211 insertions(+), 266 deletions(-) diff --git a/bson/raw_bson.py b/bson/raw_bson.py index 8bada7d1d..2ce53143c 100644 --- a/bson/raw_bson.py +++ b/bson/raw_bson.py @@ -52,17 +52,16 @@ overhead of decoding or encoding BSON. """ from __future__ import annotations -from typing import Any, ItemsView, Iterator, Mapping, MutableMapping, Optional +from typing import Any, ItemsView, Iterator, Mapping, Optional from bson import _get_object_size, _raw_to_dict from bson.codec_options import _RAW_BSON_DOCUMENT_MARKER, CodecOptions from bson.codec_options import DEFAULT_CODEC_OPTIONS as DEFAULT -from bson.son import SON def _inflate_bson( bson_bytes: bytes, codec_options: CodecOptions[RawBSONDocument], raw_array: bool = False -) -> MutableMapping[str, Any]: +) -> dict[str, Any]: """Inflates the top level fields of a BSON document. :param bson_bytes: the BSON bytes that compose this document @@ -70,10 +69,7 @@ def _inflate_bson( :class:`~bson.codec_options.CodecOptions` whose ``document_class`` must be :class:`RawBSONDocument`. """ - # Use SON to preserve ordering of elements. - return _raw_to_dict( - bson_bytes, 4, len(bson_bytes) - 1, codec_options, SON(), raw_array=raw_array - ) + return _raw_to_dict(bson_bytes, 4, len(bson_bytes) - 1, codec_options, {}, raw_array=raw_array) class RawBSONDocument(Mapping[str, Any]): @@ -152,7 +148,6 @@ class RawBSONDocument(Mapping[str, Any]): if self.__inflated_doc is None: # We already validated the object's size when this document was # created, so no need to do that again. - # Use SON to preserve ordering of elements. self.__inflated_doc = self._inflate_bson(self.__raw, self.__codec_options) return self.__inflated_doc diff --git a/doc/changelog.rst b/doc/changelog.rst index 38f4c531f..799bf8c41 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -11,6 +11,8 @@ PyMongo 4.7 brings a number of improvements including: :attr:`pymongo.monitoring.CommandSucceededEvent.server_connection_id`, and :attr:`pymongo.monitoring.CommandFailedEvent.server_connection_id` properties. - Fixed a bug where inflating a :class:`~bson.raw_bson.RawBSONDocument` containing a :class:`~bson.code.Code` would cause an error. +- Replaced usage of :class:`bson.son.SON` on all internal classes and commands to dict, + :attr:`options.pool_options.metadata` is now of type ``dict`` as opposed to :class:`bson.son.SON`. Changes in Version 4.6.1 ------------------------ diff --git a/gridfs/grid_file.py b/gridfs/grid_file.py index cb78c65cc..16fa103fd 100644 --- a/gridfs/grid_file.py +++ b/gridfs/grid_file.py @@ -24,7 +24,6 @@ from typing import Any, Iterable, Mapping, NoReturn, Optional from bson.binary import Binary from bson.int64 import Int64 from bson.objectid import ObjectId -from bson.son import SON from gridfs.errors import CorruptGridFile, FileExists, NoFile from pymongo import ASCENDING from pymongo.client_session import ClientSession @@ -50,8 +49,8 @@ NEWLN = b"\n" # Slightly under a power of 2, to work well with server's record allocations. DEFAULT_CHUNK_SIZE = 255 * 1024 -_C_INDEX: SON[str, Any] = SON([("files_id", ASCENDING), ("n", ASCENDING)]) -_F_INDEX: SON[str, Any] = SON([("filename", ASCENDING), ("uploadDate", ASCENDING)]) +_C_INDEX: dict[str, Any] = {"files_id": ASCENDING, "n": ASCENDING} +_F_INDEX: dict[str, Any] = {"filename": ASCENDING, "uploadDate": ASCENDING} def _grid_in_property( diff --git a/pymongo/aggregation.py b/pymongo/aggregation.py index a6f8eeade..574db10ac 100644 --- a/pymongo/aggregation.py +++ b/pymongo/aggregation.py @@ -18,7 +18,6 @@ from __future__ import annotations from collections.abc import Callable, Mapping, MutableMapping from typing import TYPE_CHECKING, Any, Optional, Union -from bson.son import SON from pymongo import common from pymongo.collation import validate_collation_or_none from pymongo.errors import ConfigurationError @@ -137,7 +136,7 @@ class _AggregationCommand: read_preference: _ServerMode, ) -> CommandCursor[_DocumentType]: # Serialize command. - cmd = SON([("aggregate", self._aggregation_target), ("pipeline", self._pipeline)]) + cmd = {"aggregate": self._aggregation_target, "pipeline": self._pipeline} cmd.update(self._options) # Apply this target's read concern if: diff --git a/pymongo/auth.py b/pymongo/auth.py index 1926a3ba9..a2c9c2998 100644 --- a/pymongo/auth.py +++ b/pymongo/auth.py @@ -36,7 +36,6 @@ from typing import ( from urllib.parse import quote from bson.binary import Binary -from bson.son import SON from pymongo.auth_aws import _authenticate_aws from pymongo.auth_oidc import _authenticate_oidc, _get_authenticator, _OIDCProperties from pymongo.errors import ConfigurationError, OperationFailure @@ -217,15 +216,13 @@ def _authenticate_scram_start( nonce = standard_b64encode(os.urandom(32)) first_bare = b"n=" + user + b",r=" + nonce - cmd = SON( - [ - ("saslStart", 1), - ("mechanism", mechanism), - ("payload", Binary(b"n,," + first_bare)), - ("autoAuthorize", 1), - ("options", {"skipEmptyExchange": True}), - ] - ) + cmd = { + "saslStart": 1, + "mechanism": mechanism, + "payload": Binary(b"n,," + first_bare), + "autoAuthorize": 1, + "options": {"skipEmptyExchange": True}, + } return nonce, first_bare, cmd @@ -288,13 +285,11 @@ def _authenticate_scram(credentials: MongoCredential, conn: Connection, mechanis server_sig = standard_b64encode(_hmac(server_key, auth_msg, digestmod).digest()) - cmd = SON( - [ - ("saslContinue", 1), - ("conversationId", res["conversationId"]), - ("payload", Binary(client_final)), - ] - ) + cmd = { + "saslContinue": 1, + "conversationId": res["conversationId"], + "payload": Binary(client_final), + } res = conn.command(source, cmd) parsed = _parse_scram_response(res["payload"]) @@ -304,13 +299,11 @@ def _authenticate_scram(credentials: MongoCredential, conn: Connection, mechanis # A third empty challenge may be required if the server does not support # skipEmptyExchange: SERVER-44857. if not res["done"]: - cmd = SON( - [ - ("saslContinue", 1), - ("conversationId", res["conversationId"]), - ("payload", Binary(b"")), - ] - ) + cmd = { + "saslContinue": 1, + "conversationId": res["conversationId"], + "payload": Binary(b""), + } res = conn.command(source, cmd) if not res["done"]: raise OperationFailure("SASL conversation failed to complete.") @@ -415,14 +408,12 @@ def _authenticate_gssapi(credentials: MongoCredential, conn: Connection) -> None # Since mongo accepts base64 strings as the payload we don't # have to use bson.binary.Binary. payload = kerberos.authGSSClientResponse(ctx) - cmd = SON( - [ - ("saslStart", 1), - ("mechanism", "GSSAPI"), - ("payload", payload), - ("autoAuthorize", 1), - ] - ) + cmd = { + "saslStart": 1, + "mechanism": "GSSAPI", + "payload": payload, + "autoAuthorize": 1, + } response = conn.command("$external", cmd) # Limit how many times we loop to catch protocol / library issues @@ -433,13 +424,11 @@ def _authenticate_gssapi(credentials: MongoCredential, conn: Connection) -> None payload = kerberos.authGSSClientResponse(ctx) or "" - cmd = SON( - [ - ("saslContinue", 1), - ("conversationId", response["conversationId"]), - ("payload", payload), - ] - ) + cmd = { + "saslContinue": 1, + "conversationId": response["conversationId"], + "payload": payload, + } response = conn.command("$external", cmd) if result == kerberos.AUTH_GSS_COMPLETE: @@ -456,13 +445,11 @@ def _authenticate_gssapi(credentials: MongoCredential, conn: Connection) -> None raise OperationFailure("Unknown kerberos failure during GSS_Wrap step.") payload = kerberos.authGSSClientResponse(ctx) - cmd = SON( - [ - ("saslContinue", 1), - ("conversationId", response["conversationId"]), - ("payload", payload), - ] - ) + cmd = { + "saslContinue": 1, + "conversationId": response["conversationId"], + "payload": payload, + } conn.command("$external", cmd) finally: @@ -478,14 +465,12 @@ def _authenticate_plain(credentials: MongoCredential, conn: Connection) -> None: username = credentials.username password = credentials.password payload = (f"\x00{username}\x00{password}").encode() - cmd = SON( - [ - ("saslStart", 1), - ("mechanism", "PLAIN"), - ("payload", Binary(payload)), - ("autoAuthorize", 1), - ] - ) + cmd = { + "saslStart": 1, + "mechanism": "PLAIN", + "payload": Binary(payload), + "autoAuthorize": 1, + } conn.command(source, cmd) @@ -511,7 +496,7 @@ def _authenticate_mongo_cr(credentials: MongoCredential, conn: Connection) -> No key = _auth_key(nonce, username, password) # Actually authenticate - query = SON([("authenticate", 1), ("user", username), ("nonce", nonce), ("key", key)]) + query = {"authenticate": 1, "user": username, "nonce": nonce, "key": key} conn.command(source, query) @@ -588,7 +573,7 @@ class _ScramContext(_AuthContext): class _X509Context(_AuthContext): def speculate_command(self) -> MutableMapping[str, Any]: - cmd = SON([("authenticate", 1), ("mechanism", "MONGODB-X509")]) + cmd = {"authenticate": 1, "mechanism": "MONGODB-X509"} if self.credentials.username is not None: cmd["user"] = self.credentials.username return cmd diff --git a/pymongo/auth_aws.py b/pymongo/auth_aws.py index e704dcd9c..d18788d9a 100644 --- a/pymongo/auth_aws.py +++ b/pymongo/auth_aws.py @@ -50,7 +50,6 @@ from typing import TYPE_CHECKING, Any, Mapping, Optional, Type import bson from bson.binary import Binary -from bson.son import SON from pymongo.errors import ConfigurationError, OperationFailure if TYPE_CHECKING: @@ -94,21 +93,17 @@ def _authenticate_aws(credentials: MongoCredential, conn: Connection) -> None: ) ) client_payload = ctx.step(None) - client_first = SON( - [("saslStart", 1), ("mechanism", "MONGODB-AWS"), ("payload", client_payload)] - ) + client_first = {"saslStart": 1, "mechanism": "MONGODB-AWS", "payload": client_payload} server_first = conn.command("$external", client_first) res = server_first # Limit how many times we loop to catch protocol / library issues for _ in range(10): client_payload = ctx.step(res["payload"]) - cmd = SON( - [ - ("saslContinue", 1), - ("conversationId", server_first["conversationId"]), - ("payload", client_payload), - ] - ) + cmd = { + "saslContinue": 1, + "conversationId": server_first["conversationId"], + "payload": client_payload, + } res = conn.command("$external", cmd) if res["done"]: # SASL complete. diff --git a/pymongo/auth_oidc.py b/pymongo/auth_oidc.py index ad9223809..357cb62fb 100644 --- a/pymongo/auth_oidc.py +++ b/pymongo/auth_oidc.py @@ -251,13 +251,11 @@ class _OIDCAuthenticator: token = self.get_current_token() conn.oidc_token_gen_id = self.token_gen_id bin_payload = Binary(bson.encode({"jwt": token})) - cmd = SON( - [ - ("saslContinue", 1), - ("conversationId", conversation_id), - ("payload", bin_payload), - ] - ) + cmd = { + "saslContinue": 1, + "conversationId": conversation_id, + "payload": bin_payload, + } resp = self.run_command(conn, cmd) assert resp is not None if not resp["done"]: diff --git a/pymongo/bulk.py b/pymongo/bulk.py index cb60fe875..e5d56fe4c 100644 --- a/pymongo/bulk.py +++ b/pymongo/bulk.py @@ -34,7 +34,6 @@ from typing import ( from bson.objectid import ObjectId from bson.raw_bson import RawBSONDocument -from bson.son import SON from pymongo import _csot, common from pymongo.client_session import ClientSession, _validate_session_write_concern from pymongo.common import ( @@ -215,7 +214,7 @@ class _Bulk: upsert: bool = False, collation: Optional[Mapping[str, Any]] = None, array_filters: Optional[list[Mapping[str, Any]]] = None, - hint: Union[str, SON[str, Any], None] = None, + hint: Union[str, dict[str, Any], None] = None, ) -> None: """Create an update document and add it to the list of ops.""" validate_ok_for_update(update) @@ -242,11 +241,11 @@ class _Bulk: replacement: Mapping[str, Any], upsert: bool = False, collation: Optional[Mapping[str, Any]] = None, - hint: Union[str, SON[str, Any], None] = None, + hint: Union[str, dict[str, Any], None] = None, ) -> None: """Create a replace document and add it to the list of ops.""" validate_ok_for_replace(replacement) - cmd = SON([("q", selector), ("u", replacement), ("multi", False), ("upsert", upsert)]) + cmd = {"q": selector, "u": replacement, "multi": False, "upsert": upsert} if collation is not None: self.uses_collation = True cmd["collation"] = collation @@ -260,10 +259,10 @@ class _Bulk: selector: Mapping[str, Any], limit: int, collation: Optional[Mapping[str, Any]] = None, - hint: Union[str, SON[str, Any], None] = None, + hint: Union[str, dict[str, Any], None] = None, ) -> None: """Create a delete document and add it to the list of ops.""" - cmd = SON([("q", selector), ("limit", limit)]) + cmd = {"q": selector, "limit": limit} if collation is not None: self.uses_collation = True cmd["collation"] = collation @@ -350,7 +349,7 @@ class _Bulk: if last_run and (len(run.ops) - run.idx_offset) == 1: write_concern = final_write_concern or write_concern - cmd = SON([(cmd_name, self.collection.name), ("ordered", self.ordered)]) + cmd = {cmd_name: self.collection.name, "ordered": self.ordered} if self.comment: cmd["comment"] = self.comment _csot.apply_write_concern(cmd, write_concern) @@ -469,13 +468,11 @@ class _Bulk: ) while run.idx_offset < len(run.ops): - cmd = SON( - [ - (cmd_name, self.collection.name), - ("ordered", False), - ("writeConcern", {"w": 0}), - ] - ) + cmd = { + cmd_name: self.collection.name, + "ordered": False, + "writeConcern": {"w": 0}, + } conn.add_server_api(cmd) ops = islice(run.ops, run.idx_offset, None) # Run as many ops as possible. diff --git a/pymongo/client_session.py b/pymongo/client_session.py index 5a817fc48..e23ab4ad1 100644 --- a/pymongo/client_session.py +++ b/pymongo/client_session.py @@ -154,7 +154,6 @@ from typing import ( from bson.binary import Binary from bson.int64 import Int64 -from bson.son import SON from bson.timestamp import Timestamp from pymongo import _csot from pymongo.cursor import _ConnectionManager @@ -844,7 +843,7 @@ class ClientSession: opts = self._transaction.opts assert opts wc = opts.write_concern - cmd = SON([(command_name, 1)]) + cmd = {command_name: 1} if command_name == "commitTransaction": if opts.max_commit_time_ms and _csot.get_timeout() is None: cmd["maxTimeMS"] = opts.max_commit_time_ms diff --git a/pymongo/collection.py b/pymongo/collection.py index 95a9f2477..8b0320c0b 100644 --- a/pymongo/collection.py +++ b/pymongo/collection.py @@ -328,7 +328,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): qev2_required: bool = False, ) -> None: """Sends a create command with the given options.""" - cmd: SON[str, Any] = SON([("create", name)]) + cmd: dict[str, Any] = {"create": name} if encrypted_fields: cmd["encryptedFields"] = encrypted_fields @@ -576,7 +576,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): """Internal helper for inserting a single document.""" write_concern = write_concern or self.write_concern acknowledged = write_concern.acknowledged - command = SON([("insert", self.name), ("ordered", ordered), ("documents", [doc])]) + command = {"insert": self.name, "ordered": ordered, "documents": [doc]} if comment is not None: command["comment"] = comment @@ -767,9 +767,12 @@ class Collection(common.BaseObject, Generic[_DocumentType]): collation = validate_collation_or_none(collation) write_concern = write_concern or self.write_concern acknowledged = write_concern.acknowledged - update_doc: SON[str, Any] = SON( - [("q", criteria), ("u", document), ("multi", multi), ("upsert", upsert)] - ) + update_doc: dict[str, Any] = { + "q": criteria, + "u": document, + "multi": multi, + "upsert": upsert, + } if collation is not None: if not acknowledged: raise ConfigurationError("Collation is unsupported for unacknowledged writes.") @@ -788,7 +791,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): if not isinstance(hint, str): hint = helpers._index_document(hint) update_doc["hint"] = hint - command = SON([("update", self.name), ("ordered", ordered), ("updates", [update_doc])]) + command = {"update": self.name, "ordered": ordered, "updates": [update_doc]} if let is not None: common.validate_is_mapping("let", let) command["let"] = let @@ -1245,7 +1248,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): common.validate_is_mapping("filter", criteria) write_concern = write_concern or self.write_concern acknowledged = write_concern.acknowledged - delete_doc = SON([("q", criteria), ("limit", int(not multi))]) + delete_doc = {"q": criteria, "limit": int(not multi)} collation = validate_collation_or_none(collation) if collation is not None: if not acknowledged: @@ -1260,7 +1263,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): if not isinstance(hint, str): hint = helpers._index_document(hint) delete_doc["hint"] = hint - command = SON([("delete", self.name), ("ordered", ordered), ("deletes", [delete_doc])]) + command = {"delete": self.name, "ordered": ordered, "deletes": [delete_doc]} if let is not None: common.validate_is_document_type("let", let) @@ -1708,7 +1711,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): session: Optional[ClientSession], conn: Connection, read_preference: Optional[_ServerMode], - cmd: SON[str, Any], + cmd: dict[str, Any], collation: Optional[Collation], ) -> int: """Internal count command helper.""" @@ -1732,7 +1735,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): self, conn: Connection, read_preference: Optional[_ServerMode], - cmd: SON[str, Any], + cmd: dict[str, Any], collation: Optional[_CollationIn], session: Optional[ClientSession], ) -> Optional[Mapping[str, Any]]: @@ -1791,7 +1794,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): conn: Connection, read_preference: Optional[_ServerMode], ) -> int: - cmd: SON[str, Any] = SON([("count", self.__name)]) + cmd: dict[str, Any] = {"count": self.__name} cmd.update(kwargs) return self._count_cmd(session, conn, read_preference, cmd, collation=None) @@ -1867,7 +1870,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): if comment is not None: kwargs["comment"] = comment pipeline.append({"$group": {"_id": 1, "n": {"$sum": 1}}}) - cmd = SON([("aggregate", self.__name), ("pipeline", pipeline), ("cursor", {})]) + cmd = {"aggregate": self.__name, "pipeline": pipeline, "cursor": {}} if "hint" in kwargs and not isinstance(kwargs["hint"], str): kwargs["hint"] = helpers._index_document(kwargs["hint"]) collation = validate_collation_or_none(kwargs.pop("collation", None)) @@ -1970,7 +1973,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): names.append(document["name"]) yield document - cmd = SON([("createIndexes", self.name), ("indexes", list(gen_indexes()))]) + cmd = {"createIndexes": self.name, "indexes": list(gen_indexes())} cmd.update(kwargs) if "commitQuorum" in kwargs and not supports_quorum: raise ConfigurationError( @@ -2193,7 +2196,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): if not isinstance(name, str): raise TypeError("index_or_name must be an instance of str or list") - cmd = SON([("dropIndexes", self.__name), ("index", name)]) + cmd = {"dropIndexes": self.__name, "index": name} cmd.update(kwargs) if comment is not None: cmd["comment"] = comment @@ -2248,7 +2251,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): conn: Connection, read_preference: _ServerMode, ) -> CommandCursor[MutableMapping[str, Any]]: - cmd = SON([("listIndexes", self.__name), ("cursor", {})]) + cmd = {"listIndexes": self.__name, "cursor": {}} if comment is not None: cmd["comment"] = comment @@ -2429,7 +2432,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): ) yield index.document - cmd = SON([("createSearchIndexes", self.name), ("indexes", list(gen_indexes()))]) + cmd = {"createSearchIndexes": self.name, "indexes": list(gen_indexes())} cmd.update(kwargs) with self._conn_for_writes(session) as conn: @@ -2462,7 +2465,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): .. versionadded:: 4.5 """ - cmd = SON([("dropSearchIndex", self.__name), ("name", name)]) + cmd = {"dropSearchIndex": self.__name, "name": name} cmd.update(kwargs) if comment is not None: cmd["comment"] = comment @@ -2498,7 +2501,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): .. versionadded:: 4.5 """ - cmd = SON([("updateSearchIndex", self.__name), ("name", name), ("definition", definition)]) + cmd = {"updateSearchIndex": self.__name, "name": name, "definition": definition} cmd.update(kwargs) if comment is not None: cmd["comment"] = comment @@ -2916,7 +2919,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): raise InvalidName("collection names must not contain '$'") new_name = f"{self.__database.name}.{new_name}" - cmd = SON([("renameCollection", self.__full_name), ("to", new_name)]) + cmd = {"renameCollection": self.__full_name, "to": new_name} cmd.update(kwargs) if comment is not None: cmd["comment"] = comment @@ -2977,7 +2980,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): """ if not isinstance(key, str): raise TypeError("key must be an instance of str") - cmd = SON([("distinct", self.__name), ("key", key)]) + cmd = {"distinct": self.__name, "key": key} if filter is not None: if "query" in kwargs: raise ConfigurationError("can't pass both filter and query") @@ -3034,7 +3037,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): "return_document must be ReturnDocument.BEFORE or ReturnDocument.AFTER" ) collation = validate_collation_or_none(kwargs.pop("collation", None)) - cmd = SON([("findAndModify", self.__name), ("query", filter), ("new", return_document)]) + cmd = {"findAndModify": self.__name, "query": filter, "new": return_document} if let is not None: common.validate_is_mapping("let", let) cmd["let"] = let diff --git a/pymongo/cursor.py b/pymongo/cursor.py index d2ee09f09..3151fcaf3 100644 --- a/pymongo/cursor.py +++ b/pymongo/cursor.py @@ -272,14 +272,14 @@ class Cursor(Generic[_DocumentType]): self.__comment = comment self.__max_time_ms = max_time_ms self.__max_await_time_ms: Optional[int] = None - self.__max: Optional[Union[SON[Any, Any], _Sort]] = max - self.__min: Optional[Union[SON[Any, Any], _Sort]] = min + self.__max: Optional[Union[dict[Any, Any], _Sort]] = max + self.__min: Optional[Union[dict[Any, Any], _Sort]] = min self.__collation = validate_collation_or_none(collation) self.__return_key = return_key self.__show_record_id = show_record_id self.__allow_disk_use = allow_disk_use self.__snapshot = snapshot - self.__hint: Union[str, SON[str, Any], None] + self.__hint: Union[str, dict[str, Any], None] self.__set_hint(hint) # Exhaust cursor support @@ -473,17 +473,12 @@ class Cursor(Generic[_DocumentType]): if operators: # Make a shallow copy so we can cleanly rewind or clone. - spec = copy.copy(self.__spec) + spec = dict(self.__spec) # Allow-listed commands must be wrapped in $query. if "$query" not in spec: # $query has to come first - spec = SON([("$query", spec)]) - - if not isinstance(spec, SON): - # Ensure the spec is SON. As order is important this will - # ensure its set before merging in any extra operators. - spec = SON(spec) + spec = {"$query": spec} spec.update(operators) return spec @@ -495,7 +490,7 @@ class Cursor(Generic[_DocumentType]): elif "query" in self.__spec and ( len(self.__spec) == 1 or next(iter(self.__spec)) == "query" ): - return SON({"$query": self.__spec}) + return {"$query": self.__spec} return self.__spec @@ -800,7 +795,7 @@ class Cursor(Generic[_DocumentType]): raise TypeError("spec must be an instance of list or tuple") self.__check_okay_to_chain() - self.__max = SON(spec) + self.__max = dict(spec) return self def min(self, spec: _Sort) -> Cursor[_DocumentType]: @@ -822,7 +817,7 @@ class Cursor(Generic[_DocumentType]): raise TypeError("spec must be an instance of list or tuple") self.__check_okay_to_chain() - self.__min = SON(spec) + self.__min = dict(spec) return self def sort( diff --git a/pymongo/database.py b/pymongo/database.py index fd28d8bf5..a1ffcd0e0 100644 --- a/pymongo/database.py +++ b/pymongo/database.py @@ -33,7 +33,6 @@ from typing import ( from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions from bson.dbref import DBRef -from bson.son import SON from bson.timestamp import Timestamp from pymongo import _csot, common from pymongo.aggregation import _DatabaseAggregationCommand @@ -729,7 +728,7 @@ class Database(common.BaseObject, Generic[_DocumentType]): ) -> Union[dict[str, Any], _CodecDocumentType]: """Internal command helper.""" if isinstance(command, str): - command = SON([(command, value)]) + command = {command: value} command.update(kwargs) with self.__client._tmp_session(session) as s: @@ -804,16 +803,22 @@ class Database(common.BaseObject, Generic[_DocumentType]): using: >>> db.command("buildinfo") + OR + >>> db.command({"buildinfo": 1}) For a command where the value matters, like ``{count: collection_name}`` we can do: >>> db.command("count", collection_name) + OR + >>> db.command({"count": collection_name}) For commands that take additional arguments we can use kwargs. So ``{filemd5: object_id, root: file_root}`` becomes: >>> db.command("filemd5", object_id, root=file_root) + OR + >>> db.command({"filemd5": object_id, "root": file_root}) :param command: document representing the command to be issued, or the name of the command (for simple commands only). @@ -821,8 +826,7 @@ class Database(common.BaseObject, Generic[_DocumentType]): .. note:: the order of keys in the `command` document is significant (the "verb" must come first), so commands which require multiple keys (e.g. `findandmodify`) - should use an instance of :class:`~bson.son.SON` or - a string and kwargs instead of a Python `dict`. + should be done with this in mind. :param value: value to use for the command verb when `command` is passed as a string @@ -1028,7 +1032,7 @@ class Database(common.BaseObject, Generic[_DocumentType]): Collection[MutableMapping[str, Any]], self.get_collection("$cmd", read_preference=read_preference), ) - cmd = SON([("listCollections", 1), ("cursor", {})]) + cmd = {"listCollections": 1, "cursor": {}} cmd.update(kwargs) with self.__client._tmp_session(session, close=False) as tmp_session: cursor = self._command(conn, cmd, read_preference=read_preference, session=tmp_session)[ @@ -1137,7 +1141,7 @@ class Database(common.BaseObject, Generic[_DocumentType]): def _drop_helper( self, name: str, session: Optional[ClientSession] = None, comment: Optional[Any] = None ) -> dict[str, Any]: - command = SON([("drop", name)]) + command = {"drop": name} if comment is not None: command["comment"] = comment @@ -1277,7 +1281,7 @@ class Database(common.BaseObject, Generic[_DocumentType]): if not isinstance(name, str): raise TypeError("name_or_collection must be an instance of str or Collection") - cmd = SON([("validate", name), ("scandata", scandata), ("full", full)]) + cmd = {"validate": name, "scandata": scandata, "full": full} if comment is not None: cmd["comment"] = comment diff --git a/pymongo/encryption.py b/pymongo/encryption.py index 25e9960f7..34c496f0c 100644 --- a/pymongo/encryption.py +++ b/pymongo/encryption.py @@ -23,6 +23,7 @@ from copy import deepcopy from typing import ( TYPE_CHECKING, Any, + Dict, Generic, Iterator, Mapping, @@ -49,7 +50,6 @@ from bson.binary import STANDARD, UUID_SUBTYPE, Binary from bson.codec_options import CodecOptions from bson.errors import BSONError from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument, _inflate_bson -from bson.son import SON from pymongo import _csot from pymongo.collection import Collection from pymongo.common import CONNECT_TIMEOUT @@ -83,9 +83,8 @@ _HTTPS_PORT = 443 _KMS_CONNECT_TIMEOUT = CONNECT_TIMEOUT # CDRIVER-3262 redefined this value to CONNECT_TIMEOUT _MONGOCRYPTD_TIMEOUT_MS = 10000 - -_DATA_KEY_OPTS: CodecOptions[SON[str, Any]] = CodecOptions( - document_class=SON[str, Any], uuid_representation=STANDARD +_DATA_KEY_OPTS: CodecOptions[dict[str, Any]] = CodecOptions( + document_class=Dict[str, Any], uuid_representation=STANDARD ) # Use RawBSONDocument codec options to avoid needlessly decoding # documents from the key vault. @@ -388,7 +387,7 @@ class _Encrypter: def encrypt( self, database: str, cmd: Mapping[str, Any], codec_options: CodecOptions[_DocumentTypeArg] - ) -> MutableMapping[str, Any]: + ) -> dict[str, Any]: """Encrypt a MongoDB command. :param database: The database for this command. diff --git a/pymongo/helpers.py b/pymongo/helpers.py index cd7d434b0..63c8d4fc6 100644 --- a/pymongo/helpers.py +++ b/pymongo/helpers.py @@ -33,7 +33,6 @@ from typing import ( cast, ) -from bson.son import SON from pymongo import ASCENDING from pymongo.errors import ( CursorNotFound, @@ -124,7 +123,7 @@ def _index_list( return values -def _index_document(index_list: _IndexList) -> SON[str, Any]: +def _index_document(index_list: _IndexList) -> dict[str, Any]: """Helper to generate an index specifying document. Takes a list of (key, direction) pairs. @@ -136,7 +135,7 @@ def _index_document(index_list: _IndexList) -> SON[str, Any]: if not len(index_list): raise ValueError("key_or_list must not be empty") - index: SON[str, Any] = SON() + index: dict[str, Any] = {} if isinstance(index_list, abc.Mapping): for key in index_list: diff --git a/pymongo/message.py b/pymongo/message.py index 7b93015a4..1a19b3635 100644 --- a/pymongo/message.py +++ b/pymongo/message.py @@ -35,7 +35,6 @@ from typing import ( NoReturn, Optional, Union, - cast, ) import bson @@ -47,7 +46,6 @@ from bson.raw_bson import ( RawBSONDocument, _inflate_bson, ) -from bson.son import SON try: from pymongo import _cmessage # type: ignore[attr-defined] @@ -129,7 +127,7 @@ def _maybe_add_read_preference( # the secondaryOkay bit has the same effect). if mode and (mode != ReadPreference.SECONDARY_PREFERRED.mode or len(document) > 1): if "$query" not in spec: - spec = SON([("$query", spec)]) + spec = {"$query": spec} spec["$readPreference"] = document return spec @@ -175,33 +173,29 @@ def _convert_write_result( return res -_OPTIONS = SON( - [ - ("tailable", 2), - ("oplogReplay", 8), - ("noCursorTimeout", 16), - ("awaitData", 32), - ("allowPartialResults", 128), - ] -) +_OPTIONS = { + "tailable": 2, + "oplogReplay": 8, + "noCursorTimeout": 16, + "awaitData": 32, + "allowPartialResults": 128, +} -_MODIFIERS = SON( - [ - ("$query", "filter"), - ("$orderby", "sort"), - ("$hint", "hint"), - ("$comment", "comment"), - ("$maxScan", "maxScan"), - ("$maxTimeMS", "maxTimeMS"), - ("$max", "max"), - ("$min", "min"), - ("$returnKey", "returnKey"), - ("$showRecordId", "showRecordId"), - ("$showDiskLoc", "showRecordId"), # <= MongoDb 3.0 - ("$snapshot", "snapshot"), - ] -) +_MODIFIERS = { + "$query": "filter", + "$orderby": "sort", + "$hint": "hint", + "$comment": "comment", + "$maxScan": "maxScan", + "$maxTimeMS": "maxTimeMS", + "$max": "max", + "$min": "min", + "$returnKey": "returnKey", + "$showRecordId": "showRecordId", + "$showDiskLoc": "showRecordId", # <= MongoDb 3.0 + "$snapshot": "snapshot", +} def _gen_find_command( @@ -216,9 +210,9 @@ def _gen_find_command( collation: Optional[Mapping[str, Any]] = None, session: Optional[ClientSession] = None, allow_disk_use: Optional[bool] = None, -) -> SON[str, Any]: +) -> dict[str, Any]: """Generate a find command document.""" - cmd: SON[str, Any] = SON([("find", coll)]) + cmd: dict[str, Any] = {"find": coll} if "$query" in spec: cmd.update( [ @@ -262,9 +256,9 @@ def _gen_get_more_command( max_await_time_ms: Optional[int], comment: Optional[Any], conn: Connection, -) -> SON[str, Any]: +) -> dict[str, Any]: """Generate a getMore command document.""" - cmd: SON[str, Any] = SON([("getMore", cursor_id), ("collection", coll)]) + cmd: dict[str, Any] = {"getMore": cursor_id, "collection": coll} if batch_size: cmd["batchSize"] = batch_size if max_await_time_ms is not None: @@ -337,7 +331,7 @@ class _Query: self.client = client self.allow_disk_use = allow_disk_use self.name = "find" - self._as_command: Optional[tuple[SON[str, Any], str]] = None + self._as_command: Optional[tuple[dict[str, Any], str]] = None self.exhaust = exhaust def reset(self) -> None: @@ -364,7 +358,7 @@ class _Query: def as_command( self, conn: Connection, apply_timeout: bool = False - ) -> tuple[SON[str, Any], str]: + ) -> tuple[dict[str, Any], str]: """Return a find command document for this query.""" # We use the command twice: on the wire and for command monitoring. # Generate it once, for speed and to avoid repeating side-effects. @@ -372,7 +366,7 @@ class _Query: return self._as_command explain = "$explain" in self.spec - cmd: SON[str, Any] = _gen_find_command( + cmd: dict[str, Any] = _gen_find_command( self.coll, self.spec, self.fields, @@ -387,7 +381,7 @@ class _Query: ) if explain: self.name = "explain" - cmd = SON([("explain", cmd)]) + cmd = {"explain": cmd} session = self.session conn.add_server_api(cmd) if session: @@ -399,7 +393,7 @@ class _Query: # Support auto encryption client = self.client if client._encrypter and not client._encrypter._bypass_auto_encryption: - cmd = cast(SON[str, Any], client._encrypter.encrypt(self.db, cmd, self.codec_options)) + cmd = client._encrypter.encrypt(self.db, cmd, self.codec_options) # Support CSOT if apply_timeout: conn.apply_timeout(client, cmd) @@ -505,7 +499,7 @@ class _GetMore: self.client = client self.max_await_time_ms = max_await_time_ms self.conn_mgr = conn_mgr - self._as_command: Optional[tuple[SON[str, Any], str]] = None + self._as_command: Optional[tuple[dict[str, Any], str]] = None self.exhaust = exhaust self.comment = comment @@ -528,13 +522,13 @@ class _GetMore: def as_command( self, conn: Connection, apply_timeout: bool = False - ) -> tuple[SON[str, Any], str]: + ) -> tuple[dict[str, Any], str]: """Return a getMore command document for this query.""" # See _Query.as_command for an explanation of this caching. if self._as_command is not None: return self._as_command - cmd: SON[str, Any] = _gen_get_more_command( + cmd: dict[str, Any] = _gen_get_more_command( self.cursor_id, self.coll, self.ntoreturn, @@ -549,7 +543,7 @@ class _GetMore: # Support auto encryption client = self.client if client._encrypter and not client._encrypter._bypass_auto_encryption: - cmd = cast(SON[str, Any], client._encrypter.encrypt(self.db, cmd, self.codec_options)) + cmd = client._encrypter.encrypt(self.db, cmd, self.codec_options) # Support CSOT if apply_timeout: conn.apply_timeout(client, cmd=None) @@ -1129,7 +1123,7 @@ class _EncryptedBulkWriteContext(_BulkWriteContext): def __batch_command( self, cmd: MutableMapping[str, Any], docs: list[Mapping[str, Any]] - ) -> tuple[MutableMapping[str, Any], list[Mapping[str, Any]]]: + ) -> tuple[dict[str, Any], list[Mapping[str, Any]]]: namespace = self.db_name + ".$cmd" msg, to_send = _encode_batched_write_command( namespace, self.op_type, cmd, docs, self.codec, self diff --git a/pymongo/mongo_client.py b/pymongo/mongo_client.py index bccacdf7e..4991d59f1 100644 --- a/pymongo/mongo_client.py +++ b/pymongo/mongo_client.py @@ -57,7 +57,6 @@ from typing import ( import bson from bson.codec_options import DEFAULT_CODEC_OPTIONS, TypeRegistry -from bson.son import SON from bson.timestamp import Timestamp from pymongo import ( _csot, @@ -1190,7 +1189,7 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]): return for i in range(0, len(session_ids), common._MAX_END_SESSIONS): - spec = SON([("endSessions", session_ids[i : i + common._MAX_END_SESSIONS])]) + spec = {"endSessions": session_ids[i : i + common._MAX_END_SESSIONS]} conn.command("admin", spec, read_preference=read_pref, client=self) except PyMongoError: # Drivers MUST ignore any errors returned by the endSessions @@ -1693,7 +1692,7 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]): ) -> None: namespace = address.namespace db, coll = namespace.split(".", 1) - spec = SON([("killCursors", coll), ("cursors", cursor_ids)]) + spec = {"killCursors": coll, "cursors": cursor_ids} conn.command(db, spec, session=session, client=self) def _process_kill_cursors(self) -> None: @@ -1903,7 +1902,7 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]): .. versionadded:: 3.6 """ - cmd = SON([("listDatabases", 1)]) + cmd = {"listDatabases": 1} cmd.update(kwargs) if comment is not None: cmd["comment"] = comment diff --git a/pymongo/operations.py b/pymongo/operations.py index 52bce1ad1..0baf662a8 100644 --- a/pymongo/operations.py +++ b/pymongo/operations.py @@ -35,7 +35,6 @@ from pymongo.typings import _CollationIn, _DocumentType, _Pipeline from pymongo.write_concern import validate_boolean if TYPE_CHECKING: - from bson.son import SON from pymongo.bulk import _Bulk # Hint supports index name, "myIndex", a list of either strings or index pairs: [('x', 1), ('y', -1), 'z''], or a dictionary @@ -109,7 +108,7 @@ class DeleteOne: if filter is not None: validate_is_mapping("filter", filter) if hint is not None and not isinstance(hint, str): - self._hint: Union[str, SON[str, Any], None] = helpers._index_document(hint) + self._hint: Union[str, dict[str, Any], None] = helpers._index_document(hint) else: self._hint = hint self._filter = filter @@ -173,7 +172,7 @@ class DeleteMany: if filter is not None: validate_is_mapping("filter", filter) if hint is not None and not isinstance(hint, str): - self._hint: Union[str, SON[str, Any], None] = helpers._index_document(hint) + self._hint: Union[str, dict[str, Any], None] = helpers._index_document(hint) else: self._hint = hint self._filter = filter @@ -244,7 +243,7 @@ class ReplaceOne(Generic[_DocumentType]): if upsert is not None: validate_boolean("upsert", upsert) if hint is not None and not isinstance(hint, str): - self._hint: Union[str, SON[str, Any], None] = helpers._index_document(hint) + self._hint: Union[str, dict[str, Any], None] = helpers._index_document(hint) else: self._hint = hint self._filter = filter @@ -314,7 +313,7 @@ class _UpdateOp: if array_filters is not None: validate_list("array_filters", array_filters) if hint is not None and not isinstance(hint, str): - self._hint: Union[str, SON[str, Any], None] = helpers._index_document(hint) + self._hint: Union[str, dict[str, Any], None] = helpers._index_document(hint) else: self._hint = hint diff --git a/pymongo/pool.py b/pymongo/pool.py index e7b5a0dd4..610fa5b97 100644 --- a/pymongo/pool.py +++ b/pymongo/pool.py @@ -40,7 +40,6 @@ from typing import ( import bson from bson import DEFAULT_CODEC_OPTIONS -from bson.son import SON from pymongo import __version__, _csot, auth, helpers from pymongo.client_session import _validate_session_write_concern from pymongo.common import ( @@ -180,11 +179,7 @@ else: _set_tcp_option(sock, "TCP_KEEPCNT", _MAX_TCP_KEEPCNT) -_METADATA: SON[str, Any] = SON( - [ - ("driver", SON([("name", "PyMongo"), ("version", __version__)])), - ] -) +_METADATA: dict[str, Any] = {"driver": {"name": "PyMongo", "version": __version__}} if sys.platform.startswith("linux"): # platform.linux_distribution was deprecated in Python 3.5 @@ -192,61 +187,51 @@ if sys.platform.startswith("linux"): # raises DeprecationWarning # DeprecationWarning: dist() and linux_distribution() functions are deprecated in Python 3.5 _name = platform.system() - _METADATA["os"] = SON( - [ - ("type", _name), - ("name", _name), - ("architecture", platform.machine()), - # Kernel version (e.g. 4.4.0-17-generic). - ("version", platform.release()), - ] - ) + _METADATA["os"] = { + "type": _name, + "name": _name, + "architecture": platform.machine(), + # Kernel version (e.g. 4.4.0-17-generic). + "version": platform.release(), + } elif sys.platform == "darwin": - _METADATA["os"] = SON( - [ - ("type", platform.system()), - ("name", platform.system()), - ("architecture", platform.machine()), - # (mac|i|tv)OS(X) version (e.g. 10.11.6) instead of darwin - # kernel version. - ("version", platform.mac_ver()[0]), - ] - ) + _METADATA["os"] = { + "type": platform.system(), + "name": platform.system(), + "architecture": platform.machine(), + # (mac|i|tv)OS(X) version (e.g. 10.11.6) instead of darwin + # kernel version. + "version": platform.mac_ver()[0], + } elif sys.platform == "win32": - _METADATA["os"] = SON( - [ - ("type", platform.system()), - # "Windows XP", "Windows 7", "Windows 10", etc. - ("name", " ".join((platform.system(), platform.release()))), - ("architecture", platform.machine()), - # Windows patch level (e.g. 5.1.2600-SP3) - ("version", "-".join(platform.win32_ver()[1:3])), - ] - ) + _METADATA["os"] = { + "type": platform.system(), + # "Windows XP", "Windows 7", "Windows 10", etc. + "name": " ".join((platform.system(), platform.release())), + "architecture": platform.machine(), + # Windows patch level (e.g. 5.1.2600-SP3) + "version": "-".join(platform.win32_ver()[1:3]), + } elif sys.platform.startswith("java"): _name, _ver, _arch = platform.java_ver()[-1] - _METADATA["os"] = SON( - [ - # Linux, Windows 7, Mac OS X, etc. - ("type", _name), - ("name", _name), - # x86, x86_64, AMD64, etc. - ("architecture", _arch), - # Linux kernel version, OSX version, etc. - ("version", _ver), - ] - ) + _METADATA["os"] = { + # Linux, Windows 7, Mac OS X, etc. + "type": _name, + "name": _name, + # x86, x86_64, AMD64, etc. + "architecture": _arch, + # Linux kernel version, OSX version, etc. + "version": _ver, + } else: # Get potential alias (e.g. SunOS 5.11 becomes Solaris 2.11) _aliased = platform.system_alias(platform.system(), platform.release(), platform.version()) - _METADATA["os"] = SON( - [ - ("type", platform.system()), - ("name", " ".join([part for part in _aliased[:2] if part])), - ("architecture", platform.machine()), - ("version", _aliased[2]), - ] - ) + _METADATA["os"] = { + "type": platform.system(), + "name": " ".join([part for part in _aliased[:2] if part]), + "architecture": platform.machine(), + "version": _aliased[2], + } if platform.python_implementation().startswith("PyPy"): _METADATA["platform"] = " ".join( @@ -682,7 +667,7 @@ class PoolOptions: return self.__compression_settings @property - def metadata(self) -> SON[str, Any]: + def metadata(self) -> dict[str, Any]: """A dict of metadata about the application, driver, os, and platform.""" return self.__metadata.copy() @@ -824,14 +809,14 @@ class Connection: else: self.close_conn(ConnectionClosedReason.STALE) - def hello_cmd(self) -> SON[str, Any]: + def hello_cmd(self) -> dict[str, Any]: # Handshake spec requires us to use OP_MSG+hello command for the # initial handshake in load balanced or stable API mode. if self.opts.server_api or self.hello_ok or self.opts.load_balanced: self.op_msg_enabled = True - return SON([(HelloCompat.CMD, 1)]) + return {HelloCompat.CMD: 1} else: - return SON([(HelloCompat.LEGACY_CMD, 1), ("helloOk", True)]) + return {HelloCompat.LEGACY_CMD: 1, "helloOk": True} def hello(self) -> Hello[dict[str, Any]]: return self._hello(None, None, None) @@ -974,7 +959,7 @@ class Connection: # Ensure command name remains in first place. if not isinstance(spec, ORDERED_TYPES): # type:ignore[arg-type] - spec = SON(spec) + spec = dict(spec) if not (write_concern is None or write_concern.acknowledged or collation is None): raise ConfigurationError("Collation is unsupported for unacknowledged writes.") diff --git a/test/test_collection.py b/test/test_collection.py index 0ce9cc2a4..1667a3dd0 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -2124,9 +2124,7 @@ class TestCollection(IntegrationTest): None, None, ) - self.assertEqual( - cmd.to_dict(), SON([("find", "coll"), ("$dumb", 2), ("filter", {"foo": 1})]).to_dict() - ) + self.assertEqual(cmd, {"find": "coll", "$dumb": 2, "filter": {"foo": 1}}) def test_bool(self): with self.assertRaises(NotImplementedError): diff --git a/test/test_cursor.py b/test/test_cursor.py index 103378ed6..a54e025f5 100644 --- a/test/test_cursor.py +++ b/test/test_cursor.py @@ -912,7 +912,8 @@ class TestCursor(IntegrationTest): # Ensure hints are cloned as the correct type cursor = self.db.test.find().hint([("z", 1), ("a", 1)]) cursor2 = copy.deepcopy(cursor) - self.assertTrue(isinstance(cursor2._Cursor__hint, SON)) + # Internal types are now dict rather than SON by default + self.assertTrue(isinstance(cursor2._Cursor__hint, dict)) self.assertEqual(cursor._Cursor__hint, cursor2._Cursor__hint) def test_clone_empty(self):