Skip to content

Commit

Permalink
handle pairs with descriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaqz committed Jul 9, 2022
1 parent 6ead575 commit 2e7cc76
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 93 deletions.
10 changes: 6 additions & 4 deletions BaseInterfaces/src/BaseInterfaces.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ export IterationInterface

include("iteration.jl")

@implements IterationInterface{(:reverse,:indexing,)} StepRange 1:2:10
@implements IterationInterface{(:reverse,:indexing,)} Array [1, 2, 3, 4]
@implements IterationInterface{(:reverse,)} Base.Generator (i for i in 1:5)
@implements IterationInterface{(:reverse,:indexing,)} Tuple (1, 2, 3, 4)
# Some example interface delarations.
@implements IterationInterface{(:reverse,:indexing,)} UnitRange [1:5, -2:2]
@implements IterationInterface{(:reverse,:indexing,)} StepRange [1:2:10, 20:-10:-20]
@implements IterationInterface{(:reverse,:indexing,)} Array [[1, 2, 3, 4], [:a :b; :c :d]]
@implements IterationInterface{(:reverse,)} Base.Generator [(i for i in 1:5), (i for i in 1:5)]
@implements IterationInterface{(:reverse,:indexing,)} Tuple [(1, 2, 3, 4)]

end
94 changes: 52 additions & 42 deletions BaseInterfaces/src/iteration.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#=
From the Base julia interface docs:
Required methods Brief description
iterate(iter) Returns either a tuple of the first item and initial state or nothing if empty
iterate(iter, state) Returns either a tuple of the next item and next state or nothing if no items remain
Expand All @@ -21,60 +23,68 @@ HasEltype() eltype(IterType)
EltypeUnknown() (none)
=#


