Exemplo n.º 1
0
    def compose_usage_notes(self):
        """
        Get a description of the property's semantics to include in its documentation.

        :returns: A list of strings describing the semantics of the
                  :class:`custom_property` in reStructuredText_ format with
                  Sphinx_ directives.

        .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText
        .. _Sphinx: http://sphinx-doc.org/
        """
        template = DYNAMIC_PROPERTY_NOTE if self.dynamic else CUSTOM_PROPERTY_NOTE
        cls = custom_property if self.dynamic else self.__class__
        dotted_path = "%s.%s" % (cls.__module__, cls.__name__)
        notes = [format(template, name=self.__name__, type=dotted_path)]
        if self.environment_variable:
            notes.append(
                format(ENVIRONMENT_PROPERTY_NOTE,
                       variable=self.environment_variable))
        if self.required:
            notes.append(format(REQUIRED_PROPERTY_NOTE, name=self.__name__))
        if self.key:
            notes.append(KEY_PROPERTY_NOTE)
        if self.writable:
            notes.append(WRITABLE_PROPERTY_NOTE)
        if self.cached:
            notes.append(CACHED_PROPERTY_NOTE)
        if self.resettable:
            if self.cached:
                notes.append(RESETTABLE_CACHED_PROPERTY_NOTE)
            else:
                notes.append(RESETTABLE_WRITABLE_PROPERTY_NOTE)
        return notes
Exemplo n.º 2
0
    def __getattr__(self, name):
        """
        Override module attribute lookup.

        :param name: The name to look up (a string).
        :returns: The attribute value.
        """
        # Check if the given name is an alias.
        target = self.aliases.get(name)
        if target is not None:
            # Emit the deprecation warning.
            warnings.warn(
                format("%s.%s was moved to %s, please update your imports",
                       self.module.__name__, name, target),
                category=DeprecationWarning,
                stacklevel=2,
            )
            # Resolve the dotted path.
            return self.resolve(target)
        # Look up the name in the original module namespace.
        value = getattr(self.module, name, None)
        if value is not None:
            return value
        # Fall back to the default behavior.
        raise AttributeError(
            format("module '%s' has no attribute '%s'", self.module.__name__,
                   name))
Exemplo n.º 3
0
def append_property_docs(app, what, name, obj, options, lines):
    """
    Render an overview with properties and methods of :class:`.PropertyManager` subclasses.

    This function implements a callback for ``autodoc-process-docstring`` that
    generates and appends an overview of member details to the docstrings of
    :class:`.PropertyManager` subclasses.

    The parameters expected by this function are those defined for Sphinx event
    callback functions (i.e. I'm not going to document them here :-).
    """
    if is_suitable_type(obj):
        paragraphs = []
        details = TypeInspector(type=obj)
        paragraphs.append(format("Here's an overview of the :class:`%s` class:", obj.__name__))
        # Whitespace in labels is replaced with non breaking spaces to disable wrapping of the label text.
        data = [(format("%s:", label.replace(' ', u'\u00A0')), text) for label, text in details.overview if text]
        paragraphs.append(format_rst_table(data))
        # Append any hints after the overview.
        hints = (details.required_hint, details.initializer_hint)
        if any(hints):
            paragraphs.append(' '.join(h for h in hints if h))
        # Insert padding between the regular docstring and generated content.
        if lines:
            lines.append('')
        lines.extend('\n\n'.join(paragraphs).splitlines())
Exemplo n.º 4
0
def render_error(filename, line_number, text, *args, **kw):
    """Render an error message including line number and optional filename."""
    message = []
    if filename and line_number:
        message.append(
            format("Control file parsing error in %s at line %i:", filename,
                   line_number))
    else:
        message.append(
            format("Failed to parse control field at line %i:", line_number))
    message.append(compact(text, *args, **kw))
    return u" ".join(message)
Exemplo n.º 5
0
def deprecation_note_callback(app, what, name, obj, options, lines):
    """
    Automatically document aliases defined using :func:`~humanfriendly.deprecation.define_aliases()`.

    Refer to :func:`enable_deprecation_notes()` to enable the use of this
    function (you probably don't want to call :func:`deprecation_note_callback()`
    directly).

    This function implements a callback for ``autodoc-process-docstring`` that
    reformats module docstrings to append an overview of aliases defined by the
    module.

    The parameters expected by this function are those defined for Sphinx event
    callback functions (i.e. I'm not going to document them here :-).
    """
    if isinstance(obj, types.ModuleType) and lines:
        aliases = get_aliases(obj.__name__)
        if aliases:
            # Convert the existing docstring to a string and remove leading
            # indentation from that string, otherwise our generated content
            # would have to match the existing indentation in order not to
            # break docstring parsing (because indentation is significant
            # in the reStructuredText format).
            blocks = [dedent("\n".join(lines))]
            # Use an admonition to group the deprecated aliases together and
            # to distinguish them from the autodoc entries that follow.
            blocks.append(".. note:: Deprecated names")
            indent = " " * 3
            if len(aliases) == 1:
                explanation = """
                    The following alias exists to preserve backwards compatibility,
                    however a :exc:`~exceptions.DeprecationWarning` is triggered
                    when it is accessed, because this alias will be removed
                    in a future release.
                """
            else:
                explanation = """
                    The following aliases exist to preserve backwards compatibility,
                    however a :exc:`~exceptions.DeprecationWarning` is triggered
                    when they are accessed, because these aliases will be
                    removed in a future release.
                """
            blocks.append(indent + compact(explanation))
            for name, target in aliases.items():
                blocks.append(format("%s.. data:: %s", indent, name))
                blocks.append(
                    format("%sAlias for :obj:`%s`.", indent * 2, target))
            update_lines(lines, "\n\n".join(blocks))
Exemplo n.º 6
0
def coerce_boolean(value):
    """
    Coerce any value to a boolean.

    :param value: Any Python value. If the value is a string:

                  - The strings '1', 'yes', 'true' and 'on' are coerced to :data:`True`.
                  - The strings '0', 'no', 'false' and 'off' are coerced to :data:`False`.
                  - Other strings raise an exception.

                  Other Python values are coerced using :func:`bool()`.
    :returns: A proper boolean value.
    :raises: :exc:`exceptions.ValueError` when the value is a string but
             cannot be coerced with certainty.
    """
    if is_string(value):
        normalized = value.strip().lower()
        if normalized in ('1', 'yes', 'true', 'on'):
            return True
        elif normalized in ('0', 'no', 'false', 'off', ''):
            return False
        else:
            msg = "Failed to coerce string to boolean! (%r)"
            raise ValueError(format(msg, value))
    else:
        return bool(value)
Exemplo n.º 7
0
def coerce_boolean(value):
    """
    Coerce any value to a boolean.

    :param value: Any Python value. If the value is a string:

                  - The strings '1', 'yes', 'true' and 'on' are coerced to :data:`True`.
                  - The strings '0', 'no', 'false' and 'off' are coerced to :data:`False`.
                  - Other strings raise an exception.

                  Other Python values are coerced using :func:`bool()`.
    :returns: A proper boolean value.
    :raises: :exc:`exceptions.ValueError` when the value is a string but
             cannot be coerced with certainty.
    """
    if is_string(value):
        normalized = value.strip().lower()
        if normalized in ('1', 'yes', 'true', 'on'):
            return True
        elif normalized in ('0', 'no', 'false', 'off', ''):
            return False
        else:
            msg = "Failed to coerce string to boolean! (%r)"
            raise ValueError(format(msg, value))
    else:
        return bool(value)
