text:
using Python, Test, Dates, Compat

mutable struct Struct1
    x :: String
    y :: Int
end

@testset "Python.jl" begin

    @testset "cpython" begin
    end

    @testset "eval" begin
        @pyg ```
        import sys, os, datetime, array, io, fractions, array, ctypes
        eq = lambda a, b: type(a) is type(b) and a == b
        class Foo:
            def __init__(self, x=None):
                self.x = x
            def __repr__(self):
                return "Foo()"
            def __str__(self):
                return "<some Foo>"
        $(x::Int) = 234
        ```
        @test x === 234
        @test (@pyv `x`::Int x=123) == 123
        @test (@pyv `"$$"`::String) == "\$"
        @test (@pya `ans=1.0`::Float64) == 1.0
        @test (@pyr `return 12`::Int) == 12
    end

    @testset "convert-to-python" begin
        for v in [nothing, missing]
            @test @pyv `eq($v, None)`::Bool
        end
        @test @pyv `eq($true, True)`::Bool
        @test @pyv `eq($false, False)`::Bool
        for T in [Int8,Int16,Int32,Int64,Int128,UInt8,UInt16,UInt32,UInt64,UInt128,BigInt]
            @test @pyv `eq($(zero(T)), 0)`::Bool
            @test @pyv `eq($(Rational{T}(2,3)), fractions.Fraction(2,3))`::Bool
            @test @pyv `eq($(T(0):T(9)), range(10))`::Bool
        end
        for T in [Float16,Float32,Float64]
            @test @pyv `eq($(zero(T)), 0.0)`::Bool
        end
        for T in [Float16,Float32,Float64]
            @test @pyv `eq($(zero(Complex{T})), complex(0,0))`::Bool
        end
        @test @pyv `eq($("hello"), "hello")`::Bool
        @test @pyv `eq($(SubString("hello", 1, 2)), "he")`::Bool
        @test @pyv `eq($('a'), "a")`::Bool
        @test @pyv `eq($(()), ())`::Bool
        @test @pyv `eq($((1,2,"foo",nothing)), (1,2,"foo",None))`::Bool
        @test @pyv `eq($(Date(2000,1,1)), datetime.date(2000,1,1))`::Bool
        @test @pyv `eq($(Time(12,0,0)), datetime.time(12,0,0))`::Bool
        @test @pyv `eq($(DateTime(2000,1,1,12,0,0)), datetime.datetime(2000,1,1,12,0,0))`::Bool
        for T in [Second, Millisecond, Microsecond, Nanosecond]
            @test @pyv `eq($(T(0)), datetime.timedelta())`::Bool
        end
        for v in Any[zero(BigFloat), (x=1, y=nothing), IOBuffer(), [], [1,2,3], Dict(), Set(), [1 2; 3 4]]
            @test pyisjl(PyObject(v))
        end
    end

    @testset "convert-to-julia" begin
        # PRIORITY=1000: wrapped julia values are just unwrapped
        for x in [nothing, missing, 2, 2.1, [], Int[1,2,3], (1,2,3), "foo", (x=1, y="two")]
            @test pyconvert(Any, pyjl(x)) === x
            @test pyconvert(typeof(x), pyjl(x)) === x
            @test pyconvert(Union{typeof(x), Nothing}, pyjl(x)) === x
        end

        # PRIORITY=200: buffer/array -> PyArray
        x = @pyv `b"foo"`::Any
        @test x isa PyVector{UInt8,UInt8,false,true}
        @test x == Vector{UInt8}("foo")
        x = @pyv `bytearray(b"foo")`::Any
        @test x isa PyVector{UInt8,UInt8,true,true}
        @test x == Vector{UInt8}("foo")
        x = @pyv `array.array("f", range(10))`::Any
        @test x isa PyVector{Float32,Float32,true,true}
        @test x == 0:9
        x = pyconvert(PyArray, [1 2; 3 4])
        @test x isa PyMatrix{Int,Int,true,true}
        @test x == [1 2; 3 4]

        # PRIORITY=100: other canonical conversions
        # None -> nothing
        x = @pyv `None`::Any
        @test x === nothing
        # bool -> Bool
        x = @pyv `True`::Any
        @test x === true
        # float -> Float64
        x = @pyv `1.5`::Any
        @test x === 1.5
        # complex -> Complex{Float64}
        x = @pyv `complex(1, 2.5)`::Any
        @test x === Complex(1, 2.5)
        # range -> StepRange
        x = @pyv `range(10)`::Any
        @test x === Clonglong(0):Clonglong(1):Clonglong(9)
        x = @pyv `range(2**1000)`::Any
        @test x isa StepRange{BigInt,Clonglong}
        @test x == 0:1:(big"2"^1000-1)
        x = @pyv `range(2**500, 2**1000, 2**400)`::Any
        @test x isa StepRange{BigInt,BigInt}
        @test x == (big"2"^500):(big"2"^400):(big"2"^1000-1)
        # str -> String
        x = @pyv `"hello there"`::Any
        @test x === "hello there"
        # tuple -> Tuple
        x = @pyv `(1,None,"foo")`::Any
        @test x === (Clonglong(1), nothing, "foo")
        x = @pyv `("foo",1,2,3)`::Tuple{String,Vararg{Int}}
        @test x === ("foo",1,2,3)
        x = @pyv `(None, 1)`::Tuple{Nothing,Int}
        @test x === (nothing, 1)
        # Mapping -> PyDict{PyObject, PyObject}
        x = @pyv `dict(x=1, y=2)`::Any
        @test x isa PyDict{PyObject, PyObject}
        # Sequence -> PyList{PyObject}
        x = @pyv `[1,2,3]`::Any
        @test x isa PyList{PyObject}
        # Set -> PySet{PyObject}
        x = @pyv `{1,2,3}`::Any
        @test x isa PySet{PyObject}
        x = @pyv `frozenset([1,2,3])`::Any
        @test x isa PySet{PyObject}
        # date -> Date
        x = @pyv `datetime.date(2001, 2, 3)`::Any
        @test x === Date(2001, 2, 3)
        # time -> Time
        x = @pyv `datetime.time(1, 2, 3, 4)`::Any
        @test x === Time(1, 2, 3, 0, 4)
        # datetime -> DateTime
        x = @pyv `datetime.datetime(2001, 2, 3, 4, 5, 6, 7000)`::Any
        @test x === DateTime(2001, 2, 3, 4, 5, 6, 7)
        # timedelta -> Period (Microsecond, unless overflow then Millisecond or Second)
        x = @pyv `datetime.timedelta(microseconds=12)`::Any
        @test x === Microsecond(12)
        # x = @pyv `datetime.timedelta(milliseconds=$(cld(typemax(Int),1000)+10))`::Any
        # @test x === Millisecond(cld(typemax(Int), 1000) + 10)
        # In fact, you can't make a timedelta big enough to overflow into seconds.
        # x = @pyv `datetime.timedelta(seconds=$(cld(typemax(Int),1000)+10))`::Any
        # @test x === Second(cld(typemax(Int), 1000) + 10)
        # Integral -> Integer (Clonglong, unless overflow then BigInt)
        x = @pyv `123`::Any
        @test x === Clonglong(123)
        x = @pyv `2**123`::Any
        @test x isa BigInt
        @test x == big"2"^123
        # TODO: numpy (e.g. numpy.float32 -> Float32)

        # PRIORITY=0: other reasonable conversions
        # None -> missing
        x = @pyv `None`::Missing
        @test x === missing
        x = @pyv `None`::Union{PyObject,Missing}
        @test x === missing
        # bytes -> Vector{UInt8}, Vector{Int8}, String
        x = @pyv `b"abc"`::Vector
        @test x isa Vector{UInt8}
        @test x == Vector{UInt8}("abc")
        x = @pyv `b"abc"`::Vector{Int8}
        @test x isa Vector{Int8}
        x = @pyv `b"abc"`::AbstractString
        @test x isa String
        @test x == "abc"
        # str -> Symbol, Char, Vector{UInt8}, Vector{Int8}
        x = @pyv `"foo"`::Symbol
        @test x === :foo
        x = @pyv `"x"`::AbstractChar
        @test x == 'x'
        x = @pyv `"abc"`::Vector{UInt8}
        @test x isa Vector{UInt8}
        @test x == Vector{UInt8}("abc")
        x = @pyv `"abc"`::Vector{Int8}
        @test x isa Vector{Int8}
        # range -> UnitRange
        x = @pyv `range(123)`::UnitRange{Int}
        @test x === 0:122
        # Iterable -> Vector, Set, Tuple, Pair
        x = @pyv `(1,2,3)`::Vector{Int}
        @test x isa Vector{Int}
        @test x == [1,2,3]
        x = @pyv `(1,2,3)`::Set{Int}
        @test x isa Set{Int}
        @test x == Set([1,2,3])
        x = @pyv `[1,2,3]`::Tuple{Int,Int,Int}
        @test x isa Tuple{Int,Int,Int}
        @test x === (1,2,3)
        x = @pyv `[4,5]`::Pair{Int,Int}
        @test x isa Pair{Int,Int}
        @test x === (4 => 5)
        # Mapping -> Dict
        x = @pyv `dict(x=1, y=2)`::Dict{String,Int}
        @test x isa Dict{String,Int}
        @test x == Dict("x"=>1, "y"=>2)
        # timedelta -> CompoundPeriod
        x = @pyv `datetime.timedelta(microseconds=123)`::Dates.CompoundPeriod
        @test x isa Dates.CompoundPeriod
        @test x == Dates.CompoundPeriod([Microsecond(123)])
        # Integral -> Rational, Real, Number, Any
        x = @pyv `123`::Rational{Int}
        @test x === 123//1
        x = @pyv `123`::AbstractFloat
        @test x === 123.0
        x = @pyv `123`::Complex{Int}
        @test x === Complex(123)
        # Real -> AbstractFloat, Number
        x = @pyv `1234.5`::AbstractFloat
        @test x === 1234.5
        x = @pyv `1234.5`::BigFloat
        @test x isa BigFloat
        @test x == big"1234.5"
        x = @pyv `1234.5`::Complex
        @test x === Complex(1234.5)
        x = @pyv `123.0`::Integer
        @test x === 123
        x = @pyv `1234.5`::Rational{Int}
        @test x === 2469//2
        # Complex -> Complex, Number, Any
        x = @pyv `complex(1,2)`::Complex
        @test x === Complex(1.0, 2.0)
        x = @pyv `complex(1,2)`::Complex{Int}
        @test x === Complex(1,2)
        x = @pyv `complex(1234.5, 0)`::AbstractFloat
        @test x === 1234.5
        x = @pyv `complex(123, 0)`::Integer
        @test x === 123
        # ctypes
        x = @pyv `ctypes.c_float(12)`::Number
        @test x === Cfloat(12)
        x = @pyv `ctypes.c_double(12)`::Number
        @test x === Cdouble(12)
        x = @pyv `ctypes.c_byte(12)`::Number
        @test x == Cchar(12)
        x = @pyv `ctypes.c_short(12)`::Number
        @test x === Cshort(12)
        x = @pyv `ctypes.c_int(12)`::Number
        @test x === Cint(12)
        x = @pyv `ctypes.c_long(12)`::Number
        @test x === Clong(12)
        x = @pyv `ctypes.c_longlong(12)`::Number
        @test x === Clonglong(12)
        x = @pyv `ctypes.c_ubyte(12)`::Number
        @test x == Cuchar(12)
        x = @pyv `ctypes.c_ushort(12)`::Number
        @test x === Cushort(12)
        x = @pyv `ctypes.c_uint(12)`::Number
        @test x === Cuint(12)
        x = @pyv `ctypes.c_ulong(12)`::Number
        @test x === Culong(12)
        x = @pyv `ctypes.c_ulonglong(12)`::Number
        @test x === Culonglong(12)
        x = @pyv `ctypes.c_char(12)`::Number
        @test x === Cchar(12)
        x = @pyv `ctypes.c_wchar('x')`::Number
        @test x === Cwchar_t('x')
        x = @pyv `ctypes.c_size_t(12)`::Number
        @test x === Csize_t(12)
        x = @pyv `ctypes.c_ssize_t(12)`::Number
        @test x === Cssize_t(12)
        x = @pyv `ctypes.c_void_p()`::Ptr
        @test x === C_NULL
        x = @pyv `ctypes.c_char_p()`::Ptr
        @test x === Ptr{Cchar}(0)
        x = @pyv `ctypes.c_char_p()`::Cstring
        @test x == Cstring(C_NULL)
        x = @pyv `ctypes.c_wchar_p()`::Ptr
        @test x === Ptr{Cwchar_t}(0)
        x = @pyv `ctypes.c_wchar_p()`::Cwstring
        @test x === Cwstring(C_NULL)
        # TODO: numpy

        # PRIORITY=-100: fallback to object -> PyObject
        x = @pyv `Foo()`::Any
        @test x isa PyObject
        x = @pyv `123`::PyObject
        @test x isa PyObject
        x = @pyv `123`::Union{Nothing,PyObject}
        @test x isa PyObject

        # PRIORITY=-200: conversions that must be specifically requested by excluding PyObject
        # object -> PyRef
        x = @pyv `123`::Union{Nothing,PyRef}
        @test x isa PyRef
        # buffer -> PyBuffer
        x = @pyv `b"abc"`::Union{Nothing,PyBuffer}
        @test x isa PyBuffer
    end

    @testset "builtins" begin
        Foo = @pyv `Foo`
        foo = @pyv `Foo()`
        xstr = @pyv `"x"`
        ystr = @pyv `"y"`
        # convert
        @test pyconvert(Int, @pyv `12`) === 12
        # hasattr, getattr, setattr, delattr
        @test pyhasattr(foo, :x)
        @test !pyhasattr(foo, :y)
        @test pyhasattr(foo, "x")
        @test !pyhasattr(foo, "y")
        @test pyhasattr(foo, xstr)
        @test !pyhasattr(foo, ystr)
        @test pysetattr(foo, :x, 1) === foo
        @test pysetattr(foo, "y", 2) === foo
        @test pyhasattr(foo, "x")
        @test pyhasattr(foo, :y)
        @test pysetattr(foo, ystr, 3) === foo
        @test @pyv `$(pygetattr(foo, "x")) is $foo.x`::Bool
        @test @pyv `$(pygetattr(foo, :y)) is $foo.y`::Bool
        @test @pyv `$(pygetattr(foo, xstr)) is $foo.x`::Bool
        # dir
        @test @pyv `"x" in $(pydir(foo))`::Bool
        @test @pyv `"y" in $(pydir(foo))`::Bool
        @test @pyv `"z" not in $(pydir(foo))`::Bool
        # call
        @test @pyv `type($(pycall(Foo))) is Foo`::Bool
        # repr
        @test @pyv `eq($(pyrepr(foo)), "Foo()")`::Bool
        # str
        @test @pyv `eq($(pystr(foo)), "<some Foo>")`::Bool
        @test @pyv `eq($(pystr("foo")), "foo")`::Bool
        # bytes
        @test @pyv `eq($(pybytes(Int8[])), b"")`::Bool
        @test @pyv `eq($(pybytes(pybytes(Int8[]))), b"")`::Bool
        # len, contains, in, getitem, setitem, delitem
        @test pylen(@pyv `[]`) == 0
        list = @pyv `[1,2,3]`
        @test pylen(list) == 3
        @test pycontains(list, 3)
        @test !pycontains(list, 4)
        @test pyin(1, list)
        @test !pyin(0, list)
        @test 3 ∈ list
        @test 4 ∉ list
        @test @pyv `eq($(pygetitem(list, 2)), 3)`::Bool
        @test pysetitem(list, 2, 99) === list
        @test @pyv `eq($(pygetitem(list, 2)), 99)`::Bool
        dict = @pyv `dict()`
        @test "x" ∉ dict
        @test pysetitem(dict, "x", "foo") === dict
        @test "x" ∈ dict
        @test pylen(dict) == 1
        @test @pyv `eq($(pygetitem(dict, "x")), "foo")`::Bool
        @test pydelitem(dict, "x") === dict
        @test "x" ∉ dict
        @test pylen(dict) == 0
        # none
        @test @pyv `$(pynone()) is None`::Bool
        # bool
        @test @pyv `$(pybool(false)) is False`::Bool
        @test @pyv `$(pybool(true)) is True`::Bool
        @test @pyv `$(pybool(0)) is False`::Bool
        @test @pyv `$(pybool(1)) is True`::Bool
        @test @pyv `$(pybool("")) is False`::Bool
        @test @pyv `$(pybool("foo")) is True`::Bool
        @test @pyv `$(pybool(())) is False`::Bool
        @test @pyv `$(pybool((0,))) is True`::Bool
        @test @pyv `$(pybool((1,2,3))) is True`::Bool
        # int
        @test @pyv `eq($(pyint(true)), 1)`::Bool
        @test @pyv `eq($(pyint(false)), 0)`::Bool
        for T in [Int8,Int16,Int32,Int64,Int128,UInt8,UInt16,UInt32,UInt64,UInt128,BigInt]
            x = pyint(T(123))
            @test @pyv `eq($x, 123)`::Bool
            @test @pyv `eq($(pyint(x)), 123)`::Bool
        end
        @test @pyv `eq($(pyint()), 0)`::Bool
        # float
        for T in [Float16,Float32,Float64]
            x = pyfloat(T(12.3))
            @test @pyv `type($x) is float`::Bool
            @test @pyv `abs($x - 12.3) < 0.01`::Bool
            @test @pyv `$(pyfloat(x)) == $x`::Bool
        end
        @test @pyv `eq($(pyfloat("12.3")), 12.3)`::Bool
        @test @pyv `eq($(pyfloat()), 0.0)`::Bool
        # import
        sysstr = @pyv `"sys"`
        @test @pyv `$(pyimport("sys")) is sys`::Bool
        @test @pyv `$(pyimport(sysstr)) is sys`::Bool
        @test @pyv `$(pyimport("sys"=>"stdout")) is sys.stdout`::Bool
        @test @pyv `all(a is getattr(sys,b) for (a,b) in zip($(pyimport("sys"=>("stdout","stdin"))), ["stdout","stdin"]))`::Bool
        @test @pyv `all(a is b for (a,b) in zip($(pyimport("sys","os")), [sys,os]))`::Bool
        # is, truth, issubclass, isinstance
        @test pyis(nothing, nothing)
        @test pyis(foo, foo)
        @test !pyis(foo, @pyv `Foo()`)
        @test !pytruth(@pyv `[]`)
        @test pytruth(@pyv `[1,2,3]`)
        @test pyissubclass(@pyv(`bool`), @pyv(`int`))
        @test !pyissubclass(@pyv(`bool`), @pyv(`list`))
        @test pyisinstance(@pyv(`True`), @pyv(`int`))
        @test !pyisinstance(@pyv(`True`), @pyv(`list`))
        # comparison
        @test @pyv `eq($(pyhash("foo")), hash("foo"))`::Bool
        @test @pyv `$(pyeq(1,2)) is False`::Bool
        @test @pyv `$(pyne(1,2)) is True`::Bool
        @test @pyv `$(pyle(1,2)) is True`::Bool
        @test @pyv `$(pylt(1,2)) is True`::Bool
        @test @pyv `$(pyge(1,2)) is False`::Bool
        @test @pyv `$(pygt(1,2)) is False`::Bool
        @test !pyeq(Bool, 1, 2)
        @test pyne(Bool, 1, 2)
        @test pyle(Bool, 1, 2)
        @test pylt(Bool, 1, 2)
        @test !pyge(Bool, 1, 2)
        @test !pygt(Bool, 1, 2)
        # arithmetic
        @test @pyv `eq($(pyadd(1,2)), 3)`::Bool
        @test @pyv `eq($(pyiadd(1,2)), 3)`::Bool
        @test @pyv `eq($(pysub(1,2)), -1)`::Bool
        @test @pyv `eq($(pyisub(1,2)), -1)`::Bool
        @test @pyv `eq($(pymul(1,2)), 2)`::Bool
        @test @pyv `eq($(pyimul(1,2)), 2)`::Bool
        @test_throws PyException pymatmul(1,2)
        @test_throws PyException pyimatmul(1,2)
        @test @pyv `eq($(pyfloordiv(1,2)), 0)`::Bool
        @test @pyv `eq($(pyifloordiv(1,2)), 0)`::Bool
        @test @pyv `eq($(pytruediv(1,2)), 0.5)`::Bool
        @test @pyv `eq($(pyitruediv(1.0,2)), 0.5)`::Bool
        @test @pyv `eq($(pymod(1,2)), 1)`::Bool
        @test @pyv `eq($(pyimod(1,2)), 1)`::Bool
        @test @pyv `eq($(pydivmod(1,2)), (0,1))`::Bool
        @test @pyv `eq($(pylshift(1,2)), 4)`::Bool
        @test @pyv `eq($(pyilshift(1,2)), 4)`::Bool
        @test @pyv `eq($(pyrshift(1,2)), 0)`::Bool
        @test @pyv `eq($(pyirshift(1,2)), 0)`::Bool
        @test @pyv `eq($(pyand(1,2)), 0)`::Bool
        @test @pyv `eq($(pyiand(1,2)), 0)`::Bool
        @test @pyv `eq($(pyxor(1,2)), 3)`::Bool
        @test @pyv `eq($(pyixor(1,2)), 3)`::Bool
        @test @pyv `eq($(pyor(1,2)), 3)`::Bool
        @test @pyv `eq($(pyior(1,2)), 3)`::Bool
        @test @pyv `eq($(pypow(2,3)), 8)`::Bool
        @test @pyv `eq($(pypow(2,3,5)), 3)`::Bool
        @test @pyv `eq($(pyipow(2,3)), 8)`::Bool
        @test @pyv `eq($(pyipow(2,3,5)), 3)`::Bool
        @test @pyv `eq($(pyneg(2)), -2)`::Bool
        @test @pyv `eq($(pypos(2)), 2)`::Bool
        @test @pyv `eq($(pyabs(-2)), 2)`::Bool
        @test @pyv `eq($(pyinv(2)), ~2)`::Bool
        # iter
        @test @pyv `eq($(pyiter(list)).__next__(), 1)`::Bool
        # tuple
        list = @pyv `[1,2,3]`
        @test @pyv `eq($(pytuple(list)), (1,2,3))`::Bool
        @test @pyv `eq($(pytuple([1,2,3])), (1,2,3))`::Bool
        @test @pyv `eq($(pytuple()), ())`::Bool
        # list
        @test @pyv `eq($(pylist(list)), [1,2,3])`::Bool
        @test @pyv `eq($(pylist([1,2,3])), [1,2,3])`::Bool
        @test @pyv `eq($(pylist()), [])`::Bool
        # pycollist
        @test @pyv `eq($(pycollist(fill(1))), 1)`::Bool
        @test @pyv `eq($(pycollist([1,2])), [1,2])`::Bool
        @test @pyv `eq($(pycollist([1 2; 3 4])), [[1,3],[2,4]])`::Bool
        # pyrowlist
        @test @pyv `eq($(pyrowlist(fill(1))), 1)`::Bool
        @test @pyv `eq($(pyrowlist([1,2])), [1,2])`::Bool
        @test @pyv `eq($(pyrowlist([1 2; 3 4])), [[1,2],[3,4]])`::Bool
        # set
        @test @pyv `eq($(pyset(list)), {1,2,3})`::Bool
        @test @pyv `eq($(pyset([1,2,3])), {1,2,3})`::Bool
        @test @pyv `eq($(pyset()), set())`::Bool
        # frozenset
        @test @pyv `eq($(pyfrozenset(list)), frozenset({1,2,3}))`::Bool
        @test @pyv `eq($(pyfrozenset([1,2,3])), frozenset({1,2,3}))`::Bool
        @test @pyv `eq($(pyfrozenset()), frozenset())`::Bool
        # dict
        @test @pyv `eq($(pydict(dict)), dict())`::Bool
        @test @pyv `eq($(pydict(Dict(1=>2))), {1:2})`::Bool
        @test @pyv `eq($(pydict()), dict())`::Bool
        # slice
        @test @pyv `eq($(pyslice(1)), slice(1))`::Bool
        @test @pyv `eq($(pyslice(1,2)), slice(1,2))`::Bool
        @test @pyv `eq($(pyslice(1,2,3)), slice(1,2,3))`::Bool
        # ellipsis
        @test @pyv `$(pyellipsis()) is Ellipsis`::Bool
        # NotImplemented
        @test @pyv `$(pynotimplemented()) is NotImplemented`::Bool
        # method
        @test pymethod(identity) isa PyObject
        # type
        @test @pyv `$(pytype(3)) is int`::Bool
        @test @pyv `issubclass($(pytype("Foo", (pytype(3),), ())), int)`::Bool
    end

    @testset "PyObject" begin
        x = PyObject(1)
        y = Python.pylazyobject(()->error("nope"))
        z = PyObject(2)
        foo = @pyv `Foo()`
        Foo = @pyv `Foo`
        list = @pyv `[1,2,3]`
        dict = @pyv `{"x":1, "y":2}`
        arr = @pyv `array.array('f', [1,2,3])`
        mat = PyObject([1 2; 3 4])
        bio = @pyv `io.BytesIO()`
        @test x isa PyObject
        @test y isa PyObject
        @test foo isa PyObject
        @test Python.pyptr(y) === Python.C.PyNULL
        Python.C.PyErr_Clear()
        @test string(x) == "1"
        @test (io=IOBuffer(); print(io, x); String(take!(io))) == "1"
        @test repr(x) == "<py 1>"
        @test startswith(repr(foo), "<py Foo")
        @test (io=IOBuffer(); show(io, x); String(take!(io))) == "<py 1>"
        @test (io=IOBuffer(); ioc=IOContext(io, :typeinfo=>PyObject); show(ioc, x); String(take!(io))) == "1"
        @test (io=IOBuffer(); show(io, MIME("text/plain"), x); String(take!(io))) == "py: 1"
        @test @pyv `$(foo.x) is $foo.x`::Bool
        @test (foo.x = 99) === 99
        @test @pyv `eq($(foo.x), 99)`::Bool
        @test !hasproperty(x, :invalid)
        @test hasproperty(x, :__str__)
        @test issubset([:__str__, :__repr__, :x], propertynames(foo))
        @test x.jl!(Int8) === Int8(1)
        @test x.jl!i === Int(1)
        @test x.jl!b === true
        @test x.jl!s == "1"
        @test x.jl!r == "1"
        @test x.jl!f === Cdouble(1)
        @test x.jl!c === Complex{Cdouble}(1)
        @test collect(list.jl!iter(Int)) == [1,2,3]
        @test list.jl!list() isa PyList{PyObject}
        @test list.jl!list(Int) == [1,2,3]
        @test list.jl!set() isa PySet{PyObject}
        @test list.jl!set(Int) == Set([1,2,3])
        @test dict.jl!dict() isa PyDict{PyObject, PyObject}
        @test dict.jl!dict(String) isa PyDict{String, PyObject}
        @test dict.jl!dict(String, Int) == Dict("x"=>1, "y"=>2)
        @test arr.jl!buffer() isa PyBuffer
        @test arr.jl!array() == [1,2,3]
        @test arr.jl!array(Cfloat) == [1,2,3]
        @test arr.jl!array(Cfloat, 1) == [1,2,3]
        @test arr.jl!array(Cfloat, 1, Cfloat) == [1,2,3]
        @test arr.jl!array(Cfloat, 1, Cfloat, true) == [1,2,3]
        @test arr.jl!array(Cfloat, 1, Cfloat, true, true) == [1,2,3]
        @test arr.jl!vector() == [1,2,3]
        @test arr.jl!vector(Cfloat) == [1,2,3]
        @test arr.jl!vector(Cfloat, Cfloat) == [1,2,3]
        @test arr.jl!vector(Cfloat, Cfloat, true) == [1,2,3]
        @test arr.jl!vector(Cfloat, Cfloat, true, true) == [1,2,3]
        @test mat.jl!matrix() == [1 2; 3 4]
        @test mat.jl!matrix(Int) == [1 2; 3 4]
        @test mat.jl!matrix(Int, Int) == [1 2; 3 4]
        @test mat.jl!matrix(Int, Int, true) == [1 2; 3 4]
        @test mat.jl!matrix(Int, Int, true, false) == [1 2; 3 4]
        @test bio.jl!io() isa PyIO
        @test @pyv `type($(Foo())) is Foo`::Bool
        @test Base.IteratorSize(PyObject) === Base.SizeUnknown()
        @test Base.eltype(list) === PyObject
        @test @pyv `eq($(list[0]), 1)`::Bool
        @test (list[0] = 99) === 99
        @test @pyv `eq($(list[0]), 99)`::Bool
        @test length(list) == 3
        @test delete!(list, 0) === list
        @test length(list) == 2
        @test @pyv `eq($list, [2,3])`::Bool
        clist = collect(list)
        @test length(clist) == 2
        @test @pyv `eq($(clist[1]), 2)`::Bool
        @test @pyv `eq($(clist[2]), 3)`::Bool
        @test hash(x) === UInt(1)
        # comparison
        @test @pyv `$(x == x) is True`::Bool
        @test @pyv `$(x == z) is False`::Bool
        @test @pyv `$(x != x) is False`::Bool
        @test @pyv `$(x != z) is True`::Bool
        @test @pyv `$(x <= x) is True`::Bool
        @test @pyv `$(x <= z) is True`::Bool
        @test @pyv `$(x < x) is False`::Bool
        @test @pyv `$(x < z) is True`::Bool
        @test @pyv `$(x >= x) is True`::Bool
        @test @pyv `$(x >= z) is False`::Bool
        @test @pyv `$(x > x) is False`::Bool
        @test @pyv `$(x > z) is False`::Bool
        @test isequal(x, x)
        @test !isequal(x, z)
        @test !isless(x, x)
        @test isless(x, z)
        # arithmetic
        @test @pyv `eq($(zero(PyObject)), 0)`::Bool
        @test @pyv `eq($(one(PyObject)), 1)`::Bool
        @test @pyv `eq($(-x), -1)`::Bool
        @test @pyv `eq($(+x), 1)`::Bool
        @test @pyv `eq($(~x), ~1)`::Bool
        @test @pyv `eq($(abs(x)), 1)`::Bool
        @test @pyv `eq($(x+z), 3)`::Bool
        @test @pyv `eq($(x-z), -1)`::Bool
        @test @pyv `eq($(x*z), 2)`::Bool
        @test @pyv `eq($(x/z), 0.5)`::Bool
        @test @pyv `eq($(fld(x,z)), 0)`::Bool
        @test @pyv `eq($(mod(x,z)), 1)`::Bool
        @test @pyv `eq($(z^z), 4)`::Bool
        @test @pyv `eq($(x<<z), 4)`::Bool
        @test @pyv `eq($(z>>x), 1)`::Bool
        @test @pyv `eq($(x&z), 0)`::Bool
        @test @pyv `eq($(x|z), 3)`::Bool
        @test @pyv `eq($(xor(x,z)), 3)`::Bool
        @test @pyv `eq($(x+2), 3)`::Bool
        @test @pyv `eq($(x-2), -1)`::Bool
        @test @pyv `eq($(x*2), 2)`::Bool
        @test @pyv `eq($(x/2), 0.5)`::Bool
        @test @pyv `eq($(fld(x,2)), 0)`::Bool
        @test @pyv `eq($(mod(x,2)), 1)`::Bool
        @test @pyv `eq($(z^2), 4)`::Bool
        @test @pyv `eq($(x<<2), 4)`::Bool
        @test @pyv `eq($(z>>1), 1)`::Bool
        @test @pyv `eq($(x&2), 0)`::Bool
        @test @pyv `eq($(x|2), 3)`::Bool
        @test @pyv `eq($(xor(x,2)), 3)`::Bool
        @test @pyv `eq($(1+z), 3)`::Bool
        @test @pyv `eq($(1-z), -1)`::Bool
        @test @pyv `eq($(1*z), 2)`::Bool
        @test @pyv `eq($(1/z), 0.5)`::Bool
        @test @pyv `eq($(fld(1,z)), 0)`::Bool
        @test @pyv `eq($(mod(1,z)), 1)`::Bool
        # @test @pyv `eq($(2^z), 4)`::Bool
        # @test @pyv `eq($(1<<z), 4)`::Bool
        # @test @pyv `eq($(2>>x), 1)`::Bool
        @test @pyv `eq($(1&z), 0)`::Bool
        @test @pyv `eq($(1|z), 3)`::Bool
        @test @pyv `eq($(xor(1,z)), 3)`::Bool
        @test @pyv `eq($(powermod(z,3,5)), 3)`::Bool
    end

    @testset "PyDict" begin
        o = pydict(a=1, b=2)
        @test PyDict(o) isa PyDict{PyObject, PyObject}
        @test PyDict{String}(o) isa PyDict{String, PyObject}
        @test PyDict{String, Int}(o) isa PyDict{String, Int}
        d = PyDict{String, Int}(o)
        d2 = copy(d)
        @test d == Dict("a"=>1, "b"=>2)
        @test Set(keys(d)) == Set(["a", "b"])
        @test Set(values(d)) == Set([1, 2])
        @test length(d) == 2
        @test d["a"] == 1
        @test d["b"] == 2
        @test d2 == d
        d["c"] = 3
        @test d["c"] == 3
        @test length(d) == 3
        @test haskey(d, "c")
        @test d2 != d
        @test !haskey(d2, "c")
        delete!(d, "c")
        @test length(d) == 2
        @test !haskey(d, "c")
        empty!(d2)
        @test length(d2) == 0
        @test !haskey(d2, "a")
        @test !haskey(d2, "b")
        @test get(d, "a", 0) == 1
        @test get(d, "x", 0) == 0
        @test !haskey(d, "x")
        @test get(()->0, d, "a") == 1
        @test get(()->0, d, "x") == 0
        @test !haskey(d, "x")
        @test get!(d, "a", 0) == 1
        @test length(d) == 2
        @test get!(d, "x", 0) == 0
        @test length(d) == 3
        @test d["x"] == 0
        delete!(d, "x")
        @test length(d) == 2
        @test !haskey(d, "x")
        @test get!(()->0, d, "a") == 1
        @test length(d) == 2
        @test get!(()->0, d, "x") == 0
        @test length(d) == 3
        @test d["x"] == 0
    end

    @testset "PyIO" begin
        bio = PyIO((@pyv `io.BytesIO()`), buflen=1)
        sio = PyIO((@pyv `io.StringIO()`), buflen=1)
        @test bio isa PyIO
        @test sio isa PyIO
        @test !bio.text
        @test sio.text
        @test flush(bio) === nothing
        @test flush(sio) === nothing
        @test eof(bio)
        @test eof(sio)
        @test eof(bio)
        @test eof(sio)
        @test_throws PyException fd(bio)
        @test_throws PyException fd(sio)
        @test isreadable(bio)
        @test isreadable(sio)
        @test iswritable(bio)
        @test iswritable(sio)
        write(bio, "hello")
        write(sio, "foo")
        flush(bio)
        flush(sio)
        seekstart(bio)
        seekstart(sio)
        bpos = position(bio)
        spos = position(sio)
        @test bpos isa Int
        @test spos isa Int
        @test !eof(bio)
        @test !eof(sio)
        seekend(bio)
        seekend(sio)
        @test eof(bio)
        @test eof(sio)
        seek(bio, bpos)
        seek(sio, bpos)
        @test position(bio) == bpos
        @test position(sio) == spos
        skip(bio, 2)
        skip(sio, 1)
        skip(bio, -1)
        @test_throws Exception skip(sio, -1)
        read(sio, length(sio.ibuf))
        @test position(bio) > bpos
        @test position(sio) > spos
        seekstart(bio)
        seekstart(sio)
        @test read(bio, String) == "hello"
        @test read(sio, String) == "foo"
        @test eof(bio)
        @test eof(sio)
        bpos = position(bio)
        spos = position(sio)
        @test write(bio, 'x') == 1
        @test write(sio, 'x') == 1
        seekstart(bio)
        seekstart(sio)
        @test read(bio, String) == "hellox"
        @test read(sio, String) == "foox"
        truncate(bio, bpos)
        truncate(sio, spos)
        seekstart(bio)
        seekstart(sio)
        @test read(bio, String) == "hello"
        @test read(sio, String) == "foo"
        @test isopen(bio)
        @test isopen(sio)
        @test close(bio) === nothing
        @test close(sio) === nothing
        @test !isopen(bio)
        @test !isopen(sio)
    end

    @testset "PyArray" begin
        vec = [1, 2, 3]
        mat = transpose([1.0 2.5; 3.0 4.5]) # transpose => not linearly indexable
        arr = PyObjectArray(reshape(1:8, 2, 2, 2))
        @test_throws Exception PyArray(pylist())
        @test PyArray(vec) isa PyVector{Int, Int, true, true}
        @test PyVector(vec) isa PyVector{Int, Int, true, true}
        @test PyArray{Int}(vec) isa PyVector{Int, Int, true, true}
        @test PyArray{Int,1}(vec) isa PyVector{Int, Int, true, true}
        @test PyArray{Int,1,Int}(vec) isa PyVector{Int, Int, true, true}
        @test PyArray{Int,1,Int,true}(vec) isa PyVector{Int, Int, true, true}
        @test PyArray{Int,1,Int,true,true}(vec) isa PyVector{Int, Int, true, true}
        @test PyArray{Int,1,Int,false,false}(vec) isa PyVector{Int, Int, false, false}
        @test PyArray(mat) isa PyMatrix{Float64, Float64, true, false}
        @test PyMatrix(mat) isa PyMatrix{Float64, Float64, true, false}
        @test PyArray{Float64}(mat) isa PyMatrix{Float64, Float64, true, false}
        @test PyArray{Float64,2}(mat) isa PyMatrix{Float64, Float64, true, false}
        @test PyArray{Float64,2,Float64}(mat) isa PyMatrix{Float64, Float64, true, false}
        @test PyArray{Float64,2,Float64,true}(mat) isa PyMatrix{Float64, Float64, true, false}
        @test PyArray{Float64,2,Float64,true,false}(mat) isa PyMatrix{Float64, Float64, true, false}
        @test PyArray(arr) isa PyArray{Python.CPython.PyObjectRef, 3, PyObject, true, true}
        @test PyArray(["foo", "bar"]) isa PyVector{Python.CPython.PyObjectRef, PyObject, true, true}
        veco = PyArray(vec)
        mato = PyArray(mat)
        arro = PyArray(arr)
        @test Python.ismutablearray(veco)
        @test Python.ismutablearray(mato)
        @test Python.ismutablearray(arro)
        @test !Python.ismutablearray(PyArray{Int,1,Int,false,true}(vec))
        @test size(veco) == size(vec)
        @test size(mato) == size(mat)
        @test size(arro) == size(arr)
        @test length(veco) == length(vec)
        @test length(mato) == length(mat)
        @test length(arro) == length(arr)
        @test isequal(veco, vec)
        @test isequal(mato, mat)
        @test isequal(arro, arr)
        @test Base.IndexStyle(typeof(veco)) == Base.IndexLinear()
        @test Base.IndexStyle(typeof(mato)) == Base.IndexCartesian()
        @test Base.IndexStyle(typeof(arro)) == Base.IndexLinear()
        vec[1] += 1
        mat[1] += 1
        arr[1] += 1
        @test isequal(veco, vec)
        @test isequal(mato, mat)
        @test isequal(arro, arr)
        veco[1] += 1
        mato[1] += 1
        arro[1] += 1
        @test isequal(veco, vec)
        @test isequal(mato, mat)
        @test isequal(arro, arr)
        mato[2,2] += 1
        @test isequal(mato, mat)
        arro[2,2,2] += 1
        @test isequal(arro, arr)
    end

    @testset "julia" begin
        for value in Any[nothing, false, true, 0, 1, typemin(Int), 1.0, (), (1,false,()), [], Int[1,2,3], stdin, stdout, IOBuffer()]
            r = pyjlraw(value)
            @test r isa PyObject
            @test pyisjl(r)
            @test pyjlgetvalue(r) === value
            v = pyjl(value)
            @test v isa PyObject
            @test pyisjl(v)
            @test pyjlgetvalue(v) === value
        end

        @testset "juliaany" begin
            for value in Any[nothing, missing, (), identity, push!]
                @test @pyv `type($(pyjl(value))).__name__ == "AnyValue"`::Bool
            end
            @test @pyv `repr($(pyjl(missing))) == "<jl missing>"`::Bool
            @test @pyv `str($(pyjl(missing))) == "missing"`::Bool
            x = Struct1("foo", 2)
            @test @pyv `$(pyjl(x)).x == "foo"`::Bool
            @test @pyv `$(pyjl(x)).y == 2`::Bool
            @py `$(pyjl(x)).y = 0`
            @test x.y == 0
            @test @pyv `"x" in dir($(pyjl(x)))`::Bool
            @test @pyv `"Int" in dir($(pyjl(Base)))`::Bool
            @test @pyv `"push_b" in dir($(pyjl(Base)))`::Bool
            @test @pyv `$(Int)(1.0) == 1`::Bool
            @test (@pyv `$(pyjl(Vector))()`::Any) == Vector()
            @test (@pyv `$(pyjl(sort))($(pyjl([1,2,3])), rev=True)`::Any) == [3,2,1]
            @test_throws PyException @pyv `$(pyjl(nothing))("not", "callable")`::Union{}
            @test @pyv `len($(pyjl((1,2,3)))) == 3`::Bool
            @test_throws PyException @pyv `len($(pyjl(x)))`::Bool
            x = Dict(1=>2)
            @test @pyv `$(pyjl(x))[1] == 2`::Bool
            @test_throws PyException @pyv `$(pyjl((1, 2)))[0]`::Union{}
            @test_throws PyException @pyv `$(pyjl(Dict("x"=>1, "y"=>2)))["z"]`::Union{}
            @py `$(pyjl(x))[1] = 0`
            @test x[1] == 0
            @py `del $(pyjl(x))[1]`
            @test isempty(x)
            @test_throws PyException @py `$(pyjl((1, 2)))[1] = 0`::Union{}
            @test_throws PyException @py `$(pyjl(Dict("x"=>1, "y"=>2)))[nothing] = 0`::Union{}
            @test @pyv `1 in $(pyjl((1,2,3)))`::Bool
            @test @pyv `0 not in $(pyjl((1,2,3)))`::Bool
            @test @pyv `1.5 not in $(pyjl((1,2,3)))`::Bool
            @test @pyv `$(pyjl(3)) == 3`::Bool
            @test @pyv `not ($(pyjl(3)) == 5)`::Bool
            @test @pyv `not ($(pyjl(3)) != 3)`::Bool
            @test @pyv `$(pyjl(3)) != 5`::Bool
            @test @pyv `$(pyjl(3)) <= 3`::Bool
            @test @pyv `not ($(pyjl(3)) <= 2)`::Bool
            @test @pyv `not ($(pyjl(3)) < 3)`::Bool
            @test @pyv `$(pyjl(3)) < 5`::Bool
            @test @pyv `$(pyjl(3)) >= 3`::Bool
            @test @pyv `not ($(pyjl(3)) >= 5)`::Bool
            @test @pyv `not ($(pyjl(3)) > 3)`::Bool
            @test @pyv `$(pyjl(3)) > 2`::Bool
            @test @pyv `$(pyjl(identity)).__name__ == "identity"`::Bool
        end

        @testset "juliaio" begin
            for value in Any[stdin, stdout, IOBuffer()]
                @test @pyv `type($(pyjl(value))).__name__ == "BufferedIOValue"`::Bool
                @test @pyv `type($(pyrawio(value))).__name__ == "RawIOValue"`::Bool
                @test @pyv `type($(pybufferedio(value))).__name__ == "BufferedIOValue"`::Bool
                @test @pyv `type($(pytextio(value))).__name__ == "TextIOValue"`::Bool
            end
            io = IOBuffer()
            @test @pyv `"BufferedIOValue" in $io.__class__.__name__`::Bool
            @test @pyv `"BufferedIOValue" in $(pybufferedio(io)).__class__.__name__`::Bool
            @test @pyv `"BufferedIOValue" in $io.tobufferedio().__class__.__name__`::Bool
            @test @pyv `"RawIOValue" in $io.torawio().__class__.__name__`::Bool
            @test @pyv `"TextIOValue" in $io.totextio().__class__.__name__`::Bool
            @test @pyv `"TextIOValue" in $(pytextio(io)).__class__.__name__`::Bool
            @test @pyv `"TextIOValue" in $(pytextio(io)).totextio().__class__.__name__`::Bool
            @test @pyv `"BufferedIOValue" in $(pytextio(io)).tobufferedio().__class__.__name__`::Bool
            @test @pyv `"RawIOValue" in $(pytextio(io)).torawio().__class__.__name__`::Bool
            @test @pyv `"RawIOValue" in $(pyrawio(io)).__class__.__name__`::Bool
            @test @pyv `"RawIOValue" in $(pyrawio(io)).torawio().__class__.__name__`::Bool
            @test @pyv `"BufferedIOValue" in $(pyrawio(io)).tobufferedio().__class__.__name__`::Bool
            @test @pyv `"TextIOValue" in $(pyrawio(io)).totextio().__class__.__name__`::Bool
            println(io, "foo")
            println(io, "bar")
            seekstart(io)
            @test @pyv `eq(list($io), [b"foo\n", b"bar\n"])`::Bool
            @test @pyv `not $io.closed`::Bool
            @test_throws PyException @pyv `$io.fileno()`::Int
            @test @pyv `$io.flush() is None`::Bool
            @test @pyv `not $io.isatty()`::Bool
            @test @pyv `$io.readable()`::Bool
            @test @pyv `$io.writable()`::Bool
            @test @pyv `$io.close() is None`::Bool
            @test @pyv `$io.closed`::Bool
            io = IOBuffer()
            @test @pyv `$io.tell() == 0`::Bool
            print(io, "foo")
            @test @pyv `$io.tell() == 3`::Bool
            @test @pyv `$io.seek(2) == 2`::Bool
            @test @pyv `$io.seek(1, 0) == 1`::Bool
            @test @pyv `$io.seek(1, 1) == 2`::Bool
            @test @pyv `$io.seek(-2, 2) == 1`::Bool
            @test_throws PyException @pyv `$io.seek(0, 3)`::Union{}
            @test @pyv `$io.seek(2) == 2`::Bool
            @test @pyv `$io.truncate() == 2`::Bool
            @test @pyv `$io.truncate(1) == 1`::Bool
            @test @pyv `$io.tell() == 1`::Bool
            @test @pyv `$io.seekable()`::Bool
            io = IOBuffer()
            @test @pyv `$io.writelines([b"f", b"oo"]) is None`::Bool
            @test String(take!(io)) == "foo"
            println(io, "foo")
            println(io, "bar")
            seekstart(io)
            @test @pyv `$io.readlines() == [b"foo\n", b"bar\n"]`::Bool
            @test @pyv `eq($(pytextio(io)).encoding, "UTF-8")`::Bool
            @test @pyv `eq($(pytextio(io)).errors, "strict")`::Bool
            @test_throws PyException @pyv `$(pytextio(io)).detach()`::Union{}
            io = IOBuffer()
            @test @pyv `$io.write(b"fooo") == 4`::Bool
            @test String(take!(io)) == "fooo"
            @test @pyv `$io.totextio().write("a\nb\nc") == 5`::Bool
            linesep = @pyv `os.linesep`::String
            @test String(take!(io)) == "a$(linesep)b$(linesep)c"
            x = zeros(UInt8, 10)
            io = IOBuffer()
            write(io, "foo")
            seekstart(io)
            @test @pyv `$io.readinto($x) == 3`::Bool
            @test String(take!(io)) == "foo"
            write(io, "bar")
            seekstart(io)
            @test @pyv `eq($io.read(), b"bar")`::Bool
            seekstart(io)
            @test @pyv `eq($io.totextio().read(), "bar")`::Bool
            io = IOBuffer()
            println(io, "foo")
            println(io, "bar")
            seekstart(io)
            @test @pyv `eq($io.readline(), b"foo\n")`::Bool
            @test @pyv `eq($io.totextio().readline(), "bar\n")`::Bool
        end
    end

