Skip to content

Commit

Permalink
Allow orjson default to not be set during cargo nextest tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
obi1kenobi committed Jan 8, 2025
1 parent 7fe888e commit d70526e
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 7 deletions.
14 changes: 13 additions & 1 deletion rust/crates/langsmith-pyo3/src/py_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ impl RunCreateExtended {

impl FromPyObject<'_> for RunCreateExtended {
fn extract_bound(value: &Bound<'_, PyAny>) -> PyResult<Self> {
// Perform a runtime check that we've successfully located the `langsmith` Python code
// used to transform Python objects which aren't natively serializeable by `orjson`.
//
// This assertion ensures that we won't later fail to serialize e.g. Pydantic objects.
serialization::assert_orjson_default_is_present();

let run_create = value.extract::<RunCreate>()?.into_inner();

let attachments = {
Expand Down Expand Up @@ -58,6 +64,12 @@ impl RunUpdateExtended {

impl FromPyObject<'_> for RunUpdateExtended {
fn extract_bound(value: &Bound<'_, PyAny>) -> PyResult<Self> {
// Perform a runtime check that we've successfully located the `langsmith` Python code
// used to transform Python objects which aren't natively serializeable by `orjson`.
//
// This assertion ensures that we won't later fail to serialize e.g. Pydantic objects.
serialization::assert_orjson_default_is_present();

let run_update = value.extract::<RunUpdate>()?.into_inner();

// TODO: attachments are WIP at the moment, ignore them here for now.
Expand Down Expand Up @@ -273,7 +285,7 @@ fn extract_string_like(value: &Bound<'_, PyAny>) -> PyResult<String> {
// However, orjson supports serializing UUID objects, so the easiest way to get
// a Rust string from a Python UUID object is to serialize the UUID to a JSON string
// and then parse out the string.
let Ok(buffer) = self::serialization::dumps(value.as_ptr()) else {
let Ok(buffer) = serialization::dumps(value.as_ptr()) else {
// orjson failed to deserialize the object. The fact that orjson is involved
// is an internal implementation detail, so return the original error instead.
// It looks like this:
Expand Down
27 changes: 21 additions & 6 deletions rust/crates/langsmith-pyo3/src/serialization/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,45 @@ use pyo3::types::PyAnyMethods as _;
mod writer;

thread_local! {
static ORJSON_DEFAULT: NonNull<pyo3_ffi::PyObject> = {
static ORJSON_DEFAULT: Result<NonNull<pyo3_ffi::PyObject>, String> = {
pyo3::Python::with_gil(|py| {
let module = match py.import("langsmith._internal._serde") {
Ok(m) => m,
Err(e) => {
let _ = py.import("langsmith").expect("failed to import `langsmith` package; please make sure `langsmith-pyo3` is only used via the `langsmith` package");
panic!("Failed to import `langsmith._internal._serde` even though `langsmith` can be imported. Did internal `langsmith` package structure change? Underlying error: {e}");
let _ = py.import("langsmith").map_err(|_| "failed to import `langsmith` package; please make sure `langsmith-pyo3` is only used via the `langsmith` package".to_string())?;
return Err(format!("Failed to import `langsmith._internal._serde` even though `langsmith` can be imported. Did internal `langsmith` package structure change? Underlying error: {e}"));
}
};

let function = module.getattr("_serialize_json").expect("`_serialize_json` function not found").as_ptr();
NonNull::new(function).expect("function was null, which shouldn't ever happen")
let function = module.getattr("_serialize_json").map_err(|e| format!("`_serialize_json` function not found; underlying error: {e}"))?.as_ptr();
Ok(NonNull::new(function).expect("function was null, which shouldn't ever happen"))
})
}
}

/// Perform a runtime check that we've successfully located the `langsmith` Python code
/// used to transform Python objects which aren't natively serializeable by `orjson`.
///
/// This assertion ensures that we won't later fail to serialize e.g. Pydantic objects.
///
/// The cost of this call is trivial: just one easily branch-predictable comparison on
/// an already-initialized thread-local.
pub(crate) fn assert_orjson_default_is_present() {
ORJSON_DEFAULT.with(|res| {
if let Err(e) = res {
panic!("{e}");
}
})
}

pub(crate) fn dumps(ptr: *mut pyo3_ffi::PyObject) -> Result<Vec<u8>, String> {
let mut writer = writer::BufWriter::new();

ORJSON_DEFAULT.with(|default| {
let obj = orjson::PyObjectSerializer::new(
ptr,
orjson::SerializerState::new(Default::default()),
Some(*default),
default.as_ref().cloned().ok(),
);

let res = orjson::to_writer(&mut writer, &obj);
Expand Down

0 comments on commit d70526e

Please sign in to comment.