| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762 |
- import os
- import stat
- from datetime import datetime
- from ._compat import _get_argv_encoding
- from ._compat import filename_to_ui
- from ._compat import get_filesystem_encoding
- from ._compat import get_streerror
- from ._compat import open_stream
- from ._compat import PY2
- from ._compat import text_type
- from .exceptions import BadParameter
- from .utils import LazyFile
- from .utils import safecall
- class ParamType(object):
- """Helper for converting values through types. The following is
- necessary for a valid type:
- * it needs a name
- * it needs to pass through None unchanged
- * it needs to convert from a string
- * it needs to convert its result type through unchanged
- (eg: needs to be idempotent)
- * it needs to be able to deal with param and context being `None`.
- This can be the case when the object is used with prompt
- inputs.
- """
- is_composite = False
- #: the descriptive name of this type
- name = None
- #: if a list of this type is expected and the value is pulled from a
- #: string environment variable, this is what splits it up. `None`
- #: means any whitespace. For all parameters the general rule is that
- #: whitespace splits them up. The exception are paths and files which
- #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
- #: Windows).
- envvar_list_splitter = None
- def __call__(self, value, param=None, ctx=None):
- if value is not None:
- return self.convert(value, param, ctx)
- def get_metavar(self, param):
- """Returns the metavar default for this param if it provides one."""
- def get_missing_message(self, param):
- """Optionally might return extra information about a missing
- parameter.
- .. versionadded:: 2.0
- """
- def convert(self, value, param, ctx):
- """Converts the value. This is not invoked for values that are
- `None` (the missing value).
- """
- return value
- def split_envvar_value(self, rv):
- """Given a value from an environment variable this splits it up
- into small chunks depending on the defined envvar list splitter.
- If the splitter is set to `None`, which means that whitespace splits,
- then leading and trailing whitespace is ignored. Otherwise, leading
- and trailing splitters usually lead to empty items being included.
- """
- return (rv or "").split(self.envvar_list_splitter)
- def fail(self, message, param=None, ctx=None):
- """Helper method to fail with an invalid value message."""
- raise BadParameter(message, ctx=ctx, param=param)
- class CompositeParamType(ParamType):
- is_composite = True
- @property
- def arity(self):
- raise NotImplementedError()
- class FuncParamType(ParamType):
- def __init__(self, func):
- self.name = func.__name__
- self.func = func
- def convert(self, value, param, ctx):
- try:
- return self.func(value)
- except ValueError:
- try:
- value = text_type(value)
- except UnicodeError:
- value = str(value).decode("utf-8", "replace")
- self.fail(value, param, ctx)
- class UnprocessedParamType(ParamType):
- name = "text"
- def convert(self, value, param, ctx):
- return value
- def __repr__(self):
- return "UNPROCESSED"
- class StringParamType(ParamType):
- name = "text"
- def convert(self, value, param, ctx):
- if isinstance(value, bytes):
- enc = _get_argv_encoding()
- try:
- value = value.decode(enc)
- except UnicodeError:
- fs_enc = get_filesystem_encoding()
- if fs_enc != enc:
- try:
- value = value.decode(fs_enc)
- except UnicodeError:
- value = value.decode("utf-8", "replace")
- else:
- value = value.decode("utf-8", "replace")
- return value
- return value
- def __repr__(self):
- return "STRING"
- class Choice(ParamType):
- """The choice type allows a value to be checked against a fixed set
- of supported values. All of these values have to be strings.
- You should only pass a list or tuple of choices. Other iterables
- (like generators) may lead to surprising results.
- The resulting value will always be one of the originally passed choices
- regardless of ``case_sensitive`` or any ``ctx.token_normalize_func``
- being specified.
- See :ref:`choice-opts` for an example.
- :param case_sensitive: Set to false to make choices case
- insensitive. Defaults to true.
- """
- name = "choice"
- def __init__(self, choices, case_sensitive=True):
- self.choices = choices
- self.case_sensitive = case_sensitive
- def get_metavar(self, param):
- return "[{}]".format("|".join(self.choices))
- def get_missing_message(self, param):
- return "Choose from:\n\t{}.".format(",\n\t".join(self.choices))
- def convert(self, value, param, ctx):
- # Match through normalization and case sensitivity
- # first do token_normalize_func, then lowercase
- # preserve original `value` to produce an accurate message in
- # `self.fail`
- normed_value = value
- normed_choices = {choice: choice for choice in self.choices}
- if ctx is not None and ctx.token_normalize_func is not None:
- normed_value = ctx.token_normalize_func(value)
- normed_choices = {
- ctx.token_normalize_func(normed_choice): original
- for normed_choice, original in normed_choices.items()
- }
- if not self.case_sensitive:
- if PY2:
- lower = str.lower
- else:
- lower = str.casefold
- normed_value = lower(normed_value)
- normed_choices = {
- lower(normed_choice): original
- for normed_choice, original in normed_choices.items()
- }
- if normed_value in normed_choices:
- return normed_choices[normed_value]
- self.fail(
- "invalid choice: {}. (choose from {})".format(
- value, ", ".join(self.choices)
- ),
- param,
- ctx,
- )
- def __repr__(self):
- return "Choice('{}')".format(list(self.choices))
- class DateTime(ParamType):
- """The DateTime type converts date strings into `datetime` objects.
- The format strings which are checked are configurable, but default to some
- common (non-timezone aware) ISO 8601 formats.
- When specifying *DateTime* formats, you should only pass a list or a tuple.
- Other iterables, like generators, may lead to surprising results.
- The format strings are processed using ``datetime.strptime``, and this
- consequently defines the format strings which are allowed.
- Parsing is tried using each format, in order, and the first format which
- parses successfully is used.
- :param formats: A list or tuple of date format strings, in the order in
- which they should be tried. Defaults to
- ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``,
- ``'%Y-%m-%d %H:%M:%S'``.
- """
- name = "datetime"
- def __init__(self, formats=None):
- self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
- def get_metavar(self, param):
- return "[{}]".format("|".join(self.formats))
- def _try_to_convert_date(self, value, format):
- try:
- return datetime.strptime(value, format)
- except ValueError:
- return None
- def convert(self, value, param, ctx):
- # Exact match
- for format in self.formats:
- dtime = self._try_to_convert_date(value, format)
- if dtime:
- return dtime
- self.fail(
- "invalid datetime format: {}. (choose from {})".format(
- value, ", ".join(self.formats)
- )
- )
- def __repr__(self):
- return "DateTime"
- class IntParamType(ParamType):
- name = "integer"
- def convert(self, value, param, ctx):
- try:
- return int(value)
- except ValueError:
- self.fail("{} is not a valid integer".format(value), param, ctx)
- def __repr__(self):
- return "INT"
- class IntRange(IntParamType):
- """A parameter that works similar to :data:`click.INT` but restricts
- the value to fit into a range. The default behavior is to fail if the
- value falls outside the range, but it can also be silently clamped
- between the two edges.
- See :ref:`ranges` for an example.
- """
- name = "integer range"
- def __init__(self, min=None, max=None, clamp=False):
- self.min = min
- self.max = max
- self.clamp = clamp
- def convert(self, value, param, ctx):
- rv = IntParamType.convert(self, value, param, ctx)
- if self.clamp:
- if self.min is not None and rv < self.min:
- return self.min
- if self.max is not None and rv > self.max:
- return self.max
- if (
- self.min is not None
- and rv < self.min
- or self.max is not None
- and rv > self.max
- ):
- if self.min is None:
- self.fail(
- "{} is bigger than the maximum valid value {}.".format(
- rv, self.max
- ),
- param,
- ctx,
- )
- elif self.max is None:
- self.fail(
- "{} is smaller than the minimum valid value {}.".format(
- rv, self.min
- ),
- param,
- ctx,
- )
- else:
- self.fail(
- "{} is not in the valid range of {} to {}.".format(
- rv, self.min, self.max
- ),
- param,
- ctx,
- )
- return rv
- def __repr__(self):
- return "IntRange({}, {})".format(self.min, self.max)
- class FloatParamType(ParamType):
- name = "float"
- def convert(self, value, param, ctx):
- try:
- return float(value)
- except ValueError:
- self.fail(
- "{} is not a valid floating point value".format(value), param, ctx
- )
- def __repr__(self):
- return "FLOAT"
- class FloatRange(FloatParamType):
- """A parameter that works similar to :data:`click.FLOAT` but restricts
- the value to fit into a range. The default behavior is to fail if the
- value falls outside the range, but it can also be silently clamped
- between the two edges.
- See :ref:`ranges` for an example.
- """
- name = "float range"
- def __init__(self, min=None, max=None, clamp=False):
- self.min = min
- self.max = max
- self.clamp = clamp
- def convert(self, value, param, ctx):
- rv = FloatParamType.convert(self, value, param, ctx)
- if self.clamp:
- if self.min is not None and rv < self.min:
- return self.min
- if self.max is not None and rv > self.max:
- return self.max
- if (
- self.min is not None
- and rv < self.min
- or self.max is not None
- and rv > self.max
- ):
- if self.min is None:
- self.fail(
- "{} is bigger than the maximum valid value {}.".format(
- rv, self.max
- ),
- param,
- ctx,
- )
- elif self.max is None:
- self.fail(
- "{} is smaller than the minimum valid value {}.".format(
- rv, self.min
- ),
- param,
- ctx,
- )
- else:
- self.fail(
- "{} is not in the valid range of {} to {}.".format(
- rv, self.min, self.max
- ),
- param,
- ctx,
- )
- return rv
- def __repr__(self):
- return "FloatRange({}, {})".format(self.min, self.max)
- class BoolParamType(ParamType):
- name = "boolean"
- def convert(self, value, param, ctx):
- if isinstance(value, bool):
- return bool(value)
- value = value.lower()
- if value in ("true", "t", "1", "yes", "y"):
- return True
- elif value in ("false", "f", "0", "no", "n"):
- return False
- self.fail("{} is not a valid boolean".format(value), param, ctx)
- def __repr__(self):
- return "BOOL"
- class UUIDParameterType(ParamType):
- name = "uuid"
- def convert(self, value, param, ctx):
- import uuid
- try:
- if PY2 and isinstance(value, text_type):
- value = value.encode("ascii")
- return uuid.UUID(value)
- except ValueError:
- self.fail("{} is not a valid UUID value".format(value), param, ctx)
- def __repr__(self):
- return "UUID"
- class File(ParamType):
- """Declares a parameter to be a file for reading or writing. The file
- is automatically closed once the context tears down (after the command
- finished working).
- Files can be opened for reading or writing. The special value ``-``
- indicates stdin or stdout depending on the mode.
- By default, the file is opened for reading text data, but it can also be
- opened in binary mode or for writing. The encoding parameter can be used
- to force a specific encoding.
- The `lazy` flag controls if the file should be opened immediately or upon
- first IO. The default is to be non-lazy for standard input and output
- streams as well as files opened for reading, `lazy` otherwise. When opening a
- file lazily for reading, it is still opened temporarily for validation, but
- will not be held open until first IO. lazy is mainly useful when opening
- for writing to avoid creating the file until it is needed.
- Starting with Click 2.0, files can also be opened atomically in which
- case all writes go into a separate file in the same folder and upon
- completion the file will be moved over to the original location. This
- is useful if a file regularly read by other users is modified.
- See :ref:`file-args` for more information.
- """
- name = "filename"
- envvar_list_splitter = os.path.pathsep
- def __init__(
- self, mode="r", encoding=None, errors="strict", lazy=None, atomic=False
- ):
- self.mode = mode
- self.encoding = encoding
- self.errors = errors
- self.lazy = lazy
- self.atomic = atomic
- def resolve_lazy_flag(self, value):
- if self.lazy is not None:
- return self.lazy
- if value == "-":
- return False
- elif "w" in self.mode:
- return True
- return False
- def convert(self, value, param, ctx):
- try:
- if hasattr(value, "read") or hasattr(value, "write"):
- return value
- lazy = self.resolve_lazy_flag(value)
- if lazy:
- f = LazyFile(
- value, self.mode, self.encoding, self.errors, atomic=self.atomic
- )
- if ctx is not None:
- ctx.call_on_close(f.close_intelligently)
- return f
- f, should_close = open_stream(
- value, self.mode, self.encoding, self.errors, atomic=self.atomic
- )
- # If a context is provided, we automatically close the file
- # at the end of the context execution (or flush out). If a
- # context does not exist, it's the caller's responsibility to
- # properly close the file. This for instance happens when the
- # type is used with prompts.
- if ctx is not None:
- if should_close:
- ctx.call_on_close(safecall(f.close))
- else:
- ctx.call_on_close(safecall(f.flush))
- return f
- except (IOError, OSError) as e: # noqa: B014
- self.fail(
- "Could not open file: {}: {}".format(
- filename_to_ui(value), get_streerror(e)
- ),
- param,
- ctx,
- )
- class Path(ParamType):
- """The path type is similar to the :class:`File` type but it performs
- different checks. First of all, instead of returning an open file
- handle it returns just the filename. Secondly, it can perform various
- basic checks about what the file or directory should be.
- .. versionchanged:: 6.0
- `allow_dash` was added.
- :param exists: if set to true, the file or directory needs to exist for
- this value to be valid. If this is not required and a
- file does indeed not exist, then all further checks are
- silently skipped.
- :param file_okay: controls if a file is a possible value.
- :param dir_okay: controls if a directory is a possible value.
- :param writable: if true, a writable check is performed.
- :param readable: if true, a readable check is performed.
- :param resolve_path: if this is true, then the path is fully resolved
- before the value is passed onwards. This means
- that it's absolute and symlinks are resolved. It
- will not expand a tilde-prefix, as this is
- supposed to be done by the shell only.
- :param allow_dash: If this is set to `True`, a single dash to indicate
- standard streams is permitted.
- :param path_type: optionally a string type that should be used to
- represent the path. The default is `None` which
- means the return value will be either bytes or
- unicode depending on what makes most sense given the
- input data Click deals with.
- """
- envvar_list_splitter = os.path.pathsep
- def __init__(
- self,
- exists=False,
- file_okay=True,
- dir_okay=True,
- writable=False,
- readable=True,
- resolve_path=False,
- allow_dash=False,
- path_type=None,
- ):
- self.exists = exists
- self.file_okay = file_okay
- self.dir_okay = dir_okay
- self.writable = writable
- self.readable = readable
- self.resolve_path = resolve_path
- self.allow_dash = allow_dash
- self.type = path_type
- if self.file_okay and not self.dir_okay:
- self.name = "file"
- self.path_type = "File"
- elif self.dir_okay and not self.file_okay:
- self.name = "directory"
- self.path_type = "Directory"
- else:
- self.name = "path"
- self.path_type = "Path"
- def coerce_path_result(self, rv):
- if self.type is not None and not isinstance(rv, self.type):
- if self.type is text_type:
- rv = rv.decode(get_filesystem_encoding())
- else:
- rv = rv.encode(get_filesystem_encoding())
- return rv
- def convert(self, value, param, ctx):
- rv = value
- is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
- if not is_dash:
- if self.resolve_path:
- rv = os.path.realpath(rv)
- try:
- st = os.stat(rv)
- except OSError:
- if not self.exists:
- return self.coerce_path_result(rv)
- self.fail(
- "{} '{}' does not exist.".format(
- self.path_type, filename_to_ui(value)
- ),
- param,
- ctx,
- )
- if not self.file_okay and stat.S_ISREG(st.st_mode):
- self.fail(
- "{} '{}' is a file.".format(self.path_type, filename_to_ui(value)),
- param,
- ctx,
- )
- if not self.dir_okay and stat.S_ISDIR(st.st_mode):
- self.fail(
- "{} '{}' is a directory.".format(
- self.path_type, filename_to_ui(value)
- ),
- param,
- ctx,
- )
- if self.writable and not os.access(value, os.W_OK):
- self.fail(
- "{} '{}' is not writable.".format(
- self.path_type, filename_to_ui(value)
- ),
- param,
- ctx,
- )
- if self.readable and not os.access(value, os.R_OK):
- self.fail(
- "{} '{}' is not readable.".format(
- self.path_type, filename_to_ui(value)
- ),
- param,
- ctx,
- )
- return self.coerce_path_result(rv)
- class Tuple(CompositeParamType):
- """The default behavior of Click is to apply a type on a value directly.
- This works well in most cases, except for when `nargs` is set to a fixed
- count and different types should be used for different items. In this
- case the :class:`Tuple` type can be used. This type can only be used
- if `nargs` is set to a fixed number.
- For more information see :ref:`tuple-type`.
- This can be selected by using a Python tuple literal as a type.
- :param types: a list of types that should be used for the tuple items.
- """
- def __init__(self, types):
- self.types = [convert_type(ty) for ty in types]
- @property
- def name(self):
- return "<{}>".format(" ".join(ty.name for ty in self.types))
- @property
- def arity(self):
- return len(self.types)
- def convert(self, value, param, ctx):
- if len(value) != len(self.types):
- raise TypeError(
- "It would appear that nargs is set to conflict with the"
- " composite type arity."
- )
- return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value))
- def convert_type(ty, default=None):
- """Converts a callable or python type into the most appropriate
- param type.
- """
- guessed_type = False
- if ty is None and default is not None:
- if isinstance(default, tuple):
- ty = tuple(map(type, default))
- else:
- ty = type(default)
- guessed_type = True
- if isinstance(ty, tuple):
- return Tuple(ty)
- if isinstance(ty, ParamType):
- return ty
- if ty is text_type or ty is str or ty is None:
- return STRING
- if ty is int:
- return INT
- # Booleans are only okay if not guessed. This is done because for
- # flags the default value is actually a bit of a lie in that it
- # indicates which of the flags is the one we want. See get_default()
- # for more information.
- if ty is bool and not guessed_type:
- return BOOL
- if ty is float:
- return FLOAT
- if guessed_type:
- return STRING
- # Catch a common mistake
- if __debug__:
- try:
- if issubclass(ty, ParamType):
- raise AssertionError(
- "Attempted to use an uninstantiated parameter type ({}).".format(ty)
- )
- except TypeError:
- pass
- return FuncParamType(ty)
- #: A dummy parameter type that just does nothing. From a user's
- #: perspective this appears to just be the same as `STRING` but internally
- #: no string conversion takes place. This is necessary to achieve the
- #: same bytes/unicode behavior on Python 2/3 in situations where you want
- #: to not convert argument types. This is usually useful when working
- #: with file paths as they can appear in bytes and unicode.
- #:
- #: For path related uses the :class:`Path` type is a better choice but
- #: there are situations where an unprocessed type is useful which is why
- #: it is is provided.
- #:
- #: .. versionadded:: 4.0
- UNPROCESSED = UnprocessedParamType()
- #: A unicode string parameter type which is the implicit default. This
- #: can also be selected by using ``str`` as type.
- STRING = StringParamType()
- #: An integer parameter. This can also be selected by using ``int`` as
- #: type.
- INT = IntParamType()
- #: A floating point value parameter. This can also be selected by using
- #: ``float`` as type.
- FLOAT = FloatParamType()
- #: A boolean parameter. This is the default for boolean flags. This can
- #: also be selected by using ``bool`` as a type.
- BOOL = BoolParamType()
- #: A UUID parameter.
- UUID = UUIDParameterType()
|