Skip to content
Draft

pyo3 #30

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,16 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Install dependencies
run: |
python -m pip install --upgrade pip flake8
python -m pip install --upgrade pip flake8 maturin
# Try to build Rust extension, but don't fail if it doesn't work
maturin develop --release || true
python -m pip install -e '.[test]'
- name: Lint with flake8
run: |
Expand Down
76 changes: 76 additions & 0 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Build and publish wheels

on:
push:
tags:
- 'v*'
pull_request:
workflow_dispatch:

jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13']

steps:
- uses: actions/checkout@v3

- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true

- name: Build wheels
uses: PyO3/maturin-action@v1
with:
command: build
args: --release --out dist
manylinux: auto

- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist

build_sdist:
name: Build source distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Build sdist
run: pipx run build --sdist

- uses: actions/upload-artifact@v3
with:
name: wheels
path: dist/*.tar.gz

release:
name: Release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
needs: [build_wheels, build_sdist]
steps:
- uses: actions/download-artifact@v3
with:
name: wheels

- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --skip-existing *
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ MANIFEST
/build
.coverage
.pytest_cache

# Rust
target/
Cargo.lock
*.rs.bk
*.so
*.pyd
88 changes: 88 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

wirerope is a Python library that provides a wrapper interface for Python callables, implementing advanced decorator functionality through the Wire and Rope system.

## Development Commands

### Testing
- Run tests: `pytest`
- Run tests with coverage: `pytest --verbose --cov-config .coveragerc --cov wirerope`
- Run a single test file: `pytest tests/test_wire.py`
- Run a specific test: `pytest tests/test_wire.py::test_specific_test_name`

### Benchmarking
- Run only benchmarks: `pytest tests/test_benchmark.py --benchmark-only`
- Run benchmarks with comparison: `pytest tests/test_benchmark.py --benchmark-compare`
- Skip benchmarks in normal test run: `pytest --benchmark-skip`
- Save benchmark results: `pytest tests/test_benchmark.py --benchmark-save=NAME`
- Compare with saved results: `pytest tests/test_benchmark.py --benchmark-compare=NAME`

### Linting
- Run flake8: `flake8 . --statistics`
- Configuration: See `[flake8]` section in setup.cfg (ignores E701)

### Building

#### Python-only build
- Install development dependencies: `pip install -e '.[test]'`
- Build package: `python -m build` (requires build package)
- Create wheel: `python setup.py bdist_wheel`

#### Rust extension build
- Install Rust: https://rustup.rs/
- Install maturin: `pip install maturin`
- Development build: `maturin develop`
- Release build: `maturin develop --release`
- Build wheels: `maturin build --release`

## Architecture

The wirerope library implements a sophisticated decorator system with three main components:

### Core Classes

1. **Wire** (`wirerope/wire.py`): The core data object for each function or bound method. It wraps callables and maintains binding information.

2. **WireRope** (`wirerope/rope.py`): The main wrapper interface for callables. It dispatches to different rope mixins based on the callable type:
- `FunctionRopeMixin`: For regular functions and static methods
- `MethodRopeMixin`: For instance and class methods
- `PropertyRopeMixin`: For properties

3. **Callable** (`wirerope/callable.py`): Analyzes and wraps Python callables, detecting their type (function, method, property, etc.)

### Key Concepts

- **Wire objects** are created per binding context:
- For functions/staticmethods: single Wire instance
- For methods: Wire instance per object instance
- For classmethods: Wire instance per class
- For properties: Wire instance per object with special handling

- **Rope** acts as a dispatcher that creates appropriate Wire instances based on the callable type and binding context.

- The library uses `functools.singledispatch` for type-based dispatching and maintains compatibility across Python versions through `_compat.py`.

### Usage Pattern

Users typically:
1. Create a custom Wire subclass with desired behavior
2. Wrap callables with WireRope, passing the Wire class
3. The system automatically creates Wire instances for each binding context

### Rust Implementation

The library includes an optional high-performance Rust implementation:
- Automatically used when available (falls back to Python if not)
- Provides ~30-46% performance improvement
- 100% API compatible with Python implementation
- Check availability: `wirerope._RUST_AVAILABLE`

The Rust implementation uses a thin Python wrapper (`_rust_wrapper.py`) to ensure
full compatibility with functools.wraps and other Python features. This wrapper
maintains complete API compatibility while leveraging Rust's performance benefits.

See RUST_IMPLEMENTATION.md for details.
20 changes: 20 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "wirerope-rust"
version = "1.0.0"
edition = "2021"
authors = ["Jeong, YunWon <wirerope@youknowone.org>"]
license = "BSD-2-Clause"
description = "High-performance Rust implementation of wirerope"

[lib]
name = "_wirerope_rust"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.25", features = ["extension-module", "abi3-py39"] }
parking_lot = "0.12"

[profile.release]
lto = true
codegen-units = 1
opt-level = 3
14 changes: 14 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ See also
example.


Performance
-----------

wirerope includes an optional high-performance Rust implementation that provides
30-46% performance improvements while maintaining 100% compatibility. The Rust
implementation is automatically used when available.

To build with Rust support:

.. code-block:: bash

pip install maturin
maturin develop --release

Python2 support
---------------

Expand Down
85 changes: 85 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Pytest configuration for testing both Rust and Python implementations."""

import pytest
import sys
import importlib


def pytest_addoption(parser):
parser.addoption(
"--implementation",
action="store",
default="both",
choices=["rust", "python", "both"],
help="Run tests with specific implementation: rust, python, or both (default: both)",
)


def pytest_generate_tests(metafunc):
"""Generate tests for both implementations if requested."""
if "implementation" in metafunc.fixturenames:
option_value = metafunc.config.option.implementation
if option_value == "both":
metafunc.parametrize("implementation", ["rust", "python"])
else:
metafunc.parametrize("implementation", [option_value])


def reload_wirerope_with_implementation(use_rust):
"""Reload wirerope module with specified implementation."""
# Remove all wirerope modules from sys.modules
modules_to_remove = [
key for key in sys.modules.keys() if key.startswith("wirerope")
]
for module in modules_to_remove:
del sys.modules[module]

if use_rust:
# Try to import Rust version directly
try:
import wirerope

if not wirerope._RUST_AVAILABLE:
pytest.fail("Rust implementation not available")
return wirerope
except ImportError:
pytest.fail("Rust implementation not available")
else:
# Force Python implementation by blocking Rust import
import unittest.mock

with unittest.mock.patch.dict(sys.modules, {"wirerope._wirerope_rust": None}):
import wirerope

return wirerope


@pytest.fixture
def implementation(request):
"""Fixture that provides the implementation name."""
return request.param if hasattr(request, "param") else "rust"


@pytest.fixture
def wirerope_impl(implementation):
"""Fixture that provides both Rust and Python implementations."""
use_rust = implementation == "rust"
return reload_wirerope_with_implementation(use_rust)


@pytest.fixture
def Wire(wirerope_impl):
"""Wire class from the selected implementation."""
return wirerope_impl.Wire


@pytest.fixture
def WireRope(wirerope_impl):
"""WireRope class from the selected implementation."""
return wirerope_impl.WireRope


@pytest.fixture
def RopeCore(wirerope_impl):
"""RopeCore class from the selected implementation."""
return wirerope_impl.RopeCore
26 changes: 13 additions & 13 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@

# -- Project information -----------------------------------------------------

project = 'wirerope'
copyright = '2020, Jeong YunWon'
author = 'Jeong YunWon'
project = "wirerope"
copyright = "2020, Jeong YunWon"
author = "Jeong YunWon"

version = wirerope.__version__
release = wirerope.__version__
Expand All @@ -33,35 +33,35 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
'sphinx.ext.intersphinx',
'sphinx.ext.githubpages',
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.intersphinx",
"sphinx.ext.githubpages",
]

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]

master_doc = 'index'
master_doc = "index"

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]


# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
html_theme = "alabaster"

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]

intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
"python": ("https://docs.python.org/3", None),
}
Loading
Loading