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
@typing.runtime_checkable
class From(typing.Protocol[-T_co]):
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))
From(*args, **kwargs)
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)
@classmethod
def from_t(cls, value: -T_co) -> Self:
141    @classmethod
142    def from_t(cls, value: T_co) -> Self:
143        """Perform the conversion."""
144        raise NotImplementedError

Perform the conversion.

@typing.runtime_checkable
class TryFrom(typing.Protocol[-T_co, ~E]):
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 generic T 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)))
TryFrom(*args, **kwargs)
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)
@classmethod
def try_from(cls, value: -T_co) -> Union[sain.Ok[Self], sain.Err[~E]]:
181    @classmethod
182    def try_from(cls, value: T_co) -> Result[Self, E]:
183        """Perform the conversion."""
184        raise NotImplementedError

Perform the conversion.

@typing.runtime_checkable
class Into(typing.Protocol[+T_cov]):
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)
Into(*args, **kwargs)
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)
def into(self) -> +T_cov:
205    def into(self) -> T_cov:
206        """Perform the conversion."""
207        raise NotImplementedError

Perform the conversion.

@typing.runtime_checkable
class TryInto(typing.Protocol[~T, ~E]):
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 generic T 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))
TryInto(*args, **kwargs)
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)
def try_into(self) -> Union[sain.Ok[~T], sain.Err[~E]]:
239    def try_into(self) -> Result[T, E]:
240        """Perform the conversion."""
241        raise NotImplementedError

Perform the conversion.