Exemplo n.º 8
0
    def smart_search(self, *arguments):
        """
        Perform a smart search on the given keywords or patterns.

        :param arguments: The keywords or patterns to search for.
        :returns: The matched password names (a list of strings).
        :raises: The following exceptions can be raised:

                 - :exc:`.NoMatchingPasswordError` when no matching passwords are found.
                 - :exc:`.EmptyPasswordStoreError` when the password store is empty.

        This method first tries :func:`simple_search()` and if that doesn't
        produce any matches it will fall back to :func:`fuzzy_search()`. If no
        matches are found an exception is raised (see above).
        """
        matches = self.simple_search(*arguments)
        if not matches:
            logger.verbose("Falling back from substring search to fuzzy search ..")
            matches = self.fuzzy_search(*arguments)
        if not matches:
            if len(self.filtered_entries) > 0:
                raise NoMatchingPasswordError(
                    format("No passwords matched the given arguments! (%s)", concatenate(map(repr, arguments)))
                )
            else:
                msg = "You don't have any passwords yet! (no *.gpg files found)"
                raise EmptyPasswordStoreError(msg)
        return matches
Exemplo n.º 9
0
def parse_length(length):
    """
    Parse a human readable length and return the number of metres.

    :param length: The human readable length to parse (a string).
    :returns: The corresponding length in metres (a float).
    :raises: :exc:`InvalidLength` when the input can't be parsed.

    Some examples:

    >>> from humanfriendly import parse_length
    >>> parse_length('42')
    42
    >>> parse_length('1 km')
    1000
    >>> parse_length('5mm')
    0.005
    >>> parse_length('15.3cm')
    0.153
    """
    tokens = tokenize(length)
    if tokens and isinstance(tokens[0], numbers.Number):
        # If the input contains only a number, it's assumed to be the number of metres.
        if len(tokens) == 1:
            return tokens[0]
        # Otherwise we expect to find two tokens: A number and a unit.
        if len(tokens) == 2 and is_string(tokens[1]):
            normalized_unit = tokens[1].lower()
            # Try to match the first letter of the unit.
            for unit in length_size_units:
                if normalized_unit.startswith(unit['prefix']):
                    return tokens[0] * unit['divider']
    # We failed to parse the length specification.
    msg = "Failed to parse length! (input %r was tokenized as %r)"
    raise InvalidLength(format(msg, length, tokens))
    def validate_system(self):
        """
        Make sure the system is running a supported version of Debian or Ubuntu.

        :raises: :exc:`UnsupportedSystemError` when validation fails.
        """
        # Make sure we're dealing with a Debian or Ubuntu system.
        logger.verbose("Validating operating system distribution ..")
        if self.distributor_id.lower() not in ('debian', 'ubuntu'):
            raise UnsupportedSystemError(
                compact("""
                According to the output of the 'lsb_release --id' command you
                are running an unsupported operating system distribution!
                (output: {output})
            """,
                        output=repr(self.distributor_id)))
        # Make sure we're dealing with a supported version of Debian or Ubuntu.
        base_url = format(
            'https://deb.nodesource.com/{version}/dists/{codename}/',
            version=self.nodejs_version,
            codename=self.distribution_codename.lower())
        logger.info("Validating repository availability (%s) ..", base_url)
        if not requests.get(base_url).ok:
            raise UnsupportedSystemError(
                compact("""
                Based on the output of the 'lsb_release --codename' command
                ({codename}) it seems that your version of {distro} isn't
                supported by NodeSource! (more specifically, it seems that
                {url} isn't available)
            """,
                        distro=self.distributor_id,
                        codename=self.distribution_codename,
                        url=base_url))
Exemplo n.º 11
0
def parse_length(length):
    """
    Parse a human readable length and return the number of metres.

    :param length: The human readable length to parse (a string).
    :returns: The corresponding length in metres (a float).
    :raises: :exc:`InvalidLength` when the input can't be parsed.

    Some examples:

    >>> from humanfriendly import parse_length
    >>> parse_length('42')
    42
    >>> parse_length('1 km')
    1000
    >>> parse_length('5mm')
    0.005
    >>> parse_length('15.3cm')
    0.153
    """
    tokens = tokenize(length)
    if tokens and isinstance(tokens[0], numbers.Number):
        # If the input contains only a number, it's assumed to be the number of metres.
        if len(tokens) == 1:
            return int(tokens[0])
        # Otherwise we expect to find two tokens: A number and a unit.
        if len(tokens) == 2 and is_string(tokens[1]):
            normalized_unit = tokens[1].lower()
            # Try to match the first letter of the unit.
            for unit in length_size_units:
                if normalized_unit.startswith(unit['prefix']):
                    return tokens[0] * unit['divider']
    # We failed to parse the length specification.
    msg = "Failed to parse length! (input %r was tokenized as %r)"
    raise InvalidLength(format(msg, length, tokens))
Exemplo n.º 12
0
def say(text, *args, **kw):
    """Reliably print Unicode strings to the terminal (standard output stream)."""
    text = format(text, *args, **kw)
    try:
        print(text)
    except UnicodeEncodeError:
        print(codecs.encode(text, OUTPUT_ENCODING))
Exemplo n.º 13
0
def say(text, *args, **kw):
    """Reliably print Unicode strings to the terminal (standard output stream)."""
    text = format(text, *args, **kw)
    try:
        print(text)
    except UnicodeEncodeError:
        print(codecs.encode(text, OUTPUT_ENCODING))
