Skip to content

Commit f107c08

Browse files
committed
PYTHON-1355 - list_collections improvements
1 parent 8468dfd commit f107c08

File tree

5 files changed

+90
-80
lines changed

5 files changed

+90
-80
lines changed

doc/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Highlights include:
1919
external libraries that can parse raw batches of BSON data.
2020
- New methods :meth:`~pymongo.mongo_client.MongoClient.list_databases` and
2121
:meth:`~pymongo.mongo_client.MongoClient.list_database_names`.
22+
- New methods :meth:`~pymongo.database.Database.list_collections` and
23+
:meth:`~pymongo.database.Database.list_collection_names`.
2224
- Support for mongodb+srv:// URIs. See
2325
:class:`~pymongo.mongo_client.MongoClient` for details.
2426

pymongo/collection.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1968,9 +1968,8 @@ def options(self, session=None):
19681968
.. versionchanged:: 3.6
19691969
Added ``session`` parameter.
19701970
"""
1971-
criteria = {"name": self.__name}
1972-
cursor = self.__database.list_collections(filter=criteria,
1973-
session=session)
1971+
cursor = self.__database.list_collections(
1972+
session=session, filter={"name": self.__name})
19741973

19751974
result = None
19761975
for doc in cursor:

pymongo/database.py

Lines changed: 64 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717
import warnings
1818

1919
from bson.code import Code
20-
from bson.codec_options import CodecOptions, DEFAULT_CODEC_OPTIONS
20+
from bson.codec_options import DEFAULT_CODEC_OPTIONS
2121
from bson.dbref import DBRef
22-
from bson.objectid import ObjectId
2322
from bson.py3compat import iteritems, string_type, _unicode
2423
from bson.son import SON
2524
from pymongo import auth, common
@@ -35,6 +34,10 @@
3534
from pymongo.write_concern import WriteConcern
3635

3736

37+
_INDEX_REGEX = {"name": {"$regex": "^(?!.*\$)"}}
38+
_SYSTEM_FILTER = {"filter": {"name": {"$regex": "^(?!system\.)"}}}
39+
40+
3841
def _check_name(name):
3942
"""Check if a database name is valid.
4043
"""
@@ -543,56 +546,73 @@ def command(self, command, value=1, check=True,
543546
check, allowable_errors, read_preference,
544547
codec_options, session=session, **kwargs)
545548

546-
def _list_collections(self, sock_info, slave_okay, filter=None,
547-
session=None):
549+
def _list_collections(self, sock_info, slave_okay, session=None, **kwargs):
548550
"""Internal listCollections helper."""
549-
filter = filter or {}
550-
cmd = SON([("listCollections", 1), ("cursor", {})])
551-
552-
if filter:
553-
cmd["filter"] = filter
554551

552+
coll = self["$cmd"]
555553
if sock_info.max_wire_version > 2:
556-
coll = self["$cmd"]
557-
with self.__client._tmp_session(session, close=False) as s:
554+
cmd = SON([("listCollections", 1),
555+
("cursor", {})])
556+
cmd.update(kwargs)
557+
with self.__client._tmp_session(
558+
session, close=False) as tmp_session:
558559
cursor = self._command(
559-
sock_info, cmd, slave_okay, session=s)["cursor"]
560-
return CommandCursor(coll, cursor, sock_info.address, session=s,
561-
explicit_session=session is not None)
560+
sock_info, cmd, slave_okay, session=tmp_session)["cursor"]
561+
return CommandCursor(
562+
coll,
563+
cursor,
564+
sock_info.address,
565+
session=tmp_session,
566+
explicit_session=session is not None)
562567
else:
563-
coll = self["system.namespaces"]
564-
if "name" in filter:
565-
if not isinstance(filter["name"], string_type):
566-
raise TypeError("filter['name'] must be a string on MongoDB 2.6")
567-
filter["name"] = coll.database.name + "." + filter["name"]
568-
res = _first_batch(sock_info, coll.database.name, coll.name,
569-
filter, 0, slave_okay,
570-
CodecOptions(), ReadPreference.PRIMARY, cmd,
571-
self.client._event_listeners, session=None)
572-
data = res["data"]
573-
cursor = {
574-
"id": res["cursor_id"],
575-
"firstBatch": data,
576-
"ns": coll.full_name,
577-
}
568+
match = _INDEX_REGEX
569+
if "filter" in kwargs:
570+
match = {"$and": [_INDEX_REGEX, kwargs["filter"]]}
571+
dblen = len(self.name.encode("utf8") + b".")
572+
pipeline = [
573+
{"$project": {"name": {"$substr": ["$name", dblen, -1]},
574+
"options": 1}},
575+
{"$match": match}
576+
]
577+
cmd = SON([("aggregate", "system.namespaces"),
578+
("pipeline", pipeline),
579+
("cursor", kwargs.get("cursor", {}))])
580+
cursor = self._command(sock_info, cmd, slave_okay)["cursor"]
578581
return CommandCursor(coll, cursor, sock_info.address)
579582

580-
def list_collections(self, filter=None, session=None):
581-
"""Get info about the collections in this database."""
583+
def list_collections(self, session=None, **kwargs):
584+
"""Get a cursor over the collectons of this database.
585+
586+
:Parameters:
587+
- `session` (optional): a
588+
:class:`~pymongo.client_session.ClientSession`.
589+
- `**kwargs` (optional): Optional parameters of the
590+
`listCollections command
591+
<https://docs.mongodb.com/manual/reference/command/listCollections/>`_
592+
can be passed as keyword arguments to this method. The supported
593+
options differ by server version.
594+
595+
:Returns:
596+
An instance of :class:`~pymongo.command_cursor.CommandCursor`.
597+
598+
.. versionadded:: 3.6
599+
"""
582600
with self.__client._socket_for_reads(
583601
ReadPreference.PRIMARY) as (sock_info, slave_okay):
602+
return self._list_collections(
603+
sock_info, slave_okay, session=session, **kwargs)
604+
605+
def list_collection_names(self, session=None):
606+
"""Get a list of all the collection names in this database.
584607
585-
wire_version = sock_info.max_wire_version
586-
results = self._list_collections(sock_info, slave_okay, filter=filter,
587-
session=session)
588-
for result in results:
589-
if wire_version <= 2:
590-
name = result["name"]
591-
if "$" in name:
592-
continue
593-
result["name"] = name.split(".", 1)[1]
594-
yield result
608+
:Parameters:
609+
- `session` (optional): a
610+
:class:`~pymongo.client_session.ClientSession`.
595611
612+
.. versionadded:: 3.6
613+
"""
614+
return [result["name"]
615+
for result in self.list_collections(session=session)]
596616

597617
def collection_names(self, include_system_collections=True,
598618
session=None):
@@ -607,17 +627,9 @@ def collection_names(self, include_system_collections=True,
607627
.. versionchanged:: 3.6
608628
Added ``session`` parameter.
609629
"""
610-
611-
results = self.list_collections(session=session)
612-
613-
# Iterating the cursor to completion may require a socket for getmore.
614-
# Ensure we do that outside the "with" block so we don't require more
615-
# than one socket at a time.
616-
names = [result["name"] for result in results]
617-
618-
if not include_system_collections:
619-
names = [name for name in names if not name.startswith("system.")]
620-
return names
630+
kws = {} if include_system_collections else _SYSTEM_FILTER
631+
return [result["name"]
632+
for result in self.list_collections(session=session, **kws)]
621633

