aboutsummaryrefslogtreecommitdiffstats
path: root/python/click/types.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/click/types.py')
-rw-r--r--python/click/types.py668
1 files changed, 668 insertions, 0 deletions
diff --git a/python/click/types.py b/python/click/types.py
new file mode 100644
index 0000000..1f88032
--- /dev/null
+++ b/python/click/types.py
@@ -0,0 +1,668 @@
+import os
+import stat
+from datetime import datetime
+
+from ._compat import open_stream, text_type, filename_to_ui, \
+ get_filesystem_encoding, get_streerror, _get_argv_encoding, PY2
+from .exceptions import BadParameter
+from .utils import safecall, LazyFile
+
+
+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')
+ 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.
+
+ 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 '[%s]' % '|'.join(self.choices)
+
+ def get_missing_message(self, param):
+ return 'Choose from:\n\t%s.' % ',\n\t'.join(self.choices)
+
+ def convert(self, value, param, ctx):
+ # Exact match
+ if value in self.choices:
+ return value
+
+ # 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 = 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(choice) for choice in
+ self.choices]
+
+ if not self.case_sensitive:
+ normed_value = normed_value.lower()
+ normed_choices = [choice.lower() for choice in normed_choices]
+
+ if normed_value in normed_choices:
+ return normed_value
+
+ self.fail('invalid choice: %s. (choose from %s)' %
+ (value, ', '.join(self.choices)), param, ctx)
+
+ def __repr__(self):
+ return 'Choice(%r)' % 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, UnicodeError):
+ self.fail('%s is not a valid integer' % 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('%s is bigger than the maximum valid value '
+ '%s.' % (rv, self.max), param, ctx)
+ elif self.max is None:
+ self.fail('%s is smaller than the minimum valid value '
+ '%s.' % (rv, self.min), param, ctx)
+ else:
+ self.fail('%s is not in the valid range of %s to %s.'
+ % (rv, self.min, self.max), param, ctx)
+ return rv
+
+ def __repr__(self):
+ return 'IntRange(%r, %r)' % (self.min, self.max)
+
+
+class FloatParamType(ParamType):
+ name = 'float'
+
+ def convert(self, value, param, ctx):
+ try:
+ return float(value)
+ except (UnicodeError, ValueError):
+ self.fail('%s is not a valid floating point value' %
+ 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('%s is bigger than the maximum valid value '
+ '%s.' % (rv, self.max), param, ctx)
+ elif self.max is None:
+ self.fail('%s is smaller than the minimum valid value '
+ '%s.' % (rv, self.min), param, ctx)
+ else:
+ self.fail('%s is not in the valid range of %s to %s.'
+ % (rv, self.min, self.max), param, ctx)
+ return rv
+
+ def __repr__(self):
+ return 'FloatRange(%r, %r)' % (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('%s is not a valid boolean' % 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 (UnicodeError, ValueError):
+ self.fail('%s is not a valid UUID value' % 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:
+ self.fail('Could not open file: %s: %s' % (
+ 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('%s "%s" does not exist.' % (
+ self.path_type,
+ filename_to_ui(value)
+ ), param, ctx)
+
+ if not self.file_okay and stat.S_ISREG(st.st_mode):
+ self.fail('%s "%s" is a file.' % (
+ self.path_type,
+ filename_to_ui(value)
+ ), param, ctx)
+ if not self.dir_okay and stat.S_ISDIR(st.st_mode):
+ self.fail('%s "%s" is a directory.' % (
+ self.path_type,
+ filename_to_ui(value)
+ ), param, ctx)
+ if self.writable and not os.access(value, os.W_OK):
+ self.fail('%s "%s" is not writable.' % (
+ self.path_type,
+ filename_to_ui(value)
+ ), param, ctx)
+ if self.readable and not os.access(value, os.R_OK):
+ self.fail('%s "%s" is not readable.' % (
+ 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 "<" + " ".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 ty into the most appropriate param
+ ty.
+ """
+ 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 (%s).' % 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()