1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
//! Python context definition.
use http::Extensions;
use pyo3::{PyObject, PyResult, Python, ToPyObject};
mod lambda;
pub mod layer;
#[cfg(test)]
mod testing;
/// PyContext is a wrapper for context object provided by the user.
/// It injects some values (currently only [super::lambda::PyLambdaContext]) that is type-hinted by the user.
///
/// PyContext is initialised during the startup, it inspects the provided context object for fields
/// that are type-hinted to inject some values provided by the framework (see [PyContext::new()]).
///
/// After finding fields that needs to be injected, [layer::AddPyContextLayer], a [tower::Layer],
/// populates request-scoped values from incoming request.
///
/// And finally PyContext implements [ToPyObject] (so it can by passed to Python handlers)
/// that provides [PyObject] provided by the user with the additional values injected by the framework.
#[derive(Clone)]
pub struct PyContext {
inner: PyObject,
// TODO(Refactor): We should ideally keep record of injectable fields in a hashmap like:
// `injectable_fields: HashMap<Field, Box<dyn Injectable>>` where `Injectable` provides a method to extract a `PyObject` from a `Request`,
// but I couldn't find a way to extract a trait object from a Python object.
// We could introduce a registry to keep track of every injectable type but I'm not sure that is the best way to do it,
// so until we found a good way to achive that, I didn't want to introduce any abstraction here and
// keep it simple because we only have one field that is injectable.
lambda_ctx: lambda::PyContextLambda,
}
impl PyContext {
pub fn new(inner: PyObject) -> PyResult<Self> {
Ok(Self {
lambda_ctx: lambda::PyContextLambda::new(inner.clone())?,
inner,
})
}
pub fn populate_from_extensions(&self, _ext: &Extensions) {
self.lambda_ctx
.populate_from_extensions(self.inner.clone(), _ext);
}
}
impl ToPyObject for PyContext {
fn to_object(&self, _py: Python<'_>) -> PyObject {
self.inner.clone()
}
}
#[cfg(test)]
mod tests {
use http::Extensions;
use pyo3::{prelude::*, py_run};
use super::testing::get_context;
#[test]
fn py_context() -> PyResult<()> {
pyo3::prepare_freethreaded_python();
let ctx = get_context(
r#"
class Context:
foo: int = 0
bar: str = 'qux'
ctx = Context()
ctx.foo = 42
"#,
);
Python::with_gil(|py| {
py_run!(
py,
ctx,
r#"
assert ctx.foo == 42
assert ctx.bar == 'qux'
# Make some modifications
ctx.foo += 1
ctx.bar = 'baz'
"#
);
});
ctx.populate_from_extensions(&Extensions::new());
Python::with_gil(|py| {
py_run!(
py,
ctx,
r#"
# Make sure we are preserving any modifications
assert ctx.foo == 43
assert ctx.bar == 'baz'
"#
);
});
Ok(())
}
#[test]
fn works_with_none() -> PyResult<()> {
// Users can set context to `None` by explicity or implicitly by not providing a custom context class,
// it shouldn't be fail in that case.
pyo3::prepare_freethreaded_python();
let ctx = get_context("ctx = None");
Python::with_gil(|py| {
py_run!(py, ctx, "assert ctx is None");
});
Ok(())
}
}