feat: backward-compatible Python API improvements (umbrella)#102
feat: backward-compatible Python API improvements (umbrella)#102HCookie wants to merge 7 commits into
Conversation
…serialisation - Fix coordinate intersection to return Empty instead of panicking on cross-type or empty operands - Add wrap_ints/wrap_strs helpers to avoid wrapping empty coordinates - Implement Mixed coordinates to_string formatting - Add type coercion between Strings and Integers in coordinate extension - Fix leading-zero detection to be consistent across multi-value coords - Make from_datacube sort remaining dimensions for deterministic output - Build dimension translation map in merge to handle different interning orders between qubes - Handle dimension-only-in-other case during node_merge - Fix drop_recurse to iteratively splice out consecutive matching dims - Add Clone for Node and Qube - Add axes(), dimensions(), all_dim_ids(), get_or_intern_dim() methods - Add to_tree_json/from_tree_json serialisation format - Add comprehensive unit tests for coordinates, datacube, and merge
…e operations - Rename class from PyQube to Qube (Python-side name) - Add from_json/to_json and from_tree_json/to_tree_json bindings - Make from_arena_json accept str or dict (via py_to_json_value) - Preserve Python dict insertion order as default dimension ordering - Accept lists, ints, floats in datacube dicts (not just strings) - Return native types from to_datacubes and all_unique_dim_coords - Make drop() and squeeze() return new Qube instead of mutating - Add __or__ operator for non-mutating merge - Add axes(), dimensions(), is_empty(), empty() methods - Add __copy__/__deepcopy__ support via Clone - Add py_to_json_value recursive converter for Python->serde_json
- Rename all PyQube references to Qube - Update assertions for native type returns (int instead of str) - Add tests for to_json/from_json and to_tree_json/from_tree_json - Add tests for copy/deepcopy, __or__ operator - Add tests for immutable drop() and squeeze() - Add tests for axes(), dimensions(), is_empty() - Add test_qubed_initialisation.py for Qube.empty()
…oject metadata - Remove top-level [project] section from pyproject.toml (now in py_qubed) - Rewrite py_qubed.md for Qube class name, new methods, and native types - Document from_json, to_json, from_tree_json, to_tree_json - Document axes(), dimensions(), is_empty(), __or__, copy support - Update drop/squeeze docs to reflect immutable return semantics
- Keep [project] section in pyproject.toml (main removed version field) - Keep from_py_object and Clone derive on PyQube struct - Keep immutable drop()/squeeze() semantics (return new Qube)
- Add Qube::prepend() on Rust side to wrap tree under new parent node - Add Python prepend() with dict+order signature mirroring append_datacube - Fix pydict_to_datacube to use Coordinates::from_string for slash-splitting - Add from_string_splits_on_slash test - Add prepend tests (wraps tree, order control, multi-value) - Fix __repr__ to return to_ascii() - Update docs to reflect __repr__ behaviour - Remove stale [project] section from root pyproject.toml
| pub fn from_tree_json(value: Value) -> Result<Qube, String> { | ||
| let mut qube = Qube::new(); | ||
| let root = qube.root(); | ||
| parse_tree_node(&mut qube, root, &value)?; | ||
| Ok(qube) | ||
| } | ||
| } |
There was a problem hiding this comment.
This is likely unneeded, I can adapt my qube dumps to another format if needed.
There was a problem hiding this comment.
I think we might remove this entirely. We can keep it maybe in intermediate versions to have a smoother transition though if it's useful.
We switched the way that the tree is created in Rust with an arena, and changed the standard associated json Qube format at the same time. I think it would be better in the long-term to work with this standard, which we will continue to maintain.
There was a problem hiding this comment.
I am using it to walk the qube atm, so a function to do that will be handy
mathleur
left a comment
There was a problem hiding this comment.
Just had a quick go through. Maybe we can separate some of these into different PRs since this is quite a large one and we're addressing different points?
| } | ||
| Coordinates::Integers(ints) => { | ||
| // Try to coerce all new strings to integers | ||
| if let Some(converted) = try_strings_to_integers(new_strings) { |
There was a problem hiding this comment.
Just curious, but why is the conversion to int needed here? I feel this should probably be a separate pre/post-processing function in this case, as it seems likely related to a specific use case? Think this might still be useful in other use cases, and we should implement something, but not sure this is the right place
| pub fn from_tree_json(value: Value) -> Result<Qube, String> { | ||
| let mut qube = Qube::new(); | ||
| let root = qube.root(); | ||
| parse_tree_node(&mut qube, root, &value)?; | ||
| Ok(qube) | ||
| } | ||
| } |
There was a problem hiding this comment.
I think we might remove this entirely. We can keep it maybe in intermediate versions to have a smoother transition though if it's useful.
We switched the way that the tree is created in Rust with an arena, and changed the standard associated json Qube format at the same time. I think it would be better in the long-term to work with this standard, which we will continue to maintain.
| // Root should still have "class" children | ||
| let root_node = qube.node(root).unwrap(); | ||
| assert!(root_node.children(qube.dimension("class").unwrap()).is_some()); | ||
|
|
There was a problem hiding this comment.
Note to self: did this not work before? Maybe test this functionality more on more complex trees
| @@ -1,8 +1,3 @@ | |||
| [project] | |||
| name = "qubed" | |||
There was a problem hiding this comment.
Is there a reason why we wouldn't want the pyproject project here?
There was a problem hiding this comment.
For uv it didn't work sync-ing in this dir, and was very confused
| #### `drop(dims: list[str]) -> Qube` | ||
|
|
||
| Remove one or more dimensions from the tree. Children of removed nodes are re-parented to the grandparent, preserving the rest of the structure. The result is automatically compressed. | ||
| Return a new Qube with one or more dimensions removed. Children of removed nodes are re-parented to the grandparent, preserving the rest of the structure. The result is automatically compressed. The original Qube is not modified. |
There was a problem hiding this comment.
A lot of these modified functions seem to be so that we do not modify the Qubes in place, is that needed in your application? We had initial design discussions, where we initially decided to modify the Qubes in place for these functions. It might be more consistent with other libraries if we modify in place though and if it doesn't affect the performance too much :)
There was a problem hiding this comment.
Sure, I can change my use cases
|
|
||
| pub fn to_json(&self) -> PyResult<String> { | ||
| let v = self.inner.to_json(); | ||
| serde_json::to_string(&v).map_err(|e| PyTypeError::new_err(e.to_string())) |
There was a problem hiding this comment.
That's my bad sorry, but the to_ and from_json were tests, the actual serde format we will be using is to and from_arena_json
Will remove the to and from_json in the Rust version too, so it's less confusing
| } | ||
| } | ||
|
|
||
| fn json_scalar_to_py<'py>( |
There was a problem hiding this comment.
Note to self: probably should put this in another file/somewhere else if we need them
|
If you want to see why these changes were made, I've linked the three PR's which update qubed above |
Warning
This is the full set of changes that were needed to restore functionality for Forecast-In-A-Box.
They were agentically built, and have no care for correctness to the vision of
qubed.Let this serve as a showcase as to what was needed, to begin discussion.
Important
This PR has been split into focused, independently-mergeable PRs below.
Each can be reviewed and merged on its own. This PR represents the sum of all changes.
Split PRs
Each PR is independently mergeable. #108 includes all Rust-level dependencies so it can land on its own if preferred as a single merge.
Full summary of changes
Core Rust library (
qubed/src/)Coordinates (
coordinates/mod.rs,coordinates/ops.rs) — #104Emptyinstead of panicking on cross-type or empty operandsMixedcoordinatesto_string()formatting (wastodo!())StringsandIntegerswhen extending coordinatesMerge (
merge.rs) — #105otherduringnode_mergeQube (
qube.rs) — #105CloneforNodeandQubeaxes(),dimensions(),all_dim_ids(),get_or_intern_dim(),prepend()methodsdrop_recurseto iteratively splice out consecutive matching dimensionsDatacube (
datacube.rs) — #107from_datacubesorts remaining dimensions alphabetically for deterministic tree structureSerialisation (
serde/json.rs) — #106to_tree_json/from_tree_json: recursive format with key, values, metadata, children fieldsPython bindings (
py_qubed/src/lib.rs) — #108Breaking changes:
PyQubetoQubeon the Python sidedrop()andsqueeze()return a new Qube instead of mutating in placeto_datacubes()andall_unique_dim_coords()return native Python typesfrom_arena_json()now acceptsstr | dictNew API surface:
Qube.empty(),Qube.is_empty()Qube.from_json()/to_json(),from_tree_json()/to_tree_json()axes(),dimensions()__or__operator,__copy__/__deepcopy__supportfrom_datacubepreserves Python dict insertion orderlist,int,floatvaluesTests & Docs
docs/src/python/py_qubed.mdrewritten