1# SPDX-License-Identifier: MIT

2

3"""

4 Commonly useful converters.

5"""

6

7

8 import typing

9

10 from ._compat import _AnnotationExtractor

11 from ._make import NOTHING, Factory, pipe

12

13

14__all__ = [

15 "default_if_none",

16 "optional",

17 "pipe",

18 "to_bool",

19]

20

21

22 def optional(converter):

23 """

24 A converter that allows an attribute to be optional. An optional attribute

25 is one which can be set to ``None``.

26

27 Type annotations will be inferred from the wrapped converter's, if it

28 has any.

29

30 :param callable converter: the converter that is used for non-``None``

31 values.

32

33 .. versionadded:: 17.1.0

34 """

35

36 def optional_converter(val):

37 if val is None:

38 return None

39 return converter(val)

40

41 xtr = _AnnotationExtractor(converter)

42

43 t = xtr.get_first_param_type()

44 if t:

45 optional_converter.__annotations__["val"] = typing.Optional[t]

46

47 rt = xtr.get_return_type()

48 if rt:

49 optional_converter.__annotations__["return"] = typing.Optional[rt]

50

51 return optional_converter

52

53

54 def default_if_none(default=NOTHING, factory=None):

55 """

56 A converter that allows to replace ``None`` values by *default* or the

57 result of *factory*.

58

59 :param default: Value to be used if ``None`` is passed. Passing an instance

60 of `attrs.Factory` is supported, however the ``takes_self`` option

61 is *not*.

62 :param callable factory: A callable that takes no parameters whose result

63 is used if ``None`` is passed.

64

65 :raises TypeError: If **neither** *default* or *factory* is passed.

66 :raises TypeError: If **both** *default* and *factory* are passed.

67 :raises ValueError: If an instance of `attrs.Factory` is passed with

68 ``takes_self=True``.

69

70 .. versionadded:: 18.2.0

71 """

72 if default is NOTHING and factory is None:

73 raise TypeError("Must pass either `default` or `factory`.")

74

75 if default is not NOTHING and factory is not None:

76 raise TypeError(

77 "Must pass either `default` or `factory` but not both."

78 )

79

80 if factory is not None:

81 default = Factory(factory)

82

83 if isinstance(default, Factory):

84 if default.takes_self:

85 raise ValueError(

86 "`takes_self` is not supported by default_if_none."

87 )

88

89 def default_if_none_converter(val):

90 if val is not None:

91 return val

92

93 return default.factory()

94

95 else:

96

97 def default_if_none_converter(val):

98 if val is not None:

99 return val

100

101 return default

102

103 return default_if_none_converter

104

105

106 def to_bool(val):

107 """

108 Convert "boolean" strings (e.g., from env. vars.) to real booleans.

109

110 Values mapping to :code:`True`:

111

112 - :code:`True`

113 - :code:`"true"` / :code:`"t"`

114 - :code:`"yes"` / :code:`"y"`

115 - :code:`"on"`

116 - :code:`"1"`

117 - :code:`1`

118

119 Values mapping to :code:`False`:

120

121 - :code:`False`

122 - :code:`"false"` / :code:`"f"`

123 - :code:`"no"` / :code:`"n"`

124 - :code:`"off"`

125 - :code:`"0"`

126 - :code:`0`

127

128 :raises ValueError: for any other value.

129

130 .. versionadded:: 21.3.0

131 """

132 if isinstance(val, str):

133 val = val.lower()

134 truthy = {True, "true", "t", "yes", "y", "on", "1", 1}

135 falsy = {False, "false", "f", "no", "n", "off", "0", 0}

136 try:

137 if val in truthy:

138 return True

139 if val in falsy:

140 return False

141 except TypeError:

142 # Raised when "val" is not hashable (e.g., lists)

143 pass

144 raise ValueError(f"Cannot convert value to bool: {val}")