Skip to content

Commit cea76d4

Browse files
authored
Merge pull request #2 from timholy/teh/definition
Implement an initial API
2 parents 4ba9c6f + 9b9f5cd commit cea76d4

File tree

8 files changed

+257
-15
lines changed

8 files changed

+257
-15
lines changed

.travis.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
language: julia
2+
3+
os:
4+
- linux
5+
- osx
6+
7+
julia:
8+
- 1.0
9+
- 1.1
10+
- nightly
11+
12+
branches:
13+
only:
14+
- master
15+
- /^v\d+\.\d+(\.\d+)?(-\S*)?$/
16+
17+
notifications:
18+
email: false
19+
20+
script:
21+
- export JULIA_PROJECT=""
22+
- julia --project -e 'using Pkg; Pkg.build(); Pkg.test();'
23+
24+
after_success:
25+
- julia -e 'import Pkg; Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder())'

Project.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2"
33
authors = ["Tim Holy <tim.holy@gmail.com>"]
44
version = "0.1.0"
55

6+
[deps]
7+
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
8+
69
[extras]
10+
ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
711
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
812

913
[targets]
10-
test = ["Test"]
14+
test = ["Test", "ColorTypes"]

README.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
CodeTracking is a minimal package designed to work with (a future version of)
44
[Revise.jl](https://github.com/timholy/Revise.jl).
5-
Its main purpose is to support packages that need to know the location
6-
(file and line number) of code that might move around as it's edited.
5+
Its main purpose is to support packages that need to interact with code that might move
6+
around as it's edited.
77

88
CodeTracking is a very lightweight dependency.
99

@@ -23,6 +23,29 @@ In this (ficticious) example, `sum` moved because I deleted a few lines higher i
2323
these didn't affect the functionality of `sum` (so we didn't need to redefine and recompile it),
2424
but it does change the starting line number of the file at which this method appears.
2525

26+
Other features:
27+
28+
```julia
29+
julia> using CodeTracking, ColorTypes
30+
31+
julia> pkgfiles(ColorTypes)
32+
PkgFiles(ColorTypes [3da002f7-5984-5a60-b8a6-cbb66c0b333f]):
33+
basedir: /home/tim/.julia/packages/ColorTypes/BsAWO
34+
files: ["src/ColorTypes.jl", "src/types.jl", "src/traits.jl", "src/conversions.jl", "src/show.jl", "src/operations.jl"]
35+
36+
julia> m = @which red(RGB(1,1,1))
37+
red(c::AbstractRGB) in ColorTypes at /home/tim/.julia/packages/ColorTypes/BsAWO/src/traits.jl:14
38+
39+
julia> definition(m)
40+
:(red(c::AbstractRGB) = begin
41+
#= /home/tim/.julia/packages/ColorTypes/BsAWO/src/traits.jl:14 =#
42+
c.r
43+
end)
44+
45+
julia> definition(m, String)
46+
"red(c::AbstractRGB ) = c.r\n"
47+
```
48+
2649
## A few details
2750

2851
CodeTracking won't do anything *useful* unless the user is also running Revise,
@@ -32,15 +55,12 @@ file/line info in the method itself if Revise isn't running.)
3255

3356
However, Revise is a fairly large (and fairly complex) package, and currently it's not
3457
easy to discover how to extract particular kinds of information from its internal storage.
35-
CodeTracking will be designed to be the new "query" part of Revise.jl.
58+
CodeTracking is designed to be the new "query" part of Revise.jl.
3659
The aim is to have a very simple API that developers can learn in a few minutes and then
3760
incorporate into their own packages; its lightweight nature means that they potentially gain
3861
a lot of functionality without being forced to take a big hit in startup time.
3962

40-
## Current state
63+
## Status
4164

42-
Currently this package is just a stub---it doesn't do anything useful,
43-
but neither should it hurt anything.
44-
Candidate users may wish to start `import`ing it and then file issues
45-
or submit PRs as they discover what kinds of functionality they need
46-
from CodeTracking.
65+
If you want CodeTracking to do anything useful, currently you have to check out the `teh/codetracking`
66+
branch of Revise.

