Skip to content

Commit 6a66fa7

Browse files
authored
✨ Add path types (#318)
1 parent ca36479 commit 6a66fa7

File tree

2 files changed

+187
-0
lines changed

2 files changed

+187
-0
lines changed

pydantic_extra_types/path.py

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