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

Refactor skeleton.to_json to serialize skeleton object without jsonpickle #1934

Closed
wants to merge 20 commits into from

Conversation

eberrigan
Copy link
Contributor

@eberrigan eberrigan commented Sep 3, 2024

Description

  • We are seeing issues with skeleton.json where jsonpickle is encoding the skeleton graph dictionary in a way that breaks backwards compatibility, specifically:
    • """The change in attrs version used by jsonpickle to write/read the skeleton.json is causing problems, namely, attrs >=22.2.0 returns the object state as a dictionary instead of a tuple. This results in incompatibilities when attempting to deserialize skeleton.json files (w/ an older attrs <22.2.0) that were serialized with attrs >=22.2.0. Based on the discussion in Fix backward compatibility with pickles before v22.2.0 python-attrs/attrs#1085 it sounds like attrs ==22.2.0 was unable to deserialize pickled data from attrs <22.2.0, and attrs ==23.1.0 (>=23.1.0?) at least fixes backwards compatibility. The long-term plan for attrs (discussed in Remove ability to __setstate__ from "legacy" tuple format soon python-attrs/attrs#1091) is to remove support for deserializing tuples after giving everyone a few versions of attrs to convert their data over to a dictionary serialization.""" from Error during Inference. ValueError: Found Multiple nodes named (name). #1918 .
    • sleap/sleap/skeleton.py

      Lines 986 to 1028 in 1370782

      def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str:
      """Convert the :class:`Skeleton` to a JSON representation.
      Args:
      node_to_idx: optional dict which maps :class:`Node`sto index
      in some list. This is used when saving
      :class:`Labels`where we want to serialize the
      :class:`Nodes` outside the :class:`Skeleton` object.
      If given, then we replace each :class:`Node` with
      specified index before converting :class:`Skeleton`.
      Otherwise, we convert :class:`Node` objects with the rest of
      the :class:`Skeleton`.
      Returns:
      A string containing the JSON representation of the skeleton.
      """
      jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4)
      if node_to_idx is not None:
      indexed_node_graph = nx.relabel_nodes(
      G=self._graph, mapping=node_to_idx
      ) # map nodes to int
      else:
      indexed_node_graph = self._graph
      # Encode to JSON
      graph = json_graph.node_link_data(indexed_node_graph)
      # SLEAP v1.3.0 added `description` and `preview_image` to `Skeleton`, but saving
      # these fields breaks data format compatibility. Currently, these are only
      # added in our custom template skeletons. To ensure backwards data format
      # compatibilty of user data, we only save these fields if they are not None.
      if self.is_template:
      data = {
      "nx_graph": graph,
      "description": self.description,
      "preview_image": self.preview_image,
      }
      else:
      data = graph
      json_str = jsonpickle.encode(data)
      return json_str
  • The solution is to move away from jsonpickle and manually serialize our skeleton objects to json strings.
  • We can re-use code written in sleap-io to do this
  • Tests will be added to make sure the new serialized skeletons can be used as expected

Types of changes

  • Bugfix
  • New feature
  • Refactor / Code style update (no logical changes)
  • Build / CI changes
  • Documentation Update
  • Other (explain)

Does this address any currently open issues?

Outside contributors checklist

  • Review the guidelines for contributing to this repository
  • Read and sign the CLA and add yourself to the authors list
  • Make sure you are making a pull request against the develop branch (not main). Also you should start your branch off develop
  • Add tests that prove your fix is effective or that your feature works
  • Add necessary documentation (if appropriate)

Thank you for contributing to SLEAP!

❤️

Summary by CodeRabbit

Summary by CodeRabbit

  • Chores
    • Added a TODO comment for future enhancement regarding JSON encoding in the Skeleton class.
  • New Features
    • Improved the to_json method for the Skeleton class, enhancing the serialization of skeleton data with a more structured representation of nodes and edges.
    • Introduced a new Jupyter notebook for testing the conversion of skeleton data to JSON format, demonstrating serialization and deserialization processes.

Copy link

coderabbitai bot commented Sep 3, 2024

Walkthrough

The changes involve modifying the to_json method of the Skeleton class in the sleap/skeleton.py file. The previous implementation using jsonpickle has been replaced with a new approach that constructs node and edge dictionaries for a more structured JSON representation. Additionally, a new Jupyter notebook has been introduced to demonstrate the conversion of skeleton data to JSON format and includes functionality for serialization and deserialization.

Changes

Files Change Summary
sleap/skeleton.py Modified the to_json method to replace jsonpickle with a new structure for node and edge dictionaries. Added a new from_json method.
test_Skeleton_to_json.v006.ipynb Introduced a new notebook for demonstrating the conversion of Skeleton data to JSON format and back, including code for loading skeleton data and handling serialization errors.

Poem

In the code where bunnies play,
A note was added, hip-hip-hooray!
"Replace the old," the rabbit said,
"With something new, let's forge ahead!"
Hopping through JSON, swift and bright,
A future change brings pure delight! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share
Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    -- I pushed a fix in commit <commit_id>, please review it.
    -- Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    -- @coderabbitai generate unit testing code for this file.
    -- @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    -- @coderabbitai generate interesting stats about this repository and render them as a table.
    -- @coderabbitai read src/utils.ts and generate unit testing code.
    -- @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    -- @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.
