-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #38 from rofinn/rf/conversion-macro
Compat macro for taking strings or paths
- Loading branch information
Showing
8 changed files
with
319 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,28 @@ | ||
# Documentation: http://docs.travis-ci.com/user/languages/julia/ | ||
language: julia | ||
os: | ||
- linux | ||
- osx | ||
julia: | ||
- 0.6 | ||
- 0.7 | ||
- 1.0 | ||
- nightly | ||
notifications: | ||
email: false | ||
matrix: | ||
fast_finish: true | ||
allow_failures: | ||
- julia: nightly | ||
# uncomment the following lines to override the default test script | ||
#script: | ||
# - if [[ -a .git/shallow ]]; then git fetch --unshallow; fi | ||
# - julia -e 'Pkg.clone(pwd()); Pkg.build("FilePaths"); Pkg.test("FilePaths"; coverage=true)' | ||
after_success: | ||
- | | ||
julia -e ' | ||
VERSION >= v"0.7.0-DEV.3656" && using Pkg | ||
VERSION >= v"0.7.0-DEV.5183" || cd(Pkg.dir("FilePaths")) | ||
Pkg.add("Coverage"); using Coverage | ||
Codecov.submit(process_folder())' | ||
- julia -e 'using Pkg; Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())' | ||
# jobs: | ||
# include: | ||
# - stage: "Documentation" | ||
# julia: 1.0 | ||
# os: linux | ||
# script: | ||
# - julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' | ||
# - julia --project=docs/ docs/make.jl | ||
# after_success: skip |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
name = "FilePaths" | ||
uuid = "8fc22ac5-c921-52a6-82fd-178b2807b824" | ||
authors = ["Rory Finnegan"] | ||
version = "0.8.0" | ||
|
||
[deps] | ||
FilePathsBase = "48062228-2e41-5def-b9a4-89aafe57970f" | ||
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" | ||
Reexport = "189a3867-3050-52da-a836-e630ba90ab69" | ||
URIParser = "30578b45-9adc-5946-b283-645ec420af67" | ||
|
||
[compat] | ||
FilePathsBase = "0.5" | ||
|
||
[extras] | ||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" | ||
|
||
[targets] | ||
test = ["Test"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
""" | ||
FilePaths.@compat fn | ||
Generates a compatibility method for handling paths as strings based on the function | ||
definition passed in. | ||
Wrapper method properties: | ||
- Any arguments that accepted `P <: AbstractPath` will accept `Union{String, P}` | ||
- All string path inputs will be converted to path types | ||
- If a path return type was specified then the path result will be converted back to a string. | ||
# Examples | ||
``` | ||
julia> using FilePaths | ||
julia> FilePaths.@compat function myrelative(x::AbstractPath, y::AbstractPath) | ||
return relative(x, y) | ||
end | ||
myrelative (generic function with 2 methods) | ||
julia> FilePaths.@compat function myjoin(x::P, y::String)::P where P <: AbstractPath | ||
return x / y | ||
end | ||
myjoin (generic function with 2 methods) | ||
julia> myrelative(cwd(), home()) | ||
p"repos/FilePaths.jl" | ||
julia> myrelative(pwd(), homedir()) | ||
p"repos/FilePaths.jl" | ||
julia> myjoin(parent(cwd()), "FilePaths.jl") | ||
p"/Users/rory/repos/FilePaths.jl" | ||
julia> myjoin("/Users/rory/repos", "FilePaths.jl") | ||
"/Users/rory/repos/FilePaths.jl" | ||
``` | ||
""" | ||
macro compat(ex) | ||
mod = @__MODULE__ | ||
new_ex = compat_exp(mod, deepcopy(ex)) | ||
|
||
return quote | ||
$ex | ||
$new_ex | ||
end |> esc | ||
end | ||
|
||
function compat_exp(mod::Module, ex::Expr) | ||
fdef = splitdef(ex) | ||
args = Symbol[] | ||
kwargs = Expr[] | ||
convert_vars = Symbol[] | ||
body = Expr[] | ||
params = Symbol[] | ||
|
||
# A function that identifies args that need to be converted | ||
function parse_arg(a) | ||
if a.args[2] in params | ||
push!(convert_vars, a.args[1]) | ||
elseif _ispath(mod, a.args[2]) | ||
# Modify the arg type declaration from P<:AbstractPath to Union{String, P} | ||
# NOTE: If the variable is parameterized then we should have already updated the | ||
# parameterized type in the where clause | ||
a.args[2] = :(Union{AbstractString, $(a.args[2])}) | ||
push!(convert_vars, a.args[1]) | ||
end | ||
end | ||
|
||
# Identify any where params that are a subtype of AbstractPath | ||
if haskey(fdef, :whereparams) | ||
for (i, p) in enumerate(fdef[:whereparams]) | ||
# If the param is a subtype of AbstracPath | ||
# then we store the lookup symbol in the params array | ||
if _ispath(mod, p.args[2]) | ||
# Modify parameterized where clause from P<:AbstractPath to Union{String, P} | ||
p.args[2] = :(Union{AbstractString, $(p.args[2])}) | ||
push!(params, p.args[1]) | ||
end | ||
end | ||
end | ||
|
||
# Identify args that need to be converted | ||
if haskey(fdef, :args) | ||
for (i, a) in enumerate(fdef[:args]) | ||
# An arg can be an expression or a symbol (no type information) | ||
if isa(a, Expr) | ||
if (a.head) === :kw | ||
# Optional arguments show up as `kw` and need to be parsed as such | ||
T = a.args[1] | ||
parse_arg(T) | ||
push!(args, T.args[1]) | ||
else | ||
parse_arg(a) | ||
push!(args, a.args[1]) | ||
end | ||
elseif isa(a, Symbol) | ||
push!(args, a) | ||
else | ||
throw(ArgumentError("Uknown argument type $a")) | ||
end | ||
end | ||
end | ||
|
||
# Identify kwargs that need to be converted | ||
if haskey(fdef, :kwargs) | ||
for (i, k) in enumerate(fdef[:kwargs]) | ||
T = k.args[1] | ||
parse_arg(T) | ||
# Rewrite the kwarg expression to pass them along | ||
push!(kwargs, :($(T.args[1])=$(T.args[1]))) | ||
end | ||
end | ||
|
||
# Insert our convert statements for the appropriate variables | ||
for v in convert_vars | ||
push!(body, :($v = Path($v))) | ||
end | ||
|
||
# Push our paths method call into the body | ||
push!(body, :(result = $(fdef[:name])($(args...); $(kwargs...)))) | ||
|
||
# If we have a return type and it's a path then convert the result back to a string | ||
if haskey(fdef, :rtype) && (fdef[:rtype] in params || _ispath(mod, fdef[:rtype])) | ||
push!(body, :(result = string(result))) | ||
# Set the return type to String to avoid an incorrect conversion back to a path | ||
fdef[:rtype] = :String | ||
end | ||
|
||
# Finally, insert a return statement | ||
push!(body, :(return result)) | ||
|
||
# Update our definition with the new body | ||
fdef[:body].args = body | ||
|
||
# Combine the modified definition back into an expression | ||
return MacroTools.combinedef(fdef) | ||
end | ||
|
||
function _ispath(mod::Module, t::Symbol) | ||
T = getfield(mod, t) | ||
return T <: AbstractPath | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
module TestPkg | ||
|
||
using FilePaths | ||
|
||
# Test for basic definitions taking abstract paths | ||
FilePaths.@compat function typical(x::AbstractPath, y::AbstractPath) | ||
return relative(x, y) | ||
end | ||
|
||
# Test for definition taking parameterized paths | ||
FilePaths.@compat function parameterized(x::P, y::P) where P <: AbstractPath | ||
return relative(x, y) | ||
end | ||
|
||
# Test for definition with a path return type | ||
# (indicating that we should convert it back to a string) | ||
FilePaths.@compat function rtype(x::P, y::P)::P where P <: AbstractPath | ||
return relative(x, y) | ||
end | ||
|
||
# Test for a mixed input definition | ||
# z is an unused variable to make sure untyped variables don't break the macro | ||
FilePaths.@compat function mixed(x::P, y::String, z)::P where P <: AbstractPath | ||
return x / y | ||
end | ||
|
||
# Test kwargs | ||
FilePaths.@compat function kwargs(x::AbstractPath; y::AbstractPath=cwd())::AbstractPath | ||
return relative(x, y) | ||
end | ||
|
||
# Test optional args | ||
FilePaths.@compat function optargs(x::AbstractPath, y::AbstractPath=cwd())::AbstractPath | ||
return relative(x, y) | ||
end | ||
|
||
# Test for inline definitions | ||
FilePaths.@compat inline(x::AbstractPath, y::AbstractPath) = relative(x, y) | ||
|
||
end | ||
|
||
@testset "@compat" begin | ||
cd(abs(parent(Path(@__FILE__)))) do | ||
reg = Sys.iswindows() ? "..\\src\\FilePaths.jl" : "../src/FilePaths.jl" | ||
@test ispath(reg) | ||
p = Path(reg) | ||
|
||
@testset "typical" begin | ||
# Passing paths should should work just like calling relative | ||
@test TestPkg.typical(p, home()) == relative(p, home()) | ||
|
||
# Passing strings should now also work just like calling relative | ||
@test TestPkg.typical(string(p), string(home())) == relative(p, home()) | ||
|
||
# Mixing strings and paths should also work here | ||
@test TestPkg.typical(string(p), home()) == relative(p, home()) | ||
end | ||
|
||
@testset "parameterized" begin | ||
# Passing paths should should work just like calling relative | ||
@test TestPkg.parameterized(p, home()) == relative(p, home()) | ||
|
||
# Passing strings should now also work just like calling relative | ||
@test TestPkg.parameterized(string(p), string(home())) == relative(p, home()) | ||
|
||
# Mixing strings and paths should error because x and y need to be the same type | ||
@test_throws MethodError TestPkg.parameterized(string(p), home()) | ||
end | ||
|
||
@testset "rtype" begin | ||
# Passing paths should should work just like calling relative | ||
@test TestPkg.rtype(p, home()) == relative(p, home()) | ||
|
||
# Passing strings should also convert the output to a string | ||
@test TestPkg.rtype(string(p), string(home())) == string(relative(p, home())) | ||
|
||
# Mixing strings and paths should error because x and y need to be the same type | ||
@test_throws MethodError TestPkg.rtype(string(p), home()) | ||
end | ||
|
||
@testset "mixed" begin | ||
# Passing in a path as the first argument should return the same output as p / home() | ||
prefix = parent(p) | ||
suffix = basename(p) | ||
@test TestPkg.mixed(prefix, suffix, 2) == p | ||
|
||
# Passing 2 strings should also convert the output to a string | ||
@test TestPkg.mixed(string(prefix), suffix, 2) == string(p) | ||
|
||
# Passing a path where we explicitly want a string will error | ||
@test_throws MethodError TestPkg.mixed(prefix, Path(suffix), 2) | ||
end | ||
|
||
@testset "optargs" begin | ||
# Passing paths should should work just like calling relative | ||
@test TestPkg.optargs(p, home()) == relative(p, home()) | ||
|
||
# Passing strings should also return the relative path as a string | ||
@test TestPkg.optargs(string(p), string(home())) == string(relative(p, home())) | ||
|
||
# With optional arguments we should be able to mix strings on either argument | ||
@test TestPkg.optargs(p, string(home())) == string(relative(p, home())) | ||
@test TestPkg.optargs(string(p), home()) == string(relative(p, home())) | ||
|
||
# Use default | ||
@test TestPkg.optargs(p) == relative(p, cwd()) | ||
@test TestPkg.optargs(string(p)) == string(relative(p, cwd())) | ||
end | ||
|
||
@testset "inline" begin | ||
# Passing paths should should work just like calling relative | ||
@test TestPkg.inline(p, home()) == relative(p, home()) | ||
|
||
# Passing strings should now also work just like calling relative | ||
@test TestPkg.inline(string(p), string(home())) == relative(p, home()) | ||
|
||
# Mixing strings and paths should also work here | ||
@test TestPkg.inline(string(p), home()) == relative(p, home()) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
using FilePaths | ||
using Compat | ||
using Compat.Test | ||
using Test | ||
|
||
@testset "FilePaths" begin | ||
|
||
include("test_uri.jl") | ||
include("compat.jl") | ||
|
||
end |