Skip to content

Commit

Permalink
Merge pull request #174 from 0Hughman0/custom-validation-error
Browse files Browse the repository at this point in the history
created MetaValidationError
  • Loading branch information
0Hughman0 authored Sep 25, 2024
2 parents f849725 + 08a6ffc commit ae568b5
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 14 deletions.
44 changes: 37 additions & 7 deletions cassini/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
)
from typing_extensions import Tuple, cast, Self, Type

from pydantic import BaseModel, ConfigDict, Field, create_model, JsonValue
from pydantic import (
BaseModel,
ConfigDict,
Field,
create_model,
JsonValue,
ValidationError,
)
from pydantic.fields import FieldInfo


Expand All @@ -33,6 +40,15 @@ class MetaCache(BaseModel):
)


class MetaValidationError(ValueError):
def __init__(self, *, validation_error: ValidationError, file: Path):
message = f"Invalid data in file: {file}, due to the following validation error:\n\n{validation_error}"

super().__init__(message)
self.file = file
self.validation_error = validation_error


class Meta:
"""
Like a dictionary, except linked to a json file on disk. Caches the value of the json in itself.
Expand Down Expand Up @@ -80,10 +96,15 @@ def fetch(self) -> MetaCache:
overwritten, it'll just be loaded.
"""
if self.file.exists():
self._cache = self.model.model_validate_json(
self.file.read_text(), strict=False
)
try:
self._cache = self.model.model_validate_json(
self.file.read_text(), strict=False
)
except ValidationError as e:
raise MetaValidationError(validation_error=e, file=self.file)

self._cache_born = time.time()

return self._cache

def refresh(self) -> None:
Expand All @@ -100,7 +121,6 @@ def write(self) -> None:
jsons = self._cache.model_dump_json(
exclude_defaults=True, exclude={"__pydantic_extra__"}
)
# Danger moment - writing bad cache to file.
with self.file.open("w", encoding="utf-8") as f:
f.write(jsons)

Expand All @@ -126,15 +146,25 @@ def __setattr__(self, name: str, value: Any) -> None:
super().__setattr__(name, value)
else:
self.fetch()
setattr(self._cache, name, value)

try:
setattr(self._cache, name, value)
except ValidationError as e:
raise MetaValidationError(validation_error=e, file=self.file)

self.write()

def __delitem__(self, key: str) -> None:
self.fetch()
excluded = self._cache.model_dump(
exclude={"__pydantic_extra__", key}, exclude_defaults=True
)
self._cache = self.model.model_validate(excluded)
# it might not be possible for this to happen, because all fields have to have defaults.
try:
self._cache = self.model.model_validate(excluded)
except ValidationError as e:
raise MetaValidationError(validation_error=e, file=self.file)

self.write()

def __repr__(self) -> str:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "cassini"
version = "0.3.0a9"
version = "0.3.0a10"
description = "A tool to structure experimental work, data and analysis using Jupyter Lab."
authors = ["0Hughman0 <rammers2@hotmail.co.uk>"]
license = "GPL-3.0-only"
Expand Down
71 changes: 65 additions & 6 deletions tests/test_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest # type: ignore[import]
from cassini import HomeTierBase, NotebookTierBase
from cassini.meta import MetaAttr, MetaManager, Meta, MetaCache
from cassini.meta import MetaAttr, MetaManager, Meta, MetaCache, MetaValidationError
from cassini.testing_utils import get_Project, patch_project, patched_default_project

import pydantic
Expand Down Expand Up @@ -132,11 +132,11 @@ def test_jsonable(mk_meta):
meta = mk_meta

# attributes have to be serialisable in some way!
with pytest.raises(pydantic.ValidationError):
with pytest.raises(MetaValidationError):
meta['object'] = object

# values must be json values. If you want type coersion, define a meta attr!
with pytest.raises(pydantic.ValidationError):
with pytest.raises(MetaValidationError):
meta['pathlike'] = pathlib.Path().absolute()

# type changes are allowed without meta definition.
Expand All @@ -158,7 +158,7 @@ class Model(MetaCache):

assert meta['strict_str'] == 'default'

with pytest.raises(pydantic.ValidationError):
with pytest.raises(MetaValidationError):
meta['strict_str'] = 5

meta['strict_str'] = 'new val'
Expand Down Expand Up @@ -205,7 +205,7 @@ def test_started_requires_timezone(patched_default_project):

WP1, = create_tiers(['WP1'])

with pytest.raises(pydantic.ValidationError):
with pytest.raises(MetaValidationError):
WP1.started = datetime.datetime.now()


Expand Down Expand Up @@ -269,7 +269,7 @@ class Fourth(NotebookTierBase):

assert obj.description == 'new description'

with pytest.raises(pydantic.ValidationError):
with pytest.raises(MetaValidationError):
obj.description = 124

obj = project['Second1Third1']
Expand All @@ -286,6 +286,65 @@ class Fourth(NotebookTierBase):
assert 'started' in obj.meta.model.model_fields


def test_meta_validation_error_has_path(tmp_path):
class Model(MetaCache):
strict_str: str = 'default'

meta = Meta(tmp_path / 'test.json', Model)

with pytest.raises(MetaValidationError, match=str('test.json')):
meta.strict_str = 10


def test_bad_setattr_raises_meta_error(tmp_path):
class Model(MetaCache):
strict_str: str = 'default'

meta = Meta(tmp_path / 'test.json', Model)

with pytest.raises(MetaValidationError, match=str('test.json')):
meta.strict_str = 10


def test_bad_setitem_raises_meta_error(tmp_path):
class Model(MetaCache):
strict_str: str = 'default'

meta = Meta(tmp_path / 'test.json', Model)

with pytest.raises(MetaValidationError, match=str('test.json')):
meta['strict_str'] = 10


def test_bad_fetch_raises_meta_error(tmp_path):
class Model(MetaCache):
strict_str: str = 'default'

file = tmp_path / 'test.json'

file.write_text('{"strict_str": 10}')

meta = Meta(tmp_path / 'test.json', Model)

with pytest.raises(MetaValidationError, match=str('test.json')):
meta.fetch()


def test_bad_del_raises_meta_error(tmp_path):
class Model(MetaCache):
strict_str: str

file = tmp_path / 'test.json'

file.write_text('{"strict_str": "value"}')

meta = Meta(tmp_path / 'test.json')
meta.model = Model # currently, we cannot set model=Model, because all fields must have defaults.

with pytest.raises(MetaValidationError, match=str('test.json')):
del meta["strict_str"]


def test_cas_field_meta():
m = MetaAttr(None, str, str, cas_field='core')
assert m.cas_field == 'core'
Expand Down

0 comments on commit ae568b5

Please sign in to comment.