From 497820aeffeeb88bd3537cf77f1d34c2b5dea609 Mon Sep 17 00:00:00 2001 From: uermel Date: Tue, 7 Jul 2026 16:35:43 -0700 Subject: [PATCH] normalize more CLI flags. --- src/copick_utils/cli/fit_spline.py | 38 +++++++++++++--- src/copick_utils/cli/picks2mesh.py | 28 +++++++++++- src/copick_utils/cli/thickness_filter.py | 33 ++++++++++++-- src/copick_utils/cli/util.py | 35 ++++++++++----- tests/test_flag_normalization.py | 57 ++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 tests/test_flag_normalization.py diff --git a/src/copick_utils/cli/fit_spline.py b/src/copick_utils/cli/fit_spline.py index 2cd4647..03961e6 100644 --- a/src/copick_utils/cli/fit_spline.py +++ b/src/copick_utils/cli/fit_spline.py @@ -1,7 +1,12 @@ import click import copick from click_option_group import optgroup -from copick.cli.util import add_config_option, add_debug_option, add_run_names_option +from copick.cli.util import ( + add_config_option, + add_debug_option, + add_run_names_option, + resolve_deprecated_option, +) from copick.util.log import get_logger from copick.util.uri import parse_copick_uri @@ -18,12 +23,15 @@ @add_run_names_option @optgroup.group("\nInput Options", help="Options related to the input segmentation.") @add_input_option("segmentation") +# TODO:remove once deprecation takes effect -- legacy --voxel-spacing/-vs override (vs now comes from the input URI) @optgroup.option( "--voxel-spacing", "-vs", type=float, - required=True, - help="Voxel spacing for coordinate scaling.", + required=False, + default=None, + hidden=True, + help="Deprecated: the @voxel_spacing in -i/--input is used instead.", ) @optgroup.group("\nTool Options", help="Options related to this tool.") @optgroup.option( @@ -111,14 +119,14 @@ def fit_spline( Examples: \b - # Fit splines to skeletonized components + # Fit splines to skeletonized components (voxel spacing from the @10.0 in -i) copick process fit_spline -i "skeleton:skel/inst-.*@10.0" \\ - -o "skeleton:spline/spline-{input_session_id}" --spacing-distance 4.4 --voxel-spacing 10.0 + -o "skeleton:spline/spline-{input_session_id}" --spacing-distance 4.4 \b # Process a single skeleton component copick process fit_spline -i "skeleton:skel/skel-0@10.0" \\ - -o "skeleton:spline/spline-0" --spacing-distance 2.0 --voxel-spacing 10.0 + -o "skeleton:spline/spline-0" --spacing-distance 2.0 See Also: @@ -147,6 +155,24 @@ def fit_spline( # Extract parameters for logging only input_params = parse_copick_uri(input_uri, "segmentation") + + # Voxel spacing comes from the @voxel_spacing in the input URI; the legacy + # --voxel-spacing/-vs flag remains a deprecated override. + uri_vs_raw = input_params.get("voxel_spacing") + uri_vs = float(uri_vs_raw) if uri_vs_raw not in (None, "*") else None + # TODO:remove once deprecation takes effect -- legacy -vs override (drop the voxel_spacing param; use uri_vs directly) + voxel_spacing = resolve_deprecated_option( + uri_vs, + voxel_spacing, + old_flag="--voxel-spacing/-vs", + new_flag="the @voxel_spacing in -i/--input", + logger=logger, + ) + if voxel_spacing is None: + raise click.BadParameter( + "Input URI must include a specific voxel spacing (e.g., @10.0), or pass --voxel-spacing/-vs.", + ) + logger.info(f"Fitting splines to segmentations '{input_params['name']}'") logger.info( f"Source segmentation pattern: {input_params['name']} ({input_params['user_id']}/{input_params['session_id']})", diff --git a/src/copick_utils/cli/picks2mesh.py b/src/copick_utils/cli/picks2mesh.py index e1e656f..af11cb1 100644 --- a/src/copick_utils/cli/picks2mesh.py +++ b/src/copick_utils/cli/picks2mesh.py @@ -1,7 +1,12 @@ import click import copick from click_option_group import optgroup -from copick.cli.util import add_config_option, add_debug_option, add_run_names_option +from copick.cli.util import ( + add_config_option, + add_debug_option, + add_run_names_option, + resolve_deprecated_option, +) from copick.util.log import get_logger from copick.util.uri import parse_copick_uri @@ -26,11 +31,20 @@ @optgroup.group("\nTool Options", help="Options related to this tool.") @optgroup.option( "--mesh-type", - "-t", + "-mt", type=click.Choice(["convex_hull", "alpha_shape"]), default="convex_hull", help="Type of mesh to create.", ) +# TODO:remove once deprecation takes effect -- legacy -t alias (reserve -t for --tomogram) +@optgroup.option( + "-t", + "legacy_mesh_type", + type=click.Choice(["convex_hull", "alpha_shape"]), + default=None, + hidden=True, + help="Deprecated: use -mt/--mesh-type instead.", +) @optgroup.option( "--alpha", "-a", @@ -54,6 +68,7 @@ def picks2mesh( run_names, input_uri, mesh_type, + legacy_mesh_type, alpha, use_clustering, clustering_method, @@ -114,6 +129,15 @@ def picks2mesh( logger = get_logger(__name__, debug=debug) + # TODO:remove once deprecation takes effect -- legacy -t alias resolution (also drop the legacy_mesh_type param) + mesh_type = resolve_deprecated_option( + mesh_type, + legacy_mesh_type, + old_flag="-t", + new_flag="-mt/--mesh-type", + logger=logger, + ) + root = copick.from_file(config) run_names_list = list(run_names) if run_names else None diff --git a/src/copick_utils/cli/thickness_filter.py b/src/copick_utils/cli/thickness_filter.py index 5338ac7..1105910 100644 --- a/src/copick_utils/cli/thickness_filter.py +++ b/src/copick_utils/cli/thickness_filter.py @@ -3,7 +3,12 @@ import click import copick from click_option_group import optgroup -from copick.cli.util import add_config_option, add_debug_option, add_run_names_option +from copick.cli.util import ( + add_config_option, + add_debug_option, + add_run_names_option, + resolve_deprecated_option, +) from copick.util.log import get_logger from copick.util.uri import parse_copick_uri @@ -23,11 +28,21 @@ @optgroup.group("\nTool Options", help="Options related to this tool.") @optgroup.option( "--min-thickness", - "-t", + "-mt", type=float, - required=True, + required=False, + default=None, help="Minimum thickness (diameter) of structures to keep. Unit set by --thickness-unit.", ) +# TODO:remove once deprecation takes effect -- legacy -t alias (reserve -t for --tomogram); also restore --min-thickness required=True +@optgroup.option( + "-t", + "legacy_min_thickness", + type=float, + default=None, + hidden=True, + help="Deprecated: use -mt/--min-thickness instead.", +) @optgroup.option( "--thickness-unit", type=click.Choice(["angstrom", "voxel"]), @@ -43,6 +58,7 @@ def thickness_filter( run_names, input_uri, min_thickness, + legacy_min_thickness, thickness_unit, workers, output_uri, @@ -88,6 +104,17 @@ def thickness_filter( logger = get_logger(__name__, debug=debug) + # TODO:remove once deprecation takes effect -- legacy -t alias resolution (drop legacy_min_thickness + None-check) + min_thickness = resolve_deprecated_option( + min_thickness, + legacy_min_thickness, + old_flag="-t", + new_flag="-mt/--min-thickness", + logger=logger, + ) + if min_thickness is None: + raise click.BadParameter("Missing required option: -mt/--min-thickness.") + root = copick.from_file(config) run_names_list = list(run_names) if run_names else None diff --git a/src/copick_utils/cli/util.py b/src/copick_utils/cli/util.py index 5519ef9..aa0b262 100644 --- a/src/copick_utils/cli/util.py +++ b/src/copick_utils/cli/util.py @@ -335,17 +335,20 @@ def add_distance_options(func: click.Command) -> click.Command: # ============================================================================ -def add_input_option(object_type: str, func: click.Command = None) -> Callable: +def add_input_option(object_type: str, func: click.Command = None, required: bool = True) -> Callable: """ Add --input/-i option for URI-based input selection. Supports copick URI format with pattern matching: - Picks/Meshes: object_name:user_id/session_id - Segmentations: name:user_id/session_id@voxel_spacing?multilabel=true + - Tomograms: tomo_type@voxel_spacing Args: - object_type (str): Type of object ('picks', 'mesh', 'segmentation'). + object_type (str): Type of object ('picks', 'mesh', 'segmentation', 'tomogram'). func (click.Command, optional): The Click command to which the option will be added. + required (bool): Whether the option is required. Default is True. Pass False for + commands that keep deprecated fallback flags (e.g. --tomo-alg/--voxel-size). Returns: Callable: The Click command with the input option added. @@ -358,6 +361,7 @@ def add_input_option_decorator(_func: click.Command) -> click.Command: "picks": "object_name:user_id/session_id", "mesh": "object_name:user_id/session_id", "segmentation": "name:user_id/session_id@voxel_spacing", + "tomogram": "tomo_type@voxel_spacing", } help_text = ( @@ -369,7 +373,7 @@ def add_input_option_decorator(_func: click.Command) -> click.Command: "-i", "input_uri", type=CopickURI(object_type, "input"), - required=True, + required=required, help=help_text, ) return opt(_func) @@ -380,7 +384,12 @@ def add_input_option_decorator(_func: click.Command) -> click.Command: return add_input_option_decorator(func) -def add_output_option(object_type: str, func: click.Command = None, default_tool: str = None) -> Callable: +def add_output_option( + object_type: str, + func: click.Command = None, + default_tool: str = None, + required: bool = True, +) -> Callable: """ Add --output/-o option for URI-based output specification with smart defaults. @@ -411,21 +420,25 @@ def add_output_option_decorator(_func: click.Command) -> click.Command: "picks": '"ribosome", "ribosome/my-session", or "/my-session"', "mesh": '"membrane", "membrane/my-session", or "/my-session"', "segmentation": '"membrane", "membrane/my-session", or "/my-session"', + "tomogram": '"wbp@20.0" or "denoised@10.0"', } - voxel_suffix = "@voxel_spacing" if object_type == "segmentation" else "" - help_text = ( - f"Output {object_type} URI. " - f"Supports smart defaults (e.g., {shorthand_examples.get(object_type, 'shorthand')}). " - f"Full format: object_name:user_id/session_id{voxel_suffix}." - ) + voxel_suffix = "@voxel_spacing" if object_type in ("segmentation", "tomogram") else "" + if object_type == "tomogram": + help_text = f"Output tomogram URI (format: tomo_type{voxel_suffix}). Example: 'wbp@20.0'." + else: + help_text = ( + f"Output {object_type} URI. " + f"Supports smart defaults (e.g., {shorthand_examples.get(object_type, 'shorthand')}). " + f"Full format: object_name:user_id/session_id{voxel_suffix}." + ) opt = optgroup.option( "--output", "-o", "output_uri", type=CopickURI(object_type, "output"), - required=True, + required=required, help=help_text, ) return opt(_func) diff --git a/tests/test_flag_normalization.py b/tests/test_flag_normalization.py new file mode 100644 index 0000000..92c09e8 --- /dev/null +++ b/tests/test_flag_normalization.py @@ -0,0 +1,57 @@ +"""Tests for the Part 2 CLI flag normalization in copick-utils. + +- picks2mesh / thickness_filter: `-t` is reserved for tomogram refs; their old + `-t` short flag moved to `-mt` with a hidden `-t` deprecated alias. +- fit_spline: voxel spacing now comes from the input URI's `@voxel_spacing`; the + `--voxel-spacing/-vs` flag is a hidden deprecated override. +""" + +import pytest +from click.testing import CliRunner + + +@pytest.fixture +def runner(): + return CliRunner() + + +def _hidden_alias(cmd, flag): + param = next((p for p in cmd.params if flag in getattr(p, "opts", [])), None) + return param, (param.hidden if param else None) + + +def test_picks2mesh_reserves_t_for_tomogram(runner): + from copick_utils.cli.picks2mesh import picks2mesh + + out = runner.invoke(picks2mesh, ["--help"]).output + assert "--mesh-type" in out and "-mt" in out + assert "-t " not in out # -t no longer shown (reserved for --tomogram) + + param, hidden = _hidden_alias(picks2mesh, "-t") + assert param is not None and hidden is True + assert param.name == "legacy_mesh_type" + + +def test_thickness_filter_reserves_t_for_tomogram(runner): + from copick_utils.cli.thickness_filter import thickness_filter + + out = runner.invoke(thickness_filter, ["--help"]).output + assert "--min-thickness" in out and "-mt" in out + assert "-t " not in out + + param, hidden = _hidden_alias(thickness_filter, "-t") + assert param is not None and hidden is True + assert param.name == "legacy_min_thickness" + + +def test_fit_spline_derives_vs_from_uri(runner): + from copick_utils.cli.fit_spline import fit_spline + + out = runner.invoke(fit_spline, ["--help"]).output + assert "-i" in out and "--input" in out + # voxel spacing is derived from the input URI; the flag is hidden + deprecated + assert "--voxel-spacing" not in out + assert "-vs" not in out + + param, hidden = _hidden_alias(fit_spline, "--voxel-spacing") + assert param is not None and hidden is True