end


text:
# Note that this script can accept some limited command-line arguments, run
# `julia build_tarballs.jl --help` to see a usage message.
using BinaryBuilder

name = "Elemental"
version = v"0.87.7"

# Collection of sources required to build Elemental
sources = [
    GitSource("https://github.com/elemental/Elemental.git",
              "477e503a7a840cc1a75173552711b980505a0b06"),
]

# Bash recipe for building across all platforms
script = raw"""
if [[ "$nbits" == "64" ]]; then
  INT64="ON"
else
  INT64="OFF"
fi

if [[ "$nbits" == "64" ]] && [[ "$target" != aarch64-* ]]; then
  BLAS_LAPACK_LIB="$libdir/libopenblas64_.$dlext"
  BLAS_LAPACK_SUFFIX="_64_"
  BLAS_INT64="ON"
else
  BLAS_LAPACK_LIB="$libdir/libopenblas.$dlext"
  BLAS_LAPACK_SUFFIX=""
  BLAS_INT64="OFF"
fi

mkdir "$WORKSPACE/srcdir/$SRC_NAME/build"
cd "$WORKSPACE/srcdir/$SRC_NAME/build"

cmake \
  -DCMAKE_INSTALL_PREFIX="$prefix" \
  -DCMAKE_TOOLCHAIN_FILE="$CMAKE_TARGET_TOOLCHAIN" \
  -DCMAKE_BUILD_TYPE="Release" \
  -DEL_USE_64BIT_INTS="$INT64" \
  -DEL_USE_64BIT_BLAS_INTS="$BLAS_INT64" \
  -DEL_DISABLE_PARMETIS="ON" \
  -DMETIS_TEST_RUNS_EXITCODE="0" \
  -DMETIS_TEST_RUNS_EXITCODE__TRYRUN_OUTPUT="" \
  -DMATH_LIBS="$BLAS_LAPACK_LIB" \
  -DBLAS_LIBRARIES="$BLAS_LAPACK_LIB" \
  -DLAPACK_LIBRARIES="$BLAS_LAPACK_LIB" \
  -DEL_BLAS_SUFFIX="$BLAS_LAPACK_SUFFIX" \
  -DEL_LAPACK_SUFFIX="$BLAS_LAPACK_SUFFIX" \
  "$WORKSPACE/srcdir/$SRC_NAME"

make "-j$nproc"
make install
"""

