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
@rustc_diagnostic_item('deprecated')
def deprecated( *, obj: 'collections.Callable[P, U] | None' = None, since: Union[Literal['CURRENT_VERSION'], LiteralString, NoneType] = None, removed_in: Optional[LiteralString] = None, use_instead: Optional[LiteralString] = None, hint: Optional[LiteralString] = None) -> 'collections.Callable[P, U] | collections.Callable[[collections.Callable[P, U]], collections.Callable[P, U]]':
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. the CURRENT_VERSION is 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:
@rustc_diagnostic_item('unimplemented')
def unimplemented( *, obj: 'collections.Callable[P, U] | None' = None, message: Optional[LiteralString] = None, available_in: Optional[LiteralString] = None) -> 'collections.Callable[P, U] | collections.Callable[[collections.Callable[P, U]], collections.Callable[P, U]]':
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:
@rustc_diagnostic_item('todo')
def todo(message: Optional[LiteralString] = None) -> NoReturn:
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:
@rustc_diagnostic_item('doc')
def doc( path: 'Read') -> 'collections.Callable[[collections.Callable[P, U]], collections.Callable[P, U]]':
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:
@rustc_diagnostic_item('assert_eq')
def assert_eq(left: +T, right: +T) -> None:
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.

@rustc_diagnostic_item('assert_ne')
def assert_ne(left: +T, right: +T) -> None:
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.

@rustc_diagnostic_item('include_str')
def include_str(file: LiteralString) -> LiteralString:
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.

@rustc_diagnostic_item('include_bytes')
def include_bytes(file: LiteralString) -> bytes:
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.

def safe(fn: 'collections.Callable[P, U]') -> 'collections.Callable[P, U]':
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()
@rustc_diagnostic_item('unsafe')
def unsafe(fn: 'collections.Callable[P, U]') -> 'collections.Callable[P, U]':
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.

def rustc_diagnostic_item(item: 'RustItem', /) -> Callable[[+T], +T]:
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.

RustItem