From 3b4957d03262b8cd972edcccc97cadf87a4111f2 Mon Sep 17 00:00:00 2001 From: Tom Close Date: Mon, 16 Sep 2024 12:38:52 +1000 Subject: [PATCH] added load, save and new methods to core.FileSet --- .pre-commit-config.yaml | 2 +- .../extras/application/serialization.py | 2 +- extras/fileformats/extras/image/converters.py | 4 +- extras/fileformats/extras/image/readwrite.py | 8 ++-- fileformats/application/serialization.py | 21 +-------- fileformats/core/extras.py | 6 ++- fileformats/core/fileset.py | 46 +++++++++++++++++++ fileformats/core/mixin.py | 14 ++++-- fileformats/image/raster.py | 19 +------- 9 files changed, 71 insertions(+), 51 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a24b14..260549c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,4 +37,4 @@ repos: --non-interactive, ] exclude: tests - additional_dependencies: [pytest, attrs] + additional_dependencies: [pytest, attrs, imageio] diff --git a/extras/fileformats/extras/application/serialization.py b/extras/fileformats/extras/application/serialization.py index 0fdb189..e63721e 100644 --- a/extras/fileformats/extras/application/serialization.py +++ b/extras/fileformats/extras/application/serialization.py @@ -24,7 +24,7 @@ def convert_data_serialization( output_path = out_dir / ( in_file.fspath.stem + (output_format.ext if output_format.ext else "") ) - return output_format.save_new(output_path, dct) + return output_format.new(output_path, dct) @extra_implementation(DataSerialization.load) diff --git a/extras/fileformats/extras/image/converters.py b/extras/fileformats/extras/image/converters.py index db01286..f4aba56 100644 --- a/extras/fileformats/extras/image/converters.py +++ b/extras/fileformats/extras/image/converters.py @@ -19,10 +19,10 @@ def convert_image( output_format: ty.Type[RasterImage], out_dir: ty.Optional[Path] = None, ) -> RasterImage: - data_array = in_file.read_data() + data_array = in_file.load() if out_dir is None: out_dir = Path(tempfile.mkdtemp()) output_path = out_dir / ( in_file.fspath.stem + (output_format.ext if output_format.ext else "") ) - return output_format.save_new(output_path, data_array) + return output_format.new(output_path, data_array) diff --git a/extras/fileformats/extras/image/readwrite.py b/extras/fileformats/extras/image/readwrite.py index 2592e38..5f67552 100644 --- a/extras/fileformats/extras/image/readwrite.py +++ b/extras/fileformats/extras/image/readwrite.py @@ -3,11 +3,11 @@ from fileformats.image.raster import RasterImage, DataArrayType -@extra_implementation(RasterImage.read_data) +@extra_implementation(RasterImage.load) def read_raster_data(image: RasterImage) -> DataArrayType: return imageio.imread(image.fspath) # type: ignore -@extra_implementation(RasterImage.write_data) -def write_raster_data(image: RasterImage, data_array: DataArrayType) -> None: - imageio.imwrite(image.fspath, data_array) +@extra_implementation(RasterImage.save) +def write_raster_data(image: RasterImage, data: DataArrayType) -> None: + imageio.imwrite(image.fspath, data) diff --git a/fileformats/application/serialization.py b/fileformats/application/serialization.py index a454aea..ad9ab29 100644 --- a/fileformats/application/serialization.py +++ b/fileformats/application/serialization.py @@ -1,8 +1,8 @@ import json import typing as ty -from fileformats.core.typing import Self, TypeAlias +from fileformats.core.typing import TypeAlias from pathlib import Path -from fileformats.core import extra, DataType, FileSet, extra_implementation +from fileformats.core import DataType, FileSet, extra_implementation from fileformats.core.mixin import WithClassifiers from ..generic import File from fileformats.core.exceptions import FormatMismatchError @@ -44,23 +44,6 @@ class DataSerialization(WithClassifiers, File): iana_mime: ty.Optional[str] = None - @extra - def load(self) -> LoadedSerialization: - """Load the contents of the file into a dictionary""" - raise NotImplementedError - - @extra - def save(self, data: LoadedSerialization) -> None: - """Serialise a dictionary to a new file""" - raise NotImplementedError - - @classmethod - def save_new(cls, fspath: ty.Union[str, Path], data: LoadedSerialization) -> Self: - # We have to use a mock object as the data file hasn't been written yet - mock = cls.mock(fspath) - mock.save(data) - return cls(fspath) - class Xml(DataSerialization): ext: ty.Optional[str] = ".xml" diff --git a/fileformats/core/extras.py b/fileformats/core/extras.py index 14bb7df..cfd9b59 100644 --- a/fileformats/core/extras.py +++ b/fileformats/core/extras.py @@ -78,7 +78,11 @@ def decorator(implementation: ExtraImplementation) -> ExtraImplementation: def type_match(a: ty.Union[str, type], b: ty.Union[str, type]) -> bool: return ( - a == b or inspect.isclass(a) and inspect.isclass(b) and issubclass(b, a) + a is ty.Any # type: ignore[comparison-overlap] + or a == b + or inspect.isclass(a) + and inspect.isclass(b) + and issubclass(b, a) ) mhas_kwargs = msig_args and msig_args[-1].kind == inspect.Parameter.VAR_KEYWORD diff --git a/fileformats/core/fileset.py b/fileformats/core/fileset.py index 3b666e6..179d16d 100644 --- a/fileformats/core/fileset.py +++ b/fileformats/core/fileset.py @@ -178,6 +178,52 @@ def __hash__(self) -> int: def __repr__(self) -> str: return f"{self.type_name}('" + "', '".join(str(p) for p in self.fspaths) + "')" + @extra + def load(self) -> ty.Any: + """Load the contents of the file into an object of type that make sense for the + datat type + + Returns + ------- + Any + the data loaded from the file in an type to the format + """ + + @extra + def save(self, data: ty.Any) -> None: + """Load new contents from a format-specific object + + Parameters + ---------- + data: Any + the data to be saved to the file in a type that matches the one loaded by + the `load` method + """ + + @classmethod + def new(cls, fspath: ty.Union[str, Path], data: ty.Any) -> Self: + """Create a new file-set object with the given data saved to the file + + Parameters + ---------- + fspath: str | Path + the file-system path to save the data to. Additional paths should be + able to be inferred from this path + data: Any + the data to be saved to the file in a type that matches the one loaded by + the `load` method + + Returns + ------- + FileSet + a new file-set object with the given data saved to the file + """ + # We have to use a mock object as the data file hasn't been written yet so can't + # be validated + mock = cls.mock(fspath) + mock.save(data) + return cls(fspath) + @property def parent(self) -> Path: "A common parent directory for all the top-level paths in the file-set" diff --git a/fileformats/core/mixin.py b/fileformats/core/mixin.py index 9d87a58..4e4802f 100644 --- a/fileformats/core/mixin.py +++ b/fileformats/core/mixin.py @@ -186,7 +186,7 @@ def header(self) -> "fileformats.core.FileSet": def read_metadata( self, selected_keys: ty.Optional[ty.Collection[str]] = None ) -> ty.Mapping[str, ty.Any]: - header: ty.Dict[str, ty.Any] = self.header.load() # type: ignore[attr-defined] + header: ty.Dict[str, ty.Any] = self.header.load() if selected_keys: header = {k: v for k, v in header.items() if k in selected_keys} return header @@ -227,12 +227,16 @@ def read_metadata( metadata: ty.Dict[str, ty.Any] = dict(self.primary_type.read_metadata(self, selected_keys=selected_keys)) # type: ignore[arg-type] for side_car in self.side_cars: try: - side_car_metadata: ty.Dict[str, ty.Any] = side_car.load() # type: ignore[attr-defined] + side_car_metadata: ty.Dict[str, ty.Any] = side_car.load() except AttributeError: continue - else: - side_car_class_name: str = to_mime_format_name(type(side_car).__name__) - metadata[side_car_class_name] = side_car_metadata + if not isinstance(side_car_metadata, dict): + raise TypeError( + f"`load` method of side-car type {type(side_car)} must return a " + f"dictionary, not {type(side_car_metadata)!r}" + ) + side_car_class_name: str = to_mime_format_name(type(side_car).__name__) + metadata[side_car_class_name] = side_car_metadata return metadata @classproperty diff --git a/fileformats/image/raster.py b/fileformats/image/raster.py index 0e391ed..1d20c5f 100644 --- a/fileformats/image/raster.py +++ b/fileformats/image/raster.py @@ -1,8 +1,6 @@ -from pathlib import Path -from fileformats.core.typing import Self, TypeAlias +from fileformats.core.typing import TypeAlias import typing as ty from fileformats.core.mixin import WithMagicNumber -from fileformats.core import extra from fileformats.core.exceptions import FormatMismatchError from .base import Image @@ -21,21 +19,6 @@ class RasterImage(Image): pass binary = True - @extra - def read_data(self) -> DataArrayType: - ... - - @extra - def write_data(self, data_array: DataArrayType) -> None: - ... - - @classmethod - def save_new(cls, fspath: Path, data_array: DataArrayType) -> Self: - # We have to use a mock object as the data file hasn't been written yet - mock = cls.mock(fspath) - mock.write_data(data_array) - return cls(fspath) - class Bitmap(WithMagicNumber, RasterImage): ext = ".bmp"