Exemplo n.º 14
0
 def test_html_conversion(self):
     """Check the conversion from ANSI escape sequences to HTML."""
     # Check conversion of colored text.
     for color_name, ansi_code in ANSI_COLOR_CODES.items():
         ansi_encoded_text = 'plain text followed by %s text' % ansi_wrap(color_name, color=color_name)
         expected_html = format(
             '<code>plain text followed by <span style="color:{css}">{name}</span> text</code>',
             css=EIGHT_COLOR_PALETTE[ansi_code], name=color_name,
         )
         self.assertEquals(expected_html, convert(ansi_encoded_text))
     # Check conversion of bright colored text.
     expected_html = '<code><span style="color:#FF0">bright yellow</span></code>'
     self.assertEquals(expected_html, convert(ansi_wrap('bright yellow', color='yellow', bright=True)))
     # Check conversion of text with a background color.
     expected_html = '<code><span style="background-color:#DE382B">red background</span></code>'
     self.assertEquals(expected_html, convert(ansi_wrap('red background', background='red')))
     # Check conversion of text with a bright background color.
     expected_html = '<code><span style="background-color:#F00">bright red background</span></code>'
     self.assertEquals(expected_html, convert(ansi_wrap('bright red background', background='red', bright=True)))
     # Check conversion of text that uses the 256 color mode palette as a foreground color.
     expected_html = '<code><span style="color:#FFAF00">256 color mode foreground</span></code>'
     self.assertEquals(expected_html, convert(ansi_wrap('256 color mode foreground', color=214)))
     # Check conversion of text that uses the 256 color mode palette as a background color.
     expected_html = '<code><span style="background-color:#AF0000">256 color mode background</span></code>'
     self.assertEquals(expected_html, convert(ansi_wrap('256 color mode background', background=124)))
     # Check that invalid 256 color mode indexes don't raise exceptions.
     expected_html = '<code>plain text expected</code>'
     self.assertEquals(expected_html, convert('\x1b[38;5;256mplain text expected\x1b[0m'))
     # Check conversion of bold text.
     expected_html = '<code><span style="font-weight:bold">bold text</span></code>'
     self.assertEquals(expected_html, convert(ansi_wrap('bold text', bold=True)))
     # Check conversion of underlined text.
     expected_html = '<code><span style="text-decoration:underline">underlined text</span></code>'
     self.assertEquals(expected_html, convert(ansi_wrap('underlined text', underline=True)))
     # Check conversion of strike-through text.
     expected_html = '<code><span style="text-decoration:line-through">strike-through text</span></code>'
     self.assertEquals(expected_html, convert(ansi_wrap('strike-through text', strike_through=True)))
     # Check conversion of inverse text.
     expected_html = '<code><span style="background-color:#FFC706;color:#000">inverse</span></code>'
     self.assertEquals(expected_html, convert(ansi_wrap('inverse', color='yellow', inverse=True)))
     # Check conversion of URLs.
     for sample_text in 'www.python.org', 'http://coloredlogs.rtfd.org', 'https://coloredlogs.rtfd.org':
         sample_url = sample_text if '://' in sample_text else ('http://' + sample_text)
         expected_html = '<code><a href="%s" style="color:inherit">%s</a></code>' % (sample_url, sample_text)
         self.assertEquals(expected_html, convert(sample_text))
     # Check that the capture pattern for URLs doesn't match ANSI escape
     # sequences and also check that the short hand for the 0 reset code is
     # supported. These are tests for regressions of bugs found in
     # coloredlogs <= 8.0.
     reset_short_hand = '\x1b[0m'
     blue_underlined = ansi_style(color='blue', underline=True)
     ansi_encoded_text = '<%shttps://coloredlogs.readthedocs.io%s>' % (blue_underlined, reset_short_hand)
     expected_html = (
         '<code>&lt;<span style="color:#006FB8;text-decoration:underline">'
         '<a href="https://coloredlogs.readthedocs.io" style="color:inherit">'
         'https://coloredlogs.readthedocs.io'
         '</a></span>&gt;</code>'
     )
     self.assertEquals(expected_html, convert(ansi_encoded_text))
Exemplo n.º 15
0
    def run(self):
        """
        Keep spawning commands and collecting results until all commands have run.

        :returns: The value of :attr:`results`.
        :raises: Any exceptions raised by :func:`collect()`.

        This method calls :func:`spawn()` and :func:`collect()` in a loop until
        all commands registered using :func:`add()` have run and finished. If
        :func:`collect()` raises an exception any running commands are
        terminated before the exception is propagated to the caller.

        If you're writing code where you want to own the main loop then
        consider calling :func:`spawn()` and :func:`collect()` directly instead
        of using :func:`run()`.

        When :attr:`concurrency` is set to one, specific care is taken to make
        sure that the callbacks configured by :attr:`.start_event` and
        :attr:`.finish_event` are called in the expected (intuitive) order.
        """
        # Start spawning processes to execute the commands.
        timer = Timer()
        logger.debug("Preparing to run %s with a concurrency of %i ..",
                     pluralize(self.num_commands, "command"), self.concurrency)
        try:
            with self.get_spinner(timer) as spinner:
                num_started = 0
                num_collected = 0
                while not self.is_finished:
                    # When concurrency is set to one (I know, initially it
                    # sounds like a silly use case, bear with me) I want the
                    # start_event and finish_event callbacks of external
                    # commands to fire in the right order. The following
                    # conditional is intended to accomplish this goal.
                    if self.concurrency > (num_started - num_collected):
                        num_started += self.spawn()
                    num_collected += self.collect()
                    spinner.step(label=format(
                        "Waiting for %i/%i %s",
                        self.num_commands - self.num_finished,
                        self.num_commands,
                        "command" if self.num_commands == 1 else "commands",
                    ))
                    spinner.sleep()
        except Exception:
            if self.num_running > 0:
                logger.warning(
                    "Command pool raised exception, terminating running commands!"
                )
            # Terminate commands that are still running.
            self.terminate()
            # Re-raise the exception to the caller.
            raise
        # Collect the output and return code of any commands not yet collected.
        self.collect()
        logger.debug("Finished running %s in %s.",
                     pluralize(self.num_commands, "command"), timer)
        # Report the results to the caller.
        return self.results
Exemplo n.º 16
0
def parse_size(size, binary=False):
    """
    Parse a human readable data size and return the number of bytes.

    :param size: The human readable file size to parse (a string).
    :param binary: :data:`True` to use binary multiples of bytes (base-2) for
                   ambiguous unit symbols and names, :data:`False` to use
                   decimal multiples of bytes (base-10).
    :returns: The corresponding size in bytes (an integer).
    :raises: :exc:`InvalidSize` when the input can't be parsed.

    This function knows how to parse sizes in bytes, kilobytes, megabytes,
    gigabytes, terabytes and petabytes. Some examples:

    >>> from humanfriendly import parse_size
    >>> parse_size('42')
    42
    >>> parse_size('13b')
    13
    >>> parse_size('5 bytes')
    5
    >>> parse_size('1 KB')
    1000
    >>> parse_size('1 kilobyte')
    1000
    >>> parse_size('1 KiB')
    1024
    >>> parse_size('1 KB', binary=True)
    1024
    >>> parse_size('1.5 GB')
    1500000000
    >>> parse_size('1.5 GB', binary=True)
    1610612736
    """
    tokens = tokenize(size)
    if tokens and isinstance(tokens[0], numbers.Number):
        # Get the normalized unit (if any) from the tokenized input.
        normalized_unit = tokens[1].lower() if len(tokens) == 2 and is_string(tokens[1]) else ''
        # If the input contains only a number, it's assumed to be the number of
        # bytes. The second token can also explicitly reference the unit bytes.
        if len(tokens) == 1 or normalized_unit.startswith('b'):
            return int(tokens[0])
        # Otherwise we expect two tokens: A number and a unit.
        if normalized_unit:
            for unit in disk_size_units:
                # First we check for unambiguous symbols (KiB, MiB, GiB, etc)
                # and names (kibibyte, mebibyte, gibibyte, etc) because their
                # handling is always the same.
                if normalized_unit in (unit.binary.symbol.lower(), unit.binary.name.lower()):
                    return int(tokens[0] * unit.binary.divider)
                # Now we will deal with ambiguous prefixes (K, M, G, etc),
                # symbols (KB, MB, GB, etc) and names (kilobyte, megabyte,
                # gigabyte, etc) according to the caller's preference.
                if (normalized_unit in (unit.decimal.symbol.lower(), unit.decimal.name.lower()) or
                        normalized_unit.startswith(unit.decimal.symbol[0].lower())):
                    return int(tokens[0] * (unit.binary.divider if binary else unit.decimal.divider))
    # We failed to parse the size specification.
    msg = "Failed to parse size! (input %r was tokenized as %r)"
    raise InvalidSize(format(msg, size, tokens))
