outerbounds 0.3.68__py3-none-any.whl → 0.3.104__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. outerbounds/_vendor/PyYAML.LICENSE +20 -0
  2. outerbounds/_vendor/__init__.py +0 -0
  3. outerbounds/_vendor/_yaml/__init__.py +34 -0
  4. outerbounds/_vendor/click/__init__.py +73 -0
  5. outerbounds/_vendor/click/_compat.py +626 -0
  6. outerbounds/_vendor/click/_termui_impl.py +717 -0
  7. outerbounds/_vendor/click/_textwrap.py +49 -0
  8. outerbounds/_vendor/click/_winconsole.py +279 -0
  9. outerbounds/_vendor/click/core.py +2998 -0
  10. outerbounds/_vendor/click/decorators.py +497 -0
  11. outerbounds/_vendor/click/exceptions.py +287 -0
  12. outerbounds/_vendor/click/formatting.py +301 -0
  13. outerbounds/_vendor/click/globals.py +68 -0
  14. outerbounds/_vendor/click/parser.py +529 -0
  15. outerbounds/_vendor/click/py.typed +0 -0
  16. outerbounds/_vendor/click/shell_completion.py +580 -0
  17. outerbounds/_vendor/click/termui.py +787 -0
  18. outerbounds/_vendor/click/testing.py +479 -0
  19. outerbounds/_vendor/click/types.py +1073 -0
  20. outerbounds/_vendor/click/utils.py +580 -0
  21. outerbounds/_vendor/click.LICENSE +28 -0
  22. outerbounds/_vendor/vendor_any.txt +2 -0
  23. outerbounds/_vendor/yaml/__init__.py +471 -0
  24. outerbounds/_vendor/yaml/_yaml.cpython-311-darwin.so +0 -0
  25. outerbounds/_vendor/yaml/composer.py +146 -0
  26. outerbounds/_vendor/yaml/constructor.py +862 -0
  27. outerbounds/_vendor/yaml/cyaml.py +177 -0
  28. outerbounds/_vendor/yaml/dumper.py +138 -0
  29. outerbounds/_vendor/yaml/emitter.py +1239 -0
  30. outerbounds/_vendor/yaml/error.py +94 -0
  31. outerbounds/_vendor/yaml/events.py +104 -0
  32. outerbounds/_vendor/yaml/loader.py +62 -0
  33. outerbounds/_vendor/yaml/nodes.py +51 -0
  34. outerbounds/_vendor/yaml/parser.py +629 -0
  35. outerbounds/_vendor/yaml/reader.py +208 -0
  36. outerbounds/_vendor/yaml/representer.py +378 -0
  37. outerbounds/_vendor/yaml/resolver.py +245 -0
  38. outerbounds/_vendor/yaml/scanner.py +1555 -0
  39. outerbounds/_vendor/yaml/serializer.py +127 -0
  40. outerbounds/_vendor/yaml/tokens.py +129 -0
  41. outerbounds/command_groups/apps_cli.py +586 -0
  42. outerbounds/command_groups/cli.py +9 -5
  43. outerbounds/command_groups/local_setup_cli.py +1 -5
  44. outerbounds/command_groups/perimeters_cli.py +198 -25
  45. outerbounds/command_groups/tutorials_cli.py +111 -0
  46. outerbounds/command_groups/workstations_cli.py +2 -2
  47. outerbounds/utils/kubeconfig.py +2 -2
  48. outerbounds/utils/metaflowconfig.py +68 -9
  49. outerbounds/utils/schema.py +2 -2
  50. outerbounds/utils/utils.py +19 -0
  51. outerbounds/vendor.py +159 -0
  52. {outerbounds-0.3.68.dist-info → outerbounds-0.3.104.dist-info}/METADATA +14 -7
  53. outerbounds-0.3.104.dist-info/RECORD +59 -0
  54. {outerbounds-0.3.68.dist-info → outerbounds-0.3.104.dist-info}/WHEEL +1 -1
  55. outerbounds-0.3.68.dist-info/RECORD +0 -15
  56. {outerbounds-0.3.68.dist-info → outerbounds-0.3.104.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1073 @@
1
+ import os
2
+ import stat
3
+ import typing as t
4
+ from datetime import datetime
5
+ from gettext import gettext as _
6
+ from gettext import ngettext
7
+
8
+ from ._compat import _get_argv_encoding
9
+ from ._compat import get_filesystem_encoding
10
+ from ._compat import open_stream
11
+ from .exceptions import BadParameter
12
+ from .utils import LazyFile
13
+ from .utils import safecall
14
+
15
+ if t.TYPE_CHECKING:
16
+ import typing_extensions as te
17
+ from .core import Context
18
+ from .core import Parameter
19
+ from .shell_completion import CompletionItem
20
+
21
+
22
+ class ParamType:
23
+ """Represents the type of a parameter. Validates and converts values
24
+ from the command line or Python into the correct type.
25
+
26
+ To implement a custom type, subclass and implement at least the
27
+ following:
28
+
29
+ - The :attr:`name` class attribute must be set.
30
+ - Calling an instance of the type with ``None`` must return
31
+ ``None``. This is already implemented by default.
32
+ - :meth:`convert` must convert string values to the correct type.
33
+ - :meth:`convert` must accept values that are already the correct
34
+ type.
35
+ - It must be able to convert a value if the ``ctx`` and ``param``
36
+ arguments are ``None``. This can occur when converting prompt
37
+ input.
38
+ """
39
+
40
+ is_composite: t.ClassVar[bool] = False
41
+ arity: t.ClassVar[int] = 1
42
+
43
+ #: the descriptive name of this type
44
+ name: str
45
+
46
+ #: if a list of this type is expected and the value is pulled from a
47
+ #: string environment variable, this is what splits it up. `None`
48
+ #: means any whitespace. For all parameters the general rule is that
49
+ #: whitespace splits them up. The exception are paths and files which
50
+ #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
51
+ #: Windows).
52
+ envvar_list_splitter: t.ClassVar[t.Optional[str]] = None
53
+
54
+ def to_info_dict(self) -> t.Dict[str, t.Any]:
55
+ """Gather information that could be useful for a tool generating
56
+ user-facing documentation.
57
+
58
+ Use :meth:`click.Context.to_info_dict` to traverse the entire
59
+ CLI structure.
60
+
61
+ .. versionadded:: 8.0
62
+ """
63
+ # The class name without the "ParamType" suffix.
64
+ param_type = type(self).__name__.partition("ParamType")[0]
65
+ param_type = param_type.partition("ParameterType")[0]
66
+
67
+ # Custom subclasses might not remember to set a name.
68
+ if hasattr(self, "name"):
69
+ name = self.name
70
+ else:
71
+ name = param_type
72
+
73
+ return {"param_type": param_type, "name": name}
74
+
75
+ def __call__(
76
+ self,
77
+ value: t.Any,
78
+ param: t.Optional["Parameter"] = None,
79
+ ctx: t.Optional["Context"] = None,
80
+ ) -> t.Any:
81
+ if value is not None:
82
+ return self.convert(value, param, ctx)
83
+
84
+ def get_metavar(self, param: "Parameter") -> t.Optional[str]:
85
+ """Returns the metavar default for this param if it provides one."""
86
+
87
+ def get_missing_message(self, param: "Parameter") -> t.Optional[str]:
88
+ """Optionally might return extra information about a missing
89
+ parameter.
90
+
91
+ .. versionadded:: 2.0
92
+ """
93
+
94
+ def convert(
95
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
96
+ ) -> t.Any:
97
+ """Convert the value to the correct type. This is not called if
98
+ the value is ``None`` (the missing value).
99
+
100
+ This must accept string values from the command line, as well as
101
+ values that are already the correct type. It may also convert
102
+ other compatible types.
103
+
104
+ The ``param`` and ``ctx`` arguments may be ``None`` in certain
105
+ situations, such as when converting prompt input.
106
+
107
+ If the value cannot be converted, call :meth:`fail` with a
108
+ descriptive message.
109
+
110
+ :param value: The value to convert.
111
+ :param param: The parameter that is using this type to convert
112
+ its value. May be ``None``.
113
+ :param ctx: The current context that arrived at this value. May
114
+ be ``None``.
115
+ """
116
+ return value
117
+
118
+ def split_envvar_value(self, rv: str) -> t.Sequence[str]:
119
+ """Given a value from an environment variable this splits it up
120
+ into small chunks depending on the defined envvar list splitter.
121
+
122
+ If the splitter is set to `None`, which means that whitespace splits,
123
+ then leading and trailing whitespace is ignored. Otherwise, leading
124
+ and trailing splitters usually lead to empty items being included.
125
+ """
126
+ return (rv or "").split(self.envvar_list_splitter)
127
+
128
+ def fail(
129
+ self,
130
+ message: str,
131
+ param: t.Optional["Parameter"] = None,
132
+ ctx: t.Optional["Context"] = None,
133
+ ) -> "t.NoReturn":
134
+ """Helper method to fail with an invalid value message."""
135
+ raise BadParameter(message, ctx=ctx, param=param)
136
+
137
+ def shell_complete(
138
+ self, ctx: "Context", param: "Parameter", incomplete: str
139
+ ) -> t.List["CompletionItem"]:
140
+ """Return a list of
141
+ :class:`~click.shell_completion.CompletionItem` objects for the
142
+ incomplete value. Most types do not provide completions, but
143
+ some do, and this allows custom types to provide custom
144
+ completions as well.
145
+
146
+ :param ctx: Invocation context for this command.
147
+ :param param: The parameter that is requesting completion.
148
+ :param incomplete: Value being completed. May be empty.
149
+
150
+ .. versionadded:: 8.0
151
+ """
152
+ return []
153
+
154
+
155
+ class CompositeParamType(ParamType):
156
+ is_composite = True
157
+
158
+ @property
159
+ def arity(self) -> int: # type: ignore
160
+ raise NotImplementedError()
161
+
162
+
163
+ class FuncParamType(ParamType):
164
+ def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None:
165
+ self.name = func.__name__
166
+ self.func = func
167
+
168
+ def to_info_dict(self) -> t.Dict[str, t.Any]:
169
+ info_dict = super().to_info_dict()
170
+ info_dict["func"] = self.func
171
+ return info_dict
172
+
173
+ def convert(
174
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
175
+ ) -> t.Any:
176
+ try:
177
+ return self.func(value)
178
+ except ValueError:
179
+ try:
180
+ value = str(value)
181
+ except UnicodeError:
182
+ value = value.decode("utf-8", "replace")
183
+
184
+ self.fail(value, param, ctx)
185
+
186
+
187
+ class UnprocessedParamType(ParamType):
188
+ name = "text"
189
+
190
+ def convert(
191
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
192
+ ) -> t.Any:
193
+ return value
194
+
195
+ def __repr__(self) -> str:
196
+ return "UNPROCESSED"
197
+
198
+
199
+ class StringParamType(ParamType):
200
+ name = "text"
201
+
202
+ def convert(
203
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
204
+ ) -> t.Any:
205
+ if isinstance(value, bytes):
206
+ enc = _get_argv_encoding()
207
+ try:
208
+ value = value.decode(enc)
209
+ except UnicodeError:
210
+ fs_enc = get_filesystem_encoding()
211
+ if fs_enc != enc:
212
+ try:
213
+ value = value.decode(fs_enc)
214
+ except UnicodeError:
215
+ value = value.decode("utf-8", "replace")
216
+ else:
217
+ value = value.decode("utf-8", "replace")
218
+ return value
219
+ return str(value)
220
+
221
+ def __repr__(self) -> str:
222
+ return "STRING"
223
+
224
+
225
+ class Choice(ParamType):
226
+ """The choice type allows a value to be checked against a fixed set
227
+ of supported values. All of these values have to be strings.
228
+
229
+ You should only pass a list or tuple of choices. Other iterables
230
+ (like generators) may lead to surprising results.
231
+
232
+ The resulting value will always be one of the originally passed choices
233
+ regardless of ``case_sensitive`` or any ``ctx.token_normalize_func``
234
+ being specified.
235
+
236
+ See :ref:`choice-opts` for an example.
237
+
238
+ :param case_sensitive: Set to false to make choices case
239
+ insensitive. Defaults to true.
240
+ """
241
+
242
+ name = "choice"
243
+
244
+ def __init__(self, choices: t.Sequence[str], case_sensitive: bool = True) -> None:
245
+ self.choices = choices
246
+ self.case_sensitive = case_sensitive
247
+
248
+ def to_info_dict(self) -> t.Dict[str, t.Any]:
249
+ info_dict = super().to_info_dict()
250
+ info_dict["choices"] = self.choices
251
+ info_dict["case_sensitive"] = self.case_sensitive
252
+ return info_dict
253
+
254
+ def get_metavar(self, param: "Parameter") -> str:
255
+ choices_str = "|".join(self.choices)
256
+
257
+ # Use curly braces to indicate a required argument.
258
+ if param.required and param.param_type_name == "argument":
259
+ return f"{{{choices_str}}}"
260
+
261
+ # Use square braces to indicate an option or optional argument.
262
+ return f"[{choices_str}]"
263
+
264
+ def get_missing_message(self, param: "Parameter") -> str:
265
+ return _("Choose from:\n\t{choices}").format(choices=",\n\t".join(self.choices))
266
+
267
+ def convert(
268
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
269
+ ) -> t.Any:
270
+ # Match through normalization and case sensitivity
271
+ # first do token_normalize_func, then lowercase
272
+ # preserve original `value` to produce an accurate message in
273
+ # `self.fail`
274
+ normed_value = value
275
+ normed_choices = {choice: choice for choice in self.choices}
276
+
277
+ if ctx is not None and ctx.token_normalize_func is not None:
278
+ normed_value = ctx.token_normalize_func(value)
279
+ normed_choices = {
280
+ ctx.token_normalize_func(normed_choice): original
281
+ for normed_choice, original in normed_choices.items()
282
+ }
283
+
284
+ if not self.case_sensitive:
285
+ normed_value = normed_value.casefold()
286
+ normed_choices = {
287
+ normed_choice.casefold(): original
288
+ for normed_choice, original in normed_choices.items()
289
+ }
290
+
291
+ if normed_value in normed_choices:
292
+ return normed_choices[normed_value]
293
+
294
+ choices_str = ", ".join(map(repr, self.choices))
295
+ self.fail(
296
+ ngettext(
297
+ "{value!r} is not {choice}.",
298
+ "{value!r} is not one of {choices}.",
299
+ len(self.choices),
300
+ ).format(value=value, choice=choices_str, choices=choices_str),
301
+ param,
302
+ ctx,
303
+ )
304
+
305
+ def __repr__(self) -> str:
306
+ return f"Choice({list(self.choices)})"
307
+
308
+ def shell_complete(
309
+ self, ctx: "Context", param: "Parameter", incomplete: str
310
+ ) -> t.List["CompletionItem"]:
311
+ """Complete choices that start with the incomplete value.
312
+
313
+ :param ctx: Invocation context for this command.
314
+ :param param: The parameter that is requesting completion.
315
+ :param incomplete: Value being completed. May be empty.
316
+
317
+ .. versionadded:: 8.0
318
+ """
319
+ from outerbounds._vendor.click.shell_completion import CompletionItem
320
+
321
+ str_choices = map(str, self.choices)
322
+
323
+ if self.case_sensitive:
324
+ matched = (c for c in str_choices if c.startswith(incomplete))
325
+ else:
326
+ incomplete = incomplete.lower()
327
+ matched = (c for c in str_choices if c.lower().startswith(incomplete))
328
+
329
+ return [CompletionItem(c) for c in matched]
330
+
331
+
332
+ class DateTime(ParamType):
333
+ """The DateTime type converts date strings into `datetime` objects.
334
+
335
+ The format strings which are checked are configurable, but default to some
336
+ common (non-timezone aware) ISO 8601 formats.
337
+
338
+ When specifying *DateTime* formats, you should only pass a list or a tuple.
339
+ Other iterables, like generators, may lead to surprising results.
340
+
341
+ The format strings are processed using ``datetime.strptime``, and this
342
+ consequently defines the format strings which are allowed.
343
+
344
+ Parsing is tried using each format, in order, and the first format which
345
+ parses successfully is used.
346
+
347
+ :param formats: A list or tuple of date format strings, in the order in
348
+ which they should be tried. Defaults to
349
+ ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``,
350
+ ``'%Y-%m-%d %H:%M:%S'``.
351
+ """
352
+
353
+ name = "datetime"
354
+
355
+ def __init__(self, formats: t.Optional[t.Sequence[str]] = None):
356
+ self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
357
+
358
+ def to_info_dict(self) -> t.Dict[str, t.Any]:
359
+ info_dict = super().to_info_dict()
360
+ info_dict["formats"] = self.formats
361
+ return info_dict
362
+
363
+ def get_metavar(self, param: "Parameter") -> str:
364
+ return f"[{'|'.join(self.formats)}]"
365
+
366
+ def _try_to_convert_date(self, value: t.Any, format: str) -> t.Optional[datetime]:
367
+ try:
368
+ return datetime.strptime(value, format)
369
+ except ValueError:
370
+ return None
371
+
372
+ def convert(
373
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
374
+ ) -> t.Any:
375
+ if isinstance(value, datetime):
376
+ return value
377
+
378
+ for format in self.formats:
379
+ converted = self._try_to_convert_date(value, format)
380
+
381
+ if converted is not None:
382
+ return converted
383
+
384
+ formats_str = ", ".join(map(repr, self.formats))
385
+ self.fail(
386
+ ngettext(
387
+ "{value!r} does not match the format {format}.",
388
+ "{value!r} does not match the formats {formats}.",
389
+ len(self.formats),
390
+ ).format(value=value, format=formats_str, formats=formats_str),
391
+ param,
392
+ ctx,
393
+ )
394
+
395
+ def __repr__(self) -> str:
396
+ return "DateTime"
397
+
398
+
399
+ class _NumberParamTypeBase(ParamType):
400
+ _number_class: t.ClassVar[t.Type]
401
+
402
+ def convert(
403
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
404
+ ) -> t.Any:
405
+ try:
406
+ return self._number_class(value)
407
+ except ValueError:
408
+ self.fail(
409
+ _("{value!r} is not a valid {number_type}.").format(
410
+ value=value, number_type=self.name
411
+ ),
412
+ param,
413
+ ctx,
414
+ )
415
+
416
+
417
+ class _NumberRangeBase(_NumberParamTypeBase):
418
+ def __init__(
419
+ self,
420
+ min: t.Optional[float] = None,
421
+ max: t.Optional[float] = None,
422
+ min_open: bool = False,
423
+ max_open: bool = False,
424
+ clamp: bool = False,
425
+ ) -> None:
426
+ self.min = min
427
+ self.max = max
428
+ self.min_open = min_open
429
+ self.max_open = max_open
430
+ self.clamp = clamp
431
+
432
+ def to_info_dict(self) -> t.Dict[str, t.Any]:
433
+ info_dict = super().to_info_dict()
434
+ info_dict.update(
435
+ min=self.min,
436
+ max=self.max,
437
+ min_open=self.min_open,
438
+ max_open=self.max_open,
439
+ clamp=self.clamp,
440
+ )
441
+ return info_dict
442
+
443
+ def convert(
444
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
445
+ ) -> t.Any:
446
+ import operator
447
+
448
+ rv = super().convert(value, param, ctx)
449
+ lt_min: bool = self.min is not None and (
450
+ operator.le if self.min_open else operator.lt
451
+ )(rv, self.min)
452
+ gt_max: bool = self.max is not None and (
453
+ operator.ge if self.max_open else operator.gt
454
+ )(rv, self.max)
455
+
456
+ if self.clamp:
457
+ if lt_min:
458
+ return self._clamp(self.min, 1, self.min_open) # type: ignore
459
+
460
+ if gt_max:
461
+ return self._clamp(self.max, -1, self.max_open) # type: ignore
462
+
463
+ if lt_min or gt_max:
464
+ self.fail(
465
+ _("{value} is not in the range {range}.").format(
466
+ value=rv, range=self._describe_range()
467
+ ),
468
+ param,
469
+ ctx,
470
+ )
471
+
472
+ return rv
473
+
474
+ def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float:
475
+ """Find the valid value to clamp to bound in the given
476
+ direction.
477
+
478
+ :param bound: The boundary value.
479
+ :param dir: 1 or -1 indicating the direction to move.
480
+ :param open: If true, the range does not include the bound.
481
+ """
482
+ raise NotImplementedError
483
+
484
+ def _describe_range(self) -> str:
485
+ """Describe the range for use in help text."""
486
+ if self.min is None:
487
+ op = "<" if self.max_open else "<="
488
+ return f"x{op}{self.max}"
489
+
490
+ if self.max is None:
491
+ op = ">" if self.min_open else ">="
492
+ return f"x{op}{self.min}"
493
+
494
+ lop = "<" if self.min_open else "<="
495
+ rop = "<" if self.max_open else "<="
496
+ return f"{self.min}{lop}x{rop}{self.max}"
497
+
498
+ def __repr__(self) -> str:
499
+ clamp = " clamped" if self.clamp else ""
500
+ return f"<{type(self).__name__} {self._describe_range()}{clamp}>"
501
+
502
+
503
+ class IntParamType(_NumberParamTypeBase):
504
+ name = "integer"
505
+ _number_class = int
506
+
507
+ def __repr__(self) -> str:
508
+ return "INT"
509
+
510
+
511
+ class IntRange(_NumberRangeBase, IntParamType):
512
+ """Restrict an :data:`click.INT` value to a range of accepted
513
+ values. See :ref:`ranges`.
514
+
515
+ If ``min`` or ``max`` are not passed, any value is accepted in that
516
+ direction. If ``min_open`` or ``max_open`` are enabled, the
517
+ corresponding boundary is not included in the range.
518
+
519
+ If ``clamp`` is enabled, a value outside the range is clamped to the
520
+ boundary instead of failing.
521
+
522
+ .. versionchanged:: 8.0
523
+ Added the ``min_open`` and ``max_open`` parameters.
524
+ """
525
+
526
+ name = "integer range"
527
+
528
+ def _clamp( # type: ignore
529
+ self, bound: int, dir: "te.Literal[1, -1]", open: bool
530
+ ) -> int:
531
+ if not open:
532
+ return bound
533
+
534
+ return bound + dir
535
+
536
+
537
+ class FloatParamType(_NumberParamTypeBase):
538
+ name = "float"
539
+ _number_class = float
540
+
541
+ def __repr__(self) -> str:
542
+ return "FLOAT"
543
+
544
+
545
+ class FloatRange(_NumberRangeBase, FloatParamType):
546
+ """Restrict a :data:`click.FLOAT` value to a range of accepted
547
+ values. See :ref:`ranges`.
548
+
549
+ If ``min`` or ``max`` are not passed, any value is accepted in that
550
+ direction. If ``min_open`` or ``max_open`` are enabled, the
551
+ corresponding boundary is not included in the range.
552
+
553
+ If ``clamp`` is enabled, a value outside the range is clamped to the
554
+ boundary instead of failing. This is not supported if either
555
+ boundary is marked ``open``.
556
+
557
+ .. versionchanged:: 8.0
558
+ Added the ``min_open`` and ``max_open`` parameters.
559
+ """
560
+
561
+ name = "float range"
562
+
563
+ def __init__(
564
+ self,
565
+ min: t.Optional[float] = None,
566
+ max: t.Optional[float] = None,
567
+ min_open: bool = False,
568
+ max_open: bool = False,
569
+ clamp: bool = False,
570
+ ) -> None:
571
+ super().__init__(
572
+ min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp
573
+ )
574
+
575
+ if (min_open or max_open) and clamp:
576
+ raise TypeError("Clamping is not supported for open bounds.")
577
+
578
+ def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float:
579
+ if not open:
580
+ return bound
581
+
582
+ # Could use Python 3.9's math.nextafter here, but clamping an
583
+ # open float range doesn't seem to be particularly useful. It's
584
+ # left up to the user to write a callback to do it if needed.
585
+ raise RuntimeError("Clamping is not supported for open bounds.")
586
+
587
+
588
+ class BoolParamType(ParamType):
589
+ name = "boolean"
590
+
591
+ def convert(
592
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
593
+ ) -> t.Any:
594
+ if value in {False, True}:
595
+ return bool(value)
596
+
597
+ norm = value.strip().lower()
598
+
599
+ if norm in {"1", "true", "t", "yes", "y", "on"}:
600
+ return True
601
+
602
+ if norm in {"0", "false", "f", "no", "n", "off"}:
603
+ return False
604
+
605
+ self.fail(
606
+ _("{value!r} is not a valid boolean.").format(value=value), param, ctx
607
+ )
608
+
609
+ def __repr__(self) -> str:
610
+ return "BOOL"
611
+
612
+
613
+ class UUIDParameterType(ParamType):
614
+ name = "uuid"
615
+
616
+ def convert(
617
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
618
+ ) -> t.Any:
619
+ import uuid
620
+
621
+ if isinstance(value, uuid.UUID):
622
+ return value
623
+
624
+ value = value.strip()
625
+
626
+ try:
627
+ return uuid.UUID(value)
628
+ except ValueError:
629
+ self.fail(
630
+ _("{value!r} is not a valid UUID.").format(value=value), param, ctx
631
+ )
632
+
633
+ def __repr__(self) -> str:
634
+ return "UUID"
635
+
636
+
637
+ class File(ParamType):
638
+ """Declares a parameter to be a file for reading or writing. The file
639
+ is automatically closed once the context tears down (after the command
640
+ finished working).
641
+
642
+ Files can be opened for reading or writing. The special value ``-``
643
+ indicates stdin or stdout depending on the mode.
644
+
645
+ By default, the file is opened for reading text data, but it can also be
646
+ opened in binary mode or for writing. The encoding parameter can be used
647
+ to force a specific encoding.
648
+
649
+ The `lazy` flag controls if the file should be opened immediately or upon
650
+ first IO. The default is to be non-lazy for standard input and output
651
+ streams as well as files opened for reading, `lazy` otherwise. When opening a
652
+ file lazily for reading, it is still opened temporarily for validation, but
653
+ will not be held open until first IO. lazy is mainly useful when opening
654
+ for writing to avoid creating the file until it is needed.
655
+
656
+ Starting with Click 2.0, files can also be opened atomically in which
657
+ case all writes go into a separate file in the same folder and upon
658
+ completion the file will be moved over to the original location. This
659
+ is useful if a file regularly read by other users is modified.
660
+
661
+ See :ref:`file-args` for more information.
662
+ """
663
+
664
+ name = "filename"
665
+ envvar_list_splitter = os.path.pathsep
666
+
667
+ def __init__(
668
+ self,
669
+ mode: str = "r",
670
+ encoding: t.Optional[str] = None,
671
+ errors: t.Optional[str] = "strict",
672
+ lazy: t.Optional[bool] = None,
673
+ atomic: bool = False,
674
+ ) -> None:
675
+ self.mode = mode
676
+ self.encoding = encoding
677
+ self.errors = errors
678
+ self.lazy = lazy
679
+ self.atomic = atomic
680
+
681
+ def to_info_dict(self) -> t.Dict[str, t.Any]:
682
+ info_dict = super().to_info_dict()
683
+ info_dict.update(mode=self.mode, encoding=self.encoding)
684
+ return info_dict
685
+
686
+ def resolve_lazy_flag(self, value: t.Any) -> bool:
687
+ if self.lazy is not None:
688
+ return self.lazy
689
+ if value == "-":
690
+ return False
691
+ elif "w" in self.mode:
692
+ return True
693
+ return False
694
+
695
+ def convert(
696
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
697
+ ) -> t.Any:
698
+ try:
699
+ if hasattr(value, "read") or hasattr(value, "write"):
700
+ return value
701
+
702
+ lazy = self.resolve_lazy_flag(value)
703
+
704
+ if lazy:
705
+ f: t.IO = t.cast(
706
+ t.IO,
707
+ LazyFile(
708
+ value, self.mode, self.encoding, self.errors, atomic=self.atomic
709
+ ),
710
+ )
711
+
712
+ if ctx is not None:
713
+ ctx.call_on_close(f.close_intelligently) # type: ignore
714
+
715
+ return f
716
+
717
+ f, should_close = open_stream(
718
+ value, self.mode, self.encoding, self.errors, atomic=self.atomic
719
+ )
720
+
721
+ # If a context is provided, we automatically close the file
722
+ # at the end of the context execution (or flush out). If a
723
+ # context does not exist, it's the caller's responsibility to
724
+ # properly close the file. This for instance happens when the
725
+ # type is used with prompts.
726
+ if ctx is not None:
727
+ if should_close:
728
+ ctx.call_on_close(safecall(f.close))
729
+ else:
730
+ ctx.call_on_close(safecall(f.flush))
731
+
732
+ return f
733
+ except OSError as e: # noqa: B014
734
+ self.fail(f"'{os.fsdecode(value)}': {e.strerror}", param, ctx)
735
+
736
+ def shell_complete(
737
+ self, ctx: "Context", param: "Parameter", incomplete: str
738
+ ) -> t.List["CompletionItem"]:
739
+ """Return a special completion marker that tells the completion
740
+ system to use the shell to provide file path completions.
741
+
742
+ :param ctx: Invocation context for this command.
743
+ :param param: The parameter that is requesting completion.
744
+ :param incomplete: Value being completed. May be empty.
745
+
746
+ .. versionadded:: 8.0
747
+ """
748
+ from outerbounds._vendor.click.shell_completion import CompletionItem
749
+
750
+ return [CompletionItem(incomplete, type="file")]
751
+
752
+
753
+ class Path(ParamType):
754
+ """The ``Path`` type is similar to the :class:`File` type, but
755
+ returns the filename instead of an open file. Various checks can be
756
+ enabled to validate the type of file and permissions.
757
+
758
+ :param exists: The file or directory needs to exist for the value to
759
+ be valid. If this is not set to ``True``, and the file does not
760
+ exist, then all further checks are silently skipped.
761
+ :param file_okay: Allow a file as a value.
762
+ :param dir_okay: Allow a directory as a value.
763
+ :param readable: if true, a readable check is performed.
764
+ :param writable: if true, a writable check is performed.
765
+ :param executable: if true, an executable check is performed.
766
+ :param resolve_path: Make the value absolute and resolve any
767
+ symlinks. A ``~`` is not expanded, as this is supposed to be
768
+ done by the shell only.
769
+ :param allow_dash: Allow a single dash as a value, which indicates
770
+ a standard stream (but does not open it). Use
771
+ :func:`~click.open_file` to handle opening this value.
772
+ :param path_type: Convert the incoming path value to this type. If
773
+ ``None``, keep Python's default, which is ``str``. Useful to
774
+ convert to :class:`pathlib.Path`.
775
+
776
+ .. versionchanged:: 8.1
777
+ Added the ``executable`` parameter.
778
+
779
+ .. versionchanged:: 8.0
780
+ Allow passing ``type=pathlib.Path``.
781
+
782
+ .. versionchanged:: 6.0
783
+ Added the ``allow_dash`` parameter.
784
+ """
785
+
786
+ envvar_list_splitter = os.path.pathsep
787
+
788
+ def __init__(
789
+ self,
790
+ exists: bool = False,
791
+ file_okay: bool = True,
792
+ dir_okay: bool = True,
793
+ writable: bool = False,
794
+ readable: bool = True,
795
+ resolve_path: bool = False,
796
+ allow_dash: bool = False,
797
+ path_type: t.Optional[t.Type] = None,
798
+ executable: bool = False,
799
+ ):
800
+ self.exists = exists
801
+ self.file_okay = file_okay
802
+ self.dir_okay = dir_okay
803
+ self.readable = readable
804
+ self.writable = writable
805
+ self.executable = executable
806
+ self.resolve_path = resolve_path
807
+ self.allow_dash = allow_dash
808
+ self.type = path_type
809
+
810
+ if self.file_okay and not self.dir_okay:
811
+ self.name = _("file")
812
+ elif self.dir_okay and not self.file_okay:
813
+ self.name = _("directory")
814
+ else:
815
+ self.name = _("path")
816
+
817
+ def to_info_dict(self) -> t.Dict[str, t.Any]:
818
+ info_dict = super().to_info_dict()
819
+ info_dict.update(
820
+ exists=self.exists,
821
+ file_okay=self.file_okay,
822
+ dir_okay=self.dir_okay,
823
+ writable=self.writable,
824
+ readable=self.readable,
825
+ allow_dash=self.allow_dash,
826
+ )
827
+ return info_dict
828
+
829
+ def coerce_path_result(self, rv: t.Any) -> t.Any:
830
+ if self.type is not None and not isinstance(rv, self.type):
831
+ if self.type is str:
832
+ rv = os.fsdecode(rv)
833
+ elif self.type is bytes:
834
+ rv = os.fsencode(rv)
835
+ else:
836
+ rv = self.type(rv)
837
+
838
+ return rv
839
+
840
+ def convert(
841
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
842
+ ) -> t.Any:
843
+ rv = value
844
+
845
+ is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
846
+
847
+ if not is_dash:
848
+ if self.resolve_path:
849
+ # os.path.realpath doesn't resolve symlinks on Windows
850
+ # until Python 3.8. Use pathlib for now.
851
+ import pathlib
852
+
853
+ rv = os.fsdecode(pathlib.Path(rv).resolve())
854
+
855
+ try:
856
+ st = os.stat(rv)
857
+ except OSError:
858
+ if not self.exists:
859
+ return self.coerce_path_result(rv)
860
+ self.fail(
861
+ _("{name} {filename!r} does not exist.").format(
862
+ name=self.name.title(), filename=os.fsdecode(value)
863
+ ),
864
+ param,
865
+ ctx,
866
+ )
867
+
868
+ if not self.file_okay and stat.S_ISREG(st.st_mode):
869
+ self.fail(
870
+ _("{name} {filename!r} is a file.").format(
871
+ name=self.name.title(), filename=os.fsdecode(value)
872
+ ),
873
+ param,
874
+ ctx,
875
+ )
876
+ if not self.dir_okay and stat.S_ISDIR(st.st_mode):
877
+ self.fail(
878
+ _("{name} '{filename}' is a directory.").format(
879
+ name=self.name.title(), filename=os.fsdecode(value)
880
+ ),
881
+ param,
882
+ ctx,
883
+ )
884
+
885
+ if self.readable and not os.access(rv, os.R_OK):
886
+ self.fail(
887
+ _("{name} {filename!r} is not readable.").format(
888
+ name=self.name.title(), filename=os.fsdecode(value)
889
+ ),
890
+ param,
891
+ ctx,
892
+ )
893
+
894
+ if self.writable and not os.access(rv, os.W_OK):
895
+ self.fail(
896
+ _("{name} {filename!r} is not writable.").format(
897
+ name=self.name.title(), filename=os.fsdecode(value)
898
+ ),
899
+ param,
900
+ ctx,
901
+ )
902
+
903
+ if self.executable and not os.access(value, os.X_OK):
904
+ self.fail(
905
+ _("{name} {filename!r} is not executable.").format(
906
+ name=self.name.title(), filename=os.fsdecode(value)
907
+ ),
908
+ param,
909
+ ctx,
910
+ )
911
+
912
+ return self.coerce_path_result(rv)
913
+
914
+ def shell_complete(
915
+ self, ctx: "Context", param: "Parameter", incomplete: str
916
+ ) -> t.List["CompletionItem"]:
917
+ """Return a special completion marker that tells the completion
918
+ system to use the shell to provide path completions for only
919
+ directories or any paths.
920
+
921
+ :param ctx: Invocation context for this command.
922
+ :param param: The parameter that is requesting completion.
923
+ :param incomplete: Value being completed. May be empty.
924
+
925
+ .. versionadded:: 8.0
926
+ """
927
+ from outerbounds._vendor.click.shell_completion import CompletionItem
928
+
929
+ type = "dir" if self.dir_okay and not self.file_okay else "file"
930
+ return [CompletionItem(incomplete, type=type)]
931
+
932
+
933
+ class Tuple(CompositeParamType):
934
+ """The default behavior of Click is to apply a type on a value directly.
935
+ This works well in most cases, except for when `nargs` is set to a fixed
936
+ count and different types should be used for different items. In this
937
+ case the :class:`Tuple` type can be used. This type can only be used
938
+ if `nargs` is set to a fixed number.
939
+
940
+ For more information see :ref:`tuple-type`.
941
+
942
+ This can be selected by using a Python tuple literal as a type.
943
+
944
+ :param types: a list of types that should be used for the tuple items.
945
+ """
946
+
947
+ def __init__(self, types: t.Sequence[t.Union[t.Type, ParamType]]) -> None:
948
+ self.types = [convert_type(ty) for ty in types]
949
+
950
+ def to_info_dict(self) -> t.Dict[str, t.Any]:
951
+ info_dict = super().to_info_dict()
952
+ info_dict["types"] = [t.to_info_dict() for t in self.types]
953
+ return info_dict
954
+
955
+ @property
956
+ def name(self) -> str: # type: ignore
957
+ return f"<{' '.join(ty.name for ty in self.types)}>"
958
+
959
+ @property
960
+ def arity(self) -> int: # type: ignore
961
+ return len(self.types)
962
+
963
+ def convert(
964
+ self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
965
+ ) -> t.Any:
966
+ len_type = len(self.types)
967
+ len_value = len(value)
968
+
969
+ if len_value != len_type:
970
+ self.fail(
971
+ ngettext(
972
+ "{len_type} values are required, but {len_value} was given.",
973
+ "{len_type} values are required, but {len_value} were given.",
974
+ len_value,
975
+ ).format(len_type=len_type, len_value=len_value),
976
+ param=param,
977
+ ctx=ctx,
978
+ )
979
+
980
+ return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value))
981
+
982
+
983
+ def convert_type(ty: t.Optional[t.Any], default: t.Optional[t.Any] = None) -> ParamType:
984
+ """Find the most appropriate :class:`ParamType` for the given Python
985
+ type. If the type isn't provided, it can be inferred from a default
986
+ value.
987
+ """
988
+ guessed_type = False
989
+
990
+ if ty is None and default is not None:
991
+ if isinstance(default, (tuple, list)):
992
+ # If the default is empty, ty will remain None and will
993
+ # return STRING.
994
+ if default:
995
+ item = default[0]
996
+
997
+ # A tuple of tuples needs to detect the inner types.
998
+ # Can't call convert recursively because that would
999
+ # incorrectly unwind the tuple to a single type.
1000
+ if isinstance(item, (tuple, list)):
1001
+ ty = tuple(map(type, item))
1002
+ else:
1003
+ ty = type(item)
1004
+ else:
1005
+ ty = type(default)
1006
+
1007
+ guessed_type = True
1008
+
1009
+ if isinstance(ty, tuple):
1010
+ return Tuple(ty)
1011
+
1012
+ if isinstance(ty, ParamType):
1013
+ return ty
1014
+
1015
+ if ty is str or ty is None:
1016
+ return STRING
1017
+
1018
+ if ty is int:
1019
+ return INT
1020
+
1021
+ if ty is float:
1022
+ return FLOAT
1023
+
1024
+ if ty is bool:
1025
+ return BOOL
1026
+
1027
+ if guessed_type:
1028
+ return STRING
1029
+
1030
+ if __debug__:
1031
+ try:
1032
+ if issubclass(ty, ParamType):
1033
+ raise AssertionError(
1034
+ f"Attempted to use an uninstantiated parameter type ({ty})."
1035
+ )
1036
+ except TypeError:
1037
+ # ty is an instance (correct), so issubclass fails.
1038
+ pass
1039
+
1040
+ return FuncParamType(ty)
1041
+
1042
+
1043
+ #: A dummy parameter type that just does nothing. From a user's
1044
+ #: perspective this appears to just be the same as `STRING` but
1045
+ #: internally no string conversion takes place if the input was bytes.
1046
+ #: This is usually useful when working with file paths as they can
1047
+ #: appear in bytes and unicode.
1048
+ #:
1049
+ #: For path related uses the :class:`Path` type is a better choice but
1050
+ #: there are situations where an unprocessed type is useful which is why
1051
+ #: it is is provided.
1052
+ #:
1053
+ #: .. versionadded:: 4.0
1054
+ UNPROCESSED = UnprocessedParamType()
1055
+
1056
+ #: A unicode string parameter type which is the implicit default. This
1057
+ #: can also be selected by using ``str`` as type.
1058
+ STRING = StringParamType()
1059
+
1060
+ #: An integer parameter. This can also be selected by using ``int`` as
1061
+ #: type.
1062
+ INT = IntParamType()
1063
+
1064
+ #: A floating point value parameter. This can also be selected by using
1065
+ #: ``float`` as type.
1066
+ FLOAT = FloatParamType()
1067
+
1068
+ #: A boolean parameter. This is the default for boolean flags. This can
1069
+ #: also be selected by using ``bool`` as a type.
1070
+ BOOL = BoolParamType()
1071
+
1072
+ #: A UUID parameter.
1073
+ UUID = UUIDParameterType()