Early access features: enabled

We are currently testing the following features in early access:

  • OpenAI o1 for code reviews: OpenAI's new o1 model is being tested for generating code suggestions in code reviews.

Note:

  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 35463a1 and cb62a2d.

Files selected for processing (1)
  • sleap/skeleton.py (1 hunks)
Files skipped from review due to trivial changes (1)
  • sleap/skeleton.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between cb62a2d and 4c03ca2.

Files selected for processing (1)
  • sleap/skeleton.py (2 hunks)
Files skipped from review as they are similar to previous changes (1)
  • sleap/skeleton.py

Copy link

codecov bot commented Sep 3, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 75.33%. Comparing base (7ed1229) to head (4c03ca2).
Report is 41 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1934      +/-   ##
===========================================
+ Coverage    73.30%   75.33%   +2.02%     
===========================================
  Files          134      133       -1     
  Lines        24087    24476     +389     
===========================================
+ Hits         17658    18438     +780     
+ Misses        6429     6038     -391     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@talmo
Copy link
Collaborator

talmo commented Sep 3, 2024

Relevant sleap-io portion:

https://github.com/talmolab/sleap-io/blob/2bc3d5210c46bdb25413d25970c4bdc7adb6e8cc/sleap_io/io/slp.py#L606-L710

^ That is 100% self-contained and can probably be ported over directly.

A nicer (if more onerous) approach would be to finally include sleap-io as a dependency and just deep import that exact function and nothing else (for this PR).

For example, in SLEAP's Skeleton.to_json method, you could do something like:

def to_json(self, ...):
    from sleap_io.io.slp import serialize_skeletons

    skeleton_dicts = serialize_skeletons([self])

    ...

This should probably happen at a higher level though and just refactor the serialization logic.

Also note a couple of gotchas with the sleap-io implementation:

  • The insert idx is not exactly the same [1][2]. This is generated by networkx I believe, which we don't use in sleap-io since we do all the serialization ourselves. Shouldn't matter functionally I think.
  • We don't use the weights attribute. [1]

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 4c03ca2 and f352d03.

Files selected for processing (1)
  • sleap/skeleton.py (2 hunks)
Additional context used
Ruff
sleap/skeleton.py

1009-1009: Local variable indexed_node_graph is assigned to but never used

Remove assignment to unused variable indexed_node_graph

(F841)

Additional comments not posted (4)
sleap/skeleton.py (4)

Line range hint 990-1000: LGTM!

The docstring update for the new node_to_idx parameter looks good.


1011-1075: Approved the refactored serialization logic.

The code changes for constructing node and edge dictionaries to serialize the skeleton look good. The code is well-documented and aligns with the PR objective of refactoring the to_json method to leverage the serialization logic from sleap-io. The resulting JSON representation is more structured and eliminates the reliance on jsonpickle.


1077-1119: Approved the final skeleton dictionary construction.

The code changes for creating the final skeleton dictionary using the node and edge dictionaries look good. The resulting JSON representation is structured and eliminates the reliance on jsonpickle. The previous serialization logic has been appropriately commented out.


1095-1100: Remove the unused variable indexed_node_graph.

The Ruff static analysis tool has identified that the variable indexed_node_graph is assigned but never used. Since the code block is commented out and the variable is not used elsewhere, it can be safely removed.

Apply this diff to remove the unused variable:

-        # if node_to_idx is not None:
-        #     indexed_node_graph = nx.relabel_nodes(
-        #         G=self._graph, mapping=node_to_idx
-        #     )  # map nodes to int
-        # else:
-        #     indexed_node_graph = self._graph

Likely invalid or redundant comment.

Comment on lines 1002 to 1003
# TODO: Replace jsonpickle with a custom encoder from sleap-io.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged the TODO for replacing jsonpickle.

The TODO comment is a good reminder for a future enhancement.

Let me know if you need any help with implementing the custom encoder from sleap-io to replace jsonpickle. I can assist with the implementation or open a GitHub issue to track this task.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Outside diff range, codebase verification and nitpick comments (1)
sleap/skeleton.py (1)

1075-1104: Consider removing the old to_json implementation in the future.

It's a good practice to keep the old implementation as a reference during the transition to the new implementation. However, once the new implementation is stable and well-tested, consider removing the commented-out code to improve code readability and maintainability.

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between f352d03 and c3448f4.

Files selected for processing (1)
  • sleap/skeleton.py (2 hunks)
Additional comments not posted (4)
sleap/skeleton.py (4)

Line range hint 1-1:


Line range hint 990-998: LGTM!

The docstring accurately describes the node_to_idx parameter.


1002-1071: Excellent work on the new to_json implementation!

The new implementation follows the logic from sleap-io and constructs a more structured JSON representation of the skeleton. It correctly handles both body edges and symmetry edges by creating separate node and edge dictionaries.

The code changes are approved.


1075-1104: [consistent_with_summary]

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between c3448f4 and 3a7e65a.

Files selected for processing (1)
  • sleap/skeleton.py (2 hunks)
Additional comments not posted (1)
sleap/skeleton.py (1)

Line range hint 990-1076: Review of the to_json method changes

