No description
  • Python 99.6%
  • Nix 0.4%
Find a file
rus07tam 425a8ff89b
All checks were successful
CI / test (push) Successful in 48s
Publish Package / build-and-publish (push) Successful in 27s
release: v0.1.4
2026-09-13 09:48:43 +03:00
.forgejo/workflows fix: add missing dev dependencies and fix module name in CI 2026-09-12 22:14:31 +03:00
pydorust feat(macros): implement hygienic macro expansion system 2026-09-13 09:47:52 +03:00
scripts fix: resolve ruff issues 2026-09-13 00:00:41 +03:00
tests feat(macros): implement hygienic macro expansion system 2026-09-13 09:47:52 +03:00
.gitignore fix(macros): ignore internal macros in MacroFinder 2026-09-13 00:09:59 +03:00
.python-version release: v0.1.0 2026-09-12 22:11:06 +03:00
flake.lock release: v0.1.0 2026-09-12 22:11:06 +03:00
flake.nix release: v0.1.0 2026-09-12 22:11:06 +03:00
LICENSE chore: change license 2026-09-13 00:44:55 +03:00
pyproject.toml release: v0.1.4 2026-09-13 09:48:43 +03:00
README.md chore: apply ruff format 2026-09-13 00:45:08 +03:00
uv.lock fix(macros): ignore internal macros in MacroFinder 2026-09-13 00:09:59 +03:00

pydorust

