sain.macros
A module that contains useful functions and decorators for marking objects.
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"""A module that contains useful functions and decorators for marking objects.""" 32 33from __future__ import annotations 34 35__all__ = ( 36 "deprecated", 37 "unimplemented", 38 "todo", 39 "doc", 40 "assert_eq", 41 "assert_ne", 42 "include_str", 43 "include_bytes", 44 "safe", 45 "unsafe", 46 "rustc_diagnostic_item", 47 "RustItem", 48) 49 50import functools 51import inspect 52import sys 53import typing 54import warnings 55 56if typing.TYPE_CHECKING: 57 from typing_extensions import LiteralString 58 59 T = typing.TypeVar("T", covariant=True) 60 import collections.abc as collections 61 62 import _typeshed 63 64 P = typing.ParamSpec("P") 65 U = typing.TypeVar("U") 66 Read = _typeshed.FileDescriptorOrPath 67 # fmt: off 68 RustItem = typing.Literal[ 69 # mem 70 "MaybeUninit", 71 # option 72 "Option", "Some", "None", 73 # result 74 "Result", "Ok", "Err", 75 # macros 76 "unimplemented", "todo", 77 "deprecated", "doc", 78 "cfg", "cfg_attr", 79 "assert_eq", "assert_ne", 80 "include_bytes", "include_str", 81 # std::iter::* 82 "Iterator", "Iter", "empty", 83 "once", "repeat", "into_iter", 84 # errors 85 "Error", "catch_unwind", 86 # sync 87 "Lazy", 88 "Once", 89 # convert 90 "From", "TryFrom", 91 "Into", "TryInto", 92 "convert_identity", 93 # default 94 "Default", "default_fn", 95 # std::collections::* 96 "HashMap", 97 "Vec", "vec!", 98 # alloc 99 "String", "ToString", 100 # keywords 101 "unsafe", 102 # primitives 103 "&[u8]", 104 "&mut [u8]", 105 # time 106 "Duration" 107 ] 108 """An array of all the Rust items that can be marked as `rustc_diagnostic_item`.""" 109 # fmt: on 110 111_MAP_TO_PATH: dict[RustItem, LiteralString] = { 112 # mem 113 "MaybeUninit": "std/mem/union.MaybeUninit.html", 114 # option 115 "Option": "std/option/enum.Option.html", 116 "Some": "std/option/enum.Option.html#variant.Some", 117 "None": "std/option/enum.Option.html#variant.None", 118 # result, 119 "Result": "std/result/enum.Result.html", 120 "Ok": "std/result/enum.Result.html#variant.Ok", 121 "Err": "std/result/enum.Result.html#variant.Err", 122 # macros 123 "unimplemented": "std/macro.unimplemented.html", 124 "todo": "std/macro.todo.html", 125 "deprecated": "reference/attributes/diagnostics.html#the-deprecated-attribute", 126 "cfg": "std/macro.cfg.html", 127 "cfg_attr": "reference/conditional-compilation.html#the-cfg_attr-attribute", 128 "doc": "rustdoc/write-documentation/the-doc-attribute.html", 129 "assert_eq": "std/macro.assert_eq.html", 130 "assert_ne": "std/macro.assert_ne.html", 131 "include_bytes": "std/macro.include_bytes.html", 132 "include_str": "std/macro.include_str.html", 133 # "iter" 134 "Iterator": "std/iter/trait.Iterator.html", 135 "Iter": "std/slice/struct.Iter.html", 136 "empty": "std/iter/fn.empty.html", 137 "repeat": "std/iter/fn.repeat.html", 138 "once": "std/iter/fn.once.html", 139 "into_iter": "std/iter/trait.IntoIterator.html#tymethod.into_iter", 140 # errors 141 "Error": "std/error/trait.Error.html", 142 "catch_unwind": "std/panic/fn.catch_unwind.html", 143 # sync 144 "Lazy": "std/sync/struct.LazyLock.html", 145 "Once": "std/sync/struct.OnceLock.html", 146 # convert 147 "From": "std/convert/trait.From.html", 148 "TryFrom": "std/convert/trait.TryFrom.html", 149 "Into": "std/convert/trait.Into.html", 150 "TryInto": "std/convert/trait.TryInto.html", 151 "convert_identity": "std/convert/fn.identity.html", 152 # default 153 "Default": "std/default/trait.Default.html", 154 "default_fn": "std/default/trait.Default.html#tymethod.default", 155 # collections 156 "HashMap": "std/collections/struct.HashMap.html", 157 "Vec": "std/vec/struct.Vec.html", 158 "vec!": "std/macro.vec.html", 159 # alloc 160 "String": "alloc/string/struct.String.html", 161 "ToString": "alloc/string/trait.ToString.html", 162 # keywords 163 "unsafe": "std/keyword.unsafe.html", 164 # primitives 165 "&[u8]": "std/primitive.slice.html", 166 "&mut [u8]": "std/primitive.slice.html", 167 # time 168 "Duration": "std/time/struct.Duration.html", 169} 170 171_RUSTC_DOCS = "https://doc.rust-lang.org" 172 173 174def _warn(msg: str, stacklevel: int = 2, warn_ty: type[Warning] = Warning) -> None: 175 warnings.warn(message=msg, stacklevel=stacklevel, category=warn_ty) 176 177 178@functools.cache 179def _obj_type(obj: type[typing.Any]) -> typing.Literal["class", "function"]: 180 return "class" if inspect.isclass(obj) else "function" 181 182 183def rustc_diagnostic_item(item: RustItem, /) -> collections.Callable[[T], T]: 184 '''Expands a Python callable object's documentation, generating the corresponding Rust implementation of the marked object. 185 186 This is a decorator that applies on both classes, methods and functions. 187 188 Assuming we're implementing the `FnOnce` trait from Rust, the object in Python may be marked with this decorator like this. 189 ```py 190 from sain.macros import rustc_diagnostic_item 191 192 @rustc_diagnostic_item("FnOnce") 193 class FnOnce[Output, *Args]: 194 """The version of the call operator that takes a by-value receiver.""" 195 196 def __init__(self, fn: Callable[[*Args], Output]) -> None: 197 self._call = fn 198 199 @rustc_diagnostic_item("rust-call") 200 def call_once(self, *args: *Args) -> Output: 201 return self._call(*args) 202 ``` 203 204 Now that the class is marked, 205 It will generate documentation that links to the Rust object that we implemented in Python. 206 ''' 207 208 def decorator(obj: T) -> T: 209 additional_doc = f"\n\n# Implementations\nThis {_obj_type(obj)} implements [{item}]({_RUSTC_DOCS}/{_MAP_TO_PATH[item]}) in Rust." 210 obj.__doc__ = inspect.cleandoc(obj.__doc__ or "") + additional_doc 211 return obj 212 213 return decorator 214 215 216def assert_precondition( 217 condition: bool, 218 message: str = "", 219 exception: type[BaseException] = Exception, 220) -> None: 221 """Checks if `condition` is true, raising an exception if not. 222 223 This is used to inline check preconditions for functions and methods. 224 225 Example 226 ------- 227 ```py 228 from sain.macros import assert_precondition 229 230 def divide(a: int, b: int) -> float: 231 assert_precondition( 232 b != 0, 233 "b must not be zero", 234 ZeroDivisionError 235 ) 236 return a / b 237 ``` 238 239 An inlined version would be: 240 ```py 241 if not condition: 242 raise Exception( 243 f"precondition check violated: {message}" 244 ) from None 245 ``` 246 247 Parameters 248 ---------- 249 condition : `bool` 250 The condition to check. 251 message : `LiteralString` 252 The message to display if the condition is false. 253 Defaults to an empty string. 254 exception : `type[BaseException]` 255 The exception to raise if the condition is false. 256 Defaults to `Exception`. 257 """ 258 if not condition: 259 raise exception( 260 f"precondition check violated: \033[91m{message}\033[0m" 261 ) from None 262 263 264@typing.final 265class ub_checks(RuntimeWarning): 266 """A special type of runtime warning that is only invoked on objects using `unsafe`.""" 267 268 269def safe(fn: collections.Callable[P, U]) -> collections.Callable[P, U]: 270 """Permit the use of `unsafe` marked function within a specific object. 271 272 This allows you to call functions marked with `unsafe` without causing runtime warnings. 273 274 Example 275 ------- 276 ```py 277 @unsafe 278 def unsafe_fn() -> None: ... 279 280 @safe 281 def unsafe_in_safe() -> None: 282 # Calling this won't cause any runtime warns. 283 unsafe_fn() 284 285 unsafe_in_safe() 286 ``` 287 """ 288 289 @functools.wraps(fn) 290 def wrapper(*args: P.args, **kwargs: P.kwargs) -> U: 291 if sys.version_info >= (3, 12): 292 with warnings.catch_warnings(action="ignore", category=ub_checks): 293 return fn(*args, **kwargs) 294 else: 295 with warnings.catch_warnings(): 296 warnings.simplefilter("ignore", category=ub_checks) 297 return fn(*args, **kwargs) 298 299 return wrapper 300 301 302@rustc_diagnostic_item("unsafe") 303def unsafe(fn: collections.Callable[P, U]) -> collections.Callable[P, U]: 304 """Mark a function as unsafe. 305 306 ## What this marker does 307 * Generates an unsafe warning to the docstring of the decorated object. 308 * Warn callers of unsafe usage of an object. 309 * Never crashes your code, only warns the user, the programmer is responsible 310 for the code they've written, this is a utility decorator only. 311 312 however, ignoring these warnings is possible (*not recommended*), see he listed examples. 313 314 Example 315 ------- 316 Use the `safe` decorator 317 318 ```py 319 @unsafe 320 def unsafe_fn() -> None: ... 321 322 # This decorator desugar into `infallible` in the next example. 323 @safe 324 def unsafe_in_safe() -> None: 325 # Calling this won't cause any runtime warns. 326 unsafe_fn() 327 328 unsafe_in_safe() 329 ``` 330 331 Using warnings lib: 332 ```py 333 import warnings 334 from sain.macros import unsafe, ub_checks 335 336 # globally ignore all `ub_checks` warns, not recommended. 337 warnings.filterwarnings("ignore", category=ub_checks) 338 339 @unsafe 340 def from_str_unchecked(val: str) -> float: 341 return float(val) 342 343 # This is a function that calls `from_str_unchecked` 344 # but we know it will never fails. 345 def infallible() -> float: 346 with warnings.catch_warnings(): 347 # ignore `ub_checks` specific warnings from `from_str_unchecked`. 348 warnings.simplefilter("ignore", category=ub_checks) 349 return from_str_unchecked("3.14") 350 ``` 351 352 Another way is to simply run your program with `-O` opt flag. 353 354 This won't generate the code needed to execute the warning, 355 this will also disable all `assert` calls. 356 357 ```sh 358 # This enable optimization level 1, which will opt-out of `ub_checks` warnings. 359 python script.py -O 360 # This will ignore all the warnings. 361 python -W ignore script.py 362 ``` 363 364 The caller of the decorated function is responsible for the undefined behavior if occurred. 365 """ 366 m = "\n# Safety ⚠️\nCalling this method without knowing the output is considered [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior).\n" 367 if fn.__doc__ is not None: 368 # append this message to an existing document. 369 fn.__doc__ = inspect.cleandoc(fn.__doc__) + m 370 else: 371 fn.__doc__ = m 372 373 if __debug__: 374 375 @functools.wraps(fn) 376 def wrapper(*args: P.args, **kwargs: P.kwargs) -> U: 377 call_once = fn(*args, **kwargs) 378 _warn( 379 f"\033[93mcalling `{wrapper.__qualname__}` " 380 "is considered unsafe and may lead to undefined behavior.\n" 381 "you can disable this warning by using `-O` opt level if you know what you're doing.\033[0m", 382 warn_ty=ub_checks, 383 stacklevel=3, 384 ) 385 return call_once 386 387 return wrapper 388 else: 389 return fn 390 391 392@rustc_diagnostic_item("assert_eq") 393def assert_eq(left: T, right: T) -> None: 394 """Asserts that two expressions are equal to each other. 395 396 This exactly as `assert left == right`, but includes a useful message in case of failure. 397 398 Example 399 ------- 400 ```py 401 from sain.macros import assert_eq 402 a = 3 403 b = 1 + 2 404 assert_eq(a, b) 405 ``` 406 """ 407 assert ( 408 left == right 409 ), f'assertion `left == right` failed\nleft: "{left!r}"\nright: "{right!r}"' 410 411 412@rustc_diagnostic_item("assert_ne") 413def assert_ne(left: T, right: T) -> None: 414 """Asserts that two expressions are not equal to each other. 415 416 This exactly as `assert left == right`, but includes a useful message in case of failure. 417 418 Example 419 ------- 420 ```py 421 from sain.macros import assert_ne 422 a = 3 423 b = 2 + 2 424 assert_ne(a, b) 425 ``` 426 """ 427 assert ( 428 left != right 429 ), f'assertion `left != right` failed\nleft: "{left!r}"\nright: "{right!r}"' 430 431 432@rustc_diagnostic_item("include_bytes") 433def include_bytes(file: LiteralString) -> bytes: 434 """Includes a file as `bytes`. 435 436 This function is not magic, It is literally defined as 437 438 ```py 439 with open(file, "rb") as f: 440 return f.read() 441 ``` 442 443 The file name can may be either a relative to the current file or a complete path. 444 445 Example 446 ------- 447 File "spanish.in": 448 ```text 449 adiós 450 ``` 451 File "main.py": 452 ```py 453 from sain.macros import include_bytes 454 buffer = include_bytes("spanish.in") 455 assert buffer.decode() == "adiós" 456 ``` 457 """ 458 with open(file, "rb") as buf: 459 return buf.read() 460 461 462@rustc_diagnostic_item("include_str") 463def include_str(file: LiteralString) -> LiteralString: 464 """Includes a file as literal `str`. 465 466 This function is not magic, It is literally defined as 467 468 ```py 469 with open(file, "r") as f: 470 return f.read() 471 ``` 472 473 The file name can may be either a relative to the current file or a complete path. 474 475 Example 476 ------- 477 ```py 478 from sain.macros import include_str 479 480 def entry() -> None: 481 ... 482 483 entry.__doc__ = include_str("README.md") 484 485 ``` 486 """ 487 with open(file, "r") as buf: 488 return buf.read() # pyright: ignore - simulates a `&'static str` slice. 489 490 491def unstable( 492 *, reason: LiteralString = "none" 493) -> collections.Callable[ 494 [collections.Callable[P, typing.Any]], 495 collections.Callable[P, typing.NoReturn], 496]: 497 """A decorator that marks an internal object explicitly unstable. 498 499 Unstable objects never ran, even inside the library. 500 501 Calling any object that is unstable will raise an `RuntimeError` exception. 502 Also using this outside the library isn't allowed. 503 504 Example 505 ------- 506 ```py 507 508 from sain.macros import unstable 509 510 @unstable(reason = "none") 511 def unstable_function() -> int: 512 return -1 513 514 if unstable_function(): 515 # never reachable 516 517 ``` 518 """ 519 520 def decorator( 521 obj: collections.Callable[P, U], 522 ) -> collections.Callable[P, typing.NoReturn]: 523 @functools.wraps(obj) 524 def wrapper(*_args: P.args, **_kwargs: P.kwargs) -> typing.NoReturn: 525 raise RuntimeError( 526 f"\033[91m{_obj_type(obj)} `{obj.__name__}` is not stable: {reason}.\033[0m" 527 ) 528 529 m = ( 530 f"\n# Stability ⚠️\nThis {_obj_type(obj)} is unstable, " 531 "Calling it may result in failure or [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior)." 532 ) 533 # Append the formatted string to the existing documentation if it exists, otherwise set it as the documentation. 534 wrapper.__doc__ = ( 535 (inspect.cleandoc(wrapper.__doc__) + m) if wrapper.__doc__ else m 536 ) 537 return wrapper 538 539 return decorator 540 541 542# TODO: in 2.0.0 remove this and use typing_extensions.deprecated instead. 543@typing.overload 544def deprecated( 545 *, 546 obj: collections.Callable[P, U] | None = None, 547) -> collections.Callable[P, U]: ... 548 549 550@typing.overload 551def deprecated( 552 *, 553 since: typing.Literal["CURRENT_VERSION"] | LiteralString | None = None, 554 removed_in: LiteralString | None = None, 555 use_instead: LiteralString | None = None, 556 hint: LiteralString | None = None, 557) -> collections.Callable[ 558 [collections.Callable[P, U]], 559 collections.Callable[P, U], 560]: ... 561 562 563@rustc_diagnostic_item("deprecated") 564def deprecated( 565 *, 566 obj: collections.Callable[P, U] | None = None, 567 since: typing.Literal["CURRENT_VERSION"] | LiteralString | None = None, 568 removed_in: LiteralString | None = None, 569 use_instead: LiteralString | None = None, 570 hint: LiteralString | None = None, 571) -> ( 572 collections.Callable[P, U] 573 | collections.Callable[ 574 [collections.Callable[P, U]], 575 collections.Callable[P, U], 576 ] 577): 578 """A decorator that marks a function as deprecated. 579 580 An attempt to call the object that's marked will cause a runtime warn. 581 582 Example 583 ------- 584 ```py 585 from sain import deprecated 586 587 @deprecated( 588 since = "1.0.0", 589 removed_in ="3.0.0", 590 use_instead = "UserImpl()", 591 hint = "Hint for ux." 592 ) 593 class User: 594 # calling the decorator is not necessary. 595 @deprecated 596 def username(self) -> str: 597 ... 598 599 user = User() # This will cause a warning at runtime. 600 601 ``` 602 603 Parameters 604 ---------- 605 since : `str` 606 The version that the function was deprecated. the `CURRENT_VERSION` is used internally only. 607 removed_in : `str | None` 608 If provided, It will log when will the object will be removed in. 609 use_instead : `str | None` 610 If provided, This should be the alternative object name that should be used instead. 611 hint: `str` 612 An optional hint for the user. 613 """ 614 615 def _create_message( 616 f: typing.Any, 617 ) -> str: 618 msg = f"{_obj_type(f)} `{f.__module__}.{f.__name__}` is deprecated." 619 620 if since is not None: 621 if since == "CURRENT_VERSION": 622 from ._misc import __version__ 623 624 msg += " since " + __version__ 625 else: 626 msg += " since " + since 627 628 if removed_in: 629 msg += f" Scheduled for removal in `{removed_in}`." 630 631 if use_instead is not None: 632 msg += f" Use `{use_instead}` instead." 633 634 if hint: 635 msg += f" Hint: {hint}" 636 return msg 637 638 def decorator(func: collections.Callable[P, U]) -> collections.Callable[P, U]: 639 message = _create_message(func) 640 641 @functools.wraps(func) 642 def wrapper(*args: P.args, **kwargs: P.kwargs) -> U: 643 _warn("\033[93m" + message + "\033[0m", warn_ty=DeprecationWarning) 644 return func(*args, **kwargs) 645 646 # idk why pyright doesn't know the type of wrapper. 647 m = f"\n# Warning ⚠️\n{message}." 648 if wrapper.__doc__: 649 # append this message to an existing document. 650 wrapper.__doc__ = inspect.cleandoc(wrapper.__doc__) + f"{m}" 651 else: 652 wrapper.__doc__ = m 653 654 return wrapper 655 656 # marked only. 657 if obj is not None: 658 return decorator(obj) 659 660 return decorator 661 662 663@rustc_diagnostic_item("todo") 664def todo(message: LiteralString | None = None) -> typing.NoReturn: 665 """A place holder that indicates unfinished code. 666 667 Example 668 ------- 669 ```py 670 from sain import todo 671 672 def from_json(payload: dict[str, int]) -> int: 673 # Calling this function will raise `Error`. 674 todo() 675 ``` 676 677 Parameters 678 ---------- 679 message : `str | None` 680 Multiple optional arguments to pass if the error was raised. 681 """ 682 raise RuntimeWarning( 683 f"not yet implemented: {message}" if message else "not yet implemented" 684 ) 685 686 687# TODO: in 2.0.0 make this the same as `todo` 688@typing.overload 689def unimplemented( 690 *, 691 obj: collections.Callable[P, U] | None = None, 692) -> collections.Callable[P, U]: ... 693 694 695@typing.overload 696def unimplemented( 697 *, 698 message: LiteralString | None = None, 699 available_in: LiteralString | None = None, 700) -> collections.Callable[ 701 [collections.Callable[P, U]], 702 collections.Callable[P, U], 703]: ... 704 705 706@rustc_diagnostic_item("unimplemented") 707def unimplemented( 708 *, 709 obj: collections.Callable[P, U] | None = None, 710 message: LiteralString | None = None, 711 available_in: LiteralString | None = None, 712) -> ( 713 collections.Callable[P, U] 714 | collections.Callable[ 715 [collections.Callable[P, U]], 716 collections.Callable[P, U], 717 ] 718): 719 """A decorator that marks an object as unimplemented. 720 721 An attempt to call the object that's marked will cause a runtime warn. 722 723 Example 724 ------- 725 ```py 726 from sain import unimplemented 727 728 @unimplemented # Can be used without calling 729 class User: 730 ... 731 732 @unimplemented(message="Not ready", available_in="2.0.0") # Or with parameters 733 class Config: 734 ... 735 ``` 736 737 Parameters 738 ---------- 739 message : `str | None` 740 An optional message to be displayed when the function is called. Otherwise default message will be used. 741 available_in : `str | None` 742 If provided, This will be shown as what release this object be implemented. 743 """ 744 745 def _create_message(f: typing.Any) -> str: 746 msg = ( 747 message 748 or f"{_obj_type(f)} `{f.__module__}.{f.__name__}` is not yet implemented." 749 ) 750 751 if available_in: 752 msg += f" Available in `{available_in}`." 753 return msg 754 755 def decorator(func: collections.Callable[P, U]) -> collections.Callable[P, U]: 756 msg = _create_message(func) 757 758 @functools.wraps(func) 759 def wrapper(*args: P.args, **kwargs: P.kwargs) -> U: 760 _warn("\033[93m" + msg + "\033[0m", warn_ty=RuntimeWarning) 761 return func(*args, **kwargs) 762 763 m = f"\n# Warning ⚠️\n{msg}." 764 if wrapper.__doc__: 765 # Append the new documentation string to the existing docstring. 766 wrapper.__doc__ = inspect.cleandoc(wrapper.__doc__) + m 767 else: 768 # Assign the new documentation string as the docstring when no existing docstring is present. 769 wrapper.__doc__ = m 770 return wrapper 771 772 if obj is not None: 773 return decorator(obj) 774 775 return decorator 776 777 778@rustc_diagnostic_item("doc") 779def doc( 780 path: Read, 781) -> collections.Callable[ 782 [collections.Callable[P, U]], 783 collections.Callable[P, U], 784]: 785 """Set `path` to be the object's documentation. 786 787 Example 788 ------- 789 ```py 790 from sain import doc 791 from pathlib import Path 792 793 @doc(Path("../README.md")) 794 class builtins: 795 @doc("bool.html") 796 def bool_docs() -> None: 797 ... 798 ``` 799 800 Parameters 801 ---------- 802 path: `type[int] | type[str] | type[bytes] | type[PathLike[str]] | type[PathLike[bytes]]` 803 The path to read the content from. 804 """ 805 806 def decorator(f: collections.Callable[P, U]) -> collections.Callable[P, U]: 807 with open(path, "r") as file: 808 f.__doc__ = file.read() 809 810 return lambda *args, **kwargs: f(*args, **kwargs) 811 812 return decorator
564@rustc_diagnostic_item("deprecated") 565def deprecated( 566 *, 567 obj: collections.Callable[P, U] | None = None, 568 since: typing.Literal["CURRENT_VERSION"] | LiteralString | None = None, 569 removed_in: LiteralString | None = None, 570 use_instead: LiteralString | None = None, 571 hint: LiteralString | None = None, 572) -> ( 573 collections.Callable[P, U] 574 | collections.Callable[ 575 [collections.Callable[P, U]], 576 collections.Callable[P, U], 577 ] 578): 579 """A decorator that marks a function as deprecated. 580 581 An attempt to call the object that's marked will cause a runtime warn. 582 583 Example 584 ------- 585 ```py 586 from sain import deprecated 587 588 @deprecated( 589 since = "1.0.0", 590 removed_in ="3.0.0", 591 use_instead = "UserImpl()", 592 hint = "Hint for ux." 593 ) 594 class User: 595 # calling the decorator is not necessary. 596 @deprecated 597 def username(self) -> str: 598 ... 599 600 user = User() # This will cause a warning at runtime. 601 602 ``` 603 604 Parameters 605 ---------- 606 since : `str` 607 The version that the function was deprecated. the `CURRENT_VERSION` is used internally only. 608 removed_in : `str | None` 609 If provided, It will log when will the object will be removed in. 610 use_instead : `str | None` 611 If provided, This should be the alternative object name that should be used instead. 612 hint: `str` 613 An optional hint for the user. 614 """ 615 616 def _create_message( 617 f: typing.Any, 618 ) -> str: 619 msg = f"{_obj_type(f)} `{f.__module__}.{f.__name__}` is deprecated." 620 621 if since is not None: 622 if since == "CURRENT_VERSION": 623 from ._misc import __version__ 624 625 msg += " since " + __version__ 626 else: 627 msg += " since " + since 628 629 if removed_in: 630 msg += f" Scheduled for removal in `{removed_in}`." 631 632 if use_instead is not None: 633 msg += f" Use `{use_instead}` instead." 634 635 if hint: 636 msg += f" Hint: {hint}" 637 return msg 638 639 def decorator(func: collections.Callable[P, U]) -> collections.Callable[P, U]: 640 message = _create_message(func) 641 642 @functools.wraps(func) 643 def wrapper(*args: P.args, **kwargs: P.kwargs) -> U: 644 _warn("\033[93m" + message + "\033[0m", warn_ty=DeprecationWarning) 645 return func(*args, **kwargs) 646 647 # idk why pyright doesn't know the type of wrapper. 648 m = f"\n# Warning ⚠️\n{message}." 649 if wrapper.__doc__: 650 # append this message to an existing document. 651 wrapper.__doc__ = inspect.cleandoc(wrapper.__doc__) + f"{m}" 652 else: 653 wrapper.__doc__ = m 654 655 return wrapper 656 657 # marked only. 658 if obj is not None: 659 return decorator(obj) 660 661 return decorator
A decorator that marks a function as deprecated.
An attempt to call the object that's marked will cause a runtime warn.
Example
from sain import deprecated
@deprecated(
since = "1.0.0",
removed_in ="3.0.0",
use_instead = "UserImpl()",
hint = "Hint for ux."
)
class User:
# calling the decorator is not necessary.
@deprecated
def username(self) -> str:
...
user = User() # This will cause a warning at runtime.
Parameters
- since (
str): The version that the function was deprecated. theCURRENT_VERSIONis used internally only. - removed_in (
str | None): If provided, It will log when will the object will be removed in. - use_instead (
str | None): If provided, This should be the alternative object name that should be used instead. - hint (
str): An optional hint for the user. - # Implementations
- **This function implements deprecated:
707@rustc_diagnostic_item("unimplemented") 708def unimplemented( 709 *, 710 obj: collections.Callable[P, U] | None = None, 711 message: LiteralString | None = None, 712 available_in: LiteralString | None = None, 713) -> ( 714 collections.Callable[P, U] 715 | collections.Callable[ 716 [collections.Callable[P, U]], 717 collections.Callable[P, U], 718 ] 719): 720 """A decorator that marks an object as unimplemented. 721 722 An attempt to call the object that's marked will cause a runtime warn. 723 724 Example 725 ------- 726 ```py 727 from sain import unimplemented 728 729 @unimplemented # Can be used without calling 730 class User: 731 ... 732 733 @unimplemented(message="Not ready", available_in="2.0.0") # Or with parameters 734 class Config: 735 ... 736 ``` 737 738 Parameters 739 ---------- 740 message : `str | None` 741 An optional message to be displayed when the function is called. Otherwise default message will be used. 742 available_in : `str | None` 743 If provided, This will be shown as what release this object be implemented. 744 """ 745 746 def _create_message(f: typing.Any) -> str: 747 msg = ( 748 message 749 or f"{_obj_type(f)} `{f.__module__}.{f.__name__}` is not yet implemented." 750 ) 751 752 if available_in: 753 msg += f" Available in `{available_in}`." 754 return msg 755 756 def decorator(func: collections.Callable[P, U]) -> collections.Callable[P, U]: 757 msg = _create_message(func) 758 759 @functools.wraps(func) 760 def wrapper(*args: P.args, **kwargs: P.kwargs) -> U: 761 _warn("\033[93m" + msg + "\033[0m", warn_ty=RuntimeWarning) 762 return func(*args, **kwargs) 763 764 m = f"\n# Warning ⚠️\n{msg}." 765 if wrapper.__doc__: 766 # Append the new documentation string to the existing docstring. 767 wrapper.__doc__ = inspect.cleandoc(wrapper.__doc__) + m 768 else: 769 # Assign the new documentation string as the docstring when no existing docstring is present. 770 wrapper.__doc__ = m 771 return wrapper 772 773 if obj is not None: 774 return decorator(obj) 775 776 return decorator
A decorator that marks an object as unimplemented.
An attempt to call the object that's marked will cause a runtime warn.
Example
from sain import unimplemented
@unimplemented # Can be used without calling
class User:
...
@unimplemented(message="Not ready", available_in="2.0.0") # Or with parameters
class Config:
...
Parameters
- message (
str | None): An optional message to be displayed when the function is called. Otherwise default message will be used. - available_in (
str | None): If provided, This will be shown as what release this object be implemented. - # Implementations
- **This function implements unimplemented:
664@rustc_diagnostic_item("todo") 665def todo(message: LiteralString | None = None) -> typing.NoReturn: 666 """A place holder that indicates unfinished code. 667 668 Example 669 ------- 670 ```py 671 from sain import todo 672 673 def from_json(payload: dict[str, int]) -> int: 674 # Calling this function will raise `Error`. 675 todo() 676 ``` 677 678 Parameters 679 ---------- 680 message : `str | None` 681 Multiple optional arguments to pass if the error was raised. 682 """ 683 raise RuntimeWarning( 684 f"not yet implemented: {message}" if message else "not yet implemented" 685 )
A place holder that indicates unfinished code.
Example
from sain import todo
def from_json(payload: dict[str, int]) -> int:
# Calling this function will raise `Error`.
todo()
Parameters
- message (
str | None): Multiple optional arguments to pass if the error was raised. - # Implementations
- **This function implements todo:
779@rustc_diagnostic_item("doc") 780def doc( 781 path: Read, 782) -> collections.Callable[ 783 [collections.Callable[P, U]], 784 collections.Callable[P, U], 785]: 786 """Set `path` to be the object's documentation. 787 788 Example 789 ------- 790 ```py 791 from sain import doc 792 from pathlib import Path 793 794 @doc(Path("../README.md")) 795 class builtins: 796 @doc("bool.html") 797 def bool_docs() -> None: 798 ... 799 ``` 800 801 Parameters 802 ---------- 803 path: `type[int] | type[str] | type[bytes] | type[PathLike[str]] | type[PathLike[bytes]]` 804 The path to read the content from. 805 """ 806 807 def decorator(f: collections.Callable[P, U]) -> collections.Callable[P, U]: 808 with open(path, "r") as file: 809 f.__doc__ = file.read() 810 811 return lambda *args, **kwargs: f(*args, **kwargs) 812 813 return decorator
Set path to be the object's documentation.
Example
from sain import doc
from pathlib import Path
@doc(Path("../README.md"))
class builtins:
@doc("bool.html")
def bool_docs() -> None:
...
Parameters
- path (
type[int] | type[str] | type[bytes] | type[PathLike[str]] | type[PathLike[bytes]]): The path to read the content from. - # Implementations
- **This function implements doc:
393@rustc_diagnostic_item("assert_eq") 394def assert_eq(left: T, right: T) -> None: 395 """Asserts that two expressions are equal to each other. 396 397 This exactly as `assert left == right`, but includes a useful message in case of failure. 398 399 Example 400 ------- 401 ```py 402 from sain.macros import assert_eq 403 a = 3 404 b = 1 + 2 405 assert_eq(a, b) 406 ``` 407 """ 408 assert ( 409 left == right 410 ), f'assertion `left == right` failed\nleft: "{left!r}"\nright: "{right!r}"'
Asserts that two expressions are equal to each other.
This exactly as assert left == right, but includes a useful message in case of failure.
Example
from sain.macros import assert_eq
a = 3
b = 1 + 2
assert_eq(a, b)
Implementations
This function implements assert_eq in Rust.
413@rustc_diagnostic_item("assert_ne") 414def assert_ne(left: T, right: T) -> None: 415 """Asserts that two expressions are not equal to each other. 416 417 This exactly as `assert left == right`, but includes a useful message in case of failure. 418 419 Example 420 ------- 421 ```py 422 from sain.macros import assert_ne 423 a = 3 424 b = 2 + 2 425 assert_ne(a, b) 426 ``` 427 """ 428 assert ( 429 left != right 430 ), f'assertion `left != right` failed\nleft: "{left!r}"\nright: "{right!r}"'
Asserts that two expressions are not equal to each other.
This exactly as assert left == right, but includes a useful message in case of failure.
Example
from sain.macros import assert_ne
a = 3
b = 2 + 2
assert_ne(a, b)
Implementations
This function implements assert_ne in Rust.
463@rustc_diagnostic_item("include_str") 464def include_str(file: LiteralString) -> LiteralString: 465 """Includes a file as literal `str`. 466 467 This function is not magic, It is literally defined as 468 469 ```py 470 with open(file, "r") as f: 471 return f.read() 472 ``` 473 474 The file name can may be either a relative to the current file or a complete path. 475 476 Example 477 ------- 478 ```py 479 from sain.macros import include_str 480 481 def entry() -> None: 482 ... 483 484 entry.__doc__ = include_str("README.md") 485 486 ``` 487 """ 488 with open(file, "r") as buf: 489 return buf.read() # pyright: ignore - simulates a `&'static str` slice.
Includes a file as literal str.
This function is not magic, It is literally defined as
with open(file, "r") as f:
return f.read()
The file name can may be either a relative to the current file or a complete path.
Example
from sain.macros import include_str
def entry() -> None:
...
entry.__doc__ = include_str("README.md")
Implementations
This function implements include_str in Rust.
433@rustc_diagnostic_item("include_bytes") 434def include_bytes(file: LiteralString) -> bytes: 435 """Includes a file as `bytes`. 436 437 This function is not magic, It is literally defined as 438 439 ```py 440 with open(file, "rb") as f: 441 return f.read() 442 ``` 443 444 The file name can may be either a relative to the current file or a complete path. 445 446 Example 447 ------- 448 File "spanish.in": 449 ```text 450 adiós 451 ``` 452 File "main.py": 453 ```py 454 from sain.macros import include_bytes 455 buffer = include_bytes("spanish.in") 456 assert buffer.decode() == "adiós" 457 ``` 458 """ 459 with open(file, "rb") as buf: 460 return buf.read()
Includes a file as bytes.
This function is not magic, It is literally defined as
with open(file, "rb") as f:
return f.read()
The file name can may be either a relative to the current file or a complete path.
Example
File "spanish.in":
adiós
File "main.py":
from sain.macros import include_bytes
buffer = include_bytes("spanish.in")
assert buffer.decode() == "adiós"
Implementations
This function implements include_bytes in Rust.
270def safe(fn: collections.Callable[P, U]) -> collections.Callable[P, U]: 271 """Permit the use of `unsafe` marked function within a specific object. 272 273 This allows you to call functions marked with `unsafe` without causing runtime warnings. 274 275 Example 276 ------- 277 ```py 278 @unsafe 279 def unsafe_fn() -> None: ... 280 281 @safe 282 def unsafe_in_safe() -> None: 283 # Calling this won't cause any runtime warns. 284 unsafe_fn() 285 286 unsafe_in_safe() 287 ``` 288 """ 289 290 @functools.wraps(fn) 291 def wrapper(*args: P.args, **kwargs: P.kwargs) -> U: 292 if sys.version_info >= (3, 12): 293 with warnings.catch_warnings(action="ignore", category=ub_checks): 294 return fn(*args, **kwargs) 295 else: 296 with warnings.catch_warnings(): 297 warnings.simplefilter("ignore", category=ub_checks) 298 return fn(*args, **kwargs) 299 300 return wrapper
Permit the use of unsafe marked function within a specific object.
This allows you to call functions marked with unsafe without causing runtime warnings.
Example
@unsafe
def unsafe_fn() -> None: ...
@safe
def unsafe_in_safe() -> None:
# Calling this won't cause any runtime warns.
unsafe_fn()
unsafe_in_safe()
303@rustc_diagnostic_item("unsafe") 304def unsafe(fn: collections.Callable[P, U]) -> collections.Callable[P, U]: 305 """Mark a function as unsafe. 306 307 ## What this marker does 308 * Generates an unsafe warning to the docstring of the decorated object. 309 * Warn callers of unsafe usage of an object. 310 * Never crashes your code, only warns the user, the programmer is responsible 311 for the code they've written, this is a utility decorator only. 312 313 however, ignoring these warnings is possible (*not recommended*), see he listed examples. 314 315 Example 316 ------- 317 Use the `safe` decorator 318 319 ```py 320 @unsafe 321 def unsafe_fn() -> None: ... 322 323 # This decorator desugar into `infallible` in the next example. 324 @safe 325 def unsafe_in_safe() -> None: 326 # Calling this won't cause any runtime warns. 327 unsafe_fn() 328 329 unsafe_in_safe() 330 ``` 331 332 Using warnings lib: 333 ```py 334 import warnings 335 from sain.macros import unsafe, ub_checks 336 337 # globally ignore all `ub_checks` warns, not recommended. 338 warnings.filterwarnings("ignore", category=ub_checks) 339 340 @unsafe 341 def from_str_unchecked(val: str) -> float: 342 return float(val) 343 344 # This is a function that calls `from_str_unchecked` 345 # but we know it will never fails. 346 def infallible() -> float: 347 with warnings.catch_warnings(): 348 # ignore `ub_checks` specific warnings from `from_str_unchecked`. 349 warnings.simplefilter("ignore", category=ub_checks) 350 return from_str_unchecked("3.14") 351 ``` 352 353 Another way is to simply run your program with `-O` opt flag. 354 355 This won't generate the code needed to execute the warning, 356 this will also disable all `assert` calls. 357 358 ```sh 359 # This enable optimization level 1, which will opt-out of `ub_checks` warnings. 360 python script.py -O 361 # This will ignore all the warnings. 362 python -W ignore script.py 363 ``` 364 365 The caller of the decorated function is responsible for the undefined behavior if occurred. 366 """ 367 m = "\n# Safety ⚠️\nCalling this method without knowing the output is considered [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior).\n" 368 if fn.__doc__ is not None: 369 # append this message to an existing document. 370 fn.__doc__ = inspect.cleandoc(fn.__doc__) + m 371 else: 372 fn.__doc__ = m 373 374 if __debug__: 375 376 @functools.wraps(fn) 377 def wrapper(*args: P.args, **kwargs: P.kwargs) -> U: 378 call_once = fn(*args, **kwargs) 379 _warn( 380 f"\033[93mcalling `{wrapper.__qualname__}` " 381 "is considered unsafe and may lead to undefined behavior.\n" 382 "you can disable this warning by using `-O` opt level if you know what you're doing.\033[0m", 383 warn_ty=ub_checks, 384 stacklevel=3, 385 ) 386 return call_once 387 388 return wrapper 389 else: 390 return fn
Mark a function as unsafe.
What this marker does
- Generates an unsafe warning to the docstring of the decorated object.
- Warn callers of unsafe usage of an object.
- Never crashes your code, only warns the user, the programmer is responsible for the code they've written, this is a utility decorator only.
however, ignoring these warnings is possible (not recommended), see he listed examples.
Example
Use the safe decorator
@unsafe
def unsafe_fn() -> None: ...
# This decorator desugar into `infallible` in the next example.
@safe
def unsafe_in_safe() -> None:
# Calling this won't cause any runtime warns.
unsafe_fn()
unsafe_in_safe()
Using warnings lib:
import warnings
from sain.macros import unsafe, ub_checks
# globally ignore all `ub_checks` warns, not recommended.
warnings.filterwarnings("ignore", category=ub_checks)
@unsafe
def from_str_unchecked(val: str) -> float:
return float(val)
# This is a function that calls `from_str_unchecked`
# but we know it will never fails.
def infallible() -> float:
with warnings.catch_warnings():
# ignore `ub_checks` specific warnings from `from_str_unchecked`.
warnings.simplefilter("ignore", category=ub_checks)
return from_str_unchecked("3.14")
Another way is to simply run your program with -O opt flag.
This won't generate the code needed to execute the warning,
this will also disable all assert calls.
# This enable optimization level 1, which will opt-out of `ub_checks` warnings.
python script.py -O
# This will ignore all the warnings.
python -W ignore script.py
The caller of the decorated function is responsible for the undefined behavior if occurred.
Implementations
This function implements unsafe in Rust.
184def rustc_diagnostic_item(item: RustItem, /) -> collections.Callable[[T], T]: 185 '''Expands a Python callable object's documentation, generating the corresponding Rust implementation of the marked object. 186 187 This is a decorator that applies on both classes, methods and functions. 188 189 Assuming we're implementing the `FnOnce` trait from Rust, the object in Python may be marked with this decorator like this. 190 ```py 191 from sain.macros import rustc_diagnostic_item 192 193 @rustc_diagnostic_item("FnOnce") 194 class FnOnce[Output, *Args]: 195 """The version of the call operator that takes a by-value receiver.""" 196 197 def __init__(self, fn: Callable[[*Args], Output]) -> None: 198 self._call = fn 199 200 @rustc_diagnostic_item("rust-call") 201 def call_once(self, *args: *Args) -> Output: 202 return self._call(*args) 203 ``` 204 205 Now that the class is marked, 206 It will generate documentation that links to the Rust object that we implemented in Python. 207 ''' 208 209 def decorator(obj: T) -> T: 210 additional_doc = f"\n\n# Implementations\nThis {_obj_type(obj)} implements [{item}]({_RUSTC_DOCS}/{_MAP_TO_PATH[item]}) in Rust." 211 obj.__doc__ = inspect.cleandoc(obj.__doc__ or "") + additional_doc 212 return obj 213 214 return decorator
Expands a Python callable object's documentation, generating the corresponding Rust implementation of the marked object.
This is a decorator that applies on both classes, methods and functions.
Assuming we're implementing the FnOnce trait from Rust, the object in Python may be marked with this decorator like this.
from sain.macros import rustc_diagnostic_item
@rustc_diagnostic_item("FnOnce")
class FnOnce[Output, *Args]:
"""The version of the call operator that takes a by-value receiver."""
def __init__(self, fn: Callable[[*Args], Output]) -> None:
self._call = fn
@rustc_diagnostic_item("rust-call")
def call_once(self, *args: *Args) -> Output:
return self._call(*args)
Now that the class is marked, It will generate documentation that links to the Rust object that we implemented in Python.