diff --git a/src/humanize/__init__.py b/src/humanize/__init__.py index 4f54bc4..f9967ed 100644 --- a/src/humanize/__init__.py +++ b/src/humanize/__init__.py @@ -32,7 +32,10 @@ precisedelta, ) -from ._version import __version__ +try: + from ._version import __version__ +except ImportError: + __version__ = "4.12.2.dev0" __all__ = [ "__version__", diff --git a/src/humanize/lists.py b/src/humanize/lists.py index 525f0e3..7381aa7 100644 --- a/src/humanize/lists.py +++ b/src/humanize/lists.py @@ -4,35 +4,43 @@ TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Iterable from typing import Any __all__ = ["natural_list"] -def natural_list(items: list[Any]) -> str: +def natural_list(items: Iterable[Any], oxford_comma: bool = False) -> str: """Natural list. - Convert a list of items into a human-readable string with commas and 'and'. + Convert a list or iterable of items into a human-readable string with + commas and 'and'. Examples: >>> natural_list(["one", "two", "three"]) 'one, two and three' + >>> natural_list(["one", "two", "three"], oxford_comma=True) + 'one, two, and three' >>> natural_list(["one", "two"]) 'one and two' >>> natural_list(["one"]) 'one' Args: - items (list): An iterable of items. + items (iterable): An iterable of items. + oxford_comma (bool): If True, includes an Oxford comma before 'and' + for 3+ items. Returns: str: A string with commas and 'and' in the right places. """ - if not items: + item_list = [str(item) for item in items] + if not item_list: return "" - if len(items) == 1: - return str(items[0]) - elif len(items) == 2: - return f"{str(items[0])} and {str(items[1])}" + if len(item_list) == 1: + return item_list[0] + elif len(item_list) == 2: + return f"{item_list[0]} and {item_list[1]}" else: - return ", ".join([str(item) for item in items[:-1]]) + f" and {str(items[-1])}" + sep = ", and " if oxford_comma else " and " + return ", ".join(item_list[:-1]) + f"{sep}{item_list[-1]}" diff --git a/src/humanize/number.py b/src/humanize/number.py index 2fb22c6..593b408 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -109,7 +109,8 @@ def ordinal(value: NumberOrString, gender: str = "male") -> str: except (TypeError, ValueError): return str(value) gender = "male" if gender == "male" else "female" - digit = 0 if value % 100 in (11, 12, 13) else value % 10 + abs_value = abs(value) + digit = 0 if abs_value % 100 in (11, 12, 13) else abs_value % 10 return f"{value}{P_(f'{digit} ({gender})', _ORDINAL_SUFFIXES[digit])}" diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py index c200dd0..a85127f 100644 --- a/tests/test_benchmarks.py +++ b/tests/test_benchmarks.py @@ -6,6 +6,8 @@ import pytest +pytest.importorskip("pytest_codspeed") + import humanize TYPE_CHECKING = False diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 6ea61a2..63c4380 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -6,10 +6,12 @@ import importlib import pytest -from freezegun import freeze_time import humanize +freezegun = pytest.importorskip("freezegun") +freeze_time = freezegun.freeze_time + with freeze_time("2020-02-02"): NOW = dt.datetime.now(tz=dt.timezone.utc) diff --git a/tests/test_lists.py b/tests/test_lists.py index cc514f3..bbd4bf6 100644 --- a/tests/test_lists.py +++ b/tests/test_lists.py @@ -16,9 +16,20 @@ ([[""]], ""), ([[1, 2, 3]], "1, 2 and 3"), ([[1, "two"]], "1 and two"), + ([("a", "b", "c")], "a, b and c"), ], ) def test_natural_list( test_args: list[str] | list[int] | list[str | int], expected: str ) -> None: assert humanize.natural_list(*test_args) == expected + + +def test_natural_list_generator_and_oxford_comma() -> None: + gen = (x for x in ["alpha", "beta", "gamma"]) + assert humanize.natural_list(gen) == "alpha, beta and gamma" + assert ( + humanize.natural_list(["one", "two", "three"], oxford_comma=True) + == "one, two, and three" + ) + assert humanize.natural_list(["one", "two"], oxford_comma=True) == "one and two" diff --git a/tests/test_number.py b/tests/test_number.py index 78639c3..b2c3a0c 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -25,6 +25,12 @@ ("102", "102nd"), ("103", "103rd"), ("111", "111th"), + ("-1", "-1st"), + ("-2", "-2nd"), + ("-3", "-3rd"), + ("-11", "-11th"), + ("-21", "-21st"), + ("-22", "-22nd"), ("something else", "something else"), (None, "None"), (math.nan, "NaN"), diff --git a/tests/test_time.py b/tests/test_time.py index 7699770..6488045 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -6,11 +6,13 @@ import typing import pytest -from freezegun import freeze_time import humanize from humanize import time +freezegun = pytest.importorskip("freezegun") +freeze_time = freezegun.freeze_time + ONE_DAY_DELTA = dt.timedelta(days=1) # In seconds