Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions pydantic_extra_types/path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import annotations

import typing
from dataclasses import dataclass
from pathlib import Path

import pydantic
from pydantic.types import PathType
from pydantic_core import core_schema
from typing_extensions import Annotated

ExistingPath = typing.Union[pydantic.FilePath, pydantic.DirectoryPath]


@dataclass
class ResolvedPathType(PathType):
"""A custom PathType that resolves the path to its absolute form.

Args:
path_type (typing.Literal['file', 'dir', 'new']): The type of path to resolve. Can be 'file', 'dir' or 'new'.

Returns:
Resolved path as a pathlib.Path object.

Example:
```python
from pydantic import BaseModel
from pydantic_extra_types.path import ResolvedFilePath, ResolvedDirectoryPath, ResolvedNewPath


class MyModel(BaseModel):
file_path: ResolvedFilePath
dir_path: ResolvedDirectoryPath
new_path: ResolvedNewPath


model = MyModel(file_path='~/myfile.txt', dir_path='~/mydir', new_path='~/newfile.txt')
print(model.file_path)
# > file_path=PosixPath('/home/user/myfile.txt') dir_path=PosixPath('/home/user/mydir') new_path=PosixPath('/home/user/newfile.txt')"""

@staticmethod
def validate_file(path: Path, _: core_schema.ValidationInfo) -> Path:
return PathType.validate_file(path.expanduser().resolve(), _)

@staticmethod
def validate_directory(path: Path, _: core_schema.ValidationInfo) -> Path:
return PathType.validate_directory(path.expanduser().resolve(), _)

@staticmethod
def validate_new(path: Path, _: core_schema.ValidationInfo) -> Path:
return PathType.validate_new(path.expanduser().resolve(), _)

def __hash__(self) -> int:
return hash(type(self.path_type))


ResolvedFilePath = Annotated[Path, ResolvedPathType('file')]
ResolvedDirectoryPath = Annotated[Path, ResolvedPathType('dir')]
ResolvedNewPath = Annotated[Path, ResolvedPathType('new')]
ResolvedExistingPath = typing.Union[ResolvedFilePath, ResolvedDirectoryPath]
127 changes: 127 additions & 0 deletions tests/test_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import os
import pathlib

import pytest
from pydantic import BaseModel

from pydantic_extra_types.path import (
ExistingPath,
ResolvedDirectoryPath,
ResolvedExistingPath,
ResolvedFilePath,
ResolvedNewPath,
)


class File(BaseModel):
file: ResolvedFilePath


class Directory(BaseModel):
directory: ResolvedDirectoryPath


class NewPath(BaseModel):
new_path: ResolvedNewPath


class Existing(BaseModel):
existing: ExistingPath


class ResolvedExisting(BaseModel):
resolved_existing: ResolvedExistingPath


@pytest.fixture
def absolute_file_path(tmp_path: pathlib.Path) -> pathlib.Path:
directory = tmp_path / 'test-relative'
directory.mkdir()
file_path = directory / 'test-relative.txt'
file_path.touch()
return file_path


@pytest.fixture
def relative_file_path(absolute_file_path: pathlib.Path) -> pathlib.Path:
return pathlib.Path(os.path.relpath(absolute_file_path, os.getcwd()))


@pytest.fixture
def absolute_directory_path(tmp_path: pathlib.Path) -> pathlib.Path:
directory = tmp_path / 'test-relative'
directory.mkdir()
return directory


@pytest.fixture
def relative_directory_path(absolute_directory_path: pathlib.Path) -> pathlib.Path:
return pathlib.Path(os.path.relpath(absolute_directory_path, os.getcwd()))


@pytest.fixture
def absolute_new_path(tmp_path: pathlib.Path) -> pathlib.Path:
return tmp_path / 'test-relative'


@pytest.fixture
def relative_new_path(absolute_new_path: pathlib.Path) -> pathlib.Path:
return pathlib.Path(os.path.relpath(absolute_new_path, os.getcwd()))


def test_relative_file(absolute_file_path: pathlib.Path, relative_file_path: pathlib.Path):
file = File(file=relative_file_path)
assert file.file == absolute_file_path


def test_absolute_file(absolute_file_path: pathlib.Path):
file = File(file=absolute_file_path)
assert file.file == absolute_file_path


def test_relative_directory(absolute_directory_path: pathlib.Path, relative_directory_path: pathlib.Path):
directory = Directory(directory=relative_directory_path)
assert directory.directory == absolute_directory_path


def test_absolute_directory(absolute_directory_path: pathlib.Path):
directory = Directory(directory=absolute_directory_path)
assert directory.directory == absolute_directory_path


def test_relative_new_path(absolute_new_path: pathlib.Path, relative_new_path: pathlib.Path):
new_path = NewPath(new_path=relative_new_path)
assert new_path.new_path == absolute_new_path


def test_absolute_new_path(absolute_new_path: pathlib.Path):
new_path = NewPath(new_path=absolute_new_path)
assert new_path.new_path == absolute_new_path


@pytest.mark.parametrize(
('pass_fixture', 'expect_fixture'),
(
('relative_file_path', 'relative_file_path'),
('absolute_file_path', 'absolute_file_path'),
('relative_directory_path', 'relative_directory_path'),
('absolute_directory_path', 'absolute_directory_path'),
),
)
def test_existing_path(request: pytest.FixtureRequest, pass_fixture: str, expect_fixture: str):
existing = Existing(existing=request.getfixturevalue(pass_fixture))
assert existing.existing == request.getfixturevalue(expect_fixture)


@pytest.mark.parametrize(
('pass_fixture', 'expect_fixture'),
(
('relative_file_path', 'absolute_file_path'),
('absolute_file_path', 'absolute_file_path'),
('relative_directory_path', 'absolute_directory_path'),
('absolute_directory_path', 'absolute_directory_path'),
),
)
def test_resolved_existing_path(request: pytest.FixtureRequest, pass_fixture: str, expect_fixture: str):
resolved_existing = ResolvedExisting(resolved_existing=request.getfixturevalue(pass_fixture))
assert resolved_existing.resolved_existing == request.getfixturevalue(expect_fixture)