FileIO.jl

repository·master·Indexed 42 days ago

https://github.com/juliaio/fileio.jl

A framework for Julia that provides a common interface for detecting file formats and dispatching to appropriate readers and writers using `load` and `save` functions. It supports high-level formatted file I/O, streaming I/O via `loadstreaming` and `savestreaming` for large files, and a `query` function to inspect file formats without loading full content. The framework allows developers to register new formats using `add_format` and implement custom loaders and savers.

Tokens
4K
Snippets
15
Records
21
Agent score
52%

What's inside FileIO.jl

  1. Overview of FileIO.jl

    master
    FileIO.jl provides a unified framework for detecting file formats and automatically dispatching to the correct readers or writers. It abstracts away the low-level details of Julia's read and write functions by providing high-level, format-aware alternatives.
  2. Use load and save for formatted files

    master

    Instead of using Julia's low-level read and write functions, use load and save to handle formatted files. FileIO detects the appropriate reader or writer based on the file extension or provided information.

    Note: To prevent name conflicts, other packages providing format support via load and save are encouraged to register with the FileIO framework.

    # High-level usage pattern
    data = load("filename.ext")
    save("filename.ext", data)
  3. How FileIO discovers and calls loaders/savers

    master

    FileIO uses module qualification to handle files. When you register a format (e.g., via add_format), FileIO identifies the package associated with that format and calls its internal load or save functions directly by their module name (e.g., MyFileFormat.load(file)).

    Crucial Rule: Do not extend FileIO.load or FileIO.save. Instead, define these functions as private methods within your own module. This prevents naming conflicts and ensures that if multiple packages support the same format, the one loaded last in the user's session doesn't accidentally overwrite others, making the system brittle.

  4. Naming conventions for loaders and savers

    master

    By default, FileIO looks for load and save functions in your module. If your module already uses these names for other purposes, you can use an alternative naming convention.

    Note: You must be consistent. You cannot mix styles within a single module; you must either use load/save for everything or fileio_load/fileio_save for everything.

  5. Understanding the World Age Issue in FileIO

    master

    FileIO uses lazy loading to improve startup performance. Instead of loading all IO backends (like PNGFiles.jl or ImageIO.jl) when you run using FileIO, it only loads the specific backend required when you actually call load() or save().

    While this makes using FileIO very fast, it can trigger a world age issue if you call load inside a function. This happens because the backend packages (and their dependencies, like IndirectArrays) are loaded into a 'newer' Julia world than the one where your function was compiled. When your function tries to interact with the newly loaded types, Julia may throw a MethodError stating that the applicable method is too new.

    julia> using FileIO
    
    julia> f() = size(load("indexed_image.png"))
    
    # This might fail with a MethodError if the backend
    # packages weren't loaded when f() was compiled:
    # ERROR: MethodError: ... The applicable method may be too new ...
  6. Read and save files with load and save

    master

    FileIO provides high-level load and save functions to detect file formats and dispatch to the appropriate readers or writers. If a format is registered, you can read data directly from a filename or a URI.

    To save data, use save(filename, obj) or use the piping operator: obj |> save(filename).

    using FileIO
    # Reading a file
    obj = load("filename.ext")
    
    # Reading from a URI
    using HTTP
    img = load(HTTP.URI("https://github.com/JuliaLang/julia-logo-graphics/raw/master/images/julia-logo-color.png"));
    
    # Saving a file
    save("filename.ext", obj)
    
    # Saving using piping
    obj |> save("filename.ext")
  7. Implement streaming I/O with `loadstreaming` and `savestreaming`

    master

    Streaming I/O allows users to obtain a decoded stream object that they can interact with using standard read or write calls.

    Implementation Steps:

    1. Define a Reader/Writer type: Create a struct (e.g., WAVReader) that wraps the underlying IO.
    2. Implement Base.read or Base.write: Define how to process data through your wrapper.
    3. Implement Base.close: This is critical for resource management.
      • If the user provided a File, your close method must close the underlying stream.
      • If the user provided a Stream, your close method must only perform internal cleanup and must not close the stream.
    4. Implement loadstreaming and savestreaming: Provide implementations for both File and Stream types.

    Shortcut: Implementing load/save via streaming

    If you have already implemented loadstreaming and savestreaming, you can implement the standard load and save methods easily by using the Formatted abstract supertype (which covers both File and Stream).

    struct WAVReader
        io::IO
        ownstream::Bool
    end
    
    function Base.read(reader::WAVReader, frames::Int)
        # read and decode audio samples from reader.io
    end
    
    function Base.close(reader::WAVReader)
        # do whatever cleanup the reader needs
        reader.ownstream && close(reader.io)
    end
    
    # Implementations for FileIO to call
    loadstreaming(f::File{format"WAV"}) = WAVReader(open(f), true)
    loadstreaming(s::Stream{format"WAV"}) = WAVReader(s, false)
    
    # Shortcut to implement standard load/save using the streaming logic
    function save(q::Formatted{format"WAV"}, data, args...; kwargs...)
        savestreaming(q, args...; kwargs...) do stream
            write(stream, data)
        end
    end
    
    function load(q::Formatted{format"WAV"}, args...; kwargs...)
        loadstreaming(q, args...; kwargs...) do stream
            read(stream)
        end
    end
  8. Implement all-at-once I/O with `load` and `save`

    master

    To implement standard file loading and saving, you should provide methods for both File and Stream types.

    Implementation Requirements:

    • load(f::File{format"FORMAT"}): Should open the file, handle magic bytes (e.g., by skipping them), and then call the load(s::Stream{format"FORMAT"}) method. This method must close the stream it opens (using do blocks is recommended).
    • load(s::Stream{format"FORMAT"}; keywords...): Should perform the actual reading from the stream. The stream is assumed to be positioned after the magic bytes. This method must not close the stream.
    • save(f::File{format"FORMAT"}, data): Should open the file for writing, write the necessary magic bytes, and then perform the save. This method must close the stream it opens.
    • save(s::Stream{format"FORMAT"}; keywords...): Should perform the actual writing to the stream. This method must not close the stream.
    module MyFileFormat
    
    using FileIO
    
    # Private load function for Files
    function load(f::File{format"PNG"})
        open(f) do s
            skipmagic(s)  # skip over the magic bytes
            load(s)       # call the Stream-based load
        end
    end
    
    # Private load function for Streams
    function load(s::Stream{format"PNG"}; keywords...)
        # s is already positioned after the magic bytes
        chunklength = read(s, UInt32)
        # ... read the rest
    end
    
    # Private save function for Files
    function save(f::File{format"PNG"}, data)
        open(f, "w") do s
            write(s, magic(format"PNG"))
            # ... write the rest
        end
    end
    
    end # module MyFileFormat
  9. Register a new file format with add_format

    master

    To register a new file format in FileIO, you must add a call to add_format to FileIO's registry.

    Workflow for contributors:

    1. Use pkg> dev FileIO to develop locally.
    2. Experiment with add_format locally to ensure the format is correctly identified and handled.
    3. Submit a pull request once verified.

    Note on formats without magic bytes: If a format uses common extensions (like .out) but lacks unique magic bytes, it cannot be registered with FileIO. In these cases, you should provide I/O directly within your own package using module-qualified names (e.g., MyPkg.load) to avoid conflicts with FileIO's load and save.

    add_format(fmt, magic, extension, libraries...)
  10. Access FileIO API help in the REPL

    master

    You can explore the FileIO API directly from the Julia REPL using the help mode.

    • Use ?FileIO to get a general overview of the package API.
    • Use ?function_name (e.g., ?add_format) to see documentation for specific functions.
  11. Fix the World Age Issue by Eagerly Loading Dependencies

    master

    The most efficient way to avoid world age issues without sacrificing performance is to eagerly load the required backend or dependency packages. If you know your code will interact with specific types (e.g., IndirectArray), include that package in your using statements.

    For package developers, this means including the necessary IO dependencies in your module's using block to ensure they are present in the user's current world age.

    # Instead of just using FileIO, include the dependency causing the issue
    using FileIO, IndirectArrays
    
    f(file) = length(load(file))