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
  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]:
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:
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: