From 2616d3de761732b7fbbfd46e64cebe086e98ce80 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 17 Dec 2020 02:42:11 +0000 Subject: [PATCH] python: add version() to get running version --- CHANGELOG.md | 1 + src/err/mod.rs | 10 +-- src/exceptions.rs | 18 ++--- src/lib.rs | 2 +- src/python.rs | 170 +++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 185 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c744eb60..a57f1a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Add support for conversion between `char` and `PyString`. [#1282](https://github.com/PyO3/pyo3/pull/1282) - Add FFI definitions for `PyBuffer_SizeFromFormat`, `PyObject_LengthHint`, `PyObject_CallNoArgs`, `PyObject_CallOneArg`, `PyObject_CallMethodNoArgs`, `PyObject_CallMethodOneArg`, `PyObject_VectorcallDict`, and `PyObject_VectorcallMethod`. [#1287](https://github.com/PyO3/pyo3/pull/1287) - Add conversions between u128/i128 and PyLong for PyPy. [#1310](https://github.com/PyO3/pyo3/pull/1310) +- Add `Python::version()` and `Python::version_info()` to get the running interpreter version. [#1322](https://github.com/PyO3/pyo3/pull/1322) ### Changed - Change return type `PyType::name()` from `Cow` to `PyResult<&str>`. [#1152](https://github.com/PyO3/pyo3/pull/1152) diff --git a/src/err/mod.rs b/src/err/mod.rs index bfbc1f9a..7e0241bc 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -602,10 +602,12 @@ mod tests { .split(", "); assert_eq!(fields.next().unwrap(), "type: "); - #[cfg(not(Py_3_7))] // Python 3.6 and below formats the repr differently - assert_eq!(fields.next().unwrap(), ("value: Exception('banana',)")); - #[cfg(Py_3_7)] - assert_eq!(fields.next().unwrap(), "value: Exception('banana')"); + if py.version_info() >= (3, 7) { + assert_eq!(fields.next().unwrap(), "value: Exception('banana')"); + } else { + // Python 3.6 and below formats the repr differently + assert_eq!(fields.next().unwrap(), ("value: Exception('banana',)")); + } let traceback = fields.next().unwrap(); assert!(traceback.starts_with("traceback: Some(= (3, 7) { + assert_eq!(format!("{:?}", exc), "Exception('banana')"); + } else { + assert_eq!(format!("{:?}", exc), "Exception('banana',)"); + } let source = exc.source().expect("cause should exist"); - #[cfg(Py_3_7)] - assert_eq!(format!("{:?}", source), "TypeError('peach')"); - #[cfg(not(Py_3_7))] - assert_eq!(format!("{:?}", source), "TypeError('peach',)"); + if py.version_info() >= (3, 7) { + assert_eq!(format!("{:?}", source), "TypeError('peach')"); + } else { + assert_eq!(format!("{:?}", source), "TypeError('peach',)"); + } let source_source = source.source(); assert!(source_source.is_none(), "source_source should be None"); diff --git a/src/lib.rs b/src/lib.rs index c07f4690..32d145fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,7 @@ pub use crate::instance::{Py, PyNativeType, PyObject}; pub use crate::pycell::{PyCell, PyRef, PyRefMut}; pub use crate::pyclass::PyClass; pub use crate::pyclass_init::PyClassInitializer; -pub use crate::python::{prepare_freethreaded_python, Python}; +pub use crate::python::{prepare_freethreaded_python, Python, PythonVersionInfo}; pub use crate::type_object::{type_flags, PyTypeInfo}; // Since PyAny is as important as PyObject, we expose it to the top level. pub use crate::types::PyAny; diff --git a/src/python.rs b/src/python.rs index 3904f011..b1f0668b 100644 --- a/src/python.rs +++ b/src/python.rs @@ -7,12 +7,96 @@ use crate::gil::{self, GILGuard, GILPool}; use crate::type_object::{PyTypeInfo, PyTypeObject}; use crate::types::{PyAny, PyDict, PyModule, PyType}; use crate::{ffi, AsPyPointer, FromPyPointer, IntoPyPointer, PyNativeType, PyObject, PyTryFrom}; -use std::ffi::CString; +use std::ffi::{CStr, CString}; use std::marker::PhantomData; -use std::os::raw::c_int; +use std::os::raw::{c_char, c_int}; pub use gil::prepare_freethreaded_python; +/// Represents the major, minor, and patch (if any) versions of this interpreter. +/// +/// See [Python::version]. +#[derive(Debug)] +pub struct PythonVersionInfo<'p> { + pub major: u8, + pub minor: u8, + pub patch: u8, + pub suffix: Option<&'p str>, +} + +impl<'p> PythonVersionInfo<'p> { + /// Parses a hard-coded Python interpreter version string (e.g. 3.9.0a4+). + /// + /// Panics if the string is ill-formatted. + fn from_str(version_number_str: &'p str) -> Self { + fn split_and_parse_number(version_part: &str) -> (u8, Option<&str>) { + match version_part.find(|c: char| !c.is_ascii_digit()) { + None => (version_part.parse().unwrap(), None), + Some(version_part_suffix_start) => { + let (version_part, version_part_suffix) = + version_part.split_at(version_part_suffix_start); + (version_part.parse().unwrap(), Some(version_part_suffix)) + } + } + } + + let mut parts = version_number_str.split('.'); + let major_str = parts.next().expect("Python major version missing"); + let minor_str = parts.next().expect("Python minor version missing"); + let patch_str = parts.next(); + assert!( + parts.next().is_none(), + "Python version string has too many parts" + ); + + let major = major_str + .parse() + .expect("Python major version not an integer"); + let (minor, suffix) = split_and_parse_number(minor_str); + if suffix.is_some() { + assert!(patch_str.is_none()); + return PythonVersionInfo { + major, + minor, + patch: 0, + suffix, + }; + } + + let (patch, suffix) = patch_str.map(split_and_parse_number).unwrap_or_default(); + PythonVersionInfo { + major, + minor, + patch, + suffix, + } + } +} + +impl PartialEq<(u8, u8)> for PythonVersionInfo<'_> { + fn eq(&self, other: &(u8, u8)) -> bool { + self.major == other.0 && self.minor == other.1 + } +} + +impl PartialEq<(u8, u8, u8)> for PythonVersionInfo<'_> { + fn eq(&self, other: &(u8, u8, u8)) -> bool { + self.major == other.0 && self.minor == other.1 && self.patch == other.2 + } +} + +impl PartialOrd<(u8, u8)> for PythonVersionInfo<'_> { + fn partial_cmp(&self, other: &(u8, u8)) -> Option { + (self.major, self.minor).partial_cmp(other) + } +} + +impl PartialOrd<(u8, u8, u8)> for PythonVersionInfo<'_> { + fn partial_cmp(&self, other: &(u8, u8, u8)) -> Option { + (self.major, self.minor, self.patch).partial_cmp(other) + } +} + /// Marker type that indicates that the GIL is currently held. /// /// The `Python` struct is a zero-sized marker struct that is required for most Python operations. @@ -302,6 +386,49 @@ impl<'p> Python<'p> { unsafe { PyObject::from_borrowed_ptr(self, ffi::Py_NotImplemented()) } } + /// Gets the running Python interpreter version as a string. + /// + /// This is a wrapper around the ffi call Py_GetVersion. + /// + /// # Example + /// ```rust + /// # use pyo3::Python; + /// Python::with_gil(|py| { + /// // The full string could be, for example: + /// // "3.0a5+ (py3k:63103M, May 12 2008, 00:53:55) \n[GCC 4.2.3]" + /// assert!(py.version().starts_with("3.")); + /// }); + /// ``` + pub fn version(self) -> &'p str { + unsafe { + CStr::from_ptr(ffi::Py_GetVersion() as *const c_char) + .to_str() + .expect("Python version string not UTF-8") + } + } + + /// Gets the running Python interpreter version as a struct similar to + /// `sys.version_info`. + /// + /// # Example + /// ```rust + /// # use pyo3::Python; + /// Python::with_gil(|py| { + /// // PyO3 supports Python 3.6 and up. + /// assert!(py.version_info() >= (3, 6)); + /// assert!(py.version_info() >= (3, 6, 0)); + /// }); + /// ``` + pub fn version_info(self) -> PythonVersionInfo<'p> { + let version_str = self.version(); + + // Portion of the version string returned by Py_GetVersion up to the first space is the + // version number. + let version_number_str = version_str.split(' ').next().unwrap_or(version_str); + + PythonVersionInfo::from_str(version_number_str) + } + /// Registers the object in the release pool, and tries to downcast to specific type. pub fn checked_cast_as(self, obj: PyObject) -> Result<&'p T, PyDowncastError<'p>> where @@ -527,8 +654,8 @@ impl<'p> Python<'p> { #[cfg(test)] mod test { + use super::*; use crate::types::{IntoPyDict, PyAny, PyBool, PyInt, PyList}; - use crate::Python; #[test] fn test_eval() { @@ -618,4 +745,41 @@ mod test { let list = PyList::new(py, &[1, 2, 3, 4]); assert_eq!(list.extract::>().unwrap(), vec![1, 2, 3, 4]); } + + #[test] + fn test_python_version_info() { + Python::with_gil(|py| { + let version = py.version_info(); + #[cfg(Py_3_6)] + assert!(version >= (3, 6)); + #[cfg(Py_3_6)] + assert!(version >= (3, 6, 0)); + #[cfg(Py_3_7)] + assert!(version >= (3, 7)); + #[cfg(Py_3_7)] + assert!(version >= (3, 7, 0)); + #[cfg(Py_3_8)] + assert!(version >= (3, 8)); + #[cfg(Py_3_8)] + assert!(version >= (3, 8, 0)); + #[cfg(Py_3_9)] + assert!(version >= (3, 9)); + #[cfg(Py_3_9)] + assert!(version >= (3, 9, 0)); + }); + } + + #[test] + fn test_python_version_info_parse() { + assert!(PythonVersionInfo::from_str("3.5.0a1") >= (3, 5, 0)); + assert!(PythonVersionInfo::from_str("3.5+") >= (3, 5, 0)); + assert!(PythonVersionInfo::from_str("3.5+") == (3, 5, 0)); + assert!(PythonVersionInfo::from_str("3.5+") != (3, 5, 1)); + assert!(PythonVersionInfo::from_str("3.5.2a1+") < (3, 5, 3)); + assert!(PythonVersionInfo::from_str("3.5.2a1+") == (3, 5, 2)); + assert!(PythonVersionInfo::from_str("3.5.2a1+") == (3, 5)); + assert!(PythonVersionInfo::from_str("3.5+") == (3, 5)); + assert!(PythonVersionInfo::from_str("3.5.2a1+") < (3, 6)); + assert!(PythonVersionInfo::from_str("3.5.2a1+") > (3, 4)); + } }