Skip to content

Commit

Permalink
Merge pull request #38 from rofinn/rf/conversion-macro
Browse files Browse the repository at this point in the history
Compat macro for taking strings or paths
  • Loading branch information
rofinn authored Jul 15, 2019
2 parents da1ffd0 + 5fdf9cf commit 9a5eeb4
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 27 deletions.
25 changes: 16 additions & 9 deletions .travis.yml
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
19 changes: 19 additions & 0 deletions Project.toml
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"]
5 changes: 0 additions & 5 deletions REQUIRE

This file was deleted.

24 changes: 14 additions & 10 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
environment:
matrix:
- julia_version: 0.6
- julia_version: 0.7
- julia_version: 1
- julia_version: latest
- julia_version: 1.0
- julia_version: nightly

platform:
- x86
- x64
- x86 # 32-bit
- x64 # 64-bit

## uncomment the following lines to allow failures on nightly julia
## (tests will run but not make your overall status red)
#matrix:
# allow_failures:
# - julia_version: latest
matrix:
allow_failures:
- julia_version: nightly

branches:
only:
Expand All @@ -35,4 +33,10 @@ build_script:

test_script:
- echo "%JL_TEST_SCRIPT%"
- C:\julia\bin\julia -e "%JL_TEST_SCRIPT%"
- C:\julia\bin\julia -e "%JL_TEST_SCRIPT%"

# Uncomment to support code coverage upload. Should only be enabled for packages
# which would have coverage gaps without running on Windows
on_success:
- echo "%JL_CODECOV_SCRIPT%"
- C:\julia\bin\julia -e "%JL_CODECOV_SCRIPT%"
3 changes: 2 additions & 1 deletion src/FilePaths.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ __precompile__()

module FilePaths

using Reexport, URIParser
using Reexport, URIParser, MacroTools
@reexport using FilePathsBase

include("uri.jl")
include("compat.jl")

end # end of module
145 changes: 145 additions & 0 deletions src/compat.jl
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
121 changes: 121 additions & 0 deletions test/compat.jl
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
4 changes: 2 additions & 2 deletions test/runtests.jl
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

0 comments on commit 9a5eeb4

Please sign in to comment.