Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stringinterface etc #52

Merged
merged 5 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading