diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ff1adb9..53674b8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -57,6 +57,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.12' + if: ${{ ! startsWith(matrix.build, 'Anaconda') }} - name: Install Matplotlib (pip) if: ${{ ! startsWith(matrix.build, 'Anaconda') }} run: pip install matplotlib @@ -67,6 +68,7 @@ jobs: run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} shell: bash - run: rustup target add ${{ matrix.target }} - - run: cargo build --verbose - - run: cargo test tests --verbose + - run: cargo build -vv + - run: cargo test + if: ${{ ! startsWith(matrix.build, 'Anaconda') }} - run: cargo run --example a_simple_example diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..784d615 --- /dev/null +++ b/build.rs @@ -0,0 +1,57 @@ +use std::{ + env, + path::{Path, PathBuf}, + process::Command, +}; + +fn conda_bin() -> String { + match env::var("CONDA") { + Ok(mut p) => { + p.push_str("/bin/conda"); + p + } + Err(_) => "conda".to_string() + } +} + +fn conda_path() -> Result { + Command::new(conda_bin()).args(["info", "--base"]).output() + .map(|o| { + let base = String::from_utf8_lossy(&o.stdout); + base.as_ref().trim().into() + }) +} + +fn main() { + let matplotlib = "import matplotlib; print(matplotlib.__file__)"; + + let plt = Command::new("python3").args(["-c", matplotlib]).output(); + if let Ok(plt) = plt { + if plt.status.success() { + // The Python in the PATH has matplotlib. + return + } + } + // See whether Anaconda Python is installed and has matplotlib. + if let Ok(conda) = conda_path() { + let python = conda.join("bin").join("python3"); + let plt = Command::new(python).args(["-c", matplotlib]).output(); + if let Ok(plt) = plt { + if plt.status.success() { + let plt = String::from_utf8_lossy(&plt.stdout); + let plt = Path::new(plt.as_ref()); + if let Some(d) = plt.parent().and_then(|d| d.parent()) { + let d = d.to_str().unwrap(); + println!("cargo:rustc-env=PYTHONHOME={d}"); + return + } + } + } + } + panic!("Python or package matplotlib not found (including in Anaconda)"); + + // if let Some(d) = plt_path.parent().and_then(|d| d.parent()) { + // let d = d.to_str().unwrap(); + // println!("cargo:rustc-env=MATPLOTLIB_DIR={d}"); + // } +} diff --git a/src/lib.rs b/src/lib.rs index 459ca1d..1d17aa1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ use std::{ fmt::{Display, Formatter}, path::Path, pin::Pin, borrow::Cow, + sync::Once, }; use lazy_static::lazy_static; use pyo3::{ @@ -88,7 +89,7 @@ impl Display for Error { write!(f, "The matplotlib library has not been found.\n\ Please install it. See https://matplotlib.or/\n\ If you use Anaconda, see https://github.com/PyO3/pyo3/issues/1554"), - Error::FileNotFoundError => + Error::FileNotFoundError => write!(f, "A path contains an element that is not a \ directory or does not exist"), Error::PermissionError => @@ -102,11 +103,33 @@ If you use Anaconda, see https://github.com/PyO3/pyo3/issues/1554"), impl std::error::Error for Error {} +fn initialize_python() { + static INIT: Once = Once::new(); + + INIT.call_once(|| { + if let Some(python_home) = option_env!("PYTHONHOME") { + // The Python C API uses null-terminated UTF-16 strings, + // so we need to encode the path into that format here. + let mut python_home: Vec + = python_home.encode_utf16().collect(); + python_home.push(0); + unsafe { + pyo3::ffi::Py_SetPythonHome(python_home.as_ptr() as *const _); + } + + // Manually initialize PyO3. + pyo3::prepare_freethreaded_python(); + } + }) +} + /// Import and return a handle to the module `$m`. -macro_rules! pyimport { ($m: literal) => { +macro_rules! pyimport { ($m: literal) => {{ + initialize_python(); Python::with_gil(|py| - PyModule::import(py, intern!(py, $m)).map(|m| m.into())) -}} + PyModule::import(py, intern!(py, $m)).map(|m| m.into()) + ) +}}} lazy_static! { // Import matplotlib modules. @@ -445,6 +468,29 @@ impl Axes { } + pub fn contour<'a>(&'a mut self, ) -> Contour<'a> { + Contour { + axes: self, + options: PlotOptions::new(), + } + } + + pub fn contour_fun<'a, F>( + &'a mut self, + f: F, + ab: [f64; 2], + cd: [f64; 2], + ) -> ContourFun<'a, F> + where F: FnMut(f64, f64) -> f64 { + ContourFun { + axes: self, + options: PlotOptions::new(), + f, ab, cd, + n1: 100, + n2: 100, + } + } + #[must_use] pub fn scatter(&mut self, x: D, y: D) -> &mut Self where D: AsRef<[f64]> { @@ -801,6 +847,36 @@ where F: FnMut(f64) -> f64 { } +#[must_use] +pub struct Contour<'a> { + axes: &'a Axes, + options: PlotOptions<'a>, +} + +impl<'a> Contour<'a> { + set_plotoptions!(); + +} + + +#[must_use] +pub struct ContourFun<'a, F> { + axes: &'a Axes, + options: PlotOptions<'a>, + f: F, + ab: [f64; 2], + cd: [f64; 2], + n1: usize, + n2: usize, +} + +impl<'a, F> ContourFun<'a, F> +where F: FnMut(f64, f64) -> f64 { + set_plotoptions!(); + +} + + impl Line2D { fn set_kw(&self, prop: &str, v: impl ToPyObject) { Python::with_gil(|py| {