-
Notifications
You must be signed in to change notification settings - Fork 110
/
packagedef.jl
1393 lines (1268 loc) · 50.8 KB
/
packagedef.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@optlevel"))
@eval Base.Experimental.@optlevel 1
end
using FileWatching, REPL, Distributed, UUIDs
import LibGit2
using Base: PkgId
using Base.Meta: isexpr
using Core: CodeInfo
export revise, includet, entr, MethodSummary
"""
Revise.watching_files[]
Returns `true` if we watch files rather than their containing directory.
FreeBSD and NFS-mounted systems should watch files, otherwise we prefer to watch
directories.
"""
const watching_files = Ref(Sys.KERNEL === :FreeBSD)
"""
Revise.polling_files[]
Returns `true` if we should poll the filesystem for changes to the files that define
loaded code. It is preferable to avoid polling, instead relying on operating system
notifications via `FileWatching.watch_file`. However, NFS-mounted
filesystems (and perhaps others) do not support file-watching, so for code stored
on such filesystems you should turn polling on.
See the documentation for the `JULIA_REVISE_POLL` environment variable.
"""
const polling_files = Ref(false)
function wait_changed(file)
try
polling_files[] ? poll_file(file) : watch_file(file)
catch err
if Sys.islinux() && err isa Base.IOError && err.code == -28 # ENOSPC
@warn """Your operating system has run out of inotify capacity.
Check the current value with `cat /proc/sys/fs/inotify/max_user_watches`.
Set it to a higher level with, e.g., `echo 65536 | sudo tee -a /proc/sys/fs/inotify/max_user_watches`.
This requires having administrative privileges on your machine (or talk to your sysadmin).
See https://github.com/timholy/Revise.jl/issues/26 for more information."""
end
rethrow(err)
end
return nothing
end
"""
Revise.tracking_Main_includes[]
Returns `true` if files directly included from the REPL should be tracked.
The default is `false`. See the documentation regarding the `JULIA_REVISE_INCLUDE`
environment variable to customize it.
"""
const tracking_Main_includes = Ref(false)
include("relocatable_exprs.jl")
include("types.jl")
include("utils.jl")
include("parsing.jl")
include("lowered.jl")
include("pkgs.jl")
include("git.jl")
include("recipes.jl")
include("logging.jl")
include("callbacks.jl")
### Globals to keep track of state
"""
Revise.watched_files
Global variable, `watched_files[dirname]` returns the collection of files in `dirname`
that we're monitoring for changes. The returned value has type [`Revise.WatchList`](@ref).
This variable allows us to watch directories rather than files, reducing the burden on
the OS.
"""
const watched_files = Dict{String,WatchList}()
"""
Revise.revision_queue
Global variable, `revision_queue` holds `(pkgdata,filename)` pairs that we need to revise, meaning
that these files have changed since we last processed a revision.
This list gets populated by callbacks that watch directories for updates.
"""
const revision_queue = Set{Tuple{PkgData,String}}()
"""
Revise.queue_errors
Global variable, maps `(pkgdata, filename)` pairs that errored upon last revision to
`(exception, backtrace)`.
"""
const queue_errors = Dict{Tuple{PkgData,String},Tuple{Exception, Any}}()
"""
Revise.NOPACKAGE
Global variable; default `PkgId` used for files which do not belong to any
package, but still have to be watched because user callbacks have been
registered for them.
"""
const NOPACKAGE = PkgId(nothing, "")
"""
Revise.pkgdatas
`pkgdatas` is the core information that tracks the relationship between source code
and julia objects, and allows re-evaluation of code in the proper module scope.
It is a dictionary indexed by PkgId:
`pkgdatas[id]` returns a value of type [`Revise.PkgData`](@ref).
"""
const pkgdatas = Dict{PkgId,PkgData}(NOPACKAGE => PkgData(NOPACKAGE))
const moduledeps = Dict{Module,DepDict}()
function get_depdict(mod::Module)
if !haskey(moduledeps, mod)
moduledeps[mod] = DepDict()
end
return moduledeps[mod]
end
"""
Revise.included_files
Global variable, `included_files` gets populated by callbacks we register with `include`.
It's used to track non-precompiled packages and, optionally, user scripts (see docs on
`JULIA_REVISE_INCLUDE`).
"""
const included_files = Tuple{Module,String}[] # (module, filename)
"""
Revise.basesrccache
Full path to the running Julia's cache of source code defining `Base`.
"""
const basesrccache = normpath(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "base.cache"))
"""
Revise.basebuilddir
Julia's top-level directory when Julia was built, as recorded by the entries in
`Base._included_files`.
"""
const basebuilddir = begin
sysimg = filter(x->endswith(x[2], "sysimg.jl"), Base._included_files)[1][2]
dirname(dirname(sysimg))
end
"""
Revise.juliadir
Constant specifying full path to julia top-level source directory.
This should be reliable even for local builds, cross-builds, and binary installs.
"""
const juliadir = begin
local jldir = basebuilddir
try
isdir(joinpath(jldir, "base")) || throw(ErrorException("$(jldir) does not have \"base\""))
catch
# Binaries probably end up here. We fall back on Sys.BINDIR
jldir = joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia")
if !isdir(joinpath(jldir, "base"))
while true
trydir = joinpath(jldir, "base")
isdir(trydir) && break
trydir = joinpath(jldir, "share", "julia", "base")
if isdir(trydir)
jldir = joinpath(jldir, "share", "julia")
break
end
jldirnext = dirname(jldir)
jldirnext == jldir && break
jldir = jldirnext
end
end
end
normpath(jldir)
end
const cache_file_key = Dict{String,String}() # corrected=>uncorrected filenames
const src_file_key = Dict{String,String}() # uncorrected=>corrected filenames
"""
Revise.dont_watch_pkgs
Global variable, use `push!(Revise.dont_watch_pkgs, :MyPackage)` to prevent Revise
from tracking changes to `MyPackage`. You can do this from the REPL or from your
`.julia/config/startup.jl` file.
See also [`Revise.silence`](@ref).
"""
const dont_watch_pkgs = Set{Symbol}()
const silence_pkgs = Set{Symbol}()
const depsdir = joinpath(dirname(@__DIR__), "deps")
const silencefile = Ref(joinpath(depsdir, "silence.txt")) # Ref so that tests don't clobber
##
## The inputs are sets of expressions found in each file.
## Some of those expressions will generate methods which are identified via their signatures.
## From "old" expressions we know their corresponding signatures, but from "new"
## expressions we have not yet computed them. This makes old and new asymmetric.
##
## Strategy:
## - For every old expr not found in the new ones,
## + delete the corresponding methods (using the signatures we've previously computed)
## + remove the sig entries from CodeTracking.method_info (")
## Best to do all the deletion first (across all files and modules) in case a method is
## simply being moved from one file to another.
## - For every new expr found among the old ones,
## + update the location info in CodeTracking.method_info
## - For every new expr not found in the old ones,
## + eval the expr
## + extract signatures
## + add to the ModuleExprsSigs
## + add to CodeTracking.method_info
##
## Interestingly, the ex=>sigs link may not be the same as the sigs=>ex link.
## Consider a conditional block,
## if Sys.islinux()
## f() = 1
## g() = 2
## else
## g() = 3
## end
## From the standpoint of Revise's diff-and-patch functionality, we should look for
## diffs in this entire block. (Really good backedge support---or a variant of `lower` that
## links back to the specific expression---might change this, but for
## now this is the right strategy.) From the standpoint of CodeTracking, we should
## link the signature to the actual method-defining expression (either :(f() = 1) or :(g() = 2)).
function delete_missing!(exs_sigs_old::ExprsSigs, exs_sigs_new)
with_logger(_debug_logger) do
for (ex, sigs) in exs_sigs_old
haskey(exs_sigs_new, ex) && continue
# ex was deleted
sigs === nothing && continue
for sig in sigs
ret = Base._methods_by_ftype(sig, -1, typemax(UInt))
success = false
if !isempty(ret)
m = ret[end][3]::Method # the last method returned is the least-specific that matches, and thus most likely to be type-equal
methsig = m.sig
if sig <: methsig && methsig <: sig
locdefs = get(CodeTracking.method_info, sig, nothing)
if isa(locdefs, Vector{Tuple{LineNumberNode,Expr}})
if length(locdefs) > 1
# Just delete this reference but keep the method
line = firstline(ex)
ld = map(pr->linediff(line, pr[1]), locdefs)
idx = argmin(ld)
@assert ld[idx] < typemax(eltype(ld))
deleteat!(locdefs, idx)
continue
else
@assert length(locdefs) == 1
end
end
@debug "DeleteMethod" _group="Action" time=time() deltainfo=(sig, MethodSummary(m))
# Delete the corresponding methods
for p in workers()
try # guard against serialization errors if the type isn't defined on the worker
remotecall(Core.eval, p, Main, :(delete_method_by_sig($sig)))
catch
end
end
Base.delete_method(m)
# Remove the entries from CodeTracking data
delete!(CodeTracking.method_info, sig)
# Remove frame from JuliaInterpreter, if applicable. Otherwise debuggers
# may erroneously work with outdated code (265-like problems)
if haskey(JuliaInterpreter.framedict, m)
delete!(JuliaInterpreter.framedict, m)
end
if isdefined(m, :generator)
# defensively delete all generated functions
empty!(JuliaInterpreter.genframedict)
end
success = true
end
end
if !success
@debug "FailedDeletion" _group="Action" time=time() deltainfo=(sig,)
end
end
end
end
return exs_sigs_old
end
const empty_exs_sigs = ExprsSigs()
function delete_missing!(mod_exs_sigs_old::ModuleExprsSigs, mod_exs_sigs_new)
for (mod, exs_sigs_old) in mod_exs_sigs_old
exs_sigs_new = get(mod_exs_sigs_new, mod, empty_exs_sigs)
delete_missing!(exs_sigs_old, exs_sigs_new)
end
return mod_exs_sigs_old
end
function eval_rex(rex::RelocatableExpr, exs_sigs_old::ExprsSigs, mod::Module; mode::Symbol=:eval)
sigs, includes = nothing, nothing
with_logger(_debug_logger) do
rexo = getkey(exs_sigs_old, rex, nothing)
# extract the signatures and update the line info
if rexo === nothing
ex = rex.ex
# ex is not present in old
@debug "Eval" _group="Action" time=time() deltainfo=(mod, ex)
sigs, deps, includes, thunk = eval_with_signatures(mod, ex; mode=mode) # All signatures defined by `ex`
if VERSION < v"1.3.0" || !isexpr(thunk, :thunk)
thunk = ex
end
if myid() == 1
for p in workers()
p == myid() && continue
try # don't error if `mod` isn't defined on the worker
remotecall(Core.eval, p, mod, thunk)
catch
end
end
end
storedeps(deps, rex, mod)
else
sigs = exs_sigs_old[rexo]
# Update location info
ln, lno = firstline(unwrap(rex)), firstline(unwrap(rexo))
if sigs !== nothing && !isempty(sigs) && ln != lno
@debug "LineOffset" _group="Action" time=time() deltainfo=(sigs, lno=>ln)
for sig in sigs
locdefs = CodeTracking.method_info[sig]
ld = map(pr->linediff(lno, pr[1]), locdefs)
idx = argmin(ld)
if ld[idx] === typemax(eltype(ld))
# println("Missing linediff for $lno and $(first.(locdefs)) with ", rex.ex)
idx = length(locdefs)
end
methloc, methdef = locdefs[idx]
locdefs[idx] = (newloc(methloc, ln, lno), methdef)
end
end
end
end
return sigs, includes
end
# These are typically bypassed in favor of expression-by-expression evaluation to
# allow handling of new `include` statements.
function eval_new!(exs_sigs_new::ExprsSigs, exs_sigs_old, mod::Module; mode::Symbol=:eval)
includes = Vector{Pair{Module,String}}()
for rex in keys(exs_sigs_new)
sigs, _includes = eval_rex(rex, exs_sigs_old, mod; mode=mode)
if sigs !== nothing
exs_sigs_new[rex] = sigs
end
if _includes !== nothing
append!(includes, _includes)
end
end
return exs_sigs_new, includes
end
function eval_new!(mod_exs_sigs_new::ModuleExprsSigs, mod_exs_sigs_old; mode::Symbol=:eval)
includes = Vector{Pair{Module,String}}()
for (mod, exs_sigs_new) in mod_exs_sigs_new
# Allow packages to override the supplied mode
if isdefined(mod, :__revise_mode__)
mode = getfield(mod, :__revise_mode__)::Symbol
end
exs_sigs_old = get(mod_exs_sigs_old, mod, empty_exs_sigs)
_, _includes = eval_new!(exs_sigs_new, exs_sigs_old, mod; mode=mode)
append!(includes, _includes)
end
return mod_exs_sigs_new, includes
end
"""
CodeTrackingMethodInfo(ex::Expr)
Create a cache for storing information about method definitions.
Adding signatures to such an object inserts them into `CodeTracking.method_info`,
which maps signature Tuple-types to `(lnn::LineNumberNode, ex::Expr)` pairs.
Because method signatures are unique within a module, this is the foundation for
identifying methods in a manner independent of source-code location.
It also has the following fields:
- `exprstack`: used when descending into `@eval` statements (via `push_expr` and `pop_expr!`)
`ex` (used in creating the `CodeTrackingMethodInfo` object) is the first entry in the stack.
- `allsigs`: a list of all method signatures defined by a given expression
- `deps`: list of top-level named objects (`Symbol`s and `GlobalRef`s) that method definitions
in this block depend on. For example, `if Sys.iswindows() f() = 1 else f() = 2 end` would
store `Sys.iswindows` here.
- `includes`: a list of `module=>filename` for any `include` statements encountered while the
expression was parsed.
"""
struct CodeTrackingMethodInfo
exprstack::Vector{Expr}
allsigs::Vector{Any}
deps::Set{Union{GlobalRef,Symbol}}
includes::Vector{Pair{Module,String}}
end
CodeTrackingMethodInfo(ex::Expr) = CodeTrackingMethodInfo([ex], Any[], Set{Union{GlobalRef,Symbol}}(), Pair{Module,String}[])
function add_signature!(methodinfo::CodeTrackingMethodInfo, @nospecialize(sig), ln)
locdefs = get(CodeTracking.method_info, sig, nothing)
locdefs === nothing && (locdefs = CodeTracking.method_info[sig] = Tuple{LineNumberNode,Expr}[])
newdef = unwrap(methodinfo.exprstack[end])
if !any(locdef->locdef[1] == ln && isequal(RelocatableExpr(locdef[2]), RelocatableExpr(newdef)), locdefs)
push!(locdefs, (fixpath(ln), newdef))
end
push!(methodinfo.allsigs, sig)
return methodinfo
end
push_expr!(methodinfo::CodeTrackingMethodInfo, mod::Module, ex::Expr) = (push!(methodinfo.exprstack, ex); methodinfo)
pop_expr!(methodinfo::CodeTrackingMethodInfo) = (pop!(methodinfo.exprstack); methodinfo)
function add_dependencies!(methodinfo::CodeTrackingMethodInfo, edges::CodeEdges, src, musteval)
isempty(src.code) && return methodinfo
stmt1 = first(src.code)
if (isexpr(stmt1, :gotoifnot) && (dep = (stmt1::Expr).args[1]; isa(dep, Union{GlobalRef,Symbol}))) ||
(is_GotoIfNot(stmt1) && (dep = stmt1.cond; isa(dep, Union{GlobalRef,Symbol})))
# This is basically a hack to look for symbols that control definition of methods via a conditional.
# It is aimed at solving #249, but this will have to be generalized for anything real.
for (stmt, me) in zip(src.code, musteval)
me || continue
if hastrackedexpr(stmt)[1]
push!(methodinfo.deps, dep)
break
end
end
end
# for (dep, lines) in be.byname
# for ln in lines
# stmt = src.code[ln]
# if isexpr(stmt, :(=)) && stmt.args[1] == dep
# continue
# else
# push!(methodinfo.deps, dep)
# end
# end
# end
return methodinfo
end
function add_includes!(methodinfo::CodeTrackingMethodInfo, mod::Module, filename)
push!(methodinfo.includes, mod=>filename)
return methodinfo
end
# Eval and insert into CodeTracking data
function eval_with_signatures(mod, ex::Expr; mode=:eval, kwargs...)
methodinfo = CodeTrackingMethodInfo(ex)
docexprs = DocExprs()
_, frame = methods_by_execution!(finish_and_return!, methodinfo, docexprs, mod, ex; mode=mode, kwargs...)
return methodinfo.allsigs, methodinfo.deps, methodinfo.includes, frame
end
function instantiate_sigs!(modexsigs::ModuleExprsSigs; mode=:sigs, kwargs...)
for (mod, exsigs) in modexsigs
for rex in keys(exsigs)
is_doc_expr(rex.ex) && continue
sigs, deps, _ = eval_with_signatures(mod, rex.ex; mode=mode, kwargs...)
exsigs[rex] = sigs
storedeps(deps, rex, mod)
end
end
return modexsigs
end
function storedeps(deps, rex, mod)
for dep in deps
if isa(dep, GlobalRef)
haskey(moduledeps, dep.mod) || continue
ddict, sym = get_depdict(dep.mod), dep.name
else
ddict, sym = get_depdict(mod), dep
end
if !haskey(ddict, sym)
ddict[sym] = Set{DepDictVals}()
end
push!(ddict[sym], (mod, rex))
end
return rex
end
# This is intended for testing purposes, but not general use. The key problem is
# that it doesn't properly handle methods that move from one file to another; there is the
# risk you could end up deleting the method altogether depending on the order in which you
# process these.
# See `revise` for the proper approach.
function eval_revised(mod_exs_sigs_new, mod_exs_sigs_old)
delete_missing!(mod_exs_sigs_old, mod_exs_sigs_new)
eval_new!(mod_exs_sigs_new, mod_exs_sigs_old) # note: drops `includes`
instantiate_sigs!(mod_exs_sigs_new)
end
"""
Revise.init_watching(files)
Revise.init_watching(pkgdata::PkgData, files)
For every filename in `files`, monitor the filesystem for updates. When the file is
updated, either [`Revise.revise_dir_queued`](@ref) or [`Revise.revise_file_queued`](@ref) will
be called.
Use the `pkgdata` version if the files are supplied using relative paths.
"""
function init_watching(pkgdata::PkgData, files=srcfiles(pkgdata))
udirs = Set{String}()
for file in files
dir, basename = splitdir(file)
dirfull = joinpath(basedir(pkgdata), dir)
already_watching = haskey(watched_files, dirfull)
already_watching || (watched_files[dirfull] = WatchList())
push!(watched_files[dirfull], basename=>pkgdata)
if watching_files[]
fwatcher = TaskThunk(revise_file_queued, (pkgdata, file))
schedule(Task(fwatcher))
else
already_watching || push!(udirs, dir)
end
end
for dir in udirs
dirfull = joinpath(basedir(pkgdata), dir)
updatetime!(watched_files[dirfull])
if !watching_files[]
dwatcher = TaskThunk(revise_dir_queued, (dirfull,))
schedule(Task(dwatcher))
end
end
return nothing
end
init_watching(files) = init_watching(pkgdatas[NOPACKAGE], files)
"""
revise_dir_queued(dirname)
Wait for one or more of the files registered in `Revise.watched_files[dirname]` to be
modified, and then queue the corresponding files on [`Revise.revision_queue`](@ref).
This is generally called via a [`Revise.TaskThunk`](@ref).
"""
@noinline function revise_dir_queued(dirname)
@assert isabspath(dirname)
if !isdir(dirname)
sleep(0.1) # in case git has done a delete/replace cycle
end
stillwatching = true
while stillwatching
if !isdir(dirname)
with_logger(SimpleLogger(stderr)) do
@warn "$dirname is not an existing directory, Revise is not watching"
end
break
end
latestfiles, stillwatching = watch_files_via_dir(dirname) # will block here until file(s) change
for (file, id) in latestfiles
key = joinpath(dirname, file)
if key in keys(user_callbacks_by_file)
union!(user_callbacks_queue, user_callbacks_by_file[key])
notify(revision_event)
end
if id != NOPACKAGE
pkgdata = pkgdatas[id]
if hasfile(pkgdata, key) # issue #228
push!(revision_queue, (pkgdata, relpath(key, pkgdata)))
notify(revision_event)
end
end
end
end
return
end
# See #66.
"""
revise_file_queued(pkgdata::PkgData, filename)
Wait for modifications to `filename`, and then queue the corresponding files on [`Revise.revision_queue`](@ref).
This is generally called via a [`Revise.TaskThunk`](@ref).
This is used only on platforms (like BSD) which cannot use [`Revise.revise_dir_queued`](@ref).
"""
function revise_file_queued(pkgdata::PkgData, file)
if !isabspath(file)
file = joinpath(basedir(pkgdata), file)
end
if !file_exists(file)
sleep(0.1) # in case git has done a delete/replace cycle
end
dirfull, basename = splitdir(file)
stillwatching = true
while stillwatching
if !file_exists(file) && !isdir(file)
let file=file
with_logger(SimpleLogger(stderr)) do
@warn "$file is not an existing file, Revise is not watching"
end
end
notify(revision_event)
break
end
try
wait_changed(file) # will block here until the file changes
catch e
# issue #459
(isa(e, InterruptException) && throwto_repl(e)) || throw(e)
end
if file in keys(user_callbacks_by_file)
union!(user_callbacks_queue, user_callbacks_by_file[file])
notify(revision_event)
end
# Check to see if we're still watching this file
stillwatching = haskey(watched_files, dirfull)
PkgId(pkgdata) != NOPACKAGE && push!(revision_queue, (pkgdata, relpath(file, pkgdata)))
end
return
end
# Because we delete first, we have to make sure we've parsed the file
function handle_deletions(pkgdata, file)
fi = maybe_parse_from_cache!(pkgdata, file)
maybe_extract_sigs!(fi)
mexsold = fi.modexsigs
filep = normpath(joinpath(basedir(pkgdata), file))
topmod = first(keys(mexsold))
fileok = file_exists(filep)
mexsnew = fileok ? parse_source(filep, topmod) : ModuleExprsSigs(topmod)
if mexsnew !== nothing
delete_missing!(mexsold, mexsnew)
end
if !fileok
@warn("$filep no longer exists, deleted all methods")
idx = fileindex(pkgdata, file)
deleteat!(pkgdata.fileinfos, idx)
deleteat!(pkgdata.info.files, idx)
wl = get(watched_files, basedir(pkgdata), nothing)
if isa(wl, WatchList)
delete!(wl.trackedfiles, file)
end
end
return mexsnew, mexsold
end
"""
Revise.revise_file_now(pkgdata::PkgData, file)
Process revisions to `file`. This parses `file` and computes an expression-level diff
between the current state of the file and its most recently evaluated state.
It then deletes any removed methods and re-evaluates any changed expressions.
Note that generally it is better to use [`revise`](@ref) as it properly handles methods
that move from one file to another.
`id` must be a key in [`Revise.pkgdatas`](@ref), and `file` a key in
`Revise.pkgdatas[id].fileinfos`.
"""
function revise_file_now(pkgdata::PkgData, file)
# @assert !isabspath(file)
i = fileindex(pkgdata, file)
if i === nothing
println("Revise is currently tracking the following files in $(PkgId(pkgdata)): ", srcfiles(pkgdata))
error(file, " is not currently being tracked.")
end
mexsnew, mexsold = handle_deletions(pkgdata, file)
if mexsnew != nothing
_, includes = eval_new!(mexsnew, mexsold)
fi = fileinfo(pkgdata, i)
pkgdata.fileinfos[i] = FileInfo(mexsnew, fi)
maybe_add_includes_to_pkgdata!(pkgdata, file, includes; eval_now=true)
end
nothing
end
"""
Revise.errors()
Report the errors represented in [`Revise.queue_errors`](@ref).
Errors are automatically reported the first time they are encountered, but this function
can be used to report errors again.
"""
function errors(revision_errors=keys(queue_errors))
printed = Set{eltype(revision_errors)}()
for item in revision_errors
item in printed && continue
push!(printed, item)
pkgdata, file = item
(err, bt) = queue_errors[(pkgdata, file)]
fullpath = joinpath(basedir(pkgdata), file)
if err isa ReviseEvalException
@error "Failed to revise $fullpath" exception=err
else
@error "Failed to revise $fullpath" exception=(err, trim_toplevel!(bt))
end
end
end
"""
Revise.retry()
Attempt to perform previously-failed revisions. This can be useful in cases of order-dependent errors.
"""
function retry()
for (k, v) in queue_errors
push!(revision_queue, k)
end
revise()
end
"""
revise(; throw=false)
`eval` any changes in the revision queue. See [`Revise.revision_queue`](@ref).
If `throw` is `true`, throw any errors that occur during revision or callback;
otherwise these are only logged.
"""
function revise(; throw=false)
sleep(0.01) # in case the file system isn't quite done writing out the new files
have_queue_errors = !isempty(queue_errors)
# Do all the deletion first. This ensures that a method that moved from one file to another
# won't get redefined first and deleted second.
revision_errors = Tuple{PkgData,String}[]
queue = sort!(collect(revision_queue); lt=pkgfileless)
finished = eltype(revision_queue)[]
mexsnews = ModuleExprsSigs[]
interrupt = false
for (pkgdata, file) in queue
try
push!(mexsnews, handle_deletions(pkgdata, file)[1])
push!(finished, (pkgdata, file))
catch err
throw && Base.throw(err)
interrupt |= isa(err, InterruptException)
push!(revision_errors, (pkgdata, file))
queue_errors[(pkgdata, file)] = (err, catch_backtrace())
end
end
# Do the evaluation
for ((pkgdata, file), mexsnew) in zip(finished, mexsnews)
defaultmode = PkgId(pkgdata).name == "Main" ? :evalmeth : :eval
i = fileindex(pkgdata, file)
i === nothing && continue # file was deleted by `handle_deletions`
fi = fileinfo(pkgdata, i)
try
for (mod, exsnew) in mexsnew
mode = defaultmode
# Allow packages to override the supplied mode
if isdefined(mod, :__revise_mode__)
mode = getfield(mod, :__revise_mode__)::Symbol
end
mode ∈ (:sigs, :eval, :evalmeth, :evalassign) || error("unsupported mode ", mode)
exsold = get(fi.modexsigs, mod, empty_exs_sigs)
for rex in keys(exsnew)
sigs, includes = eval_rex(rex, exsold, mod; mode=mode)
if sigs !== nothing
exsnew[rex] = sigs
end
if includes !== nothing
maybe_add_includes_to_pkgdata!(pkgdata, file, includes; eval_now=true)
end
end
end
pkgdata.fileinfos[i] = FileInfo(mexsnew, fi)
delete!(queue_errors, (pkgdata, file))
catch err
throw && Base.throw(err)
interrupt |= isa(err, InterruptException)
push!(revision_errors, (pkgdata, file))
queue_errors[(pkgdata, file)] = (err, catch_backtrace())
end
end
if interrupt
for pkgfile in finished
haskey(queue_errors, pkgfile) || delete!(revision_queue, pkgfile)
end
else
empty!(revision_queue)
end
errors(revision_errors)
if !isempty(queue_errors)
if !have_queue_errors # only print on the first time errors occur
io = IOBuffer()
println(io, "\n") # better here than in the triple-quoted literal, see https://github.com/JuliaLang/julia/issues/34105
for (pkgdata, file) in keys(queue_errors)
println(io, " ", joinpath(basedir(pkgdata), file))
end
str = String(take!(io))
@warn """The running code does not match the saved version for the following files:$str
If the error was due to evaluation order, it can sometimes be resolved by calling `Revise.retry()`.
Use Revise.errors() to report errors again. Only the first error in each file is shown.
Your prompt color may be yellow until the errors are resolved."""
maybe_set_prompt_color(:warn)
end
else
maybe_set_prompt_color(:ok)
end
tracking_Main_includes[] && queue_includes(Main)
process_user_callbacks!(throw=throw)
nothing
end
revise(backend::REPL.REPLBackend) = revise()
"""
revise(mod::Module)
Reevaluate every definition in `mod`, whether it was changed or not. This is useful
to propagate an updated macro definition, or to force recompiling generated functions.
"""
function revise(mod::Module)
mod == Main && error("cannot revise(Main)")
id = PkgId(mod)
pkgdata = pkgdatas[id]
for (i, file) in enumerate(srcfiles(pkgdata))
fi = fileinfo(pkgdata, i)
for (mod, exsigs) in fi.modexsigs
for def in keys(exsigs)
ex = def.ex
exuw = unwrap(ex)
isexpr(exuw, :call) && exuw.args[1] === :include && continue
try
Core.eval(mod, ex)
catch err
@show mod
display(ex)
rethrow(err)
end
end
end
end
return true # fixme try/catch?
end
"""
Revise.track(mod::Module, file::AbstractString)
Revise.track(file::AbstractString)
Watch `file` for updates and [`revise`](@ref) loaded code with any
changes. `mod` is the module into which `file` is evaluated; if omitted,
it defaults to `Main`.
If this produces many errors, check that you specified `mod` correctly.
"""
function track(mod::Module, file::AbstractString; mode=:sigs, kwargs...)
isfile(file) || error(file, " is not a file")
# Determine whether we're already tracking this file
id = PkgId(mod)
if haskey(pkgdatas, id)
pkgdata = pkgdatas[id]
relfile = relpath(abspath(file), pkgdata)
hasfile(pkgdata, relfile) && return nothing
# Use any "fixes" provided by relpath
file = joinpath(basedir(pkgdata), relfile)
else
# Check whether `track` was called via a @require. Ref issue #403 & #431.
st = stacktrace(backtrace())
if any(sf->sf.func === :listenpkg && endswith(String(sf.file), "require.jl"), st)
nameof(mod) === :Plots || Base.depwarn("[email protected] or higher automatically handles `include` statements in `@require` expressions.\nPlease do not call `Revise.track` from such blocks.", :track)
return nothing
end
file = abspath(file)
end
# Set up tracking
fm = parse_source(file, mod; mode=mode)
if fm !== nothing
if mode === :includet
mode = :sigs # we already handled evaluation in `parse_source`
end
instantiate_sigs!(fm; mode=mode, kwargs...)
if !haskey(pkgdatas, id)
# Wait a bit to see if `mod` gets initialized
sleep(0.1)
end
pkgdata = get(pkgdatas, id, nothing)
if pkgdata === nothing
pkgdata = PkgData(id, pathof(mod))
end
if !haskey(CodeTracking._pkgfiles, id)
CodeTracking._pkgfiles[id] = pkgdata.info
end
push!(pkgdata, relpath(file, pkgdata)=>FileInfo(fm))
init_watching(pkgdata, (file,))
pkgdatas[id] = pkgdata
end
return nothing
end
function track(file::AbstractString; kwargs...)
startswith(file, juliadir) && error("use Revise.track(Base) or Revise.track(<stdlib module>)")
track(Main, file; kwargs...)
end
"""
includet(filename)
Load `filename` and track future changes. `includet` is intended for quick "user scripts"; larger or more
established projects are encouraged to put the code in one or more packages loaded with `using`
or `import` instead of using `includet`. See https://timholy.github.io/Revise.jl/stable/cookbook/
for tips about setting up the package workflow.
By default, `includet` only tracks modifications to *methods*, not *data*. See the extended help for details.
Note that this differs from packages, which evaluate all changes by default.
This default behavior can be overridden; see [Configuring the revise mode](@ref).
# Extended help
## Behavior and justification for the default revision mode (`:evalmeth`)
`includet` uses a default `__revise_mode__ = :evalmeth`. The consequence is that if you change
```
a = [1]
f() = 1
```
to
```
a = [2]
f() = 2
```
then Revise will update `f` but not `a`.
This is the default choice for `includet` because such files typically mix method definitions and data-handling.
Data often has many untracked dependencies; later in the same file you might `push!(a, 22)`, but Revise cannot
determine whether you wish it to re-run that line after redefining `a`.
Consequently, the safest default choice is to leave the user in charge of data.
## Workflow tips
If you have a series of computations that you want to run when you redefine your methods, consider separating
your method definitions from your computations:
- method definitions go in a package, or a file that you `includet` *once*
- the computations go in a separate file, that you re-`include` (no "t" at the end) each time you want to rerun
your computations.
This can be automated using [`entr`](@ref).
## Internals
`includet` is essentially shorthand for
Revise.track(Main, filename; mode=:eval, skip_include=false)
Do *not* use `includet` for packages, as those should be handled by `using` or `import`.
If `using` and `import` aren't working, you may have packages in a non-standard location;
try fixing it with something like `push!(LOAD_PATH, "/path/to/my/private/repos")`.
(If you're working with code in Base or one of Julia's standard libraries, use
`Revise.track(mod)` instead, where `mod` is the module.)
`includet` is deliberately non-recursive, so if `filename` loads any other files,
they will not be automatically tracked.
(See [`Revise.track`](@ref) to set it up manually.)
"""
function includet(mod::Module, file::AbstractString)
prev = Base.source_path(nothing)
if prev === nothing
file = abspath(file)
else
file = normpath(joinpath(dirname(prev), file))
end
tls = task_local_storage()
tls[:SOURCE_PATH] = file
try
track(mod, file; mode=:includet, skip_include=false)
if prev === nothing
delete!(tls, :SOURCE_PATH)
else
tls[:SOURCE_PATH] = prev
end
catch err
if prev === nothing
delete!(tls, :SOURCE_PATH)
else
tls[:SOURCE_PATH] = prev
end
if isa(err, ReviseEvalException)
printstyled(stderr, "ERROR: "; color=Base.error_color());
showerror(stderr, err; blame_revise=false)
println(stderr, "\nin expression starting at ", err.loc)
else
throw(err)
end
end
return nothing
end
includet(file::AbstractString) = includet(Main, file)
"""
Revise.silence(pkg)
Silence warnings about not tracking changes to package `pkg`.
"""
function silence(pkg::Symbol)
push!(silence_pkgs, pkg)
if !isdir(depsdir)
mkpath(depsdir)