A Python library bringing Rust programming language paradigms, semantics, and strict typing patterns to Python. Rather than attempting to enforce physical memory safety (which is fundamentally limited by Python's dynamic runtime), pydorust provides familiar Rust semantics, zero-cost abstractions, and structural patterns:

  • Macros (pydorust.macros): Procedural, declarative, and built-in AST metaprogramming.
  • Traits (pydorust.trait): True ad-hoc polymorphism and interface decoupling.
  • Algebraic Data Types (pydorust.adt): Tagged unions via Enum with nested Variant classes.
  • Option and Result (pydorust.types): Explicit error handling with Option[T] (Some, Null) and Result[T, E] (Ok, Err).
  • Interior Mutability (pydorust.lock): Cell, RefCell with runtime borrow checking, OnceLock, and LazyLock.
  • Synchronization Primitives (pydorust.sync): Semantic Mutex and RwLock with RAII-style context manager guards.
  • Fixed-Width Numbers (pydorust.num): u8..u64, i8..i64, usize, isize with wrapping, checked, saturating, and overflowing arithmetic.
  • Panics and Unsafe Contracts (pydorust.panics, pydorust.unsafe, pydorust.mem): Explicit panic(), unreachable(), @unsafe_func, and raw memory allocations.

Installation

pip install pydorust

Macros

The macro system brings syntactic metaprogramming to Python, inspired by Rust's procedural and declarative macros. Macros operate at the Python Abstract Syntax Tree (AST) level, allowing you to transform syntax, inline computations, and generate boilerplate at compile time or import time.

Invocation Forms

pydorust supports three ways to invoke macros in code:

  1. m_call(macro, target, *args): Invokes a macro as an expression or statement. The first argument is the macro, and subsequent arguments are passed as AST nodes.
  2. @m_decorator(macro, *args): Wraps a macro for use as a standard Python decorator on functions or classes.
  3. m_derive(macro, *args): A statement-level macro that intercepts and transforms the next AST node immediately following the call.

Built-in Macros

pydorust.macros.builtins provides standard macros:

Compile-Time Evaluation: comptime

Evaluates Python expressions at compile/expansion time and inlines the literal result into the AST:

from pydorust.macros import m_call
from pydorust.macros.builtins import comptime

# Evaluated once during macro expansion:
BUFFER_SIZE = m_call(comptime, 1024 * 1024 * 4)
# Compiles to: BUFFER_SIZE = 4194304

Compile-Time Environment: env and option_env

Reads environment variables during macro expansion and inlines their string literals into the AST (identical to Rust's env!() / option_env!()):

from pydorust.macros import m_call
from pydorust.macros.builtins import env

# Inlined as a string constant at compile time:
BUILD_PROFILE = m_call(env, "APP_ENV")

Compile-Time File Inclusion: include_str and include_bytes

Embeds the contents of an external file directly into the AST as a string or bytes literal at compile time (mirroring Rust's include_str!() / include_bytes!()):

from pydorust.macros import m_call
from pydorust.macros.builtins import include_str

# Inlines file contents directly into the code:
SCHEMA_JSON = m_call(include_str, "schema.json")

Declarative Macros: @m_define_declarative

Inspired by Rust's macro_rules!, declarative macros allow writing template-based code generators. Arguments represent AST variables, and names or strings prefixed with __q_ are replaced with the input AST nodes:

from typing import Any
from pydorust.macros import m_call, m_decorator
from pydorust.macros.builtins import m_define_declarative


@m_decorator(m_define_declarative)
def make_getter(field_name: Any) -> Any:
    def __q_field_name(self):
        return self._value


class Item:
    def __init__(self, value: int) -> None:
        self._value = value

    # Generates: def get_value(self): return self._value
    m_call(make_getter, get_value)


item = Item(42)
print(item.get_value())  # 42

Declarative macros also support substituting into classes, docstrings, and f-strings:

@m_decorator(m_define_declarative)
def create_service(service_name: Any, endpoint: Any) -> Any:
    class __q_service_name:
        """Client for __q_service_name."""

        URL = __q_endpoint

        def connect(self) -> str:
            return f"Connected to {__q_service_name.URL}"


m_call(create_service, PaymentService, "https://api.payments.com")

Imperative AST Macros: @m_define

For advanced transformations, @m_define allows writing procedural macros that inspect and manipulate AST nodes directly using MacroContext:

import ast
from pydorust.macros import m_define, MacroContext, MacroResult


@m_define
def assert_positive(ctx: MacroContext, node: ast.AST, *args: ast.AST) -> MacroResult:
    code = ctx.unparse(node)
    return ctx.parse(
        f"if not ({code} > 0): raise ValueError(f'{code} must be positive, got {{{code}}}')",
        unwrap_module=True,
    )

Tooling and Build Integration

Macros can be expanded in multiple workflows:

  1. CLI Tool (python -m pydorust.macros):

    # Expand a file and print the transformed Python code
    python -m pydorust.macros expand my_module.py
    
    # Compile a directory, expanding all macros into a target build folder
    python -m pydorust.macros compile src/ --out build/
    
    # Generate type stub (.pyi) files for macro-expanded code
    python -m pydorust.macros stubs src/ --out stubs/
    
  2. Hatchling Build Hook: pydorust registers a hatch build hook (pydorust.plugins.hatch). When building wheels or sdist packages (hatch build), macros are automatically expanded ahead of packaging.

  3. Pytest Integration: pydorust registers a pytest plugin (pydorust.plugins.pytest) via pytest11. Any test suite running under pytest automatically expands macros at import time without extra configuration.

  4. Runtime Expansion:

    from pydorust.macros import enable_runtime_expand, disable_runtime_expand
    
    enable_runtime_expand()  # Installs import hook in sys.meta_path
    import my_macro_code  # Expanded on import
    
    disable_runtime_expand()
    

Traits

pydorust introduces Rust-style traits, decoupling method and property declarations from class inheritance. This enables ad-hoc polymorphism (extension methods) on existing classes — including built-in types like int, str, or third-party classes — without monkey-patching.

Core Components

  • Trait: Base class for defining a trait interface.
  • @trait_method: Declares a required method that implementors must provide.
  • @default_method: Provides a default implementation that can call other trait methods.
  • @trait_property: Declares a required property.
  • @default_property: Provides a default property implementation.
  • @impl(Trait, for_type=...): Registers a trait implementation for a specific type and verifies completeness at registration time.

Example

from pydorust.trait import (
    Trait,
    impl,
    trait_method,
    default_method,
    trait_property,
    default_property,
)


class Summary(Trait):
    @trait_method
    def summarize_author(self) -> str:
        """Required method: implementors must provide this."""

    @default_method
    def summarize(self) -> str:
        """Provided default method: calls summarize_author()."""
        return f"(Read more from {self.summarize_author()}...)"

    @trait_property
    def headline(self) -> str:
        """Required property."""

    @default_property
    def full_summary(self) -> str:
        """Provided default property combining headline and summarize."""
        return f"{self.headline} -- {self.summarize()}"


class Article:
    def __init__(self, title: str, author: str) -> None:
        self.title = title
        self.author = author


@impl(Summary)
class ArticleSummary(Article):
    def summarize_author(self) -> str:
        return f"@{self.author}"

    @property
    def headline(self) -> str:
        return self.title


article = Article("Rust in Python", "rus07tam")

# Type checking works seamlessly:
assert isinstance(article, Summary)

# Dispatch style A: Trait proxy wrapper
summary_view = Summary(article)
print(summary_view.summarize_author())  # "@rus07tam"
print(summary_view.summarize())  # "(Read more from @rus07tam...)"
print(summary_view.full_summary)  # "Rust in Python -- (Read more from @rus07tam...)"

# Dispatch style B: Static / functional call
print(Summary.summarize(article))  # "(Read more from @rus07tam...)"
print(
    Summary.full_summary(article)
)  # "Rust in Python -- (Read more from @rus07tam...)"

Algebraic Data Types

In Rust, algebraic data types are represented via enum, where each variant can optionally contain different types and amounts of data. In pydorust, this pattern is implemented through the Enum class containing nested Variant classes.

Each nested Variant class is automatically treated as an immutable (frozen) data structure:

from pydorust.adt import Enum, Variant


class Message(Enum):
    # Unit variant (no data)
    class Quit(Variant):
        pass

    # Struct-like variant with fields
    class Move(Variant):
        x: int
        y: int

    class Write(Variant):
        text: str

    class ChangeColor(Variant):
        r: int
        g: int
        b: int

Instantiation and Type Guarantees

Variants can be instantiated directly as members of the Enum:

msg1 = Message.Quit
msg2 = Message.Move(x=10, y=20)
msg3 = Message.Write(text="Hello, Rust!")

# Type hierarchy is strictly verified:
assert isinstance(msg1, Message)
assert isinstance(msg2, Message)
assert isinstance(msg2, Message.Move)
assert issubclass(Message.Move, Message)

Handling Variants

Variants can be inspected and branched on using standard isinstance checks:

def process_message(msg: Message) -> str:
    if isinstance(msg, Message.Quit):
        return "Application quit."
    if isinstance(msg, Message.Move):
        return f"Moved to coordinates ({msg.x}, {msg.y})."
    if isinstance(msg, Message.Write):
        return f"Message content: {msg.text}"
    if isinstance(msg, Message.ChangeColor):
        return f"Color changed to RGB({msg.r}, {msg.g}, {msg.b})."
    raise TypeError(f"Unknown message variant: {msg}")


print(process_message(Message.Move(x=100, y=200)))
# "Moved to coordinates (100, 200)."

Types: Option and Result

Rust eliminates null reference errors via Option<T> and manages fallible operations through Result<T, E>.

Option[T] (Some and Null)

Represents an optional value: every Option is either Some(value) or Null:

from pydorust.types import Option, Some, Null, ret_option


@ret_option
def find_first_even(numbers: list[int]) -> int | None:
    for n in numbers:
        if n % 2 == 0:
            return n
    return None


opt: Option[int] = find_first_even([1, 3, 5, 8, 9])
assert opt.is_some()
assert opt.unwrap() == 8

empty_opt: Option[int] = find_first_even([1, 3, 5])
assert empty_opt.is_null()
assert empty_opt.unwrap_or(0) == 0

# Functional combinators: .map(), .filter(), .and_then()
doubled = opt.map(lambda x: x * 2).unwrap_or(0)  # 16

Result[T, E] (Ok and Err)

Represents either success (Ok) or failure (Err):

from pydorust.types import Result, Ok, Err, ret_result


@ret_result
def parse_int(value: str) -> int:
    return int(value)


res_ok: Result[int, Exception] = parse_int("42")
assert res_ok.is_ok()
assert res_ok.unwrap() == 42

res_err: Result[int, Exception] = parse_int("invalid")
assert res_err.is_err()
assert res_err.unwrap_or(0) == 0

# Chaining operations safely:
computed = (
    parse_int("10")
    .map(lambda x: x * 5)
    .and_then(lambda x: Ok(f"Result: {x}"))
    .unwrap_or("Fallback")
)
print(computed)  # "Result: 50"

Interior Mutability

The pydorust.lock module brings Rust-style interior mutability primitives to Python:

RefCell[T]: Runtime Borrow Checking

RefCell provides dynamically checked borrow rules at runtime using context managers:

  • Multiple immutable borrows (with cell.borrow():) are allowed simultaneously.
  • Only one mutable borrow (with cell.borrow_mut():) is permitted at a time.
  • Conflicting borrows raise a RuntimeError:
from pydorust.lock import RefCell

data = RefCell([1, 2, 3])

# Multiple immutable borrows:
with data.borrow() as b1, data.borrow() as b2:
    print(b1.value, b2.value)

# Single mutable borrow:
with data.borrow_mut() as m:
    m.value.append(4)

# Conflicting borrows raise RuntimeError:
with data.borrow():
    with data.borrow_mut():  # Raises RuntimeError: "already borrowed"
        pass

Cell[T]

A container providing interior mutability for copyable or value-like data without borrow-check tracking overhead:

from pydorust.lock import Cell

cell = Cell(10)
print(cell.unsafe_get())  # 10
cell.unsafe_set(25)
print(cell.unsafe_get())  # 25

OnceLock[T] and LazyLock[T]

Thread-safe primitives for once-initialization and lazy evaluation:

from pydorust.lock import OnceLock, LazyLock

# OnceLock: Write once, read many times
config = OnceLock()
assert config.try_get().is_null()
config.get_or_init(lambda: {"env": "production", "port": 8080})
print(config.unwrap())  # {'env': 'production', 'port': 8080}

# LazyLock: Computed on first access
heavy_resource = LazyLock(lambda: sum(range(1_000_000)))
print(heavy_resource.get())  # Computed and cached on first call

Synchronization Primitives

Provides RAII-style semantic synchronization primitives mirroring Rust's std::sync:

Mutex[T] and MutexGuard

Protects shared data behind an exclusive lock:

import threading
from pydorust.sync import Mutex

counter = Mutex(0)


def worker():
    for _ in range(1000):
        with counter.lock() as guard:
            guard.value += 1


threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()

with counter.lock() as guard:
    print(guard.value)  # 5000

RwLock[T]

Allows concurrent read access by multiple readers, or exclusive access by a single writer:

from pydorust.sync import RwLock

data = RwLock({"key": "initial"})

# Readers
with data.read() as reader:
    print(reader.value["key"])

# Writer
with data.write() as writer:
    writer.value["key"] = "updated"

Fixed-Width Numbers

pydorust.num introduces fixed-width numeric types matching Rust integer specifications:

  • Unsigned: u8, u16, u32, u64, usize
  • Signed: i8, i16, i32, i64, isize

Fixed-width types prevent silent overflows and provide explicit Rust arithmetic modes:

from pydorust.num import u8, i32

# Bounds checking:
val = u8(255)
# u8(256) raises Panic: "out of bounds"

# 1. Wrapping arithmetic (modulo 2^N)
assert u8(255).wrapping_add(2) == 1
assert u8(0).wrapping_sub(1) == 255

# 2. Checked arithmetic (returns Option[T] to prevent panics)
assert u8(250).checked_add(5).unwrap() == 255
assert u8(250).checked_add(10).is_null()

# 3. Saturating arithmetic (clamps to MIN / MAX bounds)
assert u8(250).saturating_add(20) == 255
assert u8(10).saturating_sub(20) == 0

# 4. Overflowing arithmetic (returns tuple: (result, did_overflow))
res, overflow = u8(250).overflowing_add(10)
assert res == 4 and overflow is True

Panics, Unsafe, and Memory Management

Unrecoverable Errors (pydorust.panics)

For irrecoverable conditions, pydorust provides explicit panic primitives:

  • panic("reason"): Terminates the program or raises Panic (depending on panic_mode).
  • unimplemented(): Explicit crash for missing features.
  • unreachable(): Marks unreachable code paths.
  • panic_mode(PanicMode.ABORT | PanicMode.RAISE): Configures whether panics terminate the process (os._exit(1)) or raise a Python Panic exception (useful for test runners).
from pydorust.panics import panic, unimplemented, unreachable, panic_mode, PanicMode

panic_mode(PanicMode.RAISE)


def handle_choice(option: int):
    if option == 1:
        return "Option 1"
    elif option == 2:
        return "Option 2"
    else:
        unreachable()

Unsafe Contracts (pydorust.unsafe)

In Rust, operations that circumvent safety invariants must reside inside unsafe blocks. pydorust establishes this explicit contract using @unsafe_func and unsafe_call:

from pydorust.unsafe import unsafe_func, unsafe_call


@unsafe_func
def raw_hardware_access(address: int) -> None:
    print(f"Accessing memory at {address:#x}")


# raw_hardware_access(0x1000)  # Type error / linter warning
unsafe_call(raw_hardware_access, 0x1000)  # Explicit contract

Low-Level Memory (pydorust.mem)

Exposes direct memory management primitives wrapping ctypes.pythonapi:

from pydorust.mem import RawBuffer

with RawBuffer(128) as buf:
    buf.write(b"Hello from raw memory!")
    data = buf.read(22)
    print(data)  # b'Hello from raw memory!'

License

This project is dedicated to the public domain under the Unlicense. See LICENSE for details.