Skip to content

Commit

Permalink
add some more interfaces to BaseInterfaces.jl (#27)
Browse files Browse the repository at this point in the history
* add some more interfaces

* fix set

* dont show iteration tests

* copy objects in testing

* more interface tests

* test and document BaseInterfaces in CI

* use local path

* add BaseInterfaces docs page

* dont dev the project

* no Pairs

* fix md path

* add readme to BaseInterfaces

* test Base.isiterable

* add supertypes

* updates

* more comments and fixes from review

* fix Dicts

* fix args

* fix basics tests

* fix for 1.6
  • Loading branch information
rafaqz authored Nov 1, 2023
1 parent b23bfa5 commit bd13c15
Show file tree
Hide file tree
Showing 18 changed files with 572 additions and 141 deletions.
25 changes: 4 additions & 21 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
matrix:
version:
- '1.6'
- '1.7'
- '1'
- 'nightly'
os:
- ubuntu-latest
Expand All @@ -35,27 +35,10 @@ jobs:
- uses: julia-actions/cache@v1
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- name: Run BaseInterfaces tests
run: julia --project=BaseInterfaces -e 'using Pkg; pkg"dev ."; pkg"update"; pkg"test"'
shell: bash
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v2
with:
files: lcov.info
docs:
name: Documentation
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v2
- uses: julia-actions/setup-julia@v1
with:
version: '1'
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-docdeploy@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
julia --project=docs -e '
using Documenter: DocMeta, doctest
using Interfaces
DocMeta.setdocmeta!(Interfaces, :DocTestSetup, :(using Interfaces); recursive=true)
doctest(Interfaces)'
27 changes: 27 additions & 0 deletions .github/workflows/Documentation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Documentation

on:
push:
branches:
- main # update to match your development branch (master, main, dev, trunk, ...)
tags: '*'
pull_request:

jobs:
build:
permissions:
contents: write
statuses: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v1
with:
version: '1'
- name: Install dependencies
run: julia --project=docs/ -e 'using Pkg; pkg"dev ."; pkg"dev ./BaseInterfaces"; Pkg.instantiate()'
- name: Build and deploy
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key
run: julia --project=docs/ docs/make.jl
38 changes: 34 additions & 4 deletions BaseInterfaces/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
# BaseInterfaces

[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://rafaqz.github.io/BaseInterfaces.jl/stable/)
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://rafaqz.github.io/BaseInterfaces.jl/dev/)
[![Build Status](https://github.com/rafaqz/BaseInterfaces.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/rafaqz/BaseInterfaces.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Coverage](https://codecov.io/gh/rafaqz/BaseInterfaces.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/rafaqz/BaseInterfaces.jl)
[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://rafaqz.github.io/Interfaces.jl/stable/)
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://rafaqz.github.io/Interfaces.jl/dev/)
[![Build Status](https://github.com/rafaqz/Interfaces.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/rafaqz/Interfaces.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Coverage](https://codecov.io/gh/rafaqz/Interfaces.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/rafaqz/Interfaces.jl)

BaseInterfaces.jl is a subpackage of Interfaces.jl that provides predifined
definition and testing for Base Julia interfaces.

Currently this includes:
- A general iteration interface: `IterationInterface`
- `AbstractArray` interface: `ArrayInterface`
- `AbstractSet` interface: `SetInterface`
- `AbstractDict` interface: `DictInterface`


Testing your object follows the interfaces is as simple as:

```julia
using BaseInterfaces, Interfaces
Interfaces.tests(DictInterface, MyDict, [mydict1, mydict2, ...])
```

Declaring that it follows the interface is done with:

```julia
@implements DictInterface{(:component1, :component2)} MyDict
```

Where components can be chosen from `Interfaces.optional_keys(DictInterface)`.

See [the docs](https://rafaqz.github.io/Interfaces.jl/stable/) for use.

If you want to add more Base julia interfaces here, or think the existing
ones could be improved, please make an issue or pull request.
12 changes: 5 additions & 7 deletions BaseInterfaces/src/BaseInterfaces.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ module BaseInterfaces

using Interfaces

export IterationInterface
export ArrayInterface, DictInterface, IterationInterface, SetInterface

include("iteration.jl")
include("dict.jl")
include("set.jl")
include("array.jl")

# Some example interface delarations.
@implements IterationInterface{(:reverse,:indexing,)} UnitRange
@implements IterationInterface{(:reverse,:indexing,)} StepRange
@implements IterationInterface{(:reverse,:indexing,)} Array
@implements IterationInterface{(:reverse,)} Base.Generator
@implements IterationInterface{(:reverse,:indexing,)} Tuple
include("implementations.jl")

end
129 changes: 129 additions & 0 deletions BaseInterfaces/src/array.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#= Abstract Arrays
https://docs.julialang.org/en/v1/manual/interfaces/
Methods to implement Brief description
size(A) Returns a tuple containing the dimensions of A
getindex(A, i::Int) (if IndexLinear) Linear scalar indexing
getindex(A, I::Vararg{Int, N}) (if IndexCartesian, where N = ndims(A)) N-dimensional scalar indexing
Optional methods Default definition Brief description
IndexStyle(::Type) IndexCartesian() Returns either IndexLinear() or IndexCartesian(). See the description below.
setindex!(A, v, i::Int) (if IndexLinear) Scalar indexed assignment
setindex!(A, v, I::Vararg{Int, N}) (if IndexCartesian, where N = ndims(A)) N-dimensional scalar indexed assignment
getindex(A, I...) defined in terms of scalar getindex Multidimensional and nonscalar indexing
setindex!(A, X, I...) defined in terms of scalar setindex! Multidimensional and nonscalar indexed assignment
iterate defined in terms of scalar getindex Iteration
length(A) prod(size(A)) Number of elements
similar(A) similar(A, eltype(A), size(A)) Return a mutable array with the same shape and element type
similar(A, ::Type{S}) similar(A, S, size(A)) Return a mutable array with the same shape and the specified element type
similar(A, dims::Dims) similar(A, eltype(A), dims) Return a mutable array with the same element type and size dims
similar(A, ::Type{S}, dims::Dims) Array{S}(undef, dims) Return a mutable array with the specified element type and size
Non-traditional indices Default definition Brief description
axes(A) map(OneTo, size(A)) Return a tuple of AbstractUnitRange{<:Integer} of valid indices
similar(A, ::Type{S}, inds) similar(A, S, Base.to_shape(inds)) Return a mutable array with the specified indices inds (see below)
similar(T::Union{Type,Function}, inds) T(Base.to_shape(inds)) Return an array similar to T with the specified indices inds (see below)
=#

# And arbitrary new type for array values
struct ArrayTestVal
a::Int
end

# In case `eltype` and `ndims` have custom methods
# We should always be able to use these to mean the same thing
_eltype(::AbstractArray{T}) where T = T
_ndims(::AbstractArray{<:Any,N}) where N = N

array_components = (;
mandatory = (;
type = A -> A isa AbstractArray,
eltype = (
A -> eltype(A) isa Type,
A -> eltype(A) == _eltype(A),
),
ndims = (
A -> ndims(A) isa Int,
A -> ndims(A) == _ndims(A),
),
size = (
"size(A) returns a tuple of Integer" => A -> size(A) isa NTuple{<:Any,Integer},
"length of size(A) matches ndims(A)" => A -> length(size(A)) == ndims(A),
),
getindex = (
"Can index with begin/firstinex" => A -> A[begin] isa eltype(A),
"Can index with end/lastindex" => A -> A[end] isa eltype(A),
"Can index with all indices in `eachindex(A)`" => A -> all(x -> A[x] isa eltype(A), eachindex(A)),
"Can index with multiple dimensions" => A -> A[map(first, axes(A))...] isa eltype(A),
"Can use trailing ones" => A -> A[map(first, axes(A))..., 1, 1, 1] isa eltype(A),
"Can index with CartesianIndex" => A -> A[CartesianIndex(map(first, axes(A))...)] isa eltype(A),
"Can use trailing ones in CartesianIndex" => A -> A[CartesianIndex(map(first, axes(A))..., 1, 1, 1)] isa eltype(A),
),
indexstyle = "IndexStyle returns IndexCartesian or IndexLinear" => A -> IndexStyle(A) in (IndexCartesian(), IndexLinear()),
),
# TODO implement all the optional conditions
optional = (;
setindex! = (
A -> length(A) > 1 || throw(ArgumentError("Test arrays must have more than one element to test setindex!")),
"setindex! can write the first to the last element" =>
A -> begin
x1 = A[begin]; x2 = A[end]
A[begin] = x2
A[end] = x1
A[begin] == x2 && A[end] == x1
end,
"setindex! can write the first to the last element using multidimensional indices" =>
A -> begin
fs = map(first, axes(A))
ls = map(last, axes(A))
x1 = A[fs...];
x2 = A[ls...]
A[fs...] = x2
A[ls...] = x1
A[fs...] == x2 && A[ls...] == x1
end,
"setindex! can write to all indices in eachindex(A)" =>
A -> begin
v = first(A)
all(eachindex(A)) do i
A[i] = v
A[i] === v
end
end,
"setindex! can write to all indices in CartesianIndices(A)" =>
A -> begin
v = first(A) # We have already tested writing to the first index above
all(CartesianIndices(A)) do i
A[i] = v
A[i] === v
end
end,
),
similar_type = "`similar(A)` returns an object the same type and size as `A`" =>
A -> begin
A1 = similar(A)
A1 isa typeof(A) && size(A1) == size(A)
end,
similar_eltype = "similar(A, T::Type) returns an object the same base type as `A` with eltype of `T`" =>
A -> begin
A1 = similar(A, ArrayTestVal);
_wrappertype(A) == _wrappertype(A1) && eltype(A1) == ArrayTestVal && size(A) == size(A1)
end,
similar_size = "similar(A, s::NTuple{Int}) returns an object the same type as `A` with size `s`" =>
A -> begin
A1 = similar(A, (2, 3))
A2 = similar(A, (4, 5))
_wrappertype(A) == _wrappertype(A1) && size(A1) == (2, 3) && size(A2) == (4, 5)
end,
similar_eltype_size = "similar(A, T::Type, s::NTuple{Int}) returns an object the same type as `A` with eltype `T` and size `s`" =>
A -> begin
A1 = similar(A, ArrayTestVal, (2, 3))
A2 = similar(A, ArrayTestVal, (4, 5))
_wrappertype(A) == _wrappertype(A1) && eltype(A1) == ArrayTestVal && size(A1) == (2, 3) && size(A2) == (4, 5)
end,
)
)

_wrappertype(A) = Base.typename(typeof(A)).wrapper

@interface ArrayInterface AbstractArray array_components "Base Julia AbstractArray interface"
20 changes: 20 additions & 0 deletions BaseInterfaces/src/collection.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Should iteration be here?

mandatory = (;
isempty = !isempty,
)

optional = (;
empty! = c -> isempty(empty!(c)),
length = c -> length(c) isa Integer,
# push! =
# pushfirst! =
# deleteat! =
# splice! =
# pop! =
# popfirst! =
)

components = (; mandatory, optional)

@interface CollectionInterface Any _components "Base interface for shared methods of various collections"
24 changes: 24 additions & 0 deletions BaseInterfaces/src/dict.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

@interface DictInterface AbstractDict ( # <: CollectionInterface
mandatory = (;
iterate = "AbstractDict follows the IterationInterface" => a -> Interfaces.test(IterationInterface, a.d; show=false) && first(iterate(a.d)) isa Pair,
eltype = "eltype is a Pair" => a -> eltype(a.d) <: Pair,
keytype = a -> keytype(a.d) == eltype(a.d).parameters[1],
valtype = a -> valtype(a.d) == eltype(a.d).parameters[2],
keys = a -> all(k -> k isa keytype(a.d), keys(a.d)),
values = a -> all(v -> v isa valtype(a.d), values(a.d)),
getindex = (
a -> a.d[first(keys(a.d))] === first(values(a.d)),
a -> all(k -> a.d[k] isa valtype(a.d), keys(a.d)),
),
),
optional = (;
setindex! = (
"test object `d` does not yet have test key `k`" => a -> !haskey(a.d, a.k),
"can set key `k` to value `v`" => a -> (a.d[a.k] = a.v; a.d[a.k] == a.v),
),
)
) """
`AbstractDict` interface requires Arguments, with `d = the_dict` mandatory, and
when `setindex` is needed, `k = any_valid_key_not_in_d, v = any_valid_val`
"""
43 changes: 43 additions & 0 deletions BaseInterfaces/src/implementations.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Some example interface delarations.

# @implements ArrayInterface Base.LogicalIndex # No getindex
@implements ArrayInterface UnitRange
@implements ArrayInterface StepRange
@implements ArrayInterface Base.Slice
@implements ArrayInterface Base.IdentityUnitRange
@implements ArrayInterface Base.CodeUnits
@implements ArrayInterface{(:setindex!,:similar_type,:similar_eltype,:similar_size)} Array
@implements ArrayInterface{(:setindex!,:similar_type,:similar_size)} BitArray
@implements ArrayInterface{:setindex!} SubArray
@implements ArrayInterface{:setindex!} PermutedDimsArray
@implements ArrayInterface{:setindex!} Base.ReshapedArray

@implements DictInterface{:setindex!} Dict
@implements DictInterface{:setindex!} IdDict
@implements DictInterface{:setindex!} WeakKeyDict
@implements DictInterface Base.EnvDict
@implements DictInterface Base.ImmutableDict
@static if VERSION >= v"1.9.0"
@implements DictInterface Base.Pairs
end

@implements IterationInterface{(:reverse,:indexing)} UnitRange
@implements IterationInterface{(:reverse,:indexing)} StepRange
@implements IterationInterface{(:reverse,:indexing)} Array
@implements IterationInterface{(:reverse,:indexing)} Tuple
@implements IterationInterface{(:reverse,:indexing)} NamedTuple
@implements IterationInterface{(:reverse,:indexing)} String
@implements IterationInterface{(:reverse,:indexing)} Pair
@implements IterationInterface{(:reverse,:indexing)} Number
@implements IterationInterface{(:reverse,:indexing)} Base.EachLine
@implements IterationInterface{(:reverse,)} Base.Generator
@implements IterationInterface Set
@implements IterationInterface BitSet
@implements IterationInterface IdDict
@implements IterationInterface Dict
@implements IterationInterface WeakKeyDict

# TODO add grouping to reduce the number of options
@implements SetInterface{(:copy,:empty,:emptymutable,:hasfastin,:setdiff,:intersect,:union,:empty!,:delete!,:push!,:copymutable,:sizehint!)} Set
@implements SetInterface{(:copy,:empty,:emptymutable,:hasfastin,:setdiff,:intersect,:union,:empty!,:delete!,:push!,:copymutable,:sizehint!)} BitSet
@implements SetInterface{(:empty,:emptymutable,:hasfastin,:intersect,:union,:sizehint!)} Base.KeySet
Loading

0 comments on commit bd13c15

Please sign in to comment.