Skip to content

Commit 5706207

Browse files
authored
Merge pull request #10 from astropenguin/#8-include-toml
Add feature to include a custom DataArray definition written in a file
2 parents ef8f8e0 + 67c2cee commit 5706207

9 files changed

Lines changed: 277 additions & 91 deletions

File tree

.github/workflows/publish.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ jobs:
3333
run: |
3434
pip install poetry
3535
poetry install
36-
poetry run sphinx-apidoc -f -o docs/_apidoc xarray_custom
37-
poetry run sphinx-build docs docs/_build
36+
poetry run etc/builddocs
3837
- name: Deploy docs
3938
uses: peaceiris/actions-gh-pages@v3
4039
with:

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
author = "Akio Taniguchi"
2424

2525
# The full version, including alpha/beta/rc tags
26-
release = "0.3.0"
26+
release = "0.4.0"
2727

2828

2929
# -- General configuration ---------------------------------------------------

poetry.lock

Lines changed: 108 additions & 77 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "xarray-custom"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
description = "Data classes for custom xarray constructors"
55
authors = ["Akio Taniguchi <taniguchi@a.phys.nagoya-u.ac.jp>"]
66
license = "MIT"
@@ -12,6 +12,8 @@ documentation = "https://astropenguin.github.io/xarray-custom"
1212
python = "^3.6"
1313
numpy = "^1.18"
1414
xarray = "^0.15"
15+
toml = "^0.10"
16+
pyyaml = "^5.3"
1517

1618
[tool.poetry.dev-dependencies]
1719
black = "^19.10b0"

tests/test_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
# test functions
66
def test_version():
7-
assert __version__ == "0.3.0"
7+
assert __version__ == "0.4.0"

xarray_custom/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# flake8: noqa
2-
__version__ = "0.3.0"
2+
__version__ = "0.4.0"
33
__author__ = "Akio Taniguchi"
44

55

66
# aliases
77
from .abc import *
88
from .dataclasses import *
9+
from .utils import *

xarray_custom/dataclasses.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
1212
@dataarrayclass(accessor='img')
1313
class Image:
14+
\"\"\"DataArray class to represent images.\"\"\"
15+
1416
dims = 'x', 'y'
1517
dtype = float
1618
x: ctype('x', int) = 0
@@ -93,7 +95,7 @@ class WeightedImage(Image):
9395

9496

9597
# main functions
96-
def ctype(dims: Dims, dtype: Optional[Dtype] = None, desc: str = "") -> type:
98+
def ctype(dims: Dims, dtype: Optional[Dtype] = None, desc: str = "", **_) -> type:
9799
"""Create a DataArray class for the definition of a coordinate.
98100
99101
Args:
@@ -123,10 +125,10 @@ def dataarrayclass(
123125
Keyword Args:
124126
accessor: Name of an accessor for the custom DataArray.
125127
User-defined methods in the class are added to the accessor.
126-
docstring_style: Style of docstrings of special methods.
127-
``'google'`` is only available (``'numpy'`` will be added).
128128
strict_dims: Whether ``dims`` is consistent with superclasses.
129129
strict_dtype: Whether ``dtype`` is consistent with superclasses.
130+
docstring_style: Style of docstrings of special methods.
131+
``'google'`` is only available (``'numpy'`` will be added).
130132
131133
Returns:
132134
decorator: Returned if any keyword-only arguments are given.
@@ -137,6 +139,8 @@ def dataarrayclass(
137139
138140
@dataarrayclass(accessor='img')
139141
class Image:
142+
\"\"\"DataArray class to represent images.\"\"\"
143+
140144
dims = 'x', 'y'
141145
dtype = float
142146
x: ctype('x', int) = 0

xarray_custom/ensuring.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class attributes in the class and its superclasses.
6767
6868
"""
6969
if not hasattr(cls, CTYPES):
70-
cls.ctypes = {}
70+
setattr(cls, CTYPES, {})
7171

7272
for sub in reversed(cls.mro()):
7373
if not hasattr(sub, "__annotations__"):
@@ -107,9 +107,9 @@ def ensure_dims(cls: type, strict: bool = True) -> type:
107107
continue
108108

109109
if strict and set(cls.dims) != set(sub.dims):
110-
raise ValueError("Dims must be a superset of any of superclasses.")
111-
elif set(cls.dims) < set(sub.dims):
112110
raise ValueError("Dims must be equal to any of superclasses.")
111+
elif set(cls.dims) < set(sub.dims):
112+
raise ValueError("Dims must be a superset of any of superclasses.")
113113

114114
return cls
115115

@@ -129,7 +129,10 @@ def ensure_desc(cls: type) -> type:
129129
130130
"""
131131
if not hasattr(cls, DESC):
132-
cls.desc = cls.__doc__ or "No description."
132+
setattr(cls, DESC, cls.__doc__)
133+
134+
if cls.desc is None or not cls.desc:
135+
cls.desc = "No description."
133136

134137
cls.desc = re.sub(r"\n\s*", " ", cls.desc)
135138
return cls
@@ -153,7 +156,7 @@ def ensure_dtype(cls: type, strict: bool = True) -> type:
153156
154157
"""
155158
if not hasattr(cls, DTYPE):
156-
cls.dtype = None
159+
setattr(cls, DTYPE, None)
157160

158161
for sub in cls.mro():
159162
if not hasattr(sub, DTYPE):

xarray_custom/utils.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Module for utilities which help to create custom DataArray classes.
2+
3+
Currently this module provides only ``include`` class decorator
4+
which can include a custom DataArray definition written in a file.
5+
6+
"""
7+
__all__ = ["include"]
8+
9+
10+
# standard library
11+
import json
12+
import re
13+
from pathlib import Path
14+
from typing import Any, Callable, Dict, Union
15+
16+
17+
# dependencies
18+
import toml
19+
import yaml
20+
from .dataclasses import ctype
21+
from .ensuring import ensure_ctypes
22+
23+
24+
# constants
25+
ATTRS = "desc", "dims", "dtype"
26+
COORDS = "coords"
27+
DEFAULT = "default"
28+
JSON_RE = r"\.json$"
29+
TOML_RE = r"\.toml$"
30+
YAML_RE = r"\.ya?ml$"
31+
32+
33+
# main functions
34+
def include(path: Union[Path, str]) -> Callable:
35+
"""Class decorator to include a custom DataArray definition in a file.
36+
37+
File format of either JSON, TOML, or YAML is accepted.
38+
The following ``key=value`` pairs can be included if available.
39+
40+
- ``dims=<array of string>``: Dimensions of the DataArray.
41+
- ``dtype=<string>``: Datatype of the DataArray.
42+
- ``desc=<string>``: Short description of the DataArray.
43+
- ``coords=<map of coord>``: Definition of coordinates (coords).
44+
Each coord is a map which can have the following ``key=value`` pairs.
45+
46+
- ``dims=<array of string>``: Dimensions of a coordinate.
47+
- ``dtype=<string>``: Datatype of a coordinate.
48+
- ``desc=<string>``: Short description of a coordinate.
49+
- ``default=<any>``: Default value of a coordinate.
50+
51+
Args:
52+
path: Path or filename of the file.
53+
54+
Returns:
55+
decorator: Decorator to include the definition.
56+
57+
Examples:
58+
If a definition is written in ``dataarray.toml``::
59+
60+
# dataarray.toml
61+
62+
dims = [ "x", "y" ]
63+
dtype = "float"
64+
desc = "DataArray class to represent images."
65+
66+
[coords.x]
67+
dims = "x"
68+
dtype = "int"
69+
default = 0
70+
71+
[coords.y]
72+
dims = "y"
73+
dtype = "int"
74+
default = 0
75+
76+
then the following two class definitions are equivalent::
77+
78+
@dataarrayclass(accessor='img')
79+
@include('dataarray.toml')
80+
class Image:
81+
pass
82+
83+
::
84+
85+
@dataarrayclass(accessor='img')
86+
class Image:
87+
\"\"\"DataArray class to represent images.\"\"\"
88+
89+
dims = 'x', 'y'
90+
dtype = float
91+
x: ctype('x', int) = 0
92+
y: ctype('y', int) = 0
93+
94+
"""
95+
path = Path(path).expanduser()
96+
loader = choose_loader_from(path)
97+
98+
def decorator(cls: type) -> type:
99+
cls = ensure_ctypes(cls)
100+
101+
config = loader(path)
102+
coords = config.get(COORDS, {})
103+
104+
for name in ATTRS:
105+
if name in config:
106+
setattr(cls, name, config[name])
107+
108+
for name, coord in coords.items():
109+
cls.ctypes[name] = ctype(**coord)
110+
111+
if DEFAULT in coord:
112+
setattr(cls, name, coord[DEFAULT])
113+
114+
return cls
115+
116+
return decorator
117+
118+
119+
# helper functions
120+
def choose_loader_from(path: Path) -> Callable:
121+
"""Choose file loader based on a filename."""
122+
if re.search(JSON_RE, path.name):
123+
return load_json
124+
elif re.search(TOML_RE, path.name):
125+
return load_toml
126+
elif re.search(YAML_RE, path.name):
127+
return load_yaml
128+
else:
129+
raise ValueError("Invalid file format.")
130+
131+
132+
def load_json(path: Path) -> Dict[str, Any]:
133+
"""Load a JSON file to create a dictionary."""
134+
with path.open() as f:
135+
return json.load(f)
136+
137+
138+
def load_toml(path: Path) -> Dict[str, Any]:
139+
"""Load a TOML file to create a dictionary."""
140+
return toml.load(path)
141+
142+
143+
def load_yaml(path: Path) -> Dict[str, Any]:
144+
"""Load a YAML file to create a dictionary."""
145+
with path.open() as f:
146+
return yaml.load(f, Loader=yaml.SafeLoader)

0 commit comments

Comments
 (0)