diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 53d0a68..9fcd327 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -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: | diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000..faf135b --- /dev/null +++ b/.github/workflows/wheels.yml @@ -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 * \ No newline at end of file diff --git a/.gitignore b/.gitignore index 61d7bb4..2579c27 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,10 @@ MANIFEST /build .coverage .pytest_cache + +# Rust +target/ +Cargo.lock +*.rs.bk +*.so +*.pyd diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f15e01c --- /dev/null +++ b/CLAUDE.md @@ -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. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6a4f7f0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "wirerope-rust" +version = "1.0.0" +edition = "2021" +authors = ["Jeong, YunWon "] +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 \ No newline at end of file diff --git a/README.rst b/README.rst index f89563a..723e1f0 100644 --- a/README.rst +++ b/README.rst @@ -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 --------------- diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..fe39732 --- /dev/null +++ b/conftest.py @@ -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 diff --git a/docs/conf.py b/docs/conf.py index 9a72ae8..5c780aa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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__ @@ -33,21 +33,21 @@ # 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 ------------------------------------------------- @@ -55,13 +55,13 @@ # 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), } diff --git a/pyproject.toml b/pyproject.toml index 48005f1..7118362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,50 @@ [build-system] -requires = ["setuptools>=40.8.0", "wheel"] -build-backend = "setuptools.build_meta:__legacy__" +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "wirerope" +version = "1.0.0" +description = "Turn functions and methods into fully controllable objects" +readme = "README.rst" +requires-python = ">=3.9" +license = {text = "BSD-2-Clause"} +authors = [ + {name = "Jeong, YunWon", email = "wirerope@youknowone.org"} +] +classifiers = [ + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + "six>=1.11.0", +] + +[project.optional-dependencies] +test = [ + "pytest>=4.6.7", + "pytest-cov>=2.6.1", + "pytest-checkdocs>=1.2.5;python_version<'3'", + "pytest-checkdocs>=2.9.0;python_version>='3'", + "pytest-benchmark>=3.2.0", +] +doc = [ + "sphinx", +] + +[project.urls] +Homepage = "https://github.com/youknowone/wirerope" + +[tool.maturin] +features = ["pyo3/extension-module"] +python-source = "." +module-name = "wirerope._wirerope_rust" diff --git a/setup.cfg b/setup.cfg index 80dc8cf..28379a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,8 +19,9 @@ classifier = Programming Language :: Python :: 3.13 license = BSD 2-Clause License license_file = LICENSE -description = 'Turn functions and methods into fully controllable objects' +description = Turn functions and methods into fully controllable objects long_description = file: README.rst +long_description_content_type = text/x-rst keywords = ring,methodtools,hack,method [options] packages = wirerope @@ -34,6 +35,7 @@ test = pytest-cov>=2.6.1 pytest-checkdocs>=1.2.5;python_version<"3" pytest-checkdocs>=2.9.0;python_version>="3" + pytest-benchmark>=3.2.0 doc = sphinx diff --git a/setup.py b/setup.py index cf5756f..36f9ecc 100644 --- a/setup.py +++ b/setup.py @@ -5,9 +5,7 @@ get_distribution("setuptools>=39.2.0") except Exception as e: raise AssertionError( - "Please upgrade setuptools by `pip install -U setuptools`: {}".format( - e - ) + "Please upgrade setuptools by `pip install -U setuptools`: {}".format(e) ) setuptools.setup() diff --git a/src/callable.rs b/src/callable.rs new file mode 100644 index 0000000..6d6f0c0 --- /dev/null +++ b/src/callable.rs @@ -0,0 +1,108 @@ +use pyo3::prelude::*; +use std::sync::OnceLock; + +// Cached type objects for fast type checking +static FUNCTION_TYPE: OnceLock = OnceLock::new(); +static METHOD_TYPE: OnceLock = OnceLock::new(); +static PROPERTY_TYPE: OnceLock = OnceLock::new(); + +/// Initialize type cache for fast type checking +fn ensure_type_cache_initialized(py: Python) -> PyResult<()> { + if FUNCTION_TYPE.get().is_none() { + let func_type = py.import("types")?.getattr("FunctionType")?.unbind(); + let _ = FUNCTION_TYPE.set(func_type); + } + + if METHOD_TYPE.get().is_none() { + let method_type = py.import("types")?.getattr("MethodType")?.unbind(); + let _ = METHOD_TYPE.set(method_type); + } + + if PROPERTY_TYPE.get().is_none() { + let property_type = py.import("builtins")?.getattr("property")?.unbind(); + let _ = PROPERTY_TYPE.set(property_type); + } + + Ok(()) +} + +/// Check if a callable is a bare function (not a method) +#[pyfunction] +pub fn is_barefunction(py: Python, func: PyObject) -> PyResult { + ensure_type_cache_initialized(py)?; + let function_type = FUNCTION_TYPE.get().unwrap(); + + if func.bind(py).get_type().is(&function_type.bind(py)) { + // Check qualname == name to detect bare functions + if let (Ok(qualname), Ok(name)) = ( + func.getattr(py, "__qualname__"), + func.getattr(py, "__name__"), + ) { + let qualname_str: String = qualname.extract(py)?; + let name_str: String = name.extract(py)?; + + // Python's logic: split by "." and take the last part + let method_name = qualname_str + .split(".") + .last() + .unwrap_or(&qualname_str); + return Ok(method_name == name_str); + } + } + Ok(false) +} + +/// Check if a callable is a bound method +#[pyfunction] +pub fn is_boundmethod(py: Python, func: PyObject) -> PyResult { + ensure_type_cache_initialized(py)?; + let method_type = METHOD_TYPE.get().unwrap(); + Ok(func.bind(py).get_type().is(&method_type.bind(py))) +} + +/// Check if a callable is a property descriptor +#[pyfunction] +pub fn is_property(py: Python, func: PyObject) -> PyResult { + ensure_type_cache_initialized(py)?; + let property_type = PROPERTY_TYPE.get().unwrap(); + + // Check if it's a builtin property + if func.bind(py).get_type().is(&property_type.bind(py)) { + return Ok(true); + } + + // Check if it has property-like behavior (has __get__ and class name contains 'property') + if func.bind(py).hasattr("__get__")? { + if let Ok(class_name) = func.bind(py).get_type().name() { + if class_name.to_string().contains("property") { + return Ok(true); + } + } + } + + Ok(false) +} + +/// Get the wrapped object (always returns the input) +#[pyfunction] +pub fn get_wrapped_object(_py: Python, func: PyObject) -> PyResult { + Ok(func) +} + +/// Extract the wrapped callable from a descriptor +#[pyfunction] +pub fn get_wrapped_callable(py: Python, func: PyObject) -> PyResult { + // For descriptors, try to extract the actual function + if let Ok(func_attr) = func.getattr(py, "__func__") { + return Ok(func_attr); + } + + if let Ok(fget_attr) = func.getattr(py, "fget") { + if !fget_attr.is_none(py) { + return Ok(fget_attr); + } + } + + // Default: return the object itself + Ok(func) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0ed7b17 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,24 @@ +use pyo3::prelude::*; + +mod callable; +mod rope; +mod rope_core; +mod wire; + +use rope::{CallableFunctionRope, FunctionRope, MethodRope, PropertyRope, WireRope}; +use rope_core::RopeCore; +use wire::Wire; + +/// A Python module implemented in Rust. +#[pymodule] +fn _wirerope_rust(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + + Ok(()) +} diff --git a/src/rope.rs b/src/rope.rs new file mode 100644 index 0000000..8093418 --- /dev/null +++ b/src/rope.rs @@ -0,0 +1,700 @@ +use crate::callable::{get_wrapped_callable, is_barefunction, is_boundmethod, is_property}; +use crate::rope_core::{copy_wrapped_attributes, RopeCore}; +use crate::wire::descriptor_bind; +use parking_lot::Mutex; +use pyo3::exceptions::PyAssertionError; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyTuple}; +use pyo3::PyTypeInfo; +use std::collections::HashMap; +use std::sync::OnceLock; + +/// The main wrapper interface for callables. +/// +/// This is the Rust implementation of WireRope, providing high-performance +/// dispatching for different callable types. +#[pyclass(module = "wirerope._wirerope_rust", dict)] +pub struct WireRope { + #[pyo3(get)] + wire_class: PyObject, + method_rope: PyObject, + property_rope: PyObject, + function_rope: PyObject, + callable_function_rope: PyObject, + #[pyo3(get, name = "_wrapped")] + wrapped: bool, + #[pyo3(get, name = "_args")] + args: Option, +} + +#[pymethods] +impl WireRope { + #[new] + #[pyo3(signature = (wire_class, core_class=None, wraps=false, rope_args=None))] + pub fn new( + py: Python, + wire_class: PyObject, + core_class: Option, + wraps: bool, + rope_args: Option, + ) -> PyResult { + // Get RopeCore if not provided + let _core_class = match core_class { + Some(c) => c, + None => RopeCore::type_object(py).into(), + }; + + // Create rope type classes dynamically + + // Create the rope classes using Rust implementations + let method_rope = MethodRope::type_object(py); + let property_rope = PropertyRope::type_object(py); + let function_rope = FunctionRope::type_object(py); + let callable_function_rope = CallableFunctionRope::type_object(py); + + // Set _wrapped and _args on all rope types + for rope in &[ + &method_rope, + &property_rope, + &function_rope, + &callable_function_rope, + ] { + rope.setattr("_wrapped", wraps)?; + rope.setattr("_args", &rope_args)?; + } + + Ok(WireRope { + wire_class, + method_rope: method_rope.into(), + property_rope: property_rope.into(), + function_rope: function_rope.into(), + callable_function_rope: callable_function_rope.into(), + wrapped: wraps, + args: rope_args, + }) + } + + fn __call__(slf: &Bound<'_, Self>, py: Python, func: PyObject) -> PyResult { + // Use utility functions instead of creating Callable object + let func_is_barefunction = is_barefunction(py, func.clone_ref(py))?; + let func_is_boundmethod = is_boundmethod(py, func.clone_ref(py))?; + let func_is_property = is_property(py, func.clone_ref(py))?; + + // No longer need Callable - pass wrapped_object directly + + let self_ref = slf.borrow(); + + // Determine rope class based on callable type + let rope_class = if func_is_barefunction || func_is_boundmethod { + // Check if wire_class has custom __call__ + let wire_class_call = self_ref.wire_class.getattr(py, "__call__")?; + + // Try to get __qualname__ to check if it's type.__call__ + let is_default_call = if let Ok(qualname) = wire_class_call.getattr(py, "__qualname__") + { + let qualname_str: String = qualname.extract(py).unwrap_or_default(); + qualname_str == "type.__call__" + } else { + false + }; + + if is_default_call { + &self_ref.function_rope + } else { + &self_ref.callable_function_rope + } + } else if func_is_property { + &self_ref.property_rope + } else { + &self_ref.method_rope + }; + + // Create rope instance + let kwargs = PyDict::new(py); + kwargs.set_item("rope", slf)?; + let rope = rope_class.call(py, (func.clone_ref(py),), Some(&kwargs))?; + + // For CallableFunctionRope, always apply functools.wraps to preserve attributes + if rope_class + .bind(py) + .is(self_ref.callable_function_rope.bind(py)) + { + let functools = py.import("functools")?; + let wraps = functools.getattr("wraps")?; + let wrapped_callable = get_wrapped_callable(py, func.clone_ref(py))?; + let wrapper = wraps.call1((wrapped_callable,))?; + Ok(wrapper.call1((rope,))?.into()) + } else if self_ref.wrapped { + // For other rope types, only apply functools.wraps if _wrapped is True + let functools = py.import("functools")?; + let wraps = functools.getattr("wraps")?; + let wrapped_callable = get_wrapped_callable(py, func.clone_ref(py))?; + let wrapper = wraps.call1((wrapped_callable,))?; + Ok(wrapper.call1((rope,))?.into()) + } else { + Ok(rope) + } + } + + fn __setattr__(slf: &Bound, name: &str, value: PyObject) -> PyResult<()> { + // Allow setting attributes on the instance + // This is needed for functools.wraps to work properly + if let Ok(dict) = slf.getattr("__dict__") { + dict.set_item(name, value)?; + } + Ok(()) + } + + fn __getattr__(slf: &Bound, name: &str) -> PyResult { + // First check instance dict + if let Ok(dict) = slf.getattr("__dict__") { + if let Ok(value) = dict.get_item(name) { + if !value.is_none() { + return Ok(value.unbind()); + } + } + } + + // Fallback to default behavior + let class_name = slf.get_type().name()?; + Err(pyo3::exceptions::PyAttributeError::new_err(format!( + "'{}' object has no attribute '{}'", + class_name, name + ))) + } +} + +/// MethodRope implementation combining mixin functionality with RopeCore +#[pyclass(module = "wirerope._wirerope_rust", extends=RopeCore, dict)] +pub struct MethodRope { + #[pyo3(get, set)] + wire_name: Option, + #[pyo3(get, name = "_wrapped")] + wrapped: bool, + // Cached attributes for performance + cached_wrapped_object: OnceLock, + cached_wrapped_callable_name: OnceLock, + cached_wire_class: OnceLock, + // Wire cache: cache_key -> wire (optimized single key) + wire_cache: Mutex>, +} + +#[pymethods] +impl MethodRope { + #[new] + #[pyo3(signature = (wrapped_object, *, rope))] + pub fn new(wrapped_object: PyObject, rope: PyObject) -> PyResult<(Self, RopeCore)> { + Python::with_gil(|py| { + // Check if it's a barefunction using utility function + let is_barefunction = + crate::callable::is_barefunction(py, wrapped_object.clone_ref(py))?; + if is_barefunction { + return Err(PyAssertionError::new_err("Expected not barefunction")); + } + + let wrapped = rope.getattr(py, "_wrapped")?.extract::(py)?; + + Ok(( + MethodRope { + wire_name: None, + wrapped, + cached_wrapped_object: OnceLock::new(), + cached_wrapped_callable_name: OnceLock::new(), + cached_wire_class: OnceLock::new(), + wire_cache: Mutex::new(HashMap::new()), + }, + RopeCore { + wrapped_object, + rope, + }, + )) + }) + } + + fn __set_name__(&mut self, owner: &Bound<'_, PyAny>, name: &str) -> PyResult<()> { + let owner_name = owner.getattr("__name__")?.extract::()?; + self.wire_name = Some(format!("__wire|{}|{}", owner_name, name)); + Ok(()) + } + + fn __setattr__(slf: &Bound, name: &str, value: PyObject) -> PyResult<()> { + // Allow setting attributes on the instance + // This is needed for functools.wraps to work properly + if let Ok(dict) = slf.getattr("__dict__") { + dict.set_item(name, value)?; + } + Ok(()) + } + + fn __getattr__(slf: &Bound, name: &str) -> PyResult { + // First check instance dict + if let Ok(dict) = slf.getattr("__dict__") { + if let Ok(value) = dict.get_item(name) { + if !value.is_none() { + return Ok(value.unbind()); + } + } + } + + // Fallback to default behavior + let class_name = slf.get_type().name()?; + Err(pyo3::exceptions::PyAttributeError::new_err(format!( + "'{}' object has no attribute '{}'", + class_name, name + ))) + } + + fn __get__( + slf: PyRef, + obj: &Bound<'_, PyAny>, + type_: Option<&Bound<'_, PyAny>>, + ) -> PyResult { + let py = obj.py(); + let base: &RopeCore = slf.as_ref(); + + // Fast path: Check Rust-side cache first + // Optimized key: use obj_ptr when available, otherwise type_ptr + let obj_ptr = obj.as_ptr() as usize; + let wire_key = if obj_ptr > 0 { + obj_ptr // Instance method: use object pointer + } else { + type_.map_or(0, |t| t.as_ptr() as usize) // Class/static method: use type pointer + }; + + // Try to get from cache + let cache = slf.wire_cache.lock(); + if let Some(wire) = cache.get(&wire_key) { + // Wire found in Rust cache, return directly + return Ok(wire.clone_ref(py)); + } + drop(cache); // Explicitly drop the lock + + // Slow path: wire not cached, need to create it + // Get wrapped_object directly from RopeCore + let co = slf.cached_wrapped_object.get_or_init(|| { + // Now we have wrapped_object directly in RopeCore + base.wrapped_object.clone_ref(py) + }); + + // Use descriptor_bind function + let obj_py = obj.clone().unbind(); + let type_py = type_ + .map(|t| t.clone().unbind()) + .unwrap_or_else(|| py.None()); + let (owner, _) = descriptor_bind(py, co, &obj_py, &type_py)?; + + let owner = if owner.is_none(py) { + if !obj.is_none() { + obj.clone().unbind() + } else if let Some(t) = type_ { + t.clone().unbind() + } else { + obj.clone().unbind() + } + } else { + owner + }; + + let wire_name = if let Some(ref name) = slf.wire_name { + name.clone() + } else { + // Use cached wrapped_callable name + let func_name = slf.cached_wrapped_callable_name.get_or_init(|| { + let wrapped_callable = + crate::callable::get_wrapped_callable(py, base.wrapped_object.clone_ref(py)) + .unwrap(); + let name: String = wrapped_callable + .getattr(py, "__name__") + .unwrap() + .extract(py) + .unwrap(); + name + }); + + let mut wire_name_str = format!("__wire_{}", func_name); + if let Some(t) = type_ { + if owner.bind(py).is(t) { + let type_name: String = t.getattr("__name__")?.extract()?; + wire_name_str.push('_'); + wire_name_str.push_str(&type_name); + } + } + wire_name_str + }; + + // Try to find existing wire one more time (for dynamically named wires) + let wire = if slf.wire_name.is_none() { + owner + .getattr(py, wire_name.as_str()) + .unwrap_or_else(|_| py.None()) + } else { + py.None() + }; + + let wire = if wire.is_none(py) { + // Use cached wire_class + let wire_class = slf + .cached_wire_class + .get_or_init(|| base.rope.getattr(py, "wire_class").unwrap()); + + let binding = ( + obj.clone().unbind(), + type_ + .map(|t| t.clone().unbind()) + .unwrap_or_else(|| py.None()), + ); + let base_py: PyObject = Py::new(py, base.clone())?.into_any(); + let new_wire = wire_class.call1(py, (base_py, &owner, binding))?; + + if slf.wrapped { + copy_wrapped_attributes(py, co, &new_wire)?; + } + + owner.setattr(py, wire_name.as_str(), &new_wire)?; + + // Store in Rust cache + { + let mut cache = slf.wire_cache.lock(); + cache.insert(wire_key, new_wire.clone_ref(py)); + } + + new_wire + } else { + wire + }; + + Ok(wire) + } +} + +/// PropertyRope implementation +#[pyclass(module = "wirerope._wirerope_rust", dict, extends=RopeCore)] +pub struct PropertyRope { + #[pyo3(get, set)] + wire_name: Option, + #[pyo3(get, name = "_wrapped")] + wrapped: bool, + // Cached attributes for performance + cached_wrapped_object: OnceLock, + cached_wire_class: OnceLock, + cached_property_fget: OnceLock>, + // Wire cache: cache_key -> wire (optimized single key) + wire_cache: Mutex>, +} + +#[pymethods] +impl PropertyRope { + #[new] + #[pyo3(signature = (wrapped_object, *, rope))] + pub fn new(wrapped_object: PyObject, rope: PyObject) -> PyResult<(Self, RopeCore)> { + Python::with_gil(|py| { + // Check if it's a barefunction using utility function + let is_barefunction = + crate::callable::is_barefunction(py, wrapped_object.clone_ref(py))?; + if is_barefunction { + return Err(PyAssertionError::new_err("Expected not barefunction")); + } + + let wrapped = rope.getattr(py, "_wrapped")?.extract::(py)?; + + Ok(( + PropertyRope { + wire_name: None, + wrapped, + cached_wrapped_object: OnceLock::new(), + cached_wire_class: OnceLock::new(), + cached_property_fget: OnceLock::new(), + wire_cache: Mutex::new(HashMap::new()), + }, + RopeCore { + wrapped_object, + rope, + }, + )) + }) + } + + fn __set_name__(&mut self, owner: &Bound<'_, PyAny>, name: &str) -> PyResult<()> { + let owner_name = owner.getattr("__name__")?.extract::()?; + self.wire_name = Some(format!("__wire|{}|{}", owner_name, name)); + Ok(()) + } + + fn __get__( + slf: PyRef, + obj: &Bound<'_, PyAny>, + type_: Option<&Bound<'_, PyAny>>, + ) -> PyResult { + let py = obj.py(); + let base: &RopeCore = slf.as_ref(); + + // Special handling for class access (obj is None) + if obj.is_none() { + // Check if it's a hybridproperty (executes immediately for class access) + // vs regular property (returns property object for class access) + let co = slf + .cached_wrapped_object + .get_or_init(|| base.wrapped_object.clone_ref(py)); + + // Check if it might be a hybridproperty-like descriptor + if let Ok(class_name) = co.bind(py).get_type().name() { + if class_name.to_string().contains("property") && class_name != "property" { + // It's likely a hybridproperty, call __get__ to get the result + if let Some(t) = type_ { + return co.call_method1(py, "__get__", (py.None(), t)); + } + } + } + + // For regular properties, return the property object itself + return Ok(co.clone_ref(py)); + } + + // Fast path: try to get cached property getter and call it directly + let property_fget = slf.cached_property_fget.get_or_init(|| { + let wrapped_object = base.wrapped_object.clone_ref(py); + + // First try fget (standard property), then __func__ (custom descriptors) + if let Ok(fget) = wrapped_object.getattr(py, "fget") { + Some(fget) + } else if let Ok(func) = wrapped_object.getattr(py, "__func__") { + Some(func) + } else { + None + } + }); + + // If we have a direct property getter, use it for maximum performance + if let Some(ref fget) = property_fget { + return fget.call1(py, (obj,)); + } + + // Fallback to full Wire creation for complex properties + let obj_ptr = obj.as_ptr() as usize; + let wire_key = if obj_ptr > 0 { + obj_ptr // Instance property: use object pointer + } else { + type_.map_or(0, |t| t.as_ptr() as usize) // Class property: use type pointer + }; + + // Try to get from cache + let cache = slf.wire_cache.lock(); + if let Some(wire) = cache.get(&wire_key) { + let wire_obj = wire.clone_ref(py); + drop(cache); + return wire_obj + .bind(py) + .call_method0("_on_property") + .map(|r| r.unbind()); + } + drop(cache); + + // Slow path: wire not cached, need to create it + let co = slf + .cached_wrapped_object + .get_or_init(|| base.wrapped_object.clone_ref(py)); + + let obj_py = obj.clone().unbind(); + let type_py = type_ + .map(|t| t.clone().unbind()) + .unwrap_or_else(|| py.None()); + let (owner, _) = descriptor_bind(py, co, &obj_py, &type_py)?; + + let owner = if owner.is_none(py) { + obj.clone().unbind() + } else { + owner + }; + + let wire_name = if let Some(ref name) = slf.wire_name { + name.clone() + } else { + let wrapped_callable = + crate::callable::get_wrapped_callable(py, base.wrapped_object.clone_ref(py))?; + let func_name: String = wrapped_callable.getattr(py, "__name__")?.extract(py)?; + format!("__wire_{}", func_name) + }; + + // Create new wire + let wire_class = slf + .cached_wire_class + .get_or_init(|| base.rope.getattr(py, "wire_class").unwrap()); + + let binding = ( + obj.clone().unbind(), + type_ + .map(|t| t.clone().unbind()) + .unwrap_or_else(|| py.None()), + ); + let base_py: PyObject = Py::new(py, base.clone())?.into_any(); + let new_wire = wire_class.call1(py, (base_py, &owner, binding))?; + + if slf.wrapped { + copy_wrapped_attributes(py, co, &new_wire)?; + } + + owner.setattr(py, wire_name.as_str(), &new_wire)?; + + // Store in Rust cache + { + let mut cache = slf.wire_cache.lock(); + cache.insert(wire_key, new_wire.clone_ref(py)); + } + + new_wire + .bind(py) + .call_method0("_on_property") + .map(|r| r.unbind()) + } + + fn __setattr__(slf: &Bound, name: &str, value: PyObject) -> PyResult<()> { + // Allow setting attributes on the instance + // This is needed for functools.wraps to work properly + if let Ok(dict) = slf.getattr("__dict__") { + dict.set_item(name, value)?; + } + Ok(()) + } + + fn __getattr__(slf: &Bound, name: &str) -> PyResult { + // First check instance dict + if let Ok(dict) = slf.getattr("__dict__") { + if let Ok(value) = dict.get_item(name) { + if !value.is_none() { + return Ok(value.unbind()); + } + } + } + + // Fallback to default behavior + let class_name = slf.get_type().name()?; + Err(pyo3::exceptions::PyAttributeError::new_err(format!( + "'{}' object has no attribute '{}'", + class_name, name + ))) + } +} + +#[pyclass(module = "wirerope._wirerope_rust", dict, extends=RopeCore)] +pub struct FunctionRope { + #[pyo3(get, name = "_wire")] + wire: PyObject, + #[pyo3(get, name = "_wrapped")] + wrapped: bool, +} + +#[pymethods] +impl FunctionRope { + #[new] + #[pyo3(signature = (wrapped_object, *, rope))] + pub fn new(wrapped_object: PyObject, rope: PyObject) -> PyResult<(Self, RopeCore)> { + Python::with_gil(|py| { + let is_barefunction = + crate::callable::is_barefunction(py, wrapped_object.clone_ref(py))?; + let is_boundmethod = crate::callable::is_boundmethod(py, wrapped_object.clone_ref(py))?; + if !is_barefunction && !is_boundmethod { + return Err(PyAssertionError::new_err( + "Expected barefunction or boundmethod", + )); + } + + let wrapped = rope.getattr(py, "_wrapped")?.extract::(py)?; + let wire_class = rope.getattr(py, "wire_class")?; + let base_core = RopeCore { + wrapped_object: wrapped_object.clone_ref(py), + rope: rope.clone_ref(py), + }; + let base_py: PyObject = Py::new(py, base_core.clone())?.into_any(); + let wire = wire_class.call1(py, (base_py, py.None(), py.None()))?; + + if wrapped { + let wrapped_obj = wrapped_object.clone_ref(py); + copy_wrapped_attributes(py, &wrapped_obj, &wire)?; + } + + Ok((FunctionRope { wire, wrapped }, base_core)) + }) + } + + fn __getattr__(slf: &Bound, name: &str) -> PyResult { + let py = slf.py(); + // First check instance dict + if let Ok(dict) = slf.getattr("__dict__") { + if let Ok(value) = dict.get_item(name) { + if !value.is_none() { + return Ok(value.unbind()); + } + } + } + + // For all other attributes, delegate to wire + let wire = slf.borrow().wire.clone_ref(py); + wire.getattr(py, name) + } + + fn __setattr__(slf: &Bound, name: &str, value: PyObject) -> PyResult<()> { + // Allow setting attributes on the instance + // This is needed for functools.wraps to work properly + if let Ok(dict) = slf.getattr("__dict__") { + dict.set_item(name, value)?; + } + Ok(()) + } +} + +#[pyclass(module = "wirerope._wirerope_rust", dict, extends=RopeCore)] +pub struct CallableFunctionRope { + #[pyo3(get, name = "_wire")] + wire: PyObject, + #[pyo3(get, name = "_wrapped")] + wrapped: bool, +} + +#[pymethods] +impl CallableFunctionRope { + #[new] + #[pyo3(signature = (wrapped_object, *, rope))] + pub fn new(wrapped_object: PyObject, rope: PyObject) -> PyResult<(Self, RopeCore)> { + Python::with_gil(|py| { + let is_barefunction = + crate::callable::is_barefunction(py, wrapped_object.clone_ref(py))?; + let is_boundmethod = crate::callable::is_boundmethod(py, wrapped_object.clone_ref(py))?; + if !is_barefunction && !is_boundmethod { + return Err(PyAssertionError::new_err( + "Expected barefunction or boundmethod", + )); + } + + let wrapped = rope.getattr(py, "_wrapped")?.extract::(py)?; + let wire_class = rope.getattr(py, "wire_class")?; + let base_core = RopeCore { + wrapped_object: wrapped_object.clone_ref(py), + rope: rope.clone_ref(py), + }; + let base_py: PyObject = Py::new(py, base_core.clone())?.into_any(); + let wire = wire_class.call1(py, (base_py, py.None(), py.None()))?; + + if wrapped { + let wrapped_obj = wrapped_object.clone_ref(py); + copy_wrapped_attributes(py, &wrapped_obj, &wire)?; + } + + Ok((CallableFunctionRope { wire, wrapped }, base_core)) + }) + } + + fn __getattr__(slf: PyRef, py: Python, name: &str) -> PyResult { + // For all attributes, delegate to wire + slf.wire.getattr(py, name) + } + + #[pyo3(signature = (*args, **kwargs))] + fn __call__( + slf: PyRef, + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, + ) -> PyResult { + let py = slf.py(); + slf.wire.call(py, args, kwargs) + } +} diff --git a/src/rope_core.rs b/src/rope_core.rs new file mode 100644 index 0000000..2bc357c --- /dev/null +++ b/src/rope_core.rs @@ -0,0 +1,95 @@ +use pyo3::prelude::*; + +/// The base rope object - Rust implementation of RopeCore +#[pyclass(module = "wirerope._wirerope_rust", dict, subclass)] +pub struct RopeCore { + #[pyo3(get)] + pub wrapped_object: PyObject, + #[pyo3(get)] + pub rope: PyObject, +} + +impl Clone for RopeCore { + fn clone(&self) -> Self { + Python::with_gil(|py| RopeCore { + wrapped_object: self.wrapped_object.clone_ref(py), + rope: self.rope.clone_ref(py), + }) + } +} + +#[pymethods] +impl RopeCore { + #[new] + pub fn new(wrapped_object: PyObject, rope: PyObject) -> Self { + RopeCore { + wrapped_object, + rope, + } + } + + #[getter] + fn wire_class(&self, py: Python) -> PyResult { + self.rope.getattr(py, "wire_class") + } + + fn __setattr__(slf: &Bound, name: &str, value: PyObject) -> PyResult<()> { + // Allow setting attributes on the instance + // This is needed for functools.wraps to work properly + if let Ok(dict) = slf.getattr("__dict__") { + dict.set_item(name, value)?; + } + Ok(()) + } + + fn __getattr__(slf: &Bound, name: &str) -> PyResult { + // First check instance dict + if let Ok(dict) = slf.getattr("__dict__") { + if let Ok(value) = dict.get_item(name) { + if !value.is_none() { + return Ok(value.unbind()); + } + } + } + + // Fallback to default behavior + let class_name = slf.get_type().name()?; + Err(pyo3::exceptions::PyAttributeError::new_err(format!( + "'{}' object has no attribute '{}'", + class_name, name + ))) + } +} + +/// Helper function to copy wrapped attributes +pub fn copy_wrapped_attributes( + py: Python, + wrapped_obj: &PyObject, + target: &PyObject, +) -> PyResult<()> { + let attrs = vec![ + "__doc__", + "__name__", + "__module__", + "__qualname__", + "__annotations__", + ]; + + // For hybridmethod/hybridproperty, we need to get attributes from __func__ + let source_obj = if wrapped_obj.getattr(py, "__func__").is_ok() { + wrapped_obj.getattr(py, "__func__")? + } else { + wrapped_obj.clone_ref(py) + }; + + // Copy attributes from source object to target + for attr in attrs { + if let Ok(value) = source_obj.getattr(py, attr) { + if !value.is_none(py) { + let _ = target.setattr(py, attr, value); + } + } + } + + Ok(()) +} diff --git a/src/wire.rs b/src/wire.rs new file mode 100644 index 0000000..6326aed --- /dev/null +++ b/src/wire.rs @@ -0,0 +1,314 @@ +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyTuple}; +use std::sync::OnceLock; + +// Cached type objects for fast pointer comparison +static FUNCTION_TYPE: OnceLock = OnceLock::new(); +static CLASSMETHOD_TYPE: OnceLock = OnceLock::new(); +static STATICMETHOD_TYPE: OnceLock = OnceLock::new(); + +/// Initialize type cache for fast type checking +fn ensure_type_cache_initialized(py: Python) -> PyResult<()> { + // Initialize function type + if FUNCTION_TYPE.get().is_none() { + let func_type = py.import("types")?.getattr("FunctionType")?.unbind(); + let _ = FUNCTION_TYPE.set(func_type); + } + + // Initialize classmethod type + if CLASSMETHOD_TYPE.get().is_none() { + let cls_type = py.import("builtins")?.getattr("classmethod")?.unbind(); + let _ = CLASSMETHOD_TYPE.set(cls_type); + } + + // Initialize staticmethod type + if STATICMETHOD_TYPE.get().is_none() { + let static_type = py.import("builtins")?.getattr("staticmethod")?.unbind(); + let _ = STATICMETHOD_TYPE.set(static_type); + } + + Ok(()) +} + +/// Fast type checking using cached type objects +fn is_function_type(py: Python, obj: &PyObject) -> PyResult { + ensure_type_cache_initialized(py)?; + let function_type = FUNCTION_TYPE.get().unwrap(); + Ok(obj.bind(py).get_type().is(&function_type.bind(py))) +} + +fn is_classmethod_type(py: Python, obj: &PyObject) -> PyResult { + ensure_type_cache_initialized(py)?; + let classmethod_type = CLASSMETHOD_TYPE.get().unwrap(); + Ok(obj.bind(py).get_type().is(&classmethod_type.bind(py))) +} + +fn is_staticmethod_type(py: Python, obj: &PyObject) -> PyResult { + ensure_type_cache_initialized(py)?; + let staticmethod_type = STATICMETHOD_TYPE.get().unwrap(); + Ok(obj.bind(py).get_type().is(&staticmethod_type.bind(py))) +} + +/// Rust implementation of descriptor_bind logic +/// Returns (owner, binder) tuple like Python version +pub fn descriptor_bind( + py: Python, + descriptor: &PyObject, + obj: &PyObject, + type_: &PyObject, +) -> PyResult<(PyObject, Option)> { + // Fast path: use cached type checking (no string comparisons!) + if is_function_type(py, descriptor)? { + return Ok((obj.clone_ref(py), Some(obj.clone_ref(py)))); + } + + if is_classmethod_type(py, descriptor)? { + return Ok((type_.clone_ref(py), Some(type_.clone_ref(py)))); + } + + if is_staticmethod_type(py, descriptor)? { + return Ok((type_.clone_ref(py), None)); + } + + // For other descriptors, we need to call __get__ and analyze the result + let method = descriptor.call_method1(py, "__get__", (obj, type_))?; + + // Check if it's a function (not a bound method) - probably staticmethod + let method_type = method.bind(py).get_type(); + let method_type_name = method_type.name()?; + + if method_type_name == "function" { + return Ok((type_.clone_ref(py), None)); + } + + // Check if method is obj itself + if method.bind(py).is(obj.bind(py)) { + return Ok((obj.clone_ref(py), Some(obj.clone_ref(py)))); + } + + // Check if method is type itself + if method.bind(py).is(type_.bind(py)) { + return Ok((type_.clone_ref(py), Some(type_.clone_ref(py)))); + } + + // For bound methods, check __self__ to determine owner + if method_type_name == "method" { + if let Ok(self_attr) = method.getattr(py, "__self__") { + if self_attr.bind(py).is(obj.bind(py)) { + return Ok((obj.clone_ref(py), Some(obj.clone_ref(py)))); + } else if self_attr.bind(py).is(type_.bind(py)) { + return Ok((type_.clone_ref(py), Some(type_.clone_ref(py)))); + } + } + } + + // Check if method is the descriptor itself (non-method descriptor) + if method.bind(py).is(descriptor.bind(py)) { + return Ok((type_.clone_ref(py), None)); + } + + // Default case - return type with no binder + Ok((type_.clone_ref(py), None)) +} + +/// The core data object for each function or bound method. +/// +/// This is the Rust implementation of the Wire class, providing +/// high-performance method wrapping functionality. +#[pyclass(subclass, dict, module = "wirerope._wirerope_rust")] +pub struct Wire { + #[pyo3(get, name = "_rope")] + rope: PyObject, + #[pyo3(get, name = "_wrapped_object")] + wrapped_object: PyObject, + #[pyo3(get, name = "_binding")] + binding: Option<(PyObject, PyObject)>, + #[pyo3(get)] + __func__: PyObject, + #[pyo3(get, name = "_owner")] + owner: PyObject, + #[pyo3(get, name = "_bound_objects")] + bound_objects: Py, + // Store wrapped attributes if rope._wrapped is True + #[pyo3(get)] + __doc__: Option, + #[pyo3(get)] + __name__: Option, + #[pyo3(get)] + __module__: Option, + // Cache property fget for direct access + property_fget: Option, +} + +#[pymethods] +impl Wire { + #[new] + pub fn new( + rope: PyObject, + owner: PyObject, + binding: Option<(PyObject, PyObject)>, + ) -> PyResult { + Python::with_gil(|py| { + // Get wrapped_object from rope + let wrapped_object = rope.getattr(py, "wrapped_object")?; + + // Determine __func__ and property_fget based on binding + let is_property = crate::callable::is_property(py, wrapped_object.clone_ref(py))?; + let (func, property_fget) = if let Some(ref binding_tuple) = binding { + if is_property { + // For properties, extract the getter function + // Try fget first (for regular property), then __func__ (for custom descriptors) + let fget = if let Ok(fget_attr) = wrapped_object.getattr(py, "fget") { + fget_attr + } else if let Ok(func_attr) = wrapped_object.getattr(py, "__func__") { + func_attr + } else { + return Err(pyo3::exceptions::PyAttributeError::new_err( + "Property-like descriptor has neither 'fget' nor '__func__' attribute", + )); + }; + // Store fget for direct access in _on_property + let property_fget = Some(fget.clone_ref(py)); + + // Create a bound method for __func__ + // This simulates functools.partial behavior + let get_method = wrapped_object.getattr(py, "__get__")?; + let bound_func = get_method.call1(py, (&binding_tuple.0, &binding_tuple.1))?; + (bound_func, property_fget) + } else { + // For methods, call __get__ + let func = wrapped_object.call_method1( + py, + "__get__", + (&binding_tuple.0, &binding_tuple.1), + )?; + (func, None) + } + } else { + // For functions without binding + let obj_type = wrapped_object.bind(py).get_type(); + let type_name = obj_type.name()?; + + // Check if it's a classmethod or staticmethod that needs unwrapping + // (but NOT a bound method, which also has __func__) + if (type_name == "classmethod" || type_name == "staticmethod") + && wrapped_object.bind(py).hasattr("__func__")? + { + // It's a classmethod or staticmethod, get the underlying function + let func = wrapped_object.getattr(py, "__func__")?; + (func, None) + } else { + // Regular function or bound method, use wrapped_object directly + (wrapped_object.clone_ref(py), None) + } + }; + + // Calculate bound_objects using Rust descriptor_bind + let bound_objects = if let Some(ref binding_tuple) = binding { + // Use Rust implementation of descriptor_bind + let (_, binder) = + descriptor_bind(py, &wrapped_object, &binding_tuple.0, &binding_tuple.1)?; + + // If binder is not None, add it to bound_objects + if let Some(binder_obj) = binder { + PyTuple::new(py, vec![binder_obj]) + } else { + Ok(PyTuple::empty(py)) + } + } else { + Ok(PyTuple::empty(py)) + }? + .unbind(); + + // Check if rope._wrapped is True + let wrapped: bool = rope + .getattr(py, "_wrapped") + .unwrap_or(py.None()) + .extract(py) + .unwrap_or(false); + + // Extract attributes if wrapped + let (__doc__, __name__, __module__) = if wrapped { + // Try to get attributes from wrapped_object + let mut doc: Option = None; + let mut name: Option = None; + let mut module: Option = None; + + if let Ok(doc_obj) = wrapped_object.getattr(py, "__doc__") { + if !doc_obj.is_none(py) { + doc = doc_obj.extract(py).ok(); + } + } + + if let Ok(name_obj) = wrapped_object.getattr(py, "__name__") { + if !name_obj.is_none(py) { + name = name_obj.extract(py).ok(); + } + } + + if let Ok(module_obj) = wrapped_object.getattr(py, "__module__") { + if !module_obj.is_none(py) { + module = module_obj.extract(py).ok(); + } + } + + (doc, name, module) + } else { + (None, None, None) + }; + + // Check if __func__ is callable (like Python's assert callable(self.__func__)) + if !func.bind(py).is_callable() && !is_property { + let type_name = func + .bind(py) + .get_type() + .name() + .map(|n| n.to_string()) + .unwrap_or_else(|_| "".to_string()); + return Err(pyo3::exceptions::PyTypeError::new_err(format!( + "Expected callable __func__, got '{}'", + type_name + ))); + } + + let wire = Wire { + rope, + wrapped_object, + binding, + __func__: func, + owner, + bound_objects, + __doc__, + __name__, + __module__, + property_fget, + }; + + Ok(wire) + }) + } + + // Note: __call__ is intentionally not implemented to maintain compatibility + // with Python implementation where Wire objects are not directly callable + + fn _on_property(&self) -> PyResult { + Python::with_gil(|py| { + // Optimized property access - use cached property getter when possible + if let Some(ref binding) = self.binding { + // Check if we have a cached property getter for direct access + if let Some(ref property_fget) = self.property_fget { + // Direct call to the cached getter with self + return property_fget.call1(py, (&binding.0,)); + } + + // Fallback: call __get__ on the wrapped_object (the property descriptor) + // This is needed for built-in properties that don't have fget + self.wrapped_object + .call_method1(py, "__get__", (&binding.0, &binding.1)) + } else { + // No binding, call the function directly + self.__func__.call0(py) + } + }) + } +} diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py new file mode 100644 index 0000000..1c0755d --- /dev/null +++ b/tests/test_benchmark.py @@ -0,0 +1,302 @@ +"""Unified benchmark tests for wirerope implementations.""" + +import pytest +import sys +import importlib + + +class TestUnifiedBenchmark: + """Unified benchmark tests comparing all implementations.""" + + def setup_method(self): + """Setup all test implementations.""" + # Force reload to test both Python and Rust implementations + modules_to_remove = [ + "wirerope", + "wirerope._wirerope_rust", + "wirerope.wire", + "wirerope.rope", + "wirerope.callable", + ] + for module in modules_to_remove: + if module in sys.modules: + del sys.modules[module] + + # Import Python version by blocking Rust import + import unittest.mock + + with unittest.mock.patch.dict(sys.modules, {"wirerope._wirerope_rust": None}): + wirerope_py = importlib.import_module("wirerope") + self.WireRope_py = wirerope_py.WireRope + self.Wire_py = wirerope_py.Wire + + # Clean up for Rust import + for module in modules_to_remove: + if module in sys.modules: + del sys.modules[module] + + # Import Rust version (current default) + wirerope = importlib.import_module("wirerope") + self.WireRope = wirerope.WireRope + self.Wire = wirerope.Wire + self.rust_available = wirerope._RUST_AVAILABLE + + if self.rust_available: + self.WireRope_rust = wirerope.WireRope + self.Wire_rust = wirerope.Wire + else: + self.WireRope_rust = self.WireRope_py + self.Wire_rust = self.Wire_py + + # Define Wire implementations + class SimpleWire(self.Wire): + """Simple callable wire for current implementation.""" + + def __call__(self, *args, **kwargs): + return self.__func__(*args, **kwargs) + + class CallableWirePy(self.Wire_py): + """Callable wire for Python implementation.""" + + def __call__(self, *args, **kwargs): + return self.__func__(*args, **kwargs) + + class CallableWireRust(self.Wire_rust): + """Callable wire for Rust implementation.""" + + def __call__(self, *args, **kwargs): + return self.__func__(*args, **kwargs) + + # Store Wire classes + self.SimpleWire = SimpleWire + self.CallableWirePy = CallableWirePy + self.CallableWireRust = CallableWireRust + + # Define test classes + self._setup_test_classes() + + def _setup_test_classes(self): + """Setup all test class variations.""" + + # Regular (unwrapped) class + class Regular: + def method(self): + return 42 + + @classmethod + def classmethod(cls): + return cls + + @staticmethod + def staticmethod(): + return "static" + + @property + def prop(self): + return "property" + + + # Python with default Wire + class PythonDefault: + @self.WireRope_py(self.Wire_py) + def method(self): + return 42 + + @self.WireRope_py(self.Wire_py) + @classmethod + def classmethod(cls): + return cls + + @self.WireRope_py(self.Wire_py) + @staticmethod + def staticmethod(): + return "static" + + @self.WireRope_py(self.Wire_py) + @property + def prop(self): + return "property" + + # Rust with default Wire + class RustDefault: + @self.WireRope_rust(self.Wire_rust) + def method(self): + return 42 + + @self.WireRope_rust(self.Wire_rust) + @classmethod + def classmethod(cls): + return cls + + @self.WireRope_rust(self.Wire_rust) + @staticmethod + def staticmethod(): + return "static" + + @self.WireRope_rust(self.Wire_rust) + @property + def prop(self): + return "property" + + # Python with callable Wire + class PythonCallable: + @self.WireRope_py(self.CallableWirePy) + def method(self): + return 42 + + @self.WireRope_py(self.CallableWirePy) + @classmethod + def classmethod(cls): + return cls + + @self.WireRope_py(self.CallableWirePy) + @staticmethod + def staticmethod(): + return "static" + + @self.WireRope_py(self.CallableWirePy) + @property + def prop(self): + return "property" + + # Rust with callable Wire + class RustCallable: + @self.WireRope_rust(self.CallableWireRust) + def method(self): + return 42 + + @self.WireRope_rust(self.CallableWireRust) + @classmethod + def classmethod(cls): + return cls + + @self.WireRope_rust(self.CallableWireRust) + @staticmethod + def staticmethod(): + return "static" + + @self.WireRope_rust(self.CallableWireRust) + @property + def prop(self): + return "property" + + # Store classes and instances + self.Regular = Regular + self.PythonDefault = PythonDefault + self.RustDefault = RustDefault + self.PythonCallable = PythonCallable + self.RustCallable = RustCallable + + self.regular = Regular() + self.python_default = PythonDefault() + self.rust_default = RustDefault() + self.python_callable = PythonCallable() + self.rust_callable = RustCallable() + + + # Group 2: Three-way comparison with callable Wire + @pytest.mark.benchmark(group="three-way-callable") + @pytest.mark.parametrize( + "impl,method_type", + [ + ("regular", "method"), + ("python", "method"), + ("rust", "method"), + ("regular", "classmethod"), + ("python", "classmethod"), + ("rust", "classmethod"), + ("regular", "staticmethod"), + ("python", "staticmethod"), + ("rust", "staticmethod"), + ("regular", "property"), + ("python", "property"), + ("rust", "property"), + ], + ) + def test_three_way_callable(self, benchmark, impl, method_type): + """Compare regular vs Python vs Rust with callable Wire.""" + if not self.rust_available and impl == "rust": + pytest.skip("Rust implementation not available") + + # Select implementation + if impl == "regular": + obj = self.regular + cls = self.Regular + elif impl == "python": + obj = self.python_callable + cls = self.PythonCallable + else: # rust + obj = self.rust_callable + cls = self.RustCallable + + # Pre-warm cache by accessing each method once + if method_type == "method": + _ = obj.method() # Warm up cache + def func(): + # Use the same object repeatedly (cache hits) + total = 0 + for _ in range(100): + total += obj.method() + return total + elif method_type == "classmethod": + _ = cls.classmethod() # Warm up cache + def func(): + # Use the same class repeatedly (cache hits) + result = None + for _ in range(100): + result = cls.classmethod() + return result + elif method_type == "staticmethod": + _ = cls.staticmethod() # Warm up cache + def func(): + # Use the same class repeatedly (cache hits) + result = None + for _ in range(100): + result = cls.staticmethod() + return result + else: # property + _ = obj.prop # Warm up cache + def func(): + # Use the same object repeatedly (cache hits) + result = None + for _ in range(100): + result = obj.prop + return result + + # Run benchmark - now mostly cache hits + benchmark.group = f"{method_type}-three-way" + benchmark.name = f"{method_type}_{impl}" + result = benchmark(func) + + # Verify result + if method_type == "classmethod": + assert result == cls + elif method_type == "property": + assert result == "property" + elif method_type == "staticmethod": + assert result == "static" + else: + assert result == 4200 # 100 calls * 42 + + + # Group 4: First call vs cached call + @pytest.mark.benchmark(group="first-vs-cached") + def test_first_call_vs_cached(self, benchmark): + """Benchmark first call (wire creation) vs cached calls.""" + + class TestClass: + @self.WireRope(self.SimpleWire) + def method(self): + return 42 + + def first_and_cached_calls(): + instance = TestClass() + # First call - creates wire + result1 = instance.method() + # Cached calls + for _ in range(9): + instance.method() + return result1 + + result = benchmark(first_and_cached_calls) + assert result == 42 diff --git a/tests/test_callable.py b/tests/test_callable.py index 9936c4f..300b8d4 100644 --- a/tests/test_callable.py +++ b/tests/test_callable.py @@ -2,9 +2,9 @@ def test_callable_attributes(): - def f(): pass + w = Callable(f) assert w.is_barefunction is True assert w.is_boundmethod is False @@ -14,10 +14,10 @@ def f(): assert w.is_classmethod is False assert w.is_property is False - class A(): - + class A: def m(self): pass + w = Callable(m) assert w.is_barefunction is False assert w.is_boundmethod is False @@ -30,6 +30,7 @@ def m(self): @classmethod def c(cls): pass + w = Callable(c) assert w.is_barefunction is False assert w.is_boundmethod is False @@ -42,6 +43,7 @@ def c(cls): @staticmethod def s(): pass + w = Callable(s) assert w.is_barefunction is False assert w.is_boundmethod is False @@ -54,6 +56,7 @@ def s(): @property def p(self): pass + w = Callable(p) assert w.is_barefunction is False assert w.is_boundmethod is False diff --git a/tests/test_implementations.py b/tests/test_implementations.py new file mode 100644 index 0000000..dfd8983 --- /dev/null +++ b/tests/test_implementations.py @@ -0,0 +1,186 @@ +"""Test both Rust and Python implementations.""" + +import pytest +import sys +import unittest.mock + + +def get_rust_implementation(): + """Get Rust implementation if available.""" + # Clear modules + for key in list(sys.modules.keys()): + if key.startswith("wirerope"): + del sys.modules[key] + + try: + import wirerope + + if wirerope._RUST_AVAILABLE: + return wirerope + except ImportError: + pass + return None + + +def get_python_implementation(): + """Get Python implementation.""" + # Clear modules + for key in list(sys.modules.keys()): + if key.startswith("wirerope"): + del sys.modules[key] + + # Force Python implementation + with unittest.mock.patch.dict(sys.modules, {"wirerope._wirerope_rust": None}): + import wirerope + + return wirerope + + +# Get implementations +rust_impl = get_rust_implementation() +python_impl = get_python_implementation() + +# Create test parameters +implementations = [] +if rust_impl: + implementations.append(("rust", rust_impl)) +implementations.append(("python", python_impl)) + + +@pytest.mark.parametrize("impl_name,wirerope", implementations) +def test_basic_wire(impl_name, wirerope): + """Test basic Wire functionality with both implementations.""" + Wire = wirerope.Wire + WireRope = wirerope.WireRope + + class TestWire(Wire): + def __call__(self, *args, **kwargs): + return self.__func__(*args, **kwargs) + + rope = WireRope(TestWire) + + @rope + def add(a, b): + """Add two numbers.""" + return a + b + + # Test function works + result = add(2, 3) + assert result == 5 + + # Test docstring is preserved + assert add.__doc__ == "Add two numbers." + + print(f"✓ Basic wire test passed with {impl_name} implementation") + + +@pytest.mark.parametrize("impl_name,wirerope", implementations) +def test_wraps_functionality(impl_name, wirerope): + """Test functools.wraps functionality with both implementations.""" + Wire = wirerope.Wire + WireRope = wirerope.WireRope + + class TestWire(Wire): + pass + + rope = WireRope(TestWire, wraps=True) + + @rope + def multiply(x, y): + """Multiply two numbers.""" + return x * y + + # Check that attributes are copied + assert multiply.__doc__ == "Multiply two numbers." + assert multiply.__name__ == "multiply" + + print(f"✓ Wraps functionality test passed with {impl_name} implementation") + + +@pytest.mark.parametrize("impl_name,wirerope", implementations) +def test_method_wrapping(impl_name, wirerope): + """Test method wrapping with both implementations.""" + Wire = wirerope.Wire + WireRope = wirerope.WireRope + + class TestWire(Wire): + pass + + rope = WireRope(TestWire, wraps=True) + + class Calculator: + @rope + def add(self, a, b): + """Add method.""" + return a + b + + @rope + @classmethod + def multiply(cls, a, b): + """Multiply classmethod.""" + return a * b + + @rope + @staticmethod + def divide(a, b): + """Divide staticmethod.""" + return a / b + + calc = Calculator() + + # Test instance method + assert calc.add.__doc__ == "Add method." + assert Calculator.add.__doc__ == "Add method." + + # Test classmethod + assert calc.multiply.__doc__ == "Multiply classmethod." + assert Calculator.multiply.__doc__ == "Multiply classmethod." + + # Test staticmethod + assert calc.divide.__doc__ == "Divide staticmethod." + assert Calculator.divide.__doc__ == "Divide staticmethod." + + print(f"✓ Method wrapping test passed with {impl_name} implementation") + + +@pytest.mark.parametrize("impl_name,wirerope", implementations) +def test_property_wrapping(impl_name, wirerope): + """Test property wrapping with both implementations.""" + Wire = wirerope.Wire + WireRope = wirerope.WireRope + + class TestWire(Wire): + def _on_property(self): + return self.__func__() + + rope = WireRope(TestWire, wraps=True) + + class Thing: + def __init__(self, value): + self._value = value + + @rope + @property + def value(self): + """Value property.""" + return self._value + + thing = Thing(42) + + # Test property access + assert thing.value == 42 + + # Test docstring + assert Thing.value.__doc__ == "Value property." + + print(f"✓ Property wrapping test passed with {impl_name} implementation") + + +if __name__ == "__main__": + # Run tests manually + for impl_name, impl in implementations: + print(f"\nTesting {impl_name} implementation:") + test_basic_wire(impl_name, impl) + test_wraps_functionality(impl_name, impl) + test_method_wrapping(impl_name, impl) + test_property_wrapping(impl_name, impl) diff --git a/tests/test_wire.py b/tests/test_wire.py index 9904fb0..413bd82 100644 --- a/tests/test_wire.py +++ b/tests/test_wire.py @@ -1,12 +1,8 @@ import sys - -from wirerope import Wire, WireRope, RopeCore - import pytest class hybridmethod(object): - def __init__(self, func): self.__func__ = func @@ -16,7 +12,6 @@ def __get__(self, obj, type=None): class hybridproperty(object): - def __init__(self, func): self.__func__ = func @@ -26,7 +21,6 @@ def __get__(self, obj, type=None): class awfuldescriptor(object): - def __init__(self, func): self.__func__ = func @@ -35,9 +29,7 @@ def __get__(self, obj, type=None): def test_hybridmethod(): - class X(object): - @hybridmethod def f(self): return self @@ -49,9 +41,7 @@ def f(self): def test_hybridproperty(): - class X(object): - @hybridproperty def p(self): return self @@ -61,7 +51,9 @@ def p(self): assert x.p is x -def test_default_wire(): +def test_default_wire(Wire, WireRope, implementation): + """Test default wire behavior (implementation: {implementation})""" + class TestWire(Wire): pass @@ -69,50 +61,52 @@ class TestWire(Wire): @rope def function(v): - '''function''' + """function""" return v class X(object): def baremethod(self, v): - '''baremethod''' + """baremethod""" return (self, v) @rope def method(self, v): - '''method''' + """method""" return (self, v) @rope @classmethod def cmethod(cls, v): - '''cmethod''' + """cmethod""" return (cls, v) @rope @staticmethod def smethod(v): - '''smethod''' + """smethod""" return (None, v) @rope @hybridmethod def hmethod(self_or_cls, v): - '''hmethod''' + """hmethod""" return (self_or_cls, v) @rope @property def property(self): - '''property''' - return (self, ) + """property""" + return (self,) @rope @hybridproperty def hproperty(self_or_cls): - '''hproperty''' + """hproperty""" return (self_or_cls,) - assert isinstance(function, RopeCore) + # Check if function inherits from RopeCore + # Direct isinstance check can fail after module reloading + assert any(base.__name__ == "RopeCore" for base in function.__class__.__mro__) assert isinstance(X.method, Wire) # triggered descriptor assert isinstance(X.cmethod, Wire) # triggered descriptor assert isinstance(X.smethod, Wire) # triggered descriptor @@ -157,24 +151,24 @@ def hproperty(self_or_cls): assert X.smethod.__func__(6) == (None, 6) assert x.hmethod.__func__(8) == (x, 8) assert X.hmethod.__func__(9) == (X, 9) - assert x.property == (x, ) - assert x.hproperty == (x, ) - assert X.hproperty == (X, ) - - assert function.__doc__ == 'function' - assert boundmethod.__doc__ == 'baremethod' - assert x.method.__doc__ == 'method' - assert X.method.__doc__ == 'method' - assert x.cmethod.__doc__ == 'cmethod' - assert X.cmethod.__doc__ == 'cmethod' - assert x.smethod.__doc__ == 'smethod' - assert X.smethod.__doc__ == 'smethod' - assert x.hmethod.__doc__ == 'hmethod' - assert X.hmethod.__doc__ == 'hmethod' - assert X.property.__doc__ == 'property' - - -def test_wire_super(): + assert x.property == (x,) + assert x.hproperty == (x,) + assert X.hproperty == (X,) + + assert function.__doc__ == "function" + assert boundmethod.__doc__ == "baremethod" + assert x.method.__doc__ == "method" + assert X.method.__doc__ == "method" + assert x.cmethod.__doc__ == "cmethod" + assert X.cmethod.__doc__ == "cmethod" + assert x.smethod.__doc__ == "smethod" + assert X.smethod.__doc__ == "smethod" + assert x.hmethod.__doc__ == "hmethod" + assert X.hmethod.__doc__ == "hmethod" + assert X.property.__doc__ == "property" + + +def test_wire_super(Wire, WireRope): if sys.version_info < (3, 6, 0): pytest.skip( "super() support requires __set_name__, which is not available" @@ -266,10 +260,8 @@ def hproperty(self_or_cls): assert obj.hproperty == "hproperty" + suffix -def test_callable_wire(): - +def test_callable_wire(Wire, WireRope): class CallableWire(Wire): - def __call__(self, *args, **kwargs): return self.__func__(*args, **kwargs) @@ -280,7 +272,6 @@ def f(v): return v class X(object): - @rope def g(self, v): return v @@ -316,7 +307,7 @@ def p(self): assert x.p == 42 -def test_wire(): +def test_wire(Wire, WireRope): class TestWire(Wire): x = 7 @@ -330,13 +321,12 @@ def y(self): @test_rope def f(): - return 'a' + return "a" assert f.x == 7 assert f.y() is None class A(object): - def __init__(self, v): self.v = v @@ -353,7 +343,7 @@ def f(self): assert not callable(a.f) -def test_unwirable(): +def test_unwirable(Wire, WireRope): rope = WireRope(Wire) @rope @@ -361,7 +351,6 @@ def function(v): return v class X(object): - @rope @awfuldescriptor def messed_up(self, v): diff --git a/tests/test_wirerope.py b/tests/test_wirerope.py index 8146f6e..b81b918 100644 --- a/tests/test_wirerope.py +++ b/tests/test_wirerope.py @@ -3,4 +3,4 @@ def test_package(): assert wirerope.__version__ - assert wirerope.__version__.startswith('1.') + assert wirerope.__version__.startswith("1.") diff --git a/wirerope/__init__.py b/wirerope/__init__.py index 9827fa8..ce1742d 100644 --- a/wirerope/__init__.py +++ b/wirerope/__init__.py @@ -4,7 +4,17 @@ # this line must be placed first for setup.cfg from ._version import __version__ -from .wire import Wire -from .rope import WireRope, RopeCore -__all__ = ('__version__', 'Wire', 'RopeCore', 'WireRope') +# Try to import Rust implementation first +_RUST_AVAILABLE = False +try: + # Import Rust implementation directly (no wrapper needed) + from ._wirerope_rust import Wire, WireRope, RopeCore + + _RUST_AVAILABLE = True +except ImportError: + # Fall back to Python implementation + from .wire import Wire + from .rope import WireRope, RopeCore + +__all__ = ("__version__", "Wire", "RopeCore", "WireRope", "_RUST_AVAILABLE") diff --git a/wirerope/_compat.py b/wirerope/_compat.py index 7600f7e..bc16cf6 100644 --- a/wirerope/_compat.py +++ b/wirerope/_compat.py @@ -3,8 +3,10 @@ import six import functools -if not hasattr(functools, 'singledispatch'): + +if not hasattr(functools, "singledispatch"): import singledispatch + functools.singledispatch = singledispatch.singledispatch if six.PY3: diff --git a/wirerope/_util.py b/wirerope/_util.py index cf99e49..1caaea4 100644 --- a/wirerope/_util.py +++ b/wirerope/_util.py @@ -1,4 +1,3 @@ - _missing = object() diff --git a/wirerope/_version.py b/wirerope/_version.py index 1f356cc..5becc17 100644 --- a/wirerope/_version.py +++ b/wirerope/_version.py @@ -1 +1 @@ -__version__ = '1.0.0' +__version__ = "1.0.0" diff --git a/wirerope/callable.py b/wirerope/callable.py index 73c3123..36f1009 100644 --- a/wirerope/callable.py +++ b/wirerope/callable.py @@ -5,11 +5,10 @@ from ._util import cached_property from ._compat import inspect -__all__ = ('Callable', ) +__all__ = ("Callable",) -_inspect_iscoroutinefunction = getattr( - inspect, 'iscoroutinefunction', lambda f: False) +_inspect_iscoroutinefunction = getattr(inspect, "iscoroutinefunction", lambda f: False) class _Reagent(object): @@ -39,7 +38,6 @@ def _obj_binder(descriptor, obj, type): class Descriptor(object): - def __init__(self, descriptor): self.descriptor = descriptor self.descriptor_class = type(descriptor) @@ -58,7 +56,8 @@ def detect_function_attr_name(self): else: raise RuntimeError( "The given function doesn't hold the given function as an " - "attribute. Is it a correct descriptor?") + "attribute. Is it a correct descriptor?" + ) def detect_property(self, obj, type_): d = self.descriptor_class(_f) @@ -94,7 +93,8 @@ def detect_binder(self, obj, type_): raise TypeError( "'descriptor_bind' fails to auto-detect binding rule " "of the given descriptor. Specify the rule by " - "'wirerope.wire.descriptor_bind.register'.") + "'wirerope.wire.descriptor_bind.register'." + ) _descriptor_binder_cache[key] = binder else: binder = _descriptor_binder_cache[key] @@ -113,9 +113,8 @@ def __init__(self, f): else: self.descriptor = None self.wrapped_callable = f - self.is_wrapped_coroutine = getattr(f, '_is_coroutine', None) - self.is_coroutine = self.is_wrapped_coroutine or \ - _inspect_iscoroutinefunction(f) + self.is_wrapped_coroutine = getattr(f, "_is_coroutine", None) + self.is_coroutine = self.is_wrapped_coroutine or _inspect_iscoroutinefunction(f) @cached_property def signature(self): @@ -143,6 +142,7 @@ def is_boundmethod(self): return False if six.PY2: + @property def is_unboundmethod(self): return type(self.wrapped_object) is type(Callable.__init__) # noqa @@ -151,11 +151,13 @@ def is_unboundmethod(self): def is_descriptor(self): if self.is_boundmethod: return False - is_descriptor = type(self.wrapped_object).__get__ \ - is not types.FunctionType.__get__ # noqa + is_descriptor = ( + type(self.wrapped_object).__get__ is not types.FunctionType.__get__ + ) # noqa if six.PY2: - is_descriptor = is_descriptor and \ - not (self.is_unboundmethod or self.is_boundmethod) + is_descriptor = is_descriptor and not ( + self.is_unboundmethod or self.is_boundmethod + ) return is_descriptor @cached_property @@ -164,19 +166,21 @@ def is_builtin_property(self): @cached_property def is_property(self): - return self.is_builtin_property or \ - (self.is_descriptor and self.descriptor.detect_property( - _reagent, _Reagent)) + return self.is_builtin_property or ( + self.is_descriptor and self.descriptor.detect_property(_reagent, _Reagent) + ) if six.PY34: + @cached_property def is_barefunction(self): cc = self.wrapped_callable - method_name = cc.__qualname__.split('.')[-1] + method_name = cc.__qualname__.split(".")[-1] if method_name == cc.__name__: return True return False else: + @cached_property def is_barefunction(self): # im_class does not exist at this point @@ -201,8 +205,7 @@ def is_member(self): return False if not self.is_descriptor: return True - return self.first_parameter is not None \ - and self.first_parameter.name == 'self' + return self.first_parameter is not None and self.first_parameter.name == "self" @cached_property def is_membermethod(self): @@ -236,5 +239,4 @@ def is_classmethod(self): if not self.is_descriptor: return False - return self.first_parameter is not None \ - and self.first_parameter.name == 'cls' + return self.first_parameter is not None and self.first_parameter.name == "cls" diff --git a/wirerope/rope.py b/wirerope/rope.py index aba90b1..a870c58 100644 --- a/wirerope/rope.py +++ b/wirerope/rope.py @@ -1,12 +1,49 @@ """:mod:`wirerope.rope` --- Wire access dispatcher for descriptor type. ======================================================================= """ + import six from .callable import Callable from .wire import descriptor_bind from ._compat import functools -__all__ = 'WireRope', 'RopeCore' +__all__ = ( + "WireRope", + "RopeCore", +) + + +def _copy_wrapped_attributes(wrapped_obj, target, attributes=None): + """Helper function to copy attributes from wrapped object to target. + + Args: + wrapped_obj: The wrapped object to copy attributes from + target: The target object to copy attributes to + attributes: List of attribute names to copy (default: standard function attributes) + """ + if attributes is None: + attributes = [ + "__doc__", + "__name__", + "__module__", + "__qualname__", + "__annotations__", + ] + + # For hybridmethod/hybridproperty, we need to get attributes from __func__ + if hasattr(wrapped_obj, "__func__"): + source_obj = wrapped_obj.__func__ + else: + source_obj = wrapped_obj + + # Copy attributes from source object to target + for attr in attributes: + try: + value = getattr(source_obj, attr, None) + if value is not None: + setattr(target, attr, value) + except (AttributeError, TypeError): + pass class RopeCore(object): @@ -27,14 +64,13 @@ def wire_class(self): class MethodRopeMixin(object): - def __init__(self, *args, **kwargs): super(MethodRopeMixin, self).__init__(*args, **kwargs) assert not self.callable.is_barefunction def __set_name__(self, owner, name): # Use a non-identifier character as separator to prevent name clash. - self.wire_name = '|'.join(['__wire', owner.__name__, name]) + self.wire_name = "|".join(["__wire", owner.__name__, name]) def __get__(self, obj, type=None): cw = self.callable @@ -42,63 +78,94 @@ def __get__(self, obj, type=None): owner, _ = descriptor_bind(co, obj, type) if owner is None: # invalid binding but still wire it owner = obj if obj is not None else type - if hasattr(self, 'wire_name'): + if hasattr(self, "wire_name"): wire_name = self.wire_name # Lookup in `__dict__` instead of using `getattr`, because # `getattr` falls back to class attributes. wire = owner.__dict__.get(wire_name) else: - wire_name_parts = ['__wire_', cw.wrapped_callable.__name__] + wire_name_parts = ["__wire_", cw.wrapped_callable.__name__] if owner is type: - wire_name_parts.extend(('_', type.__name__)) - wire_name = ''.join(wire_name_parts) + wire_name_parts.extend(("_", type.__name__)) + wire_name = "".join(wire_name_parts) wire = getattr(owner, wire_name, None) if wire is None: wire = self.wire_class(self, owner, (obj, type)) + # Apply functools.wraps behavior if _wrapped is True + if self._wrapped: + _copy_wrapped_attributes(self.callable.wrapped_object, wire) setattr(owner, wire_name, wire) assert callable(wire.__func__) return wire class PropertyRopeMixin(object): - def __init__(self, *args, **kwargs): super(PropertyRopeMixin, self).__init__(*args, **kwargs) assert not self.callable.is_barefunction def __set_name__(self, owner, name): # Use a non-identifier character as separator to prevent name clash. - self.wire_name = '|'.join(['__wire', owner.__name__, name]) + self.wire_name = "|".join(["__wire", owner.__name__, name]) def __get__(self, obj, type=None): cw = self.callable co = cw.wrapped_object + + # Special handling for class access (obj is None) + if obj is None: + # Distinguish between regular property and hybridproperty by behavior: + # - regular property: returns itself when accessed from class + # - hybridproperty: executes and returns result when accessed from class + + # Test the descriptor's behavior by calling __get__ with None + if hasattr(co, "__get__"): + try: + result = co.__get__(None, type) + # If result is the same object, it's a regular property + if result is co: + return co + else: + # If result is different, it's a hybridproperty-like descriptor + return result + except (TypeError, AttributeError): + # If __get__ fails with None, it's probably a regular property + return co + + # Fallback: return the descriptor itself + return co + owner, _ = descriptor_bind(co, obj, type) if owner is None: # invalid binding but still wire it owner = obj if obj is not None else type - if hasattr(self, 'wire_name'): + if hasattr(self, "wire_name"): wire_name = self.wire_name # Lookup in `__dict__` instead of using `getattr`, because # `getattr` falls back to class attributes. wire = owner.__dict__.get(wire_name) else: - wire_name_parts = ['__wire_', cw.wrapped_callable.__name__] + wire_name_parts = ["__wire_", cw.wrapped_callable.__name__] if owner is type: - wire_name_parts.extend(('_', type.__name__)) - wire_name = ''.join(wire_name_parts) + wire_name_parts.extend(("_", type.__name__)) + wire_name = "".join(wire_name_parts) wire = getattr(owner, wire_name, None) if wire is None: wire = self.wire_class(self, owner, (obj, type)) + # Apply functools.wraps behavior if _wrapped is True + if self._wrapped: + _copy_wrapped_attributes(self.callable.wrapped_object, wire) setattr(owner, wire_name, wire) return wire._on_property() # requires property path class FunctionRopeMixin(object): - def __init__(self, *args, **kwargs): super(FunctionRopeMixin, self).__init__(*args, **kwargs) assert self.callable.is_barefunction or self.callable.is_boundmethod self._wire = self.wire_class(self, None, None) + # Apply functools.wraps behavior if _wrapped is True + if self._wrapped: + _copy_wrapped_attributes(self.callable.wrapped_object, self._wire) def __getattr__(self, name): try: @@ -109,7 +176,6 @@ def __getattr__(self, name): class CallableRopeMixin(object): - def __init__(self, *args, **kwargs): super(CallableRopeMixin, self).__init__(*args, **kwargs) self.__call__ = functools.wraps(self.callable.wrapped_object)(self) @@ -131,21 +197,23 @@ class WireRope(object): defined behavior. """ - def __init__( - self, wire_class, core_class=RopeCore, - wraps=False, rope_args=None): + def __init__(self, wire_class, core_class=RopeCore, wraps=False, rope_args=None): self.wire_class = wire_class - self.method_rope = type( - '_MethodRope', (MethodRopeMixin, core_class), {}) - self.property_rope = type( - '_PropertyRope', (PropertyRopeMixin, core_class), {}) - self.function_rope = type( - '_FunctionRope', (FunctionRopeMixin, core_class), {}) + self.method_rope = type("_MethodRope", (MethodRopeMixin, core_class), {}) + self.property_rope = type("_PropertyRope", (PropertyRopeMixin, core_class), {}) + self.function_rope = type("_FunctionRope", (FunctionRopeMixin, core_class), {}) self.callable_function_rope = type( - '_CallableFunctionRope', - (CallableRopeMixin, FunctionRopeMixin, core_class), {}) - for rope in (self, self.method_rope, self.property_rope, - self.function_rope, self.callable_function_rope): + "_CallableFunctionRope", + (CallableRopeMixin, FunctionRopeMixin, core_class), + {}, + ) + for rope in ( + self, + self.method_rope, + self.property_rope, + self.function_rope, + self.callable_function_rope, + ): rope._wrapped = wraps rope._args = rope_args @@ -160,13 +228,15 @@ def __call__(self, function): rope_class = self.callable_function_rope wire_class_call = self.wire_class.__call__ if six.PY3: - if wire_class_call.__qualname__ == 'type.__call__': + if wire_class_call.__qualname__ == "type.__call__": rope_class = self.function_rope else: # method-wrapper test for CPython2.7 # im_class == type test for PyPy2.7 - if type(wire_class_call).__name__ == 'method-wrapper' or \ - wire_class_call.im_class == type: + if ( + type(wire_class_call).__name__ == "method-wrapper" + or wire_class_call.im_class == type + ): rope_class = self.function_rope elif cw.is_property: rope_class = self.property_rope diff --git a/wirerope/wire.py b/wirerope/wire.py index 974d601..e881600 100644 --- a/wirerope/wire.py +++ b/wirerope/wire.py @@ -1,23 +1,28 @@ """:mod:`wirerope.wire` --- end-point instant for each bound method =================================================================== """ + import six import types from .callable import Descriptor from ._compat import functools -__all__ = 'Wire', +__all__ = ("Wire",) -@functools.singledispatch def descriptor_bind(descriptor, obj, type_): - binder = Descriptor(descriptor).detect_binder(obj, type_) - return binder(descriptor, obj, type_) + """Bind a descriptor to an object and type. + This function determines how to bind a descriptor based on its type. + It replaces the previous singledispatch implementation for better performance. + """ + # Direct type check for FunctionType - most common case + if isinstance(descriptor, types.FunctionType): + return obj, obj -@descriptor_bind.register(types.FunctionType) -def descriptor_bind_function(descriptor, obj, type): - return obj, obj + # Fall back to the generic descriptor handling for all other types + binder = Descriptor(descriptor).detect_binder(obj, type_) + return binder(descriptor, obj, type_) class Wire(object): @@ -32,8 +37,13 @@ class Wire(object): """ __slots__ = ( - '_rope', '_callable', '_binding', '__func__', '_owner', - '_bound_objects') + "_rope", + "_callable", + "_binding", + "__func__", + "_owner", + "_bound_objects", + ) def __init__(self, rope, owner, binding): self._rope = rope @@ -43,7 +53,19 @@ def __init__(self, rope, owner, binding): if binding: func = self._callable.wrapped_object.__get__ if self._callable.is_property: - wrapped = functools.partial(func, *binding) + wrapped_obj = self._callable.wrapped_object + # Optimization: try to cache property getter for direct calls + if hasattr(wrapped_obj, "fget") and wrapped_obj.fget: + # Store the property getter and bound instance for fast access + self._property_fget = wrapped_obj.fget + self._property_instance = binding[0] + # Still create partial for compatibility + wrapped = functools.partial(func, *binding) + else: + # Fallback for non-standard properties + wrapped = functools.partial(func, *binding) + self._property_fget = None + if six.PY2: # functools.wraps requires those attributes but # py2 functools.partial doesn't have them @@ -57,10 +79,9 @@ def __init__(self, rope, owner, binding): if self._binding is None: self._bound_objects = () else: - _, binder = descriptor_bind( - self._callable.wrapped_object, *self._binding) + _, binder = descriptor_bind(self._callable.wrapped_object, *self._binding) if binder is not None: - self._bound_objects = (binder, ) + self._bound_objects = (binder,) else: self._bound_objects = () assert callable(self.__func__), self.__func__ @@ -68,4 +89,8 @@ def __init__(self, rope, owner, binding): functools.wraps(self.__func__)(self) def _on_property(self): + # Optimized property access: use cached getter when available + if hasattr(self, "_property_fget") and self._property_fget is not None: + return self._property_fget(self._property_instance) + # Fallback to original approach return self.__func__()