Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions arrow/arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ def interval(
(<Arrow [2013-05-05T14:00:00+00:00]>, <Arrow [2013-05-05T15:59:59.999999+00:00]>)
(<Arrow [2013-05-05T16:00:00+00:00]>, <Arrow [2013-05-05T17:59:59.999999+00:0]>)
"""
if interval < 1:
if isinstance(interval, bool) or not isinstance(interval, int) or interval < 1:
raise ValueError("interval has to be a positive integer")

spanRange = iter(
Expand Down Expand Up @@ -1367,7 +1367,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow":

"""

# Create a locale object based off given local
# Create a locale object based off given localee
locale_obj = locales.get_locale(locale)

# Check to see if locale is supported
Expand All @@ -1380,6 +1380,14 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow":

current_time = self.fromdatetime(self._datetime)

# Reject non-string input before looking up locale-specific patterns.
# This keeps the public error stable even when a locale stores timeframes
# in a mapping that would otherwise be searched with regex operations.
if not isinstance(input_string, str):
raise TypeError(
f"input_string must be str, not {type(input_string).__name__}."
)

# Create an object containing the relative time info
time_object_info = dict.fromkeys(
["seconds", "minutes", "hours", "days", "weeks", "months", "years"], 0
Expand All @@ -1392,7 +1400,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow":
)

# Create a regex pattern object for numbers
num_pattern = re.compile(r"\d+")
num_pattern = re.compile(r"\d+(?:\.\d+)?")

# Search input string for each time unit within locale
for unit, unit_object in locale_obj.timeframes.items():
Expand All @@ -1408,7 +1416,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow":
for time_delta, time_string in strings_to_search.items():
# Replace {0} with regex \d representing digits
search_string = str(time_string)
search_string = search_string.format(r"\d+")
search_string = search_string.format(r"\d+(?:\.\d+)?")

# Create search pattern and find within string
pattern = re.compile(rf"(^|\b|\d){search_string}")
Expand All @@ -1428,7 +1436,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow":
1 if not time_delta.isnumeric() else abs(int(time_delta))
)
else:
change_value = int(num_match.group())
change_value = float(num_match.group())

# No time to update if now is the unit
if unit == "now":
Expand Down
1 change: 1 addition & 0 deletions arrow/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,7 @@ def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime:
:raises ParserError: If no format matches the input string.
"""
_datetime: Optional[datetime] = None
formats = list(formats)

for fmt in formats:
try:
Expand Down
19 changes: 16 additions & 3 deletions tests/test_arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1557,11 +1557,12 @@ def test_small_interval_exact_open_bounds(self):


class TestArrowInterval:
def test_incorrect_input(self):
with pytest.raises(ValueError):
@pytest.mark.parametrize("interval", [0, -1, 1.5, True])
def test_incorrect_input(self, interval):
with pytest.raises(ValueError, match="positive integer"):
list(
arrow.Arrow.interval(
"month", datetime(2013, 1, 2), datetime(2013, 4, 15), 0
"month", datetime(2013, 1, 2), datetime(2013, 4, 15), interval
)
)

Expand Down Expand Up @@ -2606,6 +2607,12 @@ def slavic_locales() -> List[str]:


class TestArrowDehumanize:
def test_non_string_input_raises_type_error(self):
arw = arrow.Arrow.utcnow()

with pytest.raises(TypeError, match="input_string must be str"):
arw.dehumanize(None)

def test_now(self, locale_list_no_weeks: List[str]):
for lang in locale_list_no_weeks:
arw = arrow.Arrow(2000, 6, 18, 5, 55, 0)
Expand Down Expand Up @@ -2832,6 +2839,12 @@ def test_mixed_granularity_day_hour(self, locale_list_no_weeks: List[str]):
assert arw.dehumanize(past_string, locale=lang) == past
assert arw.dehumanize(future_string, locale=lang) == future

def test_decimal_unit_quantity_preserves_fraction(self):
arw = arrow.Arrow(2000, 6, 18, 5, 55, 0)

assert arw.dehumanize("in 1.5 hours") == arw.shift(hours=1.5)
assert arw.dehumanize("1.5 hours ago") == arw.shift(hours=-1.5)

# Test to make sure unsupported locales error out
def test_unsupported_locale(self):
arw = arrow.Arrow(2000, 6, 18, 5, 55, 0)
Expand Down
9 changes: 9 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ def test_parse_multiformat_all_fail(self, mocker):
with pytest.raises(parser.ParserError):
self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"])

def test_parse_multiformat_reports_generator_formats(self, mocker):
mocker.patch(
"arrow.parser.DateTimeParser.parse",
side_effect=parser.ParserMatchError,
)

with pytest.raises(parser.ParserError, match="fmt_a, fmt_b"):
self.parser._parse_multiformat("str", (fmt for fmt in ("fmt_a", "fmt_b")))

def test_parse_multiformat_unself_expected_fail(self, mocker):
class UnselfExpectedError(Exception):
pass
Expand Down
Loading