Skip to content

Commit

Permalink
Merge pull request #99 from jacebrowning/document-formats
Browse files Browse the repository at this point in the history
Document format options
  • Loading branch information
jacebrowning committed Jan 13, 2019
2 parents 3b05bb3 + 5f84154 commit c40a734
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 19 deletions.
12 changes: 6 additions & 6 deletions datafiles/formats.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from abc import ABCMeta, abstractmethod
from pathlib import Path
from typing import IO, Any, Dict, List, Text
from typing import IO, Any, Dict, List

import tomlkit
from ruamel import yaml
Expand All @@ -12,7 +12,7 @@ class Formatter(metaclass=ABCMeta):

@classmethod
@abstractmethod
def extensions(cls) -> List[Text]:
def extensions(cls) -> List[str]:
raise NotImplementedError

@classmethod
Expand All @@ -22,7 +22,7 @@ def deserialize(cls, file_object: IO[Any]) -> Dict:

@classmethod
@abstractmethod
def serialize(cls, data: Dict) -> Text:
def serialize(cls, data: Dict) -> str:
raise NotImplementedError


Expand Down Expand Up @@ -75,7 +75,7 @@ def serialize(cls, data):
return "" if text == "{}\n" else text


def deserialize(path: Path, extension: Text) -> Dict:
def deserialize(path: Path, extension: str) -> Dict:
for formatter in Formatter.__subclasses__():
if extension in formatter.extensions():
with path.open('r') as file_object:
Expand All @@ -84,9 +84,9 @@ def deserialize(path: Path, extension: Text) -> Dict:
raise ValueError(f'Unsupported file extension: {extension}')


def serialize(data: Dict, extension: Text) -> Text:
def serialize(data: Dict, extension: str = '.yml') -> str:
for formatter in Formatter.__subclasses__():
if extension in formatter.extensions():
return formatter.serialize(data)

raise ValueError(f'Unsupported file extension: {extension}')
raise ValueError(f'Unsupported file extension: {extension!r}')
5 changes: 3 additions & 2 deletions datafiles/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,10 @@ def text(self) -> str:
return self._get_text()

def _get_text(self, **kwargs) -> str:
extension = self.path.suffix if self.path else '.yml'
data = self._get_data(**kwargs)
return formats.serialize(data, extension)
if self.path and self.path.suffix:
return formats.serialize(data, self.path.suffix)
return formats.serialize(data)

def load(self, *, first_load=False) -> None:
if self._root:
Expand Down
10 changes: 10 additions & 0 deletions datafiles/tests/test_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ def with_json_format(expect, manager):
manager.attrs = {'foobar': MyField}
expect(manager.text) == '{\n "foobar": 42\n}'

def with_toml_format(expect, manager):
manager._pattern = '_.toml'
manager.attrs = {'foobar': MyField}
expect(manager.text) == "foobar = 42\n"

def with_no_format(expect, manager):
manager._pattern = '_'
manager.attrs = {'foobar': MyField}
expect(manager.text) == "foobar: 42\n"

def with_unknown_format(expect, manager):
manager._pattern = '_.xyz'
manager.attrs = {'foobar': MyField}
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ By default, all member variables will be included in the serialized file except
- Included in the directory pattern
- Set to default values

So the following instantiation:
So, the following instantiation:

```python
>>> sample = Sample(42, "Widget")
Expand Down
122 changes: 122 additions & 0 deletions docs/options/decorators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Synchronization

The simplest way to turn a dataclass into a datafile is to replace the `@dataclass` decorator with `@datafile`:

```python
# BEFORE

from dataclasses import dataclass

@dataclass
class Item:
name: str
count: int
available: bool
```

```python
# AFTER

from datafiles import datafile

@datafile('items/{self.name}.yml')
class Item:
name: str
count: int
available: bool
```

But you can also decorate an existing dataclass:

```python
# BEFORE

from dataclasses import dataclass

@dataclass
class Item:
name: str
count: int
available: bool
```

```python
# AFTER

from dataclasses import dataclass

from datafiles import datafile

@datafile('items/{self.name}.yml')
@dataclass
class Item:
name: str
count: int
available: bool
```

# Options

The following options can be passed to `@datafile()` decorator:

| Name | Type | Description | Default
| --- | --- | --- | --- |
| `attrs` | `dict` | Attributes to synchronize mapped to `datafile.converters` classes for serialization. | _Inferred from type annotations._ |
| `manual` | `bool` | Synchronize object and file changes manually. | `False` |
| `defaults` | `bool` | Include default values in files. | `False` |

For example:

```python
from datafiles import datafile

@datafile('items/{self.name}.yml', manual=True, defaults=True)
class Item:
name: str
count: int
available: bool
```

# Meta class

Alternatively, any of the above options can be configured through code by setting `datafile_<option>` in a `Meta` class:

```python
from datafiles import datafile, converters