# These are the platforms we will build for by default, unless further
# platforms are passed in on the command line
platforms = supported_platforms()
platforms = expand_cxxstring_abis(platforms)
filter!(!Sys.iswindows, platforms)

# The products that we will ensure are always built
products = [
    LibraryProduct("libEl", :libEl),
    LibraryProduct("libElSuiteSparse", :libElSuiteSparse),
]

# Dependencies that must be installed before this package can be built
dependencies = [
    Dependency("METIS_jll"),
    Dependency("MPICH_jll"),
    Dependency("OpenBLAS_jll"),
]

# Build the tarballs, and possibly a `build.jl` as well.
build_tarballs(ARGS, name, version, sources, script, platforms, products, dependencies)

ext: jl

max_stars_repo_name: sharanry/Yggdrasil

max_issues_repo_name: sharanry/Yggdrasil

max_forks_repo_name: sharanry/Yggdrasil


text:
module _PyJuliaHelper

const REPL =
    Base.require(Base.PkgId(Base.UUID("3fa0cd96-eef1-5676-8a61-b3b8758bbffb"), "REPL"))
const PyCall =
    Base.require(Base.PkgId(Base.UUID("438e738f-606a-5dbb-bf0a-cddfbfd45ab0"), "PyCall"))
const MacroTools = Base.require(Base.PkgId(
    Base.UUID("1914dd2f-81c6-5fcd-8719-6d5c9610ff09"),
    "MacroTools",
))