622634
def drop_collection(self, name_or_collection, session=None):
623635
"""Drop a collection.

test/test_database.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,31 +155,41 @@ def test_create_collection(self):
155155
self.assertTrue(u"test.foo" in db.collection_names())
156156
self.assertRaises(CollectionInvalid, db.create_collection, "test.foo")
157157

158-
def test_collection_names(self):
158+
def _test_collection_names(self, meth, test_no_system):
159159
db = Database(self.client, "pymongo_test")
160160
db.test.insert_one({"dummy": u"object"})
161161
db.test.mike.insert_one({"dummy": u"object"})
162162

163-
colls = db.collection_names()
163+
colls = getattr(db, meth)()
164164
self.assertTrue("test" in colls)
165165
self.assertTrue("test.mike" in colls)
166166
for coll in colls:
167167
self.assertTrue("$" not in coll)
168168

169-
colls_without_systems = db.collection_names(False)
170-
for coll in colls_without_systems:
171-
self.assertTrue(not coll.startswith("system."))
169+
if test_no_system:
170+
db.systemcoll.test.insert_one({})
171+
no_system_collections = getattr(
172+
db, meth)(include_system_collections=False)
173+
for coll in no_system_collections:
174+
self.assertTrue(not coll.startswith("system."))
175+
self.assertIn("systemcoll.test", no_system_collections)
172176

173177
# Force more than one batch.
174178
db = self.client.many_collections
175179
for i in range(101):
176180
db["coll" + str(i)].insert_one({})
177181
# No Error
178182
try:
179-
db.collection_names()
183+
getattr(db, meth)()
180184
finally:
181185
self.client.drop_database("many_collections")
182186

187+
def test_collection_names(self):
188+
self._test_collection_names('collection_names', True)
189+
190+
def test_list_collection_names(self):
191+
self._test_collection_names('list_collection_names', False)
192+
183193
def test_list_collections(self):
184194
self.client.drop_database("pymongo_test")
185195
db = Database(self.client, "pymongo_test")
@@ -215,6 +225,11 @@ def test_list_collections(self):
215225
else:
216226
self.assertTrue(False)
217227

228+
colls = db.list_collections(filter={"name": {"$regex": "^test$"}})
229+
self.assertEqual(1, len(list(colls)))
230+
231+
colls = db.list_collections(filter={"name": {"$regex": "^test.mike$"}})
232+
self.assertEqual(1, len(list(colls)))
218233

219234
db.drop_collection("test")
220235

test/test_monitoring.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,27 +1217,9 @@ def test_write_errors(self):
12171217
def test_first_batch_helper(self):
12181218
# Regardless of server version and use of helpers._first_batch
12191219
# this test should still pass.
1220-
self.listener.results.clear()
1221-
self.client.pymongo_test.collection_names()
1222-
results = self.listener.results
1223-
started = results['started'][0]
1224-
succeeded = results['succeeded'][0]
1225-
self.assertEqual(0, len(results['failed']))
1226-
self.assertIsInstance(started, monitoring.CommandStartedEvent)
1227-
expected = SON([('listCollections', 1), ('cursor', {})])
1228-
self.assertEqualCommand(expected, started.command)
1229-
self.assertEqual('pymongo_test', started.database_name)
1230-
self.assertEqual('listCollections', started.command_name)
1231-
self.assertIsInstance(started.request_id, int)
1232-
self.assertEqual(self.client.address, started.connection_id)
1233-
self.assertIsInstance(succeeded, monitoring.CommandSucceededEvent)
1234-
self.assertIsInstance(succeeded.duration_micros, int)
1235-
self.assertEqual(started.command_name, succeeded.command_name)
1236-
self.assertEqual(started.request_id, succeeded.request_id)
1237-
self.assertEqual(started.connection_id, succeeded.connection_id)
1238-
12391220
self.listener.results.clear()
12401221
tuple(self.client.pymongo_test.test.list_indexes())
1222+
results = self.listener.results
12411223
started = results['started'][0]
12421224
succeeded = results['succeeded'][0]
12431225
self.assertEqual(0, len(results['failed']))

0 commit comments

Comments
 (0)