PYTHON-2884: Replaced SON usage in all internal classes and commands (#1426)

This commit is contained in:
Jib 2023-12-19 18:42:23 -05:00 committed by GitHub
parent ffd61f8d74
commit 60d0761527
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 211 additions and 266 deletions

View File

@ -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

View File

@ -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
------------------------

View File

@ -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(

View File

@ -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:

View File

@ -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

View File

@ -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.

View File

@ -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"]:

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.")

View File

@ -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):

View File

@ -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):