From 0bb0d5335b7afea9b822aacdf533a8716701e239 Mon Sep 17 00:00:00 2001 From: Phillip Alday Date: Wed, 17 Jul 2024 12:04:02 +0000 Subject: [PATCH] Deal with normalization collisions in reimport (#532) * improve rimport * detect name collisions and give a nice warning message * use gensym instead of a default name to avoid collisions of hidden module * allow custom normalization * version bump * use default native arch in CI --- .github/workflows/ci.yml | 4 ---- .gitignore | 1 + Project.toml | 4 +--- src/RCall.jl | 3 --- src/namespaces.jl | 29 +++++++++++++++++++++++------ test/namespaces.jl | 22 +++++++++++++++++++--- 6 files changed, 44 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0c088aa..66e30a29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,17 +24,14 @@ jobs: - '4.0' - '3.4' os: [ubuntu-latest] - arch: [x64] experimental: [false] include: # test macOS and Windows with latest Julia only - os: macOS-latest - arch: x64 version: 1 experimental: false R: 'release' - os: windows-latest - arch: x64 version: 1 experimental: false R: 'release' @@ -43,7 +40,6 @@ jobs: - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 - uses: r-lib/actions/setup-r@v2 with: diff --git a/.gitignore b/.gitignore index bdc40d1b..d85e3290 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ docs/gh-pages docs/site deps/build.log lcov.info +*.cov diff --git a/Project.toml b/Project.toml index 35630648..487e620c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "RCall" uuid = "6f49c342-dc21-5d91-9882-a32aef131414" authors = ["Douglas Bates ", "Randy Lai ", "Simon Byrne "] -version = "0.14.1" +version = "0.14.2" [deps] CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" @@ -10,7 +10,6 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -Missings = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" Preferences = "21216c6a-2e73-6563-6e65-726566657250" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -24,7 +23,6 @@ CategoricalArrays = "0.8, 0.9, 0.10" Conda = "1.4" DataFrames = "0.21, 0.22, 1.0" DataStructures = "0.5, 0.6, 0.7, 0.8, 0.9, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18" -Missings = "0.2, 0.3, 0.4, 1.0" Preferences = "1" Requires = "0.5.2, 1" StatsModels = "0.6, 0.7" diff --git a/src/RCall.jl b/src/RCall.jl index de50f4c7..4cb9e3d9 100644 --- a/src/RCall.jl +++ b/src/RCall.jl @@ -6,9 +6,6 @@ using Dates using Libdl using Random using REPL -if VERSION ≤ v"1.1.1" - using Missings -end using CategoricalArrays using DataFrames using StatsModels diff --git a/src/namespaces.jl b/src/namespaces.jl index 225a6b3d..0351287d 100644 --- a/src/namespaces.jl +++ b/src/namespaces.jl @@ -7,7 +7,7 @@ const reserved = Set{String}([ "false", "true", "Tuple", "rmember", "__package__"]) -cached_namespaces = Dict{String, Module}() +const cached_namespaces = Dict{String, Module}() """ Import an R package as a julia module. @@ -17,18 +17,27 @@ E.g.: `PerformanceAnalytics::charts.Bar` in R becomes `PerformanceAnalytics.char ``` gg = rimport("ggplot2") ``` + +`normalization` is passed directly to `replace` via splatting. """ -function rimport(pkg::String, s::Symbol=:__anonymous__; normalizenames::Bool=true) +function rimport(pkg::String, s::Symbol=gensym(:rimport); + normalizenames::Bool=true, normalization=['.' => '_']) if pkg in keys(cached_namespaces) m = cached_namespaces[pkg] else ns = rcall(:asNamespace, pkg) + # XXX note that R is sensitive to definition order, so do not change the order! members = rcopy(Vector{String}, rcall(:getNamespaceExports, ns)) m = Module(s, false) id = Expr(:const, Expr(:(=), :__package__, pkg)) if normalizenames - exports = [Symbol(replace(x, '.' => '_')) for x in members] + exports = [Symbol(replace(x, normalization...)) for x in members] + dupes = [k for (k, v) in countmap(exports) if v > 1] + if !isempty(dupes) + error("Normalized names are no longer unique: " * + join(dupes, ", ", " and ")) + end else exports = [Symbol(x) for x in members] end @@ -37,13 +46,21 @@ function rimport(pkg::String, s::Symbol=:__anonymous__; normalizenames::Bool=tru collect(eachindex(exports))) consts = [Expr(:const, Expr(:(=), exports[i], - rcall(Symbol("::"), pkg, members[i]))) for i in filtered_indices ] + rcall(Symbol("::"), pkg, members[i]))) for i in filtered_indices] Core.eval(m, Expr(:toplevel, id, consts..., Expr(:export, exports...), :(rmember(x) = ($getindex)($ns, x)))) cached_namespaces[pkg] = m end - m + return m +end +rimport(pkg::Symbol, args...; kwargs...) = rimport(string(pkg), args...; kwargs...) + +function countmap(v::Vector{T}) where {T} + occurrences = Dict{Symbol,Int}() + for el in v + occurrences[el] = get(occurrences, el, 0) + 1 + end + return occurrences end -rimport(pkg::Symbol, s::Symbol=:__anonymous__) = rimport(string(pkg), s) """ Import an R Package as a Julia module. For example, diff --git a/test/namespaces.jl b/test/namespaces.jl index 68fd6634..8026aa19 100644 --- a/test/namespaces.jl +++ b/test/namespaces.jl @@ -6,9 +6,7 @@ module NamespaceTests using RCall using Test - RCall.R""" - has_mass_package = require("MASS") - """ + reval("""has_mass_package = require("MASS")""") RCall.@rget has_mass_package @@ -23,4 +21,22 @@ module NamespaceTests @test rcopy(rcall(ginv, RObject([1 2; 0 4]))) ≈ [1 -0.5; 0 0.25] end + # why exclude Windows? because the tempdir could potentially include + # unicode characters and R can't handle that + if !Sys.iswindows() + if !rcopy(reval("""require("ape")""")) # 418 + @info "installing ape to temporary lib" + tmp = mktempdir() + reval("""lib <- "$(tmp)" + .libPaths(lib) + install.packages("ape", repos="https://cloud.r-project.org", method="wget", lib=lib) + library("ape")""") + end + + @test_throws ErrorException rimport("ape") + + rimport(:ape; normalization=["." => "__"]) + end + # stats is always available + rimport(:stats; normalizenames=false) end