Skip to content

Commit a8a9e19

Browse files
: config: straighten out naming (#1995)
Summary: this diff follows up on mariusae's [comment in D87795973](https://www.internalfb.com/diff/D87795973?dst_version_fbid=861202266855169&transaction_fbid=1244851620803763) and completes the cleanup he hinted at. the main change is to make the Python-owned "Runtime" layer an explicit, coherent concept throughout the module. the Rust helpers are renamed (`*_py`, `configure_kwarg`, etc.) so their roles are unambiguous, and the documentation now reflects the actual data flow: `configure(**kwargs)` writes only into `Source::Runtime`, while `get_global_config` and `get_runtime_config` clearly separate merged vs. layer-local views. on the Python side, the API surface is aligned with these semantics. the `configured(...)` context manager (the 'test_config.py' version) is rewritten to snapshot and restore only the Runtime layer instead of resetting global config, which makes overrides composable while continuing to prevent state leakage across tests. the .pyi file is updated to document the layered model and the historical naming of `configure(...)`. this diff further formalizes the contract implied by the earlier code: Python exclusively owns the Runtime layer, and all read/write paths now reflect that consistently in naming, behavior, and documentation. Reviewed By: dulinriley Differential Revision: D87866946
1 parent 047820d commit a8a9e19

File tree

4 files changed

+227
-100
lines changed

4 files changed

+227
-100
lines changed

monarch_hyperactor/src/config.rs

Lines changed: 144 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
//! Configuration for Monarch Hyperactor.
9+
//! Configuration bridge for Monarch Hyperactor.
1010
//!
11-
//! This module provides monarch-specific configuration attributes that extend
12-
//! the base hyperactor configuration system.
11+
//! This module defines Monarch-specific configuration keys and their
12+
//! Python bindings on top of the core `hyperactor::config::global`
13+
//! system. It wires those keys into the layered config and exposes
14+
//! Python-facing helpers such as `configure(...)`,
15+
//! `get_global_config()`, `get_runtime_config()`, and
16+
//! `clear_runtime_config()`, which together implement the "Runtime"
17+
//! configuration layer used by the Monarch Python API.
1318
1419
use std::collections::HashMap;
1520
use std::fmt::Debug;
@@ -79,10 +84,14 @@ static TYPEHASH_TO_INFO: std::sync::LazyLock<HashMap<u64, &'static PythonConfigT
7984
.collect()
8085
});
8186