src/CodeTracking.jl

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,109 @@
11
module CodeTracking
22

3-
export whereis
3+
using Base: PkgId
4+
using Core: LineInfoNode
5+
using UUIDs
46

5-
# This is just a stub implementation for now
7+
export PkgFiles
8+
export whereis, definition, pkgfiles
9+
10+
include("data.jl")
11+
include("utils.jl")
12+
13+
"""
14+
filepath, line = whereis(method::Method)
15+
16+
Return the file and line of the definition of `method`. `line`
17+
is the first line of the method's body.
18+
"""
619
function whereis(method::Method)
7-
file, line = String(method.file), method.line
20+
lin = get(method_locations, method.sig, nothing)
21+
if lin === nothing
22+
file, line = String(method.file), method.line
23+
else
24+
file, line = fileline(lin)
25+
end
826
if !isabspath(file)
9-
# This is a Base method
27+
# This is a Base or Core method
1028
file = Base.find_source_file(file)
1129
end
1230
return normpath(file), line
1331
end
1432

33+
"""
34+
src = definition(method::Method, String)
35+
36+
Return a string with the code that defines `method`.
37+
38+
Note this may not be terribly useful for methods that are defined inside `@eval` statements;
39+
see [`definition(method::Method, Expr)`](@ref) instead.
40+
"""
41+
function definition(method::Method, ::Type{String})
42+
file, line = whereis(method)
43+
src = read(file, String)
44+
eol = isequal('\n')
45+
linestarts = Int[]
46+
istart = 0
47+
for i = 1:line-1
48+
push!(linestarts, istart+1)
49+
istart = findnext(eol, src, istart+1)
50+
end
51+
ex, iend = Meta.parse(src, istart)
52+
if isfuncexpr(ex)
53+
return src[istart+1:iend-1]
54+
end
55+
# The function declaration was presumably on a previous line
56+
lineindex = lastindex(linestarts)
57+
while !isfuncexpr(ex)
58+
istart = linestarts[lineindex]
59+
ex, iend = Meta.parse(src, istart)
60+
end
61+
return src[istart:iend-1]
62+
end
63+
64+
"""
65+
ex = definition(method::Method, Expr)
66+
ex = definition(method::Method)
67+
68+
Return an expression that defines `method`.
69+
"""
70+
function definition(method::Method, ::Type{Expr})
71+
def = get(method_definitions, method.sig, nothing)
72+
if def === nothing
73+
f = method_lookup_callback[]
74+
if f !== nothing
75+
Base.invokelatest(f, method)
76+
end
77+
def = get(method_definitions, method.sig, nothing)
78+
end
79+
return def === nothing ? nothing : copy(def)
80+
end
81+
82+
definition(method::Method) = definition(method, Expr)
83+
84+
"""
85+
info = pkgfiles(name::AbstractString)
86+
info = pkgfiles(name::AbstractString, uuid::UUID)
87+
88+
Return a [`PkgFiles`](@ref) structure with information about the files that define the package
89+
specified by `name` and `uuid`.
90+
Returns `nothing` if this package has not been loaded.
91+
"""
92+
pkgfiles(name::AbstractString, uuid::UUID) = pkgfiles(PkgId(uuid, name))
93+
function pkgfiles(name::AbstractString)
94+
project = Base.active_project()
95+
uuid = Base.project_deps_get(project, name)
96+
uuid == false && error("no package ", name, " recognized")
97+
return pkgfiles(name, uuid)
98+
end
99+
pkgfiles(id::PkgId) = get(_pkgfiles, id, nothing)
100+
101+
"""
102+
info = pkgfiles(mod::Module)
103+
104+
Return a [`PkgFiles`](@ref) structure with information about the files that were loaded to
105+
define the package that defined `mod`.
106+
"""
107+
pkgfiles(mod::Module) = pkgfiles(PkgId(mod))
108+
15109
end # module