@interface IterationInterface (
# Mandatory conditions: these must be met by all types
# that implement the interface.
mandatory = (
# :iterate returns a Tuple of anonymous functions
# that will each be tested for some object `x`.
iterate = (
x -> !isnothing(iterate(x)),
x -> !isnothing(iterate(iterate(x))),
x -> iterate(x) isa Tuple,
x -> iterate(x, last(iterate(x))) isa Tuple,
"test objects must be longer than 1" => x -> !isnothing(iterate(x)),
"test objects must be longer than 2" => x -> !isnothing(iterate(iterate(x))),
"iterate must return a tuple" => x -> iterate(x) isa Tuple,
"iteration on the last `iterate` output works" => x -> iterate(x, last(iterate(x))) isa Tuple,
),

#=
Base.IteratorSize allows return values of
`HasLength()`, `HasShape{N}()`, `IsInfinite()`, or `SizeUnknown()`.
`HasLength()` is the default. This means that by default `length`
must be defined for the object. If `HasShape{N}()` is returned, `length` and
`size` must be defined`.
TODO: use Invariants.jl for this
=#

# haslength = (
# x -> IteratorSize(x) == Base.HasLength(),
# x -> length(x) isa Integer,
# )
# hasshape = (
# x -> IteratorSize(x) == isa Base.HasShape
# x -> length(x) isa Integer,
# x -> size(x) isa NTuple{<:Any,<:Integer},
# x -> length(size(x)) == typeof(IteratorSize(x)).parameters[1],
# x -> length(x) == prod(size(x)),
# )
# isinfinie = x -> IteratorSize(x) == isa Base.IsInfinite(),
# sizeunknown = x -> IteratorSize(x) == isa Base.SizeUnknown(),
# eltype = x -> begin
# trait = Base.IteratorEltype(x)
# if trait isa Base.HasEltype
# eltype(x) == typeof(first(x))
# else trait isa Base.EltypeUnknown || error("IteratorEltype(x) must return `HasEltype` or `EltypeUnknown`")
# true
# end
# end,
# :size demonstrates an interface condition that instead of return a Bool,
# returns a Tuple of functions to run for `x` depending on the IteratorSize
# trait.
size = x -> begin
sizetrait = Base.IteratorSize(typeof(x))
if sizetrait isa Base.HasLength
return (
"`length(x)` returns an Integer for HasLength objects" => x -> length(x) isa Integer,
)
elseif sizetrait isa Base.HasShape
return (
"`length(x)` returns an Integer for HasShape objects" => x -> length(x) isa Integer,
"`size(x)` returns a Tuple of Integer for HasShape objects" => x -> size(x) isa NTuple{<:Any,<:Integer},
"`size(x)` returns a Tuple of length `N` matching `HasShape{N}`" => x -> length(size(x)) == typeof(sizetrait).parameters[1],
"`length(x)` is the product of `size(x)` for `HasShape` objects" => x -> length(x) == prod(size(x)),
)
elseif sizetrait isa Base.IsInfinite
return true
elseif sizetrait isa Base.SizeUnknown
return true
else
error("IteratorSize returns $sizetrait, allowed options are: `HasLength`, `HasLength`, `IsInfinite`, `SizeUnknown`")
end
end,
eltype = x -> begin
eltypetrait = Base.IteratorEltype(x)
if eltypetrait isa Base.HasEltype
x -> typeof(first(x)) <: eltype(x)
elseif eltypetrait isa Base.EltypeUnknown
true
else
error("IteratorEltype(x) returns $eltypetrait, allowed options are `HasEltype` or `EltypeUnknown`")
end
end,
),

# Optional conditions. These should be specified in the
# interface type if an object implements them: IterationInterface{(:reverse,:indexing)}
optional = (
reverse = x -> collect(Iterators.reverse(x)) == reverse(collect(x)),
# :reverse returns a single function to test Iterators.reverse
reverse = "`Iterators.reverse` gives reverse iteration" => x -> collect(Iterators.reverse(x)) == reverse(collect(x)),
#=
:indexing returns three condition functions.
We force the implementation of `firstindex` and `lastindex`
Or it is hard to test `getindex` generically
=#
indexing = (
x -> firstindex(x) isa Integer,
x -> lastindex(x) isa Integer,
x -> getindex(x, firstindex(x)) == first(iterate(x)),
"`firstindex` returns an Integer" => x -> firstindex(x) isa Integer,
"`lastindex` returns an Integer" => x -> lastindex(x) isa Integer,
"`getindex(x, firstindex(x))` returns the first value of `iterate(x)`" => x -> getindex(x, firstindex(x)) == first(iterate(x)),
),
)
)
Expand Down
1 change: 1 addition & 0 deletions BaseInterfaces/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ using Interfaces
using Test

@testset "BaseInterfaces.jl" begin
@test Interfaces.test(IterationInterface, UnitRange)
@test Interfaces.test(IterationInterface, StepRange)
@test Interfaces.test(IterationInterface, Array)
@test Interfaces.test(IterationInterface, Base.Generator)
Expand Down
41 changes: 31 additions & 10 deletions src/implements.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,35 @@ function implements end
implements(::Type{<:Interface}, obj) = false

"""
test_object(::Type{<:Interface}, ::Type)
test_objects(::Type{<:Interface}, ::Type)
Return the test object for an `Interface` and type.
"""
function test_object end
function test_objects end

# Wrap objects so we don't get confused iterating
# inside the objects themselves during tests.
struct TestObjectWrapper{O}
objects::O
end

Base.iterate(tow::TestObjectWrapper, args...) = iterate(tow.objects, args...)
Base.length(tow::TestObjectWrapper, args...) = length(tow.objects)
Base.getindex(tow::TestObjectWrapper, i::Int) = getindex(tow.objects, i)

"""
@implements(interface, objtype, obj)
@implements(dev, interface, objtype, obj)
Declare that an interface implements an interface, or
multipleinterfaces.
Declare that an interface implements an interface, or multipleinterfaces.
Also pass an object or tuple of objects to test it with.
The macro can only be used once per module for any one type.
To define multiple interfaces a type implements, combine them
in square brackets.
The macro can only be used once per module for any one type. To define
multiple interfaces a type implements, combine them in square brackets.
Passing the keyword `dev` as the first argument lets us show test output during development.
Do not use `dev` in production code, or output will appear during package precompilation.
# Example
Expand All @@ -46,14 +58,23 @@ using BaseInterfaces
@implements BaseInterfaces.IterationInterface{(:indexing,:reverse)} MyObject MyObject([1, 2, 3])
```
"""
macro implements(interface, objtype, obj)
macro implements(interface, objtype, test_objects)
_implements_inner(interface, objtype, test_objects)
end
macro implements(dev::Symbol, interface, objtype, test_objects)
dev == :dev || error("4 arg version of `@implements must start with `dev`, and should only be used in testing")
_implements_inner(interface, objtype, test_objects; show=true)
end
function _implements_inner(interface, objtype, test_objects; show=false)
if interface isa Expr && interface.head == :curly
interfacetype = interface.args[1]
optional_keys = interface.args[2]
else
interfacetype = interface
optional_keys = ()
end
test_objects.head == :vect || error("test object must be wrapped in square brackets")
test_objects = Expr(:tuple, test_objects.args...)
quote
# Define a `implements` trait stating that `objtype` implements `interface`
Interfaces.implements(::Type{<:$interfacetype}, ::Type{<:$objtype}) = true
Expand All @@ -62,9 +83,9 @@ macro implements(interface, objtype, obj)
# Define which optional components the object implements
Interfaces.optional_keys(::Type{<:$interfacetype}, ::Type{<:$objtype}) = $optional_keys
# Define the object to be used in interface tests
Interfaces.test_object(::Type{<:$interfacetype}, ::Type{<:$objtype}) = $obj
Interfaces.test_objects(::Type{<:$interfacetype}, ::Type{<:$objtype}) = Interfaces.TestObjectWrapper($test_objects)
# Run tests during precompilation
Interfaces.test($interface, $objtype; show=false)
Interfaces.test($interface, $objtype; show=$show)
end |> esc
end

Expand Down
74 changes: 58 additions & 16 deletions src/test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,29 @@ returning `true` or `false`.
If no interface type is passed, Interfaces.jl will find all the
interfaces available and test them.
"""
function test(T::Type{<:Interface{Keys}}, O::Type; kw...) where Keys
function test(T::Type{<:Interface{Keys}}, O::Type; kw...) where Keys
T1 = _get_type(T).name.wrapper
obj = test_object(T1, O)
return test(T1, obj; keys=Keys, _O=O, kw...)
objs = test_objects(T1, O)
return test(T1, O, objs; keys=Keys, kw...)
end
function test(T::Type{<:Interface}, O::Type; kw...)
obj = test_object(T, O)
return test(T, obj; _O=O, kw...)
objs = test_objects(T, O)
return test(T, O, objs; kw...)
end
function test(T::Type{<:Interface}, obj; show=true, keys=nothing, _O=typeof(obj))
if show
function test(T::Type{<:Interface}, O::Type, objs::TestObjectWrapper;
show=true, keys=nothing
)
if show
print("Testing ")
printstyled(_get_type(T).name.name; color=:blue)
print(" is implemented for ")
printstyled(_O, "\n"; color=:blue)
printstyled(O, "\n"; color=:blue)
end
if isnothing(keys)
optional = NamedTuple{optional_keys(T, obj)}(components(T).optional)
mandatory_results = _test(components(T).mandatory, obj)
optional_results = _test(optional, obj)
if show
optional = NamedTuple{optional_keys(T, O)}(components(T).optional)
mandatory_results = _test(components(T).mandatory, objs)
optional_results = _test(optional, objs)
if show
_showresults(mandatory_results, "Mandatory components")
_showresults(optional_results, "Optional components")
end
Expand All @@ -37,16 +39,50 @@ function test(T::Type{<:Interface}, obj; show=true, keys=nothing, _O=typeof(obj)
else
allcomponents = merge(components(T)...)
optional = NamedTuple{_as_tuple(keys)}(allcomponents)
results = _test(optional, obj)
results = _test(optional, objs)
show && _showresults(results, "Specified components")
println()
return all(_bool(results))
end
end
# Convenience method for users to test a single object
test(T::Type{<:Interface}, obj; kw...) =
test(T, typeof(obj), TestObjectWrapper((obj,)); kw...)

_test(tests::NamedTuple, obj) = map(t -> _test(t, obj), tests)
_test(condition::Tuple, obj) = map(c -> _test(c, obj), condition)
_test(condition, obj) = condition(obj)
function _test(tests::NamedTuple{K}, objs::TestObjectWrapper) where K
map(keys(tests), values(tests)) do k, v
_test(k, v, objs)
end |> NamedTuple{K}
end
function _test(name::Symbol, condition::Tuple, objs, i=nothing)
map(condition, ntuple(identity, length(condition))) do c, i
_test(name, c, objs, i)
end
end
function _test(name::Symbol, condition::Tuple, objs::TestObjectWrapper, i=nothing)
map(condition, ntuple(identity, length(condition))) do c, i
_test(name, c, objs, i)
end
end
function _test(name::Symbol, condition, objs::TestObjectWrapper, i=nothing)
map(o -> _test(name, condition, o, i), objs.objects)
end
function _test(name::Symbol, condition, obj, i=nothing)
try
res = condition isa Pair ? condition[2](obj) : condition(obj)
# Allow returning a function or tuple of functions that are tested again
if res isa Union{Pair,Tuple,Base.Callable}
return _test(name, res, obj, i)
else
return condition isa Pair ? condition[1] => res : res
end
catch e
num = isnothing(i) ? "" : ", condition $i"
desc = condition isa Pair ? string(" \"", condition[1], "\"") : ""
@warn "interface test :$name$num$desc failed for test object $obj"
rethrow(e)
end
end

function _showresults(results::NamedTuple, title::String)
printstyled(title; color=:light_black)
Expand All @@ -59,6 +95,11 @@ function _showresults(results::NamedTuple, title::String)
end

_showresult(key, res) = show(res)
function _showresult(key, pair::Pair)
desc, res = pair
print(desc, ": ")
printstyled(res; color=(res ? :green : :red))
end
_showresult(key, res::Bool) = printstyled(res; color=(res ? :green : :red))
function _showresult(key, res::NTuple{<:Any,Bool})
_showresult(key, first(res))
Expand All @@ -71,6 +112,7 @@ function _showresult(key, res::NTuple{<:Any})
end

_bool(xs::Union{Tuple,NamedTuple,AbstractArray}) = all(map(_bool, xs))
_bool(x::Pair) = x[2]
_bool(x::Bool) = x
_bool(x) = convert(Bool, x)

Expand Down
Loading

2 comments on commit 2e7cc76

@rafaqz
Copy link
Owner Author

@rafaqz rafaqz commented on 2e7cc76 Jul 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/63917

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.0 -m "<description of version>" 2e7cc76e020854ce5ec642ce2a1aff3f39532b6e
git push origin v0.1.0

Please sign in to comment.