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

feat: Refactor and export format_serializers module #94

Merged
merged 2 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 0 additions & 74 deletions python/ngen_init_config/src/ngen/init_config/_serlializers.py

This file was deleted.

2 changes: 1 addition & 1 deletion python/ngen_init_config/src/ngen/init_config/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.4"
__version__ = "0.0.5"
122 changes: 122 additions & 0 deletions python/ngen_init_config/src/ngen/init_config/format_serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import configparser
from collections import OrderedDict
from io import StringIO
from typing import Any, Dict, List, Union

from typing_extensions import TypeAlias

from ._constants import NO_SECTIONS
from .utils import try_import


def to_namelist_str(d: Dict[str, Any]) -> str:
"""Serialize a dictionary as an namelist formatted string."""
f90nml = try_import("f90nml", extras_require_name="namelist")

# NOTE: Cast to OrderedDict to guarantee group-name ordering
# dicts are ordered in python >= 3.7, however f90nml does an isinstance OrderedDict check
namelist = f90nml.Namelist(OrderedDict(d))
# drop eol chars
return str(namelist).rstrip()


_NON_NONE_JSON_PRIMITIVES: TypeAlias = Union[str, float, int, bool]
NON_NONE_JSON_DICT: TypeAlias = Dict[
str,
Union[
_NON_NONE_JSON_PRIMITIVES,
"NON_NONE_JSON_DICT",
List[_NON_NONE_JSON_PRIMITIVES],
List["NON_NONE_JSON_DICT"],
],
]
"""Dictionary of non-None json serializable types."""


def to_ini_str(
d: Dict[str, NON_NONE_JSON_DICT],
*,
space_around_delimiters: bool = True,
preserve_key_case: bool = False,
) -> str:
"""
Serialize a dictionary as an ini formatted string.
Note: this function does not support serializing `None` values.
`None` values must either be discarded or transformed into an appropriate type before using this
function.

Parameters
----------
d : Dict[str, NON_NONE_JSON_DICT]
dictionary of sections and their contents to serialize
space_around_delimiters : bool, optional
include spaces around `=` signs, by default True
preserve_key_case : bool, optional
preserve case of keys, by default False
"""
cp = configparser.ConfigParser(interpolation=None)
if preserve_key_case:
cp.optionxform = str
cp.read_dict(d)
with StringIO() as s:
cp.write(s, space_around_delimiters=space_around_delimiters)
# drop eol chars configparser adds to end of file
return s.getvalue().rstrip()


def to_ini_no_section_header_str(
d: NON_NONE_JSON_DICT,
space_around_delimiters: bool = True,
preserve_key_case: bool = False,
) -> str:
"""
Serialize a dictionary as an ini-_like_ formatted string.
Does not include a section header in the output string.
Note: this function does not support serializing `None` values.
`None` values must either be discarded or transformed into an appropriate type before using this
function.

Parameters
----------
d : Dict[str, NON_NONE_JSON_DICT]
dictionary to serialize
space_around_delimiters : bool, optional
include spaces around `=` signs, by default True
preserve_key_case : bool, optional
preserve case of keys, by default False
"""
cp = configparser.ConfigParser(interpolation=None)
if preserve_key_case:
cp.optionxform = str
data = {NO_SECTIONS: d}
cp.read_dict(data)
with StringIO() as s:
cp.write(s, space_around_delimiters=space_around_delimiters)
buff = s.getvalue()
# drop the [NO_SECTION] header and drop eol chars configparser adds to
# end of file
return buff[buff.find("\n") + 1 :].rstrip()


def to_yaml_str(d: Dict[str, Any]) -> str:
"""Serialize a dictionary as a yaml formatted string."""
yaml = try_import("yaml", extras_require_name="yaml")

# see: https://github.com/yaml/pyyaml/issues/234
# solution from https://github.com/yaml/pyyaml/issues/234#issuecomment-765894586
# hopefully this is resolved in the future, it would be nice to try and use yaml.CDumper instead
# of yaml.Dumper
class Dumper(yaml.Dumper):
def increase_indent(self, flow=False, *args, **kwargs):
# this resolves how lists are indented. without this, they are indented inline with keys
return super().increase_indent(flow=flow, indentless=False)

# drop eol chars
return yaml.dump(d, Dumper=Dumper).rstrip()


def to_toml_str(d: Dict[str, Any]) -> str:
"""Serialize a dictionary as a toml formatted string."""
tomli_w = try_import("tomli_w", extras_require_name="toml")
# drop eol chars
return tomli_w.dumps(d).rstrip()
85 changes: 29 additions & 56 deletions python/ngen_init_config/src/ngen/init_config/serializer.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import os
from pathlib import Path
import pathlib

from .core import Base
from .utils import merge_class_attr
from ._serlializers import (
to_namelist_str,
to_ini_str,
to_ini_no_section_header_str,
to_yaml_str,
to_toml_str,
)
from . import core, format_serializers, utils


class IniSerializer(Base):
class IniSerializer(core.Base):
"""Blanket implementation for serializing into ini format. Python's standard library
`configparser` package is used to handle serialization.

Expand All @@ -30,65 +22,47 @@ class IniSerializer(Base):
If True, keys will be case sensitively serialized (default: `False`)
"""