Exemplo n.º 17
0
 def test_html_conversion(self):
     """Check the conversion from ANSI escape sequences to HTML."""
     # Check conversion of colored text.
     for color_name, ansi_code in ANSI_COLOR_CODES.items():
         ansi_encoded_text = 'plain text followed by %s text' % ansi_wrap(color_name, color=color_name)
         expected_html = format(
             '<code>plain text followed by <span style="color:{css}">{name}</span> text</code>',
             css=EIGHT_COLOR_PALETTE[ansi_code], name=color_name,
         )
         self.assertEqual(expected_html, convert(ansi_encoded_text))
     # Check conversion of bright colored text.
     expected_html = '<code><span style="color:#FF0">bright yellow</span></code>'
     self.assertEqual(expected_html, convert(ansi_wrap('bright yellow', color='yellow', bright=True)))
     # Check conversion of text with a background color.
     expected_html = '<code><span style="background-color:#DE382B">red background</span></code>'
     self.assertEqual(expected_html, convert(ansi_wrap('red background', background='red')))
     # Check conversion of text with a bright background color.
     expected_html = '<code><span style="background-color:#F00">bright red background</span></code>'
     self.assertEqual(expected_html, convert(ansi_wrap('bright red background', background='red', bright=True)))
     # Check conversion of text that uses the 256 color mode palette as a foreground color.
     expected_html = '<code><span style="color:#FFAF00">256 color mode foreground</span></code>'
     self.assertEqual(expected_html, convert(ansi_wrap('256 color mode foreground', color=214)))
     # Check conversion of text that uses the 256 color mode palette as a background color.
     expected_html = '<code><span style="background-color:#AF0000">256 color mode background</span></code>'
     self.assertEqual(expected_html, convert(ansi_wrap('256 color mode background', background=124)))
     # Check that invalid 256 color mode indexes don't raise exceptions.
     expected_html = '<code>plain text expected</code>'
     self.assertEqual(expected_html, convert('\x1b[38;5;256mplain text expected\x1b[0m'))
     # Check conversion of bold text.
     expected_html = '<code><span style="font-weight:bold">bold text</span></code>'
     self.assertEqual(expected_html, convert(ansi_wrap('bold text', bold=True)))
     # Check conversion of underlined text.
     expected_html = '<code><span style="text-decoration:underline">underlined text</span></code>'
     self.assertEqual(expected_html, convert(ansi_wrap('underlined text', underline=True)))
     # Check conversion of strike-through text.
     expected_html = '<code><span style="text-decoration:line-through">strike-through text</span></code>'
     self.assertEqual(expected_html, convert(ansi_wrap('strike-through text', strike_through=True)))
     # Check conversion of inverse text.
     expected_html = '<code><span style="background-color:#FFC706;color:#000">inverse</span></code>'
     self.assertEqual(expected_html, convert(ansi_wrap('inverse', color='yellow', inverse=True)))
     # Check conversion of URLs.
     for sample_text in 'www.python.org', 'http://coloredlogs.rtfd.org', 'https://coloredlogs.rtfd.org':
         sample_url = sample_text if '://' in sample_text else ('http://' + sample_text)
         expected_html = '<code><a href="%s" style="color:inherit">%s</a></code>' % (sample_url, sample_text)
         self.assertEqual(expected_html, convert(sample_text))
     # Check that the capture pattern for URLs doesn't match ANSI escape
     # sequences and also check that the short hand for the 0 reset code is
     # supported. These are tests for regressions of bugs found in
     # coloredlogs <= 8.0.
     reset_short_hand = '\x1b[0m'
     blue_underlined = ansi_style(color='blue', underline=True)
     ansi_encoded_text = '<%shttps://coloredlogs.readthedocs.io%s>' % (blue_underlined, reset_short_hand)
     expected_html = (
         '<code>&lt;<span style="color:#006FB8;text-decoration:underline">'
         '<a href="https://coloredlogs.readthedocs.io" style="color:inherit">'
         'https://coloredlogs.readthedocs.io'
         '</a></span>&gt;</code>'
     )
     self.assertEqual(expected_html, convert(ansi_encoded_text))
Exemplo n.º 18
0
 def error_message(self):
     """An error message that explains which commands *failed unexpectedly* (a string)."""
     summary = format("%i out of %s failed unexpectedly:",
                      self.pool.num_failed,
                      pluralize(self.pool.num_commands, "command"))
     details = "\n".join(" - %s" % cmd.error_message
                         for cmd in self.commands)
     return summary + "\n\n" + details
Exemplo n.º 19
0
 def overview(self):
     """Render an overview with related members grouped together."""
     return (
         ("Superclass" if len(self.type.__bases__) == 1 else "Superclasses",
          concatenate(format(":class:`~%s.%s`", b.__module__, b.__name__) for b in self.type.__bases__)),
         ("Special methods", self.format_methods(self.special_methods)),
         ("Public methods", self.format_methods(self.public_methods)),
         ("Properties", self.format_properties(n for n, v in self.properties)),
     )
Exemplo n.º 20
0
 def expression(self, value):
     """Parse an SSH connection profile expression provided by the caller."""
     match = EXPRESSION_PATTERN.match(value)
     if not match:
         msg = "Failed to parse connection profile expression! (%r)"
         raise ValueError(format(msg, value))
     self.username = match.group('user') or 'root'
     self.hostname = match.group('host')
     self.port_number = int(match.group('port') or '22')
     self.identity_file = None
Exemplo n.º 21
0
def parse_timespan(timespan):
    """
    Parse a "human friendly" timespan into the number of seconds.

    :param value: A string like ``5h`` (5 hours), ``10m`` (10 minutes) or
                  ``42s`` (42 seconds).
    :returns: The number of seconds as a floating point number.
    :raises: :exc:`InvalidTimespan` when the input can't be parsed.

    Note that the :func:`parse_timespan()` function is not meant to be the
    "mirror image" of the :func:`format_timespan()` function. Instead it's
    meant to allow humans to easily and succinctly specify a timespan with a
    minimal amount of typing. It's very useful to accept easy to write time
    spans as e.g. command line arguments to programs.

    The time units (and abbreviations) supported by this function are:

    - ms, millisecond, milliseconds
    - s, sec, secs, second, seconds
    - m, min, mins, minute, minutes
    - h, hour, hours
    - d, day, days
    - w, week, weeks
    - y, year, years

    Some examples:

    >>> from humanfriendly import parse_timespan
    >>> parse_timespan('42')
    42.0
    >>> parse_timespan('42s')
    42.0
    >>> parse_timespan('1m')
    60.0
    >>> parse_timespan('1h')
    3600.0
    >>> parse_timespan('1d')
    86400.0
    """
    tokens = tokenize(timespan)
    if tokens and isinstance(tokens[0], numbers.Number):
        # If the input contains only a number, it's assumed to be the number of seconds.
        if len(tokens) == 1:
            return float(tokens[0])
        # Otherwise we expect to find two tokens: A number and a unit.
        if len(tokens) == 2 and is_string(tokens[1]):
            normalized_unit = tokens[1].lower()
            for unit in time_units:
                if (normalized_unit == unit['singular']
                        or normalized_unit == unit['plural']
                        or normalized_unit in unit['abbreviations']):
                    return float(tokens[0]) * unit['divider']
    # We failed to parse the timespan specification.
    msg = "Failed to parse timespan! (input %r was tokenized as %r)"
    raise InvalidTimespan(format(msg, timespan, tokens))
