Skip to content

Commit 6fcee87

Browse files
committed
added section on extra implementations
1 parent 04fa1a4 commit 6fcee87

File tree

1 file changed

+92
-20
lines changed

1 file changed

+92
-20
lines changed

docs/source/developer.rst

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ must also be defined in ``primary_type``
130130
Custom format patterns
131131
----------------------
132132

133-
While the standard mixin classes should cover 90% of all formats, in the wild-west of
134-
scientific data formats you might need to write custom validators. This is simply done
135-
by adding a new property to the class using the `@property` decorator.
133+
While the standard mixin classes should cover the large majority standard formats, in
134+
the wild-west of science data formats you are likely to need to design custom validators
135+
for your format. This is simply done by adding a new property to the class using the
136+
`@property` decorator.
136137

137138
Take for example the `GIS shapefile structure <https://www.earthdatascience.org/courses/earth-analytics/spatial-data-r/shapefile-structure/>`_,
138139
it is a file-set consisting of 3 to 6 files differentiated by their extensions. To
@@ -231,7 +232,7 @@ decorator. Take the ``fileformats.image.Tiff`` class
231232
magic_number_le = "49492A00"
232233
magic_number_be = "4D4D002A"
233234
234-
@mark.check
235+
@property
235236
def endianness(self):
236237
read_magic = self.read_contents(len(self.magic_number_le) // 2)
237238
if read_magic == bytes.fromhex(self.magic_number_le):
@@ -253,6 +254,80 @@ files and another one for little endian files. Therefore we can't just use the
253254
``fileformats.core.mark.check``.
254255

255256

257+
Extra methods
258+
-------------
259+
260+
FileFormats *Extras* enable the creation of hooks in `FileSet` classes using the `@extra`
261+
decorator that can be implemented in separate modules using the `@extra_implementation`
262+
decorator. The "extra methods" typically add additional functionality for accessing and
263+
maninpulating the data within the fileset, i.e. not required for format detection and
264+
validation, and should be implemented in a separate package if they have external
265+
dependencies to keep the main and extension packages dependency free. The
266+
standard place to put these extras-implementations is in the sister "extras" package
267+
named `fileformats-<yournamespace>-extras`, located in the `extras` directory in the
268+
extension package root (see `<https://github.com/ArcanaFramework/fileformats-extension-template>`__
269+
for further instructions). It is possible to implement extra methods in other modules,
270+
however, the extras package associated with formats namespace will be loaded by default
271+
when a hooked method is accessed.
272+
273+
Use the `@extra` decorator on a method in the to define an extras method,
274+
275+
.. code-block:: python
276+
from typing import Self
277+
278+
class MyFormat(File):
279+
280+
ext = ".my"
281+
282+
@extra
283+
def my_extra_method(self, index: int, scale: float, save_path: Path) -> Self:
284+
...
285+
286+
and then reference that method in the extras package using the `@extra_implementation`
287+
288+
.. code-block:: python
289+
290+
from some_external_package import load_my_format, save_my_format
291+
from fileformats.core import extra_implementation
292+
from fileformats.mypackage import MyFormat
293+
294+
@extra_implementation(MyFormat.my_extra_method)
295+
def my_extra_method(
296+
my_format: MyFormat, index: int scale: float, save_path: Path
297+
) -> MyFormat:
298+
data_array = load_my_format(my_format.fspath)
299+
data_array[:index] *= scale
300+
save_my_format(save_path, data_array)
301+
return MyFormat(save_path)
302+
303+
The first argument to the implementation functions is the instance the method
304+
is executed on, and the types of the remaining arguments and return need to match
305+
the hooked method exactly.
306+
307+
It is possible to provide multiple overloads for subclasses of the format that defines
308+
the hook. Like `functools.singledispacth` (which is used under the hood), the type of
309+
the first argument (not the type of the class the method is referenced from in the decorated)
310+
determines which of the overloaded methods is called
311+
312+
313+
.. code-block:: python
314+
315+
class MyFormatX(MyFormat):
316+
ext = ".myx"
317+
318+
@extra_implementation(MyFormat.my_extra_method)
319+
def my_extra_method(
320+
my_format: MyFormat, index: int scale: float, save_path: Path
321+
) -> MyFormat:
322+
...
323+
324+
@extra_implementation(MyFormat.my_extra_method)
325+
def my_extra_method(
326+
my_format: MyFormatX, index: int scale: float, save_path: Path
327+
) -> MyFormatX:
328+
...
329+
330+
256331
Implementing converters
257332
-----------------------
258333

@@ -261,11 +336,8 @@ Converters between two equivalent formats are defined using Pydra_ dataflow engi
261336
of Pydra_ tasks, function tasks, Python functions decorated by ``@pydra.mark.task``, and
262337
shell-command tasks, which wrap command-line tools in Python classes. To register a
263338
Pydra_ task as a converter between two file formats it needs to be decorated with the
264-
``@fileformats.core.mark.converter`` decorator. Note that converters that rely on
265-
any additional dependencies should not be implemented in your extension package, rather
266-
in a sister "extras" package named `fileformats-<yournamespace>-extras`,
267-
see the `extras template <https://github.com/ArcanaFramework/fileformats-extras-template>`__
268-
for further instructions.
339+
``@fileformats.core.converter`` decorator. Like the implementation of extra methods,
340+
converters should be implemented in the sister extras package.
269341

270342
Pydra uses type annotations to define the input and outputs of the tasks. It there is
271343
a input to the task named ``in_file``, and either a single anonymous output or an output
@@ -278,11 +350,11 @@ automatically. For example,
278350
from pathlib import Path
279351
import tempfile
280352
import pydra.mark
281-
import fileformats.core.mark
353+
from fileformats.core import converter
282354
from .mypackage import MyFormat, MyOtherFormat
283355
284356
285-
@fileformats.core.mark.converter
357+
@converter
286358
@pydra.mark.task
287359
def convert_my_format(in_file: MyFormat, conversion_argument: int = 2) -> MyOtherFormat:
288360
data = in_file.load()
@@ -309,15 +381,15 @@ to do a generic conversion between all image types,
309381
import tempfile
310382
import pydra.mark
311383
import pydra.engine.specs
312-
from fileformats.core import mark
384+
from fileformats.core import converter
313385
from .raster import RasterImage, Bitmap, Gif, Jpeg, Png, Tiff
314386
315387
316-
@mark.converter(target_format=Bitmap, output_format=Bitmap)
317-
@mark.converter(target_format=Gif, output_format=Gif)
318-
@mark.converter(target_format=Jpeg, output_format=Jpeg)
319-
@mark.converter(target_format=Png, output_format=Png)
320-
@mark.converter(target_format=Tiff, output_format=Tiff)
388+
@converter(target_format=Bitmap, output_format=Bitmap)
389+
@converter(target_format=Gif, output_format=Gif)
390+
@converter(target_format=Jpeg, output_format=Jpeg)
391+
@converter(target_format=Png, output_format=Png)
392+
@converter(target_format=Tiff, output_format=Tiff)
321393
@pydra.mark.task
322394
@pydra.mark.annotate({"return": {"out_file": RasterImage}})
323395
def convert_image(in_file: RasterImage, output_format: type, out_dir: ty.Optional[Path] = None):
@@ -368,11 +440,11 @@ such as in the ``mrconvert`` converter in the ``fileformats-medimage`` package.
368440

369441
.. code-block:: python
370442
371-
@mark.converter(source_format=MedicalImage, target_format=Analyze, out_ext=Analyze.ext)
372-
@mark.converter(
443+
@converter(source_format=MedicalImage, target_format=Analyze, out_ext=Analyze.ext)
444+
@converter(
373445
source_format=MedicalImage, target_format=MrtrixImage, out_ext=MrtrixImage.ext
374446
)
375-
@mark.converter(
447+
@converter(
376448
source_format=MedicalImage,
377449
target_format=MrtrixImageHeader,
378450
out_ext=MrtrixImageHeader.ext,

0 commit comments

Comments
 (0)