@datafile('items/{self.name}.yml')
class Item:
name: str
count: int
available: bool

class Meta:
datafile_attrs = {'count': converters.Integer}
datafile_manual = True
datafile_defaults = True

```

# Base class

Finally, a datafile can explicitly extend `datafiles.Model` to set all options in the `Meta` class:

```python
from dataclasses import dataclass

from datafiles import Model, converters

@dataclass
class Item(Model):
name: str
count: int
available: bool

class Meta:
datafile_pattern = 'items/{self.name}.yml'
datafile_attrs = {'count': converters.Integer}
datafile_manual = True
datafile_defaults = True

```

79 changes: 79 additions & 0 deletions docs/options/formats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# YAML

By default, datafiles uses the [YAML language](https://yaml.org/) for serialization. Any of the following file extensions will use this format:

- `.yml`
- `.yaml`
- (no extension)

Sample output:

```yaml
my_dict:
value: 0
my_list:
- value: 1
- value: 2
my_bool: true
my_float: 1.23
my_int: 42
my_str: Hello, world!
```

Where possible, comments and whitespace are preserved in files as shown in this [Jupyter Notebook](https://github.com/jacebrowning/datafiles/blob/develop/notebooks/roundtrip_comments.ipynb).

# JSON

The [JSON language](https://www.json.org/) is also supported. Any of the following file extensions will use this format:

- `.json`

Sample output:

```json
{
"my_dict": {
"value": 0
},
"my_list": [
{
"value": 1
},
{
"value": 2
}
],
"my_bool": true,
"my_float": 1.23,
"my_int": 42,
"my_str": "Hello, world!"
}
```

Additional examples can be found in this [Jupyter Notebook](https://github.com/jacebrowning/datafiles/blob/develop/notebooks/format_options.ipynb).

# TOML

The [TOML language](https://github.com/toml-lang/toml) is also supported. Any of the following file extensions will use this format:

- `.toml`

Sample output:

```toml
my_bool = true
my_float = 1.23
my_int = 42
my_str = "Hello, world!"

[[my_list]]
value = 1

[[my_list]]
value = 2

[my_dict]
value = 0
```

Additional examples can be found in this [Jupyter Notebook](https://github.com/jacebrowning/datafiles/blob/develop/notebooks/format_options.ipynb).
21 changes: 21 additions & 0 deletions docs/types/containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,24 @@ from typing import List, Optional
| `foobar: List[int]` | `foobar = [1.23]` | `foobar:`<br>&nbsp;&nbsp;&nbsp;&nbsp;`- 1` |
| `foobar: List[int]` | `foobar = None` | `foobar:`<br>&nbsp;&nbsp;&nbsp;&nbsp;`-` |
| `foobar: Optional[List[int]]` | `foobar = None` | `foobar: ` |

More examples can be found in this [Jupyter Notebook](https://github.com/jacebrowning/datafiles/blob/develop/notebooks/patched_containers.ipynb).

# Dictionaries

```python
from typing import Dict, Optional
```

| Type Annotation | Python Value | YAML Data |
| --- | --- | --- |
| `foobar: Dict[str, int]` | `foobar = {}` | `foobar: {}` |
| `foobar: Dict[str, int]` | `foobar = {'a': 42}` | `foobar:`<br>&nbsp;&nbsp;&nbsp;&nbsp;`a: 42` |
| `foobar: Dict[str, int]` | `foobar = None` | `foobar: {}` |
| `foobar: Optional[Dict[str, int]]` | `foobar = None` | `foobar: ` |

**NOTE:** Schema enforcement is not available with the `Dict` annotation.

# Dataclasses

Dataclasses can also be nested as shown in this [Jupyter Notebook](https://github.com/jacebrowning/datafiles/blob/develop/notebooks/nested_dataclass.ipynb).
21 changes: 12 additions & 9 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ edit_uri: https://github.com/jacebrowning/datafiles/edit/develop/docs
theme: readthedocs

nav:
- Home: index.md
- Types:
- Builtins: types/builtins.md
- Extensions: types/extensions.md
- Containers: types/containers.md
- About:
- Release Notes: about/changelog.md
- Contributing: about/contributing.md
- License: about/license.md
- Home: index.md
- Options:
- Decorators: options/decorators.md
- Formats: options/formats.md
- Types:
- Builtins: types/builtins.md
- Extensions: types/extensions.md
- Containers: types/containers.md
- About:
- Release Notes: about/changelog.md
- Contributing: about/contributing.md
- License: about/license.md
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ documentation = "https://datafiles.readthedocs.io"
repository = "https://github.com/jacebrowning/datafiles"

keywords = [
"YAML",
"dataclasses",
"JSON",
"ORM",
"serialization",
"TOML",
"type-annotations",
"YAML",
]
classifiers = [
"Development Status :: 3 - Alpha",
Expand Down

0 comments on commit c40a734

Please sign in to comment.