Skip to content

Commit ae8cdcf

Browse files
authored
Merge pull request #83 from rofinn/rf/faq
Documentation improvements
2 parents 3812bc1 + 1fcd15d commit ae8cdcf

File tree

7 files changed

+213
-53
lines changed

7 files changed

+213
-53
lines changed

README.md

Lines changed: 95 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,82 +8,125 @@
88
FilePathsBase.jl provides a type based approach to working with filesystem paths in julia.
99

1010
## Intallation
11+
1112
FilePathsBase.jl is registered, so you can to use `Pkg.add` to install it.
12-
```julia-repl
13+
```julia
1314
julia> Pkg.add("FilePathsBase")
1415
```
1516

16-
## Usage
17-
```julia-repl
18-
julia> using FilePathsBase; using FilePathsBase: /
19-
```
17+
## Getting Started
2018

21-
The first important difference about working with paths in FilePathsBase.jl is that path
22-
segments are represented as an immutable tuple of strings.
19+
Here are some common operations that you may want to perform with file paths.
2320

24-
Path creation:
25-
```julia-repl
26-
julia> Path("~/repos/FilePathsBase.jl/")
27-
p"~/repos/FilePathsBase.jl/"
28-
```
29-
or
30-
```julia-repl
31-
julia> p"~/repos/FilePathsBase.jl/"
32-
p"~/repos/FilePathsBase.jl/"
33-
```
21+
```julia
22+
#=
23+
NOTE: We're loading our `/` operator for path concatenation into the currect scope, but non-path division operations will still fallback to the base behaviour.
24+
=#
25+
julia> using FilePathsBase; using FilePathsBase: /
3426

35-
Human readable file status info:
36-
```julia-repl
37-
julia> stat(p"README.md")
27+
julia> cwd()
28+
p"/Users/rory/repos/FilePathsBase.jl"
29+
30+
julia> walkpath(cwd() / "docs") |> collect
31+
23-element Array{Any,1}:
32+
p"/Users/rory/repos/FilePathsBase.jl/docs/.DS_Store"
33+
p"/Users/rory/repos/FilePathsBase.jl/docs/Manifest.toml"
34+
p"/Users/rory/repos/FilePathsBase.jl/docs/Project.toml"
35+
p"/Users/rory/repos/FilePathsBase.jl/docs/build"
36+
p"/Users/rory/repos/FilePathsBase.jl/docs/build/api.html"
37+
p"/Users/rory/repos/FilePathsBase.jl/docs/build/assets"
38+
p"/Users/rory/repos/FilePathsBase.jl/docs/build/assets/arrow.svg"
39+
p"/Users/rory/repos/FilePathsBase.jl/docs/build/assets/documenter.css"
40+
...
41+
42+
julia> stat(p"docs/src/index.md")
3843
Status(
39-
device = 16777220,
40-
inode = 48428965,
44+
device = 16777223,
45+
inode = 32240108,
4146
mode = -rw-r--r--,
4247
nlink = 1,
43-
uid = 501,
44-
gid = 20,
48+
uid = 501 (rory),
49+
gid = 20 (staff),
4550
rdev = 0,
46-
size = 1880 (1.8K),
51+
size = 2028 (2.0K),
4752
blksize = 4096 (4.0K),
4853
blocks = 8,
49-
mtime = 2016-02-16T00:49:27,
50-
ctime = 2016-02-16T00:49:27,
54+
mtime = 2020-04-20T17:20:38.612,
55+
ctime = 2020-04-20T17:20:38.612,
5156
)
52-
```
5357