Exemplo n.º 22
0
def parse_timespan(timespan):
    """
    Parse a "human friendly" timespan into the number of seconds.

    :param value: A string like ``5h`` (5 hours), ``10m`` (10 minutes) or
                  ``42s`` (42 seconds).
    :returns: The number of seconds as a floating point number.
    :raises: :exc:`InvalidTimespan` when the input can't be parsed.

    Note that the :func:`parse_timespan()` function is not meant to be the
    "mirror image" of the :func:`format_timespan()` function. Instead it's
    meant to allow humans to easily and succinctly specify a timespan with a
    minimal amount of typing. It's very useful to accept easy to write time
    spans as e.g. command line arguments to programs.

    The time units (and abbreviations) supported by this function are:

    - ms, millisecond, milliseconds
    - s, sec, secs, second, seconds
    - m, min, mins, minute, minutes
    - h, hour, hours
    - d, day, days
    - w, week, weeks
    - y, year, years

    Some examples:

    >>> from humanfriendly import parse_timespan
    >>> parse_timespan('42')
    42.0
    >>> parse_timespan('42s')
    42.0
    >>> parse_timespan('1m')
    60.0
    >>> parse_timespan('1h')
    3600.0
    >>> parse_timespan('1d')
    86400.0
    """
    tokens = tokenize(timespan)
    if tokens and isinstance(tokens[0], numbers.Number):
        # If the input contains only a number, it's assumed to be the number of seconds.
        if len(tokens) == 1:
            return float(tokens[0])
        # Otherwise we expect to find two tokens: A number and a unit.
        if len(tokens) == 2 and is_string(tokens[1]):
            normalized_unit = tokens[1].lower()
            for unit in time_units:
                if (normalized_unit == unit['singular'] or
                        normalized_unit == unit['plural'] or
                        normalized_unit in unit['abbreviations']):
                    return float(tokens[0]) * unit['divider']
    # We failed to parse the timespan specification.
    msg = "Failed to parse timespan! (input %r was tokenized as %r)"
    raise InvalidTimespan(format(msg, timespan, tokens))
Exemplo n.º 23
0
def message(*args, **kw):
    """
    Show an informational message on the terminal.

    :param args: Any position arguments are passed on to :func:`~humanfriendly.text.format()`.
    :param kw: Any keyword arguments are passed on to :func:`~humanfriendly.text.format()`.

    Renders the message using :func:`~humanfriendly.text.format()` and writes
    the resulting string to :data:`sys.stderr` (followed by a newline).
    """
    sys.stderr.write(format(*args, **kw) + '\n')
Exemplo n.º 24
0
def message(*args, **kw):
    """
    Show an informational message on the terminal.

    :param args: Any position arguments are passed on to :func:`~humanfriendly.text.format()`.
    :param kw: Any keyword arguments are passed on to :func:`~humanfriendly.text.format()`.

    Renders the message using :func:`~humanfriendly.text.format()` and writes
    the resulting string to :data:`sys.stderr` (followed by a newline).
    """
    sys.stderr.write(format(*args, **kw) + '\n')
Exemplo n.º 25
0
    def pre_context(self):
        """
        The command execution context inside the pre-boot environment.

        The computed value of this property is a command execution context
        created by :mod:`executor.contexts`, more specifically it's a
        :class:`~executor.contexts.RemoteContext` object.
        """
        # Prepare the remote context options.
        options = dict(
            identity_file=self.pre_boot.identity_file,
            port=self.pre_boot.port_number,
            shell=False,
            ssh_alias=self.pre_boot.hostname,
            ssh_command=[
                SSH_PROGRAM_NAME,
                '-o',
                'ControlMaster=auto',
                '-o',
                'ControlPersist=60',
                '-o',
                format('ControlPath={d}/%r@%h:%p', d=self.control_directory),
            ],
            ssh_user=self.pre_boot.username,
            tty=False,
        )
        # Use the configured SSH proxy?
        if self.ssh_proxy:
            options['ssh_command'].extend(
                ('-o',
                 format('ProxyCommand=ssh %s -W %s:%i', quote(self.ssh_proxy),
                        quote(self.pre_boot.hostname),
                        self.pre_boot.port_number)))
        # Decide what to do about the `known_hosts' file.
        if self.known_hosts_file:
            options['known_hosts_file'] = self.known_hosts_file
        else:
            options['ignore_known_hosts'] = True
        # Create the remote context object.
        return RemoteContext(**options)
Exemplo n.º 26
0
 def check_usage_notes(self):
     """Check whether the correct notes are embedded in the documentation."""
     class DocumentationTest(object):
         @self.property_type
         def documented_property(self):
             """Documentation written by the author."""
             return random.random()
     documentation = DocumentationTest.documented_property.__doc__
     # Test that the sentence added for custom properties is always present.
     cls = custom_property if self.property_type.dynamic else self.property_type
     custom_property_note = format(
         DYNAMIC_PROPERTY_NOTE if self.property_type.dynamic else CUSTOM_PROPERTY_NOTE,
         name='documented_property', type="%s.%s" % (cls.__module__, cls.__name__),
     )
     if DocumentationTest.documented_property.usage_notes:
         assert custom_property_note in documentation
     else:
         assert custom_property_note not in documentation
         # If CUSTOM_PROPERTY_NOTE is not present we assume that none of the
         # other usage notes will be present either.
         return
     # Test that the sentence added for writable properties is present when applicable.
     assert self.property_type.writable == (WRITABLE_PROPERTY_NOTE in documentation)
     # Test that the sentence added for cached properties is present when applicable.
     assert self.property_type.cached == (CACHED_PROPERTY_NOTE in documentation)
     # Test that the sentence added for resettable properties is present when applicable.
     if self.is_resettable:
         assert self.is_cached == (RESETTABLE_CACHED_PROPERTY_NOTE in documentation)
         assert self.is_writable == (RESETTABLE_WRITABLE_PROPERTY_NOTE in documentation)
     else:
         assert RESETTABLE_CACHED_PROPERTY_NOTE not in documentation
         assert RESETTABLE_WRITABLE_PROPERTY_NOTE not in documentation
     # Test that the sentence added for required properties is present when applicable.
     required_property_note = format(REQUIRED_PROPERTY_NOTE, name='documented_property')
     assert self.property_type.required == (required_property_note in documentation)
     # Test that the sentence added for environment properties is present when applicable.
     environment_note = format(ENVIRONMENT_PROPERTY_NOTE, variable=self.property_type.environment_variable)
     assert bool(self.property_type.environment_variable) == (environment_note in documentation)
Exemplo n.º 27
0
    def get_url(self, endpoint, **params):
        """
        Get the server URL for the given `endpoint`.

        :param endpoint: The name of a server side endpoint (a string).
        :param params: Any query string parameters.
        """
        return format(
            "http://{hostname}:{port}/{endpoint}?{params}",
            hostname=self.hostname,
            port=self.port_number,
            endpoint=endpoint,
            params=urlencode(params),
        )
