Extending the AbstractBaseWriter
class
The AbstractBaseWriter
is designed to be extended, allowing you to create custom
writers tailored to your specific needs. This guide will walk you through the
steps to extend the class and implement your custom functionality.
Setting Up Your Writer
To create a custom writer, you need to extend the AbstractBaseWriter
and
implement the save
method. This method is the core of your writer, handling
how and where data is saved.
For a walkthrough of all key methods and features, see the Key Methods section below.
Steps to Set Up
-
Inherit from
AbstractBaseWriter
:
Create a new class and extendAbstractBaseWriter
with the appropriate type. If you are saving text data, useAbstractBaseWriter[str]
, for example. If you are saving image data, useAbstractBaseWriter[sitk.Image]
. -
Define the
save
Method:
Useresolve_path()
orpreview_path()
to generate file paths.
Implement the logic for saving data. -
Customize Behavior (Optional): Override any existing methods for specific behavior.
Add additional methods or properties to enhance functionality.
Simple Example
from pathlib import Path
from imgtools.io import AbstractBaseWriter
class MyCustomWriter(AbstractBaseWriter[str]):
def save(self, content: str, **kwargs) -> Path:
# Resolve the output file path
output_path = self.resolve_path(**kwargs)
# Write content to the file
with output_path.open(mode="w", encoding="utf-8") as f:
f.write(content)
# Log and track the save operation
self.add_to_index(output_path, **self.context)
return output_path
Implementing the save
Method
The save
method is the heart of your custom writer. It determines how data
is written to files and interacts with the core features of AbstractBaseWriter
.
Key Responsibilities of save
-
Path Resolution:
- Use
resolve_path()
to dynamically generate file paths based on the provided context and filename format. - You can optionally use
preview_path()
as well. - Ensure paths are validated to prevent overwriting or duplication.
- Use
-
Data Writing:
- Define how the content will be written to the resolved path.
- Use file-handling best practices to ensure reliability.
-
Logging and Tracking:
- Log each save operation for debugging or auditing purposes.
- Use
add_to_index()
to maintain a record of saved files and their associated context variables.
-
Return Value:
- Return the
Path
object representing the saved file. - This allows users to access the file path for further processing or verification.
- Return the
Example Implementation
Here’s a minimal implementation of the save
method for a custom writer.
from pathlib import Path
from mypackage.abstract_base_writer import AbstractBaseWriter
class MyCustomWriter(AbstractBaseWriter[str]):
def save(self, content: str, **kwargs) -> Path:
# Step 1: Resolve the output file path
# you can try-catch this in case set to "FAIL" mode
# or just let the error propagate
output_path = self.resolve_path(**kwargs) # resolve_path will always return the path
# OPTIONAL handling for "SKIP" modes
if output_path.exists():
# this will only be true if the file existence mode
# is set to SKIP
# - OVERWRITE will have already deleted the file
# - upto developer to choose to handle this if set to SKIP
pass
# Step 2: Write the content to the resolved path
with output_path.open(mode="w", encoding="utf-8") as f:
f.write(content)
# Step 3: Log and track the save operation
self.add_to_index(output_path, filepath_column="filepath", **kwargs)
# Step 4: ALWAYS Return the saved file path
return output_path
Key Methods
The AbstractBaseWriter
provides several utility methods that simplify file writing
and context management. These methods are designed to be flexible and reusable,
allowing you to focus on your custom implementation.
resolve_path
resolve_path(**kwargs: object) -> pathlib.Path
Generate a file path based on the filename format, subject ID, and additional parameters.
Meant to be used by developers when creating a new writer class
and used internally by the save
method.
What It Does:
- Dynamically generates a file path based on the provided context and filename format.
When to Use It:
- This method is meant to be used in the
save
method to determine the file’s target location, but can also be used by external code to generate paths. - It ensures you’re working with a valid path and can handle file existence scenarios.
- Only raises
FileExistsError
if the file already exists and the mode is set toFAIL
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
|
typing.Any
|
Parameters for resolving the filename and validating existence. |
{}
|
Returns:
Name | Type | Description |
---|---|---|
resolved_path |
pathlib.Path
|
The resolved path for the file. |
Source code in src/imgtools/io/writers/abstract_base_writer.py
preview_path
preview_path(
**kwargs: object,
) -> typing.Optional[pathlib.Path]
Pre-checking file existence and setting up the writer context.
Meant to be used by users to skip expensive computations if a file
already exists and you dont want to overwrite it.
Only difference between this and resolve_path is that this method
does not return the path if the file exists and the mode is set to
SKIP
.
This is because the .save()
method should be able to return
the path even if the file exists.
What It Does:
- Pre-checks the file path based on context without writing the file.
- Returns
None
if the file exists and the mode is set toSKIP
. - Raises a
FileExistsError
if the mode is set toFAIL
. - An added benefit of using
preview_path
is that it automatically caches the context variables for future use, andsave()
can be called without passing in the context variables again.
Examples:
Main idea here is to allow users to save computation if they choose to skip existing files.
i.e. if file exists and mode is SKIP
, we return
None
, so the user can skip the computation.
>>> if nifti_writer.preview_path(subject="math", name="test") is None:
>>> logger.info("File already exists. Skipping computation.")
>>> continue # could be `break` or `return` depending on the use case
if the mode is FAIL
, we raise an error if the file exists, so user
doesnt have to perform expensive computation only to fail when saving.
Useful Feature
The context is saved in the instance, so running
.save()
after this will use the same context, and user can optionally
update the context with new values passed to .save()
.
>>> if path := writer.preview_path(subject="math", name="test"):
>>> ... # do some expensive computation to generate the data
>>> writer.save(data)
.save()
automatically uses the context for subject
and name
we
passed to preview_path
Parameters:
Name | Type | Description | Default |
---|---|---|---|
|
typing.Any
|
Parameters for resolving the filename and validating existence. |
{}
|
Returns:
Type | Description |
---|---|
pathlib.Path | None
|
If the file exists and the mode is |
Source code in src/imgtools/io/writers/abstract_base_writer.py
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 |
|
clear_context
Clear the context for the writer.
Useful for resetting the context after using preview_path
or save
and want to make sure that the context is empty for new operations.
Source code in src/imgtools/io/writers/abstract_base_writer.py
add_to_index
add_to_index(
path: pathlib.Path,
include_all_context: bool = True,
filepath_column: str = "path",
replace_existing: bool = False,
**additional_context: object
) -> None
Add or update an entry in the shared CSV index file.
What It Does:
- Logs the file’s path and associated context variables to a shared CSV index file.
- Uses inter-process locking to avoid conflicts when multiple writers are active.
When to Use It:
- Use this method to maintain a centralized record of saved files for auditing or debugging.
Relevant Writer Parameters
-
The
index_filename
parameter allows you to specify a custom filename for the index file. By default, it will be named after theroot_directory
with_index.csv
appended. -
If the index file already exists in the root directory, it will overwrite it unless the
overwrite_index
parameter is set toFalse
. -
The
absolute_paths_in_index
parameter controls whether the paths in the index file are absolute or relative to theroot_directory
, withFalse
being the default.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
|
pathlib.Path
|
The file path being saved. |
required |
|
bool
|
If True, write existing context variables passed into writer and
the additional context to the CSV.
If False, determines only the context keys parsed from the
|
True
|
|
str
|
The name of the column to store the file path. Defaults to "path". |
'path'
|
|
bool
|
If True, checks if the file path already exists in the index and replaces it. |
False
|
|
typing.Any
|
Additional context information to include in the CSV passed in as keyword arguments. |
{}
|
Notes
When replace_existing
is set to True, the method will check if the
file path already exists in the index file using csv.Sniffer
and
replace the row if it does. If the file path does not exist in the
index file, it will add a new row with the file path and context
information.
Source code in src/imgtools/io/writers/abstract_base_writer.py
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 |
|
_generate_path
Helper for resolving paths with the given context.
Source code in src/imgtools/io/writers/abstract_base_writer.py
What It Does:
- A helper method for resolving file paths based on the current context and filename format.
- Automatically sanitizes filenames if
sanitize_filenames=True
.
When to Use It:
- Typically called internally by
resolve_path()
andpreview_path()
, which handle additional validation and error handling. - Can be called by your class methods to generate paths without the additional context checks.
Example:
custom_path = writer._generate_path(subject="math", name="example")
print(f"Generated path: {custom_path}")
By using these key methods effectively, you can customize your writer to handle a wide range of file-writing scenarios while maintaining clean and consistent logic.