Skip to content

Commit 4e74c82

Browse files
authored
PYTHON-4669 - Update Async GridFS APIs for Motor Compatibility (mongodb#1821)
1 parent 5a49ccc commit 4e74c82

File tree

10 files changed

+1115
-105
lines changed

10 files changed

+1115
-105
lines changed

gridfs/asynchronous/grid_file.py

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,24 +1176,6 @@ def __getattr__(self, name: str) -> Any:
11761176
raise AttributeError("GridIn object has no attribute '%s'" % name)
11771177

11781178
def __setattr__(self, name: str, value: Any) -> None:
1179-
# For properties of this instance like _buffer, or descriptors set on
1180-
# the class like filename, use regular __setattr__
1181-
if name in self.__dict__ or name in self.__class__.__dict__:
1182-
object.__setattr__(self, name, value)
1183-
else:
1184-
if _IS_SYNC:
1185-
# All other attributes are part of the document in db.fs.files.
1186-
# Store them to be sent to server on close() or if closed, send
1187-
# them now.
1188-
self._file[name] = value
1189-
if self._closed:
1190-
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1191-
else:
1192-
raise AttributeError(
1193-
"AsyncGridIn does not support __setattr__. Use AsyncGridIn.set() instead"
1194-
)
1195-
1196-
async def set(self, name: str, value: Any) -> None:
11971179
# For properties of this instance like _buffer, or descriptors set on
11981180
# the class like filename, use regular __setattr__
11991181
if name in self.__dict__ or name in self.__class__.__dict__:
@@ -1204,9 +1186,17 @@ async def set(self, name: str, value: Any) -> None:
12041186
# them now.
12051187
self._file[name] = value
12061188
if self._closed:
1207-
await self._coll.files.update_one(
1208-
{"_id": self._file["_id"]}, {"$set": {name: value}}
1209-
)
1189+
if _IS_SYNC:
1190+
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1191+
else:
1192+
raise AttributeError(
1193+
"AsyncGridIn does not support __setattr__ after being closed(). Set the attribute before closing the file or use AsyncGridIn.set() instead"
1194+
)
1195+
1196+
async def set(self, name: str, value: Any) -> None:
1197+
self._file[name] = value
1198+
if self._closed:
1199+
await self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
12101200

12111201
async def _flush_data(self, data: Any, force: bool = False) -> None:
12121202
"""Flush `data` to a chunk."""
@@ -1400,7 +1390,11 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any:
14001390
return False
14011391

14021392

1403-
class AsyncGridOut(io.IOBase):
1393+
GRIDOUT_BASE_CLASS = io.IOBase if _IS_SYNC else object # type: Any
1394+
1395+
1396+
class AsyncGridOut(GRIDOUT_BASE_CLASS): # type: ignore
1397+
14041398
"""Class to read data out of GridFS."""
14051399

14061400
def __init__(
@@ -1460,6 +1454,8 @@ def __init__(
14601454
self._position = 0
14611455
self._file = file_document
14621456
self._session = session
1457+
if not _IS_SYNC:
1458+
self.closed = False
14631459

14641460
_id: Any = _a_grid_out_property("_id", "The ``'_id'`` value for this file.")
14651461
filename: str = _a_grid_out_property("filename", "Name of this file.")
@@ -1486,16 +1482,43 @@ def __init__(
14861482
_file: Any
14871483
_chunk_iter: Any
14881484

1489-
async def __anext__(self) -> bytes:
1490-
return super().__next__()
1485+
if not _IS_SYNC:
1486+
closed: bool
14911487

1492-
def __next__(self) -> bytes: # noqa: F811, RUF100
1493-
if _IS_SYNC:
1494-
return super().__next__()
1495-
else:
1496-
raise TypeError(
1497-
"AsyncGridOut does not support synchronous iteration. Use `async for` instead"
1498-
)
1488+
async def __anext__(self) -> bytes:
1489+
line = await self.readline()
1490+
if line:
1491+
return line
1492+
raise StopAsyncIteration()
1493+
1494+
async def to_list(self) -> list[bytes]:
1495+
return [x async for x in self] # noqa: C416, RUF100
1496+
1497+
async def readline(self, size: int = -1) -> bytes:
1498+
"""Read one line or up to `size` bytes from the file.
1499+
1500+
:param size: the maximum number of bytes to read
1501+
"""
1502+
return await self._read_size_or_line(size=size, line=True)
1503+
1504+
async def readlines(self, size: int = -1) -> list[bytes]:
1505+
"""Read one line or up to `size` bytes from the file.
1506+
1507+
:param size: the maximum number of bytes to read
1508+
"""
1509+
await self.open()
1510+
lines = []
1511+
remainder = int(self.length) - self._position
1512+
bytes_read = 0
1513+
while remainder > 0:
1514+
line = await self._read_size_or_line(line=True)
1515+
bytes_read += len(line)
1516+
lines.append(line)
1517+
remainder = int(self.length) - self._position
1518+
if 0 < size < bytes_read:
1519+
break
1520+
1521+
return lines
14991522

15001523
async def open(self) -> None:
15011524
if not self._file:
@@ -1616,18 +1639,11 @@ async def read(self, size: int = -1) -> bytes:
16161639
"""
16171640
return await self._read_size_or_line(size=size)
16181641

1619-
async def readline(self, size: int = -1) -> bytes: # type: ignore[override]
1620-
"""Read one line or up to `size` bytes from the file.
1621-
1622-
:param size: the maximum number of bytes to read
1623-
"""
1624-
return await self._read_size_or_line(size=size, line=True)
1625-
16261642
def tell(self) -> int:
16271643
"""Return the current position of this file."""
16281644
return self._position
16291645

1630-
async def seek(self, pos: int, whence: int = _SEEK_SET) -> int: # type: ignore[override]
1646+
async def seek(self, pos: int, whence: int = _SEEK_SET) -> int:
16311647
"""Set the current position of this file.
16321648
16331649
:param pos: the position (or offset if using relative
@@ -1690,12 +1706,15 @@ def __aiter__(self) -> AsyncGridOut:
16901706
"""
16911707
return self
16921708

1693-
async def close(self) -> None: # type: ignore[override]
1709+
async def close(self) -> None:
16941710
"""Make GridOut more generically file-like."""
16951711
if self._chunk_iter:
16961712
await self._chunk_iter.close()
16971713
self._chunk_iter = None
1698-
super().close()
1714+
if _IS_SYNC:
1715+
super().close()
1716+
else:
1717+
self.closed = True
16991718

17001719
def write(self, value: Any) -> NoReturn:
17011720
raise io.UnsupportedOperation("write")

gridfs/grid_file_shared.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,31 @@ def _a_grid_in_property(
3838
) -> Any:
3939
"""Create a GridIn property."""
4040

41+
warn_str = ""
42+
if docstring.startswith("DEPRECATED,"):
43+
warn_str = (
44+
f"GridIn property '{field_name}' is deprecated and will be removed in PyMongo 5.0"
45+
)
46+
4147
def getter(self: Any) -> Any:
48+
if warn_str:
49+
warnings.warn(warn_str, stacklevel=2, category=DeprecationWarning)
4250
if closed_only and not self._closed:
4351
raise AttributeError("can only get %r on a closed file" % field_name)
4452
# Protect against PHP-237
4553
if field_name == "length":
4654
return self._file.get(field_name, 0)
4755
return self._file.get(field_name, None)
4856

57+
def setter(self: Any, value: Any) -> Any:
58+
if warn_str:
59+
warnings.warn(warn_str, stacklevel=2, category=DeprecationWarning)
60+
if self._closed:
61+
raise InvalidOperation(
62+
"AsyncGridIn does not support __setattr__ after being closed(). Set the attribute before closing the file or use AsyncGridIn.set() instead"
63+
)
64+
self._file[field_name] = value
65+
4966
if read_only:
5067
docstring += "\n\nThis attribute is read-only."
5168
elif closed_only:
@@ -56,6 +73,8 @@ def getter(self: Any) -> Any:
5673
"has been called.",
5774
)
5875

76+
if not read_only and not closed_only:
77+
return property(getter, setter, doc=docstring)
5978
return property(getter, doc=docstring)
6079

6180

gridfs/synchronous/grid_file.py

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,24 +1166,6 @@ def __getattr__(self, name: str) -> Any:
11661166
raise AttributeError("GridIn object has no attribute '%s'" % name)
11671167

11681168
def __setattr__(self, name: str, value: Any) -> None:
1169-
# For properties of this instance like _buffer, or descriptors set on
1170-
# the class like filename, use regular __setattr__
1171-
if name in self.__dict__ or name in self.__class__.__dict__:
1172-
object.__setattr__(self, name, value)
1173-
else:
1174-
if _IS_SYNC:
1175-
# All other attributes are part of the document in db.fs.files.
1176-
# Store them to be sent to server on close() or if closed, send
1177-
# them now.
1178-
self._file[name] = value
1179-
if self._closed:
1180-
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1181-
else:
1182-
raise AttributeError(
1183-
"GridIn does not support __setattr__. Use GridIn.set() instead"
1184-
)
1185-
1186-
def set(self, name: str, value: Any) -> None:
11871169
# For properties of this instance like _buffer, or descriptors set on
11881170
# the class like filename, use regular __setattr__
11891171
if name in self.__dict__ or name in self.__class__.__dict__:
@@ -1194,7 +1176,17 @@ def set(self, name: str, value: Any) -> None:
11941176
# them now.
11951177
self._file[name] = value
11961178
if self._closed:
1197-
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1179+
if _IS_SYNC:
1180+
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1181+
else:
1182+
raise AttributeError(
1183+
"GridIn does not support __setattr__ after being closed(). Set the attribute before closing the file or use GridIn.set() instead"
1184+
)
1185+
1186+
def set(self, name: str, value: Any) -> None:
1187+
self._file[name] = value
1188+
if self._closed:
1189+
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
11981190

11991191
def _flush_data(self, data: Any, force: bool = False) -> None:
12001192
"""Flush `data` to a chunk."""
@@ -1388,7 +1380,11 @@ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any:
13881380
return False
13891381

13901382

1391-
class GridOut(io.IOBase):
1383+
GRIDOUT_BASE_CLASS = io.IOBase if _IS_SYNC else object # type: Any
1384+
1385+
1386+
class GridOut(GRIDOUT_BASE_CLASS): # type: ignore
1387+
13921388
"""Class to read data out of GridFS."""
13931389

13941390
def __init__(
@@ -1448,6 +1444,8 @@ def __init__(
14481444
self._position = 0
14491445
self._file = file_document
14501446
self._session = session
1447+
if not _IS_SYNC:
1448+
self.closed = False
14511449

14521450
_id: Any = _grid_out_property("_id", "The ``'_id'`` value for this file.")
14531451
filename: str = _grid_out_property("filename", "Name of this file.")
@@ -1474,14 +1472,43 @@ def __init__(
14741472
_file: Any
14751473
_chunk_iter: Any
14761474

1477-
def __next__(self) -> bytes:
1478-
return super().__next__()
1475+
if not _IS_SYNC:
1476+
closed: bool
14791477

1480-
def __next__(self) -> bytes: # noqa: F811, RUF100
1481-
if _IS_SYNC:
1482-
return super().__next__()
1483-
else:
1484-
raise TypeError("GridOut does not support synchronous iteration. Use `for` instead")
1478+
def __next__(self) -> bytes:
1479+
line = self.readline()
1480+
if line:
1481+
return line
1482+
raise StopIteration()
1483+
1484+
def to_list(self) -> list[bytes]:
1485+
return [x for x in self] # noqa: C416, RUF100
1486+
1487+
def readline(self, size: int = -1) -> bytes:
1488+
"""Read one line or up to `size` bytes from the file.
1489+
1490+
:param size: the maximum number of bytes to read
1491+
"""
1492+
return self._read_size_or_line(size=size, line=True)
1493+
1494+
def readlines(self, size: int = -1) -> list[bytes]:
1495+
"""Read one line or up to `size` bytes from the file.
1496+
1497+
:param size: the maximum number of bytes to read
1498+
"""
1499+
self.open()
1500+
lines = []
1501+
remainder = int(self.length) - self._position
1502+
bytes_read = 0
1503+
while remainder > 0:
1504+
line = self._read_size_or_line(line=True)
1505+
bytes_read += len(line)
1506+
lines.append(line)
1507+
remainder = int(self.length) - self._position
1508+
if 0 < size < bytes_read:
1509+
break
1510+
1511+
return lines
14851512

14861513
def open(self) -> None:
14871514
if not self._file:
@@ -1602,18 +1629,11 @@ def read(self, size: int = -1) -> bytes:
16021629
"""
16031630
return self._read_size_or_line(size=size)
16041631

1605-
def readline(self, size: int = -1) -> bytes: # type: ignore[override]
1606-
"""Read one line or up to `size` bytes from the file.
1607-
1608-
:param size: the maximum number of bytes to read
1609-
"""
1610-
return self._read_size_or_line(size=size, line=True)
1611-
16121632
def tell(self) -> int:
16131633
"""Return the current position of this file."""
16141634
return self._position
16151635

1616-
def seek(self, pos: int, whence: int = _SEEK_SET) -> int: # type: ignore[override]
1636+
def seek(self, pos: int, whence: int = _SEEK_SET) -> int:
16171637
"""Set the current position of this file.
16181638
16191639
:param pos: the position (or offset if using relative
@@ -1676,12 +1696,15 @@ def __iter__(self) -> GridOut:
16761696
"""
16771697
return self
16781698

1679-
def close(self) -> None: # type: ignore[override]
1699+
def close(self) -> None:
16801700
"""Make GridOut more generically file-like."""
16811701
if self._chunk_iter:
16821702
self._chunk_iter.close()
16831703
self._chunk_iter = None
1684-
super().close()
1704+
if _IS_SYNC:
1705+
super().close()
1706+
else:
1707+
self.closed = True
16851708

16861709
def write(self, value: Any) -> NoReturn:
16871710
raise io.UnsupportedOperation("write")

pymongo/asynchronous/helpers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,13 @@ async def inner(*args: Any, **kwargs: Any) -> Any:
7070

7171
if sys.version_info >= (3, 10):
7272
anext = builtins.anext
73+
aiter = builtins.aiter
7374
else:
7475

7576
async def anext(cls: Any) -> Any:
7677
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#anext."""
7778
return await cls.__anext__()
79+
80+
def aiter(cls: Any) -> Any:
81+
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#anext."""
82+
return cls.__aiter__()

pymongo/asynchronous/topology.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ async def _process_change(
521521
if server:
522522
await server.pool.reset(interrupt_connections=interrupt_connections)
523523

524-
# Wake waiters in select_servers().
524+
# Wake anything waiting in select_servers().
525525
self._condition.notify_all()
526526

527527
async def on_change(

pymongo/synchronous/helpers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,13 @@ def inner(*args: Any, **kwargs: Any) -> Any:
7070

7171
if sys.version_info >= (3, 10):
7272
next = builtins.next
73+
iter = builtins.iter
7374
else:
7475

7576
def next(cls: Any) -> Any:
7677
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#next."""
7778
return cls.__next__()
79+
80+
def iter(cls: Any) -> Any:
81+
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#next."""
82+
return cls.__iter__()

0 commit comments

Comments
 (0)