Skip to content

Commit 980f05a

Browse files
committed
Add IpAddr <-> ipaddress.IPv(4/6)Address conversion
1 parent 2500e22 commit 980f05a

File tree

4 files changed

+128
-1
lines changed

4 files changed

+128
-1
lines changed

newsfragments/3197.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for converting to and from Python's `ipaddress.IPv4Address`/`ipaddress.IPv6Address` and `std::net::IpAddr`.

src/conversions/std/ipaddr.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
2+
3+
use crate::exceptions::PyValueError;
4+
use crate::sync::GILOnceCell;
5+
use crate::types::PyType;
6+
use crate::{intern, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject};
7+
8+
impl FromPyObject<'_> for IpAddr {
9+
fn extract(obj: &PyAny) -> PyResult<Self> {
10+
match obj.getattr(intern!(obj.py(), "packed")) {
11+
Ok(packed) => {
12+
if let Ok(packed) = packed.extract::<[u8; 4]>() {
13+
Ok(IpAddr::V4(Ipv4Addr::from(packed)))
14+
} else if let Ok(packed) = packed.extract::<[u8; 16]>() {
15+
Ok(IpAddr::V6(Ipv6Addr::from(packed)))
16+
} else {
17+
Err(PyValueError::new_err("invalid packed length"))
18+
}
19+
}
20+
Err(_) => {
21+
// We don't have a .packed attribute, so we try to construct an IP from str().
22+
obj.str()?.to_str()?.parse().map_err(PyValueError::new_err)
23+
}
24+
}
25+
}
26+
}
27+
28+
impl ToPyObject for Ipv4Addr {
29+
fn to_object(&self, py: Python<'_>) -> PyObject {
30+
static IPV4_ADDRESS: GILOnceCell<Py<PyType>> = GILOnceCell::new();
31+
IPV4_ADDRESS
32+
.get_or_try_init_type_ref(py, "ipaddress", "IPv4Address")
33+
.expect("failed to load ipaddress.IPv4Address")
34+
.call1((u32::from_be_bytes(self.octets()),))
35+
.expect("failed to construct ipaddress.IPv4Address")
36+
.to_object(py)
37+
}
38+
}
39+
40+
impl ToPyObject for Ipv6Addr {
41+
fn to_object(&self, py: Python<'_>) -> PyObject {
42+
static IPV6_ADDRESS: GILOnceCell<Py<PyType>> = GILOnceCell::new();
43+
IPV6_ADDRESS
44+
.get_or_try_init_type_ref(py, "ipaddress", "IPv6Address")
45+
.expect("failed to load ipaddress.IPv6Address")
46+
.call1((u128::from_be_bytes(self.octets()),))
47+
.expect("failed to construct ipaddress.IPv6Address")
48+
.to_object(py)
49+
}
50+
}
51+
52+
impl ToPyObject for IpAddr {
53+
fn to_object(&self, py: Python<'_>) -> PyObject {
54+
match self {
55+
IpAddr::V4(ip) => ip.to_object(py),
56+
IpAddr::V6(ip) => ip.to_object(py),
57+
}
58+
}
59+
}
60+
61+
impl IntoPy<PyObject> for IpAddr {
62+
fn into_py(self, py: Python<'_>) -> PyObject {
63+
self.to_object(py)
64+
}
65+
}
66+
67+
#[cfg(test)]
68+
mod test_ipaddr {
69+
use std::str::FromStr;
70+
71+
use crate::types::PyString;
72+
73+
use super::*;
74+
75+
#[test]
76+
fn test_roundtrip() {
77+
Python::with_gil(|py| {
78+
fn roundtrip(py: Python<'_>, ip: &str) {
79+
let ip = IpAddr::from_str(ip).unwrap();
80+
let py_cls = if ip.is_ipv4() {
81+
"IPv4Address"
82+
} else {
83+
"IPv6Address"
84+
};
85+
86+
let pyobj = ip.into_py(py);
87+
let repr = pyobj.as_ref(py).repr().unwrap().to_string_lossy();
88+
assert_eq!(repr, format!("{}('{}')", py_cls, ip));
89+
90+
let ip2: IpAddr = pyobj.extract(py).unwrap();
91+
assert_eq!(ip, ip2);
92+
}
93+
roundtrip(py, "127.0.0.1");
94+
roundtrip(py, "::1");
95+
roundtrip(py, "0.0.0.0");
96+
});
97+
}
98+
99+
#[test]
100+
fn test_from_pystring() {
101+
Python::with_gil(|py| {
102+
let py_str = PyString::new(py, "0:0:0:0:0:0:0:1");
103+
let ip: IpAddr = py_str.to_object(py).extract(py).unwrap();
104+
assert_eq!(ip, IpAddr::from_str("::1").unwrap());
105+
106+
let py_str = PyString::new(py, "invalid");
107+
assert!(py_str.to_object(py).extract::<IpAddr>(py).is_err());
108+
});
109+
}
110+
}

src/conversions/std/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod array;
2+
mod ipaddr;
23
mod map;
34
mod num;
45
mod osstr;

src/sync.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! Synchronization mechanisms based on the Python GIL.
2-
use crate::{types::PyString, Py, Python};
2+
use crate::{types::PyString, types::PyType, Py, PyErr, Python};
33
use std::cell::UnsafeCell;
44

55
/// Value with concurrent access protected by the GIL.
@@ -169,6 +169,21 @@ impl<T> GILOnceCell<T> {
169169
}
170170
}
171171

172+
impl GILOnceCell<Py<PyType>> {
173+
/// Get a reference to the contained Python type, initializing it if needed.
174+
///
175+
/// This is a shorthand method for `get_or_init` which imports the type from Python on init.
176+
pub(crate) fn get_or_try_init_type_ref<'py>(
177+
&'py self,
178+
py: Python<'py>,
179+
module_name: &str,
180+
attr_name: &str,
181+
) -> Result<&'py PyType, PyErr> {
182+
self.get_or_try_init(py, || py.import(module_name)?.getattr(attr_name)?.extract())
183+
.map(|ty| ty.as_ref(py))
184+
}
185+
}
186+
172187
/// Interns `text` as a Python string and stores a reference to it in static storage.
173188
///
174189
/// A reference to the same Python string is returned on each invocation.

0 commit comments

Comments
 (0)