The refactoring of the to_json method introduces a manual approach to JSON serialization, which is a significant change from the previous jsonpickle-based method. Here are some observations and suggestions:

  1. Correctness: The method constructs nodes_dicts and edges_dicts correctly based on the provided nodes and edges. However, the use of print statements (lines 1008, 1012, 1013, 1018, 1026, 1029, 1039, 1049, 1051) should be removed or replaced with logging at an appropriate level if needed for debugging.

  2. Performance: The method iterates over all nodes and edges, which is expected. However, ensure that the node_to_idx mapping and the handling of edge types are optimized for large datasets.

  3. Maintainability: The method uses hardcoded values for edge_type (lines 1021-1024, 1045-1047), which could be abstracted or managed in a more flexible way to accommodate changes in edge types.

  4. Suggestion: Consider adding error handling for cases where node_to_idx might not contain all nodes present in the skeleton, which could lead to KeyError.

  5. Documentation: Update the method's docstring to reflect the new logic and parameters used in the method.

Overall, the changes align with the goal of removing jsonpickle dependency and manually handling JSON serialization. Ensure comprehensive testing to validate the serialization format and compatibility with existing systems.

@eberrigan
Copy link
Contributor Author

Relevant sleap-io portion:

https://github.com/talmolab/sleap-io/blob/2bc3d5210c46bdb25413d25970c4bdc7adb6e8cc/sleap_io/io/slp.py#L606-L710

^ That is 100% self-contained and can probably be ported over directly.

A nicer (if more onerous) approach would be to finally include sleap-io as a dependency and just deep import that exact function and nothing else (for this PR).

For example, in SLEAP's Skeleton.to_json method, you could do something like:

def to_json(self, ...):
    from sleap_io.io.slp import serialize_skeletons

    skeleton_dicts = serialize_skeletons([self])

    ...

This should probably happen at a higher level though and just refactor the serialization logic.

Also note a couple of gotchas with the sleap-io implementation:

  • The insert idx is not exactly the same [1][2]. This is generated by networkx I believe, which we don't use in sleap-io since we do all the serialization ourselves. Shouldn't matter functionally I think.
  • We don't use the weights attribute. [1]

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Outside diff range, codebase verification and nitpick comments (7)
sleap/skeleton.py (7)

1002-1020: Remove debug print statements.

The debug print statements at lines 1007, 1017, and 1019 should be removed before merging the code.

Apply this diff to remove the debug print statements:

-                print(f'node: {node}')
                 # Note: This ID is not the same as the node index in the skeleton in
                 # legacy SLEAP, but we do not retain this information in the labels, so
                 # IDs will be different.
                 #
                 # The weight is also kept fixed here, but technically this is not
                 # modified or used in legacy SLEAP either.
                 #
                 # TODO: Store legacy metadata in labels to get byte-level compatibility?
                 node_to_id[node] = len(node_to_id)
-                print(f'node_to_id: {node_to_id}')
                 nodes_dicts.append({"name": node.name, "weight": 1.0})
-        print(f'nodes_dicts: {nodes_dicts}')

1021-1059: Remove debug print statements.

The debug print statements at lines 1024-1026, 1033, 1036, 1042-1043, 1045-1046, and 1058 should be removed before merging the code.

Apply this diff to remove the debug print statements:

         for edge_ind, edge in enumerate(self.edges):
-            print(f'edge_ind: {edge_ind}')
-            print(f'edge: {edge}')
             if edge_ind == 0:
                 edge_type = {
                     "py/reduce": [
                         {"py/type": "sleap.skeleton.EdgeType"},
                         {"py/tuple": [1]},  # 1 = real edge, 2 = symmetry edge
                     ]
                 }
-                print(f'edge_type: {edge_type}')
             else:
                 edge_type = {"py/id": 1}
-                print(f'edge_type: {edge_type}')

             # Edges are stored as a list of tuples of nodes
             # The source and target are the nodes in the tuple (edge) are the first and 
             # second nodes respectively
             source = edge[0]
-            print(f'source: {source}')
-            print(f'node_to_id[source]: {node_to_id[source]}')
             target = edge[1]
-            print(f'target: {target}')
-            print(f'node_to_id[target]: {node_to_id[target]}')
             edges_dicts.append(
                 {
                     # Note: Insert idx is not the same as the edge index in the skeleton
                     # in legacy SLEAP.
                     "edge_insert_idx": edge_ind,
                     "key": 0,  # Always 0.
                     "source": {"py/id": node_to_id[source]},
                     "target": {"py/id": node_to_id[target]},
                     "type": edge_type,
                 }
             )
-        print(f'edges_dicts: {edges_dicts}')

1060-1084: Remove debug print statements.

The debug print statements at lines 1062-1063, 1075-1076 should be removed before merging the code.

Apply this diff to remove the debug print statements:

         for symmetry_ind, symmetry in enumerate(self.symmetries):
-            print(f'symmetry_ind: {symmetry_ind}')
-            print(f'symmetry: {symmetry}')
             if symmetry_ind == 0:
                 edge_type = {
                     "py/reduce": [
                         {"py/type": "sleap.skeleton.EdgeType"},
                         {"py/tuple": [2]},  # 1 = real edge, 2 = symmetry edge
                     ]
                 }
             else:
                 edge_type = {"py/id": 2}

             src, dst = tuple(symmetry.nodes)