Exemplo n.º 28
0
 def translate(args, kw):
     # Raise TypeError when too many positional arguments are passed to the decorated function.
     if len(args) > len(names):
         raise TypeError(
             format(
                 "{name} expected at most {limit} arguments, got {count}",
                 name=function.__name__,
                 limit=len(names),
                 count=len(args),
             ))
     # Emit a deprecation warning when positional arguments are used.
     if args:
         warnings.warn(
             format(
                 "{name} has deprecated positional arguments, please switch to keyword arguments",
                 name=function.__name__,
             ),
             category=DeprecationWarning,
             stacklevel=3,
         )
     # Translate positional arguments to keyword arguments.
     for name, value in zip(names, args):
         kw[name] = value
Exemplo n.º 29
0
def warning(*args, **kw):
    """
    Show a warning message on the terminal.

    :param args: Any position arguments are passed on to :func:`format()`.
    :param kw: Any keyword arguments are passed on to :func:`format()`.

    Renders the message using :func:`format()` and writes the resulting string
    to :data:`sys.stderr` (followed by a newline). If :data:`sys.stderr` is
    connected to a terminal :func:`ansi_wrap()` is used to color the message in
    a red font (to make the warning stand out from surrounding text).
    """
    text = format(*args, **kw)
    if terminal_supports_colors(sys.stderr):
        text = ansi_wrap(text, color="red")
    sys.stderr.write(text + "\n")
Exemplo n.º 30
0
    def __init__(self, command, timeout):
        """
        Initialize a :class:`CommandTimedOut` object.

        :param command: The command that timed out (an
                        :class:`~executor.ExternalCommand` object).
        :param timeout: The timeout that was exceeded (a number).
        """
        super(CommandTimedOut, self).__init__(
            command=command,
            error_message=format(
                "External command exceeded timeout of %s: %s",
                format_timespan(timeout),
                quote(command.command_line),
            ),
        )
Exemplo n.º 31
0
 def check_ssh_connection(self):
     """Verify SSH connectivity to the pre-boot environment."""
     if self.test_ssh_connection(self.pre_boot, self.pre_context):
         logger.info("Successfully connected and authenticated over SSH.")
     else:
         msg = format(
             "Failed to authenticate to %s:%i!",
             self.pre_boot.hostname,
             self.pre_boot.port_number,
         )
         if self.pre_boot.username == 'root':
             msg += " " + compact("""
                 Maybe you're accidentally connecting to the post-boot
                 environment and you don't have 'root' access there?
             """)
         raise SystemUnreachableError(msg)
Exemplo n.º 32
0
    def wait_for_post_boot(self, pre_server):
        """
        Wait for the post-boot environment to come online.

        :param pre_server: A :class:`ServerDetails` object created by :func:`wait_for_pre_boot()`.
        """
        method_timer = Timer()
        check_keys = bool(pre_server.host_keys)
        check_headers = (
            self.pre_boot.port_number == self.post_boot.port_number)
        logger.info(
            "Waiting for post-boot environment based on SSH %s ..",
            "host keys" if check_keys else
            ("server headers" if check_headers else "port numbers"))
        with AutomaticSpinner("Waiting for post-boot environment",
                              show_time=True):
            while True:
                iteration_timer = Timer()
                if check_headers or check_keys:
                    post_server = self.scan_ssh_server(self.post_boot)
                    if check_keys and post_server.host_keys:
                        logger.verbose(
                            "Checking if SSH host keys have changed ..")
                        if post_server.host_keys != pre_server.host_keys:
                            logger.info("Detected change in SSH host keys.")
                            self.store_host_keys(pre_server, post_server)
                            break
                    if check_headers and pre_server.header and post_server.header:
                        logger.verbose(
                            "Checking if SSH server header has changed ..")
                        if post_server.header != pre_server.header:
                            logger.info(
                                "Detected change in SSH server header.")
                            break
                elif self.test_ssh_connection(self.post_boot,
                                              self.post_context):
                    logger.info("Detected change in SSH port number.")
                    break
                if method_timer.elapsed_time >= self.boot_timeout:
                    raise BootTimeoutError(
                        format(
                            "Timed out waiting for post-boot environment of %s to come online within %s!",
                            self.post_context,
                            format_timespan(self.boot_timeout),
                        ))
                iteration_timer.sleep(self.retry_interval)
        logger.info("Waited %s for post-boot environment.", method_timer)
Exemplo n.º 33
0
def generate_sources_list(mirror_url,
                          codename,
                          suites=DEFAULT_SUITES,
                          components=VALID_COMPONENTS,
                          enable_sources=False):
    """
    Generate the contents of ``/etc/apt/sources.list`` for an Ubuntu system.

    :param mirror_url: The base URL of the mirror (a string).
    :param codename: The codename of the Ubuntu release (a string like 'trusty' or 'xenial').
    :param suites: An iterable of strings (defaults to :data:`DEFAULT_SUITES`,
                   refer to :data:`VALID_SUITES` for details).
    :param components: An iterable of strings (refer to
                       :data:`VALID_COMPONENTS` for details).
    :param enable_sources: :data:`True` to include ``deb-src`` entries,
                           :data:`False` to omit them.
    :returns: The suggested contents of ``/etc/apt/sources.list`` (a string).
    """
    # Validate the suites.
    invalid_suites = [s for s in suites if s not in VALID_SUITES]
    if invalid_suites:
        msg = "Invalid Ubuntu suite(s) given! (%s)"
        raise ValueError(msg % invalid_suites)
    # Validate the components.
    invalid_components = [c for c in components if c not in VALID_COMPONENTS]
    if invalid_components:
        msg = "Invalid Ubuntu component(s) given! (%s)"
        raise ValueError(msg % invalid_components)
    # Generate the /etc/apt/sources.list file contents.
    lines = []
    directives = ('deb', 'deb-src') if enable_sources else ('deb', )
    for suite in suites:
        for directive in directives:
            lines.append(
                format(
                    '{directive} {mirror} {suite} {components}',
                    directive=directive,
                    mirror=(
                        OLD_RELEASES_URL if mirrors_are_equal(
                            mirror_url, OLD_RELEASES_URL) else
                        (SECURITY_URL if suite == 'security' else mirror_url)),
                    suite=(codename if suite == 'release' else codename + '-' +
                           suite),
                    components=' '.join(components),
                ))
    return '\n'.join(lines)
Exemplo n.º 34
0
def coerce_seconds(value):
    """
    Coerce a value to the number of seconds.

    :param value: An :class:`int`, :class:`float` or
                  :class:`datetime.timedelta` object.
    :returns: An :class:`int` or :class:`float` value.

    When `value` is a :class:`datetime.timedelta` object the
    :func:`~datetime.timedelta.total_seconds()` method is called.
    """
    if isinstance(value, datetime.timedelta):
        return value.total_seconds()
    if not isinstance(value, numbers.Number):
        msg = "Failed to coerce value to number of seconds! (%r)"
        raise ValueError(format(msg, value))
    return value
