Skip to content

Commit

Permalink
Stringinterface etc (#52)
Browse files Browse the repository at this point in the history
* add StringInterface and update the others

* add StringInterface implementations

* update BaseInterfaces README

* update BaseInterfaces readme and use it as the module docs

* limit LazyString to > v1.8
  • Loading branch information
rafaqz authored Jul 15, 2024
1 parent d36fcdc commit 89788d9
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 30 deletions.
18 changes: 11 additions & 7 deletions BaseInterfaces/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,27 @@ Currently this includes:
- `AbstractArray` interface: `ArrayInterface`
- `AbstractSet` interface: `SetInterface`
- `AbstractDict` interface: `DictInterface`
- `AbstractString` interface: `StringInterface`

None of these should be considered complete or authoritative, due both to
the size of the task and that many of the interfaces not documented in Base
Julia. However, they may be helpful in testing your objects basically conform.
Please make issues and PRs with missing behaviours if you find them.

Testing your object follows the interfaces is as simple as:
Declaring that it follows the interface is done with:

```julia
using BaseInterfaces, Interfaces
Interfaces.tests(DictInterface, MyDict, [mydict1, mydict2, ...])
@implements DictInterface{(:component1, :component2)} MyDict [MyDict(some_values...), MyDict(other_values...)]
```

Declaring that it follows the interface is done with:
Optional components can be chosen from `Interfaces.optional_keys(DictInterface)`.

After this you can easily test it with:

```julia
@implements DictInterface{(:component1, :component2)} MyDict
Interfaces.test(DictInterface, 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
Expand Down
10 changes: 9 additions & 1 deletion BaseInterfaces/src/BaseInterfaces.jl
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
module BaseInterfaces

@doc let
path = joinpath(dirname(@__DIR__), "README.md")
include_dependency(path)
read(path, String)
end BaseInterfaces

using Interfaces

import Interfaces: test, test_objects, implements, description, components, requiredtype, @implements

export Interfaces

export ArrayInterface, DictInterface, IterationInterface, SetInterface
export ArrayInterface, DictInterface, IterationInterface, SetInterface, StringInterface

include("interfaces/iteration.jl")
include("interfaces/dict.jl")
include("interfaces/set.jl")
include("interfaces/array.jl")
include("interfaces/string.jl")

include("implementations/iteration.jl")
include("implementations/dict.jl")
include("implementations/set.jl")
include("implementations/array.jl")
include("implementations/string.jl")

end
4 changes: 2 additions & 2 deletions BaseInterfaces/src/implementations/dict.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

@implements DictInterface{:setindex!} Dict [Arguments(d=Dict(:a => 1, :b => 2), k=:c, v=3)]
@implements DictInterface{:setindex!} IdDict [Arguments(d=IdDict(:a => 1, :b => 2), k=:c, v=3)]
@implements DictInterface{(:setindex!,:get!,:delete!,:empty!)} Dict [Arguments(d=Dict(:a => 1, :b => 2), k=:c, v=3)]
@implements DictInterface{(:setindex!,:get!,:delete!,:empty!)} IdDict [Arguments(d=IdDict(:a => 1, :b => 2), k=:c, v=3)]
# This errors because the ref is garbage collected
# @implements DictInterface{:setindex!} WeakKeyDict [Arguments(; d=WeakKeyDict(Ref(1) => 1, Ref(2) => 2), k=Ref(3), v=3)]
@implements DictInterface Base.EnvDict [Arguments(d=Base.EnvDict())]
Expand Down
9 changes: 9 additions & 0 deletions BaseInterfaces/src/implementations/string.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# We may want to add matching @implements declarations for
# - SubstitutionString

@implements StringInterface{:length} String ["abc"]
@implements StringInterface{:length} SubString [view("abc", 2:3)]
@implements StringInterface{:length} SubstitutionString [SubstitutionString("abc \\1")]
if VERSION > v"1.8.0"
@implements StringInterface{:length} LazyString [LazyString("abc", "def")]
end
1 change: 1 addition & 0 deletions BaseInterfaces/src/interfaces/collection.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# This is not actually loaded yet...
# Should iteration be here?

mandatory = (;
Expand Down
60 changes: 49 additions & 11 deletions BaseInterfaces/src/interfaces/dict.jl
Original file line number Diff line number Diff line change
@@ -1,24 +1,62 @@
# This is a completely insufficient interface for AbstractDict
# See https://github.com/JuliaLang/julia/issues/25941 for discussion

@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)),
# This is kind of unsatisfactory as interface inheritance, but its simple
iterate = "AbstractDict follows the IterationInterface" =>
a -> Interfaces.test(IterationInterface, a.d; show=false) && first(iterate(a.d)) isa Pair,
length = "length is defined" => a -> length(a.d) isa Integer,
eltype = (
"eltype is a Pair" => a -> eltype(a.d) <: Pair,
"the first value isa eltype" => a -> eltype(a.d) <: Pair,
),
keytype = "keytype is the same as the first type in eltype parameters" =>
a -> keytype(a.d) == eltype(a.d).parameters[1],
valtype = "valtype is the same as the second type in eltype paremeters" =>
a -> valtype(a.d) == eltype(a.d).parameters[2],
keys = "keys are all of type keytype" => a -> all(k -> k isa keytype(a.d), keys(a.d)),
values = "values are all of type valtype" => 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)),
"getindex of the first key is the first object in `values`" =>
a -> a.d[first(keys(a.d))] === first(values(a.d)),
"getindex of all keys match values" =>
a -> all(p -> a.d[p[1]] == p[2], 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),
),
get! = (
"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),
"can set and get key `k` to value `v` with using get!" =>
a -> begin
v = get!(a.d, a.k, a.v)
v == a.d[a.k] == a.v
end,
),
delete! = "can delete existing keys from the object" =>
a -> begin
k = first(keys(a.d))
delete!(a.d, k)
!haskey(a.d, k)
end,
empty! = "can empty the dictionary" =>
a -> begin
empty!(a.d)
length(a.d) == 0
end,
)
) """
`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`
`AbstractDict` interface
Requires test data wrapped with `Interfaces.Arguments`, with
- `d = the_dict` mandatory
When `get!` or `setindex!` is needed
- `k`: any valid key not initially in d
- `v`: any valid value
"""
16 changes: 10 additions & 6 deletions BaseInterfaces/src/interfaces/iteration.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ EltypeUnknown() (none)
# Mandatory conditions: these must be met by all types
# that implement the interface.
mandatory = (
isempty = "test iterator is not empty" => x -> !isempty(x),
iterate = (
"test iterator is not empty" => x -> !isempty(x),
"iterate does not return `nothing`" => x -> !isnothing(iterate(x)),
"iterate returns a Tuple" => x -> iterate(x) isa Tuple{<:Any,<:Any},
"iterate returns a Tuple" => x -> iterate(x, last(iterate(x))) isa Union{Nothing,Tuple{<:Any,<:Any}},
"`iterate` does not return `nothing`" => x -> !isnothing(iterate(x)),
"`iterate` returns a `Tuple`" => x -> iterate(x) isa Tuple{<:Any,<:Any},
"second `iterate` returns a `Tuple` or `Nothing`" => x -> iterate(x, last(iterate(x))) isa Union{Nothing,Tuple{<:Any,<:Any}},
),
isiterable = x -> Base.isiterable(typeof(x)),
eltype = x -> begin
Expand Down Expand Up @@ -72,7 +72,7 @@ EltypeUnknown() (none)
error("IteratorSize returns $sizetrait, allowed options are: `HasLength`, `HasLength`, `IsInfinite`, `SizeUnknown`")
end
end,
in = x -> first(x) in x,
in = "`in` returns true for all values in x" => x -> all(a -> a in x, x),
),
# Optional conditions. These should be specified in the
# interface type if an object implements them: IterationInterface{(:reverse,:indexing)}
Expand All @@ -95,4 +95,8 @@ EltypeUnknown() (none)
end,
),
)
) "An interface for Base Julia iteration"
) """
An interface for Base Julia iteration.
Test objects must not be empty, so that `isempty(obj) == false`.
"""
4 changes: 2 additions & 2 deletions BaseInterfaces/src/interfaces/set.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ set_components = (;
isempty = "defines `isempty` and testdata is not empty" => !isempty,
eltype = "elements eltype of set `s` are subtypes of `eltype(s)`" => s -> typeof(first(iterate(s))) <: eltype(s),
length = "set defines length and test object has length larger than zero" => s -> length(s) isa Int && length(s) > 0,
# This is kind of unsatisfactory as interface inheritance, but its simple
iteration = "follows the IterationInterface" => s -> Interfaces.test(IterationInterface, s; show=false),
in = "`in` is true for elements in set" => s -> all(x -> in(x, s), s),
),
optional = (;
copy = "creates an identical object with the same values, that is not the same object" =>
Expand Down Expand Up @@ -62,7 +62,7 @@ set_components = (;
push!(s, x)
in(x, s)
end,
# No real way to test this does anything
# No real way to test this does anything?
sizehint! = "can set a size hint" => s -> (sizehint!(s, 10); true),
)
)
Expand Down
25 changes: 25 additions & 0 deletions BaseInterfaces/src/interfaces/string.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# The AbstractString interface is not clearly documented and large.
#
# This discourse post likely has the best information, although outdated
# https://discourse.julialang.org/t/what-is-the-interface-of-abstractstring/8937/4
#
# See required methods here: https://github.com/JuliaLang/julia/blob/b88f64f16c454c238c9fa0ae858ca02b7084f329/base/strings/basic.jl#L41

@interface StringInterface AbstractString (;
mandatory = (;
ncodeunits = "ncodeunit returns an Int" => s -> ncodeunits(s) isa Int,
iterate = "AbstractString follows the IterationInterface" =>
s -> Interfaces.test(IterationInterface, s; show=false) && first(iterate(s)) isa AbstractChar,
codeunit = "the first codeunit is a UInt8/16/32" => s -> codeunit(s, 1) isa Union{UInt8,UInt16,UInt32},
isvalid = "isvalid returns a Bool" => s -> isvalid(s, 1) isa Bool,
eltype = "eltype returns a type <: AbatractChar" => s -> eltype(s) <: AbstractChar,
),
optional = (;
length = "length return an Int" => s -> length(s) isa Int,
# ?
)
) """
`AbstractString` interface
Test objects must not be empty, so that `isempty(obj) == false`.
"""
5 changes: 4 additions & 1 deletion BaseInterfaces/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ using Test
# Test some Test objects
@implements SetInterface{(:empty,:emptymutable,:hasfastin,:intersect,:union,:sizehint!)} Test.GenericSet [Test.GenericSet(Set((1, 2)))]
@implements DictInterface Test.GenericDict [Arguments(d=Test.GenericDict(Dict(:a => 1, :b => 2)), k=:c, v=3)]
@implements StringInterface Test.GenericString [Test.GenericString("abc")]

# Test all interfaces
@test Interfaces.test()

# Test all interfaaces in BaseInterfaces
# Test all interfaces in BaseInterfaces
@test Interfaces.test(BaseInterfaces)
@test Interfaces.test(Main)

Expand All @@ -18,12 +19,14 @@ using Test
@test Interfaces.test(DictInterface, BaseInterfaces)
@test Interfaces.test(IterationInterface, BaseInterfaces)
@test Interfaces.test(SetInterface, BaseInterfaces)
@test Interfaces.test(StringInterface, BaseInterfaces)

# Now test all the interfaces implementations independent of where they are implemented
@test Interfaces.test(ArrayInterface)
@test Interfaces.test(DictInterface)
@test Interfaces.test(IterationInterface)
@test Interfaces.test(SetInterface)
@test Interfaces.test(StringInterface)

@test_throws ArgumentError Interfaces.test(SetInterface{:empty})

Expand Down

0 comments on commit 89788d9

Please sign in to comment.