Source code for pydrobert.param._serializer

# Copyright 2022 Sean Robertson
#
# The Serialization subclasses are heavily inspired by the source code for
# JSONSerialization from the param package:
# https://github.com/holoviz/param/blob/76c7026346b73951dcf4308b6302f0742b0e83e7/param/serializer.py#L61
#
# param is BSD-3 licensed:
#
# Copyright (c) 2005-2022, HoloViz team.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification, are
# permitted provided that the following conditions are met:
#
#  * Redistributions of source code must retain the above copyright notice, this list of
#    conditions and the following disclaimer.
#
#  * Redistributions in binary form must reproduce the above copyright notice, this list
#    of conditions and the following disclaimer in the documentation and/or other
#    materials provided with the distribution.
#
#  * Neither the name of the copyright holder nor the names of any contributors may be
#    used to endorse or promote products derived from this software without specific
#    prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
# SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.
#
# This code (Apache v2.0):
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
# file except in compliance with the License. You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.

import abc
import argparse
import textwrap
import json
import sys

from typing import (
    Any,
    Collection,
    Dict,
    List,
    Optional,
    Sequence,
    TextIO,
    Tuple,
    Type,
    Union,
)
from io import StringIO

try:
    from typing import Protocol, Literal
except ImportError:
    from typing_extensions import Protocol, Literal

import param

from param.serializer import Serialization

from ._file_serialization import (
    yaml_is_available,
    deserialize_from_yaml_to_obj,
    serialize_from_obj_to_yaml,
)


