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

Add interconversion with json #16

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
1 change: 0 additions & 1 deletion .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: Unit test

on:
push:
pull_request:

jobs:
Expand Down
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python 3.10.13
134 changes: 129 additions & 5 deletions classopt/decorator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import json
import os
import typing
from argparse import ArgumentParser
from dataclasses import MISSING, Field, asdict, dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, overload
from json import JSONDecoder, JSONEncoder
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Tuple,
Type,
Union,
overload,
)

from classopt import config
from classopt.utils import (
Expand All @@ -28,6 +43,36 @@ def to_dict(self) -> dict:
def from_dict(cls, data: dict) -> _T:
...

def to_json(
self,
save_path: Union[str, os.PathLike, None],
skipkeys: bool = False,
ensure_ascii: bool = True,
check_circular: bool = True,
allow_nan: bool = True,
cls: Optional[Type[JSONEncoder]] = None,
indent: Union[int, str, None] = None,
separators: Optional[Tuple[str, str]] = None,
default: Optional[Callable[[Any], Any]] = None,
sort_keys: bool = False,
**kwargs,
) -> str:
...

@classmethod
def from_json(
cls,
json_content_or_path: Union[str, bytes, os.PathLike],
cls_: Optional[Type[JSONDecoder]] = None,
object_hook: Optional[Callable[[dict], Any]] = None,
parse_float: Optional[Callable[[str], Any]] = None,
parse_int: Optional[Callable[[str], Any]] = None,
parse_constant: Optional[Callable[[str], Any]] = None,
object_pairs_hook: Optional[Callable[[List[tuple]], Any]] = None,
**kwargs,
) -> _T:
...


@overload
def classopt(
Expand Down Expand Up @@ -57,7 +102,9 @@ def wrap(cls):
return wrap(cls)


def _process_class(cls, default_long: bool, default_short: bool, external_parser: ArgumentParser):
def _process_class(
cls, default_long: bool, default_short: bool, external_parser: ArgumentParser
):
@classmethod
def from_args(cls, args: Optional[List[str]] = None):
parser = external_parser if external_parser is not None else ArgumentParser()
Expand Down Expand Up @@ -102,7 +149,10 @@ def from_args(cls, args: Optional[List[str]] = None):
elif arg_field.default_factory != MISSING:
kwargs["default"] = arg_field.type(arg_field.default_factory())

if type(arg_field.type) in GENERIC_ALIASES and arg_field.type.__origin__ == list:
if (
type(arg_field.type) in GENERIC_ALIASES
and arg_field.type.__origin__ == list
):
kwargs["type"] = arg_field.type.__args__[0]
if not "nargs" in arg_field.metadata:
kwargs["nargs"] = "*"
Expand All @@ -125,7 +175,9 @@ def from_args(cls, args: Optional[List[str]] = None):

def to_dict(self):
def classopt_dict_factory(items: List[Tuple[str, Any]]) -> Dict[str, Any]:
converted_dict = {key: convert_non_primitives_to_string(value) for key, value in items}
converted_dict = {
key: convert_non_primitives_to_string(value) for key, value in items
}

return converted_dict

Expand All @@ -136,7 +188,9 @@ def classopt_dict_factory(items: List[Tuple[str, Any]]) -> Dict[str, Any]:
@classmethod
def from_dict(cls, data: dict):
reverted_data = {
key: revert_non_primitives_from_string(value, original_type=cls.__annotations__[key])
key: revert_non_primitives_from_string(
value, original_type=cls.__annotations__[key]
)
for key, value in data.items()
if key in cls.__annotations__
}
Expand All @@ -159,4 +213,74 @@ def from_dict(cls, data: dict):

setattr(cls, "from_dict", from_dict)

def to_json(
self,
save_path: Union[str, os.PathLike, None] = None,
skipkeys: bool = False,
ensure_ascii: bool = True,
check_circular: bool = True,
allow_nan: bool = True,
cls: Optional[Type[JSONEncoder]] = None,
indent: Union[int, str, None] = None,
separators: Optional[Tuple[str, str]] = None,
default: Optional[Callable[[Any], Any]] = None,
sort_keys: bool = False,
**kwargs,
) -> str:
json_content = json.dumps(
obj=self.to_dict(),
skipkeys=skipkeys,
ensure_ascii=ensure_ascii,
check_circular=check_circular,
allow_nan=allow_nan,
cls=cls,
indent=indent,
separators=separators,
default=default,
sort_keys=sort_keys,
**kwargs,
)

if not save_path is None:
Path(save_path).write_text(json_content)

return json_content

setattr(cls, "to_json", to_json)

@classmethod
def from_json(
cls,
json_content_or_path: Union[str, bytes, os.PathLike],
cls_: Optional[Type[JSONDecoder]] = None,
object_hook: Optional[Callable[[dict], Any]] = None,
parse_float: Optional[Callable[[str], Any]] = None,
parse_int: Optional[Callable[[str], Any]] = None,
parse_constant: Optional[Callable[[str], Any]] = None,
object_pairs_hook: Optional[Callable[[List[tuple]], Any]] = None,
**kwargs,
):
if not isinstance(json_content_or_path, bytes) and (
isinstance(json_content_or_path, os.PathLike)
or Path(json_content_or_path).exists()
):
json_content = Path(json_content_or_path).read_text()
else:
json_content = json_content_or_path

return cls.from_dict(
json.loads(
s=json_content,
cls=cls_,
object_hook=object_hook,
parse_float=parse_float,
parse_int=parse_int,
parse_constant=parse_constant,
object_pairs_hook=object_pairs_hook,
**kwargs,
)
)

setattr(cls, "from_json", from_json)

return dataclass(cls)
Loading