using .PyCall
using .PyCall: Py_eval_input, Py_file_input, pyeval_
using .MacroTools: isexpr, walk

"""
    fullnamestr(m)

# Examples
```jldoctest
julia> fullnamestr(Base.Enums)
"Base.Enums"
```
"""
fullnamestr(m) = join(fullname(m), ".")

isdefinedstr(parent, member) = isdefined(parent, Symbol(member))

function completions(str, pos)
    ret, ran, should_complete = REPL.completions(str, pos)
    return (
        map(REPL.completion_text, ret),
        (first(ran), last(ran)),
        should_complete,
    )
end

# takes an expression like `$foo + 1` and turns it into a pyfunction
# `(globals,locals) -> convert(PyAny, pyeval_("foo",globals,locals,PyAny)) + 1`
# so that Python code can call it and just pass the appropriate globals/locals
# dicts to perform the interpolation.
macro prepare_for_pyjulia_call(ex)

    # f(x, quote_depth) should return a transformed expression x and whether to
    # recurse into the new expression. quote_depth keeps track of how deep
    # inside of nested quote objects we arepyeval
    function stoppable_walk(f, x, quote_depth=1)
        (fx, recurse) = f(x, quote_depth)
        if isexpr(fx,:quote)
            quote_depth += 1
        end
        if isexpr(fx,:$)
            quote_depth -= 1
        end
        walk(fx, (recurse ? (x -> stoppable_walk(f,x,quote_depth)) : identity), identity)
    end

    function make_pyeval(globals, locals, expr::Union{String,Symbol}, options...)
        code = string(expr)
        T = length(options) == 1 && 'o' in options[1] ? PyObject : PyAny
        input_type = '\n' in code ? Py_file_input : Py_eval_input
        :($convert($T, $pyeval_($code, $globals, $locals, $input_type)))
    end

    function insert_pyevals(globals, locals, ex)
        stoppable_walk(ex) do x, quote_depth
            if quote_depth==1 && isexpr(x, :$)
                if x.args[1] isa Symbol
                    make_pyeval(globals, locals, x.args[1]), false
                else
                    error("""syntax error in: \$($(string(x.args[1])))
                    Use py"..." instead of \$(...) for interpolating Python expressions.""")
                end
            elseif quote_depth==1 && isexpr(x, :macrocall)
                if x.args[1]==Symbol("@py_str")
                    # in Julia 0.7+, x.args[2] is a LineNumberNode, so filter it out
                    # in a way that's compatible with Julia 0.6:
                    make_pyeval(globals, locals, filter(s->(s isa String), x.args[2:end])...), false
                else
                    x, false
                end
            else
                x, true
            end
        end
    end

    esc(quote
        $pyfunction(
            (globals, locals)->Base.eval(Main, $insert_pyevals(globals, locals, $(QuoteNode(ex)))),
            $PyObject, $PyObject
        )
    end)
