Skip to content

Commit

Permalink
Merge pull request #125 from jacebrowning/release/v0.4
Browse files Browse the repository at this point in the history
Release v0.4
  • Loading branch information
jacebrowning committed Jun 29, 2019
2 parents 4e4cb0b + 244aafc commit c8ceacd
Show file tree
Hide file tree
Showing 34 changed files with 426 additions and 276 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ omit =
.venv/*
*/tests/*
*/__main__.py
datafiles/plugins.py

[report]

Expand Down
16 changes: 15 additions & 1 deletion .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,22 @@ ignore_missing_imports = true
no_implicit_optional = true
check_untyped_defs = true

plugins = datafiles.plugins:mypy

cache_dir = .cache/mypy/

[mypy-tests.*]
[mypy-tests.test_automatic_attributes]

ignore_errors = True

[mypy-tests.test_custom_converters]

ignore_errors = True

[mypy-tests.test_saving]

ignore_errors = True

[mypy-tests.test_loading]

ignore_errors = True
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 0.4 (2019-06-29)

- Fixed ORM methods for datafiles with relative path patterns.
- Added plugin for `mypy` support.
- Updated YAML format to indent lists.

# 0.3 (2019-06-09)

- Added ORM method: `all()`
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ check: install format ## Run formaters, linters, and static analysis
ifdef CI
git diff --exit-code
endif
poetry run pylint $(PACKAGES) --rcfile=.pylint.ini
poetry run mypy $(PACKAGES) --config-file=.mypy.ini
poetry run pydocstyle $(PACKAGES) $(CONFIG)
poetry run pylint $(PACKAGES) --rcfile=.pylint.ini
poetry run pydocstyle $(PACKAGES)

# TESTS #######################################################################

Expand Down
9 changes: 6 additions & 3 deletions bin/checksum
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ def run(paths):
hash_md5 = hashlib.md5()

for path in paths:
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
hash_md5.update(chunk)
try:
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
hash_md5.update(chunk)
except IOError:
hash_md5.update(path.encode())

print(hash_md5.hexdigest())

Expand Down
8 changes: 4 additions & 4 deletions datafiles/converters/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from ._bases import Converter


class Boolean(Converter):
class Boolean(Converter, int):
"""Converter for `bool` literals."""

TYPE = bool
Expand All @@ -21,14 +21,14 @@ def to_python_value(cls, deserialized_data, *, target_object=None):
return value


class Float(Converter):
class Float(Converter, float):
"""Converter for `float` literals."""

TYPE = float
DEFAULT = 0.0


class Integer(Converter):
class Integer(Converter, int):
"""Converter for `int` literals."""

TYPE = int
Expand All @@ -50,7 +50,7 @@ def to_preserialization_data(cls, python_value, *, default_to_skip=None):
return data


class String(Converter):
class String(Converter, str):
"""Converter for `str` literals."""

TYPE = str
Expand Down
7 changes: 6 additions & 1 deletion datafiles/formats.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from abc import ABCMeta, abstractmethod
from io import StringIO
from pathlib import Path
from typing import IO, Any, Dict, List

Expand Down Expand Up @@ -71,7 +72,11 @@ def deserialize(cls, file_object):

@classmethod
def serialize(cls, data):
text = yaml.round_trip_dump(data)
f = StringIO()
y = yaml.YAML()
y.indent(mapping=2, sequence=4, offset=2)
y.dump(data, f)
text = f.getvalue().strip() + '\n'
return "" if text == "{}\n" else text


Expand Down
5 changes: 4 additions & 1 deletion datafiles/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from __future__ import annotations

import dataclasses
import inspect
from glob import iglob
from pathlib import Path
from typing import TYPE_CHECKING, Iterator, Optional
from typing_extensions import Protocol

Expand All @@ -24,7 +26,8 @@ def __init__(self, cls):
self.model = cls

def all(self) -> Iterator[HasDatafile]:
pattern = self.model.Meta.datafile_pattern
root = Path(inspect.getfile(self.model)).parent
pattern = str(root / self.model.Meta.datafile_pattern)
splatted = pattern.format(self=Splats())
log.info(f'Finding files matching pattern: {splatted}')
for filename in iglob(splatted):
Expand Down
18 changes: 4 additions & 14 deletions datafiles/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from . import config, formats, hooks
from .converters import Converter, List, map_type
from .utils import prettify, recursive_update
from .utils import display, recursive_update, write


Trilean = Optional[bool]
Expand Down Expand Up @@ -173,7 +173,7 @@ def _get_text(self, **kwargs) -> str:

@text.setter # type: ignore
def text(self, value: str):
self._write(value.strip() + '\n')
write(self.path, value.strip() + '\n')

def load(self, *, _log=True, _first=False) -> None:
if self._root:
Expand All @@ -188,10 +188,7 @@ def load(self, *, _log=True, _first=False) -> None:

data = formats.deserialize(self.path, self.path.suffix)
self._last_data = data

message = f'Data from file: {self.path}'
log.debug(message)
log.debug('=' * len(message) + '\n\n' + prettify(data) + '\n')
display(self.path, data)

with hooks.disabled():

Expand Down Expand Up @@ -328,17 +325,10 @@ def save(self, *, include_default_values: Trilean = None, _log=True) -> None:
with hooks.disabled():
text = self._get_text(include_default_values=include_default_values)

self._write(text)
write(self.path, text)

self.modified = False

def _write(self, text: str):
message = f'Writing file: {self.path}'
log.debug(message)
log.debug('=' * len(message) + '\n\n' + (text or '<nothing>\n'))
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(text)


