| # Copyright 2020 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import collections |
| import functools |
| import operator |
| |
| PYTHON_VERSION_COMPATIBILITY = "PY3" |
| |
| DEPS = [ |
| "recipe_engine/step", |
| "recipe_engine/time", |
| ] |
| |
| # We store top-level util functions in __init__.py rather than api.py to make |
| # them slightly less verbose to import. |
| # Example: `from RECIPE_MODULES.fuchsia.utils import memoize` |
| |
| |
| def memoize(func): |
| """A decorator to cache the return values of a function by args/kwargs.""" |
| cache = {} |
| |
| @functools.wraps(func) |
| def wrapper(*args, **kwargs): |
| key = (args, frozenset(kwargs.items())) |
| if key not in cache: |
| cache[key] = func(*args, **kwargs) |
| return cache[key] |
| |
| return wrapper |
| |
| |
| def pluralize(singular, items_or_count, plural=None): |
| """Conditionally pluralizes a noun with its count. |
| |
| Examples: |
| plural('cat', 1) == '1 cat' |
| plural('cat', 2) == '2 cats' |
| plural('mouse', 2, plural='mice') == '2 mice' |
| plural('animal', ['cat', 'mouse']) == '2 animals' |
| |
| Args: |
| singular (str): The singular form of the noun. |
| items_or_count (int or iterable): Count to use to pluralize the noun. If |
| iterable, the length of the iterable will be used as the count. |
| plural (str or None): Plural form of the noun; should be specified for |
| irregular nouns whose plural form isn't as simple as appending an 's' |
| to the singular form. |
| """ |
| if isinstance(items_or_count, collections.Sized): |
| count = len(items_or_count) |
| else: |
| count = items_or_count |
| |
| if count == 1: |
| noun = singular |
| else: |
| noun = plural if plural else singular + "s" |
| return "%d %s" % (count, noun) |
| |
| |
| def product(nums): |
| return functools.reduce(operator.mul, nums) |
| |
| |
| def nice_duration(seconds): |
| """Returns a human-readable duration given a number of seconds. |
| |
| For example: 3605 seconds => '1h 5s' |
| """ |
| units = [ |
| ("d", 24), |
| ("h", 60), |
| ("m", 60), |
| ("s", 1), |
| ] |
| |
| result_parts = [] |
| divisor = product(unit_size for _, unit_size in units) |
| for i, (unit_abbrev, unit_size) in enumerate(units): |
| count, seconds = divmod(seconds, divisor) |
| if count > 0: |
| result_parts.append("%d%s" % (count, unit_abbrev)) |
| elif i == len(units) - 1 and not result_parts: |
| seconds = round(seconds, 1) |
| if seconds == 0: |
| result_parts.append("0%s" % unit_abbrev) |
| else: |
| result_parts.append("%.1f%s" % (seconds, unit_abbrev)) |
| divisor /= unit_size |
| |
| return " ".join(result_parts) |
| |
| |
| # Copied from recipes-py/recipe-engine/internal/class_util.py |
| # TODO(olivernewman): Switch to using `functools.cached_property` once we're |
| # running Python >= 3.8. |
| def cached_property(getter): |
| """A very basic @property-style decorator for read-only cached properties. |
| |
| The result of the first successful call to `getter` will be cached on `self` |
| with the key _cached_property_{getter.__name__}. |
| """ |
| key = "_cached_property_%s" % (getter.__name__,) |
| |
| @property |
| def _inner(self): |
| if not hasattr(self, key): |
| # object.__setattr__ is needed to cheat attr.s' freeze. This is the |
| # documented way to work around the lack of fine-grained immutability. |
| object.__setattr__(self, key, getter(self)) |
| return getattr(self, key) |
| |
| return _inner |