end

module IOPiper

const orig_stdin  = Ref{IO}()
const orig_stdout = Ref{IO}()
const orig_stderr = Ref{IO}()

function __init__()
    orig_stdin[]  = stdin
    orig_stdout[] = stdout
    orig_stderr[] = stderr
end

"""
    num_utf8_trailing(d::Vector{UInt8})

If `d` ends with an incomplete UTF8-encoded character, return the number of trailing incomplete bytes.
Otherwise, return `0`.

Taken from IJulia.jl.
"""
function num_utf8_trailing(d::Vector{UInt8})
    i = length(d)
    # find last non-continuation byte in d:
    while i >= 1 && ((d[i] & 0xc0) == 0x80)
        i -= 1
    end
    i < 1 && return 0
    c = d[i]
    # compute number of expected UTF-8 bytes starting at i:
    n = c <= 0x7f ? 1 : c < 0xe0 ? 2 : c < 0xf0 ? 3 : 4
    nend = length(d) + 1 - i # num bytes from i to end
    return nend == n ? 0 : nend
end

function pipe_stream(sender::IO, receiver, buf::IO = IOBuffer())
    try
        while !eof(sender)
            nb = bytesavailable(sender)
            write(buf, read(sender, nb))

            # Taken from IJulia.send_stream:
            d = take!(buf)
            n = num_utf8_trailing(d)
            dextra = d[end-(n-1):end]
            resize!(d, length(d) - n)
            s = String(copy(d))

            write(buf, dextra)
            receiver(s)  # check isvalid(String, s)?
        end
    catch e
        if !isa(e, InterruptException)
            rethrow()
        end
        pipe_stream(sender, receiver, buf)
    end
end

const read_stdout = Ref{Base.PipeEndpoint}()
const read_stderr = Ref{Base.PipeEndpoint}()

function pipe_std_outputs(out_receiver, err_receiver)
    global readout_task
    global readerr_task
    read_stdout[], = redirect_stdout()
    readout_task = @async pipe_stream(read_stdout[], out_receiver)
    read_stderr[], = redirect_stderr()
    readerr_task = @async pipe_stream(read_stderr[], err_receiver)
end

end  # module

end  # module

max_stars_repo_name: manuelbb-upb/pyjulia

max_issues_repo_name: manuelbb-upb/pyjulia

max_forks_repo_name: manuelbb-upb/pyjulia


text:
"""
    to_cartesian(lat::T, lon::T, r::T) where {T}
    to_cartesian(location::GeoLocation)
    to_cartesian(locations::Vector{GeoLocation})

Converts a vector of GeoLocations to (x, y, z) cartesian coordinates (based on radius of the Earth).
"""
function to_cartesian(lat::T, lon::T, r::T) where {T}
    x = r * cos(lat) * cos(lon)
    y = r * cos(lat) * sin(lon)
    z = r * sin(lat)
    return x, y, z
end
function to_cartesian(loc::GeoLocation)
    lat = deg2rad(loc.lat)
    lon = deg2rad(loc.lon)
    r = loc.alt + RADIUS_OF_EARTH_KM # approximate radius of earth + alt in km
    x, y, z = to_cartesian(lat, lon, r)
    return x, y, z
end
function to_cartesian(locs::Vector{GeoLocation})
    n_points = length(locs)
    results = Matrix{Float64}(undef, 3, n_points)
    @inbounds for i in 1:n_points
        x, y, z = to_cartesian(locs[i])
        results[1, i] = x
        results[2, i] = y
        results[3, i] = z
    end
    return results
end

"""
    haversine(a_lat::T, a_lon::T, b_lat::T, b_lon::T) where {T}
    haversine(GeoLocation, B::GeoLocation)
    haversine(Node, B::Node)
    haversine(A::Vector{GeoLocation}, B::Vector{GeoLocation})
    haversine(A::Vector{Node}, B::Vector{Node})
    haversine(a::Vector{U}, b::Vector{U})::U where {U <: AbstractFloat}

Calculates the haversine distance (km) between two points.
"""

function haversine(a_lat::T, a_lon::T, b_lat::T, b_lon::T) where {T}
    d = sin((a_lat - b_lat) / 2)^2 + cos(b_lat) * cos(a_lat) * sin((a_lon - b_lon) / 2)^2
    return 2 * RADIUS_OF_EARTH_KM * asin(sqrt(d))
end
function haversine(A::GeoLocation, B::GeoLocation)
    a_lat = deg2rad(A.lat)
    a_lon = deg2rad(A.lon)
    b_lat = deg2rad(B.lat)
    b_lon = deg2rad(B.lon)
    return haversine(a_lat, a_lon, b_lat, b_lon)
end
haversine(a::Node, b::Node) = haversine(a.location, b.location)
haversine(A::Vector{<:GeoLocation}, B::Vector{<:GeoLocation}) = haversine.(A, B)
haversine(A::Vector{<:Node}, B::Vector{<:Node}) = haversine.(A, B)
function haversine(a::Vector{U}, b::Vector{U})::U where {U <: AbstractFloat}
    a_lat = deg2rad(a[1])
    a_lon = deg2rad(a[2])
    b_lat = deg2rad(b[1])
    b_lon = deg2rad(b[2])
    return haversine(a_lat, a_lon, b_lat, b_lon)
end

"""
    euclidean(a_x::T, a_y::T, a_z::T, b_x::T, b_y::T, b_z::T) where {T}
    euclidean(A::GeoLocation, B::GeoLocation)
    euclidean(A::Node, B::Node)
    euclidean(A::Vector{GeoLocation}, B::Vector{GeoLocation})
    euclidean(A::Vector{<:Node}, B::Vector{<:Node})
    euclidean(a::Vector{U}, b::Vector{U})::U where {U <: AbstractFloat}

Calculates the euclidean distance (km) between two points.
"""
euclidean(a_x::T, a_y::T, a_z::T, b_x::T, b_y::T, b_z::T) where {T} = hypot(a_x-b_x, a_y-b_y, a_z-b_z)
euclidean(A::GeoLocation, B::GeoLocation) = euclidean(to_cartesian(A)...,to_cartesian(B)...)
euclidean(a::Node, b::Node) = euclidean(a.location, b.location)
euclidean(A::Vector{GeoLocation}, B::Vector{GeoLocation}) = euclidean.(A, B)
euclidean(A::Vector{<:Node}, B::Vector{<:Node}) = euclidean.(A, B)
function euclidean(a::Vector{U}, b::Vector{U})::U where {U <: AbstractFloat}
    a_lat = deg2rad(a[1])
    a_lon = deg2rad(a[2])
    b_lat = deg2rad(b[1])
    b_lon = deg2rad(b[2])
    return euclidean(
        to_cartesian(a_lat, a_lon, RADIUS_OF_EARTH_KM)...,
        to_cartesian(b_lat, b_lon, RADIUS_OF_EARTH_KM)...
    )
end

"""
    distance(A::Union{Vector{GeoLocation}, GeoLocation, Vector{<:Node}, Node, Vector{<:AbstractFloat}},
             B::Union{Vector{GeoLocation}, GeoLocation, Vector{<:Node}, Node, Vector{<:AbstractFloat}},
             type::Symbol=:haversine
             )

Calculates the distance (km) between two points or two vectors of points.

# Arguments
- `A::Union{Vector{GeoLocation}, GeoLocation, Vector{<:Node}, Node, Vector{<:AbstractFloat}}`: Vector of origin points.
- `B::Union{Vector{GeoLocation}, GeoLocation, Vector{<:Node}, Node, Vector{<:AbstractFloat}}`: Vector of destination points.
- `method::Symbol=:haversine`: Either `:haversine` or `:euclidean`.

# Return
- Distance between origin and destination points in km.
"""
function distance(A::Union{Vector{GeoLocation},GeoLocation,Vector{<:Node},Node,Vector{<:AbstractFloat}},
                  B::Union{Vector{GeoLocation},GeoLocation,Vector{<:Node},Node,Vector{<:AbstractFloat}},
                  method::Symbol=:haversine
                  )
    if method == :haversine
        return haversine(A, B)
    elseif method == :euclidean
        return euclidean(A, B)
    else
        throw(ArgumentError("Distance method $method not implemented"))
    end
end

"""
    heading(a::GeoLocation, b::GeoLocation, return_units::Symbol=:degrees)
    heading(a::Node, b::Node, return_units::Symbol=:degrees)
    heading(A::Vector{GeoLocation}, B::Vector{GeoLocation}, return_units::Symbol=:degrees)
    heading(A::Vector{Node}, B::Vector{Node}, return_units::Symbol=:degrees)

Calculates heading(s) / bearing(s) between two points (`a` is origin, `b` is destination)
or two vectors of points (`A` is vector of origins, `B` is vector of destinations). Points
can be either `GeoLocation`s or `Node`s.

Depending on the `return_units` chosen, the return angle is in range of [-π, π] if `:radians`
or [-180, 180] if `:degrees`. Additionally, adjusts destination longitude in case the straight
line path between a and b crosses the International Date Line.
"""
function heading(a::GeoLocation, b::GeoLocation, return_units::Symbol=:degrees)
    a_lat = a.lat
    a_lon = a.lon
    b_lat = b.lat
    b_lon = b.lon

    # Adjust destination longitude in case straight line path between A and B crosses the International Date Line
    a_lon_left_idx = (b_lon <= a_lon) * ((b_lon + 180) + (180 - a_lon) > (a_lon - b_lon))
    a_lon_right_idx = (b_lon <= a_lon) * ((b_lon + 180) + (180 - a_lon) <= (a_lon - b_lon))

    b_lon_left_idx = (b_lon > a_lon) * ((a_lon + 180) + (180 - b_lon) .< (b_lon - a_lon))
    b_lon_right_idx = (b_lon > a_lon) * ((a_lon + 180) + (180 - b_lon) >= (b_lon - a_lon))

    b_lon_fixed = b_lon_left_idx * (-180 - (180 - b_lon)) +
                  b_lon_right_idx * b_lon +
                  a_lon_left_idx * b_lon +
                  a_lon_right_idx * (180 - abs(-180 - b_lon))

    a_lat = deg2rad(a_lat)
    a_lon = deg2rad(a_lon)
    b_lat = deg2rad(b_lat)
    b_lon_fixed = deg2rad(b_lon_fixed)

    y = sin(b_lon_fixed - a_lon) * cos(b_lat)
    x = cos(a_lat) * sin(b_lat) - sin(a_lat) * cos(b_lat) * cos(b_lon_fixed - a_lon)

    heading = atan.(y, x)

    if return_units == :radians
        return heading
    elseif return_units == :degrees
        return rad2deg(heading)
    else
        throw(ArgumentError("Incorrect input for argument `return_units`, choose either `:degrees` or `:radians`"))
    end
end
heading(A::Vector{GeoLocation}, B::Vector{GeoLocation}, return_units::Symbol=:degrees) = heading.(A, B, return_units)
heading(a::Node, b::Node, return_units::Symbol=:degrees)::AbstractFloat = heading(a.location, b.location, return_units)
heading(A::Vector{<:Node}, B::Vector{<:Node}, return_units::Symbol=:degrees) = heading.(A, B, return_units)

