Last active
February 7, 2021 23:06
-
-
Save mawillcockson/9cdce0e64e1437f8823613dda892b4fc to your computer and use it in GitHub Desktop.
[Python] practical subclassing of pathlib.Path
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| NOTE: This sort of "subclassing" only allows modifying how the object is instantiated. | |
| Other methods added to deriving classes aren't present on the instance. | |
| For example, the following fails with an AttributeError: | |
| class Example(StrictPath, Path): | |
| "example subclass with a method" | |
| def method(self) -> None: | |
| "example method" | |
| return None | |
| Example("./").method() | |
| a more complete example of subclassing pathlib.Path | |
| the current downside is repr() shows a PosixPath or WindowsPath | |
| mypy --strict is satisfied by the call to typing.cast(), and as long as the | |
| __new__() method returns an object of type pathlib.Path, everything else should | |
| work, since the created object will be the same as if it were created with | |
| pathlib.Path | |
| the last line is executed without raising an error, but mypy points out that | |
| the comparison is between non-overlapping types | |
| the correct way to compare instances of the two derived types would be: | |
| assert File(directory / "example.py") == example_file | |
| this solution is the same as: | |
| https://stackoverflow.com/a/29880095 | |
| """ | |
| from abc import ABC, abstractmethod | |
| from pathlib import Path | |
| from typing import Iterable, Type, Union, cast # pylint: disable=unused-import | |
| PathType = Union[str, Path] | |
| class StrictPath(ABC): | |
| "a Path that is guaranteed to exist" | |
| def __new__(cls, value: PathType) -> "StrictPath": | |
| # pylint: disable=arguments-differ | |
| "create a new instance of pathlib.Path with stricter requirements" | |
| return cast(StrictPath, cls._check(value)) | |
| @classmethod | |
| def __get_validators__( | |
| cls, | |
| ) -> "Iterable[Type[StrictPath]]": | |
| "enables pydantic to use this as a custom type" | |
| yield cls | |
| @staticmethod | |
| @abstractmethod | |
| def _check(value: PathType) -> Path: | |
| """ | |
| performs extra checks | |
| must return an object of type pathlib.Path | |
| """ | |
| raise NotImplementedError("the deriving class must override this") | |
| class Directory(StrictPath, Path): | |
| "an example derived class" | |
| @staticmethod | |
| def _check(value: PathType) -> Path: | |
| "description" | |
| path = Path(value) | |
| if not path.is_dir(): | |
| raise ValueError(f"'{path}' is not a directory") | |
| return path | |
| class File(StrictPath, Path): | |
| "another example derived class" | |
| @staticmethod | |
| def _check(value: PathType) -> Path: | |
| "description" | |
| path = Path(value) | |
| if not path.is_file(): | |
| raise ValueError(f"'{path}' is not a file") | |
| return path | |
| directory = Directory("./") | |
| example_file = File("./example.py") | |
| regular_path = Path("./") | |
| assert regular_path == directory | |
| assert regular_path == example_file.parent | |
| assert directory / "example.py" == example_file |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment