Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow passing different configuration files at various times. #46

Merged
merged 8 commits into from Apr 22, 2023
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -4,7 +4,7 @@

[![CI testing](https://github.com/karthikrangasai/anton/actions/workflows/ci-testing.yml/badge.svg)](https://github.com/karthikrangasai/anton/actions/workflows/ci-testing.yml)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
[![Documentation Status](https://readthedocs.org/projects/anton/badge/?version=latest)](https://anton.readthedocs.io/en/latest/?badge=latest)
<!-- [![Documentation Status](https://readthedocs.org/projects/anton/badge/?version=latest)](https://anton.readthedocs.io/en/latest/?badge=latest) -->

<!-- [![PyPI](https://img.shields.io/pypi/v/anton)](Add PyPI Link here) -->
<!-- [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/karthikrangasai/anton/blob/master/training_notebook.ipynb) -->
Expand Down Expand Up @@ -57,14 +57,14 @@ line_segment:
... first_point: Point
... second_point: Point
...
>>> @yaml_conf(conf_path="index.yaml")
>>> @yaml_conf()
... class ExampleClass:
... integer: int
... string: str
... point: Point
... line_segment: LineSegment
...
>>> example_obj = ExampleClass()
>>> example_obj = ExampleClass(conf_path="index.yaml")
>>> example_obj
ExampleClass(integer=23, string='Hello world', point=Point(x=0, y=0), line_segment=LineSegment(first_point=Point(x=10, y=10), second_point=Point(x=10, y=10)))
```
Expand Down
5 changes: 3 additions & 2 deletions docs/source/getting_started.md
Expand Up @@ -16,7 +16,6 @@ Consider a hypothetical YAML configuration file being used:

```yaml
# saved in the file: index.yaml
"""
point1:
x: 10
y: 10
Expand Down Expand Up @@ -120,11 +119,13 @@ With `anton`, all the boilerplate can be avoided by using the decorators `yaml_c
```python
import anton

@anton.yaml_conf(conf_path="index.yaml")
@anton.yaml_conf()
class CustomInput:
point1: Point
point2: Point
line_segment1: LineSegment
line_segment2: LineSegment

custom_input = CustomInput(conf_path="index.yaml")

```
2 changes: 2 additions & 0 deletions mypy.ini
@@ -0,0 +1,2 @@
[mypy]
plugins = src/anton/anton_plugin.py
51 changes: 51 additions & 0 deletions src/anton/anton_plugin.py
@@ -0,0 +1,51 @@
from typing import Callable, Optional, Type

from mypy.nodes import ARG_NAMED, Argument, Var
from mypy.plugin import ClassDefContext, Plugin
from mypy.plugins import dataclasses
from mypy.plugins.common import add_method
from mypy.types import NoneType, UnionType

ANTON_DECORATORS = ["yaml_conf", "json_conf"]


def anton_dataclass_class_maker_callback(ctx: ClassDefContext) -> bool:
"""Hooks into the class typechecking process to add support for dataclasses."""
transformer = dataclasses.DataclassTransformer(ctx)
transformed = transformer.transform()
if not transformed:
return False

conf_path_type = UnionType(
[
ctx.api.named_type("builtins.str"),
ctx.api.named_type("os.PathLike"),
ctx.api.named_type("pathlib.Path"),
]
)

init__conf_path__arg = Argument(
variable=Var("conf_path", conf_path_type), type_annotation=conf_path_type, initializer=None, kind=ARG_NAMED
)

args = [init__conf_path__arg]

add_method(ctx, "__init__", args=args, return_type=NoneType())

return True


class AntonPlugin(Plugin):
def get_class_decorator_hook(self, fullname: str) -> Optional[Callable[[ClassDefContext], None]]:
if any(decorator_name in fullname for decorator_name in ANTON_DECORATORS):
return anton_dataclass_class_maker_callback # type: ignore
return None


def plugin(version: str) -> Type[Plugin]:
"""
`version` is the mypy version string
We might want to use this to print a warning if the mypy version being used is
newer, or especially older, than we expect (or need).
"""
return AntonPlugin
25 changes: 7 additions & 18 deletions src/anton/json.py
Expand Up @@ -14,20 +14,18 @@ def _json_conf_wrapper(
cls,
/,
*,
conf_path: StrOrBytesPath,
init: bool = True,
repr: bool = True,
eq: bool = True,
order: bool = False,
unsafe_hash: bool = False,
frozen: bool = False,
):

dataclass_cls = dataclass(cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen) # type: ignore
dataclass_cls = dataclass(cls, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen) # type: ignore
actual_init = getattr(dataclass_cls, "__init__")
setattr(dataclass_cls, "init_setter", actual_init)

def modified_init(self) -> None:
def __init__(self, conf_path: StrOrBytesPath) -> None:
conf_as_dict = json_load(conf_path)
pos_args, kw_args = get_init_arguments(
conf_as_dict,
Expand All @@ -36,17 +34,13 @@ def modified_init(self) -> None:
)
getattr(self, "init_setter")(*pos_args, **kw_args)

setattr(dataclass_cls, "__init__", modified_init)
setattr(dataclass_cls, "__init__", __init__)

return dataclass_cls


def json_conf(
cls=None,
/,
*,
conf_path: StrOrBytesPath,
init: bool = True,
repr: bool = True,
eq: bool = True,
order: bool = False,
Expand Down Expand Up @@ -122,14 +116,14 @@ def json_conf(
... \""")
>>> temp_file.flush()
>>>
>>> @json_conf(conf_path=temp_file.name)
>>> @json_conf()
... class ExampleClass:
... integer: int
... string: str
... point: Point
... line_segment: LineSegment
...
>>> ExampleClass()
>>> ExampleClass(conf_path=temp_file.name)
ExampleClass(integer=23, string='Hello world', point=Point(x=0, y=0), line_segment=LineSegment(first_point=Point(x=10, y=10), second_point=Point(x=10, y=10)))

.. testcleanup::
Expand All @@ -139,11 +133,6 @@ def json_conf(
"""

def wrap(cls):
return _json_conf_wrapper(
cls, conf_path=conf_path, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen
)

if cls is None:
return wrap
return _json_conf_wrapper(cls, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)

return wrap(cls)
return wrap
Empty file added src/anton/py.typed
Empty file.
25 changes: 7 additions & 18 deletions src/anton/yaml.py
Expand Up @@ -14,20 +14,18 @@ def _yaml_conf_wrapper(
cls,
/,
*,
conf_path: StrOrBytesPath,
init: bool = True,
repr: bool = True,
eq: bool = True,
order: bool = False,
unsafe_hash: bool = False,
frozen: bool = False,
):

dataclass_cls = dataclass(cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen) # type: ignore
dataclass_cls = dataclass(cls, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen) # type: ignore
actual_init = getattr(dataclass_cls, "__init__")
setattr(dataclass_cls, "init_setter", actual_init)

def modified_init(self) -> None:
def __init__(self, conf_path: StrOrBytesPath) -> None:
conf_as_dict = yaml_load(conf_path)
pos_args, kw_args = get_init_arguments(
conf_as_dict,
Expand All @@ -36,17 +34,13 @@ def modified_init(self) -> None:
)
getattr(self, "init_setter")(*pos_args, **kw_args)

setattr(dataclass_cls, "__init__", modified_init)
setattr(dataclass_cls, "__init__", __init__)

return dataclass_cls


def yaml_conf(
cls=None,
/,
*,
conf_path: StrOrBytesPath,
init: bool = True,
repr: bool = True,
eq: bool = True,
order: bool = False,
Expand Down Expand Up @@ -117,14 +111,14 @@ def yaml_conf(
... \""")
>>> temp_file.flush()
>>>
>>> @yaml_conf(conf_path=temp_file.name)
>>> @yaml_conf()
... class ExampleClass:
... integer: int
... string: str
... point: Point
... line_segment: LineSegment
...
>>> ExampleClass()
>>> ExampleClass(conf_path=temp_file.name)
ExampleClass(integer=23, string='Hello world', point=Point(x=0, y=0), line_segment=LineSegment(first_point=Point(x=10, y=10), second_point=Point(x=10, y=10)))

.. testcleanup::
Expand All @@ -134,11 +128,6 @@ def yaml_conf(
"""

def wrap(cls):
return _yaml_conf_wrapper(
cls, conf_path=conf_path, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen
)

if cls is None:
return wrap
return _yaml_conf_wrapper(cls, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)

return wrap(cls)
return wrap
48 changes: 29 additions & 19 deletions tests/integration/test_simple.py
Expand Up @@ -5,6 +5,8 @@

from anton import json_conf, yaml_conf

FILENAME = "simple"

YAML_TEST_CASE = """string: value
integer: 69
floating: 3.14
Expand All @@ -19,32 +21,40 @@
}"""


@pytest.mark.parametrize(
("conf_path_fixture_name", "file_name", "test_case", "test_func"),
[
("base_dir_for_yaml_test_cases", "simple.yaml", YAML_TEST_CASE, yaml_conf),
("base_dir_for_json_test_cases", "simple.json", JSON_TEST_CASE, json_conf),
],
)
def test_simple_yaml(
request: pytest.FixtureRequest,
conf_path_fixture_name: str,
file_name: str,
test_case: str,
test_func: Callable[..., Any],
) -> None:
conf_path: Path = request.getfixturevalue(conf_path_fixture_name)

with open(conf_path / file_name, "w") as fp:
fp.write(test_case)
def test_simple_yaml(base_dir_for_yaml_test_cases: Path) -> None:
conf_path = base_dir_for_yaml_test_cases / f"{FILENAME}.yaml"

@yaml_conf()
class SimpleConfiguration:
string: str
integer: int
floating: float = 6.9
boolean: bool = True

with open(conf_path, "w") as fp:
fp.write(YAML_TEST_CASE)

simple_obj = SimpleConfiguration(conf_path=conf_path)
assert simple_obj.string == "value"
assert simple_obj.integer == 69
assert simple_obj.floating != 6.9 and simple_obj.floating == 3.14
assert not simple_obj.boolean


def test_simple_json(base_dir_for_json_test_cases: Path) -> None:
conf_path = base_dir_for_json_test_cases / f"{FILENAME}.json"

@json_conf()
class SimpleConfiguration:
string: str
integer: int
floating: float = 6.9
boolean: bool = True

simple_obj = test_func(SimpleConfiguration, conf_path=conf_path / file_name)()
with open(conf_path, "w") as fp:
fp.write(JSON_TEST_CASE)

simple_obj = SimpleConfiguration(conf_path=conf_path)
assert simple_obj.string == "value"
assert simple_obj.integer == 69
assert simple_obj.floating != 6.9 and simple_obj.floating == 3.14
Expand Down
45 changes: 26 additions & 19 deletions tests/integration/test_simple_any.py
Expand Up @@ -5,6 +5,8 @@

from anton import json_conf, yaml_conf

FILENAME = "simple_any"

YAML_TEST_CASE = """any1: 1
any2: \"2\"
any3: 3.14
Expand All @@ -19,30 +21,35 @@
}"""


@pytest.mark.parametrize(
("conf_path_fixture_name", "file_name", "test_case", "test_func"),
[
("base_dir_for_yaml_test_cases", "simple_any.yaml", YAML_TEST_CASE, yaml_conf),
("base_dir_for_json_test_cases", "simple_any.json", JSON_TEST_CASE, json_conf),
],
)
def test_simple_any(
request: pytest.FixtureRequest,
conf_path_fixture_name: str,
file_name: str,
test_case: str,
test_func: Callable[..., Any],
) -> None:
conf_path: Path = request.getfixturevalue(conf_path_fixture_name)

with open(conf_path / file_name, "w") as fp:
fp.write(test_case)
def test_yaml_simple_any(base_dir_for_yaml_test_cases: Path) -> None:
conf_path = base_dir_for_yaml_test_cases / f"{FILENAME}.yaml"

@yaml_conf()
class SimpleAnyConfiguration:
any1: Any
any2: Any
any3: Any
any4: Any = 1000

simple_any_obj = test_func(SimpleAnyConfiguration, conf_path=conf_path / file_name)()
with open(conf_path, "w") as fp:
fp.write(YAML_TEST_CASE)

simple_any_obj = SimpleAnyConfiguration(conf_path=conf_path)
assert simple_any_obj.any4 != 1000 and isinstance(simple_any_obj.any4, bool) and not simple_any_obj.any4


def test_json_simple_any(base_dir_for_json_test_cases: Path) -> None:
conf_path = base_dir_for_json_test_cases / f"{FILENAME}.json"

@json_conf()
class SimpleAnyConfiguration:
any1: Any
any2: Any
any3: Any
any4: Any = 1000

with open(conf_path, "w") as fp:
fp.write(JSON_TEST_CASE)

simple_any_obj = SimpleAnyConfiguration(conf_path=conf_path)
assert simple_any_obj.any4 != 1000 and isinstance(simple_any_obj.any4, bool) and not simple_any_obj.any4