1use std::collections::HashMap;
20use std::fmt::Debug;
21use std::time::Duration;
22
23use hyperactor::channel::BindSpec;
24use hyperactor_config::AttrValue;
25use hyperactor_config::CONFIG;
26use hyperactor_config::ConfigAttr;
27use hyperactor_config::attrs::AttrKeyInfo;
28use hyperactor_config::attrs::Attrs;
29use hyperactor_config::attrs::ErasedKey;
30use hyperactor_config::attrs::declare_attrs;
31use hyperactor_config::global::Source;
32use pyo3::conversion::IntoPyObject;
33use pyo3::conversion::IntoPyObjectExt;
34use pyo3::exceptions::PyTypeError;
35use pyo3::exceptions::PyValueError;
36use pyo3::prelude::*;
37use typeuri::Named;
38
39use crate::channel::PyBindSpec;
40
41#[pyclass(
45 module = "monarch._rust_bindings.monarch_hyperactor.config",
46 eq,
47 eq_int,
48 name = "Encoding"
49)]
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum PyEncoding {
52 Bincode,
53 Json,
54 Multipart,
55}
56
57impl From<wirevalue::Encoding> for PyEncoding {
58 fn from(e: wirevalue::Encoding) -> Self {
59 match e {
60 wirevalue::Encoding::Bincode => PyEncoding::Bincode,
61 wirevalue::Encoding::Json => PyEncoding::Json,
62 wirevalue::Encoding::Multipart => PyEncoding::Multipart,
63 }
64 }
65}
66
67impl From<PyEncoding> for wirevalue::Encoding {
68 fn from(e: PyEncoding) -> Self {
69 match e {
70 PyEncoding::Bincode => wirevalue::Encoding::Bincode,
71 PyEncoding::Json => wirevalue::Encoding::Json,
72 PyEncoding::Multipart => wirevalue::Encoding::Multipart,
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct PyPortRange(pub std::ops::Range<u16>);
87
88impl From<PyPortRange> for std::ops::Range<u16> {
89 fn from(r: PyPortRange) -> Self {
90 r.0
91 }
92}
93
94impl From<std::ops::Range<u16>> for PyPortRange {
95 fn from(r: std::ops::Range<u16>) -> Self {
96 PyPortRange(r)
97 }
98}
99
100impl<'py> FromPyObject<'py> for PyPortRange {
101 fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
102 let slice = ob.downcast::<pyo3::types::PySlice>().map_err(|_| {
104 PyTypeError::new_err("Port range must be a slice object: slice(start, stop)")
105 })?;
106
107 let step = slice.getattr("step")?;
109 if !step.is_none() {
110 let step_val: isize = step.extract().map_err(|_| {
111 PyTypeError::new_err("slice.step must be None or 1 for port ranges")
112 })?;
113 if step_val != 1 {
114 return Err(PyValueError::new_err(format!(
115 "Invalid slice step {}: port ranges require step=None or step=1",
116 step_val
117 )));
118 }
119 }
120
121 let start_obj = slice.getattr("start")?;
123 if start_obj.is_none() {
124 return Err(PyTypeError::new_err(
125 "slice.start must be set to an integer in range [0, 65535]",
126 ));
127 }
128 let start = start_obj.extract::<u16>().map_err(|_| {
129 PyTypeError::new_err("slice.start must be an integer in range [0, 65535]")
130 })?;
131
132 let stop_obj = slice.getattr("stop")?;
134 if stop_obj.is_none() {
135 return Err(PyTypeError::new_err(
136 "slice.stop must be set to an integer in range [0, 65535]",
137 ));
138 }
139 let stop = stop_obj.extract::<u16>().map_err(|_| {
140 PyTypeError::new_err("slice.stop must be an integer in range [0, 65535]")
141 })?;
142
143 if start > stop {
145 return Err(PyValueError::new_err(format!(
146 "Invalid port range slice({}, {}): start cannot be greater than stop",
147 start, stop
148 )));
149 }
150
151 Ok(PyPortRange(start..stop))
152 }
153}
154
155impl<'py> IntoPyObject<'py> for PyPortRange {
156 type Target = PyAny;
157 type Output = Bound<'py, Self::Target>;
158 type Error = PyErr;
159
160 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
161 Ok(pyo3::types::PySlice::new(py, self.0.start as isize, self.0.end as isize, 1).into_any())
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct PyDuration(pub Duration);
172
173impl From<PyDuration> for Duration {
174 fn from(d: PyDuration) -> Self {
175 d.0
176 }
177}
178
179impl From<Duration> for PyDuration {
180 fn from(d: Duration) -> Self {
181 PyDuration(d)
182 }
183}
184
185impl<'py> FromPyObject<'py> for PyDuration {
186 fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
187 let s: String = ob.extract()?;
188 let duration = humantime::parse_duration(&s).map_err(|e| {
189 PyValueError::new_err(format!("Invalid duration format '{}': {}", s, e))
190 })?;
191 Ok(PyDuration(duration))
192 }
193}
194
195impl<'py> IntoPyObject<'py> for PyDuration {
196 type Target = PyAny;
197 type Output = Bound<'py, Self::Target>;
198 type Error = PyErr;
199
200 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
201 let formatted = humantime::format_duration(self.0).to_string();
202 formatted.into_bound_py_any(py)
203 }
204}
205
206declare_attrs! {
208 @meta(CONFIG = ConfigAttr::new(
210 Some("MONARCH_ACTOR_QUEUE_DISPATCH".to_string()),
211 Some("actor_queue_dispatch".to_string()),
212 ))
213 pub attr ACTOR_QUEUE_DISPATCH: bool = true;
214
215 @meta(CONFIG = ConfigAttr::new(
217 Some("MONARCH_TOKIO_WORKER_THREADS".to_string()),
218 Some("tokio_worker_threads".to_string()),
219 ))
220 pub attr TOKIO_WORKER_THREADS: Option<hyperactor_config::NonZeroUsize> = None;
221}
222
223#[pyfunction()]
227pub fn reload_config_from_env() -> PyResult<()> {
228 hyperactor_config::global::init_from_env();
230 Ok(())
231}
232
233#[pyfunction()]
234pub fn reset_config_to_defaults() -> PyResult<()> {
235 hyperactor_config::global::reset_to_defaults();
237 Ok(())
238}
239
240static KEY_BY_NAME: std::sync::LazyLock<HashMap<&'static str, &'static dyn ErasedKey>> =
245 std::sync::LazyLock::new(|| {
246 inventory::iter::<AttrKeyInfo>()
247 .filter_map(|info| {
248 info.meta
249 .get(CONFIG)
250 .and_then(|cfg: &ConfigAttr| cfg.py_name.as_deref())
251 .map(|py_name| (py_name, info.erased))
252 })
253 .collect()
254 });
255
256static TYPEHASH_TO_INFO: std::sync::LazyLock<HashMap<u64, &'static PythonConfigTypeInfo>> =
260 std::sync::LazyLock::new(|| {
261 inventory::iter::<PythonConfigTypeInfo>()
262 .map(|info| ((info.typehash)(), info))
263 .collect()
264 });
265
266fn get_global_config_py<'py, P, T>(
274 py: Python<'py>,
275 key: &'static dyn ErasedKey,
276) -> PyResult<Option<Py<PyAny>>>
277where
278 T: AttrValue + TryInto<P>,
279 P: IntoPyObjectExt<'py>,
280 PyErr: From<<T as TryInto<P>>::Error>,
281{
282 let key = key.downcast_ref::<T>().ok_or_else(|| {
286 PyTypeError::new_err(format!(
287 "internal config type mismatch for key `{}`",
288 key.name(),
289 ))
290 })?;
291 let val: Option<P> = hyperactor_config::global::try_get_cloned(*key)
292 .map(|v| v.try_into())
293 .transpose()?;
294 val.map(|v| v.into_py_any(py)).transpose()
295}
296
297fn get_runtime_config_py<'py, P, T>(
306 py: Python<'py>,
307 key: &'static dyn ErasedKey,
308) -> PyResult<Option<Py<PyAny>>>
309where
310 T: AttrValue + TryInto<P>,
311 P: IntoPyObjectExt<'py>,
312 PyErr: From<<T as TryInto<P>>::Error>,
313{
314 let key = key.downcast_ref::<T>().expect("cannot fail");
315 let runtime = hyperactor_config::global::runtime_attrs();
316 let val: Option<P> = runtime
317 .get(*key)
318 .cloned()
319 .map(|v| v.try_into())
320 .transpose()?;
321 val.map(|v| v.into_py_any(py)).transpose()
322}
323
324fn set_runtime_config_py<T: AttrValue + Debug>(
331 key: &'static dyn ErasedKey,
332 value: T,
333) -> PyResult<()> {
334 let key = key.downcast_ref().expect("cannot fail");
336 let mut attrs = Attrs::new();
337 attrs.set(*key, value);
338 hyperactor_config::global::create_or_merge(Source::Runtime, attrs);
339 Ok(())
340}
341
342fn py_value_repr(py: Python<'_>, val: &Py<PyAny>) -> String {
343 val.bind(py)
344 .repr()
345 .map(|repr| repr.to_string_lossy().into_owned())
346 .unwrap_or_else(|_| "<unprintable>".to_string())
347}
348
349fn configure_kwarg(py: Python<'_>, name: &str, val: Py<PyAny>) -> PyResult<()> {
363 let key = match KEY_BY_NAME.get(name) {
366 None => {
367 return Err(PyValueError::new_err(format!(
368 "invalid configuration key: `{}`",
369 name
370 )));
371 }
372 Some(key) => *key,
373 };
374
375 match TYPEHASH_TO_INFO.get(&key.typehash()) {
379 None => Err(PyTypeError::new_err(format!(
380 "configuration key `{}` has type `{}`, but configuring with values of this type from Python is not supported.",
381 name,
382 key.typename()
383 ))),
384 Some(info) => (info.set_runtime_config)(py, key, val),
385 }
386}
387
388struct PythonConfigTypeInfo {
413 typehash: fn() -> u64,
415 get_global_config:
417 fn(py: Python<'_>, key: &'static dyn ErasedKey) -> PyResult<Option<Py<PyAny>>>,
418 set_runtime_config:
420 fn(py: Python<'_>, key: &'static dyn ErasedKey, val: Py<PyAny>) -> PyResult<()>,
421 get_runtime_config:
423 fn(py: Python<'_>, key: &'static dyn ErasedKey) -> PyResult<Option<Py<PyAny>>>,
424}
425
426inventory::collect!(PythonConfigTypeInfo);
430
431macro_rules! declare_py_config_type {
445 ($($ty:ty),+ $(,)?) => {
446 hyperactor::internal_macro_support::paste! {
447 $(
448 hyperactor::internal_macro_support::inventory::submit! {
449 PythonConfigTypeInfo {
450 typehash: $ty::typehash,
451 set_runtime_config: |py, key, val| {
452 let val: $ty = val.extract::<$ty>(py).map_err(|err| PyTypeError::new_err(format!(
453 "invalid value `{}` for configuration key `{}` ({})",
454 val, key.name(), err
455 )))?;
456 set_runtime_config_py(key, val)
457 },
458 get_global_config: |py, key| {
459 get_global_config_py::<$ty, $ty>(py, key)
460 },
461 get_runtime_config: |py, key| {
462 get_runtime_config_py::<$ty, $ty>(py, key)
463 }
464 }
465 }
466 )+
467 }
468 };
469 (Option<$py_ty:ty> as Option<$ty:ty>) => {
470 hyperactor::internal_macro_support::inventory::submit! {
471 PythonConfigTypeInfo {
472 typehash: <Option<$ty> as Named>::typehash,
473 set_runtime_config: |py, key, val| {
474 let val_repr = py_value_repr(py, &val);
475 let val: Option<$py_ty> = val.extract::<Option<$py_ty>>(py)
476 .map_err(|err| PyTypeError::new_err(format!(
477 "invalid value `{}` for configuration key `{}` ({})",
478 val_repr, key.name(), err
479 )))?;
480 let val: Option<$ty> = val
482 .map(TryInto::try_into)
483 .transpose()
484 .map_err(|err| PyValueError::new_err(format!(
485 "invalid value `{}` for configuration key `{}` ({})",
486 val_repr, key.name(), err
487 )))?;
488 set_runtime_config_py(key, val)
489 },
490 get_global_config: |py, key| {
491 let key = key.downcast_ref::<Option<$ty>>().ok_or_else(|| {
492 PyTypeError::new_err(format!(
493 "internal config type mismatch for key `{}`",
494 key.name(),
495 ))
496 })?;
497 match hyperactor_config::global::try_get_cloned(key.clone()) {
498 None => Ok(None),
499 Some(opt_val) => opt_val.map(<$py_ty>::from).into_py_any(py).map(Some),
500 }
501 },
502 get_runtime_config: |py, key| {
503 let key = key.downcast_ref::<Option<$ty>>().ok_or_else(|| {
504 PyTypeError::new_err(format!(
505 "internal config type mismatch for key `{}`",
506 key.name(),
507 ))
508 })?;
509 let runtime = hyperactor_config::global::runtime_attrs();
510 match runtime.get(key.clone()).cloned() {
511 None => Ok(None),
512 Some(opt_val) => opt_val.map(<$py_ty>::from).into_py_any(py).map(Some),
513 }
514 }
515 }
516 }
517 };
518 ($py_ty:ty as $ty:ty) => {
519 hyperactor::internal_macro_support::paste! {
520 hyperactor::internal_macro_support::inventory::submit! {
521 PythonConfigTypeInfo {
522 typehash: $ty::typehash,
523 set_runtime_config: |py, key, val| {
524 let val_repr = py_value_repr(py, &val);
525 let val: $py_ty = val.extract::<$py_ty>(py).map_err(|err| PyTypeError::new_err(format!(
526 "invalid value `{}` for configuration key `{}` ({})",
527 val_repr, key.name(), err
528 )))?;
529 let val: $ty = val.try_into().map_err(|err| PyValueError::new_err(format!(
531 "invalid value `{}` for configuration key `{}` ({})",
532 val_repr, key.name(), err
533 )))?;
534 set_runtime_config_py(key, val)
535 },
536 get_global_config: |py, key| {
537 get_global_config_py::<$py_ty, $ty>(py, key)
538 },
539 get_runtime_config: |py, key| {
540 get_runtime_config_py::<$py_ty, $ty>(py, key)
541 }
542 }
543 }
544 }
545 };
546}
547
548declare_py_config_type!(PyBindSpec as BindSpec);
549declare_py_config_type!(PyDuration as Duration);
550declare_py_config_type!(Option<PyDuration> as Option<Duration>);
551declare_py_config_type!(PyEncoding as wirevalue::Encoding);
552declare_py_config_type!(PyPortRange as std::ops::Range::<u16>);
553declare_py_config_type!(String as hyperactor_mesh::config::SocketAddrStr);
554declare_py_config_type!(usize as hyperactor_config::NonZeroUsize);
555declare_py_config_type!(Option<usize> as Option<hyperactor_config::NonZeroUsize>);
556declare_py_config_type!(
557 i8, i16, i32, i64, u8, u16, u32, u64, usize, f32, f64, bool, String
558);
559declare_py_config_type!(
560 Option::<i8>,
561 Option::<i16>,
562 Option::<i32>,
563 Option::<i64>,
564 Option::<u8>,
565 Option::<u16>,
566 Option::<u32>,
567 Option::<u64>,
568 Option::<usize>,
569 Option::<f32>,
570 Option::<f64>,
571 Option::<bool>,
572);
573
574#[pyfunction]
590#[pyo3(signature = (**kwargs))]
591fn configure(py: Python<'_>, kwargs: Option<HashMap<String, Py<PyAny>>>) -> PyResult<()> {
592 kwargs
593 .map(|kwargs| {
594 kwargs.into_iter().try_for_each(|(key, val)| {
595 let val = if key == "default_transport" {
598 PyBindSpec::new(val.bind(py))?
599 .into_pyobject(py)?
600 .into_any()
601 .unbind()
602 } else {
603 val
604 };
605 configure_kwarg(py, &key, val)
606 })
607 })
608 .transpose()?;
609 Ok(())
610}
611
612#[pyfunction]
625fn get_global_config(py: Python<'_>) -> PyResult<HashMap<String, Py<PyAny>>> {
626 KEY_BY_NAME
627 .iter()
628 .filter_map(|(name, key)| match TYPEHASH_TO_INFO.get(&key.typehash()) {
629 None => None,
630 Some(info) => match (info.get_global_config)(py, *key) {
631 Err(err) => Some(Err(err)),
632 Ok(val) => val.map(|val| Ok(((*name).into(), val))),
633 },
634 })
635 .collect()
636}
637
638#[pyfunction]
664fn get_runtime_config(py: Python<'_>) -> PyResult<HashMap<String, Py<PyAny>>> {
665 KEY_BY_NAME
666 .iter()
667 .filter_map(|(name, key)| match TYPEHASH_TO_INFO.get(&key.typehash()) {
668 None => None,
669 Some(info) => match (info.get_runtime_config)(py, *key) {
670 Err(err) => Some(Err(err)),
671 Ok(val) => val.map(|val| Ok(((*name).into(), val))),
672 },
673 })
674 .collect()
675}
676
677#[pyfunction]
689fn clear_runtime_config(_py: Python<'_>) -> PyResult<()> {
690 hyperactor_config::global::clear(Source::Runtime);
691 Ok(())
692}
693
694pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
696 let reload = wrap_pyfunction!(reload_config_from_env, module)?;
697 reload.setattr(
698 "__module__",
699 "monarch._rust_bindings.monarch_hyperactor.config",
700 )?;
701 module.add_function(reload)?;
702
703 let reset = wrap_pyfunction!(reset_config_to_defaults, module)?;
704 reset.setattr(
705 "__module__",
706 "monarch._rust_bindings.monarch_hyperactor.config",
707 )?;
708 module.add_function(reset)?;
709
710 let configure = wrap_pyfunction!(configure, module)?;
711 configure.setattr(
712 "__module__",
713 "monarch._rust_bindings.monarch_hyperactor.config",
714 )?;
715 module.add_function(configure)?;
716
717 let get_global_config = wrap_pyfunction!(get_global_config, module)?;
718 get_global_config.setattr(
719 "__module__",
720 "monarch._rust_bindings.monarch_hyperactor.config",
721 )?;
722 module.add_function(get_global_config)?;
723
724 let get_runtime_config = wrap_pyfunction!(get_runtime_config, module)?;
725 get_runtime_config.setattr(
726 "__module__",
727 "monarch._rust_bindings.monarch_hyperactor.config",
728 )?;
729 module.add_function(get_runtime_config)?;
730
731 let clear_runtime_config = wrap_pyfunction!(clear_runtime_config, module)?;
732 clear_runtime_config.setattr(
733 "__module__",
734 "monarch._rust_bindings.monarch_hyperactor.config",
735 )?;
736 module.add_function(clear_runtime_config)?;
737
738 module.add_class::<PyEncoding>()?;
739
740 Ok(())
741}
742
743#[cfg(test)]
744mod tests {
745 use std::collections::HashMap;
746 use std::time::Duration;
747
748 use hyperactor_config::attrs::declare_attrs;
749 use pyo3::exceptions::PyValueError;
750 use pyo3::prelude::*;
751 use pyo3::types::PyString;
752 use pyo3::types::PyTuple;
753
754 use super::*;
755 use crate::runtime::GilSite;
756 use crate::runtime::monarch_with_gil_blocking;
757
758 declare_attrs! {
759 @meta(CONFIG = ConfigAttr::new(
760 None,
761 Some("test_nonzero_usize".to_string()),
762 ))
763 attr TEST_NONZERO_USIZE: hyperactor_config::NonZeroUsize =
764 hyperactor_config::NonZeroUsize::MIN;
765 }
766
767 #[test]
768 fn test_pyduration_parse_valid_formats() {
769 Python::initialize();
770 monarch_with_gil_blocking(GilSite::Test, |py| {
771 let s = PyString::new(py, "30s");
773 let d: PyDuration = s.extract().unwrap();
774 assert_eq!(d.0, Duration::from_secs(30));
775
776 let s = PyString::new(py, "5m");
777 let d: PyDuration = s.extract().unwrap();
778 assert_eq!(d.0, Duration::from_mins(5));
779
780 let s = PyString::new(py, "1h");
781 let d: PyDuration = s.extract().unwrap();
782 assert_eq!(d.0, Duration::from_secs(3600));
783
784 let s = PyString::new(py, "500ms");
785 let d: PyDuration = s.extract().unwrap();
786 assert_eq!(d.0, Duration::from_millis(500));
787
788 let s = PyString::new(py, "1m 30s");
789 let d: PyDuration = s.extract().unwrap();
790 assert_eq!(d.0, Duration::from_secs(90));
791 });
792 }
793
794 #[test]
795 fn test_pyduration_parse_invalid_format() {
796 Python::initialize();
797 monarch_with_gil_blocking(GilSite::Test, |py| {
798 let s = PyString::new(py, "invalid");
799 let result: PyResult<PyDuration> = s.extract();
800 assert!(result.is_err());
801 let err_msg = format!("{}", result.unwrap_err());
802 assert!(err_msg.contains("Invalid duration format"));
803 });
804 }
805
806 #[test]
807 fn test_pyduration_roundtrip() {
808 Python::initialize();
809 monarch_with_gil_blocking(GilSite::Test, |py| {
810 let original = Duration::from_secs(42);
811 let py_duration = PyDuration(original);
812 let py_obj = py_duration.into_pyobject(py).unwrap();
813 let back: PyDuration = py_obj.extract().unwrap();
814 assert_eq!(back.0, original);
815 });
816 }
817
818 #[test]
819 fn test_pyencoding_enum_variants() {
820 Python::initialize();
821 monarch_with_gil_blocking(GilSite::Test, |py| {
822 for variant in [PyEncoding::Bincode, PyEncoding::Json, PyEncoding::Multipart] {
824 let py_obj = Bound::new(py, variant).unwrap().into_any();
825 let back: PyEncoding = py_obj.extract().unwrap();
826 assert_eq!(back, variant);
827 }
828 });
829 }
830
831 #[test]
832 fn test_pyencoding_rejects_strings() {
833 Python::initialize();
834 monarch_with_gil_blocking(GilSite::Test, |_py| {
835 let s = PyString::new(_py, "bincode");
837 let result: PyResult<PyEncoding> = s.extract();
838 assert!(result.is_err());
839 });
840 }
841
842 #[test]
843 fn test_pyencoding_conversions() {
844 Python::initialize();
845 monarch_with_gil_blocking(GilSite::Test, |_py| {
846 let rust_enc = wirevalue::Encoding::Bincode;
848 let py_enc: PyEncoding = rust_enc.into();
849 assert_eq!(py_enc, PyEncoding::Bincode);
850
851 let back: wirevalue::Encoding = py_enc.into();
852 assert_eq!(back, rust_enc);
853
854 assert_eq!(
856 PyEncoding::from(wirevalue::Encoding::Json),
857 PyEncoding::Json
858 );
859 assert_eq!(
860 PyEncoding::from(wirevalue::Encoding::Multipart),
861 PyEncoding::Multipart
862 );
863 });
864 }
865
866 #[test]
867 fn test_nonzero_usize_config_rejects_zero_from_python() {
868 Python::initialize();
869 monarch_with_gil_blocking(GilSite::Test, |py| {
870 clear_runtime_config(py).expect("clear runtime config before test");
871
872 let kwargs = HashMap::from([(
873 "test_nonzero_usize".to_string(),
874 0usize.into_py_any(py).expect("convert integer to python"),
875 )]);
876 let err = configure(py, Some(kwargs)).expect_err("zero value should be rejected");
877
878 assert!(err.is_instance_of::<PyValueError>(py));
879 let err_msg = err.to_string();
880 assert!(
881 err_msg.contains(
882 "invalid value `0` for configuration key `monarch_hyperactor::config::tests::test_nonzero_usize`"
883 ),
884 "unexpected error message: {}",
885 err_msg
886 );
887 assert!(
888 err_msg.contains("expected non-zero usize"),
889 "unexpected error message: {}",
890 err_msg
891 );
892
893 clear_runtime_config(py).expect("clear runtime config after test");
894 });
895 }
896
897 #[test]
898 fn test_pyencoding_roundtrip() {
899 Python::initialize();
900 monarch_with_gil_blocking(GilSite::Test, |py| {
901 let original = wirevalue::Encoding::Multipart;
902 let py_encoding: PyEncoding = original.into();
903 let py_obj = Bound::new(py, py_encoding).unwrap().into_any();
904 let back: PyEncoding = py_obj.extract().unwrap();
905 let rust_back: wirevalue::Encoding = back.into();
906 assert_eq!(rust_back, original);
907 });
908 }
909
910 #[test]
911 fn test_pyportrange_parse_slice_format() {
912 Python::initialize();
913 monarch_with_gil_blocking(GilSite::Test, |py| {
914 let slice = pyo3::types::PySlice::new(py, 8000, 9000, 1);
915 let r: PyPortRange = slice.extract().unwrap();
916 assert_eq!(r.0.start, 8000);
917 assert_eq!(r.0.end, 9000);
918 });
919 }
920
921 #[test]
922 fn test_pyportrange_reject_tuples_and_strings() {
923 Python::initialize();
924 monarch_with_gil_blocking(GilSite::Test, |py| {
925 let tuple = PyTuple::new(py, [8000u16, 9000u16]).unwrap();
927 let result: PyResult<PyPortRange> = tuple.extract();
928 assert!(result.is_err());
929
930 let s = PyString::new(py, "8000..9000");
932 let result: PyResult<PyPortRange> = s.extract();
933 assert!(result.is_err());
934 });
935 }
936
937 #[test]
938 fn test_pyportrange_reject_backwards_range() {
939 Python::initialize();
940 monarch_with_gil_blocking(GilSite::Test, |py| {
941 let slice = pyo3::types::PySlice::new(py, 9000, 8000, 1);
943 let result: PyResult<PyPortRange> = slice.extract();
944 assert!(result.is_err());
945 let err_msg = format!("{}", result.unwrap_err());
946 assert!(err_msg.contains("start cannot be greater than stop"));
947 });
948 }
949
950 #[test]
951 fn test_pyportrange_reject_invalid_step() {
952 Python::initialize();
953 monarch_with_gil_blocking(GilSite::Test, |py| {
954 let slice = pyo3::types::PySlice::new(py, 8000, 9000, 2);
956 let result: PyResult<PyPortRange> = slice.extract();
957 assert!(result.is_err());
958 let err_msg = format!("{}", result.unwrap_err());
959 assert!(err_msg.contains("port ranges require step=None or step=1"));
960 });
961 }
962
963 #[test]
964 fn test_pyportrange_reject_none_start() {
965 Python::initialize();
966 monarch_with_gil_blocking(GilSite::Test, |py| {
967 let slice = py.eval(c"slice(None, 9000)", None, None).unwrap();
970 let result: PyResult<PyPortRange> = slice.extract();
971 assert!(result.is_err());
972 let err_msg = format!("{}", result.unwrap_err());
973 assert!(err_msg.contains("slice.start must be set"));
974 });
975 }
976
977 #[test]
978 fn test_pyportrange_reject_none_stop() {
979 Python::initialize();
980 monarch_with_gil_blocking(GilSite::Test, |py| {
981 let slice = py.eval(c"slice(8000, None)", None, None).unwrap();
984 let result: PyResult<PyPortRange> = slice.extract();
985 assert!(result.is_err());
986 let err_msg = format!("{}", result.unwrap_err());
987 assert!(err_msg.contains("slice.stop must be set"));
988 });
989 }
990
991 #[test]
992 fn test_pyportrange_allow_empty_range() {
993 Python::initialize();
994 monarch_with_gil_blocking(GilSite::Test, |py| {
995 let slice = pyo3::types::PySlice::new(py, 8000, 8000, 1);
997 let r: PyPortRange = slice.extract().unwrap();
998 assert_eq!(r.0.start, 8000);
999 assert_eq!(r.0.end, 8000);
1000 assert!(r.0.is_empty());
1001 });
1002 }
1003
1004 #[test]
1005 fn test_pyportrange_roundtrip() {
1006 Python::initialize();
1007 monarch_with_gil_blocking(GilSite::Test, |py| {
1008 let original = 8000..9000;
1009 let py_range = PyPortRange(original.clone());
1010 let py_obj = py_range.into_pyobject(py).unwrap();
1011
1012 assert!(py_obj.downcast::<pyo3::types::PySlice>().is_ok());
1014
1015 let back: PyPortRange = py_obj.extract().unwrap();
1017 assert_eq!(back.0, original);
1018 });
1019 }
1020}