python: add version() to get running version

This commit is contained in:
David Hewitt 2020-12-17 02:42:11 +00:00
parent 1f64f98a33
commit 2616d3de76
5 changed files with 185 additions and 16 deletions

View file

@ -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 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 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 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 ### Changed
- Change return type `PyType::name()` from `Cow<str>` to `PyResult<&str>`. [#1152](https://github.com/PyO3/pyo3/pull/1152) - Change return type `PyType::name()` from `Cow<str>` to `PyResult<&str>`. [#1152](https://github.com/PyO3/pyo3/pull/1152)

View file

@ -602,10 +602,12 @@ mod tests {
.split(", "); .split(", ");
assert_eq!(fields.next().unwrap(), "type: <class 'Exception'>"); assert_eq!(fields.next().unwrap(), "type: <class 'Exception'>");
#[cfg(not(Py_3_7))] // Python 3.6 and below formats the repr differently if py.version_info() >= (3, 7) {
assert_eq!(fields.next().unwrap(), ("value: Exception('banana',)"));
#[cfg(Py_3_7)]
assert_eq!(fields.next().unwrap(), "value: Exception('banana')"); 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(); let traceback = fields.next().unwrap();
assert!(traceback.starts_with("traceback: Some(<traceback object at 0x")); assert!(traceback.starts_with("traceback: Some(<traceback object at 0x"));

View file

@ -562,17 +562,19 @@ mod test {
.into_instance(py) .into_instance(py)
.into_ref(py); .into_ref(py);
#[cfg(Py_3_7)] if py.version_info() >= (3, 7) {
assert_eq!(format!("{:?}", exc), "Exception('banana')"); assert_eq!(format!("{:?}", exc), "Exception('banana')");
#[cfg(not(Py_3_7))] } else {
assert_eq!(format!("{:?}", exc), "Exception('banana',)"); assert_eq!(format!("{:?}", exc), "Exception('banana',)");
}
let source = exc.source().expect("cause should exist"); let source = exc.source().expect("cause should exist");
#[cfg(Py_3_7)] if py.version_info() >= (3, 7) {
assert_eq!(format!("{:?}", source), "TypeError('peach')"); assert_eq!(format!("{:?}", source), "TypeError('peach')");
#[cfg(not(Py_3_7))] } else {
assert_eq!(format!("{:?}", source), "TypeError('peach',)"); assert_eq!(format!("{:?}", source), "TypeError('peach',)");
}
let source_source = source.source(); let source_source = source.source();
assert!(source_source.is_none(), "source_source should be None"); assert!(source_source.is_none(), "source_source should be None");

View file

@ -145,7 +145,7 @@ pub use crate::instance::{Py, PyNativeType, PyObject};
pub use crate::pycell::{PyCell, PyRef, PyRefMut}; pub use crate::pycell::{PyCell, PyRef, PyRefMut};
pub use crate::pyclass::PyClass; pub use crate::pyclass::PyClass;
pub use crate::pyclass_init::PyClassInitializer; 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}; pub use crate::type_object::{type_flags, PyTypeInfo};
// Since PyAny is as important as PyObject, we expose it to the top level. // Since PyAny is as important as PyObject, we expose it to the top level.
pub use crate::types::PyAny; pub use crate::types::PyAny;

View file

@ -7,12 +7,96 @@ use crate::gil::{self, GILGuard, GILPool};
use crate::type_object::{PyTypeInfo, PyTypeObject}; use crate::type_object::{PyTypeInfo, PyTypeObject};
use crate::types::{PyAny, PyDict, PyModule, PyType}; use crate::types::{PyAny, PyDict, PyModule, PyType};
use crate::{ffi, AsPyPointer, FromPyPointer, IntoPyPointer, PyNativeType, PyObject, PyTryFrom}; use crate::{ffi, AsPyPointer, FromPyPointer, IntoPyPointer, PyNativeType, PyObject, PyTryFrom};
use std::ffi::CString; use std::ffi::{CStr, CString};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::os::raw::c_int; use std::os::raw::{c_char, c_int};
pub use gil::prepare_freethreaded_python; 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<std::cmp::Ordering> {
(self.major, self.minor).partial_cmp(other)
}
}
impl PartialOrd<(u8, u8, u8)> for PythonVersionInfo<'_> {
fn partial_cmp(&self, other: &(u8, u8, u8)) -> Option<std::cmp::Ordering> {
(self.major, self.minor, self.patch).partial_cmp(other)
}
}
/// Marker type that indicates that the GIL is currently held. /// 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. /// 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()) } 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. /// Registers the object in the release pool, and tries to downcast to specific type.
pub fn checked_cast_as<T>(self, obj: PyObject) -> Result<&'p T, PyDowncastError<'p>> pub fn checked_cast_as<T>(self, obj: PyObject) -> Result<&'p T, PyDowncastError<'p>>
where where
@ -527,8 +654,8 @@ impl<'p> Python<'p> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*;
use crate::types::{IntoPyDict, PyAny, PyBool, PyInt, PyList}; use crate::types::{IntoPyDict, PyAny, PyBool, PyInt, PyList};
use crate::Python;
#[test] #[test]
fn test_eval() { fn test_eval() {
@ -618,4 +745,41 @@ mod test {
let list = PyList::new(py, &[1, 2, 3, 4]); let list = PyList::new(py, &[1, 2, 3, 4]);
assert_eq!(list.extract::<Vec<i32>>().unwrap(), vec![1, 2, 3, 4]); assert_eq!(list.extract::<Vec<i32>>().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));
}
} }