sain.convert
Protocols for conversions between types.
The protocols in this module provide a way to convert from one type to another type. Each trait serves a different purpose:
- Implement the From trait for consuming value-to-value conversions
- Implement the Into trait for consuming value-to-value conversions to types outside the current crate
- The TryFrom and TryInto traits behave like From and Into, but should be implemented when the conversion can fail.
Example
@dataclass
class Id(From[UUID], Into[int]):
id: int | float
@classmethod
def from_t(cls, value: UUID) -> Self:
# Keep in mind, this stores a 128 bit <long> integer.
return cls(int(value))
def into(self) -> int:
return int(self.id)
# Simply perform conversions.
from_uuid = Id.from_t(uuid4())
into_int = from_uuid.into()
For type conversions that may fail, two safe interfaces, TryInto
and TryFrom
exist which deal with that.
This is useful when you are doing a type conversion that may trivially succeed but may also need special handling.
@dataclass
class Message(Into[bytes], TryFrom[bytes, None]):
content: str
id: int
def into(self) -> bytes:
return json.dumps(self.__dict__).encode()
@classmethod
def try_from(cls, value: bytes) -> Result[Self, None]:
try:
payload = json.loads(value)
return Ok(cls(content=payload['content'], id=payload['id']))
except (json.decoder.JSONDecodeError, KeyError):
# Its rare to see a JSONDecodeError raised, but usually
# keys goes missing, which raises a KeyError.
return Err(None)
message_bytes = b'{"content": "content", "id": 0}'
match Message.try_from(message_bytes):
case Ok(message):
print("Successful conversion", message)
case Err(invalid_bytes):
print("Invalid bytes:", invalid_bytes)
payload = Message(content='...', id=0)
assert payload.into() == message_bytes
1# BSD 3-Clause License 2# 3# Copyright (c) 2022-Present, nxtlo 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 9# * Redistributions of source code must retain the above copyright notice, this 10# list of conditions and the following disclaimer. 11# 12# * Redistributions in binary form must reproduce the above copyright notice, 13# this list of conditions and the following disclaimer in the documentation 14# and/or other materials provided with the distribution. 15# 16# * Neither the name of the copyright holder nor the names of its 17# contributors may be used to endorse or promote products derived from 18# this software without specific prior written permission. 19# 20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 31""" 32Protocols for conversions between types. 33 34The protocols in this module provide a way to convert from one type to another type. Each trait serves a different purpose: 35 36* Implement the From trait for consuming value-to-value conversions 37* Implement the Into trait for consuming value-to-value conversions to types outside the current crate 38* The TryFrom and TryInto traits behave like From and Into, but should be implemented when the conversion can fail. 39 40Example 41-------- 42```py 43@dataclass 44class Id(From[UUID], Into[int]): 45 id: int | float 46 47 @classmethod 48 def from_t(cls, value: UUID) -> Self: 49 # Keep in mind, this stores a 128 bit <long> integer. 50 return cls(int(value)) 51 52 def into(self) -> int: 53 return int(self.id) 54 55# Simply perform conversions. 56from_uuid = Id.from_t(uuid4()) 57into_int = from_uuid.into() 58``` 59 60For type conversions that may fail, two safe interfaces, `TryInto` and `TryFrom` exist which deal with that. 61 62This is useful when you are doing a type conversion that may trivially succeed but may also need special handling. 63 64```py 65@dataclass 66class Message(Into[bytes], TryFrom[bytes, None]): 67 content: str 68 id: int 69 70 def into(self) -> bytes: 71 return json.dumps(self.__dict__).encode() 72 73 @classmethod 74 def try_from(cls, value: bytes) -> Result[Self, None]: 75 try: 76 payload = json.loads(value) 77 return Ok(cls(content=payload['content'], id=payload['id'])) 78 except (json.decoder.JSONDecodeError, KeyError): 79 # Its rare to see a JSONDecodeError raised, but usually 80 # keys goes missing, which raises a KeyError. 81 return Err(None) 82 83message_bytes = b'{"content": "content", "id": 0}' 84 85match Message.try_from(message_bytes): 86 case Ok(message): 87 print("Successful conversion", message) 88 case Err(invalid_bytes): 89 print("Invalid bytes:", invalid_bytes) 90 91payload = Message(content='...', id=0) 92assert payload.into() == message_bytes 93``` 94""" 95 96from __future__ import annotations 97 98import typing 99 100if typing.TYPE_CHECKING: 101 from typing_extensions import Self 102 103 from sain import Result 104 105T = typing.TypeVar("T") 106T_co = typing.TypeVar("T_co", contravariant=True) 107T_cov = typing.TypeVar("T_cov", covariant=True) 108E = typing.TypeVar("E") 109 110 111@typing.runtime_checkable 112class From(typing.Protocol[T_co]): 113 """Used to do value-to-value conversions while consuming the input value. It is the reciprocal of Into. 114 115 As the Rust documentation says, One should always prefer implementing From over Into 116 because implementing From automatically provides one with an implementation of Into. 117 118 But there's no such thing in Python, as it's impossible to auto-impl `Into<T>` for all types 119 that impl `From<T>`. 120 121 So for the sake of simplicity, You should implement whichever interface you want deal with, 122 Or simply, implement both as the same time. 123 124 Example 125 ------- 126 ```py 127 @dataclass 128 class Id(From[str]): 129 value: int 130 131 @classmethod 132 def from_t(cls, value: str) -> Self: 133 return cls(value=int(value)) 134 135 ``` 136 """ 137 138 __slots__ = () 139 140 @classmethod 141 def from_t(cls, value: T_co) -> Self: 142 """Perform the conversion.""" 143 raise NotImplementedError 144 145 146@typing.runtime_checkable 147class TryFrom(typing.Protocol[T_co, E]): 148 """Simple and safe type conversions that may fail in a controlled way under some circumstances. 149 It is the reciprocal of `TryInto`. 150 151 It is useful to implement this when you know that the conversion may fail in some way. 152 153 Generic Implementations 154 ------------------- 155 This interface takes two type arguments, and return `Result<Self, E>` 156 157 * `T`: Which's the first generic `T` is the type that's being converted from. 158 * `E`: If the conversion fails in a way, this is what will return as the error. 159 * `Self`: Which's the instance of the class that is being converted into. 160 161 Example 162 ------- 163 ```py 164 @dataclass 165 class Id(TryFrom[str, str]): 166 value: int 167 168 @classmethod 169 def try_from(cls, value: str) -> Result[Self, str]: 170 if not value.isnumeric(): 171 # NaN 172 return Err(f"Couldn't convert: {value} to self") 173 # otherwise convert it to an Id instance. 174 return Ok(value=cls(int(value))) 175 ``` 176 """ 177 178 __slots__ = () 179 180 @classmethod 181 def try_from(cls, value: T_co) -> Result[Self, E]: 182 """Perform the conversion.""" 183 raise NotImplementedError 184 185 186@typing.runtime_checkable 187class Into(typing.Protocol[T_cov]): 188 """Conversion from `self`, which may or may not be expensive. 189 190 Example 191 ------- 192 ```py 193 @dataclass 194 class Id(Into[str]): 195 value: int 196 197 def into(self) -> str: 198 return str(self.value) 199 ``` 200 """ 201 202 __slots__ = () 203 204 def into(self) -> T_cov: 205 """Perform the conversion.""" 206 raise NotImplementedError 207 208 209@typing.runtime_checkable 210class TryInto(typing.Protocol[T, E]): 211 """An attempted conversion from `self`, which may or may not be expensive. 212 213 It is useful to implement this when you know that the conversion may fail in some way. 214 215 Generic Implementations 216 ------------------- 217 This interface takes two type arguments, and return `Result<T, E>` 218 219 * `T`: The first generic `T` is the type that's being converted into. 220 * `E`: If the conversion fails in a way, this is what will return as the error. 221 222 Example 223 ------- 224 ```py 225 @dataclass 226 class Id(TryInto[int, str]): 227 value: str 228 229 def try_into(self) -> Result[int, str]: 230 if not self.value.isnumeric(): 231 return Err(f"{self.value} is not a number...") 232 return Ok(int(self.value)) 233 ``` 234 """ 235 236 __slots__ = () 237 238 def try_into(self) -> Result[T, E]: 239 """Perform the conversion.""" 240 raise NotImplementedError
112@typing.runtime_checkable 113class From(typing.Protocol[T_co]): 114 """Used to do value-to-value conversions while consuming the input value. It is the reciprocal of Into. 115 116 As the Rust documentation says, One should always prefer implementing From over Into 117 because implementing From automatically provides one with an implementation of Into. 118 119 But there's no such thing in Python, as it's impossible to auto-impl `Into<T>` for all types 120 that impl `From<T>`. 121 122 So for the sake of simplicity, You should implement whichever interface you want deal with, 123 Or simply, implement both as the same time. 124 125 Example 126 ------- 127 ```py 128 @dataclass 129 class Id(From[str]): 130 value: int 131 132 @classmethod 133 def from_t(cls, value: str) -> Self: 134 return cls(value=int(value)) 135 136 ``` 137 """ 138 139 __slots__ = () 140 141 @classmethod 142 def from_t(cls, value: T_co) -> Self: 143 """Perform the conversion.""" 144 raise NotImplementedError
Used to do value-to-value conversions while consuming the input value. It is the reciprocal of Into.
As the Rust documentation says, One should always prefer implementing From over Into because implementing From automatically provides one with an implementation of Into.
But there's no such thing in Python, as it's impossible to auto-impl Into<T>
for all types
that impl From<T>
.
So for the sake of simplicity, You should implement whichever interface you want deal with, Or simply, implement both as the same time.
Example
@dataclass
class Id(From[str]):
value: int
@classmethod
def from_t(cls, value: str) -> Self:
return cls(value=int(value))
1739def _no_init_or_replace_init(self, *args, **kwargs): 1740 cls = type(self) 1741 1742 if cls._is_protocol: 1743 raise TypeError('Protocols cannot be instantiated') 1744 1745 # Already using a custom `__init__`. No need to calculate correct 1746 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1747 if cls.__init__ is not _no_init_or_replace_init: 1748 return 1749 1750 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1751 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1752 # searches for a proper new `__init__` in the MRO. The new `__init__` 1753 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1754 # instantiation of the protocol subclass will thus use the new 1755 # `__init__` and no longer call `_no_init_or_replace_init`. 1756 for base in cls.__mro__: 1757 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1758 if init is not _no_init_or_replace_init: 1759 cls.__init__ = init 1760 break 1761 else: 1762 # should not happen 1763 cls.__init__ = object.__init__ 1764 1765 cls.__init__(self, *args, **kwargs)
147@typing.runtime_checkable 148class TryFrom(typing.Protocol[T_co, E]): 149 """Simple and safe type conversions that may fail in a controlled way under some circumstances. 150 It is the reciprocal of `TryInto`. 151 152 It is useful to implement this when you know that the conversion may fail in some way. 153 154 Generic Implementations 155 ------------------- 156 This interface takes two type arguments, and return `Result<Self, E>` 157 158 * `T`: Which's the first generic `T` is the type that's being converted from. 159 * `E`: If the conversion fails in a way, this is what will return as the error. 160 * `Self`: Which's the instance of the class that is being converted into. 161 162 Example 163 ------- 164 ```py 165 @dataclass 166 class Id(TryFrom[str, str]): 167 value: int 168 169 @classmethod 170 def try_from(cls, value: str) -> Result[Self, str]: 171 if not value.isnumeric(): 172 # NaN 173 return Err(f"Couldn't convert: {value} to self") 174 # otherwise convert it to an Id instance. 175 return Ok(value=cls(int(value))) 176 ``` 177 """ 178 179 __slots__ = () 180 181 @classmethod 182 def try_from(cls, value: T_co) -> Result[Self, E]: 183 """Perform the conversion.""" 184 raise NotImplementedError
Simple and safe type conversions that may fail in a controlled way under some circumstances.
It is the reciprocal of TryInto
.
It is useful to implement this when you know that the conversion may fail in some way.
Generic Implementations
This interface takes two type arguments, and return Result<Self, E>
T
: Which's the first genericT
is the type that's being converted from.E
: If the conversion fails in a way, this is what will return as the error.Self
: Which's the instance of the class that is being converted into.
Example
@dataclass
class Id(TryFrom[str, str]):
value: int
@classmethod
def try_from(cls, value: str) -> Result[Self, str]:
if not value.isnumeric():
# NaN
return Err(f"Couldn't convert: {value} to self")
# otherwise convert it to an Id instance.
return Ok(value=cls(int(value)))
1739def _no_init_or_replace_init(self, *args, **kwargs): 1740 cls = type(self) 1741 1742 if cls._is_protocol: 1743 raise TypeError('Protocols cannot be instantiated') 1744 1745 # Already using a custom `__init__`. No need to calculate correct 1746 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1747 if cls.__init__ is not _no_init_or_replace_init: 1748 return 1749 1750 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1751 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1752 # searches for a proper new `__init__` in the MRO. The new `__init__` 1753 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1754 # instantiation of the protocol subclass will thus use the new 1755 # `__init__` and no longer call `_no_init_or_replace_init`. 1756 for base in cls.__mro__: 1757 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1758 if init is not _no_init_or_replace_init: 1759 cls.__init__ = init 1760 break 1761 else: 1762 # should not happen 1763 cls.__init__ = object.__init__ 1764 1765 cls.__init__(self, *args, **kwargs)
187@typing.runtime_checkable 188class Into(typing.Protocol[T_cov]): 189 """Conversion from `self`, which may or may not be expensive. 190 191 Example 192 ------- 193 ```py 194 @dataclass 195 class Id(Into[str]): 196 value: int 197 198 def into(self) -> str: 199 return str(self.value) 200 ``` 201 """ 202 203 __slots__ = () 204 205 def into(self) -> T_cov: 206 """Perform the conversion.""" 207 raise NotImplementedError
Conversion from self
, which may or may not be expensive.
Example
@dataclass
class Id(Into[str]):
value: int
def into(self) -> str:
return str(self.value)
1739def _no_init_or_replace_init(self, *args, **kwargs): 1740 cls = type(self) 1741 1742 if cls._is_protocol: 1743 raise TypeError('Protocols cannot be instantiated') 1744 1745 # Already using a custom `__init__`. No need to calculate correct 1746 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1747 if cls.__init__ is not _no_init_or_replace_init: 1748 return 1749 1750 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1751 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1752 # searches for a proper new `__init__` in the MRO. The new `__init__` 1753 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1754 # instantiation of the protocol subclass will thus use the new 1755 # `__init__` and no longer call `_no_init_or_replace_init`. 1756 for base in cls.__mro__: 1757 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1758 if init is not _no_init_or_replace_init: 1759 cls.__init__ = init 1760 break 1761 else: 1762 # should not happen 1763 cls.__init__ = object.__init__ 1764 1765 cls.__init__(self, *args, **kwargs)
210@typing.runtime_checkable 211class TryInto(typing.Protocol[T, E]): 212 """An attempted conversion from `self`, which may or may not be expensive. 213 214 It is useful to implement this when you know that the conversion may fail in some way. 215 216 Generic Implementations 217 ------------------- 218 This interface takes two type arguments, and return `Result<T, E>` 219 220 * `T`: The first generic `T` is the type that's being converted into. 221 * `E`: If the conversion fails in a way, this is what will return as the error. 222 223 Example 224 ------- 225 ```py 226 @dataclass 227 class Id(TryInto[int, str]): 228 value: str 229 230 def try_into(self) -> Result[int, str]: 231 if not self.value.isnumeric(): 232 return Err(f"{self.value} is not a number...") 233 return Ok(int(self.value)) 234 ``` 235 """ 236 237 __slots__ = () 238 239 def try_into(self) -> Result[T, E]: 240 """Perform the conversion.""" 241 raise NotImplementedError
An attempted conversion from self
, which may or may not be expensive.
It is useful to implement this when you know that the conversion may fail in some way.
Generic Implementations
This interface takes two type arguments, and return Result<T, E>
T
: The first genericT
is the type that's being converted into.E
: If the conversion fails in a way, this is what will return as the error.
Example
@dataclass
class Id(TryInto[int, str]):
value: str
def try_into(self) -> Result[int, str]:
if not self.value.isnumeric():
return Err(f"{self.value} is not a number...")
return Ok(int(self.value))
1739def _no_init_or_replace_init(self, *args, **kwargs): 1740 cls = type(self) 1741 1742 if cls._is_protocol: 1743 raise TypeError('Protocols cannot be instantiated') 1744 1745 # Already using a custom `__init__`. No need to calculate correct 1746 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1747 if cls.__init__ is not _no_init_or_replace_init: 1748 return 1749 1750 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1751 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1752 # searches for a proper new `__init__` in the MRO. The new `__init__` 1753 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1754 # instantiation of the protocol subclass will thus use the new 1755 # `__init__` and no longer call `_no_init_or_replace_init`. 1756 for base in cls.__mro__: 1757 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1758 if init is not _no_init_or_replace_init: 1759 cls.__init__ = init 1760 break 1761 else: 1762 # should not happen 1763 cls.__init__ = object.__init__ 1764 1765 cls.__init__(self, *args, **kwargs)