def create_mapper(obj, root=None) -> Mapper:
try:
Expand Down
38 changes: 38 additions & 0 deletions datafiles/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# pylint: disable=no-name-in-module,no-self-use,unused-argument

from typing import Callable, Optional

from mypy.nodes import MDEF, SymbolTableNode, Var
from mypy.plugin import ClassDefContext, Plugin
from mypy.plugins.dataclasses import DataclassTransformer
from mypy.types import AnyType, TypeOfAny


class DatafilesPlugin(Plugin):
def get_class_decorator_hook(
self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
if fullname.endswith('.datafile'):
return datafile_class_maker_callback
return None


def datafile_class_maker_callback(ctx: ClassDefContext) -> None:
# Inherit all type definitions from dataclasses
DataclassTransformer(ctx).transform()

# Define 'objects' as a class propery
var = Var('objects', AnyType(TypeOfAny.unannotated))
var.info = ctx.cls.info
var.is_property = True
ctx.cls.info.names[var.name()] = SymbolTableNode(MDEF, var)

# Define 'datafile' as an instance property
var = Var('datafile', AnyType(TypeOfAny.unannotated))
var.info = ctx.cls.info
var.is_property = True
ctx.cls.info.names[var.name()] = SymbolTableNode(MDEF, var)


def mypy(version: str):
return DatafilesPlugin
73 changes: 70 additions & 3 deletions datafiles/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@

from contextlib import suppress
from functools import lru_cache
from pathlib import Path
from pprint import pformat
from typing import Any, Dict
from shutil import get_terminal_size
from typing import Any, Dict, Union

import log


cached = lru_cache()


def prettify(data: Dict) -> str:
return pformat(dictify(data))
def prettify(value: Any) -> str:
"""Ensure value is a dictionary pretty-format it."""
return pformat(dictify(value))


def dictify(value: Any) -> Dict:
"""Ensure value is a dictionary."""
with suppress(AttributeError):
return {k: dictify(v) for k, v in value.items()}

Expand Down Expand Up @@ -44,3 +50,64 @@ def recursive_update(old: Dict, new: Dict):
old[key] = value

return old


def dedent(text: str) -> str:
"""Remove indentation from a multiline string."""
text = text.strip('\n')
indent = text.split('\n')[0].count(' ')
return text.replace(' ' * indent, '')


def write(filename_or_path: Union[str, Path], text: str) -> None:
"""Write text to a given file with logging."""
if isinstance(filename_or_path, Path):
path = filename_or_path
else:
path = Path(filename_or_path).resolve()
text = dedent(text)

message = f'Writing file: {path}'
log.debug(message)
line = '=' * len(message)
if text:
content = text.replace(' \n', '␠\n')
else:
content = '∅\n'
log.debug(line + '\n' + content + line)

path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)


def read(filename: str) -> str:
"""Read text from a file with logging."""
path = Path(filename).resolve()
message = f'Reading file: {path}'
log.info(message)
line = '=' * len(message)
text = path.read_text()
if text:
content = text.replace(' \n', '␠\n')
else:
content = '∅\n'
log.debug(line + '\n' + content + line)
return text


def display(path: Path, data: Dict) -> None:
"""Display data read from a file."""
message = f'Data from file: {path}'
log.debug(message)
line = '=' * len(message)
log.debug(line + '\n' + prettify(data) + '\n' + line)


def logbreak(message: str = "") -> None:
"""Insert a noticeable logging record for debugging."""
width = get_terminal_size().columns - 31
if message:
line = '-' * (width - len(message) - 1) + ' ' + message
else:
line = '-' * width
log.info(line)
10 changes: 6 additions & 4 deletions docs/api/manager.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<h1>Manager API</h1>

Object-relational mapping (ORM) methods are available on all model classes via the `objects` proxy. The following sections assume an empty filesystem and the following sample datafile definition:

```python
Expand All @@ -12,7 +14,7 @@ class MyModel:

Many of the following examples are also shown in this [Jupyter Notebook](https://github.com/jacebrowning/datafiles/blob/develop/notebooks/manager_api.ipynb).

# `all`
# `all()`

Iterate over all objects matching the pattern:

Expand All @@ -35,7 +37,7 @@ MyModel(my_key='foo' my_value=0)
MyModel(my_key='bar', my_value=42)
```

# `get_or_none`
# `get_or_none()`

Instantiate an object from an existing file or return `None` if no matching file exists:

Expand All @@ -53,7 +55,7 @@ None
MyModel(my_key='foobar', my_value=42)
```

# `get_or_create`
# `get_or_create()`

Instantiate an object from an existing file or create one if no matching file exists:

Expand All @@ -71,7 +73,7 @@ MyModel(my_key='foo', my_value=42)
MyModel(my_key='bar', my_value=0)
```

# `filter`
# `filter()`

Iterate all objects matching the pattern with additional required attribute values:

Expand Down
4 changes: 3 additions & 1 deletion docs/api/mapper.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<h1>Mapper API</h1>

Instances of datafile models have an additional `datafile` proxy to manually interact with the filesystem. The following sections assume an empty filesystem and use the following sample datafile definition:

```python
Expand Down Expand Up @@ -36,7 +38,7 @@ False

_By default, the file is created automatically. Set `manual=True` to disable this behavior._

# `save` / `load`
# `save()` / `load()`

Manually synchronize an object with the filesystem:

Expand Down
Loading

0 comments on commit c8ceacd

Please sign in to comment.