class Config(Base.Config):
class Config(core.Base.Config):
no_section_headers: bool = False
space_around_delimiters: bool = True
preserve_key_case: bool = False

def to_ini(self, p: Path) -> None:
def to_ini(self, p: pathlib.Path) -> None:
with open(p, "w") as f:
if self._no_section_headers:
b_written = f.write(
to_ini_no_section_header_str(
self,
space_around_delimiters=self._space_around_delimiters,
preserve_key_case=self._preserve_key_case,
)
)
# add eol
if b_written:
f.write(os.linesep)
return

b_written = f.write(
to_ini_str(
self,
space_around_delimiters=self._space_around_delimiters,
preserve_key_case=self._preserve_key_case,
)
)
# add eol
b_written = f.write(self.to_ini_str())
if b_written:
# add eol
f.write(os.linesep)

def to_ini_str(self) -> str:
data = self.dict(by_alias=True)
if self._no_section_headers:
return to_ini_no_section_header_str(
self,
return format_serializers.to_ini_no_section_header_str(
data,
space_around_delimiters=self._space_around_delimiters,
preserve_key_case=self._preserve_key_case,
)

return to_ini_str(
self,
return format_serializers.to_ini_str(
data,
space_around_delimiters=self._space_around_delimiters,
preserve_key_case=self._preserve_key_case,
)

@property
def _space_around_delimiters(self) -> bool:
return merge_class_attr(type(self), "Config.space_around_delimiters", True) # type: ignore
return utils.merge_class_attr(type(self), "Config.space_around_delimiters", True) # type: ignore

@property
def _no_section_headers(self) -> bool:
return merge_class_attr(type(self), "Config.no_section_headers", False) # type: ignore
return utils.merge_class_attr(type(self), "Config.no_section_headers", False) # type: ignore

@property
def _preserve_key_case(self) -> bool:
return merge_class_attr(type(self), "Config.preserve_key_case", False) # type: ignore
return utils.merge_class_attr(type(self), "Config.preserve_key_case", False) # type: ignore


class NamelistSerializer(Base):
class NamelistSerializer(core.Base):
"""Blanket implementation for serializing into FORTRAN namelist format. The `f90nml` package is
used to handle serialization. `f90nml` is not included in default installations of
`ngen.init_config`. Install `ngen.init_config` with `f90nml` using the extra install option,
Expand All @@ -97,64 +71,63 @@ class NamelistSerializer(Base):
Fields are serialized using their alias, if provided.
"""

def to_namelist(self, p: Path) -> None:
def to_namelist(self, p: pathlib.Path) -> None:
with open(p, "w") as f:
b_written = f.write(to_namelist_str(self))
b_written = f.write(self.to_namelist_str())
if b_written:
# add eol
f.write(os.linesep)


def to_namelist_str(self) -> str:
return to_namelist_str(self)
return format_serializers.to_namelist_str(self.dict(by_alias=True))


class YamlSerializer(Base):
class YamlSerializer(core.Base):
"""Blanket implementation for serializing from yaml format. The `PyYAML` package is used to
handle serialization. `PyYAML` is not included in default installations of `ngen.init_config`.
Install `ngen.init_config` with `PyYAML` using the extra install option, `yaml`.

Fields are serialized using their alias, if provided.
"""

def to_yaml(self, p: Path) -> None:
def to_yaml(self, p: pathlib.Path) -> None:
with open(p, "w") as f:
b_written = f.write(to_yaml_str(self))
b_written = f.write(self.to_yaml_str())
if b_written:
# add eol
f.write(os.linesep)

def to_yaml_str(self) -> str:
return to_yaml_str(self)
return format_serializers.to_yaml_str(self.dict(by_alias=True))


class TomlSerializer(Base):
class TomlSerializer(core.Base):
"""Blanket implementation for serializing from `toml` format. The `tomli_w` package is used to
handle serialization. `tomli_w` is not included in default installations of `ngen.init_config`.
Install `ngen.init_config` with `tomli_w` using the extra install option, `toml`.

Fields are serialized using their alias, if provided.
"""

def to_toml(self, p: Path) -> None:
def to_toml(self, p: pathlib.Path) -> None:
with open(p, "w") as f:
b_written = f.write(to_toml_str(self))
b_written = f.write(self.to_toml_str())
# add eol
if b_written:
f.write(os.linesep)

def to_toml_str(self) -> str:
return to_toml_str(self)
return format_serializers.to_toml_str(self.dict(by_alias=True))


class JsonSerializer(Base):
class JsonSerializer(core.Base):
"""Blanket implementation for serializing to `json` format. This functionality is provided by
`pydantic`. See `pydantic`'s documentation for other configuration options.

Fields are serialized using their alias, if provided.
"""

def to_json(self, p: Path, *, indent: int = 0) -> None:
def to_json(self, p: pathlib.Path, *, indent: int = 0) -> None:
with open(p, "w") as f:
b_written = f.write(self.to_json_str(indent=indent))
# add eol
Expand Down
Loading