flake8 reported 49 findings and pyright 10, and two of them were real bugs rather than matters of style. The rest were unused imports and four functions over the complexity limit. - Unlock a DOI folder with the id the DOI response carries, rather than an attribute the model does not have, which raised `AttributeError` on every locked DOI folder. - Size the `ls` Size column from the sub-folder on the row rather than from its parent, which pushed the later columns out of line. - Share the path resolution and the destination checks between `cp` and `mv`, and split the recursive download and the `ls` row printing, bringing all four functions under the complexity limit. - Accept a client built without a connection, which `config` and `version` rely on, and report the reason if one is then asked for. - Declare the config protocol's constructor for the type checker alone, so the protocol keeps its guard against being instantiated. - Remove 41 unused imports, and let flake8 accept black's spacing. - Point pyright at the project's own environment, without which it resolved no dependency and reported 124 findings that were not real. - Cover the two fixes and the shared `cp`/`mv` paths with new tests.
166 lines
6.8 KiB
Python
166 lines
6.8 KiB
Python
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from mdrsclient.client import MdrsClient
|
|
from mdrsclient.exceptions import IllegalArgumentException
|
|
from mdrsclient.models import File, Folder, FolderSimple, Laboratories, Laboratory
|
|
|
|
TIMESTAMP = "2026-01-01T00:00:00+09:00"
|
|
LABORATORY = Laboratory(id=1, name="mylab", pi_name="PI", full_name="My Laboratory")
|
|
|
|
|
|
def make_file(id: str, name: str) -> File:
|
|
return File(
|
|
id=id,
|
|
name=name,
|
|
type="text/plain",
|
|
size=1,
|
|
thumbnail=None,
|
|
description="",
|
|
metadata={},
|
|
download_url=f"v3/files/{id}/download/",
|
|
created_at=TIMESTAMP,
|
|
updated_at=TIMESTAMP,
|
|
)
|
|
|
|
|
|
def make_sub_folder(id: str, name: str) -> FolderSimple:
|
|
return FolderSimple(
|
|
id=id,
|
|
pid=None,
|
|
name=name,
|
|
access_level=1,
|
|
lock=False,
|
|
size=0,
|
|
laboratory_id=1,
|
|
description="",
|
|
created_at=TIMESTAMP,
|
|
updated_at=TIMESTAMP,
|
|
restrict_opened_at=None,
|
|
)
|
|
|
|
|
|
def make_folder(id: str, name: str, path: str, sub_folders: list[FolderSimple] | None = None) -> Folder:
|
|
return Folder(
|
|
id=id,
|
|
pid=None,
|
|
name=name,
|
|
access_level=1,
|
|
lock=False,
|
|
size=0,
|
|
laboratory_id=1,
|
|
description="",
|
|
created_at=TIMESTAMP,
|
|
updated_at=TIMESTAMP,
|
|
restrict_opened_at=None,
|
|
metadata=[],
|
|
sub_folders=sub_folders if sub_folders is not None else [],
|
|
path=path,
|
|
)
|
|
|
|
|
|
class TestLsColumnWidths(unittest.TestCase):
|
|
"""Each column is as wide as the widest thing printed under it."""
|
|
|
|
def test_the_size_column_fits_a_sub_folder_larger_than_its_parent(self):
|
|
from unittest.mock import MagicMock as _MagicMock
|
|
|
|
from mdrsclient.commands.ls import LsCommand, LsCommandContext
|
|
|
|
laboratories = Laboratories()
|
|
laboratories.append(LABORATORY)
|
|
connection = _MagicMock()
|
|
connection.laboratories = laboratories
|
|
client = MdrsClient(connection)
|
|
|
|
big = make_sub_folder("s1", "big")
|
|
object.__setattr__(big, "size", 123456789)
|
|
parent = make_folder("f1", "root", "/root/", [big])
|
|
context = LsCommandContext("remote:/mylab", client, LABORATORY, "", False, False, False)
|
|
|
|
widths = LsCommand._column_widths(context, parent, [])
|
|
|
|
self.assertEqual(widths["size"], len("123456789"))
|
|
|
|
|
|
class TestCopyAndMove(unittest.TestCase):
|
|
"""`cp` and `mv` resolve both ends the same way and differ only in what they call."""
|
|
|
|
def make_client(self, folders_by_path: dict, files_by_folder: dict) -> MdrsClient:
|
|
client = MdrsClient(MagicMock())
|
|
client.find_laboratory = MagicMock(return_value=LABORATORY)
|
|
client.find_folder = MagicMock(side_effect=lambda lab, path, password=None: folders_by_path[path])
|
|
client.find_files = MagicMock(side_effect=lambda folder_id: files_by_folder.get(folder_id, []))
|
|
return client
|
|
|
|
def make_tree(self, source_has: str) -> tuple[MdrsClient, dict, dict]:
|
|
sub = make_sub_folder("fsub", "sub")
|
|
folder_a = make_folder("fa", "a", "/a/", [sub] if source_has == "folder" else [])
|
|
folder_b = make_folder("fb", "b", "/b/")
|
|
folders = {"/a": folder_a, "/b": folder_b, "/b/": folder_b}
|
|
files = {"fa": [make_file("x1", "data.txt")] if source_has == "file" else [], "fb": []}
|
|
return self.make_client(folders, files), folders, files
|
|
|
|
def test_a_file_is_copied_into_the_destination_folder(self):
|
|
client, _, files = self.make_tree("file")
|
|
with patch("mdrsclient.client.FilesApi") as files_api:
|
|
client.cp("myremote:/mylab/a/data.txt", "myremote:/mylab/b/data.txt")
|
|
files_api.return_value.copy.assert_called_once_with(files["fa"][0], "fb", "data.txt")
|
|
|
|
def test_a_file_is_moved_into_the_destination_folder(self):
|
|
client, _, files = self.make_tree("file")
|
|
with patch("mdrsclient.client.FilesApi") as files_api:
|
|
client.mv("myremote:/mylab/a/data.txt", "myremote:/mylab/b/data.txt")
|
|
files_api.return_value.move.assert_called_once_with(files["fa"][0], "fb", "data.txt")
|
|
|
|
def test_a_trailing_separator_keeps_the_source_name(self):
|
|
client, _, files = self.make_tree("file")
|
|
with patch("mdrsclient.client.FilesApi") as files_api:
|
|
client.mv("myremote:/mylab/a/data.txt", "myremote:/mylab/b/")
|
|
files_api.return_value.move.assert_called_once_with(files["fa"][0], "fb", "data.txt")
|
|
|
|
def test_copying_a_folder_needs_the_recursive_flag(self):
|
|
client, _, _ = self.make_tree("folder")
|
|
with patch("mdrsclient.client.FoldersApi") as folders_api:
|
|
with self.assertRaises(IllegalArgumentException) as caught:
|
|
client.cp("myremote:/mylab/a/sub", "myremote:/mylab/b/sub")
|
|
self.assertIn("Is a folder", str(caught.exception))
|
|
folders_api.return_value.copy.assert_not_called()
|
|
|
|
def test_a_folder_is_copied_when_recursive(self):
|
|
client, folders, _ = self.make_tree("folder")
|
|
with patch("mdrsclient.client.FoldersApi") as folders_api:
|
|
client.cp("myremote:/mylab/a/sub", "myremote:/mylab/b/sub", is_recursive=True)
|
|
folders_api.return_value.copy.assert_called_once_with(folders["/a"].sub_folders[0], "fb", "sub")
|
|
|
|
def test_a_folder_is_moved_without_the_recursive_flag(self):
|
|
client, folders, _ = self.make_tree("folder")
|
|
with patch("mdrsclient.client.FoldersApi") as folders_api:
|
|
client.mv("myremote:/mylab/a/sub", "myremote:/mylab/b/sub")
|
|
folders_api.return_value.move.assert_called_once_with(folders["/a"].sub_folders[0], "fb", "sub")
|
|
|
|
def test_a_destination_that_already_holds_the_name_is_refused(self):
|
|
sub = make_sub_folder("fsub", "sub")
|
|
folders = {"/a": make_folder("fa", "a", "/a/", [sub]), "/b": make_folder("fb", "b", "/b/")}
|
|
files = {"fa": [], "fb": [make_file("x2", "sub")]}
|
|
client = self.make_client(folders, files)
|
|
with self.assertRaises(IllegalArgumentException) as caught:
|
|
client.mv("myremote:/mylab/a/sub", "myremote:/mylab/b/sub")
|
|
self.assertIn("Cannot overwrite non-folder", str(caught.exception))
|
|
|
|
def test_a_transfer_across_laboratories_is_refused(self):
|
|
client, _, _ = self.make_tree("file")
|
|
with self.assertRaises(IllegalArgumentException) as caught:
|
|
client.cp("myremote:/mylab/a/data.txt", "myremote:/otherlab/b/data.txt")
|
|
self.assertIn("Laboratory mismatched", str(caught.exception))
|
|
|
|
def test_a_source_that_does_not_exist_is_refused(self):
|
|
client, _, _ = self.make_tree("file")
|
|
with self.assertRaises(IllegalArgumentException) as caught:
|
|
client.mv("myremote:/mylab/a/missing.txt", "myremote:/mylab/b/missing.txt")
|
|
self.assertIn("not found", str(caught.exception))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|