Skip to content

Commit

Permalink
Add support for tuples and better round tripping of named tuples (#515)
Browse files Browse the repository at this point in the history
* Add support for tuples and better round tripping of named tuples

* minor version bump

The changes here should not be breaking for most users, but may potentially cause problems for JuliaCall , so we do a breaking version bump.
  • Loading branch information
palday committed Jan 8, 2024
1 parent 69c22dd commit 5d0ddeb
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 48 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ docs/build
docs/gh-pages
docs/site
deps/build.log
lcov.info
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "RCall"
uuid = "6f49c342-dc21-5d91-9882-a32aef131414"
authors = ["Douglas Bates <[email protected]>", "Randy Lai <[email protected]>", "Simon Byrne <[email protected]>"]
version = "0.13.18"
version = "0.14.0"

[deps]
CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597"
Expand Down
1 change: 1 addition & 0 deletions src/RCall.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ include("convert/datetime.jl")
include("convert/dataframe.jl")
include("convert/formula.jl")
include("convert/namedtuple.jl")
include("convert/tuple.jl")

include("convert/default.jl")
include("eventloop.jl")
Expand Down
40 changes: 23 additions & 17 deletions src/convert/namedtuple.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
function sexp(::Type{RClass{:JuliaNamedTuple}}, nt::NamedTuple)
vs = sexp(RClass{:list}, nt)
# mark this as originating from a tuple
# for roundtrippping, which downstream JuliaCall
# relies on
# because of the way S3 classes work, this doesn't break anything on the R side
# and strictly adds more information that we can take advantage of
setattrib!(vs, :class, sexp("JuliaNamedTuple"))
vs
end

# keep this as a separate method to allow for conversion without the attribute
function sexp(::Type{RClass{:list}}, nt::NamedTuple)
n = length(nt)
vs = protect(allocArray(VecSxp,n))
Expand All @@ -14,20 +26,16 @@ function sexp(::Type{RClass{:list}}, nt::NamedTuple)
vs
end

sexpclass(::NamedTuple) = RClass{:list}
sexpclass(::NamedTuple) = RClass{:JuliaNamedTuple}

rcopytype(::Type{RClass{:JuliaNamedTuple}}, x::Ptr{VecSxp}) = NamedTuple

function rcopy(::Type{NamedTuple}, s::Ptr{VecSxp})
protect(s)
try
names = Symbol[]
vals = Any[]

for k in rcopy(Array{Symbol}, getnames(s))
push!(names, k)
push!(vals, rcopy(s[k]))
end

NamedTuple{(names...,)}(vals)
try
names = Tuple(Symbol(rcopy(n)) for n in getnames(s))
values = rcopy(Tuple, s)
NamedTuple{names}(values)
finally
unprotect(1)
end
Expand All @@ -36,13 +44,12 @@ end
function rcopy(::Type{NamedTuple{names}}, s::Ptr{VecSxp}) where names
protect(s)
try
vals = Any[]
n = rcopy(Array{Symbol}, getnames(s))
n = Tuple(Symbol(rcopy(n)) for n in getnames(s))
if length(intersect(n, names)) != length(names)
throw(ArgumentError("cannot convert to NamedTuple: wrong names"))
end

vals = rcopy(Array, s)
vals = rcopy(Tuple, s)
NamedTuple{names}(vals)
finally
unprotect(1)
Expand All @@ -52,13 +59,12 @@ end
function rcopy(::Type{NamedTuple{names, types}}, s::Ptr{VecSxp}) where {names, types}
protect(s)
try
vals = Any[]
n = rcopy(Array{Symbol}, getnames(s))
n = Tuple(Symbol(rcopy(n)) for n in getnames(s))
if length(intersect(n, names)) != length(names)
throw(ArgumentError("cannot convert to NamedTuple: wrong names"))
end

vals = rcopy(Array, s)
vals = rcopy(Tuple, s)
NamedTuple{names, types}(vals)
finally
unprotect(1)
Expand Down
36 changes: 36 additions & 0 deletions src/convert/tuple.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
function sexp(::Type{RClass{:JuliaTuple}}, t::Tuple)
vs = sexp(RClass{:list}, t)
# mark this as originating from a tuple
# for roundtrippping, which downstream JuliaCall
# relies on
# because of the way S3 classes work, this doesn't break anything on the R side
# and strictly adds more information that we can take advantage of
setattrib!(vs, :class, sexp("JuliaTuple"))
vs
end

function sexp(::Type{RClass{:list}}, t::Tuple)
n = length(t)
vs = protect(allocArray(VecSxp,n))
try
for (i, v) in enumerate(t)
vs[i] = v
end
finally
unprotect(1)
end
vs
end

sexpclass(::Tuple) = RClass{:JuliaTuple}

rcopytype(::Type{RClass{:JuliaTuple}}, x::Ptr{VecSxp}) = Tuple

function rcopy(::Type{T}, s::Ptr{VecSxp}) where {T <: Tuple}
protect(s)
try
T(rcopy(el) for el in s)
finally
unprotect(1)
end
end
3 changes: 3 additions & 0 deletions test/convert/namedtuple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ r = RObject((a="a", d=1))
@test_throws ArgumentError rcopy(typeof(nt), r)
@test_throws ArgumentError rcopy(NamedTuple{(:a,:b,:c)}, r)
@test (rcopy(NamedTuple{(:a,:d)}, r); true)

@test rcopy(RObject(sexp(RClass{:list}, nt))) isa OrderedDict
@test rcopy(RObject(nt)) isa typeof(nt)
15 changes: 15 additions & 0 deletions test/convert/tuple.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
t = ("a", 1, [1,2])
r = RObject(t)
@test r isa RObject{VecSxp}
@test length(r) == length(t)
@test rcopy(Tuple, r) == t
@test rcopy(typeof(t), r) == t
@test rcopy(r) == t
@test rcopy(Array, r) == collect(t)
r[3] = 2.5
me_test = @test_throws MethodError rcopy(typeof(t), r)
@test me_test.value.f === convert
@test me_test.value.args == (Vector{Int64}, 2.5)

@test rcopy(RObject(sexp(RClass{:list}, t))) isa Vector{Any}
@test rcopy(RObject(t)) isa typeof(t)
65 changes: 35 additions & 30 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using RCall
using Test

using DataStructures: OrderedDict
using RCall: RClass

# before RCall does anything
const R_PPSTACKTOP_INITIAL = unsafe_load(cglobal((:R_PPStackTop, RCall.libR), Int))
@info "" R_PPSTACKTOP_INITIAL
Expand Down Expand Up @@ -33,37 +36,39 @@ println(R"sessionInfo()")

println(R"l10n_info()")

# https://github.com/JuliaStats/RCall.jl/issues/68
@test hd == homedir()
@testset "RCall" begin

# https://github.com/JuliaInterop/RCall.jl/issues/206
if (Sys.which("R") !== nothing) && (strip(read(`R RHOME`, String)) == RCall.Rhome)
@test rcopy(Vector{String}, reval(".libPaths()")) == libpaths
end
# https://github.com/JuliaStats/RCall.jl/issues/68
@test hd == homedir()

tests = ["basic",
"convert/base",
"convert/missing",
"convert/datetime",
"convert/dataframe",
"convert/categorical",
"convert/formula",
"convert/namedtuple",
# "convert/axisarray",
"macros",
"namespaces",
"repl",
]

println("Running tests:")

for t in tests
println(t)
tfile = string(t, ".jl")
include(tfile)
end
# https://github.com/JuliaInterop/RCall.jl/issues/206
if (Sys.which("R") !== nothing) && (strip(read(`R RHOME`, String)) == RCall.Rhome)
@test rcopy(Vector{String}, reval(".libPaths()")) == libpaths
end

@info "" RCall.conda_provided_r
tests = ["basic",
"convert/base",
"convert/missing",
"convert/datetime",
"convert/dataframe",
"convert/categorical",
"convert/formula",
"convert/namedtuple",
"convert/tuple",
# "convert/axisarray",
"macros",
"namespaces",
"repl",
]

# make sure we're back where we started
@test unsafe_load(cglobal((:R_PPStackTop, RCall.libR), Int)) == R_PPSTACKTOP_INITIAL
for t in tests
@eval @testset $t begin
include(string($t, ".jl"))
end
end

@info "" RCall.conda_provided_r

# make sure we're back where we started
@test unsafe_load(cglobal((:R_PPStackTop, RCall.libR), Int)) == R_PPSTACKTOP_INITIAL
end

4 comments on commit 5d0ddeb

@palday
Copy link
Collaborator Author

@palday palday commented on 5d0ddeb Jan 8, 2024

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/98492

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

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.14.0 -m "<description of version>" 5d0ddeb262debe775016b1666a2b5d9082287992
git push origin v0.14.0

@palday
Copy link
Collaborator Author

@palday palday commented on 5d0ddeb Jan 8, 2024

Choose a reason for hiding this comment

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

@JuliaRegistrator register

Release notes:

  • Add support for tuples and better round tripping of named tuples
  • The changes here should not be breaking for most users, but may potentially cause problems for JuliaCall, so we do a breaking version bump.

@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 updated: JuliaRegistries/General/98492

Tagging

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.14.0 -m "<description of version>" 5d0ddeb262debe775016b1666a2b5d9082287992
git push origin v0.14.0

Please sign in to comment.