"""
    calculate_location(origin::GeoLocation, heading::Number, distance::Number)
    calculate_location(origin::Node, heading::Number, distance::Number)
    calculate_location(origin::Vector{GeoLocation}, heading::Vector{<:Number}, distance::Vector{<:Number})
    calculate_location(origin::Vector{Node}, heading::Vector{<:Number}, distance::Vector{<:Number})

Calculates next location(s) given origin `GeoLocation`(s) or `Node`(s), heading(s) (degrees)
and distance(s) (km).

Locations are returned as `GeoLocation`s.
"""
function calculate_location(origin::GeoLocation, heading::Number, distance::Number)
    lat = deg2rad(origin.lat)
    lon = deg2rad(origin.lon)
    heading = deg2rad(heading)

    lat_final = asin(sin(lat) * cos(distance / RADIUS_OF_EARTH_KM) + cos(lat) * sin(distance / RADIUS_OF_EARTH_KM) * cos(heading))
    lon_final = lon + atan(sin(heading) * sin(distance / RADIUS_OF_EARTH_KM) * cos(lat), cos(distance / RADIUS_OF_EARTH_KM) - sin(lat) * sin(lat_final))

    return GeoLocation(rad2deg(lat_final), rad2deg(lon_final))
end
calculate_location(origins::Vector{GeoLocation}, headings::Vector{<:Number}, distances::Vector{<:Number}) = calculate_location.(origins, headings, distances)
calculate_location(origin::Node, heading::Number, distance::Number)::GeoLocation = calculate_location(origin.location, heading, distance)
calculate_location(origins::Vector{<:Node}, headings::Vector{<:Number}, distances::Vector{<:Number}) = calculate_location.(origins, headings, distances)

"""
    bounding_box_from_point(point::GeoLocation, radius::Number)::NamedTuple

Calculates the coordinates of the bounding box given a centroid point and radius (km).

# Arguments
- `point::GeoLocation`: Centroid of the bounding box as an GeoLocation.
- `radius::Number`: Radius in km of the bounding box (to each corner).

# Return
- `NamedTuple`: Named tuple with attributes minlat, minlon, maxlat, right_lon.
"""
function bounding_box_from_point(point::GeoLocation, radius::Number)::NamedTuple
    bottom_left, top_right = calculate_location([point, point], [225, 45], [radius, radius])
    return (minlat = bottom_left.lat, minlon = bottom_left.lon, maxlat = top_right.lat, maxlon = top_right.lon)
end


text:
# Plain text formats returned by FDSN web services

"""
    text_response_tokens_and_throw(T, s, n) -> tokens

Split the string `s` into tokens separated by `'|'` characters and
return them, unless there are not exactly `n` tokens, in which
case throw an `ArgumentError`.

`n` may also be an iterable of `Integers`, in which case an error
is thrown if none of the values in `n` equal the length of `tokens.
"""
function text_response_tokens_and_throw(T, s, n)
    tokens = split(s, '|')
    N = length(tokens)
    # N.B.: This relies on scalars being iterable
    N in n || throw(ArgumentError("incorrect number of tokens in $T " *
        "line '$s'.  (Expecting $n; found $N.)"))
    tokens
end

"Local parsing function to throw helpful error when failing to parse"
function _parse(T, s)
    v = tryparse(T, s)
    v === nothing && throw(ArgumentError("error parsing 's' as $T"))
    v
end

function _parse(::Type{DateTime}, s)
    # Handle the fact that empty strings are always parsed as valid and
    # give you the date 0001-01-01!
    isempty(s) && return missing
    DateTime(s)
end

_parse(::Type{String}, s) = s
_parse(::Type{Union{Missing,T}}, s) where T = isempty(s) ? missing : _parse(T, s)

abstract type FDSNTextResponse end

"""
    FDSNNetworkTextResponse

Struct containing the fields returned from a `FDSNStation` request where
the `level` is `"network"` and the `format` is `"text"`.
"""
struct FDSNNetworkTextResponse <: FDSNTextResponse
    network::String
    description::String
    starttime::DateTime
    endtime::MDateTime
    total_stations::Int
end

function Base.convert(::Type{FDSNNetworkTextResponse}, s::AbstractString)
    tokens = text_response_tokens_and_throw(FDSNNetworkTextResponse, s, 5)
    network = strip(tokens[1])
    description = tokens[2]
    starttime = _parse(DateTime, tokens[3])
    endtime = _parse(DateTime, tokens[4])
    total_stations = _parse(Int, tokens[5])
    FDSNNetworkTextResponse(network, description, starttime, endtime, total_stations)
end

"""
    FDSNStationTextResponse

Struct containing the fields returned from a `FDSNStation` request where
the `level` is `"station"` and the `format` is `"text"`.
"""
struct FDSNStationTextResponse <: FDSNTextResponse
    network::String
    station::String
    latitude::Float64
    longitude::Float64
    elevation::Float64
    sitename::String
    starttime::DateTime
    endtime::MDateTime
end

function Base.convert(::Type{FDSNStationTextResponse}, s::AbstractString)
    tokens = text_response_tokens_and_throw(FDSNStationTextResponse, s, 8)
    network = strip(tokens[1])
    station = tokens[2]
    latitude = _parse(Float64, tokens[3])
    longitude = _parse(Float64, tokens[4])
    elevation = _parse(Float64, tokens[5])
    sitename = tokens[6]
    starttime = _parse(DateTime, tokens[7])
    endtime = _parse(DateTime, tokens[8])
    FDSNStationTextResponse(network, station, latitude, longitude, elevation,
        sitename, starttime, endtime)
end

"""
    FDSNChannelTextResponse

Struct containing the fields returned from a `FDSNStation` request where
the `level` is `"channel"` and the `format` is `"text"`.
"""
struct FDSNChannelTextResponse <: FDSNTextResponse
    network::String
    station::String
    location::String
    channel::String
    latitude::Float64
    longitude::Float64
    elevation::Float64
    depth::Float64
    azimuth::Float64
    dip::Float64
    sensor_description::String
    scale::MFloat
    scale_frequency::MFloat
    scale_units::MString
    sample_rate::Float64
    starttime::DateTime
    endtime::MDateTime
end

function Base.convert(::Type{FDSNChannelTextResponse}, s::AbstractString)
    tokens = text_response_tokens_and_throw(FDSNChannelTextResponse, s, 17)
    network = strip(tokens[1])
    station = strip(tokens[2])
    location = strip(tokens[3])
    channel = strip(tokens[4])
    latitude = _parse(Float64, tokens[5])
    longitude = _parse(Float64, tokens[6])
    elevation = _parse(Float64, tokens[7])
    depth = _parse(Float64, tokens[8])
    azimuth = _parse(Float64, tokens[9])
    dip = _parse(Float64, tokens[10])
    sensor_description = tokens[11]
    scale = _parse(MFloat, tokens[12])
    scale_frequency = _parse(MFloat, tokens[13])
    scale_units = scale === missing ? _parse(MString, tokens[14]) : tokens[14]
    sample_rate = _parse(Float64, tokens[15])
    starttime = _parse(DateTime, tokens[16])
    endtime = _parse(DateTime, tokens[17])
    FDSNChannelTextResponse(network, station, location, channel,
        latitude, longitude, elevation, depth, azimuth, dip,
        sensor_description, scale, scale_frequency, scale_units,
        sample_rate, starttime, endtime)
end

"""
    FDSNEventTextResponse

Struct containing the fields returned from a `FDSNEvent` request where
the format is "text".
"""
struct FDSNEventTextResponse
    event_id::String
    time::DateTime
    latitude::Float64
    longitude::Float64
    depth::Float64
    author::String
    catalog::String
    contributor::String
    contributor_id::String
    mag_type::String
    magnitude::Float64
    mag_author::String
    event_location_name::String
    event_type::Union{Missing,String}
end

function Base.convert(::Type{FDSNEventTextResponse}, s::AbstractString)
    # Version 1.2 adds a 14th column, event_type.  Allow either 13 or 14 columns
    tokens = text_response_tokens_and_throw(FDSNEventTextResponse, s, (13, 14))
    event_id = tokens[1]
    time = _parse(DateTime, tokens[2])
    latitude = _parse(Float64, tokens[3])
    longitude = _parse(Float64, tokens[4])
    depth = _parse(Float64, tokens[5])
    author = tokens[6]
    catalog = tokens[7]
    contributor = tokens[8]
    contributor_id = tokens[9]
    mag_type = tokens[10]
    mangitude = _parse(Float64, tokens[11])
    mag_author = tokens[12]
    event_location_name = tokens[13]
    # Account for FDSNWS v1.1 and below lacking the event_type field
    event_type = length(tokens) == 14 ? tokens[14] : missing
    FDSNEventTextResponse(event_id, time, latitude, longitude, depth, author,
        catalog, contributor, contributor_id, mag_type, mangitude, mag_author,
        event_location_name, event_type)
end

"""
    ISFTextResponse

Struct containing **some of** the fields returned from a `FDSNEvent` request where
the format is "isf" (IASPEI Seismic Format, as returned by the ISC).
"""
struct ISFTextResponse
    isprime::Bool
    iscentroid::Bool

end


text:
using Struve
using Test

# using Table 2 and 3 from
# https://doi.org/10.1016/S0377-0427(01)00580-5
const h0_zeros = [
     4.3332378204,
     6.7810276399,
    10.4692052391,
    13.1404947133,
    16.6967131984,
    19.4599412437,
    22.9490276305,
    25.7653652428,
    29.2120126148,
    32.0639726967,
    # skipping 9 zeros here
    63.5189617208
]

@testset "H0 zeros" begin
    for z in h0_zeros
        @test abs(Struve.H0(z)) < 1e-10
    end
    @test !(abs(Struve.H0( 2.0)) < 1e-10)
    @test !(abs(Struve.H0(22.0)) < 1e-10)
    for z in h0_zeros
        @test abs(Struve.H(0, z)) < 1e-10
    end
    @test !(abs(Struve.H(0,  2.0)) < 1e-10)
    @test !(abs(Struve.H(0, 22.0)) < 1e-10)
    for z in h0_zeros
        @test abs(Struve.H0(z + 0im)) < 1e-10
    end
    @test !(abs(Struve.H0( 2.0 + 0im)) < 1e-10)
    @test !(abs(Struve.H0(22.0 + 0im)) < 1e-10)
    for z in h0_zeros
        @test_broken abs(Struve.H(0, z + 0im)) < 1e-10
    end
    @test !(abs(Struve.H(0,  2.0 + 0im)) < 1e-10)
    @test !(abs(Struve.H(0, 22.0 + 0im)) < 1e-10)
end


text:
# This file is auto-generated by AWSMetadata.jl
using AWS
using AWS.AWSServices: pricing
using AWS.Compat
using AWS.UUIDs

"""
    describe_services()
    describe_services(params::Dict{String,<:Any})

Returns the metadata for one service or a list of the metadata for all services. Use this
without a service code to get the service codes for all services. Use it with a service
code, such as AmazonEC2, to get information specific to that service, such as the attribute
names available for that service. For example, some of the attribute names available for
EC2 are volumeType, maxIopsVolume, operation, locationType, and instanceCapacity10xlarge.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"FormatVersion"`: The format version that you want the response to be in. Valid values
  are: aws_v1
- `"MaxResults"`: The maximum number of results that you want returned in the response.
- `"NextToken"`: The pagination token that indicates the next set of results that you want
  to retrieve.
- `"ServiceCode"`: The code for the service whose information you want to retrieve, such as
  AmazonEC2. You can use the ServiceCode to filter the results in a GetProducts call. To
  retrieve a list of all services, leave this blank.
"""
function describe_services(; aws_config::AbstractAWSConfig=global_aws_config())
    return pricing(
        "DescribeServices"; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET
    )
end
function describe_services(
    params::AbstractDict{String}; aws_config::AbstractAWSConfig=global_aws_config()
)
    return pricing(
        "DescribeServices", params; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET
    )
end

"""
    get_attribute_values(attribute_name, service_code)
    get_attribute_values(attribute_name, service_code, params::Dict{String,<:Any})

Returns a list of attribute values. Attibutes are similar to the details in a Price List
API offer file. For a list of available attributes, see Offer File Definitions in the
Amazon Web Services Billing and Cost Management User Guide.

# Arguments
- `attribute_name`: The name of the attribute that you want to retrieve the values for,
  such as volumeType.
- `service_code`: The service code for the service whose attributes you want to retrieve.
  For example, if you want the retrieve an EC2 attribute, use AmazonEC2.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"MaxResults"`: The maximum number of results to return in response.
- `"NextToken"`: The pagination token that indicates the next set of results that you want
  to retrieve.
"""
function get_attribute_values(
    AttributeName, ServiceCode; aws_config::AbstractAWSConfig=global_aws_config()
)
    return pricing(
        "GetAttributeValues",
        Dict{String,Any}("AttributeName" => AttributeName, "ServiceCode" => ServiceCode);
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function get_attribute_values(
    AttributeName,
    ServiceCode,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return pricing(
        "GetAttributeValues",
        Dict{String,Any}(
            mergewith(
                _merge,
                Dict{String,Any}(
                    "AttributeName" => AttributeName, "ServiceCode" => ServiceCode
                ),
                params,
            ),
        );
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    get_products()
    get_products(params::Dict{String,<:Any})

Returns a list of all products that match the filter criteria.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"Filters"`: The list of filters that limit the returned products. only products that
  match all filters are returned.
- `"FormatVersion"`: The format version that you want the response to be in. Valid values
  are: aws_v1
- `"MaxResults"`: The maximum number of results to return in the response.
- `"NextToken"`: The pagination token that indicates the next set of results that you want
  to retrieve.
- `"ServiceCode"`: The code for the service whose products you want to retrieve.
"""
function get_products(; aws_config::AbstractAWSConfig=global_aws_config())
    return pricing("GetProducts"; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
end
function get_products(
    params::AbstractDict{String}; aws_config::AbstractAWSConfig=global_aws_config()
)
    return pricing(
        "GetProducts", params; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET
    )
end


text:
function get_inverse(obj::DecompositionSolver)
    return jcall(obj, "getInverse", RealMatrix, ())
end

function is_non_singular(obj::DecompositionSolver)
    return jcall(obj, "isNonSingular", jboolean, ())
end

function solve(obj::DecompositionSolver, arg0::RealMatrix)
    return jcall(obj, "solve", RealMatrix, (RealMatrix,), arg0)
end

function solve(obj::DecompositionSolver, arg0::RealVector)
    return jcall(obj, "solve", RealVector, (RealVector,), arg0)
end


text:
using RosDataProcess
using PyPlot

dx = 0.01
# Test simple time series
a = TimeSeries(sin, -2pi:dx:6pi)
b = TimeSeries(cos, -3pi:dx:2pi)
c = TimeSeries(atan, -5pi:dx:4pi)
series = [a, b, c]
interpolated = intersect_interpolate(series)

# Optional plots to illustrate the above examples
#figure(); foreach(plot, series)
#figure(); plot(interpolated)

max_stars_repo_name: mcorah/RosDataProcess

max_issues_repo_name: mcorah/RosDataProcess

max_forks_repo_name: mcorah/RosDataProcess


text:
"""
    wernerState(dim :: Int, p ::Union{Int,Float64}) :: Matrix{Float64}

This function constructs the Werner states,
```math
    \\sigma_{d,p} = p \\frac{\\Pi_{0}}{d+1 \\choose 2} + (1-p) \\frac{\\Pi_{1}}{d \\choose 2}
```
where ``p \\in [0,1]`` and ``\\Pi_0, \\Pi_1`` are the projectors onto the symmetric and anti-symmetric
subspaces respectively. They can be determined by
```math
    \\Pi_0 = \\frac{1}{2} (I_{A} \\otimes I_{B} + \\mathbb{F}) \\hspace{1cm} \\Pi_1 = \\frac{1}{2}(I_{A} \\otimes I_{B} - \\mathbb{F})
```
where ``\\mathbb{F}`` is the swap operator.

A `DomainError` is thrown if:
* `d ≤ 1`
* `p` is not in range `0 ≤ p ≤ 1`
"""
function wernerState(d :: Int, p ::Union{Int,Float64}) :: Matrix{Float64}
    if d ≤ 1
        throw(DomainError(d,"wernerState requires the local dimension is two or greater."))
    elseif !(0 ≤ p ≤ 1)
        throw(DomainError(p,"wernerState requires p ∈ [0,1]."))
    end
    swap = swapOperator(d)
    Π0 = (I + swap)/2
    Π1 = (I - swap)/2
    return p * Π0 / binomial(d+1,2) + (1-p) * Π1 / binomial(d,2)
end

"""
    axisymmetricState(
        d :: Int64,
        x :: Union{Int,Float64},
        y :: Union{Int,Float64}
    ) :: Matrix{Float64}

Construct the axisymmetric state ``\\rho^{\\text{axi}}`` as described in section
IV.C. of [this paper](https://arxiv.org/pdf/1505.01833.pdf).
This state is a bipartite quantum state with each subspace having dimension `d`.
The diagonal of ``\\rho^{\\text{axi}}`` is parameterized as

```math
    \\rho^{\\text{axi}}_{jj,jj} = \\frac{1}{d^2}+a, \\quad \\text{and}\\quad \\rho^{\\text{axi}}_{jk,jk} = \\frac{1}{d^2}- \\frac{a}{d-1} \\; (j\\neq k)
```

while the off-diagonals are expressed as

```math
    \\rho^{\\text{axi}}_{jj,kk} = b,
```

where ``a = y\\frac{d-1}{d}`` and ``b = \\frac{x}{\\sqrt{d(d-1)}}``.
All remaining elements of ``\\rho^{\\text{axi}}`` are zero.
Inputs `x` and `y` of the `axisymmetricState` function parameterize ``\\rho^{\\text{axi}}``
and are constrained as:
* ``-\\frac{1}{d\\sqrt{d-1}} \\leq y \\leq \\frac{\\sqrt{d-1}}{d}``
* ``-\\frac{1}{\\sqrt{d(d-1)}}\\leq x \\leq \\sqrt{\\frac{d-1}{d}}``
* ``-\\frac{1}{\\sqrt{d}}\\left( y + \\frac{1}{d\\sqrt{d-1}}\\right) \\leq x \\leq \\frac{d-1}{\\sqrt{d}}\\left(y + \\frac{1}{d\\sqrt{d-1}} \\right)``

If any of the constraints above do not hold, a `DomainError` is thrown.
"""
function axisymmetricState(
    d :: Int64,
    x :: Union{Int,Float64},
    y :: Union{Int,Float64}
) :: Matrix{Float64}
    y_bounds = _axisymmetric_y_bounds(d)
    x_bounds = _axisymmetric_x_bounds(d)
    x_constraints = _axisymmetric_x_constraints(d,y)

    if !(y_bounds[1] ≤ y ≤ y_bounds[2])
        throw(DomainError(y, "input `y` does not satisfy `$(y_bounds[1]) ≤ y ≤ $(y_bounds[2])`."))
    elseif !(x_bounds[1] ≤ x ≤ x_bounds[2])
        throw(DomainError(x, "input `x` does not satisfy `$(x_bounds[1]) ≤ x ≤ $(x_bounds[2])`."))
    elseif !(x_constraints[1] ≤ x ≤ x_constraints[2])
        throw(DomainError(x, "input `x` does not satisfy `$(x_constraints[1]) ≤ x ≤ $(x_constraints[2])`."))
    elseif d ≤ 1
        throw(DomainError(d, "input `d` must satisfy `d ≥ 2`."))
    end

    a = y*sqrt(d-1)/d
    b = x/sqrt(d*(d-1))

    ρ_axi = zeros(Float64, d, d, d, d)
    for j in 1:d, k in 1:d
        if j == k
            ρ_axi[j,j,j,j] = a + 1/d^2
        else
            ρ_axi[j,k,j,k] = 1/d^2 - a/(d-1)
            ρ_axi[j,j,k,k] = b
        end
    end

    return reshape(ρ_axi, d^2, d^2)
end

# Helper for axisymmetricState, implements Eq. (42a) of https://arxiv.org/pdf/1505.01833.pdf
function _axisymmetric_y_bounds(d :: Int64) :: Tuple{Float64, Float64}
    return (-1/(d*sqrt(d-1)), sqrt(d-1)/d)
end

# Helper for axisymmetricState, implements Eq. (42b) of https://arxiv.org/pdf/1505.01833.pdf
function _axisymmetric_x_bounds(d :: Int64) :: Tuple{Float64, Float64}
    return (-1/sqrt(d*(d-1)), sqrt((d-1)/d))
end

# Helper for axisymmetricState, implements Eq. (43) of https://arxiv.org/pdf/1505.01833.pdf
function _axisymmetric_x_constraints(d,y) :: Tuple{Float64, Float64}
    lower = -1/sqrt(d)*(y + 1/(d*sqrt(d-1)))
    upper = (d-1)/sqrt(d)*(y+1/(d*sqrt(d-1)))

    return (lower, upper)
end

"""
    haarStates(n :: Int64, d :: Int64) :: Vector{Matrix{ComplexF64}}

Constructs a list of `n` random states on a `d`-dimensional Hilbert space
according to the Haar measure.
"""
function haarStates(n :: Int64, d :: Int64) :: Vector{Matrix{ComplexF64}}
    ρ0 = zeros(Float64, d, d)
    ρ0[1,1] = 1.0

    rand_unitaries = map(i -> random_unitary(d), 1:n)

    return map(U -> U*ρ0*U', rand_unitaries)
end

max_forks_repo_name: ChitambarLab/cv-channel


text:
mutable struct ConstraintData
    nb_cstr::Int
    indices::Vector{Vector{Int}}
    coeffs::Vector{Vector{Float64}}
    lb::Vector{Float64}
    ub::Vector{Float64}
end

function ConstraintData(m)
    res = ConstraintData(
            length(m.linconstr),
            [map(v-> getfield(v, :col), CSTR.terms.vars) for CSTR in m.linconstr],
            [CSTR.terms.coeffs for CSTR in m.linconstr],
            [CSTR.lb for CSTR in m.linconstr],
            [CSTR.ub for CSTR in m.linconstr])

    to_discard = find(x-> x<=0 || x==Inf, res.ub)

    if !isempty(to_discard)
        res.nb_cstr = res.nb_cstr - length(to_discard)
        deleteat!(res.indices, to_discard)
        deleteat!(res.coeffs, to_discard)
        deleteat!(res.lb, to_discard)
        deleteat!(res.ub, to_discard)
    end

    res
end

isacover(C, a, b) = sum(a[C]) > b

function extend(C, a)
    m = maximum(a[C])
    res = copy(C)
    for i in eachindex(a)
        if a[i] >= m && !(i in res)
            push!(res, i)
        end
    end
    sort!(res)
end

function isminimal(C, a, b)
    sum(a[C]) - minimum(a[C]) <= b
end

function isastrongcover(C, a, b)
    !isminimal(C, a, b) && return false
    EC = extend(C, a)
    length(EC) == length(a) && return true
    aij1 = maximum(a[C])
    aij2 = maximum(a[setdiff(1:length(a), EC)])
    return sum(a[C]) - aij1 + aij2 <= b
end

function isviolated(C, x, card)
    sum(x[C]) > card + 1e-3
end

function separateHeur(x, a, b)
    order = sortperm((1 .- x)./ a)
    C = Int[]
    astar = b
    while !isempty(order)
        p = popfirst!(order)
        a[p] <= astar && push!(C, p)
        isempty(C) && return (Int[], 0)
        if isacover(C, a, b)
            ECI = extend(C, a)
            if isviolated(ECI, x, length(C)-1)
                return (C, length(C)-1)
            else
                kstar = argmax(a[C])
                astar = a[kstar]
                deleteat!(C, kstar)
            end
        end
    end
    Int[], 0
end

function find_cover_cuts(n::Node, cstrData, lift_covers)
    success = false
    for i = 1:cstrData.nb_cstr
        inds = cstrData.indices[i] #indices of variables involved in the constraint
        x = view(n.m.colVal, inds) #value of the variables in the current solution
        a = cstrData.coeffs[i] #coefficients of the variables in the current constraint
        b = cstrData.ub[i] #RHS of the constraint

        if dot(x, a) > b - 0.5 #if the constraint is activated
            C, card = separateHeur(x, a, b) #try to find a cover
            if !isempty(C) #if success, add it to the list of cover cuts
                success = true
                if lift_covers
                    if isastrongcover(C, a, b)
                        res = lifting_procedure(C, a, b, card)
                        @constraint(n.m, dot([JuMP.Variable(n.m, j) for j in inds], res) <= card)
                    else
                        if lifting_procedure_M2!(C, a, b)
                            res = lifting_procedure(C, a, b, card)
                            @constraint(n.m, dot([JuMP.Variable(n.m, j) for j in inds], res) <= card)
                        else
                            @constraint(n.m, sum(JuMP.Variable(n.m, j) for j in inds[extend(C, a)]) <= card)
                        end
                    end
                else
                    @constraint(n.m, sum(JuMP.Variable(n.m, j) for j in inds[extend(C, a)]) <= card)
                end
            end
        end
    end
   success
end

function find_cover_cuts(n::NodeParragh, cstrData, lift_covers)
    cuts = Set{Tuple{Vector{Int}, Int}}()
    lifted_cuts = Set{Tuple{Vector{Int}, Vector{Int}, Int}}()
    success = false
    for i = 1:cstrData.nb_cstr
        inds = cstrData.indices[i] #indices of variables involved in the constraint
        a = cstrData.coeffs[i] #coefficients of the variables in the current constraint
        b = cstrData.ub[i] #RHS of the constraint

        for ind_x in 1:length(n.x) #for each solution of the convex linear relaxation
            x =  view(n.x[ind_x], inds) #value of the variables in the solution

            if dot(x, a) > b - 0.5 #if the constraint is activated
                C, card = separateHeur(x, a, b) #try to find a cover
                if !isempty(C) #if success, add it to the list of cover cuts
                    success = true
                    if lift_covers
                        if isastrongcover(C, a, b)
                            res = lifting_procedure(C,  a, b, card)
                            push!(lifted_cuts, (inds, res, card))
                        else
                            if lifting_procedure_M2!(C, a, b)
                                res = lifting_procedure(C, a, b, card)
                            else
                                push!(cuts, (inds[extend(C, a)], card))
                            end
                        end
                        # @constraint(n.m, dot([JuMP.Variable(n.m, j) for j in inds], res) <= card)
                    else
                        push!(cuts, (inds[extend(C, a)], card))
                        # @constraint(n.m, sum(JuMP.Variable(n.m, j) for j in inds[extend(C, a)]) <= card)
                    end
                end
            end
        end
    end
    if success
        for (cut, card) in cuts
            @constraint(n.m, sum(JuMP.Variable(n.m, k) for k in cut) <= card)
        end
        for (inds, cut, card) in lifted_cuts
            @constraint(n.m, dot([JuMP.Variable(n.m, k) for k in inds], cut) <= card)
        end
    end
   success
end

function lifting_procedure(C, a, b, card)
    sorted_Ci = sort(C, by = x->a[x], rev=true)
    sorted_ECi = sort(extend(C, a), by = x->a[x], rev=true)
    q = card
    n0 = setdiff(1:length(a), sorted_ECi)
    aj = ones(Int, length(a))
    aj[n0] .= 0

    lb = a[sorted_Ci[1]]
    ub = lb + a[sorted_Ci[2]]
    for h = 2:q
        lb += a[sorted_Ci[h]]
        ub += a[sorted_Ci[h+1]]
        maximum(a) < lb && break
        nh = find(x-> lb <= x < ub, a)
        aj[nh] .= h
    end

    return aj

end

function lifting_procedure_M2!(C, a, b)
    EC = extend(C, a)
    N_minus_EC = setdiff(1:length(a), EC)
    isempty(N_minus_EC) && return false
    aij1, j1 = findmax(a[C])
    aij2 = maximum(a[N_minus_EC])
    temp = copy(a)
    Δ = sum(a[C]) - aij1 + aij2 - b
    while Δ > 0
        deleteat!(C, j1)
        copyto!(temp, a)
        temp[EC] .= typemax(Int)
        temp[temp .<= (aij2 - Δ)] .= typemax(Int)
        j = indmin(temp)
        push!(C, j) ; sort!(C)
        EC = extend(C, a)
        if length(EC) == length(a)
            isastrongcover(C, a, b) && return true
            return false
        end
        N_minus_EC = setdiff(1:length(a), EC)
        aij1, j1 = findmax(a[C])
        aij2 = maximum(a[N_minus_EC])
        Δ = sum(a[C]) - aij1 + aij2 - b
    end
    true
end


text:
###
# Creating an archive
#
# To create an archive in C:
#     1. Ask `archive_write_new` for an archive writer object.
#     2. Set any global properties. In particular, you should set
#        the compression and format to use.
#     3. Call `archive_write_open` to open the file (most people
#         will use `archive_write_open_file` or `archive_write_open_fd`,
#         which provide convenient canned I/O callbacks for you).
#     4. For each entry:
#         * construct an appropriate `struct archive_entry` structure
#         * `archive_write_header` to write the header
#         * `archive_write_data` to write the entry data
#     5. `archive_write_close` to close the output
#     6. `archive_write_free` to cleanup the writer and release resources
#
# To create an archive in Julia:
#     1. Create a LibArchive.Writer
#     2. Set any global properties. In particular, you should set
#        the compression and format to use.
#     3. Write each entries. LibArchive.jl will call libarchive
#        open functions if needed.
#     4. Call LibArchive.free to end processing.

abstract type WriterData end

mutable struct Writer{T<:WriterData} <: Archive
    data::T
    ptr::Ptr{Cvoid}
    opened::Bool
    passphrase_mgr
    function Writer{T}(data::T) where T
        ptr = ccall((:archive_write_new, libarchive), Ptr{Cvoid}, ())
        ptr == C_NULL && throw(OutOfMemoryError())
        obj = new(data, ptr, false)
        finalizer(obj, free)
        obj
    end
end

Writer(data::T) where {T<:WriterData} = Writer{T}(data)

function free(archive::Writer)
    ptr = archive.ptr
    ptr == C_NULL && return
    ccall((:archive_write_free, libarchive), Cint, (Ptr{Cvoid},), ptr)
    archive.ptr = C_NULL
    nothing
end

Base.close(archive::Writer) =
    @_la_call(archive_write_close, (Ptr{Cvoid},), archive)

function Base.cconvert(::Type{Ptr{Cvoid}}, archive::Writer)
    archive.ptr == C_NULL && error("archive already freed")
    archive
end
Base.unsafe_convert(::Type{Ptr{Cvoid}}, archive::Writer) = archive.ptr

function ensure_open(archive::Writer)
    archive.opened && return
    archive.opened = true
    try
        do_open(archive)
    catch
        archive.opened = false
        rethrow()
    end
    nothing
end

set_bytes_per_block(archive::Writer, bytes_per_block) =
    @_la_call(archive_write_set_bytes_per_block, (Ptr{Cvoid}, Cint),
              archive, bytes_per_block)
get_bytes_per_block(archive::Writer) =
    ccall((:archive_write_get_bytes_per_block, libarchive),
          Cint, (Ptr{Cvoid},), archive)

set_bytes_in_last_block(archive::Writer, bytes_in_last_block) =
    @_la_call(archive_write_set_bytes_in_last_block, (Ptr{Cvoid}, Cint),
              archive, bytes_in_last_block)
get_bytes_in_last_block(archive::Writer) =
    ccall((:archive_write_get_bytes_in_last_block, libarchive),
          Cint, (Ptr{Cvoid},), archive)

"""
The dev/ino of a file that won't be archived. This is used
to avoid recursively adding an archive to itself.
"""
set_skip_file(archive::Writer, dev, ino) =
    @_la_call(archive_write_set_skip_file, (Ptr{Cvoid}, Int64, Int64),
              archive, dev, ino)

###
# Open filename

struct WriteFileName{T} <: WriterData
    name::T
end

Writer(fname::AbstractString) = Writer(WriteFileName(fname))
Writer() = Writer(WriteFileName(nothing))

function do_open(archive::Writer{WriteFileName{T}}) where T
    data = archive.data
    @_la_call(archive_write_open_filename, (Ptr{Cvoid}, Cstring),
              archive, data.name)
end

function do_open(archive::Writer{WriteFileName{Cvoid}})
    @_la_call(archive_write_open_filename, (Ptr{Cvoid}, Ptr{Cvoid}),
              archive, C_NULL)
end

struct WriteFD <: WriterData
    fd::Cint
end

Writer(fd::Integer) = Writer(WriteFD(Cint(fd)))

function do_open(archive::Writer{WriteFD})
    data = archive.data
    @_la_call(archive_write_open_fd, (Ptr{Cvoid}, Cint), archive, data.fd)
end

struct WriteFILE <: WriterData
    file::Libc.FILE
end

Writer(file::Libc.FILE) = Writer(WriteFILE(file))

function do_open(archive::Writer{WriteFILE})
    data = archive.data
    @_la_call(archive_write_open_FILE, (Ptr{Cvoid}, Ptr{Cvoid}), archive, data.file)
end

###
# Open memory

struct WriteMemory{T,TO} <: WriterData
    ptr::T
    obj::TO # Reference for GC
    size::Csize_t
    used::typeof(Ref{Csize_t}())
end

Writer(ptr::T, size, obj::TO=nothing) where {T<:Ptr,TO} =
    Writer(WriteMemory{T,TO}(ptr, obj, size, Ref(Csize_t(0))))
Writer(ary::Vector, size=sizeof(ary)) = Writer(pointer(ary), size, ary)

Base.unsafe_convert(::Type{Ptr{Cvoid}}, mem_data::WriteMemory) = mem_data.ptr

function do_open(archive::Writer{T}) where {T<:WriteMemory}
    data = archive.data
    @_la_call(archive_write_open_memory,
              (Ptr{Cvoid}, Ptr{Cvoid}, Csize_t, Ptr{Csize_t}),
              archive, data, data.size, data.used)
end

get_used(archive::Writer{T}) where {T<:WriteMemory} = archive.data.used[]

###
# Generic writer

struct GenericWriteData{T} <: WriterData
    data::T
end

Writer(data::T) where {T} = Writer(GenericWriteData{T}(data))

writer_open(archive::Writer, data) = nothing
function writer_writebytes end
writer_close(archive::Writer, data) = nothing

function writer_open_callback(c_archive, archive)
    try
        clear_error(archive)
        writer_open(archive, archive.data.data)
        return Libc.errno(archive) == 0 ? Cint(0) : Status.WARN
    catch ex
        return set_exception(archive, ex)
    end
end

function writer_write_callback(c_archive, archive, buff::Ptr{Cvoid},
                               length::Csize_t)
    try
        clear_error(archive)
        ary = unsafe_wrap(Array, Ptr{UInt8}(buff), length)
        return Cssize_t(writer_writebytes(archive, archive.data.data, ary))
    catch ex
        set_exception(archive, ex)
        return Cssize_t(-1)
    end
end

function writer_close_callback(c_archive, archive)
    try
        clear_error(archive)
        writer_close(archive, archive.data.data)
        return Libc.errno(archive) == 0 ? Cint(0) : Status.WARN
    catch ex
        return set_exception(archive, ex)
    end
end

writer_writebytes(archive, io::IO, ary) = write(io, ary)

function do_open(archive::Writer{T}) where {T<:GenericWriteData}
    @_la_call(archive_write_open,
              (Ptr{Cvoid}, Any, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
              archive, archive,
              @to_open_callback(writer_open_callback, Writer{T}),
              @to_write_callback(writer_write_callback, Writer{T}),
              @to_close_callback(writer_close_callback, Writer{T}))
end

function write_header(archive::Writer, entry::Entry)
    ensure_open(archive)
    @_la_call(archive_write_header, (Ptr{Cvoid}, Ptr{Cvoid}), archive, entry)
end

@inline function unsafe_archive_write(archive::Writer, buf, len)
    ensure_open(archive)
    nb = ccall((:archive_write_data, libarchive),
               Cssize_t, (Ptr{Cvoid}, Ptr{Cvoid}, Csize_t), archive, buf, len)
    nb < 0 && _la_error(Cint(nb), archive)
    nb % Csize_t
end

unsafe_write(archive::Writer, buf::Ptr{UInt8}, len::UInt) =
    unsafe_archive_write(archive, buf, len)
Base.write(archive::Writer, c::UInt8) =
    unsafe_archive_write(archive, Ref(c), 1)

function finish_entry(archive::Writer)
    ensure_open(archive)
    @_la_call(archive_write_finish_entry, (Ptr{Cvoid},), archive)
end

"""
Marks the archive as FATAL so that a subsequent free() operation
won't try to close() cleanly.  Provides a fast abort capability
when the client discovers that things have gone wrong.
"""
write_fail(archive::Writer) =
    @_la_call(archive_write_fail, (Ptr{Cvoid},), archive)

Writer(f::Function) = archive_guard(f, Writer())
Writer(f::Function, args...; kws...) =
    archive_guard(f, Writer(args...; kws...))

set_passphrase(writer::Writer, passphrase::AbstractString) =
    @_la_call(archive_write_set_passphrase, (Ptr{Cvoid}, Cstring), writer, passphrase)
function set_passphrase(writer::Writer, cb::Cb) where Cb
    mgr = PassphraseMgr(cb)
    fptr = @cfunction(_passphrase_cb, Ptr{UInt8}, (Ptr{Cvoid},Ref{PassphraseMgr{Cb}}))
    GC.@preserve mgr begin
        @_la_call(archive_write_set_passphrase_callback,
                  (Ptr{Cvoid}, Ref{PassphraseMgr{Cb}}, Ptr{Cvoid}),
                  writer, mgr, fptr)
    end
    writer.passphrase_mgr = mgr
    return
end

set_format_option(writer::Writer, m::AbstractString, o::AbstractString, v::AbstractString) =
    @_la_call(archive_write_set_format_option,
              (Ptr{Cvoid}, Cstring, Cstring, Cstring), writer, m, o, v)
set_filter_option(writer::Writer, m::AbstractString, o::AbstractString, v::AbstractString) =
    @_la_call(archive_write_set_filter_option,
              (Ptr{Cvoid}, Cstring, Cstring, Cstring), writer, m, o, v)
set_option(writer::Writer, m::AbstractString, o::AbstractString, v::AbstractString) =
    @_la_call(archive_write_set_option,
              (Ptr{Cvoid}, Cstring, Cstring, Cstring), writer, m, o, v)
set_options(writer::Writer, opts::AbstractString) =
    @_la_call(archive_write_set_options, (Ptr{Cvoid}, Cstring), writer, opts)

# TODO
# /* This interface is currently only available for archive_write_disk handles.  */
# ssize_t archive_write_data_block(struct archive *,
#                                  const void *, size_t, int64_t);


text:
struct MPIError <: Exception
    code::Cint
end

macro mpichk(expr)
    expr = macroexpand(@__MODULE__, :(@mpicall($expr)))
    :((errcode = $(esc(expr))) == MPI_SUCCESS || throw(MPIError(errcode)))
end

function error_string(err::MPIError)
    len_ref = Ref{Cint}()
    str_buf = Vector{UInt8}(undef, MPI_MAX_ERROR_STRING)
    # int MPI_Error_string(int errorcode, char *string, int *resultlen)
    @mpichk ccall(:MPI_Error_string, Cint,
                  (Cint, Ptr{UInt8}, Ref{Cint}), err.code, str_buf, len_ref)
    resize!(str_buf, len_ref[])
    return String(str_buf)
end

function Base.show(io::IO, err::MPIError)
    print(io, "MPIError(", err.code, "): ", error_string(err))
end