Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C++ & Python API: add helpers for constructing an entity path #4595

Merged
merged 10 commits into from
Dec 20, 2023
11 changes: 7 additions & 4 deletions crates/re_log_types/src/path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub use parse_path::PathParseError;
/// Build a `Vec<EntityPathPart>`:
/// ```
/// # use re_log_types::*;
/// let parts: Vec<EntityPathPart> = entity_path_vec!("foo", "bar");
/// let parts: Vec<EntityPathPart> = entity_path_vec!("foo", 42, "my image!");
/// ```
#[macro_export]
macro_rules! entity_path_vec {
Expand All @@ -34,16 +34,19 @@ macro_rules! entity_path_vec {
::std::vec::Vec::<$crate::EntityPathPart>::new()
};
($($part: expr),* $(,)?) => {
vec![ $($crate::EntityPathPart::from($part),)+ ]
vec![ $($crate::EntityPathPart::from(
#[allow(clippy::str_to_string, clippy::string_to_string)]
$part.to_string()
),)+ ]
};
}

/// Build an [`EntityPath`] from parts that are _not_ escaped:
///
/// ```
/// # use re_log_types::*;
/// let path: EntityPath = entity_path!("world", "my image!");
/// assert_eq!(path, EntityPath::parse_strict(r"world/my\ image\!").unwrap());
/// let path: EntityPath = entity_path!("world", 42, "my image!");
/// assert_eq!(path, EntityPath::parse_strict(r"world/42/my\ image\!").unwrap());
/// ```
#[macro_export]
macro_rules! entity_path {
Expand Down
19 changes: 15 additions & 4 deletions crates/re_sdk/src/recording_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -719,16 +719,17 @@ impl RecordingStream {
/// The data will be timestamped automatically based on the [`RecordingStream`]'s internal clock.
/// See [`RecordingStream::set_time_sequence`] etc for more information.
///
/// The entity path can either be a string
/// (with special characters escaped, split on unescaped slashes)
/// or an [`EntityPath`] constructed with [`crate::entity_path`].
/// See <https://www.rerun.io/docs/concepts/entity-path> for more on entity paths.
///
/// See also: [`Self::log_timeless`] for logging timeless data.
///
/// Internally, the stream will automatically micro-batch multiple log calls to optimize
/// transport.
/// See [SDK Micro Batching] for more information.
///
/// The entity path can either be a string
/// (with special characters escaped, split on unescaped slashes)
/// or an [`EntityPath`] constructed with [`crate::entity_path`].
///
/// # Example:
/// ```ignore
/// # use rerun;
Expand Down Expand Up @@ -791,6 +792,11 @@ impl RecordingStream {
/// internal clock.
/// See `RecordingStream::set_time_*` family of methods for more information.
///
/// The entity path can either be a string
/// (with special characters escaped, split on unescaped slashes)
/// or an [`EntityPath`] constructed with [`crate::entity_path`].
/// See <https://www.rerun.io/docs/concepts/entity-path> for more on entity paths.
///
/// Internally, the stream will automatically micro-batch multiple log calls to optimize
/// transport.
/// See [SDK Micro Batching] for more information.
Expand Down Expand Up @@ -830,6 +836,11 @@ impl RecordingStream {
/// All of the batches should have the same number of instances, or length 1 if the component is
/// a splat, or 0 if the component is being cleared.
///
/// The entity path can either be a string
/// (with special characters escaped, split on unescaped slashes)
/// or an [`EntityPath`] constructed with [`crate::entity_path`].
/// See <https://www.rerun.io/docs/concepts/entity-path> for more on entity paths.
///
/// Internally, the stream will automatically micro-batch multiple log calls to optimize
/// transport.
/// See [SDK Micro Batching] for more information.
Expand Down
4 changes: 4 additions & 0 deletions docs/code-examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ path = "depth_image_simple.rs"
name = "disconnected_space"
path = "disconnected_space.rs"

[[bin]]
name = "entity_path"
path = "entity_path.rs"

[[bin]]
name = "image_simple"
path = "image_simple.rs"
Expand Down
18 changes: 18 additions & 0 deletions docs/code-examples/entity_path.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Log a `TextDocument`

#include <rerun.hpp>

int main() {
const auto rec = rerun::RecordingStream("rerun_example_text_document");
rec.spawn().exit_on_failure();

rec.log(
R"(world/42/escaped\ string\!)",
rerun::TextDocument("This entity path was escaped manually")
);
// TODO(emilk): implement entity path escaping in C++
// rec.log(
// {"world", 42, "unescaped string!"},
// rerun::TextDocument("This entity path was provided as a list of unescaped strings")
// );
}
8 changes: 8 additions & 0 deletions docs/code-examples/entity_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import rerun as rr

rr.init("rerun_example_entity_path", spawn=True)

rr.log(r"world/42/escaped\ string\!", rr.TextDocument("This entity path was escaped manually"))
rr.log(
["world", 42, "unescaped string!"], rr.TextDocument("This entity path was provided as a list of unescaped strings")
)
14 changes: 14 additions & 0 deletions docs/code-examples/entity_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rec = rerun::RecordingStreamBuilder::new("rerun_example_text_document").spawn()?;

rec.log(
r"world/42/escaped\ string\!",
&rerun::TextDocument::new("This entity path was escaped manually"),
)?;
rec.log(
rerun::entity_path!["world", 42, "unescaped string!"],
&rerun::TextDocument::new("This entity path was provided as a list of unescaped strings"),
)?;

Ok(())
}
1 change: 1 addition & 0 deletions docs/code-examples/roundtrips.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
opt_out_compare = {
"arrow3d_simple": ["cpp", "py", "rust"], # TODO(#3206): examples use different RNGs
"asset3d_out_of_tree": ["cpp", "py", "rust"], # float issues since calculation is done slightly differently (also, Python uses doubles)
"entity_path": ["cpp"], # C++ doesn't have helpers for escaping an entity path yet
emilk marked this conversation as resolved.
Show resolved Hide resolved
"mesh3d_partial_updates": ["cpp", "py", "rust"], # float precision issues
"pinhole_simple": ["cpp", "py", "rust"], # TODO(#3206): examples use different RNGs
"point2d_random": ["cpp", "py", "rust"], # TODO(#3206): examples use different RNGs
Expand Down
54 changes: 49 additions & 5 deletions rerun_py/rerun_sdk/rerun/_log.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Iterable
from typing import Any, Iterable

import pyarrow as pa
import rerun_bindings as bindings
Expand Down Expand Up @@ -54,14 +54,14 @@ def _splat() -> cmp.InstanceKeyBatch:

@catch_and_log_exceptions()
def log(
entity_path: str,
entity_path: str | list[str],
entity: AsComponents | Iterable[ComponentBatchLike],
*extra: AsComponents | Iterable[ComponentBatchLike],
timeless: bool = False,
recording: RecordingStream | None = None,
strict: bool | None = None,
) -> None:
"""
r"""
Log data to Rerun.

This is the main entry point for logging data to rerun. It can be used to log anything
Expand Down Expand Up @@ -98,6 +98,15 @@ def log(
----------
entity_path:
Path to the entity in the space hierarchy.

The entity path can either be a string
(with special characters escaped, split on unescaped slashes)
or a list of unescaped strings.
This means that logging to `"world/my\ image\!"` is the same as logging
to ["world", "my image!"].

See <https://www.rerun.io/docs/concepts/entity-path> for more on entity paths.

entity:
Anything that implements the [`rerun.AsComponents`][] interface, usually an archetype.
*extra:
Expand Down Expand Up @@ -163,15 +172,15 @@ def log(

@catch_and_log_exceptions()
def log_components(
entity_path: str,
entity_path: str | list[str],
components: Iterable[ComponentBatchLike],
*,
num_instances: int | None = None,
timeless: bool = False,
recording: RecordingStream | None = None,
strict: bool | None = None,
) -> None:
"""
r"""
Log an entity from a collection of `ComponentBatchLike` objects.

All of the batches should have the same length as the value of
Expand All @@ -182,6 +191,15 @@ def log_components(
----------
entity_path:
Path to the entity in the space hierarchy.

The entity path can either be a string
(with special characters escaped, split on unescaped slashes)
or a list of unescaped strings.
This means that logging to `"world/my\ image\!"` is the same as logging
to ["world", "my image!"].

See <https://www.rerun.io/docs/concepts/entity-path> for more on entity paths.

components:
A collection of `ComponentBatchLike` objects that
num_instances:
Expand Down Expand Up @@ -211,6 +229,9 @@ def log_components(
if num_instances is None:
num_instances = max(len(arr) for arr in arrow_arrays)

if isinstance(entity_path, list):
entity_path = bindings.escape_entity_path([str(part) for part in entity_path])

added = set()

for name, array in zip(names, arrow_arrays):
Expand Down Expand Up @@ -256,3 +277,26 @@ def log_components(
timeless=timeless,
recording=recording,
)


def escape_entity_path(entity_path: list[Any]) -> str:
r"""
Construct an entity path, defined by a list of (unescaped) parts.

If any part if not a string, it will be converted to a string using `str()`.

For instance, `escape_entity_path(["world", 42, "my image!"])` will return `"world/42/my\ image\!"`.

See <https://www.rerun.io/docs/concepts/entity-path> for more on entity paths.

Parameters
----------
entity_path:
A list of strings to escape and join with slash.

Returns
-------
str:
The escaped entity path.
"""
return str(bindings.escape_entity_path([str(part) for part in entity_path]))
18 changes: 10 additions & 8 deletions rerun_py/src/python_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use pyo3::{
//use re_viewport::{SpaceViewBlueprint, VIEWPORT_PATH};
use re_viewport::VIEWPORT_PATH;

use re_log_types::{DataRow, StoreKind};
use re_log_types::{DataRow, EntityPathPart, StoreKind};
use rerun::{
log::RowId, sink::MemorySinkStorage, time::TimePoint, EntityPath, RecordingStream,
RecordingStreamBuilder, StoreId,
Expand Down Expand Up @@ -152,6 +152,7 @@ fn rerun_bindings(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(version, m)?)?;
m.add_function(wrap_pyfunction!(get_app_url, m)?)?;
m.add_function(wrap_pyfunction!(start_web_viewer_server, m)?)?;
m.add_function(wrap_pyfunction!(escape_entity_path, m)?)?;

// blueprint
m.add_function(wrap_pyfunction!(set_panels, m)?)?;
Expand Down Expand Up @@ -760,7 +761,7 @@ fn set_panel(entity_path: &str, is_expanded: bool, blueprint: Option<&PyRecordin
use re_viewer::blueprint::components::PanelView;

// TODO(jleibs): Validation this is a valid blueprint path?
let entity_path = parse_entity_path(entity_path);
let entity_path = EntityPath::parse_forgiving(entity_path);

let panel_state = PanelView(is_expanded);

Expand Down Expand Up @@ -867,7 +868,7 @@ fn log_arrow_msg(
return Ok(());
};

let entity_path = parse_entity_path(entity_path);
let entity_path = EntityPath::parse_forgiving(entity_path);

// It's important that we don't hold the session lock while building our arrow component.
// the API we call to back through pyarrow temporarily releases the GIL, which can cause
Expand Down Expand Up @@ -938,6 +939,12 @@ fn start_web_viewer_server(port: u16) -> PyResult<()> {
}
}

#[pyfunction]
fn escape_entity_path(parts: Vec<&str>) -> String {
let path = EntityPath::from(parts.into_iter().map(EntityPathPart::from).collect_vec());
path.to_string()
}

// --- Helpers ---

fn python_version(py: Python<'_>) -> re_log_types::PythonVersion {
Expand Down Expand Up @@ -1013,8 +1020,3 @@ authkey = multiprocessing.current_process().authkey
})
.map(|authkey: &PyBytes| authkey.as_bytes().to_vec())
}

fn parse_entity_path(entity_path: &str) -> EntityPath {
// We accept anything!
EntityPath::parse_forgiving(entity_path)
}