Source code for capybara.helpers
import re
import time
from capybara.compat import PY2, str_
from capybara.utils import decode_bytes, isbytes, isregex, isstring, encode_string
[docs]def declension(singular, plural, count):
"""
Returns the appropriate word variation for the given quantity.
Args:
singular (str): The singular variation.
plural (str): The plural variation.
count (int): The count.
Returns:
str: The appropriate variation for the quantity.
"""
return singular if count == 1 else plural
[docs]def desc(value):
""" str: A normalized representation for a user-provided value. """
def normalize_strings(value):
if isinstance(value, list):
value = [normalize_strings(e) for e in value]
if isinstance(value, dict):
value = {normalize_strings(k): normalize_strings(v) for k, v in iter(value.items())}
if isregex(value):
value = value.pattern
if isbytes(value):
value = decode_bytes(value)
if PY2:
if isstring(value):
# In Python 2, strings (``unicode`` objects) represent as ``u'...'``, so ensure
# the string is encoded (as a ``str`` object) for cleaner representation.
value = encode_string(value)
return value
value = normalize_strings(value)
return repr(value)
[docs]def expects_none(options):
"""
Returns whether the given query options expect a possible count of zero.
Args:
options (Dict[str, int | Iterable[int]]): A dictionary of query options.
Returns:
bool: Whether a possible count of zero is expected.
"""
if any(options.get(key) is not None for key in ["count", "maximum", "minimum", "between"]):
return matches_count(0, options)
else:
return False
[docs]def failure_message(description, options):
"""
Returns a expectation failure message for the given query description.
Args:
description (str): A description of the failed query.
options (Dict[str, Any]): The query options.
Returns:
str: A message describing the failure.
"""
message = "expected to find {}".format(description)
if options["count"] is not None:
message += " {count} {times}".format(
count=options["count"],
times=declension("time", "times", options["count"]))
elif options["between"] is not None:
between = options["between"]
if between:
first, last = between[0], between[-1]
else:
first, last = None, None
message += " between {first} and {last} times".format(
first=first,
last=last)
elif options["maximum"] is not None:
message += " at most {maximum} {times}".format(
maximum=options["maximum"],
times=declension("time", "times", options["maximum"]))
elif options["minimum"] is not None:
message += " at least {minimum} {times}".format(
minimum=options["minimum"],
times=declension("time", "times", options["minimum"]))
return message
[docs]def matches_count(count, options):
"""
Returns whether the given count matches the given query options.
If no quantity options are specified, any count is considered acceptable.
Args:
count (int): The count to be validated.
options (Dict[str, int | Iterable[int]]): A dictionary of query options.
Returns:
bool: Whether the count matches the options.
"""
if options.get("count") is not None:
return count == int(options["count"])
if options.get("maximum") is not None and int(options["maximum"]) < count:
return False
if options.get("minimum") is not None and int(options["minimum"]) > count:
return False
if options.get("between") is not None and count not in options["between"]:
return False
return True
[docs]def monotonic():
""" float: Returns the current time, using a monotonic clock where available. """
if hasattr(time, "monotonic"):
return time.monotonic()
else:
return time.time()
[docs]def normalize_text(value):
"""
Normalizes the given value to a string of text with extra whitespace removed.
Byte sequences are decoded. ``None`` is converted to an empty string. Everything else
is simply cast to a string.
Args:
value (Any): The data to normalize.
Returns:
str: The normalized text.
"""
if value is None:
return ""
text = decode_bytes(value) if isbytes(value) else str_(value)
return normalize_whitespace(text)
[docs]def normalize_whitespace(text):
"""
Returns the given text with outer whitespace removed and inner whitespace collapsed.
Args:
text (str): The text to normalize.
Returns:
str: The normalized text.
"""
return re.sub(r"\s+", " ", text, flags=re.UNICODE).strip()
[docs]class Timer:
"""
Args:
expire_in (int): The number of seconds from now in which this timer should expire.
"""
def __init__(self, expire_in):
self._start = monotonic()
self._expire_in = expire_in
@property
def expired(self):
""" bool: Whether this timer has expired. """
return monotonic() - self._start >= self._expire_in
@property
def stalled(self):
""" bool: Whether this timer appears to have stalled. """
return monotonic() == self._start
[docs]def toregex(text, exact=False):
"""
Returns a compiled regular expression for the given text.
Args:
text (str | RegexObject): The text to match.
exact (bool, optional): Whether the generated regular expression should match exact
strings. Defaults to False.
Returns:
RegexObject: A compiled regular expression that will match the text.
"""
if isregex(text):
return text
escaped = re.escape(normalize_text(text))
if exact:
escaped = r"\A{}\Z".format(escaped)
return re.compile(escaped)