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.
- Implement the
ToString
trait for explicitly converting objects to string.
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* Implement the `ToString` trait for explicitly converting objects to string. 40 41Example 42-------- 43```py 44@dataclass 45class Id(From[UUID], Into[int]): 46 id: int | float 47 48 @classmethod 49 def from_t(cls, value: UUID) -> Self: 50 # Keep in mind, this stores a 128 bit <long> integer. 51 return cls(int(value)) 52 53 def into(self) -> int: 54 return int(self.id) 55 56# Simply perform conversions. 57from_uuid = Id.from_t(uuid4()) 58into_int = from_uuid.into() 59``` 60 61For type conversions that may fail, two safe interfaces, `TryInto` and `TryFrom` exist which deal with that. 62 63This is useful when you are doing a type conversion that may trivially succeed but may also need special handling. 64 65```py 66@dataclass 67class Message(Into[bytes], TryFrom[bytes, None]): 68 content: str 69 id: int 70 71 def into(self) -> bytes: 72 return json.dumps(self.__dict__).encode() 73 74 @classmethod 75 def try_from(cls, value: bytes) -> Result[Self, None]: 76 try: 77 payload = json.loads(value) 78 return Ok(cls(content=payload['content'], id=payload['id'])) 79 except (json.decoder.JSONDecodeError, KeyError): 80 # Its rare to see a JSONDecodeError raised, but usually 81 # keys goes missing, which raises a KeyError. 82 return Err(None) 83 84message_bytes = b'{"content": "content", "id": 0}' 85 86match Message.try_from(message_bytes): 87 case Ok(message): 88 print("Successful conversion", message) 89 case Err(invalid_bytes): 90 print("Invalid bytes:", invalid_bytes) 91 92payload = Message(content='...', id=0) 93assert payload.into() == message_bytes 94``` 95""" 96 97from __future__ import annotations 98 99__slots__ = ("Into", "TryInto", "From", "TryFrom", "ToString") 100 101import typing 102 103if typing.TYPE_CHECKING: 104 from typing_extensions import Self 105 106 from sain import Result 107 108T = typing.TypeVar("T") 109T_co = typing.TypeVar("T_co", contravariant=True) 110T_cov = typing.TypeVar("T_cov", covariant=True) 111E = typing.TypeVar("E") 112 113 114@typing.runtime_checkable 115class From(typing.Protocol[T_co]): 116 """Used to do value-to-value conversions while consuming the input value. It is the reciprocal of Into. 117 118 As the Rust documentation says, One should always prefer implementing From over Into 119 because implementing From automatically provides one with an implementation of Into. 120 121 But there's no such thing in Python, as it's impossible to auto-impl `Into<T>` for all types 122 that impl `From<T>`. 123 124 So for the sake of simplicity, You should implement whichever interface you want deal with, 125 Or simply, implement both as the same time. 126 127 Example 128 ------- 129 ```py 130 @dataclass 131 class Id(From[str]): 132 value: int 133 134 @classmethod 135 def from_t(cls, value: str) -> Self: 136 return cls(value=int(value)) 137 138 ``` 139 """ 140 141 __slots__ = () 142 143 @classmethod 144 def from_t(cls, value: T_co) -> Self: 145 """Perform the conversion.""" 146 raise NotImplementedError 147 148 149@typing.runtime_checkable 150class TryFrom(typing.Protocol[T_co, E]): 151 """Simple and safe type conversions that may fail in a controlled way under some circumstances. 152 It is the reciprocal of `TryInto`. 153 154 It is useful to implement this when you know that the conversion may fail in some way. 155 156 Generic Implementations 157 ------------------- 158 This interface takes two type arguments, and return `Result<Self, E>` 159 160 * `T`: Which's the first generic `T` is the type that's being converted from. 161 * `E`: If the conversion fails in a way, this is what will return as the error. 162 * `Self`: Which's the instance of the class that is being converted into. 163 164 Example 165 ------- 166 ```py 167 @dataclass 168 class Id(TryFrom[str, str]): 169 value: int 170 171 @classmethod 172 def try_from(cls, value: str) -> Result[Self, str]: 173 if not value.isnumeric(): 174 # NaN 175 return Err(f"Couldn't convert: {value} to self") 176 # otherwise convert it to an Id instance. 177 return Ok(value=cls(int(value))) 178 ``` 179 """ 180 181 __slots__ = () 182 183 @classmethod 184 def try_from(cls, value: T_co) -> Result[Self, E]: 185 """Perform the conversion.""" 186 raise NotImplementedError 187 188 189@typing.runtime_checkable 190class Into(typing.Protocol[T_cov]): 191 """Conversion from `self`, which may or may not be expensive. 192 193 Example 194 ------- 195 ```py 196 @dataclass 197 class Id(Into[str]): 198 value: int 199 200 def into(self) -> str: 201 return str(self.value) 202 ``` 203 """ 204 205 __slots__ = () 206 207 def into(self) -> T_cov: 208 """Perform the conversion.""" 209 raise NotImplementedError 210 211 212@typing.runtime_checkable 213class TryInto(typing.Protocol[T, E]): 214 """An attempted conversion from `self`, which may or may not be expensive. 215 216 It is useful to implement this when you know that the conversion may fail in some way. 217 218 Generic Implementations 219 ------------------- 220 This interface takes two type arguments, and return `Result<T, E>` 221 222 * `T`: The first generic `T` is the type that's being converted into. 223 * `E`: If the conversion fails in a way, this is what will return as the error. 224 225 Example 226 ------- 227 ```py 228 @dataclass 229 class Id(TryInto[int, str]): 230 value: str 231 232 def try_into(self) -> Result[int, str]: 233 if not self.value.isnumeric(): 234 return Err(f"{self.value} is not a number...") 235 return Ok(int(self.value)) 236 ``` 237 """ 238 239 __slots__ = () 240 241 def try_into(self) -> Result[T, E]: 242 """Perform the conversion.""" 243 raise NotImplementedError 244 245 246@typing.runtime_checkable 247class ToString(typing.Protocol): 248 """A trait for explicitly converting a value to a `str`. 249 250 Example 251 ------- 252 ```py 253 class Value[T: bytes](ToString): 254 buffer: T 255 256 def to_string(self) -> str: 257 return self.buffer.decode("utf-8") 258 ``` 259 """ 260 261 __slots__ = () 262 263 def to_string(self) -> str: 264 """Converts the given value to a `str`. 265 266 Example 267 -------- 268 ```py 269 i = 5 # assume `int` implements `ToString` 270 five = "5" 271 assert five == i.to_string() 272 ``` 273 """ 274 raise NotImplementedError 275 276 def __str__(self) -> str: 277 return self.to_string()
115@typing.runtime_checkable 116class From(typing.Protocol[T_co]): 117 """Used to do value-to-value conversions while consuming the input value. It is the reciprocal of Into. 118 119 As the Rust documentation says, One should always prefer implementing From over Into 120 because implementing From automatically provides one with an implementation of Into. 121 122 But there's no such thing in Python, as it's impossible to auto-impl `Into<T>` for all types 123 that impl `From<T>`. 124 125 So for the sake of simplicity, You should implement whichever interface you want deal with, 126 Or simply, implement both as the same time. 127 128 Example 129 ------- 130 ```py 131 @dataclass 132 class Id(From[str]): 133 value: int 134 135 @classmethod 136 def from_t(cls, value: str) -> Self: 137 return cls(value=int(value)) 138 139 ``` 140 """ 141 142 __slots__ = () 143 144 @classmethod 145 def from_t(cls, value: T_co) -> Self: 146 """Perform the conversion.""" 147 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))
1430def _no_init_or_replace_init(self, *args, **kwargs): 1431 cls = type(self) 1432 1433 if cls._is_protocol: 1434 raise TypeError('Protocols cannot be instantiated') 1435 1436 # Already using a custom `__init__`. No need to calculate correct 1437 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1438 if cls.__init__ is not _no_init_or_replace_init: 1439 return 1440 1441 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1442 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1443 # searches for a proper new `__init__` in the MRO. The new `__init__` 1444 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1445 # instantiation of the protocol subclass will thus use the new 1446 # `__init__` and no longer call `_no_init_or_replace_init`. 1447 for base in cls.__mro__: 1448 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1449 if init is not _no_init_or_replace_init: 1450 cls.__init__ = init 1451 break 1452 else: 1453 # should not happen 1454 cls.__init__ = object.__init__ 1455 1456 cls.__init__(self, *args, **kwargs)
150@typing.runtime_checkable 151class TryFrom(typing.Protocol[T_co, E]): 152 """Simple and safe type conversions that may fail in a controlled way under some circumstances. 153 It is the reciprocal of `TryInto`. 154 155 It is useful to implement this when you know that the conversion may fail in some way. 156 157 Generic Implementations 158 ------------------- 159 This interface takes two type arguments, and return `Result<Self, E>` 160 161 * `T`: Which's the first generic `T` is the type that's being converted from. 162 * `E`: If the conversion fails in a way, this is what will return as the error. 163 * `Self`: Which's the instance of the class that is being converted into. 164 165 Example 166 ------- 167 ```py 168 @dataclass 169 class Id(TryFrom[str, str]): 170 value: int 171 172 @classmethod 173 def try_from(cls, value: str) -> Result[Self, str]: 174 if not value.isnumeric(): 175 # NaN 176 return Err(f"Couldn't convert: {value} to self") 177 # otherwise convert it to an Id instance. 178 return Ok(value=cls(int(value))) 179 ``` 180 """ 181 182 __slots__ = () 183 184 @classmethod 185 def try_from(cls, value: T_co) -> Result[Self, E]: 186 """Perform the conversion.""" 187 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)))
1430def _no_init_or_replace_init(self, *args, **kwargs): 1431 cls = type(self) 1432 1433 if cls._is_protocol: 1434 raise TypeError('Protocols cannot be instantiated') 1435 1436 # Already using a custom `__init__`. No need to calculate correct 1437 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1438 if cls.__init__ is not _no_init_or_replace_init: 1439 return 1440 1441 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1442 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1443 # searches for a proper new `__init__` in the MRO. The new `__init__` 1444 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1445 # instantiation of the protocol subclass will thus use the new 1446 # `__init__` and no longer call `_no_init_or_replace_init`. 1447 for base in cls.__mro__: 1448 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1449 if init is not _no_init_or_replace_init: 1450 cls.__init__ = init 1451 break 1452 else: 1453 # should not happen 1454 cls.__init__ = object.__init__ 1455 1456 cls.__init__(self, *args, **kwargs)
190@typing.runtime_checkable 191class Into(typing.Protocol[T_cov]): 192 """Conversion from `self`, which may or may not be expensive. 193 194 Example 195 ------- 196 ```py 197 @dataclass 198 class Id(Into[str]): 199 value: int 200 201 def into(self) -> str: 202 return str(self.value) 203 ``` 204 """ 205 206 __slots__ = () 207 208 def into(self) -> T_cov: 209 """Perform the conversion.""" 210 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)
1430def _no_init_or_replace_init(self, *args, **kwargs): 1431 cls = type(self) 1432 1433 if cls._is_protocol: 1434 raise TypeError('Protocols cannot be instantiated') 1435 1436 # Already using a custom `__init__`. No need to calculate correct 1437 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1438 if cls.__init__ is not _no_init_or_replace_init: 1439 return 1440 1441 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1442 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1443 # searches for a proper new `__init__` in the MRO. The new `__init__` 1444 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1445 # instantiation of the protocol subclass will thus use the new 1446 # `__init__` and no longer call `_no_init_or_replace_init`. 1447 for base in cls.__mro__: 1448 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1449 if init is not _no_init_or_replace_init: 1450 cls.__init__ = init 1451 break 1452 else: 1453 # should not happen 1454 cls.__init__ = object.__init__ 1455 1456 cls.__init__(self, *args, **kwargs)
213@typing.runtime_checkable 214class TryInto(typing.Protocol[T, E]): 215 """An attempted conversion from `self`, which may or may not be expensive. 216 217 It is useful to implement this when you know that the conversion may fail in some way. 218 219 Generic Implementations 220 ------------------- 221 This interface takes two type arguments, and return `Result<T, E>` 222 223 * `T`: The first generic `T` is the type that's being converted into. 224 * `E`: If the conversion fails in a way, this is what will return as the error. 225 226 Example 227 ------- 228 ```py 229 @dataclass 230 class Id(TryInto[int, str]): 231 value: str 232 233 def try_into(self) -> Result[int, str]: 234 if not self.value.isnumeric(): 235 return Err(f"{self.value} is not a number...") 236 return Ok(int(self.value)) 237 ``` 238 """ 239 240 __slots__ = () 241 242 def try_into(self) -> Result[T, E]: 243 """Perform the conversion.""" 244 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))
1430def _no_init_or_replace_init(self, *args, **kwargs): 1431 cls = type(self) 1432 1433 if cls._is_protocol: 1434 raise TypeError('Protocols cannot be instantiated') 1435 1436 # Already using a custom `__init__`. No need to calculate correct 1437 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1438 if cls.__init__ is not _no_init_or_replace_init: 1439 return 1440 1441 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1442 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1443 # searches for a proper new `__init__` in the MRO. The new `__init__` 1444 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1445 # instantiation of the protocol subclass will thus use the new 1446 # `__init__` and no longer call `_no_init_or_replace_init`. 1447 for base in cls.__mro__: 1448 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1449 if init is not _no_init_or_replace_init: 1450 cls.__init__ = init 1451 break 1452 else: 1453 # should not happen 1454 cls.__init__ = object.__init__ 1455 1456 cls.__init__(self, *args, **kwargs)
247@typing.runtime_checkable 248class ToString(typing.Protocol): 249 """A trait for explicitly converting a value to a `str`. 250 251 Example 252 ------- 253 ```py 254 class Value[T: bytes](ToString): 255 buffer: T 256 257 def to_string(self) -> str: 258 return self.buffer.decode("utf-8") 259 ``` 260 """ 261 262 __slots__ = () 263 264 def to_string(self) -> str: 265 """Converts the given value to a `str`. 266 267 Example 268 -------- 269 ```py 270 i = 5 # assume `int` implements `ToString` 271 five = "5" 272 assert five == i.to_string() 273 ``` 274 """ 275 raise NotImplementedError 276 277 def __str__(self) -> str: 278 return self.to_string()
A trait for explicitly converting a value to a str
.
Example
class Value[T: bytes](ToString):
buffer: T
def to_string(self) -> str:
return self.buffer.decode("utf-8")
1430def _no_init_or_replace_init(self, *args, **kwargs): 1431 cls = type(self) 1432 1433 if cls._is_protocol: 1434 raise TypeError('Protocols cannot be instantiated') 1435 1436 # Already using a custom `__init__`. No need to calculate correct 1437 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1438 if cls.__init__ is not _no_init_or_replace_init: 1439 return 1440 1441 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1442 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1443 # searches for a proper new `__init__` in the MRO. The new `__init__` 1444 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1445 # instantiation of the protocol subclass will thus use the new 1446 # `__init__` and no longer call `_no_init_or_replace_init`. 1447 for base in cls.__mro__: 1448 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1449 if init is not _no_init_or_replace_init: 1450 cls.__init__ = init 1451 break 1452 else: 1453 # should not happen 1454 cls.__init__ = object.__init__ 1455 1456 cls.__init__(self, *args, **kwargs)
264 def to_string(self) -> str: 265 """Converts the given value to a `str`. 266 267 Example 268 -------- 269 ```py 270 i = 5 # assume `int` implements `ToString` 271 five = "5" 272 assert five == i.to_string() 273 ``` 274 """ 275 raise NotImplementedError
Converts the given value to a str
.
Example
i = 5 # assume `int` implements `ToString`
five = "5"
assert five == i.to_string()