Skip to content

Commit

Permalink
adds type stubs for python imports (#556)
Browse files Browse the repository at this point in the history
The primary goals of this PR are 
1) to resolve a pylint error when running `from hyperdrive_math_py import HyperdriveState`
2) to enable Python type hints for the `HyperdriveState` object.

To achieve this, I had to change the install pattern from using [maturin](https://github.com/PyO3/maturin) to the lower-level [setuptools-rust](https://github.com/PyO3/setuptools-rust). This allows us to specify types & interface stubs so that pylint & pyright can parse type hints. While Maturin technically [claims to support this behavior as well](https://github.com/PyO3/maturin/blob/0dee40510083c03607834c821eea76964140a126/Readme.md#mixed-rustpython-projects), Matt and I were unable to get it to work with the automatic `maturin develop` install process.

The new directory structure follows the [recommended example from pyo3](https://github.com/PyO3/setuptools-rust/tree/main/examples/html-py-ever), which looks like this:
```
crates/hyperdrive-math-py/
├── Cargo.toml
├── MANIFEST.in
├── pyproject.toml
├── python
│   └── hyperdrive_math_py
│       ├── __init__.py
│       ├── hyperdrive_math_py.pyi
│       └── types.py
├── setup.py
└── src
    └── lib.rs
```
Most notably is the new `python` folder, which holds type hints, and the `setup.py` file, which specifies how `setuptools` is supposed to install the package via `pip`.

The `README.md` file has been updated to reflect the install process, which is simply to enter your preferred python environment and run `pip install crates/hyperdrive-math-py` from the project root.
  • Loading branch information
dpaiton authored Aug 15, 2023
1 parent 91163bc commit fe876b7
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 78 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
# TODO: We should just use nightly, but downgrading to fix a reversion
# in forge coverage.
version: nightly-10440422e63aae660104e079dfccd5b0ae5fd720
version: nightly

- name: forge version
run: forge --version
Expand Down
48 changes: 48 additions & 0 deletions .github/workflows/python_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Python test

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
name: python test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
token: ${{ secrets.GITHUB_TOKEN }}

# NOTE: This is needed to ensure that hyperdrive-wrappers builds correctly.
- name: Install foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Install pip
uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: "pip"
token: ${{ secrets.GITHUB_TOKEN }}
- run: |
python -m pip install --upgrade pip
python -m pip install --upgrade pytest
- name: Install hyperdrive-math-py
uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: "pip"
token: ${{ secrets.GITHUB_TOKEN }}
- run: |
python -m pip install crates/hyperdrive-math-py
- name: Run pytest
run: |
python -m pytest python/test
10 changes: 6 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ forge-cache/
.vscode/
broadcast
.env
.venv

node_modules/
yarn-error.log

#Random files on MacOs
# random files on MacOs
.DS_Store

# apeworx autogenerated files
# python build
*.egg-info
.cache
.build
.python-version
.venv
build/

# nix/shell extension - https://direnv.net/
.direnv
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ If you want to automatically format the code, run `yarn prettier`.

The current suggested way of integrating your yield source with hyperdrive is through the [ERC-4626 standard](https://eips.ethereum.org/EIPS/eip-4626) although accomodations can be made if this is not possible. Hyperdrive currently makes use of [Yield Daddy](https://github.com/timeless-fi/yield-daddy) to wrap many existing yield sources into this standard.

## Python Rust wrapper
To install the Python package `hyperdrive-math-py`, which wraps the Rust `hyperdrive_math::State` struct, you need to:
- setup a [Python venv](https://docs.python.org/3/library/venv.html) that is running at least `Python 3.7`
- from inside the environment, run `pip install crates/hyperdrive-math-py`
- test the installation by running `pip install --upgrade pytest && pytest python/test`

# Disclaimer

The language used in this codebase is for coding convenience only, and is not
Expand Down
5 changes: 1 addition & 4 deletions crates/hyperdrive-math-py/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
[package]

name = "hyperdrive-math-py"
version = "0.1.0"
edition = "2021"

[lib]

name = "hyperdrive_math"
name = "hyperdrive_math_py"
# "cdylib" is necessary to produce a shared library for Python to import from.
crate-type = ["cdylib"]

[dependencies]

ethers = "2.0.8"
eyre = "0.6.8"
rand = "0.8.5"
Expand Down
3 changes: 3 additions & 0 deletions crates/hyperdrive-math-py/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include Cargo.toml
recursive-include src *
recursive-include python *
17 changes: 10 additions & 7 deletions crates/hyperdrive-math-py/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
[build-system]
requires = ["maturin>=1.2,<2.0"]
build-backend = "maturin"

[project]
name = "hyperdrive_math_lib"
name = "hyperdrive_math_py"
version = "0.1.0"
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]

[build-system]
requires = ["setuptools", "wheel", "setuptools-rust"]
build-backend = "setuptools.build_meta"

[tool.pylint]
extension-pkg-allow-list="hyperdrive-math-py"

[tool.maturin]
features = ["pyo3/extension-module"]
[tool.setuptools.packages.find]
where = ["python", "src"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Python module wrapping the Rust implementation of HyperdriveMath"""
from .hyperdrive_math_py import *
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Stubs for hyperdrive math."""
from __future__ import annotations

from . import types

class HyperdriveState:
"""A class representing the hyperdrive contract state."""

def __new__(
cls, pool_config: types.PoolConfig, pool_info: types.PoolInfo
) -> HyperdriveState:
"""Create the HyperdriveState instance."""
def __init__(
self, pool_config: types.PoolConfig, pool_info: types.PoolInfo
) -> None:
"""Initializes the hyperdrive state.
Arguments
---------
pool_config : PoolConfig
Static configuration for the hyperdrive contract. Set at deploy time.
pool_info : PoolInfo
Current state information of the hyperdrive contract. Includes things like reserve levels and share prices.
"""
def get_spot_price(self) -> str:
"""Gets the spot price of the bond.
Returns
-------
str
The spot price as a string representation of a solidity uint256 value.
"""
43 changes: 43 additions & 0 deletions crates/hyperdrive-math-py/python/hyperdrive_math_py/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Types for the hyperdrive contract."""
from typing import NamedTuple


class Fees(NamedTuple):
"""Protocal Fees."""

curve: str
flat: str
governance: str


class PoolConfig(NamedTuple):
"""Static configuration for the hyperdrive contract. Set at deploy time."""

base_token: str
initial_share_price: str
minimum_share_reserves: str
position_duration: str
checkpoint_duration: str
time_stretch: str
governance: str
fee_collector: str
fees: Fees
oracle_size: str
update_gap: str


class PoolInfo(NamedTuple):
"""Current state information of the hyperdrive contract. Includes things like reserve levels and share prices."""

share_reserves: str
bond_reserves: str
lp_total_supply: str
share_price: str
longs_outstanding: str
long_average_maturity_time: str
shorts_outstanding: str
short_average_maturity_time: str
short_base_volume: str
withdrawal_shares_ready_to_withdraw: str
withdrawal_shares_proceeds: str
lp_share_price: str
15 changes: 15 additions & 0 deletions crates/hyperdrive-math-py/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Entry point for installing hyperdrive math python package"""
from setuptools import setup
from setuptools_rust import Binding, RustExtension

setup(
name="hyperdrive_math_py",
version="0.1.0",
packages=["hyperdrive_math_py"],
package_dir={"": "python"},
rust_extensions=[
RustExtension("hyperdrive_math_py.hyperdrive_math_py", binding=Binding.PyO3),
],
# rust extensions are not zip safe, just like C-extensions.
zip_safe=False,
)
31 changes: 16 additions & 15 deletions crates/hyperdrive-math-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@ use pyo3::PyErr;

use hyperdrive_math::hyperdrive_math::State;

#[pyclass(name = "HyperdriveState")]
pub struct PyState {
#[pyclass(module = "hyperdrive_math_py", name = "HyperdriveState")]
pub struct HyperdriveState {
pub state: State,
}

impl PyState {
impl HyperdriveState {
pub(crate) fn new(state: State) -> Self {
PyState { state }
HyperdriveState { state }
}
}

impl From<State> for PyState {
impl From<State> for HyperdriveState {
fn from(state: State) -> Self {
PyState { state }
HyperdriveState { state }
}
}

pub struct PyPoolConfig {
pub pool_config: PoolConfig,
}
Expand Down Expand Up @@ -134,13 +135,13 @@ impl FromPyObject<'_> for PyPoolInfo {
}

#[pymethods]
impl PyState {
impl HyperdriveState {
#[new]
pub fn __init__(pool_config: &PyAny, pool_info: &PyAny) -> PyResult<Self> {
let rust_pool_config = PyPoolConfig::extract(pool_config)?.pool_config;
let rust_pool_info = PyPoolInfo::extract(pool_info)?.pool_info;
let hyperdrive_state = State::new(rust_pool_config, rust_pool_info);
Ok(PyState::new(hyperdrive_state))
let state = State::new(rust_pool_config, rust_pool_info);
Ok(HyperdriveState::new(state))
}

pub fn get_spot_price(&self) -> PyResult<String> {
Expand All @@ -150,12 +151,12 @@ impl PyState {
}
}

/// A pyO3 wrapper for the hyperdrie_math crate. The State struct will be exposed with all methods
/// listed in #[pymethods] decorated impl. The name of this function must match the `lib.name`
/// setting in the `Cargo.toml`, else Python will not be able to import the module.
/// A pyO3 wrapper for the hyperdrie_math crate.
/// The Hyperdrive State struct will be exposed with the following methods:
/// - get_spot_price
#[pymodule]
#[pyo3(name = "hyperdrive_math")]
fn hyperdrive_math_lib(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<PyState>().unwrap();
#[pyo3(name = "hyperdrive_math_py")]
fn hyperdrive_math_py(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::<HyperdriveState>()?;
Ok(())
}
47 changes: 2 additions & 45 deletions python/test/test_hyperdrive_math_wrappers.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,6 @@
"""Tests for hyperdrive_math.rs wrappers"""
from typing import NamedTuple

from hyperdrive_math import HyperdriveState


class Fees(NamedTuple):
"""Protocal Fees"""

curve: str
flat: str
governance: str


class PoolConfig(NamedTuple):
"""Sample Pool Config"""

base_token: str
initial_share_price: str
minimum_share_reserves: str
position_duration: str
checkpoint_duration: str
time_stretch: str
governance: str
fee_collector: str
fees: Fees
oracle_size: str
update_gap: str

from hyperdrive_math_py import HyperdriveState
from hyperdrive_math_py.types import Fees, PoolConfig, PoolInfo

sample_pool_config = PoolConfig(
base_token="0x1234567890abcdef1234567890abcdef12345678",
Expand All @@ -43,23 +17,6 @@ class PoolConfig(NamedTuple):
)


class PoolInfo(NamedTuple):
"""Sample Pool Info"""

share_reserves: str
bond_reserves: str
lp_total_supply: str
share_price: str
longs_outstanding: str
long_average_maturity_time: str
shorts_outstanding: str
short_average_maturity_time: str
short_base_volume: str
withdrawal_shares_ready_to_withdraw: str
withdrawal_shares_proceeds: str
lp_share_price: str


sample_pool_info = PoolInfo(
share_reserves="1000000000000000000",
bond_reserves="2000000000000000000",
Expand Down

0 comments on commit fe876b7

Please sign in to comment.