pathroot 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pathroot-1.0.0/MANIFEST.in +1 -0
- pathroot-1.0.0/PKG-INFO +38 -0
- pathroot-1.0.0/README.md +24 -0
- pathroot-1.0.0/pathroot.egg-info/PKG-INFO +38 -0
- pathroot-1.0.0/pathroot.egg-info/SOURCES.txt +10 -0
- pathroot-1.0.0/pathroot.egg-info/dependency_links.txt +1 -0
- pathroot-1.0.0/pathroot.egg-info/requires.txt +4 -0
- pathroot-1.0.0/pathroot.egg-info/top_level.txt +1 -0
- pathroot-1.0.0/pathroot.py +170 -0
- pathroot-1.0.0/pyproject.toml +70 -0
- pathroot-1.0.0/setup.cfg +4 -0
- pathroot-1.0.0/test_pathroot.py +221 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include test_pathroot.py
|
pathroot-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pathroot
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Subclass of a pathlib.Path object that does not allow traversal outside of a trusted root.
|
|
5
|
+
Author-email: Josh Schneider <josh.schneider@gmail.com>
|
|
6
|
+
Maintainer-email: Josh Schneider <josh.schneider@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: path,pathlib,filesystem
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: ruff; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# PathRoot
|
|
16
|
+
|
|
17
|
+
## What is PathRoot?
|
|
18
|
+
|
|
19
|
+
A `PathRoot` object is a subclass of `pathlib.Path`. It takes an extra `safe_root=` keyword argument to set a trusted
|
|
20
|
+
root, and prevents operations that traverse outside of the trusted root.
|
|
21
|
+
|
|
22
|
+
## How do you use PathRoot?
|
|
23
|
+
|
|
24
|
+
You can initialize a `PathRoot` object like this:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from pathroot import PathRoot
|
|
28
|
+
|
|
29
|
+
root = PathRoot('/Users/foo/bar', safe_root='/Users/foo/bar')
|
|
30
|
+
root = PathRoot('/Users/foo/bar') # This also works.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
From there, you can do anything you can do with a `Path` object. For instance:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
my_file = root / 'groceries.txt' # This would work.
|
|
37
|
+
my_file = root / '..' / '..' / 'groceries.txt' # This would raise a `PathOutsideRootError` exception.
|
|
38
|
+
```
|
pathroot-1.0.0/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# PathRoot
|
|
2
|
+
|
|
3
|
+
## What is PathRoot?
|
|
4
|
+
|
|
5
|
+
A `PathRoot` object is a subclass of `pathlib.Path`. It takes an extra `safe_root=` keyword argument to set a trusted
|
|
6
|
+
root, and prevents operations that traverse outside of the trusted root.
|
|
7
|
+
|
|
8
|
+
## How do you use PathRoot?
|
|
9
|
+
|
|
10
|
+
You can initialize a `PathRoot` object like this:
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from pathroot import PathRoot
|
|
14
|
+
|
|
15
|
+
root = PathRoot('/Users/foo/bar', safe_root='/Users/foo/bar')
|
|
16
|
+
root = PathRoot('/Users/foo/bar') # This also works.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
From there, you can do anything you can do with a `Path` object. For instance:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
my_file = root / 'groceries.txt' # This would work.
|
|
23
|
+
my_file = root / '..' / '..' / 'groceries.txt' # This would raise a `PathOutsideRootError` exception.
|
|
24
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pathroot
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Subclass of a pathlib.Path object that does not allow traversal outside of a trusted root.
|
|
5
|
+
Author-email: Josh Schneider <josh.schneider@gmail.com>
|
|
6
|
+
Maintainer-email: Josh Schneider <josh.schneider@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: path,pathlib,filesystem
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: ruff; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# PathRoot
|
|
16
|
+
|
|
17
|
+
## What is PathRoot?
|
|
18
|
+
|
|
19
|
+
A `PathRoot` object is a subclass of `pathlib.Path`. It takes an extra `safe_root=` keyword argument to set a trusted
|
|
20
|
+
root, and prevents operations that traverse outside of the trusted root.
|
|
21
|
+
|
|
22
|
+
## How do you use PathRoot?
|
|
23
|
+
|
|
24
|
+
You can initialize a `PathRoot` object like this:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from pathroot import PathRoot
|
|
28
|
+
|
|
29
|
+
root = PathRoot('/Users/foo/bar', safe_root='/Users/foo/bar')
|
|
30
|
+
root = PathRoot('/Users/foo/bar') # This also works.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
From there, you can do anything you can do with a `Path` object. For instance:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
my_file = root / 'groceries.txt' # This would work.
|
|
37
|
+
my_file = root / '..' / '..' / 'groceries.txt' # This would raise a `PathOutsideRootError` exception.
|
|
38
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pathroot
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Variant of a Path that does not allow traversal outside of the root."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path, PosixPath, WindowsPath
|
|
8
|
+
|
|
9
|
+
LOG = logging.getLogger(__name__)
|
|
10
|
+
OS_NAME = os.name
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PathOutsideRootError(OSError):
|
|
14
|
+
"""Exception to raise when a path traverses outside a root."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, path: Path, root: PathRoot, *args):
|
|
17
|
+
"""Prepare a PathOutsideRootError for use.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
path: Target path.
|
|
21
|
+
root: Trusted root path.
|
|
22
|
+
*args: Arguments passed to OSError.
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(*args)
|
|
25
|
+
self.path = path
|
|
26
|
+
self.root = root
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
"""String message."""
|
|
30
|
+
return f"Path {self.path} ({self.path.resolve()}) is outside of {self.root}."
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PathRoot(Path):
|
|
34
|
+
"""Base class for a path that does not allow traversal outside.
|
|
35
|
+
|
|
36
|
+
Notes:
|
|
37
|
+
When a PathRoot is first instantiated, if a `safe_root` is not provided, then
|
|
38
|
+
the current directory is used as the SafeRoot. All methods that mutate the path
|
|
39
|
+
or work off of additional provided paths have those paths resolved and checked
|
|
40
|
+
against the safe root. If the resolved path is not relative to the safe root,
|
|
41
|
+
then a `PathOutsideRootError` is raised.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __new__(cls, *args, **kwargs) -> WindowsPathRoot | PosixPathRoot: # noqa: ARG004
|
|
45
|
+
"""Generate the OS-specific subclass based on the current OS."""
|
|
46
|
+
if cls is PathRoot:
|
|
47
|
+
cls = WindowsPathRoot if OS_NAME == "nt" else PosixPathRoot
|
|
48
|
+
return object.__new__(cls)
|
|
49
|
+
|
|
50
|
+
def __init__(self, *args, safe_root: Path | None = None):
|
|
51
|
+
"""Prepare a PathRoot for use.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
*args: Path segments, passed to Path.
|
|
55
|
+
safe_root: Root path to use for all operations. Defaults to None (current path is used).
|
|
56
|
+
"""
|
|
57
|
+
super().__init__(*args)
|
|
58
|
+
|
|
59
|
+
# If the safe_root is None, then one was not provided. Look through the args
|
|
60
|
+
# and see if we have any PathRoot instances... first one wins.
|
|
61
|
+
if safe_root is None:
|
|
62
|
+
for arg in args:
|
|
63
|
+
if isinstance(arg, PathRoot):
|
|
64
|
+
safe_root = arg.safe_root
|
|
65
|
+
break
|
|
66
|
+
else: # no break
|
|
67
|
+
# Set the safe_root to this path.
|
|
68
|
+
safe_root = Path(self)
|
|
69
|
+
self.safe_root = safe_root.resolve() # Ensure safe_root is resolved.
|
|
70
|
+
LOG.debug("Created %r", self)
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
"""Internal string representation."""
|
|
74
|
+
return f"{type(self).__name__}({self.as_posix()!r}, safe_root={self.safe_root.as_posix()!r})"
|
|
75
|
+
|
|
76
|
+
def __check_path(self, path: Path | PathRoot) -> PathRoot:
|
|
77
|
+
"""Check if a path traverses outside.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
path: Path to check.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The tested path.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
PathOutsideRootError: If the path traverses outside of the root path.
|
|
87
|
+
"""
|
|
88
|
+
p = Path(path).resolve()
|
|
89
|
+
LOG.debug("Testing %s against %s", p, self.safe_root)
|
|
90
|
+
if not p.is_relative_to(self.safe_root):
|
|
91
|
+
raise PathOutsideRootError(path, self.safe_root)
|
|
92
|
+
|
|
93
|
+
match path:
|
|
94
|
+
# If the path is a PathRoot with no safe_root set, set it.
|
|
95
|
+
case PathRoot():
|
|
96
|
+
path.safe_root = self.safe_root
|
|
97
|
+
|
|
98
|
+
# If the path is not a PathRoot, make it one.
|
|
99
|
+
case Path() if not isinstance(path, PathRoot):
|
|
100
|
+
path = PathRoot(path, safe_root=self.safe_root)
|
|
101
|
+
|
|
102
|
+
return path
|
|
103
|
+
|
|
104
|
+
def with_segments(self, *args) -> PathRoot:
|
|
105
|
+
"""Return a new path with segments.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
*args: Path segments.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
New path.
|
|
112
|
+
"""
|
|
113
|
+
return self.__check_path(super().with_segments(*args))
|
|
114
|
+
|
|
115
|
+
def rename(self, target: Path | str) -> PathRoot:
|
|
116
|
+
"""Rename this path to the target path.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
target: Target path. Must be in the root.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
New PathRoot instance pointing to the target path.
|
|
123
|
+
|
|
124
|
+
Notes:
|
|
125
|
+
The target path may be absolute or relative. Relative paths are
|
|
126
|
+
interpreted relative to the current working directory *not* the
|
|
127
|
+
directory of the Path object.
|
|
128
|
+
"""
|
|
129
|
+
return super().rename(self.__check_path(target))
|
|
130
|
+
|
|
131
|
+
def replace(self, target: Path | str) -> PathRoot:
|
|
132
|
+
"""Rename this path to the target path, overwriting if that path exists.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
target: Target path. Must be in the root.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
New PathRoot instance pointing to the target path.
|
|
139
|
+
|
|
140
|
+
Notes:
|
|
141
|
+
The target path may be absolute or relative. Relative paths are
|
|
142
|
+
interpreted relative to the current working directory *not* the
|
|
143
|
+
directory of the Path object.
|
|
144
|
+
"""
|
|
145
|
+
return super().replace(self.__check_path(target))
|
|
146
|
+
|
|
147
|
+
def symlink_to(self, target: Path | str, target_is_directory: bool = False) -> None:
|
|
148
|
+
"""Make this path a symlink pointing to the target path.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
target: Target to link to. Must be inside root path.
|
|
152
|
+
target_is_directory: Should the target be treated as a directory (only valid for Windows). Defaults to False.
|
|
153
|
+
"""
|
|
154
|
+
return super().symlink_to(self.__check_path(target), target_is_directory)
|
|
155
|
+
|
|
156
|
+
def hardlink_to(self, target: Path | str) -> None:
|
|
157
|
+
"""Make this path a hard link pointing to the same file as *target*.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
target: Target to link to. Must be inside the root path.
|
|
161
|
+
"""
|
|
162
|
+
return super().hardlink_to(self.__check_path(target))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class PosixPathRoot(PosixPath, PathRoot):
|
|
166
|
+
"""Path that does not allow traversal outside of root for Linux/MacOS."""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class WindowsPathRoot(WindowsPath, PathRoot):
|
|
170
|
+
"""Path that does not allow traversal outside of the root for Windows."""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pathroot"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Subclass of a pathlib.Path object that does not allow traversal outside of a trusted root."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = []
|
|
8
|
+
authors = [{ name = "Josh Schneider", email = "josh.schneider@gmail.com" }]
|
|
9
|
+
maintainers = [{ name = "Josh Schneider", email = "josh.schneider@gmail.com" }]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
keywords = ["path", "pathlib", "filesystem"]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
dev = ["ruff", "pytest"]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["setuptools"]
|
|
18
|
+
build-backend = "setuptools.build_meta"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
py-modules = ["pathroot"]
|
|
22
|
+
|
|
23
|
+
[tool.ruff]
|
|
24
|
+
line-length = 120
|
|
25
|
+
respect-gitignore = true
|
|
26
|
+
|
|
27
|
+
[tool.ruff.lint]
|
|
28
|
+
select = [
|
|
29
|
+
"C90", # mccabe
|
|
30
|
+
"I", # iSort
|
|
31
|
+
"N", # pep8-naming
|
|
32
|
+
"D", # pydocstyle
|
|
33
|
+
"ANN", # flake8-annotations
|
|
34
|
+
"S", # flake8-bandit
|
|
35
|
+
"B", # flake8-bugbear
|
|
36
|
+
"A", # flake8-builtins
|
|
37
|
+
"COM", # flake8-commas
|
|
38
|
+
"C4", # flake8-comprehensions
|
|
39
|
+
"EM", # flake8-errmsg
|
|
40
|
+
"FA", # flake8-future-annotations
|
|
41
|
+
"ISC", # flake8-implicit-str-concat
|
|
42
|
+
"LOG", # flake8-logging
|
|
43
|
+
"G", # flake8-logging-format
|
|
44
|
+
"T20", # flake8-print
|
|
45
|
+
"Q", # flake8-quotes
|
|
46
|
+
"SIM", # flake8-simplify
|
|
47
|
+
"ARG", # flake8-unused-arguments
|
|
48
|
+
"PT", # flake8-pytest-style
|
|
49
|
+
"PTH", # flake8-use-pathlib
|
|
50
|
+
"PERF", # perflint
|
|
51
|
+
"RUF", # Ruff-specific rules
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
ignore = [
|
|
55
|
+
"ANN002", # No need to type *args
|
|
56
|
+
"ANN003", # No need to type **kwargs
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
60
|
+
"test_*.py" = [ # Pytest tests
|
|
61
|
+
"ANN001", # No need to type hint fixtures
|
|
62
|
+
"T201", # Print is fine in unit tests
|
|
63
|
+
"S101", # assert is kinda the reason we're here...
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.pydocstyle]
|
|
67
|
+
convention = "google"
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint.flake8-annotations]
|
|
70
|
+
suppress-none-returning = true
|
pathroot-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Unit tests for pathroot."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import pathroot
|
|
10
|
+
|
|
11
|
+
LOG = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Dictionary of test files to create in root_folder.
|
|
14
|
+
# Keys should be Path objects, and values should be None
|
|
15
|
+
# for directories, or a bytes object for contents.
|
|
16
|
+
TEST_FILES = {
|
|
17
|
+
Path("d1"): None,
|
|
18
|
+
Path("d1/f1.txt"): b"First file",
|
|
19
|
+
Path("d2"): None,
|
|
20
|
+
Path("d2/f2.txt"): b"Second file",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# region Fixtures
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def root_folder(tmp_path) -> Path: # type: ignore
|
|
27
|
+
"""Self cleaning test folder, populated by TEST_FILES."""
|
|
28
|
+
for p, c in TEST_FILES.items():
|
|
29
|
+
p = tmp_path / p
|
|
30
|
+
if c is None:
|
|
31
|
+
p.mkdir(exist_ok=True, parents=True)
|
|
32
|
+
LOG.info("** Created dir %s", p)
|
|
33
|
+
else:
|
|
34
|
+
p.parent.mkdir(exist_ok=True, parents=True)
|
|
35
|
+
p.write_bytes(c)
|
|
36
|
+
LOG.info("** Create file %s", p)
|
|
37
|
+
|
|
38
|
+
LOG.info("** Returning %s", tmp_path)
|
|
39
|
+
yield tmp_path
|
|
40
|
+
|
|
41
|
+
for p in sorted(tmp_path.rglob("*"), reverse=True):
|
|
42
|
+
if p.is_symlink() or p.is_file():
|
|
43
|
+
p.unlink()
|
|
44
|
+
LOG.info("** Unlinking %s", p)
|
|
45
|
+
elif p.is_dir():
|
|
46
|
+
p.rmdir()
|
|
47
|
+
LOG.info("** Removing dir %s", p)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@contextmanager
|
|
51
|
+
def fix_os_name(v: str):
|
|
52
|
+
"""Context manager for replacing pathroot.OS_NAME temporarily.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
v: Value to set OS_NAME to.
|
|
56
|
+
"""
|
|
57
|
+
old_val = pathroot.OS_NAME
|
|
58
|
+
pathroot.OS_NAME = v
|
|
59
|
+
LOG.info("** Set OS_NAME to %r", v)
|
|
60
|
+
try:
|
|
61
|
+
yield
|
|
62
|
+
finally:
|
|
63
|
+
pathroot.OS_NAME = old_val
|
|
64
|
+
LOG.info("** Set OS_NAME back to %r", old_val)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.fixture
|
|
68
|
+
def _force_nt():
|
|
69
|
+
"""Force the OS name to nt (for Windows)."""
|
|
70
|
+
with fix_os_name("nt"):
|
|
71
|
+
yield
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.fixture
|
|
75
|
+
def _force_posix():
|
|
76
|
+
"""Force the OS name to darwin (for POSIX)."""
|
|
77
|
+
with fix_os_name("darwin"):
|
|
78
|
+
yield
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# endregion
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# region Tests
|
|
85
|
+
@pytest.mark.usefixtures("_force_nt")
|
|
86
|
+
def test_new_windows(root_folder):
|
|
87
|
+
"""Test that PathRoot, on Windows, returns a WindowsPathRoot instance."""
|
|
88
|
+
# Act
|
|
89
|
+
r = pathroot.PathRoot(root_folder)
|
|
90
|
+
|
|
91
|
+
# Assert
|
|
92
|
+
assert type(r) is pathroot.WindowsPathRoot
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.usefixtures("_force_posix")
|
|
96
|
+
def test_new_posix(root_folder):
|
|
97
|
+
"""Test that PathRoot, on a POSIX OS, returns a PosixPathRoot instance."""
|
|
98
|
+
# Act
|
|
99
|
+
r = pathroot.PathRoot(root_folder)
|
|
100
|
+
|
|
101
|
+
# Assert
|
|
102
|
+
assert type(r) is pathroot.PosixPathRoot
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_joinpath_works(root_folder):
|
|
106
|
+
"""Test that when we use joinpath with a path inside the the root, it works, and we get a PathRoot instance."""
|
|
107
|
+
# Arrange
|
|
108
|
+
r = pathroot.PathRoot(root_folder)
|
|
109
|
+
|
|
110
|
+
# Act
|
|
111
|
+
p1 = r.joinpath("foo/bar.txt")
|
|
112
|
+
|
|
113
|
+
# Assert
|
|
114
|
+
assert isinstance(p1, pathroot.PathRoot)
|
|
115
|
+
assert p1.safe_root is r.safe_root
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_joinpath_errors(root_folder):
|
|
119
|
+
"""Test that when we use joinpath with a path outside the root, it raises a PathOutsideRootError."""
|
|
120
|
+
# Arrange
|
|
121
|
+
r = pathroot.PathRoot(root_folder)
|
|
122
|
+
|
|
123
|
+
# Act and Assert
|
|
124
|
+
with pytest.raises(pathroot.PathOutsideRootError):
|
|
125
|
+
r.joinpath("..", "..", "etc")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_divide_works(root_folder):
|
|
129
|
+
"""Test that when we use the divide operator inside the root, it works, and we get a PathRoot instance."""
|
|
130
|
+
# Arrange
|
|
131
|
+
r = pathroot.PathRoot(root_folder)
|
|
132
|
+
|
|
133
|
+
# Act
|
|
134
|
+
p1 = r / "foo" / "bar.txt"
|
|
135
|
+
|
|
136
|
+
# Assert
|
|
137
|
+
assert isinstance(p1, pathroot.PathRoot)
|
|
138
|
+
assert p1.safe_root is r.safe_root
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_divide_errors(root_folder):
|
|
142
|
+
"""Test that when we use the divide operator outside the root, it raises a PathOutsideRootError."""
|
|
143
|
+
# Arrange
|
|
144
|
+
r = pathroot.PathRoot(root_folder)
|
|
145
|
+
|
|
146
|
+
# Act and Assert
|
|
147
|
+
with pytest.raises(pathroot.PathOutsideRootError):
|
|
148
|
+
r / ".." / ".." / "etc"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_with_segments_works(root_folder):
|
|
152
|
+
"""Test that with_segments with a path inside the root works, and we get a PathRoot instance."""
|
|
153
|
+
# Arrange
|
|
154
|
+
r = pathroot.PathRoot(root_folder)
|
|
155
|
+
|
|
156
|
+
# Act
|
|
157
|
+
p1 = r.with_segments(root_folder, "foo/bar.txt")
|
|
158
|
+
|
|
159
|
+
# Assert
|
|
160
|
+
assert isinstance(p1, pathroot.PathRoot)
|
|
161
|
+
assert p1.safe_root is r.safe_root
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_with_segments_errors(root_folder):
|
|
165
|
+
"""Test that when we use with_segments with a path inside the the root, it works, and we get a PathRoot instance."""
|
|
166
|
+
# Arrange
|
|
167
|
+
r = pathroot.PathRoot(root_folder)
|
|
168
|
+
|
|
169
|
+
# Act and Assert
|
|
170
|
+
with pytest.raises(pathroot.PathOutsideRootError):
|
|
171
|
+
r.with_segments(root_folder, "..", "..", "etc")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_rename_works(root_folder):
|
|
175
|
+
"""Test that rename works when it should."""
|
|
176
|
+
# Arrange
|
|
177
|
+
p1 = pathroot.PathRoot(root_folder) / "d1"
|
|
178
|
+
|
|
179
|
+
# Act
|
|
180
|
+
p2 = p1.rename(root_folder / "d3")
|
|
181
|
+
|
|
182
|
+
# Assert
|
|
183
|
+
assert isinstance(p2, pathroot.PathRoot)
|
|
184
|
+
assert p2.safe_root is p1.safe_root
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_rename_errors(root_folder):
|
|
188
|
+
"""Test that rename errors when the target path is outside of the root."""
|
|
189
|
+
# Arrange
|
|
190
|
+
r = pathroot.PathRoot(root_folder)
|
|
191
|
+
|
|
192
|
+
# Act and Assert
|
|
193
|
+
with pytest.raises(pathroot.PathOutsideRootError):
|
|
194
|
+
r.rename(root_folder / ".." / ".." / "etc")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_replace_works(root_folder):
|
|
198
|
+
"""Test that replae works."""
|
|
199
|
+
# Arrange
|
|
200
|
+
p1 = pathroot.PathRoot(root_folder) / "d1"
|
|
201
|
+
|
|
202
|
+
# Act
|
|
203
|
+
p2 = p1.replace(root_folder / "d3")
|
|
204
|
+
|
|
205
|
+
# Assert
|
|
206
|
+
assert isinstance(p2, pathroot.PathRoot)
|
|
207
|
+
assert p2.safe_root is p1.safe_root
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_replace_errors(root_folder):
|
|
211
|
+
"""Test that replace errors when the target path is outside of the root."""
|
|
212
|
+
# Arrange
|
|
213
|
+
r = pathroot.PathRoot(root_folder)
|
|
214
|
+
|
|
215
|
+
# Act and Assert
|
|
216
|
+
with pytest.raises(pathroot.PathOutsideRootError):
|
|
217
|
+
r.replace(root_folder / ".." / ".." / "etc")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# TODO: Other corner cases?
|
|
221
|
+
# endregion
|