blob: 328a741bf7532beb8da83c3eef298f74d3f6197d [file] [log] [blame]
# 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