-            print(f'src: {src}')
-            print(f'dst: {dst}')
             edges_dicts.append(
                 {
                     "key": 0,
                     "source": {"py/id": node_to_id[src]},
                     "target": {"py/id": node_to_id[dst]},
                     "type": edge_type,
                 }
             )

1113-1120: Remove commented-out code.

The commented-out code related to encoding the skeleton using jsonpickle is no longer needed and should be removed to improve code readability and maintainability.

Apply this diff to remove the commented-out code:

-        # jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4)
-        # if node_to_idx is not None:
-        #     indexed_node_graph = nx.relabel_nodes(
-        #         G=self._graph, mapping=node_to_idx
-        #     )  # map nodes to int
-        # else:
-        #     indexed_node_graph = self._graph

1121-1123: Remove commented-out code.

The commented-out code related to encoding the graph using json_graph is no longer needed and should be removed to improve code readability and maintainability.

Apply this diff to remove the commented-out code:

-        # # Encode to JSON
-        # graph = json_graph.node_link_data(indexed_node_graph)

1124-1136: Remove commented-out code.

The commented-out code related to creating a data dictionary based on the skeleton's template status is no longer needed and should be removed to improve code readability and maintainability.

Apply this diff to remove the commented-out code:

-        # # SLEAP v1.3.0 added `description` and `preview_image` to `Skeleton`, but saving
-        # # these fields breaks data format compatibility. Currently, these are only
-        # # added in our custom template skeletons. To ensure backwards data format
-        # # compatibilty of user data, we only save these fields if they are not None.
-        # if self.is_template:
-        #     data = {
-        #         "nx_graph": graph,
-        #         "description": self.description,
-        #         "preview_image": self.preview_image,
-        #     }
-        # else:
-        #     data = graph

1137-1139: Remove commented-out code.

The commented-out code related to encoding the data using jsonpickle and returning the JSON string is no longer needed and should be removed to improve code readability and maintainability.

Apply this diff to remove the commented-out code:

-        # json_str = jsonpickle.encode(data)
-
-        # return json_str
Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 3a7e65a and 5decb55.

Files selected for processing (1)
  • sleap/skeleton.py (3 hunks)
Additional context used
Ruff
sleap/skeleton.py

1102-1102: Local variable skeleton_dict is assigned to but never used

Remove assignment to unused variable skeleton_dict

(F841)

