Skip to content

Instantly share code, notes, and snippets.

@mawillcockson
Last active February 7, 2021 23:06
Show Gist options
  • Select an option

  • Save mawillcockson/9cdce0e64e1437f8823613dda892b4fc to your computer and use it in GitHub Desktop.

Select an option

Save mawillcockson/9cdce0e64e1437f8823613dda892b4fc to your computer and use it in GitHub Desktop.
[Python] practical subclassing of pathlib.Path
"""
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