niivue / ipyniivue

A WebGL-powered Jupyter Widget for Niivue based on anywidget
BSD 2-Clause "Simplified" License
25 stars 8 forks source link

Better camel/snake scale conversion #53

Closed manzt closed 6 months ago

manzt commented 6 months ago

Probably should be is_slice_mm: https://github.com/niivue/ipyniivue-experimental/blob/8f9ef1a3620a4a19530ddafd595e1ba6c085b07f/src/ipyniivue_experimental/_options_mixin.py#L276

hanayik commented 6 months ago

and 3_d should be _3d_ here

manzt commented 6 months ago

I'm sure it will be easy to fix. @kolibril13, want to take a try?

These are automatically generated from: scripts/generate_options_mixin.py:

https://github.com/niivue/ipyniivue-experimental/blob/8f9ef1a3620a4a19530ddafd595e1ba6c085b07f/scripts/generate_options_mixin.py#L9C1-L12C86

manzt commented 6 months ago

Should also make sure snake_to_camel (the opposite) behaves correctly:

https://github.com/niivue/ipyniivue-experimental/blob/0f0bc7bb2f178e5f28a582705f669c7a2ab7ea95/src/ipyniivue_experimental/_utils.py#L6

manzt commented 6 months ago

I bet we could vendor some of these from pydantic: https://github.com/pydantic/pydantic/blob/f8c01bb4245220f21cc840981b41f905b1bb2c3e/pydantic/alias_generators.py#L20

manzt commented 6 months ago

I get we could vendor some of these from pydantic

Nope!

to_snake("show3Dcrosshair")
# 'show_3_dcrosshair'

Easiest solution would be to have an "exceptions" map, ad handle these edge cases (rather than a snake_case function that has a bunch of weird edge cases baked in).

kolibril13 commented 6 months ago

I'll investigate this!

kolibril13 commented 6 months ago

We currently have 5 edge cases:

show3Dcrosshair -> show3_dcrosshair -> show_3d_crosshair  
meshThicknessOn2D -> mesh_thickness_on2_d -> mesh_thickness_on_2d  
yoke3Dto2DZoom -> yoke3_dto2_d_zoom ->  yoke_3d_to_2d_zoom  
isSliceMM -> is_slice_m_m -> is_slice_mm  
limitFrames4D -> limit_frames4_d -> limit_frames_4d  

I was also tinkering with regex, but I also prefer an exceptions maps. I've noted, that the JS naming is also not consistent, e.g. there is show3Dcrosshair but then there is yoke3Dto2DZoom Note that the "c" is lowercase, and "Z" is uppercase. -> cc @hanayik is that on purpose?

Here's my current solution:

parameters_list = ["textHeight", "colorbarHeight", "crosshairWidth", "rulerWidth", "show3Dcrosshair", "backColor", "crosshairColor", "fontColor", "selectionBoxColor", "clipPlaneColor", "rulerColor", "colorbarMargin", "trustCalMinMax", "clipPlaneHotKey", "viewModeHotKey", "doubleTouchTimeout", "longTouchTimeout", "keyDebounceTime", "isNearestInterpolation", "isResizeCanvas", "isAtlasOutline", "isRuler", "isColorbar", "isOrientCube", "multiplanarPadPixels", "multiplanarForceRender", "isRadiologicalConvention", "meshThicknessOn2D", "dragMode", "yoke3Dto2DZoom", "isDepthPickMesh", "isCornerOrientationText", "sagittalNoseLeft", "isSliceMM", "isV1SliceShader", "isHighResolutionCapable", "logLevel", "loadingText", "isForceMouseClickToVoxelCenters", "dragAndDropEnabled", "drawingEnabled", "penValue", "floodFillNeighbors", "isFilledPen", "thumbnail", "maxDrawUndoBitmaps", "sliceType", "meshXRay", "isAntiAlias", "limitFrames4D", "isAdditiveBlend", "showLegend", "legendBackgroundColor", "legendTextColor", "multiplanarLayout", "renderOverlayBlend"]

def camel_to_snake(name: str):
    name = name.replace("show3Dcrosshair", "show_3d_crosshair")
    name = name.replace("meshThicknessOn2D", "mesh_thickness_on_2d")
    name = name.replace("yoke3Dto2DZoom", "yoke_3d_to_2d_zoom")
    name = name.replace("isSliceMM", "is_slice_mm")
    name = name.replace("limitFrames4D", "limit_frames_4d")
    return "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_")

snake_case_list = [camel_to_snake(s) for s in parameters_list]

print(snake_case_list) # gives ['text_height', 'colorbar_height', 'crosshair_width', 'ruler_width', 'show_3d_crosshair', 'back_color', 'crosshair_color', 'font_color', 'selection_box_color', 'clip_plane_color', 'ruler_color', 'colorbar_margin', 'trust_cal_min_max', 'clip_plane_hot_key', 'view_mode_hot_key', 'double_touch_timeout', 'long_touch_timeout', 'key_debounce_time', 'is_nearest_interpolation', 'is_resize_canvas', 'is_atlas_outline', 'is_ruler', 'is_colorbar', 'is_orient_cube', 'multiplanar_pad_pixels', 'multiplanar_force_render', 'is_radiological_convention', 'mesh_thickness_on_2d', 'drag_mode', 'yoke_3d_to_2d_zoom', 'is_depth_pick_mesh', 'is_corner_orientation_text', 'sagittal_nose_left', 'is_slice_mm', 'is_v1_slice_shader', 'is_high_resolution_capable', 'log_level', 'loading_text', 'is_force_mouse_click_to_voxel_centers', 'drag_and_drop_enabled', 'drawing_enabled', 'pen_value', 'flood_fill_neighbors', 'is_filled_pen', 'thumbnail', 'max_draw_undo_bitmaps', 'slice_type', 'mesh_x_ray', 'is_anti_alias', 'limit_frames_4d', 'is_additive_blend', 'show_legend', 'legend_background_color', 'legend_text_color', 'multiplanar_layout', 'render_overlay_blend']
manzt commented 6 months ago

Nice! Great work finding the exceptions. Rather than baking in the exceptions to camel_to_snake, I'd probably keep a map.

RENAME_OVERRIDES = {
  "show3Dcrosshair": "show_3d_crosshair",
  "meshThicknessOn2D": "mesh_thickness_on_2d",
  "yoke3Dto2DZoom": "yoke_3d_to_2d_zoom",
  "isSliceMM": "is_slice_mm",
  "limitFrames4D": "limit_frames_4d"
}

names = [RENAME_OVERRIDES.get(s, camel_to_snake(s)) for s in parameters_list]

Want to make a PR with our approach? I'd just add the renames within the for loop in scripts/generate_options_mixin.py.

https://github.com/niivue/ipyniivue-experimental/blob/685917a0add0488fbcc98aba05e72b1077373e94/scripts/generate_options_mixin.py#L64

snake_name = RENAME_OVERRIDES.get(option, camel_to_snake(option))

then run python scripts/generate_options_mixin.py to generate the new bindings.

manzt commented 6 months ago

Now that I think of it, having these overrides internally in the Python package would be really useful. It would let us revert the snake names here:

https://github.com/niivue/ipyniivue-experimental/blob/685917a0add0488fbcc98aba05e72b1077373e94/src/ipyniivue_experimental/_widget.py#L33

# _constants.py
_SNAKE_TO_CAMEL_OVERRIDES = {
  "show_3d_crosshair": "show3Dcrosshair",
  "mesh_thickness_on_2d": "meshThicknessOn2D",
  "yoke_3d_to_2d_zoom": "yoke3Dto2DZoom",
  "is_slice_mm": "isSliceMM",
  "limit_frames_4d": "limitFrames4D",
}
# _widget.py
 _opts = {_SNAKE_TO_CAMEL_OVERRIDES.get(k, snake_to_camel(k)): v for k, v in opts.items()} 

Then in scripts/generate_options_mixin.py:

 RENAME_OVERRIDES = {v: k for k, v in _SNAKE_TO_CAMEL_OVERRIDES.items()}

This ensures the two will stay in sync (if someone passes in a snake_case option to AnyNiivue constructor)

kolibril13 commented 6 months ago

that worked, very elegant solution, love it!

image

Will make a pr right now!