54-
Working with permissions:
55-
```julia-repl
56-
julia> m = mode(p"README.md")
57-
-rw-r--r--
58+
julia> relative(p"docs/src/index.md", p"src/")
59+
p"../docs/src/index.md"
5860

59-
julia> m - readable(:ALL)
60-
--w-------
61+
julia> normalize(p"src/../docs/src/index.md")
62+
p"docs/src/index.md"
6163

62-
julia> m + executable(:ALL)
63-
-rwxr-xr-x
64+
julia> absolute(p"docs/src/index.md")
65+
p"/Users/rory/repos/FilePathsBase.jl/docs/src/index.md"
6466

65-
julia> chmod(p"README.md", "+x")
67+
julia> islink(p"docs/src/index.md")
68+
true
6669

67-
julia> mode(p"README.md")
68-
-rwxr-xr-x
70+
julia> canonicalize(p"docs/src/index.md")
71+
p"/Users/rory/repos/FilePathsBase.jl/README.md"
6972

70-
julia> chmod(p"README.md", m)
73+
julia> parents(p"./docs/src")
74+
2-element Array{PosixPath,1}:
75+
p"."
76+
p"./docs"
7177

72-
julia> m = mode(p"README.md")
73-
-rw-r--r--
78+
julia> parents(absolute(p"./docs/src"))
79+
6-element Array{PosixPath,1}:
80+
p"/"
81+
p"/Users"
82+
p"/Users/rory"
83+
p"/Users/rory/repos"
84+
p"/Users/rory/repos/FilePathsBase.jl"
85+
p"/Users/rory/repos/FilePathsBase.jl/docs"
7486

75-
julia> chmod(p"README.md", user=(READ+WRITE+EXEC), group=(READ+WRITE), other=READ)
87+
julia> absolute(p"./docs/src")[1:end-1]
88+
("Users", "rory", "repos", "FilePathsBase.jl", "docs")
7689

77-
julia> mode(p"README.md")
78-
-rwxrw-r--
90+
julia> tmpfp = mktempdir(SystemPath)
91+
p"/var/folders/vz/zx_0gsp9291dhv049t_nx37r0000gn/T/jl_1GCBFT"
7992

80-
```
93+
julia> sync(p"/Users/rory/repos/FilePathsBase.jl/docs", tmpfp / "docs")
94+
p"/var/folders/vz/zx_0gsp9291dhv049t_nx37r0000gn/T/jl_1GCBFT/docs"
95+
96+
julia> exists(tmpfp / "docs" / "make.jl")
97+
true
98+
99+
julia> m = mode(tmpfp / "docs" / "make.jl")
100+
Mode("-rw-r--r--")
101+
102+
julia> m - readable(:ALL)
103+
Mode("--w-------")
104+
105+
julia> m + executable(:ALL)
106+
Mode("-rwxr-xr-x")
107+
108+
julia> chmod(tmpfp / "docs" / "make.jl", "+x")
109+
"/var/folders/vz/zx_0gsp9291dhv049t_nx37r0000gn/T/jl_1GCBFT/docs/make.jl"
110+
111+
julia> mode(tmpfp / "docs" / "make.jl")
112+
Mode("-rwxr-xr-x")
113+
114+
# Count LOC
115+
julia> mapreduce(+, walkpath(cwd() / "src")) do x
116+
extension(x) == "jl" ? count("\n", read(x, String)) : 0
117+
end
118+
3020
119+
120+
# Concatenate multiple files.
121+
julia> str = mapreduce(*, walkpath(tmpfp / "docs" / "src")) do x
122+
read(x, String)
123+
end
124+
"# API\n\nAll the standard methods for working with paths in base julia exist in the FilePathsBase.jl. The following describes the rough mapping of method names. Use `?` at the REPL to get the documentation and arguments as they may be different than the base implementations.\n\n..."
125+
126+
# Could also write the result to a file with `write(newfile, str)`)
81127

82-
Reading and writing directly to file paths:
83-
```julia-repl
84-
julia> write(p"testfile", "foobar")
85-
6
128+
julia> rm(tmpfp; recursive=true)
86129

87-
julia> read(p"testfile")
88-
"foobar"
130+
julia> exists(tmpfp)
131+
false
89132
```

docs/.DS_Store

0 Bytes
Binary file not shown.

docs/make.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
using Documenter, FilePathsBase, FilePathsBase.TestPaths
22

