Make the mutually exclusive options helper function print the correct parameter name
Marco Ricci

Marco Ricci commited on 2024-12-25 18:59:32
Zeige 1 geänderte Dateien mit 16 Einfügungen und 3 Löschungen.


The `click.Parameter.human_readable_name` attribute yields the metavar
for arguments, and the function parameter name for options.  The
documentation calls the latter the "name" for the option, perhaps under
the assumption that you would never pass an explicit parameter name to
the option decorator.  I disagree with this, it is horrible design:
sometimes the value is usable as a function parameter (options),
sometimes it isn't (arguments), and vice versa for constructing
a command-line, so it is neither usable as a command-line parameter
display name nor as a function parameter name.

So what *is* it even good for...?

Include our own logic to select an appropriate display name for our
options.  This requires querying `click.Parameter` attributes that
should be public, but are internal.  Why...?!
... ...
@@ -1908,6 +1908,17 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
1908 1908
     def is_param_set(param: click.Parameter) -> bool:
1909 1909
         return bool(ctx.params.get(param.human_readable_name))
1910 1910
 
1911
+    def option_name(param: click.Parameter | str) -> str:
1912
+        # Annoyingly, `param.human_readable_name` contains the *function*
1913
+        # parameter name, not the list of option names.  *Those* are
1914
+        # stashed in the `.opts` and `.secondary_opts` attributes, which
1915
+        # are visible in the `.to_info_dict()` output, but not otherwise
1916
+        # documented.
1917
+        param = params_by_str[param] if isinstance(param, str) else param
1918
+        names = [param.human_readable_name, *param.opts, *param.secondary_opts]
1919
+        option_names = [n for n in names if n.startswith('--')]
1920
+        return min(option_names, key=len)
1921
+
1911 1922
     def check_incompatible_options(
1912 1923
         param1: click.Parameter | str,
1913 1924
         param2: click.Parameter | str,
... ...
@@ -1919,10 +1930,12 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
1919 1930
         if not is_param_set(param1):
1920 1931
             return
1921 1932
         if is_param_set(param2):
1922
-            param1_str = param1.human_readable_name
1923
-            param2_str = param2.human_readable_name
1933
+            param1_str = option_name(param1)
1934
+            param2_str = option_name(param2)
1924 1935
             raise click.BadOptionUsage(
1925
-                param1_str, f'mutually exclusive with {param2_str}', ctx=ctx
1936
+                param1_str,
1937
+                f'{param1_str} is mutually exclusive with {param2_str}',
1938
+                ctx=ctx,
1926 1939
             )
1927 1940
         return
1928 1941
 
1929 1942