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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions aiofiledol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import asyncio
import os

from dol import wrapped_self
from dol.base import KvReader, KvPersister
from dol.dig import inner_most_key
from dol.paths import mk_relative_path_store
from dol.filesys import (
FileCollection,
Expand All @@ -22,6 +24,22 @@
_dflt_not_found_error_msg = "Key not found: {}"


def _resolve_key(store, k):
"""The leaf-level filepath for ``k``, resolved through any wrapping layers.

``mk_relative_path_store`` (and dol's wrapping machinery generally) wraps by
delegation: a method defined on the wrapped class is reached bound to the INNER
store, but with the OUTER, un-transformed key. Handing that key straight to
``AIOFile`` would resolve it against the process CWD instead of ``rootdir``.
Walking the wrapper chain with ``inner_most_key`` recovers the real filepath.

This REPLACES ``store._id_of_key(k)`` -- it walks the whole chain including the
leaf, so the two must never be composed. For an un-wrapped store no layer defines
``_id_of_key`` and ``k`` is returned verbatim.
"""
return inner_most_key(wrapped_self(store), k, default=k)


class AioFileBytesReader(FileCollection, KvReader):
_read_open_kwargs = dict(mode="rb")

Expand All @@ -46,7 +64,7 @@ async def aget(self, k): # noqa
b'hello world'
"""

async with AIOFile(k, **self._read_open_kwargs) as fp:
async with AIOFile(_resolve_key(self, k), **self._read_open_kwargs) as fp:
v = await fp.read() # Question: Is it faster if we just did `return await fp.read(), instead of assign?
return v
# with open(k, **self._read_open_kwargs) as fp:
Expand Down Expand Up @@ -102,7 +120,7 @@ async def asetitem(self, k, v):
>>> asyncio.run(s.aget(k)) # read it back (async reader; __getitem__ is disabled)
b'bar'
"""
async with AIOFile(k, **self._write_open_kwargs) as fp:
async with AIOFile(_resolve_key(self, k), **self._write_open_kwargs) as fp:
await fp.write(v)
await fp.fsync()

Expand Down
1 change: 1 addition & 0 deletions aiofiledol/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for aiofiledol"""
75 changes: 75 additions & 0 deletions aiofiledol/tests/test_relative_path_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Tests that relativized stores read the file they advertise, not a CWD lookalike.

``mk_relative_path_store`` wraps by delegation, so a method defined on the wrapped
class (such as ``aget``) is reached bound to the INNER store while still receiving
the OUTER, un-transformed key. Passing that relative key straight to ``AIOFile``
resolves it against the process CWD instead of the store's ``rootdir`` -- silently
returning the wrong bytes when a same-named file happens to sit in the CWD, and
raising ``FileNotFoundError`` for a key the store just enumerated when it does not.
"""

import asyncio
import os

from aiofiledol import (
AioFileBytesReader,
RelPathAioFileBytesReader,
RelPathFileStringReader,
)

KEY = "greeting"
REAL = b"REAL STORE CONTENT"
DECOY = b"DECOY FROM CWD"


def _store_dir(tmp_path, name, content=None):
"""Make a directory under ``tmp_path``, optionally holding a ``KEY`` file.

Returns the path with a trailing separator -- what the relativized stores want,
so that the keys they yield carry no leading separator.
"""
d = tmp_path / name
d.mkdir()
if content is not None:
mode = "wb" if isinstance(content, bytes) else "wt"
with open(d / KEY, mode) as fp:
fp.write(content)
return str(d) + os.sep


def test_relpath_aget_reads_the_store_not_the_cwd(tmp_path, monkeypatch):
"""A relative key must resolve against ``rootdir``, never against the CWD."""
rootdir = _store_dir(tmp_path, "realstore", REAL)
monkeypatch.chdir(_store_dir(tmp_path, "decoy", DECOY))

s = RelPathAioFileBytesReader(rootdir)
assert asyncio.run(s.aget(KEY)) == REAL


def test_relpath_aget_finds_every_key_it_enumerates(tmp_path, monkeypatch):
"""Every key the store yields must be readable -- no ``FileNotFoundError``."""
rootdir = _store_dir(tmp_path, "realstore", REAL)
monkeypatch.chdir(_store_dir(tmp_path, "elsewhere"))

s = RelPathAioFileBytesReader(rootdir)
assert list(s) == [KEY]
for k in s:
assert asyncio.run(s.aget(k)) == REAL


def test_relpath_string_reader_too(tmp_path, monkeypatch):
"""The text-mode relativized reader inherits the same ``aget``, so same rule."""
rootdir = _store_dir(tmp_path, "realstore", REAL.decode())
monkeypatch.chdir(_store_dir(tmp_path, "decoy", DECOY.decode()))

s = RelPathFileStringReader(rootdir)
assert asyncio.run(s.aget(KEY)) == REAL.decode()


def test_bare_reader_still_takes_absolute_keys(tmp_path, monkeypatch):
"""Regression guard: an un-wrapped store keeps its absolute-key behaviour."""
rootdir = _store_dir(tmp_path, "realstore", REAL)
monkeypatch.chdir(_store_dir(tmp_path, "elsewhere"))

s = AioFileBytesReader(rootdir, max_levels=0)
assert asyncio.run(s.aget(os.path.join(rootdir, KEY))) == REAL
Loading