Exemplo n.º 35
0
def warning(*args, **kw):
    """
    Show a warning message on the terminal.

    :param args: Any position arguments are passed on to :func:`~humanfriendly.text.format()`.
    :param kw: Any keyword arguments are passed on to :func:`~humanfriendly.text.format()`.

    Renders the message using :func:`~humanfriendly.text.format()` and writes
    the resulting string to :data:`sys.stderr` (followed by a newline). If
    :data:`sys.stderr` is connected to a terminal :func:`ansi_wrap()` is used
    to color the message in a red font (to make the warning stand out from
    surrounding text).
    """
    text = format(*args, **kw)
    if terminal_supports_colors(sys.stderr):
        text = ansi_wrap(text, color='red')
    sys.stderr.write(text + '\n')
Exemplo n.º 36
0
    def wait_until_connected(self):
        """
        Wait until connections are being accepted.

        :raises: :exc:`TimeoutError` when the SSH server isn't fast enough to
                 initialize.
        """
        timer = Timer()
        with Spinner(timer=timer) as spinner:
            while not self.is_connected:
                if timer.elapsed_time > self.wait_timeout:
                    raise TimeoutError(format(
                        "Failed to establish connection to %s within configured timeout of %s!",
                        self.endpoint, format_timespan(self.wait_timeout),
                    ))
                spinner.step(label="Waiting for %s to accept connections" % self.endpoint)
                spinner.sleep()
        logger.debug("Waited %s for %s to accept connections.", timer, self.endpoint)
Exemplo n.º 37
0
def coerce_pattern(value, flags=0):
    """
    Coerce strings to compiled regular expressions.

    :param value: A string containing a regular expression pattern
                  or a compiled regular expression.
    :param flags: The flags used to compile the pattern (an integer).
    :returns: A compiled regular expression.
    :raises: :exc:`~exceptions.ValueError` when `value` isn't a string
             and also isn't a compiled regular expression.
    """
    if is_string(value):
        value = re.compile(value, flags)
    else:
        empty_pattern = re.compile('')
        pattern_type = type(empty_pattern)
        if not isinstance(value, pattern_type):
            msg = "Failed to coerce value to compiled regular expression! (%r)"
            raise ValueError(format(msg, value))
    return value
Exemplo n.º 38
0
def coerce_pattern(value, flags=0):
    """
    Coerce strings to compiled regular expressions.

    :param value: A string containing a regular expression pattern
                  or a compiled regular expression.
    :param flags: The flags used to compile the pattern (an integer).
    :returns: A compiled regular expression.
    :raises: :exc:`~exceptions.ValueError` when `value` isn't a string
             and also isn't a compiled regular expression.
    """
    if is_string(value):
        value = re.compile(value, flags)
    else:
        empty_pattern = re.compile('')
        pattern_type = type(empty_pattern)
        if not isinstance(value, pattern_type):
            msg = "Failed to coerce value to compiled regular expression! (%r)"
            raise ValueError(format(msg, value))
    return value
Exemplo n.º 39
0
def coerce_seconds(value):
    """
    Coerce a value to the number of seconds.

    :param value: An :class:`int`, :class:`float` or
                  :class:`datetime.timedelta` object.
    :returns: An :class:`int` or :class:`float` value.

    When `value` is a :class:`datetime.timedelta` object the
    :func:`~datetime.timedelta.total_seconds()` method is called.
    On Python 2.6 this method is not available so it is emulated.
    """
    if isinstance(value, datetime.timedelta):
        if hasattr(value, 'total_seconds'):
            return value.total_seconds()
        else:
            return (value.microseconds + (value.seconds + value.days * 24 * 3600) * 10**6) / 10**6
    if not isinstance(value, numbers.Number):
        msg = "Failed to coerce value to number of seconds! (%r)"
        raise ValueError(format(msg, value))
    return value
Exemplo n.º 40
0
def auto_encode(stream, text, *args, **kw):
    """
    Reliably write Unicode strings to the terminal.

    :param stream: The file-like object to write to (a value like
                   :data:`sys.stdout` or :data:`sys.stderr`).
    :param text: The text to write to the stream (a string).
    :param args: Refer to :func:`~humanfriendly.text.format()`.
    :param kw: Refer to :func:`~humanfriendly.text.format()`.

    Renders the text using :func:`~humanfriendly.text.format()` and writes it
    to the given stream. If an :exc:`~exceptions.UnicodeEncodeError` is
    encountered in doing so, the text is encoded using :data:`DEFAULT_ENCODING`
    and the write is retried. The reasoning behind this rather blunt approach
    is that it's preferable to get output on the command line in the wrong
    encoding then to have the Python program blow up with a
    :exc:`~exceptions.UnicodeEncodeError` exception.
    """
    text = format(text, *args, **kw)
    try:
        stream.write(text)
    except UnicodeEncodeError:
        stream.write(codecs.encode(text, DEFAULT_ENCODING))
Exemplo n.º 41
0
def parse_date(datestring):
    """
    Parse a date/time string into a tuple of integers.

    :param datestring: The date/time string to parse.
    :returns: A tuple with the numbers ``(year, month, day, hour, minute,
              second)`` (all numbers are integers).
    :raises: :exc:`InvalidDate` when the date cannot be parsed.

    Supported date/time formats:

    - ``YYYY-MM-DD``
    - ``YYYY-MM-DD HH:MM:SS``

    .. note:: If you want to parse date/time strings with a fixed, known
              format and :func:`parse_date()` isn't useful to you, consider
              :func:`time.strptime()` or :meth:`datetime.datetime.strptime()`,
              both of which are included in the Python standard library.
              Alternatively for more complex tasks consider using the date/time
              parsing module in the dateutil_ package.

    Examples:

    >>> from humanfriendly import parse_date
    >>> parse_date('2013-06-17')
    (2013, 6, 17, 0, 0, 0)
    >>> parse_date('2013-06-17 02:47:42')
    (2013, 6, 17, 2, 47, 42)

    Here's how you convert the result to a number (`Unix time`_):

    >>> from humanfriendly import parse_date
    >>> from time import mktime
    >>> mktime(parse_date('2013-06-17 02:47:42') + (-1, -1, -1))
    1371430062.0

    And here's how you convert it to a :class:`datetime.datetime` object:

    >>> from humanfriendly import parse_date
    >>> from datetime import datetime
    >>> datetime(*parse_date('2013-06-17 02:47:42'))
    datetime.datetime(2013, 6, 17, 2, 47, 42)

    Here's an example that combines :func:`format_timespan()` and
    :func:`parse_date()` to calculate a human friendly timespan since a
    given date:

    >>> from humanfriendly import format_timespan, parse_date
    >>> from time import mktime, time
    >>> unix_time = mktime(parse_date('2013-06-17 02:47:42') + (-1, -1, -1))
    >>> seconds_since_then = time() - unix_time
    >>> print(format_timespan(seconds_since_then))
    1 year, 43 weeks and 1 day

    .. _dateutil: https://dateutil.readthedocs.io/en/latest/parser.html
    .. _Unix time: http://en.wikipedia.org/wiki/Unix_time
    """
    try:
        tokens = [t.strip() for t in datestring.split()]
        if len(tokens) >= 2:
            date_parts = list(map(int, tokens[0].split('-'))) + [1, 1]
            time_parts = list(map(int, tokens[1].split(':'))) + [0, 0, 0]
            return tuple(date_parts[0:3] + time_parts[0:3])
        else:
            year, month, day = (list(map(int, datestring.split('-'))) + [1, 1])[0:3]
            return (year, month, day, 0, 0, 0)
    except Exception:
        msg = "Invalid date! (expected 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' but got: %r)"
        raise InvalidDate(format(msg, datestring))
