sain.cfg API documentation

# sain.cfg

Runtime attr configuration.

###### Notes

Target OS must be one of the following:

- `linux`
- `win32` | `windows`
- `darwin` | `macos`
- `ios`
- `unix`, which can be one of [linux, posix, macos, freebsd, openbsd].

Target architecture must be one of the following:

- `x86`
- `x86_64`
- `arm`
- `arm64`

Target Python implementation must be one of the following:

- `CPython`
- `PyPy`
- `IronPython`
- `Jython`

View Source

      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"""Runtime attr configuration.
     31
     32Notes
     33-----
     34Target OS must be one of the following:
     35* `linux`
     36* `win32` | `windows`
     37* `darwin` | `macos`
     38* `ios`
     39* `unix`, which can be one of [linux, posix, macos, freebsd, openbsd].
     40
     41Target architecture must be one of the following:
     42* `x86`
     43* `x86_64`
     44* `arm`
     45* `arm64`
     46
     47Target Python implementation must be one of the following:
     48* `CPython`
     49* `PyPy`
     50* `IronPython`
     51* `Jython`
     52"""
     53
     54from __future__ import annotations
     55
     56__all__ = ("cfg_attr", "cfg")
     57
     58import collections.abc as collections
     59import functools
     60import inspect
     61import os
     62import platform
     63import sys
     64import typing
     65
     66from sain.macros import rustc_diagnostic_item
     67
     68F = typing.TypeVar("F", bound=collections.Callable[..., object])
     69
     70System = typing.Literal["linux", "win32", "darwin", "macos", "unix", "windows", "ios"]
     71Arch = typing.Literal["x86", "x86_64", "arm", "arm64"]
     72Python = typing.Literal["CPython", "PyPy", "IronPython", "Jython"]
     73
     74if typing.TYPE_CHECKING:
     75    from typing_extensions import Self
     76
     77
     78_machine = platform.machine()
     79
     80
     81def _is_arm() -> bool:
     82    return "arm" in _machine
     83
     84
     85def _is_arm_64() -> bool:
     86    return "arm" in _machine and _machine.endswith("64")
     87
     88
     89def _is_x86_64() -> bool:
     90    return _machine == "AMD64" or _machine == "x86_64"
     91
     92
     93def _is_x86() -> bool:
     94    return _machine == "i386" or _machine == "x86"
     95
     96
     97def _py_version() -> tuple[int, int, int]:
     98    return sys.version_info[:3]
     99
    100
    101@rustc_diagnostic_item("cfg_attr")
    102def cfg_attr(
    103    *,
    104    target_os: System | None = None,
    105    python_version: tuple[int, ...] | None = None,
    106    target_arch: Arch | None = None,
    107    impl: Python | None = None,
    108) -> collections.Callable[[F], F]:
    109    """Conditional runtime object configuration based on passed arguments.
    110
    111    If the decorated object gets called and one of the attributes returns `False`,
    112    `RuntimeError` will be raised and the object will not run.
    113
    114    Example
    115    -------
    116    ```py
    117    import sain
    118
    119    @cfg_attr(target_os="windows")
    120    def windows_only():
    121        # Do stuff with Windows's API.
    122        ...
    123
    124    # Mut be PyPy Python implementation or `RuntimeError` will be raised
    125    # when creating the instance.
    126    @cfg_attr(impl="PyPy")
    127    class Zoo:
    128        @sain.cfg_attr(target_os="linux")
    129        def bark(self) -> None:
    130            ...
    131
    132    # An instance will not be created if raised.
    133    zoo = Zoo()
    134    # RuntimeError("class Zoo requires PyPy implementation")
    135    ```
    136
    137    Parameters
    138    ----------
    139    target_os : `str | None`
    140        The targeted operating system that's required for the object.
    141    python_version : `tuple[int, int, int] | None`
    142        The targeted Python version that's required for the object. Format must be `(3, ..., ...)`.
    143    target_arch : `str | None`
    144        The CPU targeted architecture that's required for the object.
    145    impl : `str | None`
    146        The Python implementation that's required for the object.
    147
    148    Raises
    149    ------
    150    `RuntimeError`
    151        This fails if any of the attributes returns `False`.
    152    `ValueError`
    153        If the passed Python implementation is unknown.
    154    """
    155
    156    def decorator(callback: F) -> F:
    157        @functools.wraps(callback)
    158        def wrapper(*args: typing.Any, **kwargs: typing.Any) -> F:
    159            checker = _AttrCheck(
    160                callback,
    161                target_os=target_os,
    162                python_version=python_version,
    163                target_arch=target_arch,
    164                impl=impl,
    165            )
    166            return checker(*args, **kwargs)
    167
    168        return typing.cast(F, wrapper)
    169
    170    return decorator
    171
    172
    173@rustc_diagnostic_item("cfg")
    174def cfg(
    175    target_os: System | None = None,
    176    python_version: tuple[int, ...] | None = None,
    177    target_arch: Arch | None = None,
    178    impl: Python | None = None,
    179) -> bool:
    180    """A function that will run the code only if all predicate attributes returns `True`.
    181
    182    The difference between this function and `cfg_attr` is that this function will not raise an exception.
    183    Instead it will return `False` if any of the attributes fails.
    184
    185    Example
    186    -------
    187    ```py
    188    import sain
    189
    190    if cfg(target_os="windows"):
    191        print("Windows")
    192    elif cfg(target_os="linux", target_arch="arm64"):
    193        print("Linux")
    194    else:
    195        print("Something else")
    196    ```
    197
    198    Parameters
    199    ----------
    200    target_os : `str | None`
    201        The targeted operating system that's required for the object to be executed.
    202    python_version : `tuple[int, ...] | None`
    203        The targeted Python version that's required for the object to be executed. Format must be `(3, ..., ...)`
    204    target_arch : `str | None`
    205        The CPU targeted architecture that's required for the object to be executed.
    206    impl : `str | None`
    207        The Python implementation that's required for the object to be executed.
    208
    209    Returns
    210    -------
    211    `bool`
    212        The condition that was checked.
    213    """
    214    checker = _AttrCheck(
    215        lambda: None,
    216        no_raise=True,
    217        target_os=target_os,
    218        python_version=python_version,
    219        target_arch=target_arch,
    220        impl=impl,
    221    )
    222    return checker.check_once()
    223
    224
    225@typing.final
    226class _AttrCheck(typing.Generic[F]):
    227    __slots__ = (
    228        "_target_os",
    229        "_callback",
    230        "_py_version",
    231        "_no_raise",
    232        "_target_arch",
    233        "_py_impl",
    234        "_debugger",
    235    )
    236
    237    def __init__(
    238        self,
    239        callback: F,
    240        target_os: System | None = None,
    241        python_version: tuple[int, ...] | None = None,
    242        target_arch: Arch | None = None,
    243        impl: Python | None = None,
    244        *,
    245        no_raise: bool = False,
    246    ) -> None:
    247        self._callback = callback
    248        self._target_os = target_os
    249        self._py_version = python_version
    250        self._target_arch = target_arch
    251        self._no_raise = no_raise
    252        self._py_impl = impl
    253        self._debugger = _Debug(callback, no_raise)
    254
    255    def __call__(self, *args: typing.Any, **kwds: typing.Any) -> F:
    256        self.check_once()
    257        return typing.cast(F, self._callback(*args, **kwds))
    258
    259    def check_once(self) -> bool:
    260        checks = (
    261            self._check_platform() if self._target_os is not None else True,
    262            self._check_py_version() if self._py_version is not None else True,
    263            self._check_target_arch() if self._target_arch is not None else True,
    264            self._check_py_impl() if self._py_impl is not None else True,
    265        )
    266        return all(checks)
    267
    268    def _check_target_arch(self) -> bool:
    269        match self._target_arch:
    270            case "arm":
    271                return _is_arm()
    272            case "arm64":
    273                return _is_arm_64()
    274            case "x86":
    275                return _is_x86()
    276            case "x86_64":
    277                return _is_x86_64()
    278            case _:
    279                raise ValueError(
    280                    f"Unknown target arch: {self._target_arch}. "
    281                    f"Valid options are: 'arm', 'arm64', 'x86', 'x86_64'."
    282                )
    283
    284    def _check_platform(self) -> bool:
    285        is_unix = os.name == "posix" or sys.platform in {"linux", "darwin", "macos"}
    286
    287        # If the target os is unix, then we assume that it's either linux or darwin.
    288        if self._target_os == "unix" and (
    289            is_unix or sys.platform in {"freebsd", "openbsd"}
    290        ):
    291            return True
    292
    293        # Alias to win32
    294        # Alias to win32
    295        if self._target_os == "windows" and sys.platform == "win32":
    296            return True
    297
    298        # Alias to darwin
    299        if self._target_os == "macos" and sys.platform == "darwin":
    300            return True
    301
    302        if sys.platform == self._target_os:
    303            return True
    304
    305        return (
    306            self._debugger.exception(RuntimeError)
    307            .message(f"requires {self._target_os} OS")
    308            .finish()
    309        )
    310
    311    def _check_py_version(self) -> bool:
    312        if self._py_version and self._py_version <= tuple(sys.version_info):
    313            return True
    314
    315        return (
    316            self._debugger.exception(RuntimeError)
    317            .message(f"requires Python >={self._py_version}")
    318            .and_then(f"But found {'.'.join(map(str, _py_version()))}")
    319            .finish()
    320        )
    321
    322    def _check_py_impl(self) -> bool:
    323        if platform.python_implementation() == self._py_impl:
    324            return True
    325
    326        return (
    327            self._debugger.exception(RuntimeError)
    328            .message(f"requires Python implementation {self._py_impl}")
    329            .finish()
    330        )
    331
    332
    333class _Debug(typing.Generic[F]):
    334    def __init__(
    335        self,
    336        callback: F,
    337        no_raise: bool,
    338        message: str | None = None,
    339        exception: type[BaseException] | None = None,
    340    ) -> None:
    341        self._callback = callback
    342        self._exception: type[BaseException] | None = exception
    343        self._no_raise = no_raise
    344        self._message = message
    345
    346    def exception(self, exc: type[BaseException]) -> Self:
    347        self._exception = exc
    348        return self
    349
    350    @functools.cached_property
    351    def _obj_type(self) -> str:
    352        if inspect.isfunction(self._callback):
    353            return "function"
    354        elif inspect.isclass(self._callback):
    355            return "class"
    356
    357        return "object"
    358
    359    def flag(self, cond: bool) -> None:
    360        self._no_raise = cond
    361
    362    def message(self, message: str) -> Self:
    363        """Set a message to be included in the exception that is getting raised."""
    364        fn_name = (
    365            "" if self._callback.__name__ == "<lambda>" else self._callback.__name__
    366        )
    367        self._message = f"{self._obj_type} {fn_name} {message}"
    368        return self
    369
    370    def and_then(self, message: str) -> Self:
    371        """Append an extra str to the end of this debugger's message."""
    372        assert self._message is not None
    373        self._message += ", " + message
    374        return self
    375
    376    def finish(self) -> bool:
    377        """Finish the result, Either returning a bool or raising an exception."""
    378        if self._no_raise:
    379            return False
    380
    381        assert self._exception is not None
    382        raise self._exception(self._message) from None

