Skip to content

Commit 5ec4038

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
1 parent 64d99d6 commit 5ec4038

5 files changed

Lines changed: 109 additions & 1 deletion

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ class InMemoryController(FileController, model.LocalFile):
2121
class Config:
2222
arbitrary_types_allowed = True
2323

24+
@property
25+
def uri(self) -> None:
26+
"""None, a stream in memory has no location to address"""
27+
return None
28+
2429
def get(self) -> IO:
2530
return self.stream
2631

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

0 commit comments

Comments
 (0)