From 789fe31acaafa41f79b3128876fceeb5a29be1e6 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Tue, 29 Oct 2024 17:08:54 +0100 Subject: [PATCH] Fix typing issues in tests --- pyproject.toml | 5 - src/scitacean/testing/backend/config.py | 28 +++-- src/scitacean/testing/backend/fixtures.py | 6 +- src/scitacean/testing/backend/seed.py | 6 +- src/scitacean/testing/transfer.py | 13 +- tests/client/attachment_client_test.py | 45 ++++--- tests/client/client_test.py | 27 ++-- tests/client/datablock_client_test.py | 18 +-- tests/client/dataset_client_test.py | 37 ++++-- tests/client/query_client_test.py | 24 ++-- tests/client/sample_client_test.py | 33 +++-- tests/dataset_fields_test.py | 54 ++++---- tests/dataset_test.py | 147 +++++++++++++--------- tests/download_test.py | 97 +++++++++----- tests/file_test.py | 70 ++++++----- tests/filesystem_test.py | 50 ++++---- tests/html_repr/html_repr_test.py | 4 +- tests/model_test.py | 43 +++++-- tests/testing/strategies_test.py | 13 +- tests/thumbnail_test.py | 37 +++--- tests/transfer/link_test.py | 15 +-- tests/transfer/sftp_test.py | 26 ++-- tests/upload_test.py | 114 +++++++++++------ tests/util/formatter_test.py | 14 +-- 24 files changed, 567 insertions(+), 359 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 34c8fca9..12cfa9ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,11 +85,6 @@ strict = true show_error_codes = true warn_unreachable = true -[[tool.mypy.overrides]] -module = "tests.*" -disallow_untyped_defs = false -disallow_untyped_calls = false - [tool.pydantic-mypy] init_forbid_extra = true init_typed = true diff --git a/src/scitacean/testing/backend/config.py b/src/scitacean/testing/backend/config.py index c7ed8583..98dc3090 100644 --- a/src/scitacean/testing/backend/config.py +++ b/src/scitacean/testing/backend/config.py @@ -5,10 +5,18 @@ import json from dataclasses import dataclass from pathlib import Path +from typing import TypedDict + + +class ScicatCredentials(TypedDict): + """A dict with testing credentials for SciCat.""" + + username: str + password: str @dataclass -class SciCatUser: +class ScicatUser: """A SciCat user. Warning @@ -24,7 +32,7 @@ class SciCatUser: group: str @property - def credentials(self) -> dict[str, str]: + def credentials(self) -> ScicatCredentials: """Return login credentials for this user. User as @@ -51,43 +59,43 @@ def dump(self) -> dict[str, str | bool]: # see https://github.com/SciCatProject/scicat-backend-next/blob/master/src/config/configuration.ts USERS = { - "ingestor": SciCatUser( + "ingestor": ScicatUser( username="ingestor", password="aman", # noqa: S106 email="scicatingestor@your.site", group="ingestor", ), - "user1": SciCatUser( + "user1": ScicatUser( username="user1", password="a609316768619f154ef58db4d847b75e", # noqa: S106 email="user1@your.site", group="group1", ), - "user2": SciCatUser( + "user2": ScicatUser( username="user2", password="f522d1d715970073a6413474ca0e0f63", # noqa: S106 email="user2@your.site", group="group2", ), - "user3": SciCatUser( + "user3": ScicatUser( username="user3", password="70dc489e8ee823ae815e18d664424df2", # noqa: S106 email="user3@your.site", group="group3", ), - "user4": SciCatUser( + "user4": ScicatUser( username="user4", password="0014890e7020f515b92b767227ef2dfa", # noqa: S106 email="user4@your.site", group="group4", ), - "user5.1": SciCatUser( + "user5.1": ScicatUser( username="user5.1", password="359a5fda99bfe5dbc42ee9b3ede77fb7", # noqa: S106 email="user5.1@your.site", group="group5", ), - "user5.2": SciCatUser( + "user5.2": ScicatUser( username="user5.2", password="f3ebd2e4def95db59ef95ee32ef45242", # noqa: S106 email="user5.2@your.site", @@ -109,7 +117,7 @@ class SciCatAccess: """Access parameters for a local SciCat backend.""" url: str - user: SciCatUser + user: ScicatUser def local_access(user: str) -> SciCatAccess: diff --git a/src/scitacean/testing/backend/fixtures.py b/src/scitacean/testing/backend/fixtures.py index eb5334f0..b0dffd2e 100644 --- a/src/scitacean/testing/backend/fixtures.py +++ b/src/scitacean/testing/backend/fixtures.py @@ -52,7 +52,7 @@ def fake_client(scicat_access: SciCatAccess) -> FakeClient: """ client = FakeClient.from_credentials( url=scicat_access.url, - **scicat_access.user.credentials, # type: ignore[arg-type] + **scicat_access.user.credentials, ) client.datasets.update( {ds.pid: ds for ds in seed.INITIAL_DATASETS.values()} # type: ignore[misc] @@ -87,7 +87,7 @@ def real_client(scicat_access: SciCatAccess, scicat_backend: bool) -> Client | N return None return Client.from_credentials( url=scicat_access.url, - **scicat_access.user.credentials, # type: ignore[arg-type] + **scicat_access.user.credentials, ) @@ -200,7 +200,7 @@ def _seed_database( ) -> None: client = client_class.from_credentials( url=scicat_access.url, - **scicat_access.user.credentials, # type: ignore[arg-type] + **scicat_access.user.credentials, ) seed.seed_database(client=client, scicat_access=scicat_access) seed.save_seed(target_dir) diff --git a/src/scitacean/testing/backend/seed.py b/src/scitacean/testing/backend/seed.py index 4d8f188e..8ddbba86 100644 --- a/src/scitacean/testing/backend/seed.py +++ b/src/scitacean/testing/backend/seed.py @@ -30,7 +30,7 @@ ) from ...pid import PID from ...thumbnail import Thumbnail -from .config import SITE, SciCatAccess, SciCatUser +from .config import SITE, SciCatAccess, ScicatUser # Dataset models to upload to the database. _DATASETS: dict[str, UploadRawDataset | UploadDerivedDataset] = { @@ -215,7 +215,7 @@ def _apply_config_dataset( - dset: UploadRawDataset | UploadDerivedDataset, user: SciCatUser + dset: UploadRawDataset | UploadDerivedDataset, user: ScicatUser ) -> UploadRawDataset | UploadDerivedDataset: dset = deepcopy(dset) dset.owner = user.username @@ -225,7 +225,7 @@ def _apply_config_dataset( def _apply_config_attachment( - attachment: UploadAttachment, user: SciCatUser + attachment: UploadAttachment, user: ScicatUser ) -> UploadAttachment: attachment = deepcopy(attachment) attachment.ownerGroup = user.group diff --git a/src/scitacean/testing/transfer.py b/src/scitacean/testing/transfer.py index 183044d3..11eadd96 100644 --- a/src/scitacean/testing/transfer.py +++ b/src/scitacean/testing/transfer.py @@ -2,10 +2,10 @@ # Copyright (c) 2024 SciCat Project (https://github.com/SciCatProject/scitacean) """Fake file transfer.""" -from collections.abc import Iterator +from collections.abc import Iterator, Mapping from contextlib import contextmanager from pathlib import Path -from typing import Any +from typing import Any, TypeVar try: from pyfakefs.fake_filesystem import FakeFilesystem @@ -19,7 +19,10 @@ from ..filesystem import RemotePath from ..transfer.util import source_folder_for +RemotePathOrStr = TypeVar("RemotePathOrStr", RemotePath, str, RemotePath | str) + +# TODO add conditionally_disabled feature and remove custom transfers in tests class FakeDownloadConnection: """'Download' files from a fake file transfer.""" @@ -110,8 +113,8 @@ def __init__( self, *, fs: FakeFilesystem | None = None, - files: dict[str | RemotePath, bytes] | None = None, - reverted: dict[str | RemotePath, bytes] | None = None, + files: Mapping[RemotePathOrStr, bytes] | None = None, + reverted: Mapping[RemotePathOrStr, bytes] | None = None, source_folder: str | RemotePath | None = None, ): """Initialize a file transfer. @@ -157,7 +160,7 @@ def connect_for_upload(self, dataset: Dataset) -> Iterator[FakeUploadConnection] def _remote_path_dict( - d: dict[str | RemotePath, bytes] | None, + d: Mapping[RemotePathOrStr, bytes] | None, ) -> dict[RemotePath, bytes]: if d is None: return {} diff --git a/tests/client/attachment_client_test.py b/tests/client/attachment_client_test.py index c627c0d1..dbf81ada 100644 --- a/tests/client/attachment_client_test.py +++ b/tests/client/attachment_client_test.py @@ -12,9 +12,11 @@ from scitacean.model import ( Attachment, DatasetType, + DownloadAttachment, UploadAttachment, UploadDerivedDataset, ) +from scitacean.testing.backend import config as backend_config from scitacean.testing.backend.seed import ( INITIAL_ATTACHMENTS, INITIAL_DATASETS, @@ -27,7 +29,7 @@ def scicat_client(client: Client) -> ScicatClient: @pytest.fixture -def derived_dataset(scicat_access): +def derived_dataset(scicat_access: backend_config.SciCatAccess) -> UploadDerivedDataset: return UploadDerivedDataset( contactEmail="black.foess@dom.koelle", creationTime=parse_date("1995-11-11T11:11:11.000Z"), @@ -44,7 +46,7 @@ def derived_dataset(scicat_access): @pytest.fixture -def attachment(scicat_access): +def attachment(scicat_access: backend_config.SciCatAccess) -> UploadAttachment: return UploadAttachment( caption="An attachment", thumbnail=Thumbnail(mime="image/png", data=b"9278c78a904jh"), @@ -53,7 +55,9 @@ def attachment(scicat_access): ) -def compare_attachment_after_upload(uploaded, downloaded): +def compare_attachment_after_upload( + uploaded: UploadAttachment, downloaded: DownloadAttachment +) -> None: for key, expected in uploaded: # The database populates a number of fields that are None in uploaded. # But we don't want to test those here as we don't want to test the database. @@ -61,7 +65,11 @@ def compare_attachment_after_upload(uploaded, downloaded): assert expected == dict(downloaded)[key], f"key = {key}" -def test_create_attachment_for_dataset(scicat_client, attachment, derived_dataset): +def test_create_attachment_for_dataset( + scicat_client: ScicatClient, + attachment: UploadAttachment, + derived_dataset: UploadDerivedDataset, +) -> None: attachment1 = deepcopy(attachment) attachment2 = deepcopy(attachment) attachment2.caption = "Another attachment" @@ -82,8 +90,11 @@ def test_create_attachment_for_dataset(scicat_client, attachment, derived_datase def test_create_attachment_for_dataset_with_existing_id( - real_client, attachment, derived_dataset, require_scicat_backend -): + real_client: Client, + attachment: UploadAttachment, + derived_dataset: UploadDerivedDataset, + require_scicat_backend: None, +) -> None: scicat_client = real_client.scicat dataset_id = scicat_client.create_dataset_model(derived_dataset).pid @@ -95,8 +106,8 @@ def test_create_attachment_for_dataset_with_existing_id( def test_cannot_create_attachment_for_dataset_for_nonexistent_dataset( - scicat_client, attachment -): + scicat_client: ScicatClient, attachment: UploadAttachment +) -> None: with pytest.raises(ScicatCommError): scicat_client.create_attachment_for_dataset( attachment, dataset_id=PID(pid="nonexistent-id") @@ -104,8 +115,10 @@ def test_cannot_create_attachment_for_dataset_for_nonexistent_dataset( def test_create_attachment_for_dataset_for_dataset_populates_ids( - scicat_client, attachment, derived_dataset -): + scicat_client: ScicatClient, + attachment: UploadAttachment, + derived_dataset: UploadDerivedDataset, +) -> None: assert attachment.id is None assert attachment.datasetId is None assert attachment.sampleId is None @@ -123,13 +136,15 @@ def test_create_attachment_for_dataset_for_dataset_populates_ids( assert finalized.proposalId is None -def test_get_attachments_for_dataset(scicat_client): +def test_get_attachments_for_dataset(scicat_client: ScicatClient) -> None: dset = INITIAL_DATASETS["derived"] attachments = scicat_client.get_attachments_for_dataset(dset.pid) assert attachments == INITIAL_ATTACHMENTS["derived"] -def test_get_attachments_for_dataset_no_attachments(scicat_client): +def test_get_attachments_for_dataset_no_attachments( + scicat_client: ScicatClient, +) -> None: assert INITIAL_ATTACHMENTS.get("raw") is None dset = INITIAL_DATASETS["raw"] attachments = scicat_client.get_attachments_for_dataset(dset.pid) @@ -137,14 +152,14 @@ def test_get_attachments_for_dataset_no_attachments(scicat_client): @pytest.mark.parametrize("key", ["raw", "derived"]) -def test_get_dataset_does_not_initialise_attachments(client, key): +def test_get_dataset_does_not_initialise_attachments(client: Client, key: str) -> None: dset = INITIAL_DATASETS["derived"] downloaded = client.get_dataset(dset.pid) assert downloaded.attachments is None @pytest.mark.parametrize("key", ["raw", "derived"]) -def test_download_attachments_for_dataset(client, key): +def test_download_attachments_for_dataset(client: Client, key: str) -> None: dset = INITIAL_DATASETS[key] downloaded = client.get_dataset(dset.pid) with_attachments = client.download_attachments_for(downloaded) @@ -156,7 +171,7 @@ def test_download_attachments_for_dataset(client, key): @pytest.mark.parametrize("key", ["raw", "derived"]) -def test_get_dataset_with_attachments(client, key): +def test_get_dataset_with_attachments(client: Client, key: str) -> None: dset = INITIAL_DATASETS[key] downloaded = client.get_dataset(dset.pid, attachments=True) expected = [ diff --git a/tests/client/client_test.py b/tests/client/client_test.py index d11acfd2..2a3ddf29 100644 --- a/tests/client/client_test.py +++ b/tests/client/client_test.py @@ -11,18 +11,19 @@ import pytest from scitacean import PID, Client +from scitacean.testing.backend import config as backend_config from scitacean.testing.backend.seed import INITIAL_DATASETS from scitacean.testing.client import FakeClient from scitacean.util.credentials import SecretStr -def test_from_token_fake(): +def test_from_token_fake() -> None: # This should not call the API client = FakeClient.from_token(url="some.url/api/v3", token="a-token") # noqa: S106 assert isinstance(client, FakeClient) -def test_from_credentials_fake(): +def test_from_credentials_fake() -> None: # This should not call the API client = FakeClient.from_credentials( url="some.url/api/v3", @@ -35,23 +36,27 @@ def test_from_credentials_fake(): ) -def test_from_credentials_real(scicat_access, require_scicat_backend): +def test_from_credentials_real( + scicat_access: backend_config.SciCatAccess, require_scicat_backend: None +) -> None: Client.from_credentials(url=scicat_access.url, **scicat_access.user.credentials) -def test_cannot_pickle_client_credentials_manual_token_str(): +def test_cannot_pickle_client_credentials_manual_token_str() -> None: client = Client.from_token(url="/", token="the-token") # noqa: S106 with pytest.raises(TypeError): pickle.dumps(client) -def test_cannot_pickle_client_credentials_manual_token_secret_str(): +def test_cannot_pickle_client_credentials_manual_token_secret_str() -> None: client = Client.from_token(url="/", token=SecretStr("the-token")) with pytest.raises(TypeError): pickle.dumps(client) -def test_cannot_pickle_client_credentials_login(scicat_access, require_scicat_backend): +def test_cannot_pickle_client_credentials_login( + scicat_access: backend_config.SciCatAccess, require_scicat_backend: None +) -> None: client = Client.from_credentials( url=scicat_access.url, **scicat_access.user.credentials ) @@ -59,7 +64,7 @@ def test_cannot_pickle_client_credentials_login(scicat_access, require_scicat_ba pickle.dumps(client) -def test_connection_error_does_not_contain_token(): +def test_connection_error_does_not_contain_token() -> None: client = Client.from_token( url="https://not-actually-a_server", token="the token/which_must-be.kept secret", # noqa: S106 @@ -73,7 +78,7 @@ def test_connection_error_does_not_contain_token(): assert "the token/which_must-be.kept secret" not in str(arg) -def test_fake_can_disable_functions(): +def test_fake_can_disable_functions() -> None: client = FakeClient( disable={ "get_dataset_model": RuntimeError("custom failure"), @@ -113,13 +118,15 @@ def make_token(exp_in: timedelta) -> str: return ".".join((encode_jwt_part(header), encode_jwt_part(payload), signature)) -def test_detects_expired_token_init(): +def test_detects_expired_token_init() -> None: token = make_token(timedelta(milliseconds=0)) with pytest.raises(RuntimeError, match="SciCat login has expired"): Client.from_token(url="scicat.com", token=token) -def test_detects_expired_token_get_dataset(scicat_access, require_scicat_backend): +def test_detects_expired_token_get_dataset( + scicat_access: backend_config.SciCatAccess, require_scicat_backend: None +) -> None: # The token is invalid, but the expiration should be detected before # even sending it to SciCat. token = make_token(timedelta(milliseconds=2100)) # > than denial period = 2s diff --git a/tests/client/datablock_client_test.py b/tests/client/datablock_client_test.py index 27f5f07f..826976a6 100644 --- a/tests/client/datablock_client_test.py +++ b/tests/client/datablock_client_test.py @@ -13,6 +13,7 @@ UploadDerivedDataset, UploadOrigDatablock, ) +from scitacean.testing.backend import config as backend_config from scitacean.testing.backend.seed import ( INITIAL_ORIG_DATABLOCKS, ) @@ -24,7 +25,7 @@ def scicat_client(client: Client) -> ScicatClient: @pytest.fixture -def derived_dataset(scicat_access): +def derived_dataset(scicat_access: backend_config.SciCatAccess) -> UploadDerivedDataset: return UploadDerivedDataset( contactEmail="black.foess@dom.koelle", creationTime=parse_date("1995-11-11T11:11:11.000Z"), @@ -41,7 +42,7 @@ def derived_dataset(scicat_access): @pytest.fixture -def orig_datablock(scicat_access): +def orig_datablock(scicat_access: backend_config.SciCatAccess) -> UploadOrigDatablock: # NOTE the placeholder! return UploadOrigDatablock( size=9235, @@ -54,24 +55,27 @@ def orig_datablock(scicat_access): @pytest.mark.parametrize("key", ["raw", "derived"]) -def test_get_orig_datablock(scicat_client, key): +def test_get_orig_datablock(scicat_client: ScicatClient, key: str) -> None: dblock = INITIAL_ORIG_DATABLOCKS[key][0] downloaded = scicat_client.get_orig_datablocks(dblock.datasetId) assert downloaded == [dblock] -def test_create_first_orig_datablock(scicat_client, derived_dataset, orig_datablock): +def test_create_first_orig_datablock( + scicat_client: ScicatClient, + derived_dataset: UploadDerivedDataset, + orig_datablock: UploadOrigDatablock, +) -> None: uploaded = scicat_client.create_dataset_model(derived_dataset) scicat_client.create_orig_datablock(orig_datablock, dataset_id=uploaded.pid) - downloaded = scicat_client.get_orig_datablocks(uploaded.pid) - assert len(downloaded) == 1 - downloaded = downloaded[0] + [downloaded] = scicat_client.get_orig_datablocks(uploaded.pid) for key, expected in orig_datablock: # The database populates a number of fields that are orig_datablock in dset. # But we don't want to test those here as we don't want to test the database. if expected is not None and key != "dataFileList": assert dict(downloaded)[key] == expected, f"key = {key}" assert downloaded.accessGroups == derived_dataset.accessGroups + assert downloaded.dataFileList is not None for i in range(len(orig_datablock.dataFileList)): for key, expected in orig_datablock.dataFileList[i]: assert ( diff --git a/tests/client/dataset_client_test.py b/tests/client/dataset_client_test.py index 73f2ab21..906c9d09 100644 --- a/tests/client/dataset_client_test.py +++ b/tests/client/dataset_client_test.py @@ -12,6 +12,7 @@ DatasetType, UploadDerivedDataset, ) +from scitacean.testing.backend import config as backend_config from scitacean.testing.backend.seed import ( INITIAL_DATASETS, INITIAL_ORIG_DATABLOCKS, @@ -24,7 +25,7 @@ def scicat_client(client: Client) -> ScicatClient: @pytest.fixture -def derived_dataset(scicat_access): +def derived_dataset(scicat_access: backend_config.SciCatAccess) -> UploadDerivedDataset: return UploadDerivedDataset( contactEmail="black.foess@dom.koelle", creationTime=parse_date("1995-11-11T11:11:11.000Z"), @@ -41,7 +42,7 @@ def derived_dataset(scicat_access): @pytest.mark.parametrize("key", ["raw", "derived"]) -def test_get_dataset_model(scicat_client, key): +def test_get_dataset_model(scicat_client: ScicatClient, key: str) -> None: dset = INITIAL_DATASETS[key] downloaded = scicat_client.get_dataset_model(dset.pid) # The backend may update the dataset after upload. @@ -50,12 +51,14 @@ def test_get_dataset_model(scicat_client, key): assert downloaded == dset -def test_get_dataset_model_bad_id(scicat_client): +def test_get_dataset_model_bad_id(scicat_client: ScicatClient) -> None: with pytest.raises(ScicatCommError): scicat_client.get_dataset_model(PID(pid="bad-pid")) -def test_create_dataset_model(scicat_client, derived_dataset): +def test_create_dataset_model( + scicat_client: ScicatClient, derived_dataset: UploadDerivedDataset +) -> None: finalized = scicat_client.create_dataset_model(derived_dataset) downloaded = scicat_client.get_dataset_model(finalized.pid) for key, expected in finalized: @@ -65,14 +68,18 @@ def test_create_dataset_model(scicat_client, derived_dataset): assert expected == dict(downloaded)[key], f"key = {key}" -def test_validate_dataset_model(real_client, require_scicat_backend, derived_dataset): +def test_validate_dataset_model( + real_client: Client, + derived_dataset: UploadDerivedDataset, + require_scicat_backend: None, +) -> None: real_client.scicat.validate_dataset_model(derived_dataset) derived_dataset.contactEmail = "NotAnEmail" with pytest.raises(ValueError, match="validation in SciCat"): real_client.scicat.validate_dataset_model(derived_dataset) -def test_get_dataset(client): +def test_get_dataset(client: Client) -> None: dset = INITIAL_DATASETS["raw"] dblock = INITIAL_ORIG_DATABLOCKS["raw"][0] downloaded = client.get_dataset(dset.pid) @@ -91,7 +98,9 @@ def test_get_dataset(client): assert dset_file.creation_time == expected_file.time -def test_can_get_public_dataset_without_login(require_scicat_backend, scicat_access): +def test_can_get_public_dataset_without_login( + require_scicat_backend: None, scicat_access: backend_config.SciCatAccess +) -> None: client = Client.without_login(url=scicat_access.url) dset = INITIAL_DATASETS["public"] @@ -111,14 +120,16 @@ def test_can_get_public_dataset_without_login(require_scicat_backend, scicat_acc def test_cannot_upload_without_login( - require_scicat_backend, derived_dataset, scicat_access -): + require_scicat_backend: None, + derived_dataset: UploadDerivedDataset, + scicat_access: backend_config.SciCatAccess, +) -> None: client = Client.without_login(url=scicat_access.url) with pytest.raises(ScicatCommError): # TODO test return code 403 client.scicat.create_dataset_model(derived_dataset) -def test_get_broken_dataset(client): +def test_get_broken_dataset(client: Client) -> None: dset = INITIAL_DATASETS["partially-broken"] downloaded = client.get_dataset(dset.pid) assert downloaded.type == DatasetType.DERIVED @@ -137,13 +148,15 @@ def test_get_broken_dataset(client): assert downloaded.size == 0 -def test_get_broken_dataset_strict_validation(real_client, require_scicat_backend): +def test_get_broken_dataset_strict_validation( + real_client: Client, require_scicat_backend: None +) -> None: dset = INITIAL_DATASETS["partially-broken"] with pytest.raises(pydantic.ValidationError): real_client.get_dataset(dset.pid, strict_validation=True) -def test_dataset_with_orig_datablock_roundtrip(client): +def test_dataset_with_orig_datablock_roundtrip(client: Client) -> None: ds = Dataset.from_download_models( INITIAL_DATASETS["raw"], INITIAL_ORIG_DATABLOCKS["raw"], [] ).as_new() diff --git a/tests/client/query_client_test.py b/tests/client/query_client_test.py index 243b25e0..de46453e 100644 --- a/tests/client/query_client_test.py +++ b/tests/client/query_client_test.py @@ -108,7 +108,7 @@ def _seed_database(request: pytest.FixtureRequest, scicat_access: SciCatAccess) client = Client.from_credentials( url=scicat_access.url, - **scicat_access.user.credentials, # type: ignore[arg-type] + **scicat_access.user.credentials, ) for key, dset in UPLOAD_DATASETS.items(): dset.ownerGroup = scicat_access.user.group @@ -117,7 +117,7 @@ def _seed_database(request: pytest.FixtureRequest, scicat_access: SciCatAccess) @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_multiple_by_single_field(real_client): +def test_query_dataset_multiple_by_single_field(real_client: Client) -> None: datasets = real_client.scicat.query_datasets({"proposalId": "p0124"}) actual = {ds.pid: ds for ds in datasets} expected = {SEED[key].pid: SEED[key] for key in ("raw1", "raw2", "raw3")} @@ -125,13 +125,13 @@ def test_query_dataset_multiple_by_single_field(real_client): @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_no_match(real_client): +def test_query_dataset_no_match(real_client: Client) -> None: datasets = real_client.scicat.query_datasets({"owner": "librarian"}) assert not datasets @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_multiple_by_multiple_fields(real_client): +def test_query_dataset_multiple_by_multiple_fields(real_client: Client) -> None: datasets = real_client.scicat.query_datasets( {"proposalId": "p0124", "principalInvestigator": "investigator 1"}, ) @@ -141,7 +141,7 @@ def test_query_dataset_multiple_by_multiple_fields(real_client): @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_multiple_by_derived_field(real_client): +def test_query_dataset_multiple_by_derived_field(real_client: Client) -> None: datasets = real_client.scicat.query_datasets( {"investigator": "investigator 1"}, ) @@ -151,7 +151,7 @@ def test_query_dataset_multiple_by_derived_field(real_client): @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_uses_conjunction_of_fields(real_client): +def test_query_dataset_uses_conjunction_of_fields(real_client: Client) -> None: datasets = real_client.scicat.query_datasets( {"proposalId": "p0124", "investigator": "investigator X"}, ) @@ -159,7 +159,7 @@ def test_query_dataset_uses_conjunction_of_fields(real_client): @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_can_use_custom_type(real_client): +def test_query_dataset_can_use_custom_type(real_client: Client) -> None: datasets = real_client.scicat.query_datasets( {"sourceFolder": RemotePath("/hex/raw4")}, ) @@ -168,7 +168,7 @@ def test_query_dataset_can_use_custom_type(real_client): @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_set_order(real_client): +def test_query_dataset_set_order(real_client: Client) -> None: datasets = real_client.scicat.query_datasets( {"proposalId": "p0124"}, order="creationTime:desc", @@ -179,7 +179,7 @@ def test_query_dataset_set_order(real_client): @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_limit_ascending_creation_time(real_client): +def test_query_dataset_limit_ascending_creation_time(real_client: Client) -> None: datasets = real_client.scicat.query_datasets( {"proposalId": "p0124"}, limit=2, @@ -191,7 +191,7 @@ def test_query_dataset_limit_ascending_creation_time(real_client): @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_limit_descending_creation_time(real_client): +def test_query_dataset_limit_descending_creation_time(real_client: Client) -> None: datasets = real_client.scicat.query_datasets( {"proposalId": "p0124"}, limit=2, @@ -203,7 +203,7 @@ def test_query_dataset_limit_descending_creation_time(real_client): @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_limit_needs_order(real_client): +def test_query_dataset_limit_needs_order(real_client: Client) -> None: with pytest.raises(ValueError, match="limit"): real_client.scicat.query_datasets( {"proposalId": "p0124"}, @@ -212,7 +212,7 @@ def test_query_dataset_limit_needs_order(real_client): @pytest.mark.usefixtures("_seed_database") -def test_query_dataset_all(real_client): +def test_query_dataset_all(real_client: Client) -> None: datasets = real_client.scicat.query_datasets({}) actual = {ds.pid: ds for ds in datasets} # We cannot test `datasets` directly because there are other datasets diff --git a/tests/client/sample_client_test.py b/tests/client/sample_client_test.py index f5762fbc..2fdfd4c6 100644 --- a/tests/client/sample_client_test.py +++ b/tests/client/sample_client_test.py @@ -7,9 +7,7 @@ from scitacean import Client, ScicatCommError from scitacean.client import ScicatClient -from scitacean.model import ( - Sample, -) +from scitacean.model import DownloadSample, Sample, UploadSample from scitacean.testing.backend import config as backend_config from scitacean.testing.backend import skip_if_not_backend @@ -34,7 +32,7 @@ def real_client( skip_if_not_backend(request) return Client.from_credentials( url=ingestor_access.url, - **ingestor_access.user.credentials, # type: ignore[arg-type] + **ingestor_access.user.credentials, ) @@ -44,7 +42,7 @@ def scicat_client(client: Client) -> ScicatClient: @pytest.fixture -def sample(ingestor_access): +def sample(ingestor_access: backend_config.SciCatAccess) -> Sample: scicat_access = ingestor_access return Sample( owner_group=scicat_access.user.group, @@ -55,7 +53,9 @@ def sample(ingestor_access): ) -def compare_sample_model_after_upload(uploaded, downloaded): +def compare_sample_model_after_upload( + uploaded: UploadSample | DownloadSample, downloaded: DownloadSample +) -> None: for key, expected in uploaded: # The database populates a number of fields that are None in uploaded. # But we don't want to test those here as we don't want to test the database. @@ -63,7 +63,7 @@ def compare_sample_model_after_upload(uploaded, downloaded): assert expected == dict(downloaded)[key], f"key = {key}" -def compare_sample_after_upload(uploaded, downloaded): +def compare_sample_after_upload(uploaded: Sample, downloaded: Sample) -> None: for field in dataclasses.fields(uploaded): # The database populates a number of fields that are None in uploaded. # But we don't want to test those here as we don't want to test the database. @@ -73,16 +73,21 @@ def compare_sample_after_upload(uploaded, downloaded): @pytest.mark.skip("Sample creation does not currently work") -def test_create_sample_model_roundtrip(scicat_client, sample): +def test_create_sample_model_roundtrip( + scicat_client: ScicatClient, sample: Sample +) -> None: upload_sample = sample.make_upload_model() finalized = scicat_client.create_sample_model(upload_sample) + assert finalized.sampleId is not None downloaded = scicat_client.get_sample_model(finalized.sampleId) compare_sample_model_after_upload(upload_sample, downloaded) compare_sample_model_after_upload(finalized, downloaded) @pytest.mark.skip("Sample creation does not currently work") -def test_create_sample_model_roundtrip_existing_id(scicat_client, sample): +def test_create_sample_model_roundtrip_existing_id( + scicat_client: ScicatClient, sample: Sample +) -> None: upload_sample = sample.make_upload_model() finalized = scicat_client.create_sample_model(upload_sample) upload_sample.sampleId = finalized.sampleId @@ -93,22 +98,24 @@ def test_create_sample_model_roundtrip_existing_id(scicat_client, sample): @pytest.mark.skip("Sample creation does not currently work") -def test_create_sample_model_populates_id(scicat_client, sample): +def test_create_sample_model_populates_id( + scicat_client: ScicatClient, sample: Sample +) -> None: upload_sample = sample.make_upload_model() finalized = scicat_client.create_sample_model(upload_sample) - downloaded = scicat_client.get_sample_model(finalized.sampleId) assert finalized.sampleId is not None + downloaded = scicat_client.get_sample_model(finalized.sampleId) assert downloaded.sampleId == finalized.sampleId @pytest.mark.skip("Sample creation does not currently work") -def test_upload_sample_roundtrip(client, sample): +def test_upload_sample_roundtrip(client: Client, sample: Sample) -> None: finalized = client.upload_new_sample_now(sample) compare_sample_after_upload(sample, finalized) @pytest.mark.skip("Sample creation does not currently work") -def test_upload_sample_overrides_id(client, sample): +def test_upload_sample_overrides_id(client: Client, sample: Sample) -> None: sample.sample_id = "my_sample-id" finalized = client.upload_new_sample_now(sample) assert finalized.sample_id != sample.sample_id diff --git a/tests/dataset_fields_test.py b/tests/dataset_fields_test.py index 84e090bc..b19f2ec7 100644 --- a/tests/dataset_fields_test.py +++ b/tests/dataset_fields_test.py @@ -30,7 +30,7 @@ _NOT_SETTABLE_FIELDS = ("type",) -def test_init_dataset_with_only_type(): +def test_init_dataset_with_only_type() -> None: dset = Dataset(type="raw") assert dset.type == DatasetType.RAW @@ -38,29 +38,29 @@ def test_init_dataset_with_only_type(): @pytest.mark.parametrize( "typ", ["raw", "derived", DatasetType.RAW, DatasetType.DERIVED] ) -def test_init_dataset_accepted_types(typ): +def test_init_dataset_accepted_types(typ: str | DatasetType) -> None: dset = Dataset(type=typ) assert dset.type == typ -def test_init_dataset_raises_for_bad_type(): +def test_init_dataset_raises_for_bad_type() -> None: with pytest.raises(ValueError, match="DatasetType"): Dataset(type="bad-type") # type: ignore[arg-type] -def test_init_dataset_needs_type(): +def test_init_dataset_needs_type() -> None: with pytest.raises(TypeError): Dataset() # type: ignore[call-arg] -def test_init_dataset_sets_creation_time(): +def test_init_dataset_sets_creation_time() -> None: expected = datetime.now(tz=timezone.utc) dset = Dataset(type="raw") assert dset.creation_time is not None assert abs(dset.creation_time - expected) < timedelta(seconds=30) -def test_init_dataset_can_set_creation_time(): +def test_init_dataset_can_set_creation_time() -> None: dt: str | datetime dt = dateutil.parser.parse("2022-01-10T11:14:52.623Z") @@ -83,7 +83,7 @@ def test_init_dataset_can_set_creation_time(): @pytest.mark.parametrize("field", Dataset.fields(read_only=True), ids=lambda f: f.name) -def test_cannot_set_read_only_fields(field): +def test_cannot_set_read_only_fields(field: Dataset.Field) -> None: dset = Dataset(type="raw") with pytest.raises(AttributeError): setattr(dset, field.name, None) @@ -100,7 +100,7 @@ def test_cannot_set_read_only_fields(field): ) @given(st.data()) @settings(max_examples=10) -def test_can_init_writable_fields(field, data): +def test_can_init_writable_fields(field: Dataset.Field, data: st.DataObject) -> None: value = data.draw(st.from_type(field.type)) dset = Dataset(type="raw", **{field.name: value}) assert getattr(dset, field.name) == value @@ -117,7 +117,7 @@ def test_can_init_writable_fields(field, data): ) @given(st.data()) @settings(max_examples=10) -def test_can_set_writable_fields(field, data): +def test_can_set_writable_fields(field: Dataset.Field, data: st.DataObject) -> None: value = data.draw(st.from_type(field.type)) dset = Dataset(type="raw") setattr(dset, field.name, value) @@ -129,13 +129,13 @@ def test_can_set_writable_fields(field, data): (f for f in Dataset.fields() if f.name != "type" and not f.read_only), ids=lambda f: f.name, ) -def test_can_set_writable_fields_to_none(field): +def test_can_set_writable_fields_to_none(field: Dataset.Field) -> None: dset = Dataset(type="raw") setattr(dset, field.name, None) assert getattr(dset, field.name) is None -def test_init_from_models_sets_metadata(): +def test_init_from_models_sets_metadata() -> None: dset = Dataset.from_download_models( dataset_model=DownloadDataset( contactEmail="p.stibbons@uu.am", @@ -184,7 +184,7 @@ def test_init_from_models_sets_metadata(): assert dset.size == 0 -def test_init_from_models_sets_files(): +def test_init_from_models_sets_files() -> None: dset = Dataset.from_download_models( dataset_model=DownloadDataset( contactEmail="p.stibbons@uu.am", @@ -241,7 +241,7 @@ def test_init_from_models_sets_files(): assert f1.make_model().path == "sub/file2.png" -def test_init_from_models_sets_files_multi_datablocks(): +def test_init_from_models_sets_files_multi_datablocks() -> None: dataset_model = DownloadDataset( contactEmail="p.stibbons@uu.am", creationTime=dateutil.parser.parse("2022-01-10T11:14:52-01:00"), @@ -308,32 +308,32 @@ def test_init_from_models_sets_files_multi_datablocks(): assert f1.make_model().path == "sub/file2.png" -def test_fields_type_filter_derived(): +def test_fields_type_filter_derived() -> None: assert all( field.used_by_derived for field in Dataset.fields(dataset_type="derived") ) -def test_fields_type_filter_raw(): +def test_fields_type_filter_raw() -> None: assert all(field.used_by_raw for field in Dataset.fields(dataset_type="raw")) -def test_fields_read_only_filter_true(): +def test_fields_read_only_filter_true() -> None: assert all(field.read_only for field in Dataset.fields(read_only=True)) -def test_fields_read_only_filter_false(): +def test_fields_read_only_filter_false() -> None: assert all(not field.read_only for field in Dataset.fields(read_only=False)) -def test_fields_read_only__and_type_filter(): +def test_fields_read_only__and_type_filter() -> None: assert all( not field.read_only and field.used_by_raw for field in Dataset.fields(read_only=False, dataset_type="raw") ) -def test_make_raw_model(): +def test_make_raw_model() -> None: dset = Dataset( type="raw", contact_email="p.stibbons@uu.am", @@ -364,7 +364,7 @@ def test_make_raw_model(): assert dset.make_upload_model() == expected -def test_make_derived_model(): +def test_make_derived_model() -> None: dset = Dataset( type="derived", contact_email="p.stibbons@uu.am;m.ridcully@uu.am", @@ -408,7 +408,9 @@ def test_make_derived_model(): ) @given(st.data()) @settings(max_examples=10) -def test_make_raw_model_raises_if_derived_field_set(field, data): +def test_make_raw_model_raises_if_derived_field_set( + field: Dataset.Field, data: st.DataObject +) -> None: dset = Dataset( type="raw", contact_email="p.stibbons@uu.am", @@ -435,7 +437,9 @@ def test_make_raw_model_raises_if_derived_field_set(field, data): ) @given(st.data()) @settings(max_examples=10) -def test_make_derived_model_raises_if_raw_field_set(field, data): +def test_make_derived_model_raises_if_raw_field_set( + field: Dataset.Field, data: st.DataObject +) -> None: dset = Dataset( type="derived", contact_email="p.stibbons@uu.am", @@ -455,7 +459,7 @@ def test_make_derived_model_raises_if_raw_field_set(field, data): @pytest.mark.parametrize("field", ["contact_email", "owner_email"]) -def test_email_validation(field): +def test_email_validation(field: Dataset.Field) -> None: dset = Dataset( type="raw", contact_email="p.stibbons@uu.am", @@ -478,7 +482,7 @@ def test_email_validation(field): "https://orcid.org/0000-0003-2818-0368", ], ) -def test_orcid_validation_valid(good_orcid): +def test_orcid_validation_valid(good_orcid: str) -> None: dset = Dataset( type="raw", contact_email="jan-lukas.wynen@ess.eu", @@ -502,7 +506,7 @@ def test_orcid_validation_valid(good_orcid): "https://orcid.org/0000-0002-3761-320X", ], ) -def test_orcid_validation_missing_url(bad_orcid): +def test_orcid_validation_missing_url(bad_orcid: str) -> None: dset = Dataset( type="raw", contact_email="jan-lukas.wynen@ess.eu", diff --git a/tests/dataset_test.py b/tests/dataset_test.py index bb649419..55649b88 100644 --- a/tests/dataset_test.py +++ b/tests/dataset_test.py @@ -9,6 +9,7 @@ from dateutil.parser import parse as parse_datetime from hypothesis import assume, given, settings from hypothesis import strategies as st +from pyfakefs.fake_filesystem import FakeFilesystem from scitacean import PID, Dataset, DatasetType, File, RemotePath, model from scitacean.testing import strategies as sst @@ -18,7 +19,7 @@ @pytest.fixture -def raw_download_model(): +def raw_download_model() -> model.DownloadDataset: return model.DownloadDataset( contactEmail="p.stibbons@uu.am", creationLocation="UnseenUniversity", @@ -87,7 +88,7 @@ def raw_download_model(): @pytest.fixture -def derived_download_model(): +def derived_download_model() -> model.DownloadDataset: return model.DownloadDataset( contactEmail="p.stibbons@uu.am", creationLocation=None, @@ -156,12 +157,14 @@ def derived_download_model(): @pytest.fixture(params=["raw_download_model", "derived_download_model"]) -def dataset_download_model(request): - return request.getfixturevalue(request.param) +def dataset_download_model(request: pytest.FixtureRequest) -> model.DownloadDataset: + return request.getfixturevalue(request.param) # type: ignore[no-any-return] -def test_from_download_models_initializes_fields(dataset_download_model): - def get_model_field(name): +def test_from_download_models_initializes_fields( + dataset_download_model: model.DownloadDataset, +) -> None: + def get_model_field(name: str) -> object: val = getattr(dataset_download_model, name) if name == "relationships": return [model.Relationship.from_download_model(v) for v in val] @@ -177,7 +180,9 @@ def get_model_field(name): assert getattr(dset, field.name) == get_model_field(field.scicat_name) -def test_from_download_models_does_not_initialize_wrong_fields(dataset_download_model): +def test_from_download_models_does_not_initialize_wrong_fields( + dataset_download_model: model.DownloadDataset, +) -> None: dset = Dataset.from_download_models(dataset_download_model, []) for field in dset.fields(): if not field.used_by(dataset_download_model.type): @@ -185,7 +190,7 @@ def test_from_download_models_does_not_initialize_wrong_fields(dataset_download_ @pytest.mark.parametrize("typ", [DatasetType.RAW, DatasetType.DERIVED]) -def test_new_dataset_has_no_files(typ): +def test_new_dataset_has_no_files(typ: DatasetType) -> None: dset = Dataset(type=typ) assert len(list(dset.files)) == 0 assert dset.number_of_files == 0 @@ -195,7 +200,7 @@ def test_new_dataset_has_no_files(typ): @pytest.mark.parametrize("typ", [DatasetType.RAW, DatasetType.DERIVED]) -def test_add_local_file_to_new_dataset(typ, fs): +def test_add_local_file_to_new_dataset(typ: DatasetType, fs: FakeFilesystem) -> None: file_data = make_file(fs, "local/folder/data.dat") dset = Dataset(type=typ) @@ -220,7 +225,9 @@ def test_add_local_file_to_new_dataset(typ, fs): @pytest.mark.parametrize("typ", [DatasetType.RAW, DatasetType.DERIVED]) -def test_add_multiple_local_files_to_new_dataset(typ, fs): +def test_add_multiple_local_files_to_new_dataset( + typ: DatasetType, fs: FakeFilesystem +) -> None: file_data0 = make_file(fs, "common/location1/data.dat") file_data1 = make_file(fs, "common/song.mp3") @@ -253,7 +260,9 @@ def test_add_multiple_local_files_to_new_dataset(typ, fs): @pytest.mark.parametrize("typ", [DatasetType.RAW, DatasetType.DERIVED]) -def test_add_multiple_local_files_to_new_dataset_with_base_path(typ, fs): +def test_add_multiple_local_files_to_new_dataset_with_base_path( + typ: DatasetType, fs: FakeFilesystem +) -> None: file_data0 = make_file(fs, "common/location1/data.dat") file_data1 = make_file(fs, "common/song.mp3") @@ -289,7 +298,9 @@ def test_add_multiple_local_files_to_new_dataset_with_base_path(typ, fs): @pytest.mark.parametrize("typ", [DatasetType.RAW, DatasetType.DERIVED]) @pytest.mark.parametrize("algorithm", ["sha256", None]) -def test_can_set_default_checksum_algorithm(typ, algorithm, fs): +def test_can_set_default_checksum_algorithm( + typ: DatasetType, algorithm: str | None, fs: FakeFilesystem +) -> None: make_file(fs, "local/data.dat") dset = Dataset(type=typ, checksum_algorithm=algorithm) @@ -301,12 +312,12 @@ def test_can_set_default_checksum_algorithm(typ, algorithm, fs): @given(sst.datasets(for_upload=True)) @settings(max_examples=100) -def test_dataset_models_roundtrip(initial): - dataset_model = initial.make_upload_model() - dblock_models = initial.make_datablock_upload_models().orig_datablocks - attachment_models = initial.make_attachment_upload_models() +def test_dataset_models_roundtrip(initial: Dataset) -> None: + dataset_upload_model = initial.make_upload_model() + dblock_upload_models = initial.make_datablock_upload_models().orig_datablocks + attachment_upload_models = initial.make_attachment_upload_models() dataset_model, dblock_models, attachment_models = process_uploaded_dataset( - dataset_model, dblock_models, attachment_models + dataset_upload_model, dblock_upload_models, attachment_upload_models ) dataset_model.createdAt = None dataset_model.createdBy = None @@ -323,13 +334,13 @@ def test_dataset_models_roundtrip(initial): @given(sst.datasets()) @settings(max_examples=10) -def test_make_scicat_models_datablock_without_files(dataset): +def test_make_scicat_models_datablock_without_files(dataset: Dataset) -> None: assert dataset.make_datablock_upload_models().orig_datablocks is None @given(sst.datasets(pid=st.builds(PID))) @settings(max_examples=10) -def test_make_scicat_models_datablock_with_one_file(dataset): +def test_make_scicat_models_datablock_with_one_file(dataset: Dataset) -> None: file_model = model.DownloadDataFile( path="path", size=6163, @@ -340,6 +351,7 @@ def test_make_scicat_models_datablock_with_one_file(dataset): dataset.add_files(File.from_download_model(local_path=None, model=file_model)) blocks = dataset.make_datablock_upload_models().orig_datablocks + assert blocks is not None assert len(blocks) == 1 block = blocks[0] @@ -347,7 +359,7 @@ def test_make_scicat_models_datablock_with_one_file(dataset): assert block.dataFileList == [model.UploadDataFile(**file_model.model_dump())] -def test_attachments_are_empty_by_default(): +def test_attachments_are_empty_by_default() -> None: dataset = Dataset( type="raw", owner="ridcully", @@ -355,12 +367,16 @@ def test_attachments_are_empty_by_default(): assert dataset.attachments == [] -def test_attachments_are_none_after_from_download_models(dataset_download_model): +def test_attachments_are_none_after_from_download_models( + dataset_download_model: model.DownloadDataset, +) -> None: dataset = Dataset.from_download_models(dataset_download_model, []) assert dataset.attachments is None -def test_attachments_initialized_in_from_download_models(dataset_download_model): +def test_attachments_initialized_in_from_download_models( + dataset_download_model: model.DownloadDataset, +) -> None: dataset = Dataset.from_download_models( dataset_download_model, [], @@ -380,7 +396,7 @@ def test_attachments_initialized_in_from_download_models(dataset_download_model) ] -def test_can_add_attachment(): +def test_can_add_attachment() -> None: dataset = Dataset(type="raw", owner_group="dset-owner") dataset.attachments.append( model.Attachment( @@ -396,7 +412,7 @@ def test_can_add_attachment(): ] -def test_can_assign_attachments(): +def test_can_assign_attachments() -> None: dataset = Dataset(type="derived", owner_group="dset-owner") dataset.attachments = [ @@ -426,7 +442,7 @@ def test_can_assign_attachments(): ] -def test_make_attachment_upload_models_fails_when_attachments_are_none(): +def test_make_attachment_upload_models_fails_when_attachments_are_none() -> None: dataset = Dataset(type="derived", owner_group="dset-owner") dataset.attachments = None with pytest.raises(ValueError, match="attachment"): @@ -435,7 +451,7 @@ def test_make_attachment_upload_models_fails_when_attachments_are_none(): @given(sst.datasets()) @settings(max_examples=10) -def test_eq_self(dset): +def test_eq_self(dset: Dataset) -> None: dset.add_files( File.from_download_model( local_path=None, @@ -468,7 +484,9 @@ def test_eq_self(dset): ) @given(sst.datasets(), st.data()) @settings(max_examples=10) -def test_neq_single_mismatched_field_writable(field, initial, data): +def test_neq_single_mismatched_field_writable( + field: Dataset.Field, initial: Dataset, data: st.DataObject +) -> None: new_val = data.draw(st.from_type(field.type)) assume(new_val != getattr(initial, field.name)) modified = initial.replace(**{field.name: new_val}) @@ -478,7 +496,7 @@ def test_neq_single_mismatched_field_writable(field, initial, data): @given(sst.datasets()) @settings(max_examples=10) -def test_neq_single_mismatched_file(initial): +def test_neq_single_mismatched_file(initial: Dataset) -> None: modified = initial.replace() modified.add_files( File.from_download_model( @@ -502,7 +520,7 @@ def test_neq_single_mismatched_file(initial): @given(sst.datasets()) @settings(max_examples=10) -def test_neq_extra_file(initial): +def test_neq_extra_file(initial: Dataset) -> None: modified = initial.replace() modified.add_files( File.from_download_model( @@ -518,7 +536,7 @@ def test_neq_extra_file(initial): @given(sst.datasets()) @settings(max_examples=1) -def test_neq_attachment_none_vs_empty(initial): +def test_neq_attachment_none_vs_empty(initial: Dataset) -> None: initial.attachments = [] modified = initial.replace() modified.attachments = None @@ -528,7 +546,7 @@ def test_neq_attachment_none_vs_empty(initial): @given(sst.datasets()) @settings(max_examples=1) -def test_neq_extra_attachment(initial): +def test_neq_extra_attachment(initial: Dataset) -> None: initial.attachments = [] modified = initial.replace() modified.attachments.append( @@ -540,12 +558,12 @@ def test_neq_extra_attachment(initial): @given(sst.datasets()) @settings(max_examples=1) -def test_neq_mismatched_attachment(initial): +def test_neq_mismatched_attachment(initial: Dataset) -> None: initial.attachments = [ (model.Attachment(caption="The attachment", owner_group="owner")) ] modified = initial.replace() - modified.attachments[0] = model.Attachment( + modified.attachments[0] = model.Attachment( # type: ignore[index] caption="Another attachment", owner_group="owner" ) assert initial != modified @@ -563,7 +581,9 @@ def test_neq_mismatched_attachment(initial): ) @given(sst.datasets(), st.data()) @settings(max_examples=5) -def test_replace_replaces_single_writable_field(field, initial, data): +def test_replace_replaces_single_writable_field( + field: Dataset.Field, initial: Dataset, data: st.DataObject +) -> None: val = data.draw(st.from_type(field.type)) replaced = initial.replace(**{field.name: val}) assert getattr(replaced, field.name) == val @@ -587,7 +607,9 @@ def test_replace_replaces_single_writable_field(field, initial, data): ) @given(sst.datasets(), st.data()) @settings(max_examples=5) -def test_replace_replaces_single_read_only_field(field, initial, data): +def test_replace_replaces_single_read_only_field( + field: Dataset.Field, initial: Dataset, data: st.DataObject +) -> None: val = data.draw(st.from_type(field.type)) replaced = initial.replace(_read_only={field.name: val}) assert getattr(replaced, field.name) == val @@ -595,7 +617,7 @@ def test_replace_replaces_single_read_only_field(field, initial, data): @given(sst.datasets()) @settings(max_examples=5) -def test_replace_replaces_multiple_fields(initial): +def test_replace_replaces_multiple_fields(initial: Dataset) -> None: replaced = initial.replace( owner="a-new-owner", used_software=["software1"], @@ -608,7 +630,7 @@ def test_replace_replaces_multiple_fields(initial): @given(sst.datasets()) @settings(max_examples=5) -def test_replace_other_fields_are_copied(initial): +def test_replace_other_fields_are_copied(initial: Dataset) -> None: replaced = initial.replace( investigator="inv@esti.gator", techniques=[model.Technique(pid="tech/abcd.01", name="magick")], @@ -622,14 +644,14 @@ def test_replace_other_fields_are_copied(initial): @given(sst.datasets()) @settings(max_examples=1) -def test_replace_rejects_bad_arguments(initial): +def test_replace_rejects_bad_arguments(initial: Dataset) -> None: with pytest.raises(TypeError): initial.replace(this_is_not_a_valid="argument", owner="the-owner-of-it-all") @given(sst.datasets()) @settings(max_examples=1) -def test_replace_does_not_change_files_no_input_files(initial): +def test_replace_does_not_change_files_no_input_files(initial: Dataset) -> None: replaced = initial.replace(owner="a-new-owner") assert replaced.number_of_files == 0 assert replaced.size == 0 @@ -638,7 +660,7 @@ def test_replace_does_not_change_files_no_input_files(initial): @given(sst.datasets()) @settings(max_examples=1) -def test_replace_does_not_change_files_with_input_files(initial): +def test_replace_does_not_change_files_with_input_files(initial: Dataset) -> None: file = File.from_download_model( local_path=None, model=model.DownloadDataFile( @@ -654,7 +676,7 @@ def test_replace_does_not_change_files_with_input_files(initial): @given(sst.datasets()) @settings(max_examples=1) -def test_replace_preserves_meta(initial): +def test_replace_preserves_meta(initial: Dataset) -> None: initial.meta["key"] = "val" replaced = initial.replace(owner="a-new-owner") assert replaced.meta == {"key": "val"} @@ -662,7 +684,7 @@ def test_replace_preserves_meta(initial): @given(sst.datasets()) @settings(max_examples=1) -def test_replace_meta(initial): +def test_replace_meta(initial: Dataset) -> None: initial.meta["key"] = {"value": 2, "unit": "m"} initial.meta["old-key"] = "old-val" replaced = initial.replace( @@ -674,7 +696,7 @@ def test_replace_meta(initial): @given(sst.datasets()) @settings(max_examples=1) -def test_replace_remove_meta(initial): +def test_replace_remove_meta(initial: Dataset) -> None: initial.meta["key"] = {"value": 2, "unit": "m"} initial.meta["old-key"] = "old-val" replaced = initial.replace(owner="a-new-owner", meta=None) @@ -687,7 +709,9 @@ def test_replace_remove_meta(initial): ) @given(initial=sst.datasets()) @settings(max_examples=1) -def test_replace_preserves_attachments(initial, attachments): +def test_replace_preserves_attachments( + initial: Dataset, attachments: None | list[model.Attachment] +) -> None: initial.attachments = attachments replaced = initial.replace(owner="a-new-owner") assert replaced.attachments == attachments @@ -703,20 +727,25 @@ def test_replace_preserves_attachments(initial, attachments): ) @given(initial=sst.datasets()) @settings(max_examples=1) -def test_replace_attachments(initial, attachments, target_attachments): +def test_replace_attachments( + initial: Dataset, + attachments: None | list[model.Attachment], + target_attachments: None | list[model.Attachment], +) -> None: replaced = initial.replace(attachments=target_attachments) assert replaced.attachments == target_attachments @given(sst.datasets()) @settings(max_examples=5) -def test_as_new(initial): +def test_as_new(initial: Dataset) -> None: new = initial.as_new() assert new.created_at is None assert new.created_by is None assert new.updated_at is None assert new.updated_by is None assert new.lifecycle is None + assert new.creation_time is not None assert abs(new.creation_time - datetime.now(tz=timezone.utc)) < timedelta(seconds=1) assert new.number_of_files == initial.number_of_files @@ -728,7 +757,7 @@ def test_as_new(initial): @given(sst.datasets(pid=PID(pid="some-id"))) @settings(max_examples=5) -def test_derive_default(initial): +def test_derive_default(initial: Dataset) -> None: derived = initial.derive() assert derived.type == "derived" assert derived.input_datasets == [initial.pid] @@ -749,7 +778,7 @@ def test_derive_default(initial): @given(sst.datasets(pid=PID(pid="some-id"))) @settings(max_examples=5) -def test_derive_set_keep(initial): +def test_derive_set_keep(initial: Dataset) -> None: derived = initial.derive(keep=("name", "used_software")) assert derived.type == "derived" assert derived.input_datasets == [initial.pid] @@ -764,7 +793,7 @@ def test_derive_set_keep(initial): @given(sst.datasets(pid=PID(pid="some-id"))) @settings(max_examples=5) -def test_derive_keep_nothing(initial): +def test_derive_keep_nothing(initial: Dataset) -> None: derived = initial.derive(keep=()) assert derived.type == "derived" assert derived.input_datasets == [initial.pid] @@ -780,7 +809,7 @@ def test_derive_keep_nothing(initial): @given(sst.datasets(pid=None)) @settings(max_examples=5) -def test_derive_requires_pid(initial): +def test_derive_requires_pid(initial: Dataset) -> None: with pytest.raises(ValueError, match="pid"): initial.derive() @@ -791,13 +820,15 @@ def test_derive_requires_pid(initial): ) @given(initial=sst.datasets(pid=PID(pid="some-id"))) @settings(max_examples=1) -def test_derive_removes_attachments(initial, attachments): +def test_derive_removes_attachments( + initial: Dataset, attachments: None | list[model.Attachment] +) -> None: initial.attachments = attachments derived = initial.derive() assert derived.attachments == [] -def invalid_field_example(my_type): +def invalid_field_example(my_type: DatasetType) -> tuple[str, str]: if my_type == DatasetType.DERIVED: return "data_format", "sth_not_None" elif my_type == DatasetType.RAW: @@ -817,7 +848,7 @@ def test_dataset_dict_like_keys_per_type(initial: Dataset) -> None: @given(initial=sst.datasets(for_upload=True)) @settings(max_examples=10) -def test_dataset_dict_like_keys_including_invalid_field(initial): +def test_dataset_dict_like_keys_including_invalid_field(initial: Dataset) -> None: invalid_name, invalid_value = invalid_field_example(initial.type) my_names = { @@ -856,7 +887,7 @@ def test_dataset_dict_like_items_with_invalid_field(initial: Dataset) -> None: @given(initial=sst.datasets(for_upload=True)) @settings(max_examples=10) -def test_dataset_dict_like_getitem(initial): +def test_dataset_dict_like_getitem(initial: Dataset) -> None: assert initial["type"] == initial.type @@ -865,7 +896,9 @@ def test_dataset_dict_like_getitem(initial): ) @given(initial=sst.datasets(for_upload=True)) @settings(max_examples=10) -def test_dataset_dict_like_getitem_wrong_field_raises(initial, is_attr, wrong_field): +def test_dataset_dict_like_getitem_wrong_field_raises( + initial: Dataset, is_attr: bool, wrong_field: str +) -> None: # 'size' should be included in the field later. # It is now excluded because it is ``manual`` field. See issue#151. assert hasattr(initial, wrong_field) == is_attr @@ -901,8 +934,8 @@ def test_dataset_dict_like_setitem_invalid_field(initial: Dataset) -> None: @given(initial=sst.datasets(for_upload=True)) @settings(max_examples=10) def test_dataset_dict_like_setitem_wrong_field_raises( - initial, is_attr, wrong_field, wrong_value -): + initial: Dataset, is_attr: bool, wrong_field: str, wrong_value: str | int +) -> None: # ``manual`` fields such as ``size`` should raise with ``__setitem__``. # However, it may need more specific error message. assert hasattr(initial, wrong_field) == is_attr diff --git a/tests/download_test.py b/tests/download_test.py index b25760e5..afc6e486 100644 --- a/tests/download_test.py +++ b/tests/download_test.py @@ -2,18 +2,20 @@ # Copyright (c) 2024 SciCat Project (https://github.com/SciCatProject/scitacean) import hashlib import re +from collections.abc import Iterator from contextlib import contextmanager from copy import deepcopy from pathlib import Path import pytest from dateutil.parser import parse as parse_date +from pyfakefs.fake_filesystem import FakeFilesystem from scitacean import PID, Client, Dataset, DatasetType, File, IntegrityError from scitacean.filesystem import RemotePath from scitacean.logging import logger_name from scitacean.model import DownloadDataFile, DownloadDataset, DownloadOrigDatablock -from scitacean.testing.transfer import FakeFileTransfer +from scitacean.testing.transfer import FakeDownloadConnection, FakeFileTransfer def _checksum(data: bytes) -> str: @@ -23,7 +25,7 @@ def _checksum(data: bytes) -> str: @pytest.fixture -def data_files(): +def data_files() -> tuple[list[DownloadDataFile], dict[str, bytes]]: contents = { "file1.dat": b"contents-of-file1", "log/what-happened.log": b"ERROR Flux is off the scale", @@ -41,8 +43,13 @@ def data_files(): return files, contents +DatasetAndFiles = tuple[Dataset, dict[RemotePath | str, bytes]] + + @pytest.fixture -def dataset_and_files(data_files): +def dataset_and_files( + data_files: tuple[list[DownloadDataFile], dict[str, bytes]], +) -> DatasetAndFiles: model = DownloadDataset( contactEmail="p.stibbons@uu.am", creationTime=parse_date("1995-08-06T14:14:14"), @@ -53,7 +60,7 @@ def dataset_and_files(data_files): packedSize=0, pid=PID(prefix="UU.000", pid="5125.ab.663.8c9f"), principalInvestigator="m.ridcully@uu.am", - size=sum(f.size for f in data_files[0]), + size=sum(f.size for f in data_files[0]), # type: ignore[misc] sourceFolder=RemotePath("/src/stibbons/774"), type=DatasetType.RAW, scientificMetadata={ @@ -64,7 +71,7 @@ def dataset_and_files(data_files): block = DownloadOrigDatablock( chkAlg="md5", ownerGroup="faculty", - size=sum(f.size for f in data_files[0]), + size=sum(f.size for f in data_files[0]), # type: ignore[misc] datasetId=PID(prefix="UU.000", pid="5125.ab.663.8c9f"), _id="0941.66.abff.41de", dataFileList=data_files[0], @@ -73,7 +80,8 @@ def dataset_and_files(data_files): dataset_model=model, orig_datablock_models=[block] ) return dset, { - dset.source_folder / name: content for name, content in data_files[1].items() + dset.source_folder / name: content # type: ignore[operator] + for name, content in data_files[1].items() } @@ -82,7 +90,9 @@ def load(name: str | Path) -> bytes: return f.read() -def test_download_files_creates_local_files_select_all(fs, dataset_and_files): +def test_download_files_creates_local_files_select_all( + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files client = Client.without_login( url="/", file_transfer=FakeFileTransfer(fs=fs, files=contents) @@ -96,7 +106,9 @@ def test_download_files_creates_local_files_select_all(fs, dataset_and_files): assert load("download/thaum.dat") == contents["/src/stibbons/774/thaum.dat"] -def test_download_files_creates_local_files_select_none(fs, dataset_and_files): +def test_download_files_creates_local_files_select_none( + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files client = Client.without_login( url="/", file_transfer=FakeFileTransfer(fs=fs, files=contents) @@ -107,7 +119,9 @@ def test_download_files_creates_local_files_select_none(fs, dataset_and_files): assert not Path("download/thaum.dat").exists() -def test_download_files_creates_local_files_select_one_by_string(fs, dataset_and_files): +def test_download_files_creates_local_files_select_one_by_string( + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files client = Client.without_login( url="/", file_transfer=FakeFileTransfer(fs=fs, files=contents) @@ -118,7 +132,9 @@ def test_download_files_creates_local_files_select_one_by_string(fs, dataset_and assert not Path("download/log/what-happened.log").exists() -def test_download_files_creates_local_files_select_two_by_string(fs, dataset_and_files): +def test_download_files_creates_local_files_select_two_by_string( + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files client = Client.without_login( url="/", file_transfer=FakeFileTransfer(fs=fs, files=contents) @@ -135,8 +151,8 @@ def test_download_files_creates_local_files_select_two_by_string(fs, dataset_and def test_download_files_creates_local_files_select_one_by_regex_name_only( - fs, dataset_and_files -): + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files client = Client.without_login( url="/", file_transfer=FakeFileTransfer(fs=fs, files=contents) @@ -148,8 +164,8 @@ def test_download_files_creates_local_files_select_one_by_regex_name_only( def test_download_files_creates_local_files_select_two_by_regex_suffix( - fs, dataset_and_files -): + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files client = Client.without_login( url="/", file_transfer=FakeFileTransfer(fs=fs, files=contents) @@ -161,8 +177,8 @@ def test_download_files_creates_local_files_select_two_by_regex_suffix( def test_download_files_creates_local_files_select_one_by_regex_full_path( - fs, dataset_and_files -): + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files client = Client.without_login( url="/", file_transfer=FakeFileTransfer(fs=fs, files=contents) @@ -176,8 +192,8 @@ def test_download_files_creates_local_files_select_one_by_regex_full_path( def test_download_files_creates_local_files_select_one_by_predicate( - fs, dataset_and_files -): + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files client = Client.without_login( url="/", file_transfer=FakeFileTransfer(fs=fs, files=contents) @@ -190,7 +206,9 @@ def test_download_files_creates_local_files_select_one_by_predicate( assert not Path("download/thaum.dat").exists() -def test_download_files_returns_updated_dataset(fs, dataset_and_files): +def test_download_files_returns_updated_dataset( + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files original = deepcopy(dataset) client = Client.without_login( @@ -210,7 +228,9 @@ def test_download_files_returns_updated_dataset(fs, dataset_and_files): assert not f.is_on_local # original is unchanged -def test_download_files_ignores_checksum_if_alg_is_none(fs, dataset_and_files): +def test_download_files_ignores_checksum_if_alg_is_none( + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files content = b"random-stuff" @@ -223,6 +243,7 @@ def test_download_files_ignores_checksum_if_alg_is_none(fs, dataset_and_files): ) dataset.add_orig_datablock(checksum_algorithm=None) dataset.add_files(File.from_download_model(model)) + assert dataset.source_folder is not None client = Client.without_login( url="/", @@ -234,7 +255,9 @@ def test_download_files_ignores_checksum_if_alg_is_none(fs, dataset_and_files): client.download_files(dataset, target="./download", select="file.txt") -def test_download_files_detects_bad_checksum(fs, dataset_and_files): +def test_download_files_detects_bad_checksum( + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: dataset, contents = dataset_and_files content = b"random-stuff" @@ -247,6 +270,7 @@ def test_download_files_detects_bad_checksum(fs, dataset_and_files): ) dataset.add_orig_datablock(checksum_algorithm="md5") dataset.add_files(File.from_download_model(model)) + assert dataset.source_folder is not None client = Client.without_login( url="/", @@ -258,7 +282,11 @@ def test_download_files_detects_bad_checksum(fs, dataset_and_files): client.download_files(dataset, target="./download", select="file.txt") -def test_download_files_detects_bad_size(fs, dataset_and_files, caplog): +def test_download_files_detects_bad_size( + fs: FakeFilesystem, + dataset_and_files: DatasetAndFiles, + caplog: pytest.LogCaptureFixture, +) -> None: dataset, contents = dataset_and_files content = b"random-stuff" @@ -271,6 +299,7 @@ def test_download_files_detects_bad_size(fs, dataset_and_files, caplog): ) dataset.add_orig_datablock(checksum_algorithm="md5") dataset.add_files(File.from_download_model(model)) + assert dataset.source_folder is not None client = Client.without_login( url="/", @@ -284,7 +313,9 @@ def test_download_files_detects_bad_size(fs, dataset_and_files, caplog): assert "89412" in caplog.text -def test_download_does_not_download_up_to_date_file(fs, dataset_and_files): +def test_download_does_not_download_up_to_date_file( + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: # Ensure the file exists locally dataset, contents = dataset_and_files client = Client.without_login( @@ -297,7 +328,7 @@ class RaisingDownloader(FakeFileTransfer): source_dir = "/" @contextmanager - def connect_for_download(self): + def connect_for_download(self) -> Iterator[FakeDownloadConnection]: raise RuntimeError("Download disabled") client = Client.without_login( @@ -310,8 +341,8 @@ def connect_for_download(self): def test_download_does_not_download_up_to_date_file_manual_checksum( - fs, dataset_and_files -): + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: # Ensure the file exists locally dataset, contents = dataset_and_files client = Client.without_login( @@ -324,7 +355,7 @@ class RaisingDownloader(FakeFileTransfer): source_dir = "/" @contextmanager - def connect_for_download(self): + def connect_for_download(self) -> Iterator[FakeDownloadConnection]: raise RuntimeError("Download disabled") client = Client.without_login( @@ -338,7 +369,9 @@ def connect_for_download(self): assert all(file.local_path is not None for file in downloaded.files) -def test_override_datablock_checksum(fs, dataset_and_files): +def test_override_datablock_checksum( + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: # Ensure the file exists locally dataset, contents = dataset_and_files client = Client.without_login( @@ -351,7 +384,7 @@ class RaisingDownloader(FakeFileTransfer): source_dir = "/" @contextmanager - def connect_for_download(self): + def connect_for_download(self) -> Iterator[FakeDownloadConnection]: raise RuntimeError("Download disabled") client = Client.without_login( @@ -367,7 +400,9 @@ def connect_for_download(self): ) -def test_force_file_download(fs, dataset_and_files): +def test_force_file_download( + fs: FakeFilesystem, dataset_and_files: DatasetAndFiles +) -> None: # Ensure the file exists locally dataset, contents = dataset_and_files client = Client.without_login( @@ -380,7 +415,7 @@ class RaisingDownloader(FakeFileTransfer): source_dir = "/" @contextmanager - def connect_for_download(self): + def connect_for_download(self) -> Iterator[FakeDownloadConnection]: raise RuntimeError("Download disabled") client = Client.without_login( diff --git a/tests/file_test.py b/tests/file_test.py index 11e84217..c79e57c0 100644 --- a/tests/file_test.py +++ b/tests/file_test.py @@ -4,9 +4,11 @@ from dataclasses import replace from datetime import datetime, timedelta, timezone from pathlib import Path +from typing import Any import pytest from dateutil.parser import parse as parse_date +from pyfakefs.fake_filesystem import FakeFilesystem from scitacean import File, IntegrityError, RemotePath from scitacean.filesystem import checksum_of_file @@ -17,11 +19,11 @@ @pytest.fixture -def fake_file(fs): +def fake_file(fs: FakeFilesystem) -> dict[str, Any]: return make_file(fs, path=Path("local", "dir", "events.nxs")) -def test_file_from_local(fake_file): +def test_file_from_local(fake_file: dict[str, Any]) -> None: file = replace(File.from_local(fake_file["path"]), checksum_algorithm="md5") assert file.remote_access_path("/remote") is None assert file.local_path == fake_file["path"] @@ -34,7 +36,7 @@ def test_file_from_local(fake_file): assert abs(fake_file["creation_time"] - file.creation_time) < timedelta(seconds=1) -def test_file_from_local_with_base_path(fake_file): +def test_file_from_local_with_base_path(fake_file: dict[str, Any]) -> None: assert fake_file["path"] == Path("local", "dir", "events.nxs") # used below file = replace( @@ -51,7 +53,7 @@ def test_file_from_local_with_base_path(fake_file): assert abs(fake_file["creation_time"] - file.creation_time) < timedelta(seconds=1) -def test_file_from_local_set_remote_path(fake_file): +def test_file_from_local_set_remote_path(fake_file: dict[str, Any]) -> None: file = replace( File.from_local(fake_file["path"], remote_path="remote/location/file.nxs"), checksum_algorithm="md5", @@ -67,7 +69,7 @@ def test_file_from_local_set_remote_path(fake_file): assert abs(fake_file["creation_time"] - file.creation_time) < timedelta(seconds=1) -def test_file_from_local_set_many_args(fake_file): +def test_file_from_local_set_many_args(fake_file: dict[str, Any]) -> None: file = replace( File.from_local( fake_file["path"], @@ -89,13 +91,15 @@ def test_file_from_local_set_many_args(fake_file): @pytest.mark.parametrize("alg", ["md5", "sha256", "blake2s"]) -def test_file_from_local_select_checksum_algorithm(fake_file, alg): +def test_file_from_local_select_checksum_algorithm( + fake_file: dict[str, Any], alg: str +) -> None: file = replace(File.from_local(fake_file["path"]), checksum_algorithm=alg) expected = checksum_of_file(fake_file["path"], algorithm=alg) assert file.checksum() == expected -def test_file_from_local_remote_path_uses_forward_slash(fs): +def test_file_from_local_remote_path_uses_forward_slash(fs: FakeFilesystem) -> None: fs.create_file(Path("data", "subdir", "file.dat")) file = File.from_local(Path("data", "subdir", "file.dat")) @@ -113,7 +117,7 @@ def test_file_from_local_remote_path_uses_forward_slash(fs): assert file.make_model().path == "file.dat" -def test_file_from_remote_default_args(): +def test_file_from_remote_default_args() -> None: file = File.from_remote( remote_path="/remote/source/file.nxs", size=6123, @@ -132,7 +136,7 @@ def test_file_from_remote_default_args(): assert file.remote_perm is None -def test_file_from_remote_all_args(): +def test_file_from_remote_all_args() -> None: file = File.from_remote( remote_path="/remote/image.png", size=9, @@ -154,7 +158,7 @@ def test_file_from_remote_all_args(): assert file.remote_perm == "wrx" -def test_file_from_remote_checksum_requires_algorithm(): +def test_file_from_remote_checksum_requires_algorithm() -> None: with pytest.raises(TypeError, match="checksum"): File.from_remote( remote_path="/remote/image.png", @@ -164,7 +168,7 @@ def test_file_from_remote_checksum_requires_algorithm(): ) -def test_file_from_download_model(): +def test_file_from_download_model() -> None: model = DownloadDataFile( path="dir/image.jpg", size=12345, time=parse_date("2022-06-22T15:42:53.123Z") ) @@ -177,7 +181,7 @@ def test_file_from_download_model(): assert file.creation_time == parse_date("2022-06-22T15:42:53.123Z") -def test_file_from_download_model_remote_path_uses_forward_slash(): +def test_file_from_download_model_remote_path_uses_forward_slash() -> None: file = File.from_download_model( DownloadDataFile( path="data/subdir/file.dat", @@ -199,7 +203,7 @@ def test_file_from_download_model_remote_path_uses_forward_slash(): assert file.remote_path == RemotePath("data/subdir/file.dat") -def test_make_model_local_file(fake_file): +def test_make_model_local_file(fake_file: dict[str, Any]) -> None: file = replace( File.from_local( fake_file["path"], @@ -219,7 +223,7 @@ def test_make_model_local_file(fake_file): assert abs(fake_file["creation_time"] - model.time) < timedelta(seconds=1) -def test_uploaded(fake_file): +def test_uploaded(fake_file: dict[str, Any]) -> None: file = replace( File.from_local( fake_file["path"], @@ -244,7 +248,7 @@ def test_uploaded(fake_file): assert uploaded._remote_creation_time == parse_date("2100-09-07T11:34:51") -def test_downloaded(): +def test_downloaded() -> None: model = DownloadDataFile( path="dir/stream.s", size=55123, @@ -262,7 +266,7 @@ def test_downloaded(): assert downloaded.remote_perm == "xrw" -def test_creation_time_is_always_local_time(fake_file): +def test_creation_time_is_always_local_time(fake_file: dict[str, Any]) -> None: file = File.from_local(path=fake_file["path"]) model = file.make_model() model.time = parse_date("2105-04-01T04:52:23") @@ -273,7 +277,7 @@ def test_creation_time_is_always_local_time(fake_file): ) -def test_size_is_always_local_size(fake_file): +def test_size_is_always_local_size(fake_file: dict[str, Any]) -> None: file = File.from_local(path=fake_file["path"]) model = file.make_model() model.size = 999999999 @@ -282,7 +286,7 @@ def test_size_is_always_local_size(fake_file): assert uploaded.size == fake_file["size"] -def test_checksum_is_always_local_checksum(fake_file): +def test_checksum_is_always_local_checksum(fake_file: dict[str, Any]) -> None: file = File.from_local(path=fake_file["path"]) uploaded = replace( file.uploaded(), _remote_checksum="6e9eb73953231aebbbc8788f39f08618" @@ -296,7 +300,9 @@ def test_checksum_is_always_local_checksum(fake_file): ).checksum() == checksum_of_file(fake_file["path"], algorithm="sha256") -def test_creation_time_is_up_to_date(fs, fake_file): +def test_creation_time_is_up_to_date( + fs: FakeFilesystem, fake_file: dict[str, Any] +) -> None: file = File.from_local(path=fake_file["path"]) with open(fake_file["path"], "wb") as f: f.write(b"some new content to update time stamp") @@ -306,7 +312,7 @@ def test_creation_time_is_up_to_date(fs, fake_file): assert file.creation_time == new_creation_time -def test_size_is_up_to_date(fs, fake_file): +def test_size_is_up_to_date(fs: FakeFilesystem, fake_file: dict[str, Any]) -> None: file = File.from_local(path=fake_file["path"]) new_contents = b"content with a new size" assert len(new_contents) != fake_file["size"] @@ -315,7 +321,7 @@ def test_size_is_up_to_date(fs, fake_file): assert file.size == len(new_contents) -def test_checksum_is_up_to_date(fs, fake_file): +def test_checksum_is_up_to_date(fs: FakeFilesystem, fake_file: dict[str, Any]) -> None: file = replace(File.from_local(path=fake_file["path"]), checksum_algorithm="md5") new_contents = b"content a different checksum" @@ -329,7 +335,9 @@ def test_checksum_is_up_to_date(fs, fake_file): assert file.checksum() == checksum_digest -def test_validate_after_download_detects_bad_checksum(fake_file): +def test_validate_after_download_detects_bad_checksum( + fake_file: dict[str, Any], +) -> None: model = DownloadDataFile( path=fake_file["path"].name, size=fake_file["size"], @@ -346,7 +354,9 @@ def test_validate_after_download_detects_bad_checksum(fake_file): downloaded.validate_after_download() -def test_validate_after_download_ignores_checksum_if_no_algorithm(fake_file): +def test_validate_after_download_ignores_checksum_if_no_algorithm( + fake_file: dict[str, Any], +) -> None: model = DownloadDataFile( path=fake_file["path"].name, size=fake_file["size"], @@ -360,7 +370,9 @@ def test_validate_after_download_ignores_checksum_if_no_algorithm(fake_file): downloaded.validate_after_download() -def test_validate_after_download_detects_size_mismatch(fake_file, caplog): +def test_validate_after_download_detects_size_mismatch( + fake_file: dict[str, Any], caplog: pytest.LogCaptureFixture +) -> None: model = DownloadDataFile( path=fake_file["path"].name, size=fake_file["size"] + 100, @@ -378,7 +390,7 @@ def test_validate_after_download_detects_size_mismatch(fake_file, caplog): @pytest.mark.parametrize("chk", ["sha256", None]) -def test_local_is_not_up_to_date_for_remote_file(chk): +def test_local_is_not_up_to_date_for_remote_file(chk: str | None) -> None: file = File.from_download_model( DownloadDataFile( path="data.csv", @@ -390,13 +402,13 @@ def test_local_is_not_up_to_date_for_remote_file(chk): assert not file.local_is_up_to_date() -def test_local_is_up_to_date_for_local_file(): +def test_local_is_up_to_date_for_local_file() -> None: # Note that the file does not actually exist on disk but the test still works. file = File.from_local(path="image.jpg") assert file.local_is_up_to_date() -def test_local_is_up_to_date_default_checksum_alg(fs): +def test_local_is_up_to_date_default_checksum_alg(fs: FakeFilesystem) -> None: contents = b"some file content" checksum = hashlib.new("blake2b") checksum.update(contents) @@ -415,7 +427,7 @@ def test_local_is_up_to_date_default_checksum_alg(fs): assert file.local_is_up_to_date() -def test_local_is_up_to_date_matching_checksum(fake_file): +def test_local_is_up_to_date_matching_checksum(fake_file: dict[str, Any]) -> None: model = DownloadDataFile( path=fake_file["path"].name, size=fake_file["size"], @@ -429,7 +441,7 @@ def test_local_is_up_to_date_matching_checksum(fake_file): assert file.local_is_up_to_date() -def test_local_is_not_up_to_date_differing_checksum(fake_file): +def test_local_is_not_up_to_date_differing_checksum(fake_file: dict[str, Any]) -> None: model = DownloadDataFile( path=fake_file["path"].name, size=fake_file["size"], diff --git a/tests/filesystem_test.py b/tests/filesystem_test.py index f4d6e24d..b3638087 100644 --- a/tests/filesystem_test.py +++ b/tests/filesystem_test.py @@ -3,10 +3,12 @@ import hashlib from datetime import datetime, timedelta, timezone from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath +from typing import Any import pytest from hypothesis import given from hypothesis import strategies as st +from pyfakefs.fake_filesystem import FakeFilesystem from scitacean.filesystem import ( RemotePath, @@ -17,14 +19,14 @@ ) -def test_remote_path_creation_and_posix(): +def test_remote_path_creation_and_posix() -> None: assert RemotePath("/mnt/data/folder/file.txt").posix == "/mnt/data/folder/file.txt" assert RemotePath("dir/image.png").posix == "dir/image.png" assert RemotePath("data.nxs").posix == "data.nxs" assert RemotePath(RemotePath("source/events.h5")).posix == "source/events.h5" -def test_remote_path_from_segments(): +def test_remote_path_from_segments() -> None: assert RemotePath("/mnt", "dir", "file.csv") == RemotePath("/mnt/dir/file.csv") assert RemotePath("folder", "file") == RemotePath("folder/file") assert RemotePath("", "folder", "file.csv") == RemotePath("folder/file.csv") @@ -32,7 +34,7 @@ def test_remote_path_from_segments(): assert RemotePath("folder", "file.csv", "") == RemotePath("folder/file.csv") -def test_remote_path_init_requires_path_like(): +def test_remote_path_init_requires_path_like() -> None: with pytest.raises(TypeError): RemotePath(6133) # type: ignore[arg-type] with pytest.raises(TypeError): @@ -40,34 +42,34 @@ def test_remote_path_init_requires_path_like(): @pytest.mark.parametrize("local_type", [PurePath, Path]) -def test_remote_path_rejects_os_path(local_type): +def test_remote_path_rejects_os_path(local_type: type) -> None: with pytest.raises(TypeError): RemotePath(local_type("dir", "file.csv")) @pytest.mark.parametrize("local_type", [PurePath, PurePosixPath, PureWindowsPath]) -def test_remote_path_from_local(local_type): +def test_remote_path_from_local(local_type: type) -> None: local_path = local_type("dir", "folder", "file.csv") remote_path = RemotePath.from_local(local_path) assert remote_path == RemotePath("dir/folder/file.csv") @pytest.mark.parametrize("local_type", [PurePath, PurePosixPath, PureWindowsPath]) -def test_remote_path_posix_uses_forward_slashes(local_type): +def test_remote_path_posix_uses_forward_slashes(local_type: type) -> None: local_path = local_type("dir", "folder", "file.csv") remote_path = RemotePath.from_local(local_path) assert remote_path.posix == "dir/folder/file.csv" -def test_remote_path_str(): +def test_remote_path_str() -> None: assert str(RemotePath("folder/file.dat")) == "RemotePath('folder/file.dat')" -def test_remote_path_repr(): +def test_remote_path_repr() -> None: assert repr(RemotePath("folder/file.dat")) == "RemotePath('folder/file.dat')" -def test_remote_path_to_local(): +def test_remote_path_to_local() -> None: assert RemotePath("folder/file.dat").to_local() == PurePath("folder", "file.dat") assert RemotePath("/folder/file.dat").to_local() == PurePath("/folder", "file.dat") assert RemotePath("folder//file.dat").to_local() == PurePath("folder", "file.dat") @@ -77,7 +79,7 @@ def test_remote_path_to_local(): @pytest.mark.parametrize( "types", [(RemotePath, RemotePath), (RemotePath, str), (str, RemotePath)] ) -def test_remote_path_eq(types): +def test_remote_path_eq(types: Any) -> None: ta, tb = types assert ta("/source/data.csv") == tb("/source/data.csv") @@ -85,7 +87,7 @@ def test_remote_path_eq(types): @pytest.mark.parametrize( "types", [(RemotePath, RemotePath), (RemotePath, str), (str, RemotePath)] ) -def test_remote_path_neq(types): +def test_remote_path_neq(types: Any) -> None: ta, tb = types assert ta("/source/data.csv") != tb("/host/dir/song.mp3") @@ -93,7 +95,7 @@ def test_remote_path_neq(types): @pytest.mark.parametrize( "types", [(RemotePath, RemotePath), (RemotePath, str), (str, RemotePath)] ) -def test_remote_path_join(types): +def test_remote_path_join(types: Any) -> None: ta, tb = types assert ta("/source/123") / tb("file.data") == RemotePath("/source/123/file.data") assert ta("/source/123/") / tb("file.data") == RemotePath("/source/123/file.data") @@ -104,7 +106,7 @@ def test_remote_path_join(types): @pytest.mark.parametrize( "types", [(RemotePath, RemotePath), (RemotePath, str), (str, RemotePath)] ) -def test_remote_path_join_url(types): +def test_remote_path_join_url(types: Any) -> None: ta, tb = types assert ta("https://server.eu") / tb("1234-abcd/data.txt") == RemotePath( "https://server.eu/1234-abcd/data.txt" @@ -120,21 +122,21 @@ def test_remote_path_join_url(types): ) -def test_remote_path_join_rejects_os_path(): +def test_remote_path_join_rejects_os_path() -> None: with pytest.raises(TypeError): RemotePath("asd") / Path("qwe") # type: ignore[operator] with pytest.raises(TypeError): Path("qwe") / RemotePath("asd") # type: ignore[operator] -def test_remote_path_name(): +def test_remote_path_name() -> None: assert RemotePath("table.csv").name == "table.csv" assert RemotePath("README").name == "README" assert RemotePath("path/").name == "path" assert RemotePath("dir/folder/file1.txt").name == "file1.txt" -def test_remote_path_suffix(): +def test_remote_path_suffix() -> None: assert RemotePath("file.txt").suffix == ".txt" assert RemotePath("folder/image.png").suffix == ".png" assert RemotePath("dir/table.txt.csv").suffix == ".csv" @@ -143,7 +145,7 @@ def test_remote_path_suffix(): assert RemotePath("source/file").suffix is None -def test_remote_path_parent(): +def test_remote_path_parent() -> None: assert RemotePath("/").parent == RemotePath("/") assert RemotePath("/folder").parent == RemotePath("/") assert RemotePath("/folder/").parent == RemotePath("/") @@ -155,7 +157,7 @@ def test_remote_path_parent(): assert RemotePath("relative/sub").parent == RemotePath("relative") -def test_remote_path_truncated(): +def test_remote_path_truncated() -> None: assert RemotePath("something-long.txt").truncated(10) == "someth.txt" assert RemotePath("longlonglong/short").truncated(5) == "longl/short" assert RemotePath("a-long.data.dir/filename.csv").truncated(7) == "a-l.dir/fil.csv" @@ -163,12 +165,12 @@ def test_remote_path_truncated(): @pytest.mark.parametrize("size", [0, 1, 57121]) -def test_file_size(fs, size): +def test_file_size(fs: FakeFilesystem, size: int) -> None: fs.create_file("image.tiff", st_size=size) assert file_size(Path("image.tiff")) == size -def test_file_modification_time(fs): +def test_file_modification_time(fs: FakeFilesystem) -> None: fs.create_file("data.dat") expected = datetime.now(tz=timezone.utc) assert abs(file_modification_time(Path("data.dat")) - expected) < timedelta( @@ -181,7 +183,7 @@ def test_file_modification_time(fs): [b"small file contents", b"large contents " * 100000], ids=("small", "large"), ) -def test_checksum_of_file(fs, contents): +def test_checksum_of_file(fs: FakeFilesystem, contents: bytes) -> None: fs.create_file("file.txt", contents=contents) assert ( @@ -195,18 +197,18 @@ def test_checksum_of_file(fs, contents): @pytest.mark.parametrize("path_type", [str, Path, RemotePath]) -def test_escape_path_returns_same_type_as_input(path_type): +def test_escape_path_returns_same_type_as_input(path_type: type) -> None: assert isinstance(escape_path(path_type("x")), path_type) @given(st.text()) -def test_escape_path_returns_ascii(path): +def test_escape_path_returns_ascii(path: str) -> None: # does not raise escape_path(path).encode("ascii", "strict") @given(st.text()) -def test_escape_path_returns_no_bad_character(path): +def test_escape_path_returns_no_bad_character(path: str) -> None: bad_linux = "/" + chr(0) bad_windows = '<>:"\\/|?*' + "".join(map(chr, range(0, 31))) escaped = escape_path(path) diff --git a/tests/html_repr/html_repr_test.py b/tests/html_repr/html_repr_test.py index da172ffc..061b749e 100644 --- a/tests/html_repr/html_repr_test.py +++ b/tests/html_repr/html_repr_test.py @@ -7,7 +7,7 @@ # We don't want to test the concrete layout as that may change # without breaking anything. So just make sure that the result # contains the relevant data. -def test_dataset_html_repr(): +def test_dataset_html_repr() -> None: ds = Dataset( type="raw", name="My dataset", @@ -30,7 +30,7 @@ def test_dataset_html_repr(): assert "unit" in res -def test_attachment_html_repr(): +def test_attachment_html_repr() -> None: att = Attachment( caption="THE_CAPTION.jpg", owner_group="ThePeople", diff --git a/tests/model_test.py b/tests/model_test.py index 59b3903b..f155aec6 100644 --- a/tests/model_test.py +++ b/tests/model_test.py @@ -8,7 +8,7 @@ from hypothesis import given, settings from hypothesis import strategies as st -from scitacean import PID, DatasetType, RemotePath, model +from scitacean import PID, Client, DatasetType, RemotePath, model from scitacean.model import ( DownloadAttachment, DownloadDataset, @@ -16,6 +16,7 @@ UploadDerivedDataset, UploadRawDataset, ) +from scitacean.testing.backend import config as backend_config T = TypeVar("T") @@ -41,7 +42,10 @@ def build_user_model_for_upload(cls: type[T]) -> st.SearchStrategy[T]: ) # Cannot test (model.Sample, model.UploadSample) because hypothesis # cannot handle fields with type Any. -def test_can_make_upload_model(model_types, data): +def test_can_make_upload_model( + model_types: tuple[type[model.BaseUserModel], type[model.BaseModel]], + data: st.DataObject, +) -> None: user_model_type, upload_model_type = model_types user_model = data.draw(build_user_model_for_upload(user_model_type)) upload_model = user_model.make_upload_model() @@ -50,7 +54,7 @@ def test_can_make_upload_model(model_types, data): @settings(max_examples=10) @given(build_user_model_for_upload(model.Attachment)) -def test_upload_attachment_fields(attachment): +def test_upload_attachment_fields(attachment: model.Attachment) -> None: upload_attachment = attachment.make_upload_model() assert upload_attachment.caption == attachment.caption assert upload_attachment.accessGroups == attachment.access_groups @@ -59,7 +63,7 @@ def test_upload_attachment_fields(attachment): @settings(max_examples=10) @given(st.builds(model.Attachment)) -def test_upload_model_rejects_non_upload_fields(attachment): +def test_upload_model_rejects_non_upload_fields(attachment: model.Attachment) -> None: attachment._created_by = "the-creator" with pytest.raises(ValueError, match="field.*upload"): attachment.make_upload_model() @@ -78,7 +82,10 @@ def test_upload_model_rejects_non_upload_fields(attachment): (model.Relationship, model.DownloadRelationship), ], ) -def test_can_make_from_download_model(model_types, data): +def test_can_make_from_download_model( + model_types: tuple[type[model.BaseUserModel], type[model.BaseModel]], + data: st.DataObject, +) -> None: user_model_type, download_model_type = model_types download_model = data.draw(st.builds(download_model_type)) # doesn't raise @@ -87,7 +94,9 @@ def test_can_make_from_download_model(model_types, data): @settings(max_examples=10) @given(st.builds(model.DownloadAttachment)) -def test_download_attachment_fields(download_attachment): +def test_download_attachment_fields( + download_attachment: model.DownloadAttachment, +) -> None: attachment = model.Attachment.from_download_model(download_attachment) assert attachment.caption == download_attachment.caption assert attachment.dataset_id == download_attachment.datasetId @@ -95,8 +104,10 @@ def test_download_attachment_fields(download_attachment): def test_derived_dataset_default_values( - real_client, require_scicat_backend, scicat_access -): + real_client: Client, + require_scicat_backend: None, + scicat_access: backend_config.SciCatAccess, +) -> None: dset = UploadDerivedDataset( accessGroups=["access1"], contactEmail="contact@email.com", @@ -111,6 +122,7 @@ def test_derived_dataset_default_values( type=DatasetType.DERIVED, ) pid = real_client.scicat.create_dataset_model(dset).pid + assert pid is not None finalized = real_client.scicat.get_dataset_model(pid) # Inputs @@ -153,7 +165,11 @@ def test_derived_dataset_default_values( assert finalized.version is None -def test_raw_dataset_default_values(real_client, require_scicat_backend, scicat_access): +def test_raw_dataset_default_values( + real_client: Client, + require_scicat_backend: None, + scicat_access: backend_config.SciCatAccess, +) -> None: dset = UploadRawDataset( accessGroups=["access1"], contactEmail="contact@email.com", @@ -167,6 +183,7 @@ def test_raw_dataset_default_values(real_client, require_scicat_backend, scicat_ type=DatasetType.RAW, ) pid = real_client.scicat.create_dataset_model(dset).pid + assert pid is not None finalized = real_client.scicat.get_dataset_model(pid) # Inputs @@ -211,7 +228,7 @@ def test_raw_dataset_default_values(real_client, require_scicat_backend, scicat_ assert finalized.version is None -def test_default_masked_fields_are_dropped(): +def test_default_masked_fields_are_dropped() -> None: mod = DownloadOrigDatablock( # type: ignore[call-arg] id="abc", _v="123", @@ -224,7 +241,7 @@ def test_default_masked_fields_are_dropped(): assert not hasattr(mod, "__v") -def test_custom_masked_fields_are_dropped(): +def test_custom_masked_fields_are_dropped() -> None: mod = DownloadDataset( # type: ignore[call-arg] attachments=[{"id": "abc"}], id="abc", @@ -239,7 +256,7 @@ def test_custom_masked_fields_are_dropped(): assert not hasattr(mod, "__v") -def test_fields_override_masks(): +def test_fields_override_masks() -> None: # '_id' is masked but the model has a field 'id' with alias '_id'. mod = DownloadOrigDatablock( # type: ignore[call-arg] _id="abc", @@ -249,7 +266,7 @@ def test_fields_override_masks(): assert not hasattr(mod, "_id") -def test_fields_override_masks_att(): +def test_fields_override_masks_att() -> None: # 'id' is masked but the model has a field 'id' without alias mod = DownloadAttachment( # type: ignore[call-arg] _id="abc", diff --git a/tests/testing/strategies_test.py b/tests/testing/strategies_test.py index 9ea4855b..e0af402d 100644 --- a/tests/testing/strategies_test.py +++ b/tests/testing/strategies_test.py @@ -4,24 +4,24 @@ from hypothesis import given, settings from hypothesis import strategies as st -from scitacean import PID, DatasetType +from scitacean import PID, Dataset, DatasetType from scitacean.testing import strategies as sst @given(sst.datasets(for_upload=True)) -def test_datasets_makes_valid_dataset(dset): +def test_datasets_makes_valid_dataset(dset: Dataset) -> None: _ = dset.make_upload_model() @settings(max_examples=10) @given(sst.datasets(type=DatasetType.RAW)) -def test_datasets_can_set_type_to_raw(dset): +def test_datasets_can_set_type_to_raw(dset: Dataset) -> None: assert dset.type == "raw" @settings(max_examples=10) @given(sst.datasets(type=DatasetType.DERIVED)) -def test_datasets_can_set_type_to_derived(dset): +def test_datasets_can_set_type_to_derived(dset: Dataset) -> None: assert dset.type == "derived" @@ -36,7 +36,7 @@ def test_datasets_can_set_type_to_derived(dset): description=None, ) ) -def test_datasets_can_fix_fields(dset): +def test_datasets_can_fix_fields(dset: Dataset) -> None: assert dset.name == "my-dataset-name" assert dset.access_groups == ["group1", "another-group"] assert dset.source_folder == "/the/source/of/truth" @@ -53,7 +53,8 @@ def test_datasets_can_fix_fields(dset): name=st.text(min_size=2, max_size=5), ) ) -def test_datasets_can_set_strategy_for_fields(dset): +def test_datasets_can_set_strategy_for_fields(dset: Dataset) -> None: assert dset.created_by == "the-creator" assert dset.owner in ("owner1", "owner2") + assert dset.name is not None assert 2 <= len(dset.name) <= 5 diff --git a/tests/thumbnail_test.py b/tests/thumbnail_test.py index 04d73e1e..06032b62 100644 --- a/tests/thumbnail_test.py +++ b/tests/thumbnail_test.py @@ -2,96 +2,97 @@ # Copyright (c) 2024 SciCat Project (https://github.com/SciCatProject/scitacean) import pytest +from pyfakefs.fake_filesystem import FakeFilesystem from scitacean import Thumbnail -def test_mime_type(): +def test_mime_type() -> None: thumbnail = Thumbnail(data=b"", mime="image/png") assert thumbnail.mime == "image/png" assert thumbnail.mime_type == "image" assert thumbnail.mime_subtype == "png" -def test_no_mime_type(): +def test_no_mime_type() -> None: thumbnail = Thumbnail(data=b"", mime=None) assert thumbnail.mime is None assert thumbnail.mime_type is None assert thumbnail.mime_subtype is None -def test_parse_mime_prefix(): +def test_parse_mime_prefix() -> None: thumbnail = Thumbnail.parse("data:image/png,YWJj") assert thumbnail.mime == "image/png" assert thumbnail.encoded_data() == "YWJj" -def test_parse_mime_encoding(): +def test_parse_mime_encoding() -> None: thumbnail = Thumbnail.parse("image/png;base64,YWJj") assert thumbnail.mime == "image/png" assert thumbnail.encoded_data() == "YWJj" -def test_parse_mime_param(): +def test_parse_mime_param() -> None: thumbnail = Thumbnail.parse("text/html; charset=utf-8,YWEzMzQ=") assert thumbnail.mime == "text/html" assert thumbnail.encoded_data() == "YWEzMzQ=" -def test_parse_mime_2_param2(): +def test_parse_mime_2_param2() -> None: thumbnail = Thumbnail.parse("image/svg+xml; charset=utf-8;base64,eXEzeGE=") assert thumbnail.mime == "image/svg+xml" assert thumbnail.encoded_data() == "eXEzeGE=" -def test_parse_empty_params(): +def test_parse_empty_params() -> None: thumbnail = Thumbnail.parse("image/jpeg;,amY7YQ==") assert thumbnail.mime == "image/jpeg" assert thumbnail.encoded_data() == "amY7YQ==" -def test_parse_no_params(): +def test_parse_no_params() -> None: thumbnail = Thumbnail.parse("image/svg,OGZh") assert thumbnail.mime == "image/svg" assert thumbnail.encoded_data() == "OGZh" -def test_parse_no_mime(): +def test_parse_no_mime() -> None: thumbnail = Thumbnail.parse("amE5MHM4aA==") assert thumbnail.mime is None assert thumbnail.encoded_data() == "amE5MHM4aA==" -def test_parse_thumbnail_argument(): +def test_parse_thumbnail_argument() -> None: thumbnail1 = Thumbnail(mime="image/png", data=b"jak2kcna") thumbnail2 = Thumbnail.parse(thumbnail1) assert thumbnail1 == thumbnail2 assert thumbnail1 is not thumbnail2 -def test_init_encode(): +def test_init_encode() -> None: thumbnail = Thumbnail(mime=None, data=b"abc") assert thumbnail.decoded_data() == b"abc" assert thumbnail.encoded_data() == "YWJj" -def test_init_decode(): +def test_init_decode() -> None: thumbnail = Thumbnail(mime=None, _encoded_data="YWEzMzQ=") assert thumbnail.decoded_data() == b"aa334" assert thumbnail.encoded_data() == "YWEzMzQ=" -def test_init_no_data_raises(): +def test_init_no_data_raises() -> None: with pytest.raises(TypeError): Thumbnail(mime=None) -def test_init_both_data_raises(): +def test_init_both_data_raises() -> None: with pytest.raises(TypeError): Thumbnail(mime=None, data=b"jalks", _encoded_data="YWEzMzQ=") -def test_load_file(fs): +def test_load_file(fs: FakeFilesystem) -> None: fs.create_file("fingers.jpg", contents=b"jal2l9vun2") thumbnail = Thumbnail.load_file("fingers.jpg") assert thumbnail.mime == "image/jpeg" @@ -99,7 +100,7 @@ def test_load_file(fs): assert thumbnail.encoded_data() == "amFsMmw5dnVuMg==" -def test_load_file_unknown_mime_type(fs): +def test_load_file_unknown_mime_type(fs: FakeFilesystem) -> None: fs.create_file("bad.xxx", contents=b"f9gas03n") thumbnail = Thumbnail.load_file("bad.xxx") assert thumbnail.mime is None @@ -107,12 +108,12 @@ def test_load_file_unknown_mime_type(fs): assert thumbnail.encoded_data() == "ZjlnYXMwM24=" -def test_serialize(): +def test_serialize() -> None: thumbnail = Thumbnail(mime="image/jpeg", data=b"ags9da0") assert thumbnail.serialize() == "data:image/jpeg;base64,YWdzOWRhMA==" -def test_serialize_parse_roundtrip(): +def test_serialize_parse_roundtrip() -> None: thumbnail = Thumbnail(mime="image/svg+xml", data=b"412897a897s") serialized = thumbnail.serialize() reconstructed = Thumbnail.parse(serialized) diff --git a/tests/transfer/link_test.py b/tests/transfer/link_test.py index 02590e41..2b1c2a7f 100644 --- a/tests/transfer/link_test.py +++ b/tests/transfer/link_test.py @@ -4,6 +4,7 @@ import hashlib import sys from datetime import datetime, timezone +from pathlib import Path import pytest @@ -16,7 +17,7 @@ pytest.skip("LinkFileTransfer does not work on Windows", allow_module_level=True) -def test_download_one_file(tmp_path): +def test_download_one_file(tmp_path: Path) -> None: remote_dir = tmp_path / "server" remote_dir.mkdir() remote_dir.joinpath("text.txt").write_text("This is some text for testing.\n") @@ -34,7 +35,7 @@ def test_download_one_file(tmp_path): ) -def test_download_two_files(tmp_path): +def test_download_two_files(tmp_path: Path) -> None: remote_dir = tmp_path / "server" remote_dir.mkdir() remote_dir.joinpath("table.csv").write_text("7,2\n5,2\n") @@ -57,14 +58,14 @@ def test_download_two_files(tmp_path): ) -def test_link_transfer_cannot_upload(): +def test_link_transfer_cannot_upload() -> None: ds = Dataset(type="raw", source_folder=RemotePath("/data/upload")) linker = LinkFileTransfer() with pytest.raises(NotImplementedError): linker.connect_for_upload(ds) -def test_client_with_link(tmp_path): +def test_client_with_link(tmp_path: Path) -> None: content = "This is some text for testing.\n" checksum = hashlib.md5(content.encode("utf-8")).hexdigest() remote_dir = tmp_path / "server" @@ -119,7 +120,7 @@ def test_client_with_link(tmp_path): ) -def test_client_with_link_local_file_exists(tmp_path): +def test_client_with_link_local_file_exists(tmp_path: Path) -> None: content = "This is some text for testing.\n" checksum = hashlib.md5(content.encode("utf-8")).hexdigest() remote_dir = tmp_path / "server" @@ -180,7 +181,7 @@ def test_client_with_link_local_file_exists(tmp_path): assert not local_dir.joinpath("file1.txt").is_symlink() -def test_client_with_link_local_file_exists_clashing_content(tmp_path): +def test_client_with_link_local_file_exists_clashing_content(tmp_path: Path) -> None: content = "This is some text for testing.\n" checksum = hashlib.md5(content.encode("utf-8")).hexdigest() remote_dir = tmp_path / "server" @@ -233,7 +234,7 @@ def test_client_with_link_local_file_exists_clashing_content(tmp_path): client.download_files(downloaded, target=local_dir) -def test_download_file_does_not_exist(tmp_path): +def test_download_file_does_not_exist(tmp_path: Path) -> None: remote_dir = tmp_path / "server" remote_dir.mkdir() local_dir = tmp_path / "user" diff --git a/tests/transfer/sftp_test.py b/tests/transfer/sftp_test.py index 8c461798..ab22036c 100644 --- a/tests/transfer/sftp_test.py +++ b/tests/transfer/sftp_test.py @@ -23,11 +23,13 @@ @pytest.fixture(scope="session", autouse=True) -def _server(request, sftp_fileserver): +def _server(request, sftp_fileserver) -> None: skip_if_not_sftp(request) -def test_download_one_file(sftp_access, sftp_connect_with_username_password, tmp_path): +def test_download_one_file( + sftp_access, sftp_connect_with_username_password, tmp_path +) -> None: sftp = SFTPFileTransfer( host=sftp_access.host, port=sftp_access.port, @@ -42,7 +44,9 @@ def test_download_one_file(sftp_access, sftp_connect_with_username_password, tmp ) -def test_download_two_files(sftp_access, sftp_connect_with_username_password, tmp_path): +def test_download_two_files( + sftp_access, sftp_connect_with_username_password, tmp_path +) -> None: sftp = SFTPFileTransfer( host=sftp_access.host, port=sftp_access.port, @@ -278,14 +282,16 @@ def test_stat_uploaded_file( class CorruptingSFTP(paramiko.SFTPClient): """Appends bytes to uploaded files to simulate a broken transfer.""" - def put(self, localpath, remotepath, callback=None, confirm=True): + def put( + self, localpath, remotepath, callback=None, confirm=True + ) -> paramiko.SFTPAttributes: with open(localpath) as f: content = f.read() with tempfile.TemporaryDirectory() as tempdir: corrupted_path = Path(tempdir) / "corrupted" with open(corrupted_path, "w") as f: f.write(content + "\nevil bytes") - super().put(str(corrupted_path), remotepath, callback, confirm) + return super().put(str(corrupted_path), remotepath, callback, confirm) class CorruptingTransfer(paramiko.Transport): @@ -296,7 +302,7 @@ def open_sftp_client(self) -> paramiko.SFTPClient: @pytest.fixture -def sftp_corrupting_connect(sftp_access, sftp_connection_config): +def sftp_corrupting_connect(sftp_access, sftp_connection_config) -> None: def connect(host: str, port: int) -> paramiko.SFTPClient: client = paramiko.SSHClient() client.set_missing_host_key_policy(IgnorePolicy()) @@ -315,7 +321,9 @@ def connect(host: str, port: int) -> paramiko.SFTPClient: class RaisingSFTP(paramiko.SFTPClient): - def put(self, localpath, remotepath, callback=None, confirm=True): + def put( + self, localpath, remotepath, callback=None, confirm=True + ) -> paramiko.SFTPAttributes: raise RuntimeError("Upload disabled") @@ -325,7 +333,7 @@ def open_sftp_client(self) -> paramiko.SFTPClient: @pytest.fixture -def sftp_raising_connect(sftp_access): +def sftp_raising_connect(sftp_access) -> None: def connect(host: str, port: int) -> paramiko.SFTPClient: client = paramiko.SSHClient() client.set_missing_host_key_policy(IgnorePolicy()) @@ -365,7 +373,7 @@ def test_upload_file_reverts_if_upload_fails( class SFTPTestFileTransfer(SFTPFileTransfer): - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) @contextmanager diff --git a/tests/upload_test.py b/tests/upload_test.py index 81528b0e..b640b04e 100644 --- a/tests/upload_test.py +++ b/tests/upload_test.py @@ -1,14 +1,16 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2024 SciCat Project (https://github.com/SciCatProject/scitacean) +from collections.abc import Iterator from contextlib import contextmanager +from datetime import datetime from typing import cast import pytest from dateutil.parser import parse as parse_date +from pyfakefs.fake_filesystem import FakeFilesystem from scitacean import ( - PID, Attachment, Client, Dataset, @@ -17,8 +19,10 @@ ScicatCommError, Thumbnail, ) +from scitacean.testing.backend import config as backend_config from scitacean.testing.client import FakeClient from scitacean.testing.transfer import FakeFileTransfer +from scitacean.typing import UploadConnection from .common.files import make_file @@ -30,7 +34,7 @@ def get_file_transfer(client: Client) -> FakeFileTransfer: @pytest.fixture -def dataset(): +def dataset() -> Dataset: return Dataset( access_groups=["group1", "2nd_group"], investigator="ridcully@uu.am", @@ -52,7 +56,7 @@ def dataset(): @pytest.fixture -def dataset_with_files(dataset, fs): +def dataset_with_files(dataset: Dataset, fs: FakeFilesystem) -> Dataset: make_file(fs, path="file.nxs", contents=b"contents of file.nxs") make_file(fs, path="the_log_file.log", contents=b"this is a log file") dataset.add_local_files("file.nxs", "the_log_file.log") @@ -60,7 +64,7 @@ def dataset_with_files(dataset, fs): @pytest.fixture -def attachments(): +def attachments() -> list[Attachment]: return [ Attachment( caption="Attachment no 1", @@ -76,7 +80,9 @@ def attachments(): @pytest.fixture -def client(fs, scicat_access): +def client( + fs: FakeFilesystem, scicat_access: backend_config.SciCatAccess +) -> FakeClient: return FakeClient.from_credentials( url="", **scicat_access.user.credentials, @@ -84,8 +90,11 @@ def client(fs, scicat_access): ) -def test_upload_returns_updated_dataset_no_attachments(client, dataset_with_files): +def test_upload_returns_updated_dataset_no_attachments( + client: FakeClient, dataset_with_files: Dataset +) -> None: finalized = client.upload_new_dataset_now(dataset_with_files) + assert finalized.pid is not None expected = client.get_dataset(finalized.pid, attachments=True).replace( # The backend may update the dataset after upload _read_only={ @@ -97,10 +106,11 @@ def test_upload_returns_updated_dataset_no_attachments(client, dataset_with_file def test_upload_returns_updated_dataset_with_attachments( - client, dataset_with_files, attachments -): + client: FakeClient, dataset_with_files: Dataset, attachments: list[Attachment] +) -> None: dataset_with_files.attachments = attachments finalized = client.upload_new_dataset_now(dataset_with_files) + assert finalized.pid is not None expected = client.get_dataset(finalized.pid, attachments=True).replace( # The backend may update the dataset after upload _read_only={ @@ -111,8 +121,11 @@ def test_upload_returns_updated_dataset_with_attachments( assert finalized == expected -def test_upload_without_files_creates_dataset(client, dataset): +def test_upload_without_files_creates_dataset( + client: FakeClient, dataset: Dataset +) -> None: finalized = client.upload_new_dataset_now(dataset) + assert finalized.pid is not None expected = client.get_dataset(finalized.pid, attachments=True).replace( # The backend may update the dataset after upload _read_only={ @@ -125,11 +138,11 @@ def test_upload_without_files_creates_dataset(client, dataset): client.scicat.get_orig_datablocks(finalized.pid) -def test_upload_without_files_does_not_need_file_transfer(dataset): +def test_upload_without_files_does_not_need_file_transfer(dataset: Dataset) -> None: client = FakeClient() finalized = client.upload_new_dataset_now(dataset) - pid = cast(PID, finalized.pid) - expected = client.get_dataset(pid, attachments=True).replace( + assert finalized.pid is not None + expected = client.get_dataset(finalized.pid, attachments=True).replace( # The backend may update the dataset after upload _read_only={ "updated_at": finalized.updated_at, @@ -138,10 +151,10 @@ def test_upload_without_files_does_not_need_file_transfer(dataset): ) assert finalized == expected with pytest.raises(ScicatCommError): - client.scicat.get_orig_datablocks(pid) + client.scicat.get_orig_datablocks(finalized.pid) -def test_upload_without_files_does_not_need_revert_files(dataset): +def test_upload_without_files_does_not_need_revert_files(dataset: Dataset) -> None: client = FakeClient( disable={"create_dataset_model": ScicatCommError("Ingestion failed")} ) @@ -150,17 +163,20 @@ def test_upload_without_files_does_not_need_revert_files(dataset): client.upload_new_dataset_now(dataset) -def test_upload_with_only_remote_files_does_not_need_file_transfer(dataset): +def test_upload_with_only_remote_files_does_not_need_file_transfer( + dataset: Dataset, +) -> None: + creation_time = cast(datetime, dataset.creation_time) dataset.add_files( File.from_remote( - remote_path="source/file1.h5", size=512, creation_time=dataset.creation_time + remote_path="source/file1.h5", size=512, creation_time=creation_time ) ) client = FakeClient() finalized = client.upload_new_dataset_now(dataset) - pid = cast(PID, finalized.pid) - expected = client.get_dataset(pid, attachments=True).replace( + assert finalized.pid is not None + expected = client.get_dataset(finalized.pid, attachments=True).replace( # The backend may update the dataset after upload _read_only={ "updated_at": finalized.updated_at, @@ -170,9 +186,11 @@ def test_upload_with_only_remote_files_does_not_need_file_transfer(dataset): assert finalized == expected -def test_upload_with_both_remote_and_local_files(client, dataset_with_files): +def test_upload_with_both_remote_and_local_files( + client: FakeClient, dataset_with_files: Dataset +) -> None: original_file_names = { - dataset_with_files.source_folder / file.remote_path + dataset_with_files.source_folder / file.remote_path # type: ignore[operator] for file in dataset_with_files.files } dataset_with_files.add_files( @@ -182,6 +200,7 @@ def test_upload_with_both_remote_and_local_files(client, dataset_with_files): ) finalized = client.upload_new_dataset_now(dataset_with_files) + assert finalized.pid is not None expected = client.get_dataset(finalized.pid, attachments=True).replace( # The backend may update the dataset after upload _read_only={ @@ -194,7 +213,9 @@ def test_upload_with_both_remote_and_local_files(client, dataset_with_files): assert get_file_transfer(client).files.keys() == original_file_names -def test_upload_with_file_with_both_remote_and_local_path(client, dataset_with_files): +def test_upload_with_file_with_both_remote_and_local_path( + client: FakeClient, dataset_with_files: Dataset +) -> None: file = File.from_remote( remote_path="file1.h5", size=6123, creation_time="2019-09-09T19:29:39Z" ) @@ -207,8 +228,11 @@ def test_upload_with_file_with_both_remote_and_local_path(client, dataset_with_f client.upload_new_dataset_now(dataset_with_files) -def test_upload_creates_dataset_and_datablock(client, dataset_with_files): +def test_upload_creates_dataset_and_datablock( + client: FakeClient, dataset_with_files: Dataset +) -> None: finalized = client.upload_new_dataset_now(dataset_with_files) + assert finalized.pid is not None assert client.datasets[finalized.pid].createdAt == finalized.created_at assert client.datasets[finalized.pid].datasetName == finalized.name assert client.datasets[finalized.pid].owner == finalized.owner @@ -219,9 +243,12 @@ def test_upload_creates_dataset_and_datablock(client, dataset_with_files): assert client.orig_datablocks[finalized.pid][0].size == finalized.size -def test_upload_creates_attachments(client, dataset, attachments): +def test_upload_creates_attachments( + client: FakeClient, dataset: Dataset, attachments: list[Attachment] +) -> None: dataset.attachments = attachments finalized = client.upload_new_dataset_now(dataset) + assert finalized.pid is not None uploaded = client.attachments[finalized.pid] assert len(uploaded) == len(attachments) @@ -233,9 +260,11 @@ def test_upload_creates_attachments(client, dataset, attachments): assert uploaded[1].thumbnail == attachments[1].thumbnail -def test_upload_uploads_files_to_source_folder(client, dataset_with_files): +def test_upload_uploads_files_to_source_folder( + client: FakeClient, dataset_with_files: Dataset +) -> None: finalized = client.upload_new_dataset_now(dataset_with_files) - source_folder = client.file_transfer.source_folder_for(finalized) + source_folder = client.file_transfer.source_folder_for(finalized) # type: ignore[union-attr] assert ( get_file_transfer(client).files[source_folder / "file.nxs"] @@ -247,15 +276,20 @@ def test_upload_uploads_files_to_source_folder(client, dataset_with_files): ) -def test_upload_does_not_create_dataset_if_file_upload_fails(dataset_with_files, fs): +def test_upload_does_not_create_dataset_if_file_upload_fails( + dataset_with_files: Dataset, fs: FakeFilesystem +) -> None: class RaisingUpload(FakeFileTransfer): source_dir = "/" - def upload_files(self, *files): + def upload_files(self, *files: File) -> list[File]: raise RuntimeError("Fake upload failure") + def revert_upload(self, *files: File) -> None: + raise RuntimeError("Not allowed to revert uploads") + @contextmanager - def connect_for_upload(self, pid): + def connect_for_upload(self, pid: object) -> Iterator[UploadConnection]: # type: ignore[override] yield self client = FakeClient(file_transfer=RaisingUpload(fs=fs)) @@ -268,7 +302,9 @@ def connect_for_upload(self, pid): assert not client.attachments -def test_upload_cleans_up_files_if_dataset_ingestion_fails(dataset_with_files, fs): +def test_upload_cleans_up_files_if_dataset_ingestion_fails( + dataset_with_files: Dataset, fs: FakeFilesystem +) -> None: client = FakeClient( disable={"create_dataset_model": ScicatCommError("Ingestion failed")}, file_transfer=FakeFileTransfer(fs=fs), @@ -279,7 +315,9 @@ def test_upload_cleans_up_files_if_dataset_ingestion_fails(dataset_with_files, f assert not get_file_transfer(client).files -def test_upload_does_not_create_dataset_if_validation_fails(dataset_with_files, fs): +def test_upload_does_not_create_dataset_if_validation_fails( + dataset_with_files: Dataset, fs: FakeFilesystem +) -> None: client = FakeClient( disable={"validate_dataset_model": ValueError("Validation failed")}, file_transfer=FakeFileTransfer(fs=fs), @@ -293,7 +331,9 @@ def test_upload_does_not_create_dataset_if_validation_fails(dataset_with_files, assert not get_file_transfer(client).files -def test_failed_datablock_upload_does_not_revert(dataset_with_files, fs): +def test_failed_datablock_upload_does_not_revert( + dataset_with_files: Dataset, fs: FakeFilesystem +) -> None: client = FakeClient( disable={"create_orig_datablock": ScicatCommError("Ingestion failed")}, file_transfer=FakeFileTransfer(fs=fs), @@ -311,8 +351,8 @@ def test_failed_datablock_upload_does_not_revert(dataset_with_files, fs): def test_upload_does_not_create_attachments_if_dataset_ingestion_fails( - attachments, dataset -): + attachments: list[Attachment], dataset: Dataset +) -> None: dataset.attachments = attachments client = FakeClient( disable={"create_dataset_model": ScicatCommError("Ingestion failed")}, @@ -325,8 +365,8 @@ def test_upload_does_not_create_attachments_if_dataset_ingestion_fails( def test_upload_does_not_create_attachments_if_datablock_ingestion_fails( - attachments, dataset_with_files, fs -): + attachments: list[Attachment], dataset_with_files: Dataset, fs: FakeFilesystem +) -> None: dataset_with_files.attachments = attachments client = FakeClient( disable={"create_orig_datablock": ScicatCommError("Ingestion failed")}, @@ -338,7 +378,9 @@ def test_upload_does_not_create_attachments_if_datablock_ingestion_fails( assert not client.attachments -def test_failed_attachment_upload_does_not_revert(attachments, dataset_with_files, fs): +def test_failed_attachment_upload_does_not_revert( + attachments: list[Attachment], dataset_with_files: Dataset, fs: FakeFilesystem +) -> None: dataset_with_files.attachments = attachments client = FakeClient( disable={"create_attachment_for_dataset": ScicatCommError("Ingestion failed")}, diff --git a/tests/util/formatter_test.py b/tests/util/formatter_test.py index 5400ab72..fc7cc08f 100644 --- a/tests/util/formatter_test.py +++ b/tests/util/formatter_test.py @@ -7,7 +7,7 @@ from scitacean.util.formatter import DatasetPathFormatter -def test_dataset_formatter_uses_dataset_fields(): +def test_dataset_formatter_uses_dataset_fields() -> None: dset = Dataset(type="raw", owner="magrat") fmt = "{type} {owner}" formatted = DatasetPathFormatter().format(fmt, dset=dset) @@ -15,14 +15,14 @@ def test_dataset_formatter_uses_dataset_fields(): assert formatted == expected -def test_dataset_formatter_can_access_attrs_of_fields(): +def test_dataset_formatter_can_access_attrs_of_fields() -> None: dset = Dataset(type="raw") dset._pid = PID.parse("prefix/actual-id") # type: ignore[assignment] formatted = DatasetPathFormatter().format("{pid.pid}", dset=dset) assert formatted == "actual-id" -def test_dataset_formatter_supports_special_uid(): +def test_dataset_formatter_supports_special_uid() -> None: dset = Dataset(type="raw", owner="magrat") fmt = "id: {uid}" @@ -35,7 +35,7 @@ def test_dataset_formatter_supports_special_uid(): assert formatted == expected -def test_dataset_formatter_supports_dset_and_uid(): +def test_dataset_formatter_supports_dset_and_uid() -> None: dset = Dataset(type="raw", owner="ogg") fmt = "{uid}({owner})" @@ -44,20 +44,20 @@ def test_dataset_formatter_supports_dset_and_uid(): assert formatted == expected -def test_dataset_formatter_escapes_characters(): +def test_dataset_formatter_escapes_characters() -> None: dset = Dataset(type="raw", owner="Harald Blåtand", owner_email="harald@viking.dk") formatted = DatasetPathFormatter().format("{owner}-{owner_email}", dset=dset) expected = "Harald Bl_xe5tand-harald_viking.dk" assert formatted == expected -def test_dataset_formatter_preserves_path_separators(): +def test_dataset_formatter_preserves_path_separators() -> None: dset = Dataset(type="raw", owner="Nanny Ogg") formatted = DatasetPathFormatter().format("{type}/{owner}.data", dset=dset) assert formatted == "raw/Nanny Ogg.data" -def test_dataset_formatter_does_not_allow_none(): +def test_dataset_formatter_does_not_allow_none() -> None: dset = Dataset(type="raw", owner=None) with pytest.raises(ValueError, match="format path"): DatasetPathFormatter().format("{owner}", dset=dset)