ContextManagers.jl

ContextManagersModule

ContextManagers

ContextManagers.jl provides composable resource management interface for Julia.

using ContextManagers: @with, opentemp, onexit

lck = ReentrantLock()
ch = Channel()

@with(
    lck,
    (path, io) = opentemp(),
    onexit(lock(ch)) do _
        unlock(ch)
        println("Successfully unlocked!")
    end,
) do
    println(io, "Hello World")
end

# output
Successfully unlocked!

See also:

ContextManagers.@withMacro
@with(
    resource₁ = source₁,
    resource₂ = source₂,
    ...
    resourceₙ = sourceₙ,
) do
    use(resource₁, resource₂, ..., resourceₙ)
end

Open resources, run the do block body, and cleanup the resources.

ContextManagers.withFunction
ContextManagers.with(sources...) do resources...
    use(resources...)
end

Open resources, run the do block body, and cleanup the resources.

Tools

ContextManagers.opentempFunction
ContextManagers.opentemp([parent]; kwargs...) -> tf

Create and open a temporary file. The path and the IO object can be accessed through the properties .path and .io of the returned object tf respectively. The positional and named arguments are passed to mktemp. The file is automatically removed and the IO object is automatically closed when used with @with or with.

ContextManagers.opentempdirFunction
ContextManagers.opentempdir(parent=tempdir(); kwargs...) -> td

Create aa temporary directory. The path can be accessed through the property .path of the returned object td. When this is used with @with or with, td is unwrapped to a path automatically. For example, in the do block of @with(paath = opentempdir()) do; ...; end, a string path is available.

ContextManagers.SharedResourceType
ContextManagers.SharedResource(source)

Create a sharable resource that is closed once the "last" context using the shared resource is closed.

Warning

The source itself should produce "thread-safe" API if this is shared between @spawned task.

SharedResource supports the following pattern

using Base.Threads: @spawn
using ContextManagers: @with, SharedResource

output = Int[]
@sync begin
    ch = Channel()
    @with(handle = SharedResource(ch)) do       # (1) create a `handle`
        for x in 1:3
            context = open(handle)              # (2) refcount++
            @spawn begin
                @with(ch = context) do          # (3) obtain the `ch` value
                    put!(ch, x)
                end                             # (4a) refcount--; maybe cleanup
            end
        end
    end                                         # (4b) refcount--; maybe cleanup
    append!(output, ch)
end

sort!(output)

# output
3-element Vector{Int64}:
 1
 2
 3

Extended help

(1) The underling source is entered when SharedResource is entered. A handle to this shared resource can be obtained by entering the context of the SharedResource.

(2) A context for obtaining the value of the context of the original source can be obtained by open the handle. When sharing the resource across tasks, it typically has to be done before spawning the task.

(3) The value from the original source can be obtained by entering the context.

(4) The last shared context exitting the @with block ends the original context. In the above pattern, the context of the source may exit at the end (4b) of the outer @with if xs is empty or all the child tasks exit first.

Notes

This is inspired by Nathaniel J. Smith's comment: https://github.com/python-trio/trio/issues/719#issuecomment-462119589

ContextManagers.IgnoreErrorType
ContextManagers.IgnoreError()

Ignore an error (if any).

Example

julia> using ContextManagers: @with, IgnoreError

julia> @with(
           IgnoreError(),
       ) do
           error("error")
       end

Extended help

Note that IgnoreError only ignores the error from the "inner" code. That is to say, in the following code, the error from the doblock and the context managers c and d are ignored but not the error from the context managers a and b.

@with(
    a,
    b,
    IgnoreError(),
    c,
    d,
) do
    doblock
end
ContextManagers.onexitFunction
ContextManagers.onexit(cleanup, x)
ContextManagers.onexit(cleanup)

Create a context manager that runs cleanup(x) upon exit.

Example

julia> using ContextManagers: @with, onexit

julia> @with(
           onexit(111) do x
               @show x
           end,
       ) do
       end;
x = 111
ContextManagers.onfailFunction
ContextManagers.onfail(cleanup, x)
ContextManagers.onfail(cleanup)

Create a context manager that runs cleanup(x) upon unsuccessful exit.

Example

julia> using ContextManagers: @with, IgnoreError, onfail

julia> @with(
           onfail(111) do x
               @show x
           end,
       ) do
       end;  # prints nothing

julia> @with(
           IgnoreError(),
           onfail(111) do x
               @show x
           end,
       ) do
           error("error")
       end;
x = 111

Interface

ContextManagers.maybeenterFunction
ContextManagers.maybeenter(source) -> context or nothing

Start a context managing the resource. Or return nothing when source does not implement the context manager interface.

Default implementation returns nothing; i.e., no context manager interface.

ContextManagers.valueFunction
ContextManagers.value(context) -> resource

Default implementation is a pass-through (identity) function.

ContextManagers.exitFunction
ContextManagers.exit(context)
ContextManagers.exit(context, err) -> nothing or ContextManagers.Handled()

Cleanup the context. Default implementation is close.

Roughly speaking,

@with(resource = f(args...)) do
    use(resource)
end

is equivalent to

context = something(ContextManagers.maybeenter(source))
try
    resource = ContextManagers.value(context)

    use(resource)

finally
    ContextManagers.exit(context)
end

In the two-argument version ContextManagers.exit(context, err) (where err is nothing or an Exception), the error can be suppressed by returning ContextManagers.Handled().