From 969ea2709447e87f0e6a8afb75efb3f12428c19f Mon Sep 17 00:00:00 2001 From: Peter Van Dyken Date: Wed, 20 Dec 2023 11:10:48 -0500 Subject: [PATCH] Test write_config function --- snakebids/io/config.py | 6 ++- snakebids/tests/test_yaml.py | 61 +++++++++++++++++++++++++++- typings/pyfakefs/fake_filesystem.pyi | 8 ++-- typings/pyfakefs/helpers.pyi | 18 ++++---- 4 files changed, 78 insertions(+), 15 deletions(-) diff --git a/snakebids/io/config.py b/snakebids/io/config.py index d5c6bb41..019261bd 100644 --- a/snakebids/io/config.py +++ b/snakebids/io/config.py @@ -1,4 +1,5 @@ from __future__ import annotations +import errno import json from pathlib import Path @@ -28,8 +29,9 @@ def write_config( """ config_file = Path(config_file) if (config_file.exists()) and not force_overwrite: - err = FileExistsError(f"'{config_file}' already exists") - err.filename = str(config_file) + err = FileExistsError( + errno.EEXIST, f"'{config_file}' already exists", str(config_file) + ) raise err config_file.parent.mkdir(exist_ok=True) diff --git a/snakebids/tests/test_yaml.py b/snakebids/tests/test_yaml.py index 67efa012..da177398 100644 --- a/snakebids/tests/test_yaml.py +++ b/snakebids/tests/test_yaml.py @@ -1,16 +1,73 @@ +from __future__ import annotations + from io import StringIO from pathlib import Path +import pytest from hypothesis import given from hypothesis import strategies as st +from pyfakefs.fake_filesystem import FakeFilesystem +from pytest_mock import MockerFixture +from pytest_mock.plugin import MockType -from snakebids.io.yaml import get_yaml_io +import snakebids.io.config as configio +import snakebids.io.yaml as yamlio +from snakebids.tests.helpers import allow_function_scoped @given(path=st.text(min_size=1).map(Path)) def test_paths_formatted_as_str(path: Path): string = StringIO() - yaml = get_yaml_io() + yaml = yamlio.get_yaml_io() yaml.dump({"key": path}, string) string.seek(0, 0) assert yaml.load(string)["key"] == str(path) + + +class TestWriteConfig: + def io_mocks(self, mocker: MockerFixture) -> dict[str, MockType]: + return { + "mopen": mocker.patch.object(configio, "open", mocker.mock_open()), + "jsondump": mocker.patch.object(configio.json, "dump"), + "mkdir": mocker.patch.object(configio.Path, "mkdir"), + "yamldump": mocker.patch.object(yamlio.YAML, "dump"), + } + + @allow_function_scoped + @given( + ext=st.sampled_from([".json", ".yaml", ".yml"]), + path=st.text().map(Path).filter(lambda p: p != Path() and not p.exists()), + ) + def test_writes_correct_format(self, ext: str, path: Path, mocker: MockerFixture): + mocker.stopall() + mocks = self.io_mocks(mocker) + path = path.with_suffix(ext) + configio.write_config(path, {}) + if ext == ".json": + mocks["mopen"].assert_called_once_with(path, "w", encoding="utf-8") + mocks["jsondump"].assert_called_once() + mocks["yamldump"].assert_not_called() + else: + mocks["mopen"].assert_not_called() + mocks["jsondump"].assert_not_called() + mocks["yamldump"].assert_called_once_with({}, path) + + @allow_function_scoped + @given(path=st.text().filter(lambda s: s not in {".", ""})) + def test_doesnt_overwrite_file(self, path: str, fakefs: FakeFilesystem): + fakefs.reset() + fakefs.create_file(path) + with pytest.raises(FileExistsError, match="already exists"): + configio.write_config(path, {}) + + @allow_function_scoped + @given(path=st.text().filter(lambda s: s not in {".", ""})) + def test_overwrites_file_if_forced( + self, path: str, fakefs: FakeFilesystem, mocker: MockerFixture + ): + mocker.stopall() + fakefs.reset() + fakefs.create_file(path) + mocks = self.io_mocks(mocker) + configio.write_config(path, {}, force_overwrite=True) + assert mocks["jsondump"].call_count ^ mocks["yamldump"].call_count diff --git a/typings/pyfakefs/fake_filesystem.pyi b/typings/pyfakefs/fake_filesystem.pyi index 162b3a0e..037a020f 100644 --- a/typings/pyfakefs/fake_filesystem.pyi +++ b/typings/pyfakefs/fake_filesystem.pyi @@ -185,7 +185,7 @@ class FakeFilesystem: """Set the simulated type of operating system underlying the fake file system.""" ... - def reset(self, total_size: int | None = ...): # -> None: + def reset(self, total_size: int | None = ...) -> None: """Remove all file system contents and reset the root.""" ... def pause(self) -> None: @@ -352,7 +352,7 @@ class FakeFilesystem: times: tuple[int | float, int | float] | None = ..., *, ns: tuple[int, int] | None = ..., - follow_symlinks: bool = ... + follow_symlinks: bool = ..., ) -> None: """Change the access and modified times of a file. @@ -724,7 +724,7 @@ class FakeFilesystem: ... def create_file( self, - file_path: AnyPath, + file_path: AnyPath[AnyStr], st_mode: int = ..., contents: AnyString = ..., st_size: int | None = ..., @@ -732,7 +732,7 @@ class FakeFilesystem: apply_umask: bool = ..., encoding: str | None = ..., errors: str | None = ..., - side_effect: Callable | None = ..., + side_effect: Callable[[FakeFile], Any] | None = ..., ) -> FakeFile: """Create `file_path`, including all the parent directories along the way. diff --git a/typings/pyfakefs/helpers.pyi b/typings/pyfakefs/helpers.pyi index 8698da19..4f711301 100644 --- a/typings/pyfakefs/helpers.pyi +++ b/typings/pyfakefs/helpers.pyi @@ -4,13 +4,12 @@ This type stub file was generated by pyright. import io import os -import platform import sys from typing import Any, AnyStr, Optional, Union, overload """Helper classes use for fake file system implementation.""" AnyString = Union[str, bytes] -AnyPath = Union[AnyStr, "os.PathLike[str]", "os.PathLike[bytes]"] +AnyPath = Union[AnyStr, "os.PathLike[AnyStr]"] IS_PYPY = ... IS_WIN = ... IN_DOCKER = ... @@ -58,12 +57,14 @@ def is_int_type(val: Any) -> bool: def is_byte_string(val: Any) -> bool: """Return True if `val` is a bytes-like object, False for a unicode - string.""" + string. + """ ... def is_unicode_string(val: Any) -> bool: """Return True if `val` is a unicode string, False for a bytes-like - object.""" + object. + """ ... @overload @@ -73,12 +74,14 @@ def make_string_path(dir_name: os.PathLike) -> str: ... def make_string_path(dir_name: AnyPath) -> AnyStr: ... def to_string(path: Union[AnyStr, Union[str, bytes]]) -> str: """Return the string representation of a byte string using the preferred - encoding, or the string itself if path is a str.""" + encoding, or the string itself if path is a str. + """ ... def to_bytes(path: Union[AnyStr, Union[str, bytes]]) -> bytes: """Return the bytes representation of a string using the preferred - encoding, or the byte string itself if path is a byte string.""" + encoding, or the byte string itself if path is a byte string. + """ ... def join_strings(s1: AnyStr, s2: AnyStr) -> AnyStr: @@ -88,7 +91,8 @@ def join_strings(s1: AnyStr, s2: AnyStr) -> AnyStr: def real_encoding(encoding: Optional[str]) -> Optional[str]: """Since Python 3.10, the new function ``io.text_encoding`` returns "locale" as the encoding if None is defined. This will be handled - as no encoding in pyfakefs.""" + as no encoding in pyfakefs. + """ ... def now(): ...