Skip to content

Commit

Permalink
Crawl registry (→ to rm restriction "Only works for pkgs in active pr…
Browse files Browse the repository at this point in the history
…oject") Close #5

finally! 🍾🥂

missing: more proper testing, code cleanup (there's some non dry in proj toml parsing and keys "deps")
  • Loading branch information
tfiers committed Jan 11, 2023
1 parent eafc989 commit 8a9f534
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 90 deletions.
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ The version numbers roughly follow <a href="https://semver.org">SemVer</a>
Possible categories: [Added, Changed, Fixed, Removed, Security,
Deprecated (for soon-to-be removed features)]
-->
- Remove limitation "package must be installed in active project".
Any package in the General registry (and standard library) can now be
queried from anywhere.
- This (re)introduced dependencies: Pkg (a big one; but stdlib), and UUIDS.
- Dark-mode option for _all_ generated images\
<sup>(not just local SVGs; also PNGs and webapp URLs)</sup>
- Pass the `mode=:dark` keyword argument to `open` and `create` for this.
Expand Down
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ version = "0.4.0-dev"
[deps]
DefaultApplication = "3f0dd361-4fe0-5fc6-8523-80b14ec94d85"
EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[compat]
DefaultApplication = "1"
Expand Down
14 changes: 1 addition & 13 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,7 @@ This will open the browser to [this url][dotlink], which renders something like
width=680
alt="Dependency graph of Unitful, rendered with Graphviz dot">


<br>
<details>

The given package (here: [Unitful][unitful]) must be installed in the currently active project for this to work.

Note that `PkgGraph` does not have to be installed in the same project however:\
you can switch projects _after_ `PkgGraph` has been imported (using `pkg> activate …`).

Even easier is to install `PkgGraph` in your base environment (see [Global Install](#global-install)),
so you don't have to switch projects at all.

</details>

To filter out binary dependencies ([JLL packages]) or packages from the Julia standard library, you can set the keyword arguments `jll = false` and `stdlib = false`.

Expand Down Expand Up @@ -85,7 +73,7 @@ pkg> add PkgGraph

You might want to install `PkgGraph` in your base environment (e.g. `v1.8`).\
You can then use it in any project, without having to install it in that project
or having to always switch projects.
or having to switch projects.

<details>

Expand Down
2 changes: 1 addition & 1 deletion docs/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
version = "1.8.0"

[[deps.PkgGraph]]
deps = ["DefaultApplication", "EzXML", "TOML", "URIs"]
deps = ["DefaultApplication", "EzXML", "Pkg", "TOML", "URIs", "UUIDs"]
path = ".."
uuid = "f9c1b9e4-72e8-4a14-ade5-14f45fc35f11"
version = "0.4.0-dev"
Expand Down
2 changes: 1 addition & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ DocMeta.setdocmeta!(PkgGraph, :DocTestSetup, :(using PkgGraph); recursive=true,
println("<makedocs>") # ..including Documenter.HTML(…) construction call
makedocs(
source = srcmod,
modules = [PkgGraph],
# modules = [PkgGraph],
# ↪ To get a warning if there are any docstrings not mentioned in the markdown.
sitename = "PkgGraph.jl",
# ↪ Displayed in page title and navbar.
Expand Down
1 change: 0 additions & 1 deletion docs/src/ref/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Internals

```@docs
depgraph
packages_in_active_manifest
is_jll
is_in_stdlib
```
Expand Down
4 changes: 0 additions & 4 deletions src/enduser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
Open the browser to an image of `pkgname`'s dependency graph.
The given package must be installed in the currently active project.
See [Settings](@ref) for possible keyword arguments.
"""
function open(pkgname; dryrun = false, kw...)
Expand All @@ -27,8 +25,6 @@ and open it with your default image viewer. Uses the external program
'`dot`' (see [graphviz.org](https://graphviz.org)), which must be
available on `PATH`.
The given package must be installed in the currently active project.
`fmt` is an output file format supported by dot, such as `:svg` or `:png`.\\
If `fmt` is `:svg`, the generated SVG file is post-processed, to add
light and dark-mode CSS.
Expand Down
9 changes: 8 additions & 1 deletion src/internals/Internals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ can be accessed as `PkgGraph.depgraph`, e.g.
module Internals

using TOML
using UUIDs
include("stdlib.jl")

using Base: active_project
include("project.jl")

using Pkg
include("registry.jl")

include("depgraph.jl")
export depgraph,
packages_in_active_manifest,
should_be_included,
is_jll,
is_in_stdlib,
Expand Down
80 changes: 12 additions & 68 deletions src/internals/depgraph.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,20 @@ julia> depgraph(:Test)
"Test" => "Serialization"
```
"""
depgraph(pkgname; jll = true, stdlib = true) = begin
depgraph(pkgname; jll = true, stdlib = true, verbose = false) = begin
rootpkg = string(pkgname)
packages = packages_in_active_manifest()
include_jll = jll
include_stdlib = stdlib
if rootpkg keys(packages)
error("""
The given package ($pkgname) must be installed in the active project
(which is currently `$(active_project())`)""")
if is_in_project(rootpkg)
verbose && @info "Package `$rootpkg` found in active project. Using Manifest.toml"
direct_deps = direct_deps_from_project()
else
verbose && @info "Package `$rootpkg` not found in active project. Using General registry"
direct_deps = direct_deps_from_registry
end
deps = Vector{Pair{String, String}}()
add_deps_of(name) = begin
pkg_info = only(packages[name]) # Two packages with same name not supported.
direct_deps = get(pkg_info, "deps", [])
for dep in direct_deps
if should_be_included(dep; include_jll, include_stdlib)
push!(deps, name => dep)
add_deps_of(pkg) = begin
for dep in direct_deps(pkg)
if should_be_included(dep, include_jll=jll, include_stdlib=stdlib)
push!(deps, pkg => dep)
add_deps_of(dep)
end
end
Expand All @@ -53,40 +50,6 @@ depgraph(pkgname; jll = true, stdlib = true) = begin
return unique!(deps) # Could use a SortedSet instead; but this spares a pkg load.
end

manifest(proj_path) = replace(proj_path, "Project.toml" => "Manifest.toml")

if VERSION v"1.7"
packages_in(manifest) = TOML.parsefile(manifest)["deps"]
else
packages_in(manifest) = TOML.parsefile(manifest)
end

"""
packages_in_active_manifest()
Read and parse the `Manifest.toml` of the active project, and return its
'deps' table (as a dictionary indexed by package names).
Every entry in this dictionary is a list. This is for when multiple
packages would share the same name.
## Example:
```jldoctest; filter = r" => .*\$"m
julia> using PkgGraph.Internals
julia> packages = packages_in_active_manifest();
julia> only(packages["PkgGraph"])
Dict{String, Any} with 4 entries:
"deps" => ["DefaultApplication", "TOML", "URIs"]
"uuid" => "f9c1b9e4-72e8-4a14-ade5-14f45fc35f11"
"version" => "0.1.0"
"path" => "C:\\Users\\tfiers\\.julia\\dev\\PkgGraph"
```
"""
packages_in_active_manifest() = packages_in(manifest(active_project()))


should_be_included(pkg; include_jll = true, include_stdlib = true) =
if !include_jll && is_jll(pkg)
Expand All @@ -102,26 +65,7 @@ should_be_included(pkg; include_jll = true, include_stdlib = true) =
"""
is_jll(pkg) = endswith(pkg, "_jll")


"""
is_in_stdlib(pkg)::Bool
"""
is_in_stdlib(pkg) = pkg in STDLIB

stdlib_packages() = begin
packages = Set{String}()
for path in readdir(Sys.STDLIB; join = true)
# ↪ `join` gets us complete paths
if isdir(path)
push!(packages, pkgname(path))
end
end
packages
end
pkgname(pkgdir) = begin
proj_file = joinpath(pkgdir, "Project.toml")
toml_dict = TOML.parsefile(proj_file)
pkgname = toml_dict["name"]
end

const STDLIB = stdlib_packages()
is_in_stdlib(pkg) = pkg in STDLIB_NAMES
78 changes: 78 additions & 0 deletions src/internals/project.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@

using TOML
using Base: active_project

is_in_project(pkg, proj = active_project()) =
isfile(proj) && (name(proj) == pkg || pkg in keys(all_deps(proj)))

name(proj_path) = name(dict(proj_path))
dict(proj_path) = TOML.parsefile(proj_path)
name(toml::Dict) = get(toml, "name", nothing)

all_deps(project) = begin
mani = manifest(project)
if !isfile(mani)
return Dict()
end
@static if VERSION v"1.7"
deps = TOML.parsefile(mani)["deps"]
else
deps = TOML.parsefile(mani)
end
end
manifest(project) = replace(project, "Project.toml" => "Manifest.toml")

direct_deps_from_project(proj = active_project()) = begin
proj_dict = dict(proj)
proj_name = name(proj_dict)
all_deps_ = all_deps(proj)
direct_deps(pkgname) =
if pkgname == proj_name
get(proj_dict, "deps", []) |> keys
else
deps_with_name = all_deps_[pkgname]
check_only(deps_with_name)
dep_dict = only(deps_with_name)
get(dep_dict, "deps", [])
end
direct_deps
end
check_only(packages_with_same_name) = @assert(
length(packages_with_same_name) == 1,
"""
Different packages with same name not supported
(The offending packages:)
$packages_with_same_name
"""
)

# The above is poop: we want dep tree of entire thing, also if no top specified
# no name. then there's multiple roots, sure (or take as name, the directory)
#


"""
packages_in_active_manifest()
Read and parse the `Manifest.toml` of the given project, and return its
'deps' table (as a dictionary indexed by package names).
Every entry in this dictionary is a list. This is for when multiple
packages would share the same name.
## Example:
```jldoctest; filter = r" => .*\$"m
julia> using PkgGraph.Internals
julia> packages = packages_in_active_manifest();
julia> only(packages["PkgGraph"])
Dict{String, Any} with 4 entries:
"deps" => ["DefaultApplication", "TOML", "URIs"]
"uuid" => "f9c1b9e4-72e8-4a14-ade5-14f45fc35f11"
"version" => "0.1.0"
"path" => "C:\\Users\\tfiers\\.julia\\dev\\PkgGraph"
```
"""
packages_in_active_manifest() = all_deps(active_project())
50 changes: 50 additions & 0 deletions src/internals/registry.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Pkg.Registry: reachable_registries,
uuids_from_name,
init_package_info!,
initialize_uncompressed!,
JULIA_UUID


const reg = first(reachable_registries())
@assert reg.name == "General"

name(uuid::UUID) =
if uuid in STDLIB_UUIDS
STDLIB[uuid]
elseif uuid in keys(reg.pkgs)
reg.pkgs[uuid].name
else
error()
end

uuid(name::AbstractString) =
if name in STDLIB_NAMES
findfirst(==(name), STDLIB)
else
uuids = uuids_from_name(reg, name)
if isempty(uuids)
error("Package `$name` not found")
elseif length(uuids) > 1
error("Multiple packages with the same name (`$name`) not supported")
else
return only(uuids)
end
end

direct_deps_from_registry(pkg) = begin
if pkg in STDLIB_NAMES
return direct_deps_of_stdlib_pkg(pkg)
end
pkgentry = reg.pkgs[uuid(pkg)]
p = init_package_info!(pkgentry)
versions = keys(p.version_info)
v = maximum(versions)
initialize_uncompressed!(p, [v])
vinfo = p.version_info[v]
compat_info = vinfo.uncompressed_compat
# ↪ All direct deps will be here, even if author didn't them
# [compat] (their versionspec will just be "*").
direct_dep_uuids = collect(keys(compat_info))
filter!(!=(JULIA_UUID), direct_dep_uuids)
return name.(direct_dep_uuids)
end
26 changes: 26 additions & 0 deletions src/internals/stdlib.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using UUIDs
using TOML

stdlib() = begin
packages = Dict{UUID,String}()
for path in readdir(Sys.STDLIB; join = true)
# ↪ `join` gets us complete paths
if isdir(path)
toml = proj_dict(path)
push!(packages, UUID(toml["uuid"]) => toml["name"])
end
end
packages
end
proj_dict(pkgdir) = TOML.parsefile(proj_file(pkgdir))
proj_file(pkgdir) = joinpath(pkgdir, "Project.toml")

const STDLIB = stdlib()
const STDLIB_UUIDS = keys(STDLIB)
const STDLIB_NAMES = values(STDLIB)

direct_deps_of_stdlib_pkg(name) = begin
pkgdir = joinpath(Sys.STDLIB, name)
d = proj_dict(pkgdir)
keys(get(d, "deps", []))
end
1 change: 1 addition & 0 deletions test/integration.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ using Test

@test isnothing(PkgGraph.open("Test", dryrun = true))
@test isnothing(PkgGraph.create("Test", dryrun = true))
@test isnothing(PkgGraph.create("PyPlot", dryrun = true))
end


Expand Down
2 changes: 1 addition & 1 deletion test/unit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ using Test

if VERSION v"1.8" # Julia v1.7 does not support error string matching
@test_throws(
"The given package (DinnaeExist) must be installed in the active project",
"Package `DinnaeExist` not found",
depgraph("DinnaeExist")
)
end
Expand Down

0 comments on commit 8a9f534

Please sign in to comment.