Skip to content

Commit ad13d17

Browse files
authored
Merge pull request #60 from disberd/fix_intermittent_import
2 parents a2775e5 + 31aee2c commit ad13d17

File tree

10 files changed

+155
-111
lines changed

10 files changed

+155
-111
lines changed

src/frompackage/code_parsing.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ function modify_package_using!(ex::Expr, loc, package_dict::Dict, eval_module::M
6767
extracted_package_name = first(package_expr_args)
6868
if extracted_package_name === package_name
6969
# We modify the specific using expression to point to the correct module path
70-
prepend!(package_expr_args, modname_path(fromparent_module[]))
70+
prepend!(package_expr_args, temp_module_path())
7171
end
7272
end
7373
return true

src/frompackage/helpers.jl

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import ..PlutoDevMacros: hide_this_log
22

33
function get_temp_module()
4-
@assert isassigned(fromparent_module) "You have to assing the parent module by calling `maybe_create_module` with a Pluto workspace module as input before you can use `get_temp_module`"
5-
fromparent_module[]
4+
isdefined(Main, TEMP_MODULE_NAME) || return nothing
5+
return getproperty(Main, TEMP_MODULE_NAME)::Module
66
end
77

88
# Extract the module that is the target in dict
9-
get_target_module(dict) = get_target_module(Symbol(dict["name"]))
10-
get_target_module(mod_name::Symbol) = getfield(get_temp_module(), mod_name)
9+
get_target_module(dict) = dict["Created Module"]
1110

1211
function get_target_uuid(dict)
1312
uuid = get(dict, "uuid", nothing)
@@ -118,7 +117,7 @@ function get_package_data(packagepath::AbstractString)
118117
return package_data
119118
end
120119

121-
## getfirst
120+
# Get the first element in itr that satisfies predicate p, or nothing if itr is empty or no elements satisfy p
122121
function getfirst(p, itr)
123122
for el in itr
124123
p(el) && return el
@@ -127,45 +126,48 @@ function getfirst(p, itr)
127126
end
128127
getfirst(itr) = getfirst(x -> true, itr)
129128

130-
## filterednames
131-
function filterednames(m::Module, caller_module = nothing; all = true, imported = true, explicit_names = nothing, package_dict = nothing)
129+
## Similar to names but allows to exclude names and add explicit ones. It also filter names based on whether they are defined already in the caller module
130+
function filterednames(m::Module; all = true, imported = true, explicit_names = Set{Symbol}(), caller_module::Module)
132131
excluded = (:eval, :include, :_fromparent_dict_, Symbol("@bind"))
133-
mod_names = names(m;all, imported)
134-
filter_args = if explicit_names isa Set{Symbol}
135-
for name in mod_names
136-
push!(explicit_names, name)
137-
end
138-
collect(explicit_names)
139-
else
140-
mod_names
141-
end
142-
filter_func = filterednames_filter_func(m; excluded, caller_module, package_dict)
132+
mod_names = names(m; all, imported)
133+
filter_args = union(mod_names, explicit_names)
134+
filter_func = filterednames_filter_func(;excluded, caller_module)
143135
filter(filter_func, filter_args)
144136
end
145137

146-
function filterednames_filter_func(m; excluded, caller_module, package_dict)
147-
f(s) = let excluded = excluded, caller_module = caller_module, package_dict = package_dict
138+
function has_ancestor_module(target::Module, ancestor_name::Symbol; previous = nothing, only_rootmodule = false)
139+
has_ancestor_module(target, (ancestor_name,); previous, only_rootmodule)
140+
end
141+
function has_ancestor_module(target::Module, ancestor_names; previous = nothing, only_rootmodule = false)
142+
nm = nameof(target)
143+
ancestor_found = nm in ancestor_names
144+
!only_rootmodule && ancestor_found && return true # Ancestor found, and no check on only_rootmodule
145+
nm === previous && return ancestor_found # The target is the same as previous, so we reached a top-level module. We return whether the ancestor was found and is a parent of itself
146+
return has_ancestor_module(parentmodule(target), ancestor_names; previous = nm, only_rootmodule)
147+
end
148+
149+
# This returns two flags: whether the name can be included and whether a warning should be generated
150+
function can_import_in_caller(name::Symbol, caller::Module)
151+
isdefined(caller, name) || return true, false # If is not defined we can surely import it
152+
owner = which(caller, name)
153+
# Skip (and do not warn) for things defined in Base or Core
154+
invalid_ancestor = has_ancestor_module(owner, (:Base, :Core, :Markdown, :InteractiveUtils))
155+
invalid_ancestor && return false, false
156+
# We check if the name is inside the list of symbols imported by the previous module
157+
in_previous = name in PREVIOUS_CATCHALL_NAMES
158+
return in_previous, !in_previous
159+
end
160+
161+
function filterednames_filter_func(;excluded, caller_module)
162+
f(s) = let excluded = excluded, caller = caller_module
148163
Base.isgensym(s) && return false
149164
s in excluded && return false
150-
if caller_module isa Module
151-
previous_target_module = get_stored_module(package_dict)
152-
# We check and avoid putting in scope symbols which are already in the caller module
153-
isdefined(caller_module, s) || return true
154-
# Here we have to extract the symbols to compare them
155-
mod_val = getfield(m, s)
156-
caller_val = getfield(caller_module, s)
157-
if caller_val !== mod_val
158-
if isdefined(previous_target_module, s) && caller_val === getfield(previous_target_module, s)
159-
# We are just replacing the previous implementation of this call's target package, so we want to overwrite
160-
return true
161-
else
162-
@warn "Symbol `:$s`, is already defined in the caller module and points to a different object. Skipping"
163-
end
164-
end
165-
return false
166-
else # We don't check for names clashes with a caller module
167-
return true
165+
should_include, should_warn = can_import_in_caller(s, caller)
166+
if should_warn
167+
owner = which(caller, s)
168+
@warn "The name `$s`, defined in $owner, is already present in the caller module and will not be imported."
168169
end
170+
return should_include
169171
end
170172
return f
171173
end
@@ -280,7 +282,7 @@ end
280282
# This relies on Base internals (and even the C API) but will allow make the loaded module behave more like if we simply did `using TargetPackage` in the REPL
281283
function register_target_module_as_root(package_dict)
282284
name_str = package_dict["name"]
283-
m = get_target_module(Symbol(name_str))
285+
m = get_target_module(package_dict)
284286
id = get_target_pkgid(package_dict)
285287
uuid = id.uuid
286288
entry_point = package_dict["file"]
@@ -317,12 +319,18 @@ function try_load_extensions(package_dict::Dict)
317319
end
318320

319321
# This function will get the module stored in the created_modules dict based on the entry point
320-
get_stored_module(package_dict) = get_stored_module(package_dict["uuid"])
321-
get_stored_module(key::String) = get(created_modules, key, nothing)
322+
get_stored_module() = STORED_MODULE[]
322323
# This will store in it
323-
update_stored_module(key::String, m::Module) = created_modules[key] = m
324+
update_stored_module(m::Module) = STORED_MODULE[] = m
324325
function update_stored_module(package_dict::Dict)
325-
uuid = package_dict["uuid"]
326326
m = get_target_module(package_dict)
327-
update_stored_module(uuid, m)
327+
update_stored_module(m)
328+
end
329+
330+
overwrite_imported_symbols(package_dict::Dict) = overwrite_imported_symbols(get(Set{Symbol}, package_dict, "Catchall Imported Symbols"))
331+
# This overwrites the PREVIOUSLY_IMPORTED_SYMBOLS with the contents of new_symbols
332+
function overwrite_imported_symbols(new_symbols)
333+
empty!(PREVIOUS_CATCHALL_NAMES)
334+
union!(PREVIOUS_CATCHALL_NAMES, new_symbols)
335+
nothing
328336
end

src/frompackage/input_parsing.jl

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -145,20 +145,8 @@ function import_type(args, dict)
145145
error("The provided import expression is not supported, please look at @frompackage documentation to see the supported imports")
146146
end
147147

148-
# Get the full path of the module as array of Symbols starting from Main
149-
function modname_path(m::Module)
150-
args = [nameof(m)]
151-
m_old = m
152-
_m = parentmodule(m)
153-
while _m !== m_old && _m !== Main
154-
m_old = _m
155-
pushfirst!(args, nameof(_m))
156-
_m = parentmodule(_m)
157-
end
158-
_m === Main || error("modname_path did not reach Main, this is not expected")
159-
pushfirst!(args, nameof(_m))
160-
return args
161-
end
148+
# This is the path of the temp module which will be prepended to the package name
149+
temp_module_path() = (:Main, TEMP_MODULE_NAME)
162150

163151
## process outside pluto
164152
# We parse all the expressions in the block provided as input to @fromparent
@@ -259,12 +247,12 @@ function process_imported_nameargs!(args, dict)
259247
end
260248
## Per-type versions
261249
function process_imported_nameargs!(args, dict, t::FromPackageImport)
262-
name_init = modname_path(fromparent_module[])
250+
name_init = temp_module_path()
263251
args[1] = t.mod_name
264252
prepend!(args, name_init)
265253
end
266254
function process_imported_nameargs!(args, dict, t::Union{FromParentImport, RelativeImport})
267-
name_init = modname_path(fromparent_module[])
255+
name_init = temp_module_path()
268256
# Here transform the relative module name to the one based on the full loaded module path
269257
target_path = get(dict, "Target Path", []) |> reverse
270258
isempty(target_path) && error("The current file was not found included in the loaded module $(t.mod_name), so you can't use relative path imports")
@@ -285,7 +273,7 @@ function process_imported_nameargs!(args, dict, t::FromDepsImport)
285273
maybe_add_loaded_module(t.id)
286274
deps_module_name = :_LoadedModules_
287275
args[1] = deps_module_name # We replace `>` with _LoadedModules_
288-
name_init = modname_path(fromparent_module[])
276+
name_init = temp_module_path()
289277
prepend!(args, name_init)
290278
return nothing
291279
end
@@ -317,7 +305,7 @@ function should_include_using_names!(ex)
317305
end
318306

319307
## parseinput
320-
function parseinput(ex, package_dict; caller_module = nothing)
308+
function parseinput(ex, package_dict; caller_module)
321309
include_using = should_include_using_names!(ex)
322310
# We get the module
323311
modname_expr, importednames_exprs = extract_import_args(ex)
@@ -346,10 +334,11 @@ function parseinput(ex, package_dict; caller_module = nothing)
346334
explicit_names = if include_using
347335
package_dict["Using Names"].explicit_names
348336
else
349-
nothing
337+
Set{Symbol}()
350338
end
351-
# We extract the imported names either due to catchall or due to the standard using
352-
imported_names = filterednames(_mod, caller_module; all = catchall, imported = catchall, explicit_names, package_dict)
339+
imported_names = filterednames(_mod; all = catchall, imported = catchall, explicit_names, caller_module)
340+
# We add the imported names to the set for tracking names imported by this macrocall
341+
union!(get!(Set{Symbol}, package_dict, "Catchall Imported Symbols"), imported_names)
353342
# At this point we have all the names and we just have to create the final expression
354343
importednames_exprs = map(n -> Expr(:., n), imported_names)
355344
return reconstruct_import_expr(modname_expr, importednames_exprs)

src/frompackage/loading.jl

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,16 @@ function eval_module_expr(parent_module, ex, dict)
124124
return out isa StopEval ? out : nothing
125125
end
126126

127-
function maybe_create_module(m::Module)
128-
if !isassigned(fromparent_module)
129-
fromparent_m = Core.eval(m, :(module $(gensym(:frompackage))
130-
end))
131-
# We create the dummy module where all the direct dependencies will be loaded
132-
Core.eval(fromparent_m, :(module _DirectDeps_ end))
133-
# We also set a reference to LoadedModules for access from the notebook
134-
Core.eval(fromparent_m, :(const _LoadedModules_ = $LoadedModules))
135-
fromparent_module[] = fromparent_m
136-
end
137-
return fromparent_module[]
127+
function maybe_create_module()
128+
m = get_temp_module()
129+
isnothing(m) || return m
130+
fromparent_m = Core.eval(Main, :(module $TEMP_MODULE_NAME
131+
end))
132+
# We create the dummy module where all the direct dependencies will be loaded
133+
Core.eval(fromparent_m, :(module _DirectDeps_ end))
134+
# We also set a reference to LoadedModules for access from the notebook
135+
Core.eval(fromparent_m, :(const _LoadedModules_ = $LoadedModules))
136+
return fromparent_m
138137
end
139138

140139
# This will explicitly import each direct dependency of the package inside the LoadedModules module. Loading all of the direct dependencies will help make every dependency available even if not directly loaded in the target source code.
@@ -168,9 +167,15 @@ function load_module_in_caller(mod_exp::Expr, package_dict::Dict, caller_module)
168167
target_file = package_dict["target"]
169168
ecg = default_ecg()
170169
# If the module Reference inside fromparent_module is not assigned, we create the module in the calling workspace and assign it
171-
_MODULE_ = maybe_create_module(caller_module)
170+
_MODULE_ = maybe_create_module()
172171
# We reset the module path in case it was not cleaned
173172
mod_name = mod_exp.args[2]
173+
# We reset the list of symbols if we loaded a different module
174+
stored_module = get_stored_module()
175+
if !isnothing(stored_module) && nameof(stored_module) !== mod_name
176+
# We reset the list of previous symbols
177+
empty!(PREVIOUS_CATCHALL_NAMES)
178+
end
174179
# We inject the project in the LOAD_PATH if it is not present already
175180
add_loadpath(ecg; should_prepend = Settings.get_setting(package_dict, :SHOULD_PREPEND_LOAD_PATH))
176181
# We start by loading each of the direct dependencies in the LoadedModules submodule
@@ -184,7 +189,9 @@ function load_module_in_caller(mod_exp::Expr, package_dict::Dict, caller_module)
184189
rethrow(e)
185190
end
186191
# Get the moduleof the parent package
187-
__module = getfield(_MODULE_, mod_name)
192+
__module = getproperty(_MODULE_, mod_name)::Module
193+
# We put the module in the dict
194+
package_dict["Created Module"] = __module
188195
# We put the dict inside the loaded module
189196
Core.eval(__module, :(_fromparent_dict_ = $package_dict))
190197
# Register this module as root module.

src/frompackage/macro.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ function frompackage(ex, target_file, caller, caller_module; macroname)
8383
end
8484
# We update th stored root module
8585
update_stored_module(package_dict)
86+
# We put the included names in PREVIOUS_CATCHALL_NAMES
87+
overwrite_imported_symbols(package_dict)
8688
# We call at runtime the function to trigger extensions loading
8789
push!(args, :($try_load_extensions($package_dict)))
8890
# We wrap the import expressions inside a try-catch, as those also correctly work from there.

src/frompackage/types.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ const _stdlibs = first.(values(Pkg.Types.stdlibs()))
22

33
const default_pkg_io = Ref{IO}(devnull)
44

5-
const fromparent_module = Ref{Module}()
5+
const TEMP_MODULE_NAME = :_FromPackage_TempModule_
6+
const STORED_MODULE = Ref{Union{Module, Nothing}}(nothing)
7+
const PREVIOUS_CATCHALL_NAMES = Set{Symbol}()
68
const macro_cell = Ref("undefined")
79
const manifest_names = ("JuliaManifest.toml", "Manifest.toml")
810

test/TestUsingNames/src/TestUsingNames.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ using Base64
55
export top_level_func
66
top_level_func() = 1
77
clash_name = 5
8+
rand_variable = rand()
89

910
module Test1
1011
using Example

test/TestUsingNames/test_notebook2.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,14 @@ isdefined(@__MODULE__, :base64encode) || error("base64encode from Base64 should
4949
clash_name === 0 || error("The clashed name was not handled correctly")
5050
╠═╡ =#
5151

52+
# ╔═╡ 4492d516-2b23-45b7-bf76-7458e7352fea
53+
#=╠═╡
54+
rand_variable # This should change at every re-run
55+
╠═╡ =#
56+
5257
# ╔═╡ Cell order:
5358
# ╠═4f8def86-f90b-4f74-ac47-93fe6e437cee
5459
# ╠═ac3d261a-86c9-453f-9d86-23a8f30ca583
5560
# ╠═dd3f662f-e2ce-422d-a91a-487a4da359cc
5661
# ╠═c72f2544-eb2e-4ed6-a89b-495ead20b5f6
62+
# ╠═4492d516-2b23-45b7-bf76-7458e7352fea

0 commit comments

Comments
 (0)