@@ -130,9 +130,10 @@ must also be defined in ``primary_type``
130130Custom 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
137138Take for example the `GIS shapefile structure <https://www.earthdatascience.org/courses/earth-analytics/spatial-data-r/shapefile-structure/ >`_,
138139it 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
261336of Pydra _ tasks, function tasks, Python functions decorated by ``@pydra.mark.task ``, and
262337shell-command tasks, which wrap command-line tools in Python classes. To register a
263338Pydra _ 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
270342Pydra uses type annotations to define the input and outputs of the tasks. It there is
271343a 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