Skip to content

Commit e2788b2

Browse files
committed
Add path types (#149)
1 parent 30780dd commit e2788b2

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed

pydantic_extra_types/path.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from __future__ import annotations
2+
3+
import typing
4+
from dataclasses import dataclass
5+
from pathlib import Path
6+
7+
import pydantic
8+
from pydantic.types import PathType
9+
from pydantic_core import core_schema
10+
11+
ExistingPath = pydantic.FilePath | pydantic.DirectoryPath
12+
13+
14+
@dataclass
15+
class ResolvedPathType(PathType):
16+
"""A custom PathType that resolves the path to its absolute form.
17+
18+
Args:
19+
path_type (typing.Literal['file', 'dir', 'new']): The type of path to resolve. Can be 'file', 'dir' or 'new'.
20+
21+
Returns:
22+
Resolved path as a pathlib.Path object.
23+
24+
Example:
25+
```python
26+
from pydantic import BaseModel
27+
from pydantic_extra_types.path import ResolvedFilePath, ResolvedDirectoryPath, ResolvedNewPath
28+
29+
30+
class MyModel(BaseModel):
31+
file_path: ResolvedFilePath
32+
dir_path: ResolvedDirectoryPath
33+
new_path: ResolvedNewPath
34+
35+
36+
model = MyModel(file_path='~/myfile.txt', dir_path='~/mydir', new_path='~/newfile.txt')
37+
print(model.file_path)
38+
# > file_path=PosixPath('/home/user/myfile.txt') dir_path=PosixPath('/home/user/mydir') new_path=PosixPath('/home/user/newfile.txt')"""
39+
40+
@staticmethod
41+
def validate_file(path: Path, _: core_schema.ValidationInfo) -> Path:
42+
return PathType.validate_file(path.expanduser().resolve(), _)
43+
44+
@staticmethod
45+
def validate_directory(path: Path, _: core_schema.ValidationInfo) -> Path:
46+
return PathType.validate_directory(path.expanduser().resolve(), _)
47+
48+
@staticmethod
49+
def validate_new(path: Path, _: core_schema.ValidationInfo) -> Path:
50+
return PathType.validate_new(path.expanduser().resolve(), _)
51+
52+
def __hash__(self) -> int:
53+
return hash(type(self.path_type))
54+
55+
56+
ResolvedFilePath = typing.Annotated[Path, ResolvedPathType('file')]
57+
ResolvedDirectoryPath = typing.Annotated[Path, ResolvedPathType('dir')]
58+
ResolvedNewPath = typing.Annotated[Path, ResolvedPathType('new')]
59+
ResolvedExistingPath = ResolvedFilePath | ResolvedDirectoryPath

tests/test_path.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import os
2+
import pathlib
3+
4+
import pytest
5+
from pydantic import BaseModel
6+
7+
from pydantic_extra_types.path import (
8+
ExistingPath,
9+
ResolvedDirectoryPath,
10+
ResolvedExistingPath,
11+
ResolvedFilePath,
12+
ResolvedNewPath,
13+
)
14+
15+
16+
class File(BaseModel):
17+
file: ResolvedFilePath
18+
19+
20+
class Directory(BaseModel):
21+
directory: ResolvedDirectoryPath
22+
23+
24+
class NewPath(BaseModel):
25+
new_path: ResolvedNewPath
26+
27+
28+
class Existing(BaseModel):
29+
existing: ExistingPath
30+
31+
32+
class ResolvedExisting(BaseModel):
33+
resolved_existing: ResolvedExistingPath
34+
35+
36+
@pytest.fixture
37+
def absolute_file_path(tmp_path: pathlib.Path) -> pathlib.Path:
38+
directory = tmp_path / 'test-relative'
39+
directory.mkdir()
40+
file_path = directory / 'test-relative.txt'
41+
file_path.touch()
42+
return file_path
43+
44+
45+
@pytest.fixture
46+
def relative_file_path(absolute_file_path: pathlib.Path) -> pathlib.Path:
47+
return pathlib.Path(os.path.relpath(absolute_file_path, os.getcwd()))
48+
49+
50+
@pytest.fixture
51+
def absolute_directory_path(tmp_path: pathlib.Path) -> pathlib.Path:
52+
directory = tmp_path / 'test-relative'
53+
directory.mkdir()
54+
return directory
55+
56+
57+
@pytest.fixture
58+
def relative_directory_path(absolute_directory_path: pathlib.Path) -> pathlib.Path:
59+
return pathlib.Path(os.path.relpath(absolute_directory_path, os.getcwd()))
60+
61+
62+
@pytest.fixture
63+
def absolute_new_path(tmp_path: pathlib.Path) -> pathlib.Path:
64+
return tmp_path / 'test-relative'
65+
66+
67+
@pytest.fixture
68+
def relative_new_path(absolute_new_path: pathlib.Path) -> pathlib.Path:
69+
return pathlib.Path(os.path.relpath(absolute_new_path, os.getcwd()))
70+
71+
72+
def test_relative_file(absolute_file_path: pathlib.Path, relative_file_path: pathlib.Path):
73+
file = File(file=relative_file_path)
74+
assert file.file == absolute_file_path
75+
76+
77+
def test_absolute_file(absolute_file_path: pathlib.Path):
78+
file = File(file=absolute_file_path)
79+
assert file.file == absolute_file_path
80+
81+
82+
def test_relative_directory(absolute_directory_path: pathlib.Path, relative_directory_path: pathlib.Path):
83+
directory = Directory(directory=relative_directory_path)
84+
assert directory.directory == absolute_directory_path
85+
86+
87+
def test_absolute_directory(absolute_directory_path: pathlib.Path):
88+
directory = Directory(directory=absolute_directory_path)
89+
assert directory.directory == absolute_directory_path
90+
91+
92+
def test_relative_new_path(absolute_new_path: pathlib.Path, relative_new_path: pathlib.Path):
93+
new_path = NewPath(new_path=relative_new_path)
94+
assert new_path.new_path == absolute_new_path
95+
96+
97+
def test_absolute_new_path(absolute_new_path: pathlib.Path):
98+
new_path = NewPath(new_path=absolute_new_path)
99+
assert new_path.new_path == absolute_new_path
100+
101+
102+
@pytest.mark.parametrize(
103+
('pass_fixture', 'expect_fixture'),
104+
(
105+
('relative_file_path', 'relative_file_path'),
106+
('absolute_file_path', 'absolute_file_path'),
107+
('relative_directory_path', 'relative_directory_path'),
108+
('absolute_directory_path', 'absolute_directory_path'),
109+
),
110+
)
111+
def test_existing_path(request: pytest.FixtureRequest, pass_fixture: str, expect_fixture: str):
112+
existing = Existing(existing=request.getfixturevalue(pass_fixture))
113+
assert existing.existing == request.getfixturevalue(expect_fixture)
114+
115+
116+
@pytest.mark.parametrize(
117+
('pass_fixture', 'expect_fixture'),
118+
(
119+
('relative_file_path', 'absolute_file_path'),
120+
('absolute_file_path', 'absolute_file_path'),
121+
('relative_directory_path', 'absolute_directory_path'),
122+
('absolute_directory_path', 'absolute_directory_path'),
123+
),
124+
)
125+
def test_resolved_existing_path(request: pytest.FixtureRequest, pass_fixture: str, expect_fixture: str):
126+
resolved_existing = ResolvedExisting(resolved_existing=request.getfixturevalue(pass_fixture))
127+
assert resolved_existing.resolved_existing == request.getfixturevalue(expect_fixture)

0 commit comments

Comments
 (0)