From d4440922113cc6db311c6e84996771b6b28fcaa4 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Tue, 3 Jul 2018 11:09:41 +0200 Subject: [PATCH] add inequality specifiers (#441) --- stdlib/Pkg/docs/src/index.md | 12 +++++++ stdlib/Pkg/src/versions.jl | 62 +++++++++++++++++++++++++++++++----- stdlib/Pkg/test/pkg.jl | 31 ++++++++++++++++++ 3 files changed, 97 insertions(+), 8 deletions(-) diff --git a/stdlib/Pkg/docs/src/index.md b/stdlib/Pkg/docs/src/index.md index e6f61c3d3ac70..6e9c8f4bf062e 100644 --- a/stdlib/Pkg/docs/src/index.md +++ b/stdlib/Pkg/docs/src/index.md @@ -708,6 +708,7 @@ Similar to other package managers, the Julia package manager respects [semantic As an example, a version specifier is given as e.g. `1.2.3` is therefore assumed to be compatible with the versions `[1.2.3 - 2.0.0)` where `)` is a non-inclusive upper bound. More specifically, a version specifier is either given as a **caret specifier**, e.g. `^1.2.3` or a **tilde specifier** `~1.2.3`. Caret specifiers are the default and hence `1.2.3 == ^1.2.3`. The difference between a caret and tilde is described in the next section. +The intersection of multiple version specifiers can be formed by comma separating indiviual version specifiers. ##### Caret specifiers @@ -740,6 +741,17 @@ This gives the following example. ~1 = [1.0.0, 2.0.0) ``` +#### Inequality specifiers + +Inequalities can also be used to specify version ranges: + +``` +>= 1.2.3 = [1.2.3, ∞) +≥ 1.2.3 = [1.2.3, ∞) += 1.2.3 = [1.2.3, 1.2.3] +< 1.2.3 = [0.0.0, 1.2.2] +``` + ## Precompiling a project diff --git a/stdlib/Pkg/src/versions.jl b/stdlib/Pkg/src/versions.jl index 89c5c4daf8c72..c33245cf0f5c3 100644 --- a/stdlib/Pkg/src/versions.jl +++ b/stdlib/Pkg/src/versions.jl @@ -247,21 +247,27 @@ Base.show(io::IO, s::VersionSpec) = print(io, "VersionSpec(\"", s, "\")") # Semver notation # ################### -const ver_reg = r"^([~^]?)?[v]?([0-9]+?)(?:\.([0-9]+?))?(?:\.([0-9]+?))?$" function semver_spec(s::String) + s = replace(s, " " => "") ranges = VersionRange[] for ver in split(s, ',') - push!(ranges, semver_interval(strip(ver))) + range = nothing + found_match = false + for (ver_reg, f) in ver_regs + if occursin(ver_reg, ver) + range = f(match(ver_reg, ver)) + found_match = true + break + end + end + found_match || error("invalid version specifier: $s") + push!(ranges, range) end return VersionSpec(ranges) end -function semver_interval(s::AbstractString) - if !occursin(ver_reg, s) - error("invalid version specifier: $s") - end - m = match(ver_reg, s) +function semver_interval(m::RegexMatch) @assert length(m.captures) == 4 n_significant = count(x -> x !== nothing, m.captures) - 1 typ, _major, _minor, _patch = m.captures @@ -269,7 +275,7 @@ function semver_interval(s::AbstractString) minor = (n_significant < 2) ? 0 : parse(Int, _minor) patch = (n_significant < 3) ? 0 : parse(Int, _patch) if n_significant == 3 && major == 0 && minor == 0 && patch == 0 - error("invalid version: $s") + error("invalid version: \"0.0.0\"") end # Default type is :caret vertyp = (typ == "" || typ == "^") ? :caret : :tilde @@ -296,3 +302,43 @@ function semver_interval(s::AbstractString) end end end + +const _inf = Pkg.Types.VersionBound("*") +function inequality_interval(m::RegexMatch) + @assert length(m.captures) == 4 + typ, _major, _minor, _patch = m.captures + n_significant = count(x -> x !== nothing, m.captures) - 1 + major = parse(Int, _major) + minor = (n_significant < 2) ? 0 : parse(Int, _minor) + patch = (n_significant < 3) ? 0 : parse(Int, _patch) + if n_significant == 3 && major == 0 && minor == 0 && patch == 0 + error("invalid version: $s") + end + v = VersionBound(major, minor, patch) + if typ == "<" + nil = VersionBound(0, 0, 0) + if v[3] == 0 + if v[2] == 0 + v1 = VersionBound(v[1]-1) + else + v1 = VersionBound(v[1], v[2]-1) + end + else + v1 = VersionBound(v[1], v[2], v[3]-1) + end + return VersionRange(nil, v1) + elseif typ == "=" + return VersionRange(v) + elseif typ == ">=" || typ == "≥" + return VersionRange(v, _inf) + else + error("invalid prefix $typ") + end +end + +const version = "v?([0-9]+?)(?:\\.([0-9]+?))?(?:\\.([0-9]+?))?" +const ver_regs = +[ + Regex("^([~^]?)?$version\$") => semver_interval, # 0.5 ^0.4 ~0.3.2 + Regex("^((?:≥)|(?:>=)|(?:=)|(?:<)|(?:=))v?$version\$") => inequality_interval,# < 0.2 >= 0.5,2 +] diff --git a/stdlib/Pkg/test/pkg.jl b/stdlib/Pkg/test/pkg.jl index 0a572a42b52b6..c31d91fb2cd79 100644 --- a/stdlib/Pkg/test/pkg.jl +++ b/stdlib/Pkg/test/pkg.jl @@ -58,6 +58,37 @@ import Pkg.Types: semver_spec, VersionSpec @test v"0.0.0" in semver_spec("0.0") @test v"0.0.99" in semver_spec("0.0") @test !(v"0.1.0" in semver_spec("0.0")) + + @test semver_spec("<1.2.3") == VersionSpec("0.0.0 - 1.2.2") + @test semver_spec("<1.2") == VersionSpec("0.0.0 - 1.1") + @test semver_spec("<1") == VersionSpec("0.0.0 - 0") + @test semver_spec("<2") == VersionSpec("0.0.0 - 1") + @test semver_spec("<0.2.3") == VersionSpec("0.0.0 - 0.2.2") + @test semver_spec("<2.0.3") == VersionSpec("0.0.0 - 2.0.2") + @test v"0.2.3" in semver_spec("<0.2.4") + @test !(v"0.2.4" in semver_spec("<0.2.4")) + + @test semver_spec("=1.2.3") == VersionSpec("1.2.3") + @test semver_spec("=1.2") == VersionSpec("1.2.0") + @test semver_spec(" =1") == VersionSpec("1.0.0") + @test v"1.2.3" in semver_spec("=1.2.3") + @test !(v"1.2.4" in semver_spec("=1.2.3")) + @test !(v"1.2.2" in semver_spec("=1.2.3")) + + @test semver_spec("≥1.3.0") == semver_spec(">=1.3.0") + + + @test semver_spec(">= 1.2.3") == VersionSpec("1.2.3-*") + @test semver_spec(">=1.2 ") == VersionSpec("1.2.0-*") + @test semver_spec(" >= 1") == VersionSpec("1.0.0-*") + @test v"1.0.0" in semver_spec(">=1") + @test v"0.0.1" in semver_spec(">=0") + @test v"1.2.3" in semver_spec(">=1.2.3") + @test !(v"1.2.2" in semver_spec(">=1.2.3")) + + @test_throws ErrorException semver_spec("^^0.2.3") + @test_throws ErrorException semver_spec("^^0.2.3.4") + @test_throws ErrorException semver_spec("0.0.0") end # TODO: Should rewrite these tests not to rely on internals like field names