9999 val.map(|v| v.into_py_any(py)).transpose()
100100}
101101
102- fn set_global_config<T: AttrValue + Debug>(key: &'static dyn ErasedKey, value: T) -> PyResult<()> {
102+ /// Fetch a config value from the **Runtime** layer only and convert
103+ /// it to Python.
104+ ///
105+ /// This mirrors [`get_global_config`] but restricts the lookup to the
106+ /// `Source::Runtime` layer (ignoring TestOverride/Env/File/defaults).
107+ /// If the key has a runtime override, it is cloned as `T`, converted
108+ /// to `P`, then to a `PyObject`; otherwise `Ok(None)` is returned.
109+ fn get_runtime_config<'py, P, T>(
110+ py: Python<'py>,
111+ key: &'static dyn ErasedKey,
112+ ) -> PyResult<Option<PyObject>>
113+ where
114+ T: AttrValue + TryInto<P>,
115+ P: IntoPyObjectExt<'py>,
116+ PyErr: From<<T as TryInto<P>>::Error>,
117+ {
118+ let key = key.downcast_ref::<T>().expect("cannot fail");
119+ let runtime = hyperactor::config::global::runtime_attrs();
120+ let val: Option<P> = runtime
121+ .get(key.clone())
122+ .cloned()
123+ .map(|v| v.try_into())
124+ .transpose()?;
125+ val.map(|v| v.into_py_any(py)).transpose()
126+ }
127+
128+ /// Note that this function writes strictly into the `Runtime` layer.
129+ fn set_runtime_config<T: AttrValue + Debug>(key: &'static dyn ErasedKey, value: T) -> PyResult<()> {
103130 // Again, can't fail unless there's a bug in the code in this file.
104131 let key = key.downcast_ref().expect("cannot fail");
105132 let mut attrs = Attrs::new();
@@ -108,7 +135,7 @@ fn set_global_config<T: AttrValue + Debug>(key: &'static dyn ErasedKey, value: T
108135 Ok(())
109136}
110137
111- fn set_global_config_from_py_obj (py: Python<'_>, name: &str, val: PyObject) -> PyResult<()> {
138+ fn set_runtime_config_from_py_obj (py: Python<'_>, name: &str, val: PyObject) -> PyResult<()> {
112139 // Get the `ErasedKey` from the kwarg `name` passed to `monarch.configure(...)`.
113140 let key = match KEY_BY_NAME.get(name) {
114141 None => {
@@ -128,7 +155,7 @@ fn set_global_config_from_py_obj(py: Python<'_>, name: &str, val: PyObject) -> P
128155 name,
129156 key.typename()
130157 ))),
131- Some(info) => (info.set_global_config )(py, key, val),
158+ Some(info) => (info.set_runtime_config )(py, key, val),
132159 }
133160}
134161
@@ -137,10 +164,15 @@ fn set_global_config_from_py_obj(py: Python<'_>, name: &str, val: PyObject) -> P
137164/// `T::typehash() == PythonConfigTypeInfo::typehash()`.
138165struct PythonConfigTypeInfo {
139166 typehash: fn() -> u64,
140- set_global_config:
141- fn(py: Python<'_>, key: &'static dyn ErasedKey, val: PyObject) -> PyResult<()>,
167+
142168 get_global_config:
143169 fn(py: Python<'_>, key: &'static dyn ErasedKey) -> PyResult<Option<PyObject>>,
170+
171+ set_runtime_config:
172+ fn(py: Python<'_>, key: &'static dyn ErasedKey, val: PyObject) -> PyResult<()>,
173+
174+ get_runtime_config:
175+ fn(py: Python<'_>, key: &'static dyn ErasedKey) -> PyResult<Option<PyObject>>,
144176}
145177
146178inventory::collect!(PythonConfigTypeInfo);
@@ -160,15 +192,18 @@ macro_rules! declare_py_config_type {
160192 hyperactor::submit! {
161193 PythonConfigTypeInfo {
162194 typehash: $ty::typehash,
163- set_global_config : |py, key, val| {
195+ set_runtime_config : |py, key, val| {
164196 let val: $ty = val.extract::<$ty>(py).map_err(|err| PyTypeError::new_err(format!(
165197 "invalid value `{}` for configuration key `{}` ({})",
166198 val, key.name(), err
167199 )))?;
168- set_global_config (key, val)
200+ set_runtime_config (key, val)
169201 },
170202 get_global_config: |py, key| {
171203 get_global_config::<$ty, $ty>(py, key)
204+ },
205+ get_runtime_config: |py, key| {
206+ get_runtime_config::<$ty, $ty>(py, key)
172207 }
173208 }
174209 }
@@ -180,15 +215,18 @@ macro_rules! declare_py_config_type {
180215 hyperactor::submit! {
181216 PythonConfigTypeInfo {
182217 typehash: $ty::typehash,
183- set_global_config : |py, key, val| {
218+ set_runtime_config : |py, key, val| {
184219 let val: $ty = val.extract::<$py_ty>(py).map_err(|err| PyTypeError::new_err(format!(
185220 "invalid value `{}` for configuration key `{}` ({})",
186221 val, key.name(), err
187222 )))?.into();
188- set_global_config (key, val)
223+ set_runtime_config (key, val)
189224 },
190225 get_global_config: |py, key| {
191226 get_global_config::<$py_ty, $ty>(py, key)
227+ },
228+ get_runtime_config: |py, key| {
229+ get_runtime_config::<$py_ty, $ty>(py, key)
192230 }
193231 }
194232 }
@@ -212,7 +250,7 @@ fn configure(py: Python<'_>, kwargs: Option<HashMap<String, PyObject>>) -> PyRes
212250 .map(|kwargs| {
213251 kwargs
214252 .into_iter()
215- .try_for_each(|(key, val)| set_global_config_from_py_obj (py, &key, val))
253+ .try_for_each(|(key, val)| set_runtime_config_from_py_obj (py, &key, val))
216254 })
217255 .transpose()?;
218256 Ok(())
@@ -236,6 +274,62 @@ fn get_configuration(py: Python<'_>) -> PyResult<HashMap<String, PyObject>> {
236274 .collect()
237275}
238276
277+ /// Get only the Runtime layer configuration (Python-exposed keys).
278+ ///
279+ /// The Runtime layer is effectively the "Python configuration layer",
280+ /// populated exclusively via `configure(**kwargs)` from Python. This
281+ /// function returns only the Python-exposed keys (those with
282+ /// `@meta(CONFIG = ConfigAttr { py_name: Some(...), .. })`) that are
283+ /// currently set in the Runtime layer.
284+ ///
285+ /// This is used by Python's `configured()` context manager to
286+ /// snapshot and restore the Runtime layer for composable, nested
287+ /// configuration overrides:
288+ ///
289+ /// ```python
290+ /// prev = get_runtime_configuration()
291+ /// try:
292+ /// configure(**overrides)
293+ /// yield get_configuration().copy()
294+ /// finally:
295+ /// clear_runtime_configuration()
296+ /// configure(**prev)
297+ /// ```
298+ ///
299+ /// Unlike `get_configuration()`, which returns the merged view across
300+ /// all layers (File, Env, Runtime, TestOverride), this returns only
301+ /// what's explicitly set in the Runtime layer.
302+ #[pyfunction]
303+ fn get_runtime_configuration(py: Python<'_>) -> PyResult<HashMap<String, PyObject>> {
304+ KEY_BY_NAME
305+ .iter()
306+ .filter_map(|(name, key)| match TYPEHASH_TO_INFO.get(&key.typehash()) {
307+ None => None,
308+ Some(info) => match (info.get_runtime_config)(py, *key) {
309+ Err(err) => Some(Err(err)),
310+ Ok(val) => val.map(|val| Ok(((*name).into(), val))),
311+ },
312+ })
313+ .collect()
314+ }
315+
316+ /// Clear runtime configuration overrides.
317+ ///
318+ /// This removes all entries from the Runtime config layer for this
319+ /// process. The Runtime layer is exclusively populated via Python's
320+ /// `configure(**kwargs)`, so clearing it is SAFE — it will not
321+ /// destroy configuration from other sources (environment variables,
322+ /// config files, or built-in defaults).
323+ ///
324+ /// This is primarily used by Python's `configured()` context manager
325+ /// to restore configuration state after applying temporary overrides.
326+ /// Other layers (Env, File, TestOverride, defaults) are unaffected.
327+ #[pyfunction]
328+ fn clear_runtime_configuration(_py: Python<'_>) -> PyResult<()> {
329+ hyperactor::config::global::clear(Source::Runtime);
330+ Ok(())
331+ }
332+
239333/// Register Python bindings for the config module
240334pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
241335 let reload = wrap_pyfunction!(reload_config_from_env, module)?;
@@ -266,5 +360,19 @@ pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
266360 )?;
267361 module.add_function(get_configuration)?;
268362
363+ let get_runtime_configuration = wrap_pyfunction!(get_runtime_configuration, module)?;
364+ get_runtime_configuration.setattr(
365+ "__module__",
366+ "monarch._rust_bindings.monarch_hyperactor.config",
367+ )?;
368+ module.add_function(get_runtime_configuration)?;
369+
370+ let clear_runtime_configuration = wrap_pyfunction!(clear_runtime_configuration, module)?;
371+ clear_runtime_configuration.setattr(
372+ "__module__",
373+ "monarch._rust_bindings.monarch_hyperactor.config",
374+ )?;
375+ module.add_function(clear_runtime_configuration)?;
376+
269377 Ok(())
270378}
0 commit comments