3+
```@meta
4+
DocTestSetup = quote
5+
using FilePathsBase; using FilePathsBase: /, join;
6+
end
7+
```
8+
39
makedocs(
410
modules=[FilePathsBase],
511
format=Documenter.HTML(
612
prettyurls = get(ENV, "CI", nothing) == "true",
713
),
814
pages=[
915
"Home" => "index.md",
16+
"Design" => "design.md",
17+
"FAQ" => "faq.md",
1018
"API" => "api.md",
1119
],
1220
repo="https://github.com/rofinn/FilePathsBase.jl/blob/{commit}{path}#L{line}",

docs/src/design.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# [Design](@id design_header)
2+
3+
FilePaths.jl and FilePathsBase.jl have gone through several design iterations over the years.
4+
To help get potential contributors up-to-speed, we'll cover several background points and design choices.
5+
Whenever possible, we'll reference existing resources (e.g., GitHub issues, blog posts, documentation, software packages) for further reading.
6+
7+
## Filesystem Abstractions
8+
9+
While filesystems themselves are abstractions for data storage, many programming languages
10+
provide APIs for writing generic/cross-platform software.
11+
Typically, these abstractions can be broken down into string or typed based solutions.
12+
13+
### String APIs:
14+
15+
- Python: [`os.path`](https://docs.python.org/3.8/library/os.path.html)
16+
- Haskell: [`System.FilePath`](https://hackage.haskell.org/package/filepath-1.4.2.1/docs/System-FilePath.html)
17+
- Julia: [`Base.Filesystem`](https://docs.julialang.org/en/v1/base/file/)
18+
19+
This approach tends to be simpler and only requires adding utility methods for interacting with filesystems. Unfortunately, any operations require significant string manipulation to work, and it often cannot be extended for remote filesystems (e.g., S3, FTP, HTTP). Enforcing path validity becomes difficult when any string operation can be applied to the path type (e.g., `join(prefix, segments...)` vs `joinpath(prefix, segments...)`).
20+
21+
### Typed APIs:
22+
23+
- Python: [`pathlib`](https://docs.python.org/3/library/pathlib.html)
24+
- Rust: [`std::path`](https://doc.rust-lang.org/std/path/index.html)
25+
- C++: [`std::filesystem`](https://en.cppreference.com/w/cpp/filesystem/path)
26+
- Haskell: [`path`](https://hackage.haskell.org/package/path)
27+
- Scala: [`os-lib`](https://github.com/lihaoyi/os-lib)
28+
29+
The primary idea is that a filesystem path is just a sequence of path segments, and so very few path operations overlap with string operations.
30+
For example, you're unlikely to call string functions like `join(...)`, `chomp(...)`, `eachline(...)`, `match(regex, ...)` or `parse(Float64, ...)` with a filesystem path.
31+
Further, differentiating strings and paths allows us to define different equality rules and dispatch behaviour on filepaths in our APIs.
32+
Finally, by defining a common API for all `AbstractPaths`, we can write generic functions that work with `PosixPath`s, `WindowsPath`s, `S3Path`s, `FTPPath`s, etc.
33+
34+
## Path Types
35+
36+
In FilePathsBase.jl, file paths are first and foremost a type that wraps a tuple of strings, representing path `segments`.
37+
Most path types will also include a `root`, `drive` and `separator`.
38+
Concrete path types should either directly subtype `AbstractPath` or in the case of local filesystems (e.g., `PosixPath`, `WindowsPath`) from `SystemPath`, as shown in the diagram below.
39+
40+
![Hierarchy](hierarchy.svg)
41+
42+
Notice that our `AbstractPath` type no longer subtypes `AbstractString` like some other libraries.
43+
We [chose](https://github.com/rofinn/FilePathsBase.jl/issues/15) drop string subtyping because not all `AbstractString` operations make sense on paths, and even more seem like they should perform a fundamentally different operation as mentioned above.
44+
Similar points have been made for [why `pathlib.Path` doesn't inherit from `str` in Python](https://snarky.ca/why-pathlib-path-doesn-t-inherit-from-str/).

docs/src/faq.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# FAQ
2+
3+
Here we have a growing list of common technical and design questions folks have raised in the past.
4+
If you feel like something is missing, please open an issue or pull request to add it.
5+
6+
**Q. Should I depend on FilePathsBase.jl and FilePaths.jl?**
7+
8+
A. FilePathsBase.jl is a lightweight dependency for packages who need to operate on `AbstractPath` types and don't need to interact with other packages (e.g., Glob.jl, URIParser.jl, FileIO.jl).
9+
FilePaths.jl extends FilePathsBase.jl to improve package interop across the Julia ecosystem, at the cost of extra dependencies.
10+
In general, FilePathsBase.jl should be used for general (low level) packages. While scripts and application-level packages should use FilePaths.jl.
11+
12+
**Q. What's wrong with strings?**
13+
14+
A. In many cases, nothing.
15+
For local filesystem paths, there's often no functional difference between using an `AbstractPath` and a `String`.
16+
Some cases where the path type distinction is useful include:
17+
18+
- Path specific operations (e.g., `join`, `/`, `==`)
19+
- Dispatch on paths vs strings (e.g., `project(name::String) = project(DEFAULT_ROOT / name)`)
20+
21+
See [design section](@ref design_header) for more details on the advantages of path types over strings.
22+
23+
**Q. Why is `AbstractPath` not a subtype of `AbstractString`?
24+
25+
A. Initially, we made `AbstractPath` a subtype of `AbstractString`, but falling back to string operations often didn't make sense (e.g., `ascii(::AbstractPath)`, `chomp(::AbstractPath)`, `match(::Regex, ::AbstractPath)`, `parse(::Type{Float64}, ::AbstractPath)`).
26+
Having a distinct path type results in fewer confusing error messages and more explicit code (via type conversions). [Link to issue/PR and blog post]
27+
28+
**Q. Why don't you concatenate paths with `*`?**
29+
30+
A. By using `/` for path concatenation (`joinpath`), we can continue to support string concatenation with `*`:
31+
32+
```julia
33+
julia> cwd() / "src" / "FilePathsBase" * ".jl"
34+
p"/Users/rory/repos/FilePathsBase.jl/src/FilePathsBase.jl
35+
```
36+
37+
**Q. How do I write code that works with strings and paths?**
38+
39+
A. FilePathsBase.jl intentionally provides aliases for `Base.Filesystem` functions, so you can perform base filesystem operations on strings and paths interchangeable.
40+
If something is missing please open an issue or pull request.
41+
Here are some more concrete tips to help you write generic code:
42+
- Don't [overly constrain](https://white.ucc.asn.au/2020/04/19/Julia-Antipatterns.html#over-constraining-argument-types) your argument types.
43+
- Avoid manual string manipulations (e.g., `match`, `replace`).
44+
- Stick to the overlapping base filesystem aliases (e.g., `joinpath` vs `/`, `normpath` vs `normalize`).
45+
46+
NOTE: The first 2 points are just general best practices independent of path types.
47+
Unfortunately, the last point is a result of the `Base.Filesystem` API (could change if FilePathsBase.jl becomes a stdlib).
48+
49+
See the usage guide for examples.
50+
51+
**Q: FilePathsBase doesn't work with package X?**
52+
53+
A: In many cases, filepath types and strings are interchangable, but if a specific package constrains the argument type (e.g., `AbstractString`, `String`) then you'll get a `MethodError`.
54+
There are a few solutions to this problem.
55+
56+
1. Loosen the argument type constraint in the given package.
57+
2. Add a separate dispatch for `AbstractPath` and add a dependency on FilePathsBase.jl.
58+
3. For very general/lightweight packages we can add the dependency to FilePaths.jl and extend the offending function there.
59+
4. Manually convert your path to a string before calling into the package.
60+
You may need to parse any returned paths to back to a filepath type if necessary.
61+
62+
NOTE: For larger packages, FilePaths.jl provides an `@convert` macro which will handle generating appropriate conversion methods for you.

0 commit comments

Comments
 (0)