[docs] class Serializable(Protocol): """Protocol for classes capable of reading and writing objects from/to strings"""
[docs] @abc.abstractclassmethod def loads(cls, serialized: str) -> Any: """Read serialized string into an object""" ...
[docs] @abc.abstractclassmethod def dumps(cls, obj: Any, help: Any = None) -> str: """Dump object (and optional help object) to string `help` should not impact the deserialization of `obj`. It can be ignored. """ ...
[docs] class JsonSerializable(Serializable): """Write/read to/from strings using JSON protocol""" @classmethod def loads(cls, serialized: str) -> Any: return json.loads(serialized) @classmethod def dumps(cls, obj: Any, help: Any = None) -> str: return json.dumps(obj)
[docs] class YamlSerializable(Serializable): """Write/read to/from strings using YAML protocol""" @classmethod def loads(cls, serialized: str) -> Any: with StringIO(serialized) as s: return deserialize_from_yaml_to_obj(s) @classmethod def dumps(cls, obj: Any, help: Any = None) -> str: with StringIO() as s: serialize_from_obj_to_yaml(s, obj, help) s.seek(0) return s.read()
PObjType = Union[param.Parameterized, Type[param.Parameterized]] NestedSubsetType = Optional[Dict[str, "NestedSubsetType"]]
[docs] class SerializableSerialization(Serialization, Serializable): """ABC for sensible serialization""" @classmethod def nest_subsets(cls, subset: Optional[Collection[str]]) -> NestedSubsetType: if subset is None: return subset nested_subsets: NestedSubsetType = dict() for s in subset: if "." in s: k, v = s.split(".", maxsplit=1) v = cls.nest_subsets({v}) d = nested_subsets.get(k, None) if d is None: nested_subsets[k] = v else: d.update(v) else: nested_subsets.setdefault(s, None) return nested_subsets @classmethod def get_serialize_pair( cls, pobj: PObjType, pname: str, nested_subsets: NestedSubsetType = None ) -> Tuple[Any, Any]: p = pobj.param[pname] value = pobj.param.get_value_generator(pname) value = p.serialize(value) doc = pobj.param[pname].doc doc = doc if not doc else textwrap.dedent(doc).replace("\n", " ").strip() return value, doc @classmethod def get_deserialize_value( cls, pobj: PObjType, pname: str, value: Any, nested_subsets: NestedSubsetType = None, ) -> Any: return pobj.param[pname].deserialize(value) @classmethod def get_serialize_dict( cls, pobj: PObjType, nested_subsets: NestedSubsetType = None ) -> Tuple[Dict[str, Any], Dict[str, Optional[str]]]: dict_, help = dict(), dict() for pname in pobj.param.objects("existing"): if nested_subsets is not None and pname not in nested_subsets: continue value, doc = cls.get_serialize_pair( pobj, pname, nested_subsets if nested_subsets is None else nested_subsets[pname], ) dict_[pname] = value help[pname] = doc return dict_, help @classmethod def serialize_parameters( cls, pobj: PObjType, subset: Optional[Collection[str]] = None ) -> str: return cls.dumps(*cls.get_serialize_dict(pobj, cls.nest_subsets(subset))) @classmethod def get_deserialize_dict( cls, pobj: PObjType, deserialized: Dict[str, Any], nested_subsets: NestedSubsetType = None, ) -> Dict[str, Any]: components = dict() for pname, value in deserialized.items(): if nested_subsets is not None and pname not in nested_subsets: continue components[pname] = cls.get_deserialize_value( pobj, pname, value, nested_subsets if nested_subsets is None else nested_subsets[pname], ) return components @classmethod def deserialize_parameters( cls, pobj: PObjType, serialization: str, subset: Optional[Collection[str]] = None, ) -> Dict[str, Any]: return cls.get_deserialize_dict( pobj, cls.loads(serialization), cls.nest_subsets(subset) ) @classmethod def serialize_parameter_value(cls, pobj: PObjType, pname: str) -> str: return cls.dumps(*cls.get_serialize_pair(pobj, pname)) @classmethod def deserialize_parameter_value(cls, pobj: PObjType, pname: str, value: str) -> Any: return cls.get_deserialize_value(pobj, pname, cls.loads(value))
class JsonSerialization(SerializableSerialization, JsonSerializable): pass
[docs] class YamlSerialization(SerializableSerialization, YamlSerializable): """YAML (de)serialization See Also -------- register_serializer For how this is used """
[docs] class RecklessSerializableSerialization(SerializableSerialization): """ABC for reckless serialization""" @classmethod def get_serialize_pair( cls, pobj: PObjType, pname: str, nested_subsets: NestedSubsetType = None ) -> Tuple[Any, Any]: p = pobj.param[pname] value = pobj.param.get_value_generator(pname) value = p.serialize(value) if isinstance(value, param.Parameterized): value, doc = cls.get_serialize_dict(value, nested_subsets) else: doc = pobj.param[pname].doc doc = doc if not doc else textwrap.dedent(doc).replace("\n", " ").strip() return value, doc @classmethod def get_deserialize_value( cls, pobj: PObjType, pname: str, value: Any, nested_subsets: NestedSubsetType = None, ) -> Any: pobjp = pobj.param[pname] value = pobj.param[pname].deserialize(value) class_ = getattr(pobjp, "class_", None) if ( class_ is not None and value is not None and getattr(pobjp, "is_instance", False) and issubclass(class_, param.Parameterized) ): value = class_(**cls.get_deserialize_dict(class_, value, nested_subsets)) return value
[docs] class RecklessJsonSerialization(RecklessSerializableSerialization, JsonSerializable): """Reckless JSON (de)serialization See Also -------- register_serializer For how this is used """ pass
[docs] class RecklessYamlSerialization(RecklessSerializableSerialization, YamlSerializable): """Reckless YAML (de)serialization See Also -------- register_serializer For how this is used """ pass
_my_serializers = { "_json": JsonSerialization, # don't advertise - for testing purposes "yaml": YamlSerialization, "reckless_json": RecklessJsonSerialization, "reckless_yaml": RecklessYamlSerialization, }
[docs] def register_serializer(mode: Literal["reckless_json", "reckless_yaml", "yaml"]): """Add a custom (de)serialization protocol to parameterized instances The serialization protocol to be registered is passed as `mode`, which can be one of 1. :obj:`'reckless_json'`, which is identical to the standard :obj:`'json'` serializer but for some simplifying assumptions to handle `nesting <https://param.holoviz.org/user_guide/Serialization_and_Persistence.html#json-limitations-and-workarounds>`_. See the below note for more information. 2. :obj:`'yaml'`, which follows a similar parsing strategy to vanilla :obj:`'json'` but (de)serializes in YAML format instead. Requires either :mod:`yaml` or :mod:`ruamel.yaml` to be installed or will raise an import error at the first request. 3. :obj:`'reckless_yaml'`, which makes the same reckless assumptions as :obj:`reckless_json'` but (de)serialized in the YAML format. After calling this function, parameters can be serialized with the same `mode` registered via :func:`param.Parameterized.param.serialize_parameters`: >>> str_ = p.param.serialize_parameters(subset, mode) >>> p = P.param.deserialize_parameters(str_, subset, mode) Warnings -------- Functionality is in beta and subject to additions and modifications. Notes ----- We make strong simplifying assumptions to handle nesting. The usual means of nesting :class:`param.parameterized.Parameterized` instances is >>> class Parent(param.Parameterized): ... child = param.ClassSelector(SomeParameterizedClass) >>> parent = Parent(child=SomeParameterizedClass(leaf_value=1)) While the obvious solution is to recursively call (de)serialization on child instances, this solution is not suited to all situations. In deserialization, the child may be of type `SomeParameterizedClass`, but may also be one of its subclasses. If children are sharing references to the same instance, that information will be lost in serialization. For now (keep track of `this bug <https://github.com/holoviz/param/issues/520>`_ for changes), ``mode="json"`` will throw if it sees a nested parameterized instance in serialization or deserialization. Reckless serialization is performed recursively with no consideration for references. Deserialization is performed by recursing on the class provided to the class selector (i.e. `SomeParameterizedClass`), not any of its subclasses. To (de)serialize only a subset of parameters in a child instance, delimit child parameters with ``<name_in_parent>.<name_in_child>`` in the `subset` argument. In the example above, we can serialize only `leaf_value` using >>> parent.param.serialize_parameters({'child.leaf_value'}, mode="reckless_json") See Also -------- unregister_serializer """ # avoids the case where the user mistakenly treats a standard serializer (e.g. json) # as something to be registered if mode in param.Parameter._serializers: return param.Parameter._serializers[mode] = _my_serializers[mode]
[docs] def unregister_serializer(mode: Literal["reckless_json", "reckless_yaml", "yaml"]): """Unregister a previously registered custom serializer See Also -------- register_serializer """ if mode in param.Parameter._serializers and ( param.Parameter._serializers[mode] is _my_serializers[mode] ): param.Parameter._serializers.pop(mode)
try: Action = argparse.Action[param.Parameterized] except: Action = argparse.Action
[docs] class DeserializationAction(Action): """Action to deserialize a parameterized object from file Given some subclass of :class:`param.parameterized.Parameterized`, `MyParameterized`, the action can be added by calling, e.g. >>> parser.add_argument( ... '--param', type=MyParameterized, action=DeserializationAction) In this example, the argument passed with the flag :obj:`--param` is treated as a path to a JSON file from which a serialized copy of `MyParameterized` is read and instantiatied. Deserialization is performed with the :func:`param.parameterized.Parameterized.param.deserialize_parameters`. The deserialization `mode` and optionally the `subset` of parameters deserialized can be changed by passing the `const` keyword argument to :func:`add_argument`. `const` can be either a string (just the `mode`) or a tuple of a string (`mode`) and set of strings (`subset`). See Also -------- ParameterizedFileReadAction Same intent, but using :mod:`pydrobert.param` custom deserialization routines. register_serializer To enable custom parsing modes. """ class_: Type[param.Parameterized] def __init__( self, option_strings: Sequence[str], dest: str, nargs: Union[str, int, None] = None, const: Union[str, Tuple[str, Optional[Collection[str]]]] = "json", default: Any = None, type: Type[param.Parameterized] = param.Parameterized, choices=None, required: bool = False, help: Optional[str] = None, metavar: Union[str, Tuple[str, ...], None] = None, ) -> None: if not issubclass(type, param.Parameterized): raise ValueError("type is not a subclass of param.Parameterized") self.class_ = type if isinstance(const, str): const = const, None else: const = const[0], (None if const[1] is None else set(const[1])) super().__init__( option_strings, dest, nargs, const, default, argparse.FileType("r"), choices, required, help, metavar, ) def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Union[TextIO, List[TextIO]], option_string: Union[str, None] = None, ) -> None: if values is self.const: values = [] values_ = list(values) if isinstance(values, list) else [values] mode, subset = self.const for i in range(len(values_)): value = values_[i] name = value.name value = value.read() try: value = self.class_( **self.class_.param.deserialize_parameters(value, subset, mode) ) except Exception as e: raise argparse.ArgumentError( self, f"error deserializing '{name}' as '{self.class_.__name__}' with " f"protocol '{mode}': {str(e)}", ) values_[i] = value if isinstance(values, list): values = values_ else: values = values_[0] setattr(namespace, self.dest, values)
[docs] class SerializationAction(Action): """Action to serialize a parameterized object and then exit The counterpart to :class:`DeserializationAction`, adding this action as an argument to an :class:`argparse.ArgumentParser` as such >>> parser.add_argument( ... '--print', type=MyParameterized, action=SerializationAction) will, by default, serialize a new `MyParameterized` instance as JSON and print it to stdout if ``'--print'`` is parsed without an argument. Afterwards, the command will terminate. If ``'--print'`` is passed a path, the JSON will be printed there instead. Serialization is performed with :func:`param.parameterized.Parameterized.param.serialize_parameters`. The serialization `mode` and optionally the `subset` of parameters serialized can be changed by passing the `const` keyword argument to :func:`add_argument`. `const` can be either a string (just the `mode`) or a tuple of a string (`mode`) and set of strings (`subset`). The `type` argument can be either a subclass of :class:`param.parameterized.Parameterized` or an instance of one. In the latter case, the parameter values of that instance will be serialized instead. See Also -------- ParameterizedFilePrintAction Same intent, but using :mod:`pydrobert.param` custom serialization routines. register_serializer To enable custom parsing modes. """ pobj: PObjType def __init__( self, option_strings: Sequence[str], dest: str, nargs: Union[str, int, None] = "?", const: Union[str, Tuple[str, Optional[Collection[str]]]] = "json", default: TextIO = argparse.FileType("w")("-"), type: PObjType = param.Parameterized, choices=None, required: bool = False, help: Optional[str] = None, metavar: Union[str, Tuple[str, ...], None] = None, ) -> None: if not isinstance(type, param.Parameterized) and not issubclass( type, param.Parameterized ): raise ValueError( "type is neither an instance nor a subclass of param.Parameterized" ) self.pobj = type if isinstance(const, str): const = const, None else: const = const[0], (None if const[1] is None else set(const[1])) super().__init__( option_strings, dest, nargs, const, default, argparse.FileType("w"), choices, required, help, metavar, ) def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Union[TextIO, List[TextIO]], option_string: Union[str, None] = None, ) -> None: if values is self.const: values = [] # nargs = '?' if not isinstance(values, list): values = [values] if not len(values): values.append(self.default) mode, subset = self.const pobj_name = ( self.pobj.__name__ if isinstance(self.pobj, type) else self.pobj.name ) try: txt = self.pobj.param.serialize_parameters(subset, mode) except Exception as e: msg = f": {e.msg}" if hasattr(e, "msg") else "" raise argparse.ArgumentError( self, f"error serializing parameterized '{pobj_name}' with protocol " f"'{mode}'{msg}", ) for value in values: value.write(txt) sys.exit(0)
[docs] def add_deserialization_group_to_parser( parser: argparse.ArgumentParser, pobj: PObjType, dest: str, file_formats: Union[ Literal["json", "yaml"], Collection[Literal["json", "yaml"]], None ] = None, subset: Optional[Collection[str]] = None, reckless: bool = False, flag_format_str: Union[str, Sequence[str]] = "--read-{file_format}", help_format_str: Optional[str] = "Read as a {file_format} file", title_format_str: Optional[str] = "{pobj_name} deserialization", desc_format_str: Optional[str] = ( "Flags to read in {pobj_name} parameters from file. Only one may be specified" ), required: bool = False, register_missing: bool = True, ): """Add flags to parser for deserializing parameterized objects from file A convenience function for coordinating :class:`DeserializationAction` arguments over multiple file formats. The usual case might look like >>> add_deserialization_group_to_parser(parser, MyParameterized, 'param') >>> namespace = parser.parse_args() >>> namespace.param # stores the MyParameterized instance or None Here it adds mutually exclusive flags to deserialize a `MyParameterized` instance using serialization protocols for the available file formats. Parameters ---------- parser pobj Either a subclass of :class:`param.parameterized.Parameterized` or an instance of one. Determines the type of parameterized object to deserialize. If `pobj` is a :class:`type`, the default value for the parameters in the namespace will be :obj:`None`. Otherwise (when `pobj` is an instance), `pobj` will be the default value. dest The name of the attribute in `namespace` to store the deserialized instance under. file_formats If specified, one or a list of file formats to add flags for. If unspecified, flags for every available file format will be added. Availability means both the correct backend is installed to parse the file and, if `register_missing` is :obj:`False`, that the corresponding mode has already been registered. The :obj:`'json'` format is always available. subset If specified, only the parameters with names in this set will be deserialized. reckless Whether to allow simplifying assumptions to make parsing easier. flag_format_str One or more Python format strings which, after formatting, will act as the flags for the argument. The following keys are available for formatting: - `file_format`, an entry in `file_formats` - `dest` - `pobj_name`, either :obj:`pobj.name` if `pobj` is an instance or :obj:`pobj.__name__` if `pobj` is a class. help_format_str A python format string which, after formatting, describes the flags. The same keys are available as those to `flag_format_str`. title_format_str A python format string which, after formatting, is used to name the group `grp`. Can only be formatted with the keys `dest` and `pobj_name`. desc_format_str A python format string which, after formatting, is used to describe the group `grp`. Can only be formatted with the keys `dest` and `pobj_name`. required Whether to require the user to specify one flag. register_missing Whether to register any custom modes corresponding to the `file_formats` (and `reckless`) which have yet to be registered via :func:`register_serializer`. Setting to :obj:`False` will restrict the dynamically chosen value of `file_formats`. If `file_formats` is manually specified, the parser will throw when it tries to deserialize using an unspecified mode. Returns ------- grp The group containing the added arguments. The mutually-exclusive group containing them is nested in a regular argument group. Warnings -------- Functionality is in beta and subject to additions and modifications. See Also -------- add_parameterized_read_group An analogous function using custom deserialization routines. pydrobert.param.serialization.register_serializer More information on the file formats and modes of serialization. """ if isinstance(flag_format_str, str): flag_format_str = (flag_format_str,) elif not len(flag_format_str): raise ValueError("Must specify at least one string in flag_format_str") if isinstance(pobj, type): default, pobj_name, class_ = None, pobj.__name__, pobj else: default, pobj_name, class_ = pobj, pobj.name, type(pobj) if reckless: fmt2mode = {"json": "reckless_json", "yaml": "reckless_yaml"} mode2fmt = {"reckless_json": "json", "reckless_yaml": "yaml"} else: fmt2mode = mode2fmt = {"json": "json", "yaml": "yaml"} if file_formats is None: file_formats = set(fmt2mode) if not register_missing: file_formats &= {mode2fmt[s] for s in param.Parameter._serializers} if "yaml" in file_formats and not yaml_is_available(): file_formats.remove("yaml") else: file_formats = set(file_formats) missing_formats = file_formats - set(fmt2mode) if missing_formats: raise ValueError(f"No serialization known for: {missing_formats}") if title_format_str is None: title_str = None else: title_str = title_format_str.format(dest=dest, pobj_name=pobj_name) if desc_format_str is None: desc_str = None else: desc_str = desc_format_str.format(dest=dest, pobj_name=pobj_name) grp = parser.add_argument_group(title=title_str, description=desc_str) grp_ = grp.add_mutually_exclusive_group(required=required) grp_.set_defaults(**{dest: default}) for file_format in file_formats: mode = fmt2mode[file_format] if register_missing: register_serializer(mode) const = (mode, subset) flag_str = ( f.format(file_format=file_format, dest=dest, pobj_name=pobj_name) for f in flag_format_str ) if help_format_str is None: help_str = None else: help_str = help_format_str.format( file_format=file_format, dest=dest, pobj_name=pobj_name ) grp_.add_argument( *flag_str, dest=dest, action=DeserializationAction, type=class_, metavar=file_format.upper(), const=const, help=help_str, ) return grp
[docs] def add_serialization_group_to_parser( parser: argparse.ArgumentParser, pobj: PObjType, file_formats: Union[ Literal["json", "yaml"], Collection[Literal["json", "yaml"]], None ] = None, subset: Optional[Collection[str]] = None, reckless: bool = False, flag_format_str: Union[str, Sequence[str]] = "--print-{file_format}", help_format_str: Optional[str] = "Print as a {file_format} file", title_format_str: Optional[str] = "{pobj_name} serialization", desc_format_str: Optional[str] = ( "Flags to print {pobj_name} parameters to file (one arg) or stdout (no args) " "and then exit" ), register_missing: bool = True, ): """Add flags to parser to serialize parameters to file or stdout A convenience function for coordinating :class:`SerializationAction` arguments over multiple file formats. The usual case might look like >>> add_deserialization_group_to_parser(parser, MyParameterized()) >>> parser.parse_args() Here it adds flags to serialize a `MyParameterized` instance to either a file or stdout using serialization protocols for the available file formats. If any associated flag is passed as an argument, the program will exit (and the :func:`parse_args` call will not return). Parameters ---------- parser pobj Either a subclass of :class:`param.parameterized.Parameterized` or an instance of one. Determines the parameterized object to serialize. If `pobj` is a :class:`type`, only the default values of the parameters of the class will end up serialized. If `pobj` is an instance, that instance's parameters will be serialized. file_formats If specified, one or a list of file formats to add flags for. If unspecified, flags for every available file format will be added. Availability means both the correct backend is installed to parse the file and, if `register_missing` is :obj:`False`, that the corresponding mode has already been registered. The :obj:`'json'` format is always available. subset If specified, only the parameters with names in this set will be serialized. reckless Whether to allow simplifying assumptions to make parsing easier. flag_format_str One or more Python format strings which, after formatting, will act as the flags for the argument. The following keys are available for formatting: - `file_format`, an entry in `file_formats` - `pobj_name`, either :obj:`pobj.name` if `pobj` is an instance or :obj:`pobj.__name__` if `pobj` is a class. help_format_str A python format string which, after formatting, describes the flags. The same keys are available as those to `flag_format_str`. title_format_str A python format string which, after formatting, is used to name the group `grp`. Can only be formatted with the key `pobj_name`. desc_format_str A python format string which, after formatting, is used to describe the group `grp`. Can only be formatted with the key `pobj_name`. register_missing Whether to register any custom modes corresponding to the `file_formats` (and `reckless`) which have yet to be registered via :func:`register_serializer`. Setting to :obj:`False` will restrict the dynamically chosen value of `file_formats`. If `file_formats` is manually specified, the parser will throw when it tries to serialize using an unspecified mode. Returns ------- grp The group which the flags have beed added to. Note that only the first argument in the group can ever be parsed as the program will try to exit after printing. Warnings -------- Functionality is in beta and subject to additions and modifications. See Also -------- add_parameterized_print_group An analogous function using custom serialization routines. pydrobert.param.serialization.register_serializer More information on the file formats and modes of serialization. """ if isinstance(flag_format_str, str): flag_format_str = (flag_format_str,) elif not len(flag_format_str): raise ValueError("Must specify at least one string in flag_format_str") pobj_name = pobj.__name__ if isinstance(pobj, type) else pobj.name if reckless: fmt2mode = {"json": "reckless_json", "yaml": "reckless_yaml"} mode2fmt = {"reckless_json": "json", "reckless_yaml": "yaml"} else: fmt2mode = mode2fmt = {"json": "json", "yaml": "yaml"} if file_formats is None: file_formats = set(fmt2mode) if not register_missing: file_formats &= {mode2fmt[s] for s in param.Parameter._serializers} if "yaml" in file_formats and not yaml_is_available(): file_formats.remove("yaml") else: file_formats = set(file_formats) missing_formats = file_formats - set(fmt2mode) if missing_formats: raise ValueError(f"No serialization known for: {missing_formats}") if title_format_str is None: title_str = None else: title_str = title_format_str.format(pobj_name=pobj_name) if desc_format_str is None: desc_str = None else: desc_str = desc_format_str.format(pobj_name=pobj_name) grp = parser.add_argument_group(title=title_str, description=desc_str) for file_format in file_formats: mode = fmt2mode[file_format] if register_missing: register_serializer(mode) const = (mode, subset) flag_str = ( f.format(file_format=file_format, pobj_name=pobj_name) for f in flag_format_str ) if help_format_str is None: help_str = None else: help_str = help_format_str.format( file_format=file_format, pobj_name=pobj_name ) grp.add_argument( *flag_str, action=SerializationAction, type=pobj, metavar=file_format.upper(), const=const, help=help_str, ) return grp