Add PathField class for storing pathlib objects

Add tests for pathfield class
Add database fixture for generating test databases
This commit is contained in:
Ethan Paul 2021-11-24 17:13:41 -05:00
parent 5499d282eb
commit adab90736c
No known key found for this signature in database
GPG Key ID: D0E2CBF1245E92BF
3 changed files with 160 additions and 0 deletions

View File

@ -1,6 +1,74 @@
from pathlib import Path
from typing import Optional
import peewee
__title__ = "peewee-plus"
__version__ = "0.1.0"
__license__ = "MIT"
__summary__ = "Various extensions, helpers, and utilities for Peewee"
__url__ = "https://github.com/enpaul/peewee-plus/"
__authors__ = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"]
__all__ = ["PathField"]
class PathField(peewee.CharField):
"""Field class for storing file paths
This field can be used to simply store pathlib paths in the database without needing to
cast to ``str`` on write and ``Path`` on read.
It can also serve to save paths relative to a root path defined at runtime. This can be
useful when an application stores files under a directory defined in the app configuration,
such as in an environment variable or a config file.
For example, if a model is defined like below to load a path from the ``MYAPP_DATA_DIR``
environment variable:
.. code-block:: python
class MyModel(peewee.Model):
some_path = peewee_plus.PathField(relative_to=Path(os.environ["MYAPP_DATA_DIR"]))
p1 = MyModel(some_path=Path(os.environ["MYAPP_DATA_DIR"]) / "foo.json").save()
p2 = MyModel(some_path=Path("bar.json")).save()
Then the data directory can be changed without updating the database, and the code can
still rely on the database always returning absolute paths:
::
>>> os.environ["MYAPP_DATA_DIR"] = "/etc/myapp"
>>> [item.some_path for item in MyModel.select()]
[PosixPath('/etc/myapp/foo.json'), PosixPath('/etc/myapp/bar.json')]
>>>
>>> os.environ["MYAPP_DATA_DIR"] = "/opt/myapp/data"
>>> [item.some_path for item in MyModel.select()]
[PosixPath('/opt/myapp/data/foo.json'), PosixPath('/opt/myapp/data/bar.json')]
>>>
:param relative_to: Optional root path that paths should be stored relative to. If specified
then values being set will be converted to relative paths under this path,
and values being read will always be absolute paths under this path.
"""
def __init__(self, *args, relative_to: Optional[Path] = None, **kwargs):
super().__init__(*args, **kwargs)
self.relative_to = relative_to
def db_value(self, value: Path) -> str:
"""Serialize a :class:`pathlib.Path` to a database string"""
if value.is_absolute() and self.relative_to:
value = value.relative_to(self.relative_to)
return super().db_value(value)
def python_value(self, value: str) -> Path:
"""Serialize a database string to a :class:`pathlib.path` object"""
return (
self.relative_to / Path(super().python_value(value))
if self.relative_to
else Path(super().python_value(value))
)

22
tests/fixtures.py Normal file
View File

@ -0,0 +1,22 @@
import uuid
import peewee
import pytest
@pytest.fixture(scope="function")
def fakedb(tmp_path):
"""Create a temporary pho-database (fakedb) for testing fields"""
sqlite = peewee.SqliteDatabase(
tmp_path / f"{uuid.uuid4()}.db",
pragmas={
"journal_mode": "wal",
"cache_size": -1 * 64000,
"foreign_keys": 1,
"ignore_check_constraints": 0,
"synchronous": 0,
},
)
yield sqlite

70
tests/test_pathfield.py Normal file
View File

@ -0,0 +1,70 @@
# pylint: disable=redefined-outer-name
# pylint: disable=missing-class-docstring
# pylint: disable=too-few-public-methods
# pylint: disable=unused-import
from pathlib import Path
import peewee
import peewee_plus
from .fixtures import fakedb
def test_conversion(fakedb):
"""Test basic usage of PathField for roundtrip compatibility"""
class TestModel(peewee.Model):
class Meta:
database = fakedb
name = peewee.CharField()
some_path = peewee_plus.PathField()
fakedb.create_tables([TestModel])
path1 = Path("foo", "bar", "baz")
model1 = TestModel(name="one", some_path=path1)
model1.save()
model1 = TestModel.get(TestModel.name == "one")
assert model1.some_path == path1
assert not model1.some_path.is_absolute()
path2 = Path("/etc", "fizz", "buzz")
model2 = TestModel(name="two", some_path=path2)
model2.save()
model2 = TestModel.get(TestModel.name == "two")
assert model2.some_path == path2
assert model2.some_path.is_absolute()
def test_relative_to(fakedb):
"""Test usage of the ``relative_to`` parameter"""
base_path = Path("/etc", "foobar")
class TestModel(peewee.Model):
class Meta:
database = fakedb
name = peewee.CharField()
some_path = peewee_plus.PathField(relative_to=base_path)
fakedb.create_tables([TestModel])
path1 = Path("foo", "bar", "baz")
model1 = TestModel(name="one", some_path=path1)
model1.save()
model1 = TestModel.get(TestModel.name == "one")
assert model1.some_path.is_absolute()
assert model1.some_path == base_path / path1
path2 = Path("fizz", "buzz")
model2 = TestModel(name="two", some_path=base_path / path2)
model2.save()
model2 = TestModel.get(TestModel.name == "two")
assert model2.some_path.is_absolute()
assert model2.some_path == base_path / path2