pyo3/conversions/std/
ipaddr.rs

1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
2
3use crate::conversion::IntoPyObject;
4use crate::exceptions::PyValueError;
5use crate::sync::PyOnceLock;
6use crate::types::any::PyAnyMethods;
7use crate::types::string::PyStringMethods;
8use crate::types::PyType;
9use crate::{intern, Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, Python};
10
11impl FromPyObject<'_, '_> for IpAddr {
12    type Error = PyErr;
13
14    fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
15        match obj.getattr(intern!(obj.py(), "packed")) {
16            Ok(packed) => {
17                if let Ok(packed) = packed.extract::<[u8; 4]>() {
18                    Ok(IpAddr::V4(Ipv4Addr::from(packed)))
19                } else if let Ok(packed) = packed.extract::<[u8; 16]>() {
20                    Ok(IpAddr::V6(Ipv6Addr::from(packed)))
21                } else {
22                    Err(PyValueError::new_err("invalid packed length"))
23                }
24            }
25            Err(_) => {
26                // We don't have a .packed attribute, so we try to construct an IP from str().
27                obj.str()?.to_cow()?.parse().map_err(PyValueError::new_err)
28            }
29        }
30    }
31}
32
33impl<'py> IntoPyObject<'py> for Ipv4Addr {
34    type Target = PyAny;
35    type Output = Bound<'py, Self::Target>;
36    type Error = PyErr;
37
38    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
39        static IPV4_ADDRESS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
40        IPV4_ADDRESS
41            .import(py, "ipaddress", "IPv4Address")?
42            .call1((u32::from_be_bytes(self.octets()),))
43    }
44}
45
46impl<'py> IntoPyObject<'py> for &Ipv4Addr {
47    type Target = PyAny;
48    type Output = Bound<'py, Self::Target>;
49    type Error = PyErr;
50
51    #[inline]
52    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
53        (*self).into_pyobject(py)
54    }
55}
56
57impl<'py> IntoPyObject<'py> for Ipv6Addr {
58    type Target = PyAny;
59    type Output = Bound<'py, Self::Target>;
60    type Error = PyErr;
61
62    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
63        static IPV6_ADDRESS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
64        IPV6_ADDRESS
65            .import(py, "ipaddress", "IPv6Address")?
66            .call1((u128::from_be_bytes(self.octets()),))
67    }
68}
69
70impl<'py> IntoPyObject<'py> for &Ipv6Addr {
71    type Target = PyAny;
72    type Output = Bound<'py, Self::Target>;
73    type Error = PyErr;
74
75    #[inline]
76    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
77        (*self).into_pyobject(py)
78    }
79}
80
81impl<'py> IntoPyObject<'py> for IpAddr {
82    type Target = PyAny;
83    type Output = Bound<'py, Self::Target>;
84    type Error = PyErr;
85
86    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
87        match self {
88            IpAddr::V4(ip) => ip.into_pyobject(py),
89            IpAddr::V6(ip) => ip.into_pyobject(py),
90        }
91    }
92}
93
94impl<'py> IntoPyObject<'py> for &IpAddr {
95    type Target = PyAny;
96    type Output = Bound<'py, Self::Target>;
97    type Error = PyErr;
98
99    #[inline]
100    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
101        (*self).into_pyobject(py)
102    }
103}
104
105#[cfg(test)]
106mod test_ipaddr {
107    use std::str::FromStr;
108
109    use crate::types::PyString;
110
111    use super::*;
112
113    #[test]
114    fn test_roundtrip() {
115        Python::attach(|py| {
116            fn roundtrip(py: Python<'_>, ip: &str) {
117                let ip = IpAddr::from_str(ip).unwrap();
118                let py_cls = if ip.is_ipv4() {
119                    "IPv4Address"
120                } else {
121                    "IPv6Address"
122                };
123
124                let pyobj = ip.into_pyobject(py).unwrap();
125                let repr = pyobj.repr().unwrap();
126                let repr = repr.to_string_lossy();
127                assert_eq!(repr, format!("{py_cls}('{ip}')"));
128
129                let ip2: IpAddr = pyobj.extract().unwrap();
130                assert_eq!(ip, ip2);
131            }
132            roundtrip(py, "127.0.0.1");
133            roundtrip(py, "::1");
134            roundtrip(py, "0.0.0.0");
135        });
136    }
137
138    #[test]
139    fn test_from_pystring() {
140        Python::attach(|py| {
141            let py_str = PyString::new(py, "0:0:0:0:0:0:0:1");
142            let ip: IpAddr = py_str.extract().unwrap();
143            assert_eq!(ip, IpAddr::from_str("::1").unwrap());
144
145            let py_str = PyString::new(py, "invalid");
146            assert!(py_str.extract::<IpAddr>().is_err());
147        });
148    }
149}