Comment on lines 1086 to 1112
# Create skeleton dict.
if self.is_template:
data = {
"nx_graph": graph,
skeleton_dict = {
"directed": True,
"graph": {
"name": self.name,
"num_edges_inserted": len(self.edges),
},
"links": edges_dicts,
"multigraph": True,
# In the order in Skeleton.nodes and must match up with nodes_dicts.
"nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes],
"description": self.description,
"preview_image": self.preview_image,
}
else:
data = graph

json_str = jsonpickle.encode(data)

return json_str
skeleton_dict ={
"directed": True,
"graph": {
"name": self.name,
"num_edges_inserted": len(self.edges),
},
"links": edges_dicts,
"multigraph": True,
# In the order in Skeleton.nodes and must match up with nodes_dicts.
"nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes],}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address the unused variable skeleton_dict.

The static analysis tool Ruff has correctly identified that the skeleton_dict variable is assigned but never used. This is a valid concern that should be addressed.

Consider the following options to address the unused variable:

  1. If the skeleton_dict is intended to be used later in the code, ensure that it is actually used. If not, remove the assignment.

  2. If the skeleton_dict is not needed, remove the entire code block that creates it.

Please let me know if you need any assistance with addressing this issue.

Tools
Ruff

1102-1102: Local variable skeleton_dict is assigned to but never used

Remove assignment to unused variable skeleton_dict

(F841)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Outside diff range and nitpick comments (1)
sleap/skeleton.py (1)

1116-1143: Remove the commented out code.

The commented out code at line range 1116-1143 is the old implementation that used jsonpickle for encoding. It has been replaced by the new implementation that manually serializes the skeleton data to JSON.

Consider removing the commented out code to improve code readability and maintainability.

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 5decb55 and 622853c.

Files selected for processing (1)
  • sleap/skeleton.py (3 hunks)
Additional comments not posted (1)
sleap/skeleton.py (1)

1090-1110: LGTM!

The code changes for creating the skeleton_dict structure based on self.is_template are approved.

sleap/skeleton.py Show resolved Hide resolved
Comment on lines 1047 to 1057
edges_dicts.append(
{
# Note: Insert idx is not the same as the edge index in the skeleton
# in legacy SLEAP.
"edge_insert_idx": edge_ind,
"key": 0, # Always 0.
"source": {"py/object": "sleap.skeleton.Node", "py/state": {"name": source.name, "weight": 1.0}},
"target": {"py/object": "sleap.skeleton.Node", "py/state": {"name": target.name, "weight": 1.0}},
# "target": {"py/id": node_to_id[target]},
"type": edge_type,
}
Copy link
Collaborator

@talmo talmo Sep 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about doing something like:

Suggested change
edges_dicts.append(
{
# Note: Insert idx is not the same as the edge index in the skeleton
# in legacy SLEAP.
"edge_insert_idx": edge_ind,
"key": 0, # Always 0.
"source": {"py/object": "sleap.skeleton.Node", "py/state": {"name": source.name, "weight": 1.0}},
"target": {"py/object": "sleap.skeleton.Node", "py/state": {"name": target.name, "weight": 1.0}},
# "target": {"py/id": node_to_id[target]},
"type": edge_type,
}
if source not in node_to_id:
source_dict = {"py/object": "sleap.skeleton.Node", "py/state": {"py/tuple": [source.name, source.weight]}}
node_to_id[source] = len(node_to_id)
else:
source_dict = {"py/id": node_to_id[source]}
if target not in node_to_id:
target_dict = {"py/object": "sleap.skeleton.Node", "py/state": {"py/tuple": [target.name, target.weight]}}
node_to_id[target] = len(node_to_id)
else:
target_dict = {"py/id": node_to_id[target]}
edges_dicts.append(
{
# Note: Insert idx is not the same as the edge index in the skeleton
# in legacy SLEAP.
"edge_insert_idx": edge_ind,
"key": 0, # Always 0.
"source": source_dict,
"target": target_dict,
"type": edge_type,
}

Copy link
Collaborator

@roomrys roomrys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try moving the "nodes" list above the "links" dictionary and see what happens

Comment on lines 1097 to 1100
"links": edges_dicts,
"multigraph": True,
# In the order in Skeleton.nodes and must match up with nodes_dicts.
"nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes],
Copy link
Collaborator

@roomrys roomrys Sep 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming jsonpickle is decoding by reading things in order, we expect that all Nodes will only appear in the "nodes" list once. Therefore, we can (hopefully) move the "nodes" list before the "links" dictionary. The "nodes" list would need to define the nodes in the {py/object: ..., py/state: ...} dictionary. Then, the "links" dictionary should just reference nodes by "py/id" (referencing the Node's index in the "nodes" list).

Suggested change
"links": edges_dicts,
"multigraph": True,
# In the order in Skeleton.nodes and must match up with nodes_dicts.
"nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes],
"nodes": [{"id": {"py/object": "sleap.skeleton.Node", "py/state": {"name": node.name, "weight": node.weight}}} for node in self.nodes],
"links": edges_dicts,
"multigraph": True,

Comment on lines 1111 to 1114
"links": edges_dicts,
"multigraph": True,
# In the order in Skeleton.nodes and must match up with nodes_dicts.
"nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes],}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming jsonpickle is decoding by reading things in order, we can assume that all Nodes will only appear in the "nodes" list once. Therefore, we can (hopefully) move the "nodes" list before the "links" dictionary. The "nodes" list would need to define the nodes in the {py/object: ..., py/state: ...} dictionary. Then, the "links" dictionary should just reference nodes by "py/id" (referencing the Node's index in the "nodes" list).

Suggested change
"links": edges_dicts,
"multigraph": True,
# In the order in Skeleton.nodes and must match up with nodes_dicts.
"nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes],}
"nodes": [{"id": {"py/object": "sleap.skeleton.Node", "py/state": {"name": node.name, "weight": node.weight}}} for node in self.nodes],
"links": edges_dicts,
"multigraph": True,

Comment on lines 1053 to 1054
"source": {"py/object": "sleap.skeleton.Node", "py/state": {"name": source.name, "weight": 1.0}},
"target": {"py/object": "sleap.skeleton.Node", "py/state": {"name": target.name, "weight": 1.0}},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are moving the "nodes" list above the "links" dictionary, then we should be able to just reference the Node's "py/id" here (i.e. their index in that "nodes" list which is also the Node's index in Skeleton.nodes)

Suggested change
"source": {"py/object": "sleap.skeleton.Node", "py/state": {"name": source.name, "weight": 1.0}},
"target": {"py/object": "sleap.skeleton.Node", "py/state": {"name": target.name, "weight": 1.0}},
"source": {"py/id": self.nodes.index(node)},
"target": {"py/id": self.nodes.index(node)},

@talmo
Copy link
Collaborator

talmo commented Sep 12, 2024

From group discussion:

  1. We want to unstructure in the legacy "tuple" format, even though attrs won't like it in the future so that we have broadest compatibility.
    • The reason to make sure that Skeleton.to_json works in the old format is that we do call it by itself outside of a Labels context. Specifically when we store the skeleton in training_job_config.json which is triggered here in training.py, and serialized here in TrainingJobConfig.to_json) using the default attrs/cattr unstructuring routines and the built-in python json module.
    • In addition to the proposed changes above, we need to make sure that the "global" nodes list in the Labels also serializes out to dictionaries in the tuple format. Specifically, this happens here-ish in Labels.to_dict.
    • We should also make sure that nothing else breaks upstream in LabelsV1Adaptor.write.
  2. We will handle structuring ourselves, mimicking the logic in sleap-io. This way, we have more control over serialization format and when we switch to a replacement.
    • We will need to handle structuring in Skeleton.from_json here.
    • This will require switching from jsonpickle to simplejson and then creating the objects ourselves by calling their constructors, manually parsing the metadata like we do in sleap-io here (note how we expect the old "tuple" format there as well).

Notes on "tuple" vs "dict" format

When pickling an attrs object, the __getstate__ method is used for pickling and first unstructures the attrs object into a dictionary. The format of this dictionary used to be tuple-based but is now dict-based in newer attrs (>=22.2.0) due to the discussion in python-attrs/attrs#1004.

Current versions of attrs support the old format (which we use and in which our users have created many files that use it). At some point, this will no longer work as per python-attrs/attrs#1091.

Our plan is to manually handle structuring/unstructuring rather than relying on jsonpickle or attrs, allowing us to define our own format and to support both legacy and new formats.

For reference, for a Node defined like:

node = sleap.skeleton.Node(name="a", weight=1.0)

The old format ("tuple") unstructures this into dictionaries like:

{"py/object": "sleap.skeleton.Node", "py/state": {"py/tuple": ["a", 1.0]}}

Whereas the new format ("dict") unstructures this into dictionaries like:

{"py/object": "sleap.skeleton.Node", "py/state": {"name": "a", "weight": 1.0}}

@roomrys roomrys mentioned this pull request Sep 12, 2024
11 tasks
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 884da47 and 01d0255.

Files selected for processing (1)
  • sleap/skeleton.py (2 hunks)
Additional comments not posted (4)
sleap/skeleton.py (4)

Line range hint 990-1169: The refactored to_json method aligns with the PR objective.

The changes made to the to_json method eliminate the reliance on jsonpickle and provide a more controlled and structured approach to serializing the skeleton object into a JSON string. The manual construction of node and edge dictionaries allows for better customization of the resulting JSON representation.

Suggestions:

  • Consider removing or commenting out the unused code from the previous implementation (lines 1142-1168) to improve code cleanliness and readability.

1021-1065: LGTM! The edge dictionary construction looks good.

The code segment correctly constructs the list of edge dictionaries (edges_dicts) based on the skeleton's edges. The inclusion of relevant information such as the edge index, source and target nodes, and edge type ensures a comprehensive representation of the edges in the JSON format.

The comments provided are helpful in explaining the differences in edge indexing compared to legacy SLEAP, which adds clarity to the implementation.


1067-1092: The symmetry edge dictionary construction is implemented correctly.

The code segment properly constructs the edge dictionaries for the symmetry edges in the skeleton. The inclusion of the source and target nodes using their IDs from the node_to_id mapping ensures consistent referencing of nodes in the JSON representation.

The determination of the edge type based on the index of the symmetry edge is a valid approach.


1093-1139: The construction of the final skeleton dictionary and JSON conversion is implemented properly.

The code segment correctly constructs the final skeleton dictionary (skeleton_dict) based on whether the skeleton is a template or not. The inclusion of additional fields such as description and preview_image for template skeletons ensures that the necessary information is preserved in the JSON representation.

The incorporation of the graph information, nodes, and edges in the skeleton dictionary provides a complete representation of the skeleton.

The use of json.dumps() to convert the skeleton dictionary to a JSON string follows standard practices and is an appropriate choice.

Comment on lines +1015 to +1016
# TODO: Store legacy metadata in labels to get byte-level compatibility?
node_to_id[node] = len(node_to_id)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged the TODO for storing legacy metadata.

The TODO comment is a good reminder for a future enhancement to improve compatibility with legacy data.

Let me know if you need any help with implementing the storage of legacy metadata in labels to achieve byte-level compatibility. I can assist with the implementation or open a GitHub issue to track this task.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Outside diff range and nitpick comments (2)
sleap/skeleton.py (2)

1068-1092: Add a comment to clarify the edge_type handling.

The code for constructing the edges_dicts list for symmetry edges looks good. However, consider adding a comment to explain why the edge_type is set differently for the first symmetry edge (lines 1072-1077) compared to the rest (lines 1078-1079). This will improve code readability and maintainability.


1142-1169: Remove the commented out code.

The commented out code that uses jsonpickle for serialization is no longer used and can be safely removed. This will improve code cleanliness and readability.

Apply this diff to remove the unused code:

-        # jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4)
-        # if node_to_idx is not None:
-        #     indexed_node_graph = nx.relabel_nodes(
-        #         G=self._graph, mapping=node_to_idx
-        #     )  # map nodes to int
-        # else:
-        #     indexed_node_graph = self._graph
-
-        # # Encode to JSON
-        # graph = json_graph.node_link_data(indexed_node_graph)
-
-        # # SLEAP v1.3.0 added `description` and `preview_image` to `Skeleton`, but saving
-        # # these fields breaks data format compatibility. Currently, these are only
-        # # added in our custom template skeletons. To ensure backwards data format
-        # # compatibilty of user data, we only save these fields if they are not None.
-        # if self.is_template:
-        #     data = {
-        #         "nx_graph": graph,
-        #         "description": self.description,
-        #         "preview_image": self.preview_image,
-        #     }
-        # else:
-        #     data = graph
-
-        # json_str = jsonpickle.encode(data)
-
-        # return json_str
-
Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 01d0255 and 71b0307.

Files selected for processing (1)
  • sleap/skeleton.py (2 hunks)
Additional comments not posted (5)
sleap/skeleton.py (5)

1002-1019: LGTM!

The logic for building the nodes_dicts list looks good. The node_to_id mapping ensures unique IDs are assigned to nodes.


1021-1065: Looks good!

The code for constructing the edges_dicts list for normal edges is implemented correctly. The comments help explain the purpose of the edge_insert_idx and key fields.


1093-1135: LGTM!

The code for creating the skeleton_dict is implemented correctly. It properly combines the nodes_dicts and edges_dicts lists and handles the case when the skeleton is a template by including the description and preview_image fields.


1137-1139: Looks good!

The serialization of the skeleton_dict to a JSON string using json.dumps() is implemented correctly. The indent and sort_keys parameters ensure a nicely formatted output.


Line range hint 990-1169: Significant refactoring with improved serialization logic.

The to_json function has undergone a major refactoring to replace the usage of jsonpickle with a custom serialization approach. The new implementation provides more control over the JSON structure and handles various scenarios correctly.

The custom serialization logic builds the nodes_dicts and edges_dicts lists to represent the skeleton nodes and edges respectively. The resulting skeleton_dict is then serialized to a JSON string using json.dumps().

The commented out code that uses jsonpickle is no longer needed and can be safely removed to improve code cleanliness and maintainability.

Overall, the refactored code is a significant improvement over the previous implementation and enhances the serialization process of the skeleton object.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Outside diff range and nitpick comments (6)
sleap/skeleton.py (6)

1015-1016: Acknowledged the TODO for storing legacy metadata.

The TODO comment is a good reminder for a future enhancement to improve compatibility with legacy data.

Let me know if you need any help with implementing the storage of legacy metadata in labels to achieve byte-level compatibility. I can assist with the implementation or open a GitHub issue to track this task.


1007-1007: Remove print statements used for debugging.

The print statements at lines 1007, 1017, and 1019 appear to be used for debugging purposes. It's a good practice to remove such statements before merging the code to keep the codebase clean.

Consider removing these print statements:

- print(f"node: {node}")
- print(f"node_to_id: {node_to_id}")
- print(f"nodes_dicts: {nodes_dicts}")

Also applies to: 1017-1017, 1019-1019


1024-1025: Remove print statements used for debugging.

The print statements at lines 1024-1025, 1033, 1036, 1042-1043, 1045-1046, and 1064 appear to be used for debugging purposes. It's a good practice to remove such statements before merging the code to keep the codebase clean.

Consider removing these print statements:

- print(f"edge_ind: {edge_ind}")
- print(f"edge: {edge}")
- print(f"edge_type: {edge_type}")
- print(f"edge_type: {edge_type}")
- print(f"source: {source}")
- print(f"node_to_id[source]: {node_to_id[source]}")
- print(f"target: {target}")
- print(f"node_to_id[target]: {node_to_id[target]}")
- print(f"edges_dicts: {edges_dicts}")

Also applies to: 1033-1033, 1036-1036, 1042-1043, 1045-1046, 1064-1064


1068-1069: Remove print statements used for debugging.

The print statements at lines 1068-1069 and 1081-1082 appear to be used for debugging purposes. It's a good practice to remove such statements before merging the code to keep the codebase clean.

Consider removing these print statements:

- print(f"symmetry_ind: {symmetry_ind}")
- print(f"symmetry: {symmetry}")
- print(f"src: {symmetry_src}")
- print(f"dst: {symmetry_dst}")

Also applies to: 1081-1082


1136-1136: Remove print statements used for debugging.

The print statements at lines 1136 and 1138 appear to be used for debugging purposes. It's a good practice to remove such statements before merging the code to keep the codebase clean.

Consider removing these print statements:

- print(f"skeleton_dict: {skeleton_dict}")
- print(f"json_str: {json_str}")

Also applies to: 1138-1138


1141-1167: Remove commented-out code.

The commented-out code from lines 1141 to 1167 appears to be the previous implementation using jsonpickle and handling node mapping. Since it is no longer needed, it's a good practice to remove such commented-out code to keep the codebase clean and maintainable.

Consider removing the commented-out code:

- # jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4)
- # if node_to_idx is not None:
- #     indexed_node_graph = nx.relabel_nodes(
- #         G=self._graph, mapping=node_to_idx
- #     )  # map nodes to int
- # else:
- #     indexed_node_graph = self._graph
- 
- # # Encode to JSON
- # graph = json_graph.node_link_data(indexed_node_graph)
- 
- # # SLEAP v1.3.0 added `description` and `preview_image` to `Skeleton`, but saving
- # # these fields breaks data format compatibility. Currently, these are only
- # # added in our custom template skeletons. To ensure backwards data format
- # # compatibilty of user data, we only save these fields if they are not None.
- # if self.is_template:
- #     data = {
- #         "nx_graph": graph,
- #         "description": self.description,
- #         "preview_image": self.preview_image,
- #     }
- # else:
- #     data = graph
- 
- # json_str = jsonpickle.encode(data)
- 
- # return json_str
Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 71b0307 and 69db671.

Files selected for processing (1)
  • sleap/skeleton.py (2 hunks)
Additional comments not posted (1)
sleap/skeleton.py (1)

1092-1134: LGTM!

The code for building the skeleton_dict dictionary looks good. It correctly handles the cases for template and non-template skeletons, includes the necessary graph properties, and populates the nodes and links fields appropriately.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Outside diff range and nitpick comments (2)
test_Skeleton_to_json.v006.ipynb (2)

60-103: Remove or refactor large blocks of commented-out code for readability

The code cell contains extensive commented-out code from the old SLEAP implementation. To enhance readability and maintainability, consider removing this code or moving it to documentation if needed for future reference.


112-120: Clean up commented-out test code for clarity

This cell includes commented-out code related to testing with a skeleton from a labels file created with sleap-io. If this code is no longer necessary, please remove it to keep the notebook focused and easy to read.

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 69db671 and 0e2f9b5.

Files selected for processing (1)
  • test_Skeleton_to_json.v006.ipynb (1 hunks)

Comment on lines +178 to +186
"# Save to json using the old to_json method (deprecated)\n",
"test_skeleton_json = test_skeleton.to_json()\n",
"print(f'test_skeleton_json: {test_skeleton_json}')\n",
"\n",
"# Save json string to file\n",
"with open(r'test_skeleton_serialization.json', 'w') as f:\n",
" f.write(test_skeleton_json)\n",
" print(f'Wrote test_skeleton_json to file')"
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace deprecated to_json method with the new serialization approach

The to_json method is deprecated, and relying on jsonpickle contradicts the PR's objective to eliminate its usage. Please update the serialization code to use the new manual serialization method that aligns with the refactored Skeleton class.

Comment on lines +209 to +212
"# Load the json file\n",
"test_skeleton_from_json = Skeleton.from_json(test_skeleton_json)\n",
"print(f'test_skeleton_from_json: {test_skeleton_from_json}')"
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolve the TypeError: unhashable type: 'dict' during deserialization

Deserializing the skeleton with Skeleton.from_json(test_skeleton_json) results in a TypeError: unhashable type: 'dict'. This error likely occurs because the nodes are not properly reconstructed as hashable objects. Update the deserialization logic to align with the new JSON structure and ensure that all nodes and edges are correctly hashed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Outside diff range and nitpick comments (2)
sleap/skeleton.py (2)

1035-1036: Simplify edge_type assignment

The conditional checks if edge_ind == 0 and if symmetry_ind == 0 to assign edge_type differently may not be necessary and can be simplified for clarity.

Consider initializing edge_type before the loops or handling it uniformly within the loops.

Also applies to: 1071-1072


1123-1125: Ensure consistent formatting in list comprehension

In building the nodes list within skeleton_dict, ensure consistent formatting and indentation of the list comprehension for better readability.

Review the formatting to align with PEP 8 guidelines for list comprehensions.

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 0e2f9b5 and 4ff6fdb.

Files selected for processing (1)
  • sleap/skeleton.py (2 hunks)

node_to_id = {}
for node in self.nodes:
if node not in node_to_id:
print(f"node: {node}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove debug print statements

The print statements throughout the to_json method appear to be for debugging purposes. These should be removed or replaced with appropriate logging to maintain clean production code.

Apply this diff to remove the print statements:

- print(f"node: {node}")
- print(f"node_to_id: {node_to_id}")
- print(f"nodes_dicts: {nodes_dicts}")
- print(f"edge_ind: {edge_ind}")
- print(f"edge: {edge}")
- print(f"edge_type: {edge_type}")
- print(f"source: {source}")
- print(f"node_to_id[source]: {node_to_id[source]}")
- print(f"target: {target}")
- print(f"node_to_id[target]: {node_to_id[target]}")
- print(f"edges_dicts: {edges_dicts}")
- print(f"symmetry_ind: {symmetry_ind}")
- print(f"symmetry: {symmetry}")
- print(f"src: {symmetry_src}")
- print(f"dst: {symmetry_dst}")
- print(f"skeleton_dict: {skeleton_dict}")
- print(f"json_str: {json_str}")

Also applies to: 1017-1017, 1019-1019, 1024-1026, 1033-1036, 1042-1046, 1058-1058, 1062-1063, 1075-1076, 1130-1132

Comment on lines 1053 to 1054
"source": {"py/id": self.nodes.index(node)},
"target": {"py/id": self.nodes.index(node)},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct the use of undefined variable node

In lines 1053-1054, the variable node is undefined in this context. This will result in a NameError.

Replace node with source and target respectively:

- "source": {"py/id": self.nodes.index(node)},
- "target": {"py/id": self.nodes.index(node)},
+ "source": {"py/id": self.nodes.index(source)},
+ "target": {"py/id": self.nodes.index(target)},
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"source": {"py/id": self.nodes.index(node)},
"target": {"py/id": self.nodes.index(node)},
"source": {"py/id": self.nodes.index(source)},
"target": {"py/id": self.nodes.index(target)},

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 4ff6fdb and b4f10af.

Files selected for processing (1)
  • sleap/skeleton.py (2 hunks)

Comment on lines +990 to 991
node_to_idx: optional dict which maps :class:`Node`s to index
in some list. This is used when saving
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unused parameter node_to_idx from to_json method

The parameter node_to_idx is not used within the to_json method. Consider removing it to simplify the method signature and avoid confusion.

@roomrys
Copy link
Collaborator

roomrys commented Sep 25, 2024

Closed in favor of:

@roomrys roomrys closed this Sep 25, 2024
@roomrys roomrys deleted the elizabeth/refactor-skeletons-serialization branch September 25, 2024 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants