sain.cfg
Runtime attr configuration.
Notes
Target OS must be one of the following:
linuxwin32|windowsdarwin|macosiosunix, which can be one of [linux, posix, macos, freebsd, openbsd].
Target architecture must be one of the following:
x86x86_64armarm64
Target Python implementation must be one of the following:
CPythonPyPyIronPythonJython
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 returnsFalse.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: