Python has great, build-in, pathlib module, that enables users to dynamically manipulate system paths. Much of its functionality is already covered by Julia's base module utilities (relpath, abspath, etc.), but one of the most handy, while trivial, functionalities is not covered. Pathlib enables users to create paths dynamically, e.g. Path("foo") / "bar" creates Path("foo/bar") etc, while sticking to system convention for joinpath-like path creation.
Example
Say that you are dealing with data in nested directories, you need to loop over the directories. With the proposed functionality, you could do this with simple and readable code:
dir = Path(pwd())
for subdir in ["A", "B", "C"]
push!(data, read(dir / subdir / "file.csv")) # instead of using: joinpath(dir, subdir, "file.csv")
end
POC implementation
This is trivial to implement in Julia:
import Base: iterate, /, *, relpath, String
struct Path{T<:AbstractString} <: AbstractString
path::T
end
String(path::Path) = path.path
iterate(path::Path) = iterate(String(path))
iterate(path::Path, i::Integer) = iterate(String(path), i)
macro p_str(path) Path(path) end
/(x::Path, y::Path) = Path(joinpath(String(x), String(y)))
/(x::AbstractString, y::Path) = Path(joinpath(x, String(y)))
/(x::Path, y::AbstractString) = Path(joinpath(String(x), y))
*(x::Path, y::Path) = Path(String(x) * String(y))
*(x::AbstractString, y::Path) = Path(x * String(y))
*(x::Path, y::AbstractString) = Path(String(x) * y)
relpath(path::Path, startpath::AbstractString = ".") = relpath(String(path), startpath)
and here some examples of usage:
@testset "Path objects arithmetic" begin
@test joinpath(pwd(), "test") == Path(pwd()) / "test"
@test joinpath("foo", "bar") == p"foo" / p"bar"
@test joinpath("foo", "bar") == p"foo" / "bar"
@test joinpath("foo", "bar") == "foo" / p"bar"
@test joinpath("foo", "bar", "baz") == p"foo" / p"bar" / p"baz"
@test joinpath("foo", "bar", "baz") == p"foo" / "bar" / "baz"
@test joinpath("foobar", "baz") == p"foo" * p"bar" / "baz"
@test joinpath("foobar", "baz") == p"foo" * "bar" / "baz"
@test joinpath("foobar", "baz") == "foo" * p"bar" / "baz"
end
Since it'd inherit from AbstractString, it is compatible with other string-based methods. If this would get accepted, methods like pwd, joinpath, relpath, etc. could return Path objects instead of regular strings, so we could use them like pwd() / "foo" / "bar" to create the ./foo/bar path dynamically.
Also, a couple things to note on your proposal
AbstractString as many string operations are not applicable and it prevents distinguishing AbstractString and AbstractPaths during dispatch. https://github.com/rofinn/FilePathsBase.jl/issues/15AbstractPath type to distinguish different local and remote filesystems. This has been really helpful for us when we write code that can either take a SystemPath or S3Path.@rofinn sure, as said, this is a minimalistic proof-of-concept with very limited functionality. Pros of this approach is that it minimizes the risk of any conflicts and problems with backward compatibility, since currently paths are just strings. The cons are the ones mentioned by you.
Fair enough, seems like the practicality vs purity discussion. Might be worth reviewing the design choices for pathlib, since that's what you're trying to model the behaviour after? https://snarky.ca/why-pathlib-path-doesn-t-inherit-from-str/
Most helpful comment
Also, a couple things to note on your proposal
AbstractStringas many string operations are not applicable and it prevents distinguishingAbstractStringandAbstractPathsduring dispatch. https://github.com/rofinn/FilePathsBase.jl/issues/15AbstractPathtype to distinguish different local and remote filesystems. This has been really helpful for us when we write code that can either take aSystemPathorS3Path.