Skip to content

Commit bd13c15

Browse files
authored
add some more interfaces to BaseInterfaces.jl (#27)
* 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
1 parent b23bfa5 commit bd13c15

File tree

18 files changed

+572
-141
lines changed

18 files changed

+572
-141
lines changed

.github/workflows/CI.yml

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
matrix:
2020
version:
2121
- '1.6'
22-
- '1.7'
22+
- '1'
2323
- 'nightly'
2424
os:
2525
- ubuntu-latest
@@ -35,27 +35,10 @@ jobs:
3535
- uses: julia-actions/cache@v1
3636
- uses: julia-actions/julia-buildpkg@v1
3737
- uses: julia-actions/julia-runtest@v1
38+
- name: Run BaseInterfaces tests
39+
run: julia --project=BaseInterfaces -e 'using Pkg; pkg"dev ."; pkg"update"; pkg"test"'
40+
shell: bash
3841
- uses: julia-actions/julia-processcoverage@v1
3942
- uses: codecov/codecov-action@v2
4043
with:
4144
files: lcov.info
42-
docs:
43-
name: Documentation
44-
runs-on: ubuntu-latest
45-
permissions:
46-
contents: write
47-
steps:
48-
- uses: actions/checkout@v2
49-
- uses: julia-actions/setup-julia@v1
50-
with:
51-
version: '1'
52-
- uses: julia-actions/julia-buildpkg@v1
53-
- uses: julia-actions/julia-docdeploy@v1
54-
env:
55-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56-
- run: |
57-
julia --project=docs -e '
58-
using Documenter: DocMeta, doctest
59-
using Interfaces
60-
DocMeta.setdocmeta!(Interfaces, :DocTestSetup, :(using Interfaces); recursive=true)
61-
doctest(Interfaces)'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Documentation
2+
3+
on:
4+
push:
5+
branches:
6+
- main # update to match your development branch (master, main, dev, trunk, ...)
7+
tags: '*'
8+
pull_request:
9+
10+
jobs:
11+
build:
12+
permissions:
13+
contents: write
14+
statuses: write
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: julia-actions/setup-julia@v1
19+
with:
20+
version: '1'
21+
- name: Install dependencies
22+
run: julia --project=docs/ -e 'using Pkg; pkg"dev ."; pkg"dev ./BaseInterfaces"; Pkg.instantiate()'
23+
- name: Build and deploy
24+
env:
25+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token
26+
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key
27+
run: julia --project=docs/ docs/make.jl

BaseInterfaces/README.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
11
# BaseInterfaces
22

3-
[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://rafaqz.github.io/BaseInterfaces.jl/stable/)
4-
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://rafaqz.github.io/BaseInterfaces.jl/dev/)
5-
[![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)
6-
[![Coverage](https://codecov.io/gh/rafaqz/BaseInterfaces.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/rafaqz/BaseInterfaces.jl)
3+
[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://rafaqz.github.io/Interfaces.jl/stable/)
4+
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://rafaqz.github.io/Interfaces.jl/dev/)
5+
[![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)
6+
[![Coverage](https://codecov.io/gh/rafaqz/Interfaces.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/rafaqz/Interfaces.jl)
7+
8+
BaseInterfaces.jl is a subpackage of Interfaces.jl that provides predifined
9+
definition and testing for Base Julia interfaces.
10+
11+
Currently this includes:
12+
- A general iteration interface: `IterationInterface`
13+
- `AbstractArray` interface: `ArrayInterface`
14+
- `AbstractSet` interface: `SetInterface`
15+
- `AbstractDict` interface: `DictInterface`
16+
17+
18+
Testing your object follows the interfaces is as simple as:
19+
20+
```julia
21+
using BaseInterfaces, Interfaces
22+
Interfaces.tests(DictInterface, MyDict, [mydict1, mydict2, ...])
23+
```
24+
25+
Declaring that it follows the interface is done with:
26+
27+
```julia
28+
@implements DictInterface{(:component1, :component2)} MyDict
29+
```
30+
31+
Where components can be chosen from `Interfaces.optional_keys(DictInterface)`.
32+
33+
See [the docs](https://rafaqz.github.io/Interfaces.jl/stable/) for use.
34+
35+
If you want to add more Base julia interfaces here, or think the existing
36+
ones could be improved, please make an issue or pull request.

BaseInterfaces/src/BaseInterfaces.jl

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ module BaseInterfaces
22

33
using Interfaces
44

5-
export IterationInterface
5+
export ArrayInterface, DictInterface, IterationInterface, SetInterface
66

77
include("iteration.jl")
8+
include("dict.jl")
9+
include("set.jl")
10+
include("array.jl")
811

9-
# Some example interface delarations.
10-
@implements IterationInterface{(:reverse,:indexing,)} UnitRange
11-
@implements IterationInterface{(:reverse,:indexing,)} StepRange
12-
@implements IterationInterface{(:reverse,:indexing,)} Array
13-
@implements IterationInterface{(:reverse,)} Base.Generator
14-
@implements IterationInterface{(:reverse,:indexing,)} Tuple
12+
include("implementations.jl")
1513

1614
end

BaseInterfaces/src/array.jl

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#= Abstract Arrays
2+
https://docs.julialang.org/en/v1/manual/interfaces/
3+
4+
Methods to implement Brief description
5+
size(A) Returns a tuple containing the dimensions of A
6+
getindex(A, i::Int) (if IndexLinear) Linear scalar indexing
7+
getindex(A, I::Vararg{Int, N}) (if IndexCartesian, where N = ndims(A)) N-dimensional scalar indexing
8+
9+
Optional methods Default definition Brief description
10+
IndexStyle(::Type) IndexCartesian() Returns either IndexLinear() or IndexCartesian(). See the description below.
11+
setindex!(A, v, i::Int) (if IndexLinear) Scalar indexed assignment
12+
setindex!(A, v, I::Vararg{Int, N}) (if IndexCartesian, where N = ndims(A)) N-dimensional scalar indexed assignment
13+
getindex(A, I...) defined in terms of scalar getindex Multidimensional and nonscalar indexing
14+
setindex!(A, X, I...) defined in terms of scalar setindex! Multidimensional and nonscalar indexed assignment
15+
iterate defined in terms of scalar getindex Iteration
16+
length(A) prod(size(A)) Number of elements
17+
similar(A) similar(A, eltype(A), size(A)) Return a mutable array with the same shape and element type
18+
similar(A, ::Type{S}) similar(A, S, size(A)) Return a mutable array with the same shape and the specified element type
19+
similar(A, dims::Dims) similar(A, eltype(A), dims) Return a mutable array with the same element type and size dims
20+
similar(A, ::Type{S}, dims::Dims) Array{S}(undef, dims) Return a mutable array with the specified element type and size
21+
22+
Non-traditional indices Default definition Brief description
23+
axes(A) map(OneTo, size(A)) Return a tuple of AbstractUnitRange{<:Integer} of valid indices
24+
similar(A, ::Type{S}, inds) similar(A, S, Base.to_shape(inds)) Return a mutable array with the specified indices inds (see below)
25+
similar(T::Union{Type,Function}, inds) T(Base.to_shape(inds)) Return an array similar to T with the specified indices inds (see below)
26+
=#
27+
28+
# And arbitrary new type for array values
29+
struct ArrayTestVal
30+
a::Int
31+
end
32+
33+
# In case `eltype` and `ndims` have custom methods
34+
# We should always be able to use these to mean the same thing
35+
_eltype(::AbstractArray{T}) where T = T
36+
_ndims(::AbstractArray{<:Any,N}) where N = N
37+
38+
array_components = (;
39+
mandatory = (;
40+
type = A -> A isa AbstractArray,
41+
eltype = (
42+
A -> eltype(A) isa Type,
43+
A -> eltype(A) == _eltype(A),
44+
),
45+
ndims = (
46+
A -> ndims(A) isa Int,
47+
A -> ndims(A) == _ndims(A),
48+
),
49+
size = (
50+
"size(A) returns a tuple of Integer" => A -> size(A) isa NTuple{<:Any,Integer},
51+
"length of size(A) matches ndims(A)" => A -> length(size(A)) == ndims(A),
52+
),
53+
getindex = (
54+
"Can index with begin/firstinex" => A -> A[begin] isa eltype(A),
55+
"Can index with end/lastindex" => A -> A[end] isa eltype(A),
56+
"Can index with all indices in `eachindex(A)`" => A -> all(x -> A[x] isa eltype(A), eachindex(A)),
57+
"Can index with multiple dimensions" => A -> A[map(first, axes(A))...] isa eltype(A),
58+
"Can use trailing ones" => A -> A[map(first, axes(A))..., 1, 1, 1] isa eltype(A),
59+
"Can index with CartesianIndex" => A -> A[CartesianIndex(map(first, axes(A))...)] isa eltype(A),
60+
"Can use trailing ones in CartesianIndex" => A -> A[CartesianIndex(map(first, axes(A))..., 1, 1, 1)] isa eltype(A),
61+
),
62+
indexstyle = "IndexStyle returns IndexCartesian or IndexLinear" => A -> IndexStyle(A) in (IndexCartesian(), IndexLinear()),
63+
),
64+
# TODO implement all the optional conditions
65+
optional = (;
66+
setindex! = (
67+
A -> length(A) > 1 || throw(ArgumentError("Test arrays must have more than one element to test setindex!")),
68+
"setindex! can write the first to the last element" =>
69+
A -> begin
70+
x1 = A[begin]; x2 = A[end]
71+
A[begin] = x2
72+
A[end] = x1
73+
A[begin] == x2 && A[end] == x1
74+
end,
75+
"setindex! can write the first to the last element using multidimensional indices" =>
76+
A -> begin
77+
fs = map(first, axes(A))
78+
ls = map(last, axes(A))
79+
x1 = A[fs...];
80+
x2 = A[ls...]
81+
A[fs...] = x2
82+
A[ls...] = x1
83+
A[fs...] == x2 && A[ls...] == x1
84+
end,
85+
"setindex! can write to all indices in eachindex(A)" =>
86+
A -> begin
87+
v = first(A)
88+
all(eachindex(A)) do i
89+
A[i] = v
90+
A[i] === v
91+
end
92+
end,
93+
"setindex! can write to all indices in CartesianIndices(A)" =>
94+
A -> begin
95+
v = first(A) # We have already tested writing to the first index above
96+
all(CartesianIndices(A)) do i
97+
A[i] = v
98+
A[i] === v
99+
end
100+
end,
101+
),
102+
similar_type = "`similar(A)` returns an object the same type and size as `A`" =>
103+
A -> begin
104+
A1 = similar(A)
105+
A1 isa typeof(A) && size(A1) == size(A)
106+
end,
107+
similar_eltype = "similar(A, T::Type) returns an object the same base type as `A` with eltype of `T`" =>
108+
A -> begin
109+
A1 = similar(A, ArrayTestVal);
110+
_wrappertype(A) == _wrappertype(A1) && eltype(A1) == ArrayTestVal && size(A) == size(A1)
111+
end,
112+
similar_size = "similar(A, s::NTuple{Int}) returns an object the same type as `A` with size `s`" =>
113+
A -> begin
114+
A1 = similar(A, (2, 3))
115+
A2 = similar(A, (4, 5))
116+
_wrappertype(A) == _wrappertype(A1) && size(A1) == (2, 3) && size(A2) == (4, 5)
117+
end,
118+
similar_eltype_size = "similar(A, T::Type, s::NTuple{Int}) returns an object the same type as `A` with eltype `T` and size `s`" =>
119+
A -> begin
120+
A1 = similar(A, ArrayTestVal, (2, 3))
121+
A2 = similar(A, ArrayTestVal, (4, 5))
122+
_wrappertype(A) == _wrappertype(A1) && eltype(A1) == ArrayTestVal && size(A1) == (2, 3) && size(A2) == (4, 5)
123+
end,
124+
)
125+
)
126+
127+
_wrappertype(A) = Base.typename(typeof(A)).wrapper
128+
129+
@interface ArrayInterface AbstractArray array_components "Base Julia AbstractArray interface"

BaseInterfaces/src/collection.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Should iteration be here?
2+
3+
mandatory = (;
4+
isempty = !isempty,
5+
)
6+
7+
optional = (;
8+
empty! = c -> isempty(empty!(c)),
9+
length = c -> length(c) isa Integer,
10+
# push! =
11+
# pushfirst! =
12+
# deleteat! =
13+
# splice! =
14+
# pop! =
15+
# popfirst! =
16+
)
17+
18+
components = (; mandatory, optional)
19+
20+
@interface CollectionInterface Any _components "Base interface for shared methods of various collections"

BaseInterfaces/src/dict.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
@interface DictInterface AbstractDict ( # <: CollectionInterface
3+
mandatory = (;
4+
iterate = "AbstractDict follows the IterationInterface" => a -> Interfaces.test(IterationInterface, a.d; show=false) && first(iterate(a.d)) isa Pair,
5+
eltype = "eltype is a Pair" => a -> eltype(a.d) <: Pair,
6+
keytype = a -> keytype(a.d) == eltype(a.d).parameters[1],
7+
valtype = a -> valtype(a.d) == eltype(a.d).parameters[2],
8+
keys = a -> all(k -> k isa keytype(a.d), keys(a.d)),
9+
values = a -> all(v -> v isa valtype(a.d), values(a.d)),
10+
getindex = (
11+
a -> a.d[first(keys(a.d))] === first(values(a.d)),
12+
a -> all(k -> a.d[k] isa valtype(a.d), keys(a.d)),
13+
),
14+
),
15+
optional = (;
16+
setindex! = (
17+
"test object `d` does not yet have test key `k`" => a -> !haskey(a.d, a.k),
18+
"can set key `k` to value `v`" => a -> (a.d[a.k] = a.v; a.d[a.k] == a.v),
19+
),
20+
)
21+
) """
22+
`AbstractDict` interface requires Arguments, with `d = the_dict` mandatory, and
23+
when `setindex` is needed, `k = any_valid_key_not_in_d, v = any_valid_val`
24+
"""
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Some example interface delarations.
2+
3+
# @implements ArrayInterface Base.LogicalIndex # No getindex
4+
@implements ArrayInterface UnitRange
5+
@implements ArrayInterface StepRange
6+
@implements ArrayInterface Base.Slice
7+
@implements ArrayInterface Base.IdentityUnitRange
8+
@implements ArrayInterface Base.CodeUnits
9+
@implements ArrayInterface{(:setindex!,:similar_type,:similar_eltype,:similar_size)} Array
10+
@implements ArrayInterface{(:setindex!,:similar_type,:similar_size)} BitArray
11+
@implements ArrayInterface{:setindex!} SubArray
12+
@implements ArrayInterface{:setindex!} PermutedDimsArray
13+
@implements ArrayInterface{:setindex!} Base.ReshapedArray
14+
15+
@implements DictInterface{:setindex!} Dict
16+
@implements DictInterface{:setindex!} IdDict
17+
@implements DictInterface{:setindex!} WeakKeyDict
18+
@implements DictInterface Base.EnvDict
19+
@implements DictInterface Base.ImmutableDict
20+
@static if VERSION >= v"1.9.0"
21+
@implements DictInterface Base.Pairs
22+
end
23+
24+
@implements IterationInterface{(:reverse,:indexing)} UnitRange
25+
@implements IterationInterface{(:reverse,:indexing)} StepRange
26+
@implements IterationInterface{(:reverse,:indexing)} Array
27+
@implements IterationInterface{(:reverse,:indexing)} Tuple
28+
@implements IterationInterface{(:reverse,:indexing)} NamedTuple
29+
@implements IterationInterface{(:reverse,:indexing)} String
30+
@implements IterationInterface{(:reverse,:indexing)} Pair
31+
@implements IterationInterface{(:reverse,:indexing)} Number
32+
@implements IterationInterface{(:reverse,:indexing)} Base.EachLine
33+
@implements IterationInterface{(:reverse,)} Base.Generator
34+
@implements IterationInterface Set
35+
@implements IterationInterface BitSet
36+
@implements IterationInterface IdDict
37+
@implements IterationInterface Dict
38+
@implements IterationInterface WeakKeyDict
39+
40+
# TODO add grouping to reduce the number of options
41+
@implements SetInterface{(:copy,:empty,:emptymutable,:hasfastin,:setdiff,:intersect,:union,:empty!,:delete!,:push!,:copymutable,:sizehint!)} Set
42+
@implements SetInterface{(:copy,:empty,:emptymutable,:hasfastin,:setdiff,:intersect,:union,:empty!,:delete!,:push!,:copymutable,:sizehint!)} BitSet
43+
@implements SetInterface{(:empty,:emptymutable,:hasfastin,:intersect,:union,:sizehint!)} Base.KeySet

0 commit comments

Comments
 (0)