src/data.jl

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# The variables here get populated by Revise.jl.
2+
3+
"""
4+
PkgFiles encodes information about the current location of a package.
5+
Fields:
6+
- `id`: the `PkgId` of the package
7+
- `basedir`: the current base directory of the package
8+
- `files`: a list of files (relative path to `basedir`) that define the package.
9+
10+
Note that `basedir` may be subsequently updated by Pkg operations such as `add` and `dev`.
11+
"""
12+
mutable struct PkgFiles
13+
id::PkgId
14+
basedir::String
15+
files::Vector{String}
16+
end
17+
18+
PkgFiles(id::PkgId, path::AbstractString) = PkgFiles(id, path, String[])
19+
PkgFiles(id::PkgId, ::Nothing) = PkgFiles(id, "")
20+
PkgFiles(id::PkgId) = PkgFiles(id, normpath(basepath(id)))
21+
PkgFiles(id::PkgId, files::AbstractVector{<:AbstractString}) =
22+
PkgFiles(id, normpath(basepath(id)), files)
23+
24+
# Abstraction interface
25+
Base.PkgId(info::PkgFiles) = info.id
26+
srcfiles(info::PkgFiles) = info.files
27+
basedir(info::PkgFiles) = info.basedir
28+
29+
function Base.show(io::IO, info::PkgFiles)
30+
println(io, "PkgFiles(", info.id, "):")
31+
println(io, " basedir: ", info.basedir)
32+
print(io, " files: ")
33+
show(io, info.files)
34+
end
35+
36+
const method_locations = IdDict{Type,LineInfoNode}()
37+
38+
const method_definitions = IdDict{Type,Expr}()
39+
40+
const _pkgfiles = Dict{PkgId,PkgFiles}()
41+
42+
const method_lookup_callback = Ref{Any}(nothing)

src/utils.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
function isfuncexpr(ex)
2+
ex.head == :function && return true
3+
if ex.head == :(=)
4+
a = ex.args[1]
5+
if isa(a, Expr)
6+
while a.head == :where
7+
a = a.args[1]
8+
isa(a, Expr) || return false
9+
end
10+
a.head == :call && return true
11+
end
12+
end
13+
return false
14+
end
15+
16+
fileline(lin::LineInfoNode) = String(lin.file), lin.line
17+
18+
function basepath(id::PkgId)
19+
id.name ("Main", "Base", "Core") && return ""
20+
loc = Base.locate_package(id)
21+
loc === nothing && return ""
22+
return dirname(dirname(loc))
23+
end

test/runtests.jl

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
11
using CodeTracking
22
using Test
3+
# Note: ColorTypes needs to be installed, but note the absence of `using`
4+
5+
include("script.jl")
36

47
@testset "CodeTracking.jl" begin
5-
# Write your own tests here.
8+
m = first(methods(f1))
9+
file, line = whereis(m)
10+
@test file == normpath(joinpath(@__DIR__, "script.jl"))
11+
src = definition(m, String)
12+
@test src == """
13+
function f1(x, y)
14+
return x + y
15+
end
16+
"""
17+
18+
m = first(methods(f2))
19+
src = definition(m, String)
20+
@test src == """
21+
f2(x, y) = x + y
22+
"""
23+
24+
info = PkgFiles(Base.PkgId(CodeTracking))
25+
@test Base.PkgId(info) === info.id
26+
@test CodeTracking.basedir(info) == dirname(@__DIR__)
27+
28+
io = IOBuffer()
29+
show(io, info)
30+
str = String(take!(io))
31+
@test startswith(str, "PkgFiles(CodeTracking [da1fd8a2-8d9e-5ec2-8556-3022fb5608a2]):\n basedir:")
32+
33+
@test pkgfiles("ColorTypes") === nothing
34+
@test_throws ErrorException pkgfiles("NotAPkg")
635
end

test/script.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function f1(x, y)
2+
return x + y
3+
end
4+
5+
f2(x, y) = x + y

0 commit comments

Comments
 (0)