82-
/// Given a key, get the associated `T`-typed value from the global config, then
83-
/// convert it to a `P`-typed object that can be converted to PyObject, and
84-
/// return that PyObject.
85-
fn get_global_config<'py, P, T>(
87+
/// Fetch a config value from the layered global config and convert it
88+
/// to Python.
89+
///
90+
/// Looks up `key` in the full configuration
91+
/// (Defaults/File/Env/Runtime/ TestOverride), clones the `T`-typed
92+
/// value if present, converts it to `P`, then into a `PyObject`. If
93+
/// the key is unset in all layers, returns `Ok(None)`.
94+
fn get_global_config_py<'py, P, T>(
8695
py: Python<'py>,
8796
key: &'static dyn ErasedKey,
8897
) -> PyResult<Option<PyObject>>
@@ -91,8 +100,15 @@ where
91100
P: IntoPyObjectExt<'py>,
92101
PyErr: From<<T as TryInto<P>>::Error>,
93102
{
94-
// Well, it can't fail unless there's a bug in the code in this file.
95-
let key = key.downcast_ref::<T>().expect("cannot fail");
103+
// The error case should never happen. If somehow it ever does
104+
// we'll represent "our typing assumptions are wrong" by returning
105+
// a PyTypeError rather than a panic.
106+
let key = key.downcast_ref::<T>().ok_or_else(|| {
107+
PyTypeError::new_err(format!(
108+
"internal config type mismatch for key `{}`",
109+
key.name(),
110+
))
111+
})?;
96112
let val: Option<P> = hyperactor::config::global::try_get_cloned(key.clone())
97113
.map(|v| v.try_into())
98114
.transpose()?;
@@ -102,11 +118,12 @@ where
102118
/// Fetch a config value from the **Runtime** layer only and convert
103119
/// it to Python.
104120
///
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>(
121+
/// This mirrors [`get_global_config_py`] but restricts the lookup to
122+
/// the `Source::Runtime` layer (ignoring
123+
/// TestOverride/Env/File/defaults). If the key has a runtime
124+
/// override, it is cloned as `T`, converted to `P`, then to a
125+
/// `PyObject`; otherwise `Ok(None)` is returned.
126+
fn get_runtime_config_py<'py, P, T>(
110127
py: Python<'py>,
111128
key: &'static dyn ErasedKey,
112129
) -> PyResult<Option<PyObject>>
@@ -125,8 +142,16 @@ where
125142
val.map(|v| v.into_py_any(py)).transpose()
126143
}
127144

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<()> {
145+
/// Store a Python-provided config value into the **Runtime** layer.
146+
///
147+
/// This is the write-path for the "Python configuration layer": it
148+
/// takes a typed key/value and merges it into `Source::Runtime` via
149+
/// `create_or_merge`. No other layers
150+
/// (Env/File/TestOverride/Defaults) are affected.
151+
fn set_runtime_config_py<T: AttrValue + Debug>(
152+
key: &'static dyn ErasedKey,
153+
value: T,
154+
) -> PyResult<()> {
130155
// Again, can't fail unless there's a bug in the code in this file.
131156
let key = key.downcast_ref().expect("cannot fail");
132157
let mut attrs = Attrs::new();
@@ -135,8 +160,22 @@ fn set_runtime_config<T: AttrValue + Debug>(key: &'static dyn ErasedKey, value:
135160
Ok(())
136161
}
137162

138-
fn set_runtime_config_from_py_obj(py: Python<'_>, name: &str, val: PyObject) -> PyResult<()> {
139-
// Get the `ErasedKey` from the kwarg `name` passed to `monarch.configure(...)`.
163+
/// Bridge a single Python kwarg into a typed Runtime config update.
164+
///
165+
/// This is the write-path behind `configure(**kwargs)`:
166+
/// - `configure(...)` calls this for each `(name, value)` pair,
167+
/// - we resolve `name` to an erased config key via `KEY_BY_NAME`,
168+
/// - we use the key's `typehash` to find the registered
169+
/// `PythonConfigTypeInfo`,
170+
/// - and finally call its `set_runtime_config` closure, which
171+
/// downcasts the value and forwards to `set_runtime_config_py` to
172+
/// write into the `Source::Runtime` layer.
173+
///
174+
/// Unknown keys or keys without a Python conversion registered result
175+
/// in a `ValueError` / `TypeError` back to Python.
176+
fn configure_kwarg(py: Python<'_>, name: &str, val: PyObject) -> PyResult<()> {
177+
// Get the `ErasedKey` from the kwarg `name` passed to
178+
// `monarch.configure(...)`.
140179
let key = match KEY_BY_NAME.get(name) {
141180
None => {
142181
return Err(PyValueError::new_err(format!(
@@ -147,8 +186,9 @@ fn set_runtime_config_from_py_obj(py: Python<'_>, name: &str, val: PyObject) ->
147186
Some(key) => *key,
148187
};
149188

150-
// Using the typehash from the erased key, get/call the function that can downcast
151-
// the key and set the value on the global config.
189+
// Using the typehash from the erased key, get/call the function
190+
// that can downcast the key and set the value on the global
191+
// config.
152192
match TYPEHASH_TO_INFO.get(&key.typehash()) {
153193
None => Err(PyTypeError::new_err(format!(
154194
"configuration key `{}` has type `{}`, but configuring with values of this type from Python is not supported.",
@@ -159,22 +199,47 @@ fn set_runtime_config_from_py_obj(py: Python<'_>, name: &str, val: PyObject) ->
159199
}
160200
}
161201

162-
/// Struct to associate a typehash with functions for getting/setting
163-
/// values in the global config with keys of type `Key<T>`, where
164-
/// `T::typehash() == PythonConfigTypeInfo::typehash()`.
202+
/// Per-type adapter for the Python config bridge.
203+
///
204+
/// Each `PythonConfigTypeInfo` provides type-specific get/set logic
205+
/// for a particular `Key<T>` via the type-erased `ErasedKey`
206+
/// interface.
207+
///
208+
/// Since we only have `&'static dyn ErasedKey` at runtime (we don't
209+
/// know `T`), we use **type erasure with recovery via function
210+
/// pointers**: the `declare_py_config_type!` macro bakes the concrete
211+
/// type `T` into each function pointer at compile time, allowing
212+
/// runtime dispatch to recover the type.
213+
///
214+
/// Fields:
215+
/// - `typehash`: Identifies the underlying `T` for runtime lookup
216+
/// - `set_global_config`: Knows how to extract `PyObject` as `T` and
217+
/// write to Runtime layer
218+
/// - `get_global_config`: Reads `T` from merged config (all layers)
219+
/// and converts to `PyObject`
220+
/// - `get_runtime_config`: Reads `T` from Runtime layer only and
221+
/// converts to `PyObject`
222+
///
223+
/// Instances are registered via `inventory` and collected into
224+
/// `TYPEHASH_TO_INFO`, enabling dynamic dispatch by typehash in
225+
/// `configure()` and `get_*_config()`.
165226
struct PythonConfigTypeInfo {
227+
/// Identifies the underlying `T` (matches `T::typehash()`).
166228
typehash: fn() -> u64,
167-
229+
/// Read this key from the merged layered config into a PyObject.
168230
get_global_config:
169231
fn(py: Python<'_>, key: &'static dyn ErasedKey) -> PyResult<Option<PyObject>>,
170-
232+
/// Write a Python value into the Runtime layer for this key.
171233
set_runtime_config:
172234
fn(py: Python<'_>, key: &'static dyn ErasedKey, val: PyObject) -> PyResult<()>,
173-
235+
/// Read this key from the Runtime layer into a PyObject.
174236
get_runtime_config:
175237
fn(py: Python<'_>, key: &'static dyn ErasedKey) -> PyResult<Option<PyObject>>,
176238
}
177239

240+
// Collect all `PythonConfigTypeInfo` instances registered by
241+
// `declare_py_config_type!`. These are later gathered into
242+
// `TYPEHASH_TO_INFO` via `inventory::iter()`.
178243
inventory::collect!(PythonConfigTypeInfo);
179244

180245
/// Macro to declare that keys of this type can be configured
@@ -197,13 +262,13 @@ macro_rules! declare_py_config_type {
197262
"invalid value `{}` for configuration key `{}` ({})",
198263
val, key.name(), err
199264
)))?;
200-
set_runtime_config(key, val)
265+
set_runtime_config_py(key, val)
201266
},
202267
get_global_config: |py, key| {
203-
get_global_config::<$ty, $ty>(py, key)
268+
get_global_config_py::<$ty, $ty>(py, key)
204269
},
205270
get_runtime_config: |py, key| {
206-
get_runtime_config::<$ty, $ty>(py, key)
271+
get_runtime_config_py::<$ty, $ty>(py, key)
207272
}
208273
}
209274
}
@@ -220,13 +285,13 @@ macro_rules! declare_py_config_type {
220285
"invalid value `{}` for configuration key `{}` ({})",
221286
val, key.name(), err
222287
)))?.into();
223-
set_runtime_config(key, val)
288+
set_runtime_config_py(key, val)
224289
},
225290
get_global_config: |py, key| {
226-
get_global_config::<$py_ty, $ty>(py, key)
291+
get_global_config_py::<$py_ty, $ty>(py, key)
227292
},
228293
get_runtime_config: |py, key| {
229-
get_runtime_config::<$py_ty, $ty>(py, key)
294+
get_runtime_config_py::<$py_ty, $ty>(py, key)
230295
}
231296
}
232297
}
@@ -239,29 +304,48 @@ declare_py_config_type!(
239304
i8, i16, i32, i64, u8, u16, u32, u64, usize, f32, f64, bool, String
240305
);
241306

242-
/// Iterate over each key-value pair. Attempt to retrieve the `Key<T>`
243-
/// associated with the key and convert the value to `T`, then set
244-
/// them on the global config. The association between kwarg and
245-
/// `Key<T>` is specified using the `CONFIG` meta-attribute.
307+
/// Python entrypoint for `monarch_hyperactor.config.configure(...)`.
308+
///
309+
/// This takes the keyword arguments passed from Python, resolves each
310+
/// kwarg name to a typed config key via `KEY_BY_NAME` (populated from
311+
/// `@meta(CONFIG = ConfigAttr { py_name: Some(...), .. })`), and then
312+
/// uses `configure_kwarg` to downcast the value and write it into the
313+
/// **Runtime** configuration layer.
314+
///
315+
/// In other words, this is the write-path from `configure(**kwargs)`
316+
/// into `Source::Runtime`; other layers
317+
/// (Env/File/TestOverride/Defaults) are untouched.
318+
///
319+
/// The name `configure(...)` is historical – conceptually this is
320+
/// `set_runtime_config(...)` for the Python-owned Runtime layer, but
321+
/// we keep the shorter name for API stability.
246322
#[pyfunction]
247323
#[pyo3(signature = (**kwargs))]
248324
fn configure(py: Python<'_>, kwargs: Option<HashMap<String, PyObject>>) -> PyResult<()> {
249325
kwargs
250326
.map(|kwargs| {
251327
kwargs
252328
.into_iter()
253-
.try_for_each(|(key, val)| set_runtime_config_from_py_obj(py, &key, val))
329+
.try_for_each(|(key, val)| configure_kwarg(py, &key, val))
254330
})
255331
.transpose()?;
256332
Ok(())
257333
}
258334

259-
/// For all attribute keys whose `@meta(CONFIG = ConfigAttr { py_name:
260-
/// Some(...), .. })` specifies a kwarg name, return the current
261-
/// associated value in the global config. Keys with no value in the
262-
/// global config are omitted from the result.
335+
/// Return a snapshot of the current Hyperactor configuration for
336+
/// Python-exposed keys.
337+
///
338+
/// Iterates over all attribute keys whose `@meta(CONFIG = ConfigAttr
339+
/// { py_name: Some(...), .. })` declares a Python kwarg name, looks
340+
/// up each key in the **layered** global config
341+
/// (Defaults/File/Env/Runtime/TestOverride), and, if set, converts
342+
/// the value to a `PyObject`.
343+
///
344+
/// The result is a plain `HashMap` from kwarg name to value for all
345+
/// such keys that currently have a value in the global config; keys
346+
/// with no value in any layer are omitted.
263347
#[pyfunction]
264-
fn get_configuration(py: Python<'_>) -> PyResult<HashMap<String, PyObject>> {
348+
fn get_global_config(py: Python<'_>) -> PyResult<HashMap<String, PyObject>> {
265349
KEY_BY_NAME
266350
.iter()
267351
.filter_map(|(name, key)| match TYPEHASH_TO_INFO.get(&key.typehash()) {
@@ -282,25 +366,25 @@ fn get_configuration(py: Python<'_>) -> PyResult<HashMap<String, PyObject>> {
282366
/// `@meta(CONFIG = ConfigAttr { py_name: Some(...), .. })`) that are
283367
/// currently set in the Runtime layer.
284368
///
285-
/// This is used by Python's `configured()` context manager to
369+
/// This can be used to implement a `configured()` context manager to
286370
/// snapshot and restore the Runtime layer for composable, nested
287371
/// configuration overrides:
288372
///
289373
/// ```python
290-
/// prev = get_runtime_configuration()
374+
/// prev = get_runtime_config()
291375
/// try:
292376
/// configure(**overrides)
293-
/// yield get_configuration()
377+
/// yield get_global_config()
294378
/// finally:
295-
/// clear_runtime_configuration()
379+
/// clear_runtime_config()
296380
/// configure(**prev)
297381
/// ```
298382
///
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.
383+
/// Unlike `get_global_config()`, which returns the merged view across
384+
/// all layers (File, Env, Runtime, TestOverride, defaults), this
385+
/// returns only what's explicitly set in the Runtime layer.
302386
#[pyfunction]
303-
fn get_runtime_configuration(py: Python<'_>) -> PyResult<HashMap<String, PyObject>> {
387+
fn get_runtime_config(py: Python<'_>) -> PyResult<HashMap<String, PyObject>> {
304388
KEY_BY_NAME
305389
.iter()
306390
.filter_map(|(name, key)| match TYPEHASH_TO_INFO.get(&key.typehash()) {
@@ -325,7 +409,7 @@ fn get_runtime_configuration(py: Python<'_>) -> PyResult<HashMap<String, PyObjec
325409
/// to restore configuration state after applying temporary overrides.
326410
/// Other layers (Env, File, TestOverride, defaults) are unaffected.
327411
#[pyfunction]
328-
fn clear_runtime_configuration(_py: Python<'_>) -> PyResult<()> {
412+
fn clear_runtime_config(_py: Python<'_>) -> PyResult<()> {
329413
hyperactor::config::global::clear(Source::Runtime);
330414
Ok(())
331415
}
@@ -353,26 +437,26 @@ pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
353437
)?;
354438
module.add_function(configure)?;
355439

356-
let get_configuration = wrap_pyfunction!(get_configuration, module)?;
357-
get_configuration.setattr(
440+
let get_global_config = wrap_pyfunction!(get_global_config, module)?;
441+
get_global_config.setattr(
358442
"__module__",
359443
"monarch._rust_bindings.monarch_hyperactor.config",
360444
)?;
361-
module.add_function(get_configuration)?;
445+
module.add_function(get_global_config)?;
362446

363-
let get_runtime_configuration = wrap_pyfunction!(get_runtime_configuration, module)?;
364-
get_runtime_configuration.setattr(
447+
let get_runtime_config = wrap_pyfunction!(get_runtime_config, module)?;
448+
get_runtime_config.setattr(
365449
"__module__",
366450
"monarch._rust_bindings.monarch_hyperactor.config",
367451
)?;
368-
module.add_function(get_runtime_configuration)?;
452+
module.add_function(get_runtime_config)?;
369453

370-
let clear_runtime_configuration = wrap_pyfunction!(clear_runtime_configuration, module)?;
371-
clear_runtime_configuration.setattr(
454+
let clear_runtime_config = wrap_pyfunction!(clear_runtime_config, module)?;
455+
clear_runtime_config.setattr(
372456
"__module__",
373457
"monarch._rust_bindings.monarch_hyperactor.config",
374458
)?;
375-
module.add_function(clear_runtime_configuration)?;
459+
module.add_function(clear_runtime_config)?;
376460

377461
Ok(())
378462
}

0 commit comments

Comments
 (0)