Exemplo n.º 42
0
def prompt_for_choice(choices, default=None, padding=True):
    """
    Prompt the user to select a choice from a group of options.

    :param choices: A sequence of strings with available options.
    :param default: The default choice if the user simply presses Enter
                    (expected to be a string, defaults to :data:`None`).
    :param padding: Refer to the documentation of :func:`prompt_for_input()`.
    :returns: The string corresponding to the user's choice.
    :raises: - :exc:`~exceptions.ValueError` if `choices` is an empty sequence.
             - Any exceptions raised by :func:`retry_limit()`.
             - Any exceptions raised by :func:`prompt_for_input()`.

    When no options are given an exception is raised:

    >>> prompt_for_choice([])
    Traceback (most recent call last):
      File "humanfriendly/prompts.py", line 148, in prompt_for_choice
        raise ValueError("Can't prompt for choice without any options!")
    ValueError: Can't prompt for choice without any options!

    If a single option is given the user isn't prompted:

    >>> prompt_for_choice(['only one choice'])
    'only one choice'

    Here's what the actual prompt looks like by default:

    >>> prompt_for_choice(['first option', 'second option'])
     <BLANKLINE>
      1. first option
      2. second option
     <BLANKLINE>
     Enter your choice as a number or unique substring (Control-C aborts): second
     <BLANKLINE>
    'second option'

    If you don't like the whitespace (empty lines and indentation):

    >>> prompt_for_choice(['first option', 'second option'], padding=False)
     1. first option
     2. second option
    Enter your choice as a number or unique substring (Control-C aborts): first
    'first option'
    """
    indent = ' ' if padding else ''
    # Make sure we can use 'choices' more than once (i.e. not a generator).
    choices = list(choices)
    if len(choices) == 1:
        # If there's only one option there's no point in prompting the user.
        logger.debug("Skipping interactive prompt because there's only option (%r).", choices[0])
        return choices[0]
    elif not choices:
        # We can't render a choice prompt without any options.
        raise ValueError("Can't prompt for choice without any options!")
    # Generate the prompt text.
    prompt_text = ('\n\n' if padding else '\n').join([
        # Present the available choices in a user friendly way.
        "\n".join([
            (u" %i. %s" % (i, choice)) + (" (default choice)" if choice == default else "")
            for i, choice in enumerate(choices, start=1)
        ]),
        # Instructions for the user.
        "Enter your choice as a number or unique substring (Control-C aborts): ",
    ])
    if terminal_supports_colors():
        prompt_text = ansi_wrap(prompt_text, bold=True, readline_hints=True)
    # Loop until a valid choice is made.
    logger.debug("Requesting interactive choice on terminal (options are %s) ..",
                 concatenate(map(repr, choices)))
    for attempt in retry_limit():
        reply = prompt_for_input(prompt_text, '', padding=padding, strip=True)
        if not reply and default is not None:
            logger.debug("Default choice selected by empty reply (%r).", default)
            return default
        elif reply.isdigit():
            index = int(reply) - 1
            if 0 <= index < len(choices):
                logger.debug("Option (%r) selected by numeric reply (%s).", choices[index], reply)
                return choices[index]
        # Check for substring matches.
        matches = []
        for choice in choices:
            lower_reply = reply.lower()
            lower_choice = choice.lower()
            if lower_reply == lower_choice:
                # If we have an 'exact' match we return it immediately.
                logger.debug("Option (%r) selected by reply (exact match).", choice, reply)
                return choice
            elif lower_reply in lower_choice and len(lower_reply) > 0:
                # Otherwise we gather substring matches.
                matches.append(choice)
        if len(matches) == 1:
            # If a single choice was matched we return it.
            logger.debug("Option (%r) selected by reply (substring match on %r).", matches[0], reply)
            return matches[0]
        else:
            # Give the user a hint about what went wrong.
            if matches:
                details = format("text '%s' matches more than one choice: %s", reply, concatenate(matches))
            elif reply.isdigit():
                details = format("number %i is not a valid choice", int(reply))
            elif reply and not reply.isspace():
                details = format("text '%s' doesn't match any choices", reply)
            else:
                details = "there's no default choice"
            logger.debug("Got %s reply (%s), retrying (%i/%i) ..",
                         "invalid" if reply else "empty", details,
                         attempt, MAX_ATTEMPTS)
            warning("%sError: Invalid input (%s).", indent, details)
Exemplo n.º 43
-1
def parse_size(size, binary=False):
    """
    Parse a human readable data size and return the number of bytes.

    :param size: The human readable file size to parse (a string).
    :param binary: :data:`True` to use binary multiples of bytes (base-2) for
                   ambiguous unit symbols and names, :data:`False` to use
                   decimal multiples of bytes (base-10).
    :returns: The corresponding size in bytes (an integer).
    :raises: :exc:`InvalidSize` when the input can't be parsed.

    This function knows how to parse sizes in bytes, kilobytes, megabytes,
    gigabytes, terabytes and petabytes. Some examples:

    >>> from humanfriendly import parse_size
    >>> parse_size('42')
    42
    >>> parse_size('13b')
    13
    >>> parse_size('5 bytes')
    5
    >>> parse_size('1 KB')
    1000
    >>> parse_size('1 kilobyte')
    1000
    >>> parse_size('1 KiB')
    1024
    >>> parse_size('1 KB', binary=True)
    1024
    >>> parse_size('1.5 GB')
    1500000000
    >>> parse_size('1.5 GB', binary=True)
    1610612736
    """
    tokens = tokenize(size)
    if tokens and isinstance(tokens[0], numbers.Number):
        # Get the normalized unit (if any) from the tokenized input.
        normalized_unit = tokens[1].lower() if len(tokens) == 2 and is_string(tokens[1]) else ''
        # If the input contains only a number, it's assumed to be the number of
        # bytes. The second token can also explicitly reference the unit bytes.
        if len(tokens) == 1 or normalized_unit.startswith('b'):
            return int(tokens[0])
        # Otherwise we expect two tokens: A number and a unit.
        if normalized_unit:
            for unit in disk_size_units:
                # First we check for unambiguous symbols (KiB, MiB, GiB, etc)
                # and names (kibibyte, mebibyte, gibibyte, etc) because their
                # handling is always the same.
                if normalized_unit in (unit.binary.symbol.lower(), unit.binary.name.lower()):
                    return int(tokens[0] * unit.binary.divider)
                # Now we will deal with ambiguous prefixes (K, M, G, etc),
                # symbols (KB, MB, GB, etc) and names (kilobyte, megabyte,
                # gigabyte, etc) according to the caller's preference.
                if (normalized_unit in (unit.decimal.symbol.lower(), unit.decimal.name.lower()) or
                        normalized_unit.startswith(unit.decimal.symbol[0].lower())):
                    return int(tokens[0] * (unit.binary.divider if binary else unit.decimal.divider))
    # We failed to parse the size specification.
    msg = "Failed to parse size! (input %r was tokenized as %r)"
    raise InvalidSize(format(msg, size, tokens))