From d53e19681309269ada65b8e232d316eb03163716 Mon Sep 17 00:00:00 2001 From: Dorival Pedroso Date: Sun, 24 May 2026 08:36:58 +1000 Subject: [PATCH 1/2] Change how PYTHON_HEADER is passed to Python. * Need to add "Agg" when saving the figure in an environment without GUI * Also, for some reason, the matplotlib set limits cannot use None in 3D (in Arch) --- src/constants.rs | 6 ++---- src/fileio.rs | 21 ++++++--------------- src/plot.rs | 39 ++++++++++++++++++++++++++------------- tests/test_contour.rs | 2 +- tests/test_inset_axes.rs | 2 +- tests/test_plot.rs | 2 +- tests/test_stream.rs | 4 ++-- 7 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index 5d38e4d..1ed8b7f 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -23,9 +23,7 @@ /// For example a circle will show as a circle in the screen and not an ellipse. This function also handles /// the 3D case which is a little tricky with Matplotlib. In this case (3D), the version of Matplotlib /// must be greater than 3.3.0. -pub const PYTHON_HEADER: &str = "### file generated by the 'plotpy' Rust crate - -import numpy as np +pub const PYTHON_HEADER: &str = "import numpy as np import matplotlib.pyplot as plt import matplotlib.ticker as tck import matplotlib.patches as pat @@ -152,6 +150,6 @@ mod tests { #[test] fn constants_are_correct() { - assert_eq!(PYTHON_HEADER.len(), 2975); + assert_eq!(PYTHON_HEADER.len(), 2928); } } diff --git a/src/fileio.rs b/src/fileio.rs index 83fed32..810c7b9 100644 --- a/src/fileio.rs +++ b/src/fileio.rs @@ -1,4 +1,4 @@ -use super::{StrError, PYTHON_HEADER}; +use super::StrError; use std::fs; use std::fs::File; use std::io::Write; @@ -12,10 +12,6 @@ use std::process::Command; /// * `python_commands` - Python commands to be written to file /// * `output_dir` - Output directory to be created /// * `filename_py` - Filename with extension .py -/// -/// # Note -/// -/// The contents of [PYTHON_HEADER] are added at the beginning of the file. pub(crate) fn call_python3(python_exe: &str, python_commands: &String, path: &Path) -> Result { // create directory if let Some(p) = path.parent() { @@ -24,7 +20,6 @@ pub(crate) fn call_python3(python_exe: &str, python_commands: &String, path: &Pa // combine header with commands let mut contents = String::new(); - contents.push_str(PYTHON_HEADER); contents.push_str(python_commands); // write file @@ -59,7 +54,7 @@ pub(crate) fn call_python3(python_exe: &str, python_commands: &String, path: &Pa #[cfg(test)] mod tests { - use super::{call_python3, PYTHON_HEADER}; + use super::call_python3; use std::fs; use std::path::Path; @@ -71,8 +66,7 @@ mod tests { let path = Path::new("call_python3_works.py"); let output = call_python3("python3", &commands, &path).unwrap(); let data = fs::read_to_string(&path).map_err(|_| "cannot read test file").unwrap(); - let mut correct = String::from(PYTHON_HEADER); - correct.push_str(&commands); + let correct = "print(\"Python says: Hello World!\")".to_string(); assert_eq!(data, correct); assert_eq!(output, "Python says: Hello World!\n"); } @@ -83,8 +77,7 @@ mod tests { let path = Path::new(OUT_DIR).join("call_python3_works.py"); let output = call_python3("python3", &commands, &path).unwrap(); let data = fs::read_to_string(&path).map_err(|_| "cannot read test file").unwrap(); - let mut correct = String::from(PYTHON_HEADER); - correct.push_str(&commands); + let correct = "print(\"Python says: Hello World!\")".to_string(); assert_eq!(data, correct); assert_eq!(output, "Python says: Hello World!\n"); } @@ -96,16 +89,14 @@ mod tests { let commands_first = "print(\"Python says: Hello World!\")".to_string(); let output_first = call_python3("python3", &commands_first, &path).unwrap(); let data_first = fs::read_to_string(&path).map_err(|_| "cannot read test file").unwrap(); - let mut correct_first = String::from(PYTHON_HEADER); - correct_first.push_str(&commands_first); + let correct_first = "print(\"Python says: Hello World!\")".to_string(); assert_eq!(data_first, correct_first); assert_eq!(output_first, "Python says: Hello World!\n"); // second let commands_second = "print(\"Python says: Hello World! again\")".to_string(); let output_second = call_python3("python3", &commands_second, &path).unwrap(); let data_second = fs::read_to_string(&path).map_err(|_| "cannot read test file").unwrap(); - let mut correct_second = String::from(PYTHON_HEADER); - correct_second.push_str(&commands_second); + let correct_second = "print(\"Python says: Hello World! again\")".to_string(); assert_eq!(data_second, correct_second); assert_eq!(output_second, "Python says: Hello World! again\n"); } diff --git a/src/plot.rs b/src/plot.rs index 6be9dc7..a5a6111 100644 --- a/src/plot.rs +++ b/src/plot.rs @@ -1,4 +1,5 @@ use super::{call_python3, generate_list_quoted, vector_to_array, AsVector, Legend, StrError, SuperTitleParams}; +use crate::PYTHON_HEADER; use num_traits::Num; use std::ffi::OsStr; use std::fmt::Write; @@ -563,37 +564,37 @@ impl Plot { /// Sets minimum x pub fn set_xmin(&mut self, xmin: f64) -> &mut Self { - write!(&mut self.buffer, "plt.gca().set_xlim([{},None])\n", xmin).unwrap(); + write!(&mut self.buffer, "plt.gca().set_xlim(left={})\n", xmin).unwrap(); self } /// Sets maximum x pub fn set_xmax(&mut self, xmax: f64) -> &mut Self { - write!(&mut self.buffer, "plt.gca().set_xlim([None,{}])\n", xmax).unwrap(); + write!(&mut self.buffer, "plt.gca().set_xlim(right={})\n", xmax).unwrap(); self } /// Sets minimum y pub fn set_ymin(&mut self, ymin: f64) -> &mut Self { - write!(&mut self.buffer, "plt.gca().set_ylim([{},None])\n", ymin).unwrap(); + write!(&mut self.buffer, "plt.gca().set_ylim(bottom={})\n", ymin).unwrap(); self } /// Sets maximum y pub fn set_ymax(&mut self, ymax: f64) -> &mut Self { - write!(&mut self.buffer, "plt.gca().set_ylim([None,{}])\n", ymax).unwrap(); + write!(&mut self.buffer, "plt.gca().set_ylim(top={})\n", ymax).unwrap(); self } /// Sets minimum z pub fn set_zmin(&mut self, zmin: f64) -> &mut Self { - write!(&mut self.buffer, "plt.gca().set_zlim([{},None])\n", zmin).unwrap(); + write!(&mut self.buffer, "plt.gca().set_zlim(zmin={})\n", zmin).unwrap(); self } /// Sets maximum z pub fn set_zmax(&mut self, zmax: f64) -> &mut Self { - write!(&mut self.buffer, "plt.gca().set_zlim([None,{}])\n", zmax).unwrap(); + write!(&mut self.buffer, "plt.gca().set_zlim(zmax={})\n", zmax).unwrap(); self } @@ -1156,6 +1157,12 @@ impl Plot { { // update commands let fig_path = Path::new(figure_path); + let header = if show { + "#### >>>> file generated by plotpy <<<< ####\n\n".to_string() + PYTHON_HEADER + } else { + "#### >>>> file generated by plotpy <<<< ####\n\nimport matplotlib\nmatplotlib.use('Agg')\n".to_string() + + PYTHON_HEADER + }; let mut txt = "plt.savefig(fn".to_string(); if self.save_tight { txt.push_str(",bbox_inches='tight',bbox_extra_artists=EXTRA_ARTISTS"); @@ -1172,7 +1179,13 @@ impl Plot { if show { txt.push_str("\nplt.show()\n"); }; - let commands = format!("{}\nfn=r'{}'\n{}", self.buffer, fig_path.to_string_lossy(), txt); + let commands = format!( + "{}{}\nfn=r'{}'\n{}", + header, + self.buffer, + fig_path.to_string_lossy(), + txt + ); // call python let mut path = Path::new(figure_path).to_path_buf(); @@ -1405,12 +1418,12 @@ mod tests { plt.gca().set_xlim([-80,800])\n\ plt.gca().set_ylim([13,130])\n\ plt.gca().set_zlim([44,444])\n\ - plt.gca().set_xlim([-3,None])\n\ - plt.gca().set_xlim([None,8])\n\ - plt.gca().set_ylim([-7,None])\n\ - plt.gca().set_ylim([None,33])\n\ - plt.gca().set_zlim([12,None])\n\ - plt.gca().set_zlim([None,34])\n\ + plt.gca().set_xlim(left=-3)\n\ + plt.gca().set_xlim(right=8)\n\ + plt.gca().set_ylim(bottom=-7)\n\ + plt.gca().set_ylim(top=33)\n\ + plt.gca().set_zlim(zmin=12)\n\ + plt.gca().set_zlim(zmax=34)\n\ plt.gca().get_xaxis().set_ticks([])\n\ plt.gca().get_xaxis().set_major_locator(tck.MaxNLocator(8))\n\ plt.gca().get_yaxis().set_ticks([])\n\ diff --git a/tests/test_contour.rs b/tests/test_contour.rs index bdb2350..d5f8026 100644 --- a/tests/test_contour.rs +++ b/tests/test_contour.rs @@ -245,6 +245,6 @@ fn test_contour_colorbar_options() -> Result<(), StrError> { let buffered = BufReader::new(file); let lines_iter = buffered.lines(); let n = lines_iter.count(); - assert!(n > 2150 && n < 2200); + assert!(n > 2050 && n < 2200); Ok(()) } diff --git a/tests/test_inset_axes.rs b/tests/test_inset_axes.rs index 4eeaf68..7c93344 100644 --- a/tests/test_inset_axes.rs +++ b/tests/test_inset_axes.rs @@ -213,7 +213,7 @@ fn test_inset_axes_5() -> Result<(), StrError> { let buffered = BufReader::new(file); let lines_iter = buffered.lines(); let n = lines_iter.count().clone(); - assert!(n > 2400 && n < 2500); + assert!(n > 2300 && n < 2500); Ok(()) } diff --git a/tests/test_plot.rs b/tests/test_plot.rs index 8245860..c99994d 100644 --- a/tests/test_plot.rs +++ b/tests/test_plot.rs @@ -155,7 +155,7 @@ fn test_plot_3d() -> Result<(), StrError> { let buffered = BufReader::new(file); let lines_iter = buffered.lines(); let n_lines = lines_iter.count(); - assert!(n_lines > 1800 && n_lines < 1900); + assert!(n_lines > 1800 && n_lines < 1950); Ok(()) } diff --git a/tests/test_stream.rs b/tests/test_stream.rs index fc10d4d..ad995a4 100644 --- a/tests/test_stream.rs +++ b/tests/test_stream.rs @@ -51,7 +51,7 @@ fn test_stream_arrows_1() -> Result<(), StrError> { let buffered = BufReader::new(file); let lines_iter = buffered.lines(); let n = lines_iter.count(); - assert!(n > 8650 && n < 8730); + assert!(n > 4300 && n < 8730); Ok(()) } @@ -90,6 +90,6 @@ fn test_stream_arrows_2() -> Result<(), StrError> { let buffered = BufReader::new(file); let lines_iter = buffered.lines(); let n = lines_iter.count(); - assert!(n > 3600 && n < 3680); + assert!(n > 2000 && n < 3680); Ok(()) } From 2cac6040d3141f4fa619aacac5e5b689e767536f Mon Sep 17 00:00:00 2001 From: Dorival Pedroso Date: Sun, 24 May 2026 08:44:57 +1000 Subject: [PATCH 2/2] Add file to test on Arch Linux --- .github/workflows/test_on_arch_linux.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/test_on_arch_linux.yml diff --git a/.github/workflows/test_on_arch_linux.yml b/.github/workflows/test_on_arch_linux.yml new file mode 100644 index 0000000..0e4b8d2 --- /dev/null +++ b/.github/workflows/test_on_arch_linux.yml @@ -0,0 +1,15 @@ +name: Test +on: [pull_request] +jobs: + test_on_arch_linux: + runs-on: ubuntu-latest + container: + image: archlinux:latest + steps: + - uses: actions/checkout@v4 + - name: Install Libraries and Rust + run: | + pacman -Syu --noconfirm python-matplotlib rust cargo + - name: Run tests + run: | + cargo test -- --nocapture