@rustc_diagnostic_item('cfg_attr')
def cfg_attr( *, target_os: System | None = Ellipsis, python_version: tuple[int, int, int] | None = Ellipsis, target_arch: Arch | None = Ellipsis, impl: Python | None = Ellipsis) -> Callable[[F], F]: View Source

    102@rustc_diagnostic_item("cfg_attr")
    103def cfg_attr(
    104    *,
    105    target_os: System | None = None,
    106    python_version: tuple[int, ...] | None = None,
    107    target_arch: Arch | None = None,
    108    impl: Python | None = None,
    109) -> collections.Callable[[F], F]:
    110    """Conditional runtime object configuration based on passed arguments.
    111
    112    If the decorated object gets called and one of the attributes returns `False`,
    113    `RuntimeError` will be raised and the object will not run.
    114
    115    Example
    116    -------
    117    ```py
    118    import sain
    119
    120    @cfg_attr(target_os="windows")
    121    def windows_only():
    122        # Do stuff with Windows's API.
    123        ...
    124
    125    # Mut be PyPy Python implementation or `RuntimeError` will be raised
    126    # when creating the instance.
    127    @cfg_attr(impl="PyPy")
    128    class Zoo:
    129        @sain.cfg_attr(target_os="linux")
    130        def bark(self) -> None:
    131            ...
    132
    133    # An instance will not be created if raised.
    134    zoo = Zoo()
    135    # RuntimeError("class Zoo requires PyPy implementation")
    136    ```
    137
    138    Parameters
    139    ----------
    140    target_os : `str | None`
    141        The targeted operating system that's required for the object.
    142    python_version : `tuple[int, int, int] | None`
    143        The targeted Python version that's required for the object. Format must be `(3, ..., ...)`.
    144    target_arch : `str | None`
    145        The CPU targeted architecture that's required for the object.
    146    impl : `str | None`
    147        The Python implementation that's required for the object.
    148
    149    Raises
    150    ------
    151    `RuntimeError`
    152        This fails if any of the attributes returns `False`.
    153    `ValueError`
    154        If the passed Python implementation is unknown.
    155    """
    156
    157    def decorator(callback: F) -> F:
    158        @functools.wraps(callback)
    159        def wrapper(*args: typing.Any, **kwargs: typing.Any) -> F:
    160            checker = _AttrCheck(
    161                callback,
    162                target_os=target_os,
    163                python_version=python_version,
    164                target_arch=target_arch,
    165                impl=impl,
    166            )
    167            return checker(*args, **kwargs)
    168
    169        return typing.cast(F, wrapper)
    170
    171    return decorator

Conditional runtime object configuration based on passed arguments.

If the decorated object gets called and one of the attributes returns `False`, `RuntimeError` will be raised and the object will not run.

###### Example

    import sain

    @cfg_attr(target_os="windows")
    def windows_only():
        # Do stuff with Windows's API.
        ...

    # Mut be PyPy Python implementation or `RuntimeError` will be raised
    # when creating the instance.
    @cfg_attr(impl="PyPy")
    class Zoo:
        @sain.cfg_attr(target_os="linux")
        def bark(self) -> None:
            ...

    # An instance will not be created if raised.
    zoo = Zoo()
    # RuntimeError("class Zoo requires PyPy implementation")

###### Parameters

- target_os (`str | None`): The targeted operating system that's required for the object.
- python_version (`tuple[int, int, int] | None`): The targeted Python version that's required for the object. Format must be `(3, ..., ...)`.
- target_arch (`str | None`): The CPU targeted architecture that's required for the object.
- impl (`str | None`): The Python implementation that's required for the object.

###### Raises

- `RuntimeError`: This fails if any of the attributes returns `False`.
- `ValueError`: If the passed Python implementation is unknown.
- # Implementations
- **This function implements cfg_attr:

@rustc_diagnostic_item('cfg')
def cfg( *, target_os: System | None = Ellipsis, python_version: tuple[int, int, int] | None = Ellipsis, target_arch: Arch | None = Ellipsis, impl: Python | None = Ellipsis) -> bool: View Source

    174@rustc_diagnostic_item("cfg")
    175def cfg(
    176    target_os: System | None = None,
    177    python_version: tuple[int, ...] | None = None,
    178    target_arch: Arch | None = None,
    179    impl: Python | None = None,
    180) -> bool:
    181    """A function that will run the code only if all predicate attributes returns `True`.
    182
    183    The difference between this function and `cfg_attr` is that this function will not raise an exception.
    184    Instead it will return `False` if any of the attributes fails.
    185
    186    Example
    187    -------
    188    ```py
    189    import sain
    190
    191    if cfg(target_os="windows"):
    192        print("Windows")
    193    elif cfg(target_os="linux", target_arch="arm64"):
    194        print("Linux")
    195    else:
    196        print("Something else")
    197    ```
    198
    199    Parameters
    200    ----------
    201    target_os : `str | None`
    202        The targeted operating system that's required for the object to be executed.
    203    python_version : `tuple[int, ...] | None`
    204        The targeted Python version that's required for the object to be executed. Format must be `(3, ..., ...)`
    205    target_arch : `str | None`
    206        The CPU targeted architecture that's required for the object to be executed.
    207    impl : `str | None`
    208        The Python implementation that's required for the object to be executed.
    209
    210    Returns
    211    -------
    212    `bool`
    213        The condition that was checked.
    214    """
    215    checker = _AttrCheck(
    216        lambda: None,
    217        no_raise=True,
    218        target_os=target_os,
    219        python_version=python_version,
    220        target_arch=target_arch,
    221        impl=impl,
    222    )
    223    return checker.check_once()

A function that will run the code only if all predicate attributes returns `True`.

The difference between this function and `cfg_attr` is that this function will not raise an exception. Instead it will return `False` if any of the attributes fails.

###### Example

    import sain

    if cfg(target_os="windows"):
        print("Windows")
    elif cfg(target_os="linux", target_arch="arm64"):
        print("Linux")
    else:
        print("Something else")

###### Parameters

- target_os (`str | None`): The targeted operating system that's required for the object to be executed.
- python_version (`tuple[int, ...] | None`): The targeted Python version that's required for the object to be executed. Format must be `(3, ..., ...)`
- target_arch (`str | None`): The CPU targeted architecture that's required for the object to be executed.
- impl (`str | None`): The Python implementation that's required for the object to be executed.

###### Returns

- `bool`: The condition that was checked.
- # Implementations
- **This function implements cfg: