google-research
716 строк · 26.7 Кб
1# coding=utf-8
2# Copyright 2024 The Google Research Authors.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""A generic serializable hyperparameter container.
17
18hparam is based on `attr` under the covers, but provides additional features,
19such as serialization and deserialization in a format that is compatible with
20(now defunct) tensorflow.HParams, runtime type checking, implicit casting where
21safe to do so (e.g. int->float, scalar->list).
22
23Unlike tensorflow.HParams, this supports hierarchical nesting of parameters for
24better organization, aliasing parameters to short abbreviations for compact
25serialization while maintaining code readability, and support for Enum values.
26
27Example usage:
28@hparam.s
29class MyNestedHParams:
30learning_rate: float = hparam.field(abbrev='lr', default=0.1)
31layer_sizes: List[int] = hparam.field(abbrev='ls', default=[256, 64, 32])
32
33@hparam.s
34class MyHParams:
35nested_params: MyNestedHParams = hparam.nest(MyNestedHParams)
36non_nested_param: int = hparam.field(abbrev='nn', default=0)
37
38hparams = MyHParams(nested_params=MyNestedHParams(
39learning_rate=0.02, layer_sizes=[100, 10]),
40non_nested_param=5)
41hparams.nested_params.learning_rate = 0.002
42serialized = hparams.serialize() # "lr=0.002,ls=[100,10],nn=5"
43hparams.nested_params.learning_rate = 0.003
44new_hparams = MyHParams(serialized)
45new_hparams.nested_params.learning_rate == 0.002 # True
46"""
47
48import collections49import copy50import csv51import enum52import inspect53import json54import numbers55import re56from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union57
58import attr59import six60
61_ABBREV_KEY = 'hparam.abbrev'62_SCALAR_TYPE_KEY = 'hparam.scalar_type'63_IS_LIST_KEY = 'hparam.is_list'64_PREFIX_KEY = 'hparam.prefix'65_SERIALIZED_ARG = '_hparam_serialized_arg'66
67# Define the regular expression for parsing a single clause of the input
68# (delimited by commas). A legal clause looks like:
69# <variable name> = <rhs>
70# where <rhs> is either a single token or [] enclosed list of tokens.
71# For example: "var = a" or "x = [1,2,3]"
72_PARAM_RE = re.compile(73r"""74(?P<name>[a-zA-Z][\w\.]*) # variable name: 'var' or 'x'
75\s*=\s*
76((?P<strval>".*") # single quoted string value: '"a,b=c"' or None
77|
78(?P<val>[^,\[]*) # single value: 'a' or None
79|
80\[(?P<vals>[^\]]*)\]) # list of values: None or '1,2,3'
81# (the regex removes the surrounding brackets)
82($|,\s*)""", re.VERBOSE)83
84_ValidScalarInstanceType = Union[int, float, str, enum.Enum]85_ValidListInstanceType = Union[List[_ValidScalarInstanceType],86Tuple[_ValidScalarInstanceType]]87_ValidFieldInstanceType = Union[_ValidScalarInstanceType,88_ValidListInstanceType]89_ValidScalarType = Type[_ValidScalarInstanceType]90T = TypeVar('T')91
92
93@attr.s94class _FieldInfo:95"""Metadata for a single HParam field."""96# The path to a field from the root hparam.s instance. This is to enable97# finding fields that are in nested hparam.s classes. For example, a path98# ['foo', 'bar'] means that the root class has a field called 'foo' created99# with hparam.nest(), whose value is another hparam.s class, which contains a100# field, 'bar' that was created using hparam.field(). All path elements other101# than the last correspond to nested classes, while the last is a field.102path: List[str] = attr.ib()103# If the field value is a single (scalar) value, then this is the type of the104# value. If it is a list then this is the type of the list elements, which105# must all be the same.106scalar_type: _ValidScalarType = attr.ib()107# Whether this field is a list type.108is_list: bool = attr.ib()109# The default value for this field.110default_value: _ValidFieldInstanceType = attr.ib()111
112
113_HParamsMapType = Dict[str, _FieldInfo]114
115
116def _get_type(instance):117"""Determines the type of a value.118
119Both whether it is an iterable and what the scalar type is. For iterables,
120scalar type is the type of its elements, which are required to all be the same
121type.
122
123Valid types are int, float, string, Enum (where the value is int, float, or
124string), and lists or tuples of those types.
125
126Args:
127instance: The value whose type is to be determined.
128
129Returns:
130scalar_type: The type of `instance`'s elements, if `instance` is a list or
131tuple. Otherwise, the type of `instance`.
132is_list: Whether `instance` is a list or tuple.
133
134Raises:
135TypeError: If instance is not a valid type.
136* `instance` is an iterable that is not a list, tuple or string.
137* `instance` is a list or tuple with elements of different types.
138* `instance` is an empty list or tuple.
139* `instance` is a list or tuple whose elements are non-string iterables.
140* `instance` is None or a list or tuple of Nones.
141* `instance` is none of {int, float, str, Enum, list, tuple}.
142* `instance` is a list or tuple whose values are not one of the above
143types.
144* `instance` is an Enum type whose values are neither numbers nor strings.
145"""
146
147is_list = False148scalar_type = type(instance)149if isinstance(instance, Iterable) and not issubclass(150scalar_type, (six.string_types, six.binary_type)):151is_list = True152if not instance:153raise TypeError('Empty iterables cannot be used as default values.')154
155if not isinstance(instance, collections.abc.Sequence):156# Most likely a Dictionary or Set.157raise TypeError('Only numbers, strings, and lists are supported. Found '158f'{scalar_type}.')159
160scalar_type = type(instance[0])161if isinstance(instance[0], Iterable) and not issubclass(162scalar_type, (six.string_types, six.binary_type)):163raise ValueError('Nested iterables and dictionaries are not supported.')164if not all([isinstance(i, scalar_type) for i in instance]):165raise TypeError('Iterables of mixed type are not supported.')166
167if issubclass(scalar_type, type(None)):168raise TypeError('Fields cannot have a default value of None.')169
170valid_field_types = (six.string_types, six.binary_type, numbers.Integral,171numbers.Number)172if not issubclass(scalar_type, valid_field_types + (enum.Enum,)):173raise TypeError(174'Supported types include: number, string, Enum, and lists of those '175f'types. {scalar_type} is not one of those.')176
177if issubclass(scalar_type, enum.Enum):178enum_value_type = type(list(scalar_type.__members__.values())[0].value)179if not issubclass(enum_value_type, valid_field_types):180raise TypeError(f'Enum type {scalar_type} has values of type '181f'{enum_value_type}, which is not allowed. Enum values '182'must be numbers or strings.')183
184return (scalar_type, is_list)185
186
187def _make_converter(scalar_type,188is_list):189"""Produces a function that casts a value to the target type, if compatible.190
191Args:
192scalar_type: The scalar type of the hparam.
193is_list: Whether the hparam is a list type.
194
195Returns:
196A function that casts its input to either `scalar_type` or
197`List[scalar_type]` depending on `is_list`.
198"""
199
200def scalar_converter(value):201"""Converts a scalar value to the target type, if compatible.202
203Args:
204value: The value to be converted.
205
206Returns:
207The converted value.
208
209Raises:
210TypeError: If the type of `value` is not compatible with scalar_type.
211* If `scalar_type` is a string type, but `value` is not.
212* If `scalar_type` is a boolean, but `value` is not, or vice versa.
213* If `scalar_type` is an integer type, but `value` is not.
214* If `scalar_type` is a float type, but `value` is not a numeric type.
215"""
216if (isinstance(value, Iterable) and217not issubclass(type(value), (six.string_types, six.binary_type))):218raise TypeError('Nested iterables are not supported')219
220# If `value` is already of type `scalar_type`, return it directly.221# `isinstance` is too weak (e.g. isinstance(True, int) == True).222if type(value) == scalar_type: # pylint: disable=unidiomatic-typecheck223return value224
225# Some callers use None, for which we can't do any casting/checking. :(226if issubclass(scalar_type, type(None)):227return value228
229# Avoid converting a non-string type to a string.230if (issubclass(scalar_type, (six.string_types, six.binary_type)) and231not isinstance(value, (six.string_types, six.binary_type))):232raise TypeError(233f'Expected a string value but found {value} with type {type(value)}.')234
235# Avoid converting a number or string type to a boolean or vice versa.236if issubclass(scalar_type, bool) != isinstance(value, bool):237raise TypeError(238f'Expected a bool value but found {value} with type {type(value)}.')239
240# Avoid converting float to an integer (the reverse is fine).241if (issubclass(scalar_type, numbers.Integral) and242not isinstance(value, numbers.Integral)):243raise TypeError(244f'Expected an integer value, but found {value} with type '245f'{type(value)}.'246)247
248# Avoid converting a non-numeric type to a numeric type.249if (issubclass(scalar_type, numbers.Number) and250not isinstance(value, numbers.Number)):251raise TypeError(252f'Expected a numeric type, but found {value} with type {type(value)}.'253)254
255return scalar_type(value)256
257def converter(value):258"""Converts a value to the target type, if compatible.259
260Args:
261value: The value to be converted.
262
263Returns:
264The converted value.
265
266Raises:
267TypeError: If the type of `value` is not compatible with `scalar_type` and
268`is_list`.
269* If `scalar_type` is a string type, but `value` is not.
270* If `scalar_type` is a boolean, but `value` is not, or vice versa.
271* If `scalar_type` is an integer type, but `value` is not.
272* If `scalar_type` is a float type, but `value` is not a numeric type.
273* If `is_list` is False, but value is a non-string iterable.
274"""
275value_is_listlike = (276isinstance(value, Iterable) and277not issubclass(type(value), (six.string_types, six.binary_type)))278if value_is_listlike:279if is_list:280return [scalar_converter(v) for v in value]281else:282raise TypeError('Assigning an iterable to a scalar field.')283else:284if is_list:285return [scalar_converter(value)]286else:287return scalar_converter(value)288
289return converter290
291
292def field(abbrev, default):293"""Create a new field on an HParams class.294
295A field is a single hyperparameter with a value. Fields must have an
296abbreviation key, which by convention is a short string, which is used to
297produce a concise serialization. Fields must also have a default value that
298determines the hyperparameter's type, which cannot be dynamically changed.
299Valid types are integers, floats, strings, enums that have values that are
300those types, and lists of those types.
301
302An HParams class can have child HParams classes, but those should be added
303using `nest` instead of `field`.
304
305Example usage:
306@hparam.s
307class MyHparams:
308learning_rate: float = hparam.field(abbrev='lr', default=0.1)
309layer_sizes: List[int] = hparam.field(abbrev='ls', default=[256, 64, 32])
310optimizer: OptimizerEnum = hparam.field(abbrev='opt',
311default=OptimizerEnum.SGD)
312
313Args:
314abbrev: A short string that represents this hyperparameter in the serialized
315format.
316default: The default value of this hyperparameter. This is required. Valid
317types are integers, floats, strings, enums that have values that are those
318types, and lists of those types. Default values for list-typed fields must
319be non-empty lists. None is not an allowed default. List values will be
320copied into instances of the field, so modifications to a list provided as
321default will not be reflected in existing or subsequently created class
322instances.
323
324Returns:
325A field-descriptor which can be consumed by a class decorated by @hparam.s.
326
327Raises:
328TypeError if the default value is not one of the allowed types.
329"""
330
331scalar_type, is_list = _get_type(default)332kwargs = {333'kw_only': True,334'metadata': {335_ABBREV_KEY: abbrev,336_SCALAR_TYPE_KEY: scalar_type,337_IS_LIST_KEY: is_list,338},339'converter': _make_converter(scalar_type, is_list),340}341if is_list:342# Lists are mutable, so we generate a factory method to produce a copy of343# the list to avoid different instances of the class mutating each other.344kwargs['factory'] = lambda: copy.copy(default)345else:346kwargs['default'] = default347return attr.ib(**kwargs) # pytype: disable=duplicate-keyword-argument348
349
350def nest(nested_class,351prefix = None):352"""Create a nested HParams class field on a parent HParams class.353
354An HParams class (a class decorated with @hparam.s) can have a field that is
355another HParams class, to create a hierarchical structure. Use `nest` to
356create these fields.
357
358Example usage:
359@hparam.s
360class MyNestedHParams:
361learning_rate: float = hparam.field(abbrev='lr', default=0.1)
362layer_sizes: List[int] = hparam.field(abbrev='ls', default=[256, 64, 32])
363
364@hparam.s
365class MyHParams:
366nested_params: MyNestedHParams = hparam.nest(MyNestedHParams)
367non_nested_param: int = hparam.field(abbrev='nn', default=0)
368
369Args:
370nested_class: The class of the nested hyperparams. The class must be
371decorated with @hparam.s.
372prefix: An optional prefix to add to the abbrev field of all fields in the
373nested hyperparams. This enables nesting the same class multiple times, as
374long as the prefix is different.
375
376Returns:
377A field-descriptor which can be consumed by a class decorated by @hparam.s.
378
379Raises:
380TypeError if `nested_class` is not decorated with @hparam.s.
381"""
382if not inspect.isclass(nested_class):383raise TypeError('nest() must be passed a class, not an instance.')384if not (attr.has(nested_class) and385getattr(nested_class, '__hparams_class__', False)):386raise TypeError('Nested hparams classes must use the @hparam.s decorator')387return attr.ib(388factory=nested_class, kw_only=True, metadata={_PREFIX_KEY: prefix})389
390
391def _serialize_value(value,392field_info):393"""Serializes a value to a string.394
395Lists are serialized by recursively calling this function on each of their
396elements. Enums use the enum value. Bools are cast to int. Strings that
397contain any of {,=[]"} are surrounded by double quotes. Everything is then
398cast using str().
399
400Args:
401value: The value to be serialized.
402field_info: The field info corresponding to `value`.
403
404Returns:
405The serialized value.
406"""
407if field_info.is_list:408list_value = value # type: _ValidListInstanceType # pytype: disable=annotation-type-mismatch409modified_field_info = copy.copy(field_info)410modified_field_info.is_list = False411# Manually string-ify the list, since default str(list) adds whitespace.412return ('[' + ','.join(413[str(_serialize_value(v, modified_field_info)) for v in list_value]) +414']')415scalar_value = value # type: _ValidScalarInstanceType # pytype: disable=annotation-type-mismatch416if issubclass(field_info.scalar_type, enum.Enum):417enum_value = scalar_value # type: enum.Enum418return str(enum_value.value)419elif field_info.scalar_type == bool:420bool_value = scalar_value # type: bool # pytype: disable=annotation-type-mismatch421# use 0/1 instead of True/False for more compact serialization.422return str(int(bool_value))423elif issubclass(field_info.scalar_type, six.string_types):424str_value = scalar_value # type: str425if any(char in str_value for char in ',=[]"'):426return f'"{str_value}"'427return str(value)428
429
430def _parse_serialized(431values,432hparams_map):433"""Parses hyperparameter values from a string into a python map.434
435`values` is a string containing comma-separated `name=value` pairs.
436For each pair, the value of the hyperparameter named `name` is set to
437`value`.
438
439If a hyperparameter name appears multiple times in `values`, a ValueError
440is raised (e.g. 'a=1,a=2').
441
442The `value` in `name=value` must follows the syntax according to the
443type of the parameter:
444
445* Scalar integer: A Python-parsable integer point value. E.g.: 1,
446100, -12.
447* Scalar float: A Python-parsable floating point value. E.g.: 1.0,
448-.54e89.
449* Boolean: True, False, true, false, 1, or 0.
450* Scalar string: A non-empty sequence of characters, possibly surrounded by
451double-quotes. E.g.: foo, bar_1, "foo,bar".
452* List: A comma separated list of scalar values of the parameter type
453enclosed in square brackets. E.g.: [1,2,3], [1.0,1e-12], [high,low].
454
455Args:
456values: Comma separated list of `name=value` pairs where 'value' must follow
457the syntax described above.
458hparams_map: A mapping from abbreviation to field info, detailing the
459expected type information for each known field.
460
461Returns:
462A python map mapping each name to either:
463* A scalar value.
464* A list of scalar values.
465
466Raises:
467ValueError: If there is a problem with input.
468* If `values` cannot be parsed.
469* If the same hyperparameter is assigned to twice.
470* If an unknown hyperparameter is assigned to.
471* If a list is assigned to a scalar hyperparameter.
472"""
473results_dictionary = {}474pos = 0475while pos < len(values):476m = _PARAM_RE.match(values, pos)477if not m:478raise ValueError(f'Malformed hyperparameter value: {values[pos:]}')479pos = m.end()480# Parse the values.481m_dict = m.groupdict()482name = m_dict['name']483if name not in hparams_map:484raise ValueError(f'Unknown hyperparameter: {name}.')485if name in results_dictionary:486raise ValueError(f'Duplicate assignment to hyperparameter \'{name}\'')487scalar_type = hparams_map[name].scalar_type488is_list = hparams_map[name].is_list489
490# Set up correct parsing function (depending on whether scalar_type is a491# bool)492def parse_bool(value):493if value in ['true', 'True']:494return True495elif value in ['false', 'False']:496return False497else:498try:499return bool(int(value))500except ValueError:501raise ValueError(502f'Could not parse {value} as a boolean for hyperparameter '503f'{name}.')504
505if scalar_type == bool:506parse = parse_bool507elif issubclass(scalar_type, enum.Enum):508enum_type = scalar_type # type: Type[enum.Enum]509enum_value_type = type(list(enum_type.__members__.values())[0].value)510enum_value_parser = (511parse_bool if enum_value_type == bool else enum_value_type)512parse = lambda x: enum_type(enum_value_parser(x))513else:514parse = scalar_type515
516# If a single value is provided517if m_dict['val'] is not None:518results_dictionary[name] = parse(m_dict['val'])519if is_list:520results_dictionary[name] = [results_dictionary[name]]521
522# A quoted string, so trim the quotes.523elif m_dict['strval'] is not None:524results_dictionary[name] = parse(m_dict['strval'][1:-1])525if is_list:526results_dictionary[name] = [results_dictionary[name]]527
528# If the assigned value is a list:529elif m_dict['vals'] is not None:530if not is_list:531raise ValueError(f'Expected single value for hyperparameter {name}, '532f'but found {m_dict["vals"]}')533list_str = m_dict['vals']534if list_str[0] == '[' and list_str[-1] == ']':535list_str = list_str[1:-1]536elements = list(csv.reader([list_str]))[0]537results_dictionary[name] = [parse(e.strip()) for e in elements]538
539else: # Not assigned a list or value540raise ValueError(f'Found empty value for hyperparameter {name}.')541
542return results_dictionary543
544
545def _build_hparams_map(hparams_class):546"""Constructs a map representing the metadata of an hparams class.547
548Contains the information needed to serialize, deserialize, and validate fields
549of the class.
550
551Includes information for fields in the class passed in, as well as any nested
552hparams class fields that are created using hparam.nest(), recursively.
553
554Args:
555hparams_class: A class that is decorated with @hparam.s.
556
557Returns:
558A mapping per field of abbreviation (used for serialization) to field
559metatdata.
560
561Raises:
562TypeError:
563* if `hparams_class` was not decorated with @hparam.s.
564* if a nested class was not decorated with @hparam.s.
565* if `hparams_class` has a field that was not created using @hparam.field
566or @hparam.nest.
567KeyError:
568* if two fields in `hparams_class` or any of its nested classes use the
569same abbreviation.
570"""
571if not attr.has(hparams_class):572raise TypeError(573'Inputs to _build_hparams_map should be classes decorated with '574'@hparam.s')575
576hparams_map = {}577for attribute in attr.fields(hparams_class.__class__):578path = [attribute.name]579default = attribute.default580# pytype: disable=invalid-annotation581factory_type = attr.Factory # type: Type[attr.Factory] # pytype: disable=annotation-type-mismatch582# pytype: enable=invalid-annotation583if isinstance(default, factory_type):584default = default.factory()585if attr.has(default): # Nested.586if '__hparams_map__' not in default.__dict__:587raise TypeError('Nested hparams classes must also be decorated with '588'@hparam.s.')589submap = default.__hparams_map__590prefix = ''591if _PREFIX_KEY in attribute.metadata:592prefix = attribute.metadata[_PREFIX_KEY] or ''593for key, value in submap.items():594abbrev = prefix + key595if abbrev in hparams_map:596raise KeyError(f'Abbrev {abbrev} is duplicated.')597updated = copy.copy(value)598updated.path = path + value.path599hparams_map[abbrev] = updated600else: # Leaf node.601if attribute.name == _SERIALIZED_ARG:602continue603if _ABBREV_KEY not in attribute.metadata:604raise AssertionError(605f'Could not find hparam metadata for field {attribute.name}. Did '606'you create a field without using hparam.field()?')607abbrev = attribute.metadata[_ABBREV_KEY]608if abbrev in hparams_map:609raise KeyError(f'Abbrev {abbrev} is duplicated.')610field_info = _FieldInfo(611path=path,612scalar_type=attribute.metadata[_SCALAR_TYPE_KEY],613is_list=attribute.metadata[_IS_LIST_KEY],614default_value=attribute.converter(default))615hparams_map[abbrev] = field_info616return hparams_map617
618
619def s(wrapped, *attrs_args,620**attrs_kwargs):621"""A class decorator for creating an hparams class.622
623The resulting class is based on `attr` under the covers, but this wrapper
624provides additional features, such as serialization and deserialization in a
625format that is compatible with (now defunct) tensorflow.HParams, runtime type
626checking, implicit casting where safe to do so (int->float, scalar->list).
627Unlike tensorflow.HParams, this supports hierarchical nesting of parameters
628for better organization, aliasing parameters to short abbreviations for
629compact serialization while maintaining code readability, and support for Enum
630values.
631
632Example usage:
633@hparam.s
634class MyNestedHParams:
635learning_rate: float = hparam.field(abbrev='lr', default=0.1)
636layer_sizes: List[int] = hparam.field(abbrev='ls', default=[256, 64, 32])
637
638@hparam.s
639class MyHParams:
640nested_params: MyNestedHParams = hparam.nest(MyNestedHParams)
641non_nested_param: int = hparam.field(abbrev='nn', default=0)
642
643Args:
644wrapped: The class being decorated. It should only contain fields created
645using `hparam.field()` and `hparam.nest()`.
646*attrs_args: Arguments passed on to `attr.s`.
647**attrs_kwargs: Keyword arguments passed on to `attr.s`.
648
649Returns:
650The class with the modifications needed to support the additional hparams
651features.
652"""
653
654def attrs_post_init(self):655self.__hparams_map__ = _build_hparams_map(self)656serialized = getattr(self, _SERIALIZED_ARG, '')657if serialized:658self.parse(serialized)659setattr(self, _SERIALIZED_ARG, '')660
661def setattr_impl(self, name, value):662ready = '__hparams_map__' in self.__dict__663# Don't mess with setattrs that are called by the attrs framework or during664# __init__.665if ready:666attribute = getattr(attr.fields(self.__class__), name)667if attribute and attribute.converter:668value = attribute.converter(value)669super(wrapped, self).__setattr__(name, value) # pytype: disable=wrong-arg-types670
671def serialize(self, readable=False, omit_defaults=False):672if readable:673d = attr.asdict(self, filter=lambda a, _: a.name != _SERIALIZED_ARG)674return json.dumps(d, default=str)675else:676serialized = ''677for key, field_info in self.__hparams_map__.items():678parent = self679for childname in field_info.path:680parent = getattr(parent, childname)681if not omit_defaults or parent != field_info.default_value:682value = _serialize_value(parent, field_info)683serialized += f'{key}={value},'684return serialized[:-1] # Prune trailing comma.685
686def parse(self, serialized):687parsed_fields = _parse_serialized(serialized, self.__hparams_map__)688for abbrev, value in parsed_fields.items():689field_info = self.__hparams_map__[abbrev]690parent = self691for i, childname in enumerate(field_info.path):692if i != len(field_info.path) - 1:693parent = getattr(parent, childname)694else:695try:696setattr(parent, childname, value)697except:698error_field = '.'.join(field_info.path)699raise RuntimeError(f'Error trying to assign value {value} to field '700f'{error_field}.')701
702wrapped.__hparams_class__ = True703setattr(704wrapped, _SERIALIZED_ARG,705attr.ib(706default='',707type=str,708kw_only=False,709validator=attr.validators.instance_of(six.string_types),710repr=False))711wrapped.__attrs_post_init__ = attrs_post_init712wrapped.__setattr__ = setattr_impl713wrapped.serialize = serialize714wrapped.parse = parse715wrapped = attr.s(wrapped, *attrs_args, **attrs_kwargs) # pytype: disable=wrong-arg-types # attr-stubs716return wrapped717