Skip to content

Commit 998e846

Browse files
committed
feat(controller): add a uri property to every file controller
- file:// for LocalFileController, via Path.as_uri - the stored url for RemoteFileController, so s3:// and https:// follow - None for InMemoryController, a stream has no location - fix InMemoryController, which could not be constructed at all
1 parent 0d32bc2 commit 998e846

5 files changed

Lines changed: 123 additions & 4 deletions

File tree

‎src/osw/controller/file/base.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
from abc import abstractmethod
2-
from typing import IO, Any, Dict
2+
from typing import IO, Any, Dict, Optional
33

44
from osw.core import model
55

66

77
class FileController(model.File):
88
"""Base class for file controllers"""
99

10+
@property
11+
@abstractmethod
12+
def uri(self) -> Optional[str]:
13+
"""The location of the file, in the URI scheme of its storage backend
14+
15+
Returns
16+
-------
17+
the URI, or None if the file is not stored anywhere addressable
18+
"""
19+
1020
@abstractmethod
1121
def get(self) -> IO:
1222
"""Reads or downloads a file from an IO object

‎src/osw/controller/file/local.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ def __init__(self, **kwargs):
2929
super().__init__(**kwargs)
3030
self._set_metadata()
3131

32+
@property
33+
def uri(self) -> str:
34+
"""The file URI of the path, see
35+
https://en.wikipedia.org/wiki/File_URI_scheme"""
36+
return Path(self.path).absolute().as_uri()
37+
3238
def get(self) -> IO:
3339
self._set_metadata()
3440
return open(self.path, "rb")

‎src/osw/controller/file/memory.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,25 @@ class InMemoryController(FileController, model.LocalFile):
1212
label: Optional[List[model.Label]] = [model.Label(text="Unnamed stream")]
1313
"""the label of the stream, e.g., the name of the file the stream
1414
originates from. Defaults to 'Unnamed stream'."""
15-
stream: IO
16-
"""the stream to the file"""
15+
stream: Any
16+
"""the stream to the file, defaults to an empty StringIO. Not annotated as IO
17+
because pydantic validates an arbitrary type by isinstance, and no stream is
18+
an instance of typing.IO."""
1719

1820
class Config:
1921
arbitrary_types_allowed = True
2022

2123
def __init__(self, **kwargs):
22-
self.stream = StringIO()
24+
# the default has to go through the constructor: assigning it beforehand
25+
# runs pydantic's __setattr__ on a model that is not initialized yet
26+
kwargs.setdefault("stream", StringIO())
2327
super().__init__(**kwargs)
2428

29+
@property
30+
def uri(self) -> None:
31+
"""None, a stream in memory has no location to address"""
32+
return None
33+
2534
def get(self) -> IO:
2635
return self.stream
2736

‎src/osw/controller/file/remote.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
# The data class must be the first base class, otherwise subclass controllers fall back
1313
# to the data model of the controller superclass
1414
class RemoteFileController(model.RemoteFile, FileController):
15+
@property
16+
def uri(self) -> str:
17+
"""The url the file is stored at, e.g., 's3://' for S3FileController and
18+
'https://' for WikiFileController"""
19+
return self.url
20+
1521
@abstractmethod
1622
def get(self) -> IO:
1723
pass

‎tests/test_file_controller_uri.py‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Offline tests for the uri property of the file controllers.
2+
3+
Covers #68: every controller reports where its file lives, in the URI scheme of
4+
its own storage backend.
5+
"""
6+
7+
from io import StringIO
8+
from pathlib import Path
9+
from types import SimpleNamespace
10+
from typing import IO, Any, Dict
11+
12+
from osw.controller.file.local import LocalFileController
13+
from osw.controller.file.memory import InMemoryController
14+
from osw.controller.file.remote import RemoteFileController
15+
from osw.controller.file.wiki import WikiFileController
16+
from osw.core import model
17+
18+
19+
class OfflineRemoteFileController(RemoteFileController):
20+
"""A remote controller carrying nothing but a url, as model.S3File does"""
21+
22+
url: str
23+
24+
def get(self) -> IO:
25+
pass
26+
27+
def put(self, file: IO, **kwargs: Dict[str, Any]):
28+
pass
29+
30+
31+
def test_local_uri_is_a_file_url(tmp_path):
32+
path = tmp_path / "test.txt"
33+
path.write_text("content", encoding="utf-8")
34+
35+
uri = LocalFileController(path=path).uri
36+
37+
assert uri.startswith("file:///")
38+
assert uri.endswith("/test.txt")
39+
40+
41+
def test_local_uri_is_absolute_for_a_relative_path(tmp_path, monkeypatch):
42+
"""as_uri() rejects a relative path, so the path has to be resolved first."""
43+
monkeypatch.chdir(tmp_path)
44+
45+
uri = LocalFileController(path=Path("test.txt")).uri
46+
47+
assert uri == (Path.cwd() / "test.txt").as_uri()
48+
49+
50+
def test_local_uri_escapes_reserved_characters(tmp_path):
51+
uri = LocalFileController(path=tmp_path / "a file.txt").uri
52+
53+
assert uri.endswith("/a%20file.txt")
54+
55+
56+
def test_in_memory_uri_is_none():
57+
"""A stream has no location, so there is nothing to point at."""
58+
assert InMemoryController().uri is None
59+
60+
61+
def test_in_memory_keeps_the_stream_it_is_given():
62+
stream = StringIO("content")
63+
64+
assert InMemoryController(stream=stream).get() is stream
65+
66+
67+
def test_remote_uri_is_the_url():
68+
url = "s3://s3.example.org/bucket/OSW0000.txt"
69+
controller = OfflineRemoteFileController(
70+
url=url, label=[model.Label(text="OSW0000.txt")]
71+
)
72+
73+
assert controller.uri == url
74+
75+
76+
def test_wiki_uri_is_the_file_page_url():
77+
"""WikiFileController builds url itself, so uri needs no override there."""
78+
page = SimpleNamespace(
79+
osw=SimpleNamespace(
80+
mw_site=SimpleNamespace(scheme="https", host="wiki.example.org")
81+
),
82+
title="OSW0000.txt",
83+
)
84+
85+
url = WikiFileController.url.fget(page)
86+
87+
assert url == "https://wiki.example.org/wiki/File:OSW0000.txt"
88+
assert RemoteFileController.uri.fget(SimpleNamespace(url=url)) == url

0 commit comments

Comments
 (0)