diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7af3b16d..95b943ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,3 +40,9 @@ repos: - id: pydocstyle args: ["--convention", "google"] files: "src/" + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.782 + hooks: + - id: mypy + additional_dependencies: [typing-extensions==3.7.4.3] diff --git a/src/humanize/filesize.py b/src/humanize/filesize.py index 5497e8e6..2ad69174 100644 --- a/src/humanize/filesize.py +++ b/src/humanize/filesize.py @@ -2,14 +2,21 @@ """Bits and bytes related humanization.""" -suffixes = { +import typing + +suffixes: typing.Dict[str, typing.Tuple[str, ...]] = { "decimal": ("kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"), "binary": ("KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"), - "gnu": "KMGTPEZY", + "gnu": ("K", "M", "G", "T", "P", "E", "Z", "Y"), } -def naturalsize(value, binary=False, gnu=False, format="%.1f"): +def naturalsize( + value: typing.Union[int, float, str], + binary: bool = False, + gnu: bool = False, + format: str = "%.1f", +) -> str: """Format a number of bytes like a human readable filesize (e.g. 10 kB). By default, decimal suffixes (kB, MB) are used. diff --git a/src/humanize/i18n.py b/src/humanize/i18n.py index 95e7be94..2e79238a 100644 --- a/src/humanize/i18n.py +++ b/src/humanize/i18n.py @@ -1,11 +1,14 @@ """Activate, get and deactivate translations.""" import gettext as gettext_module import os.path +import typing from threading import local __all__ = ["activate", "deactivate", "gettext", "ngettext"] -_TRANSLATIONS = {None: gettext_module.NullTranslations()} +_TRANSLATIONS: typing.Dict[typing.Optional[str], gettext_module.NullTranslations] = { + None: gettext_module.NullTranslations() +} _CURRENT = local() @@ -18,14 +21,16 @@ def _get_default_locale_path(): return None -def get_translation(): +def get_translation() -> gettext_module.NullTranslations: try: return _TRANSLATIONS[_CURRENT.locale] except (AttributeError, KeyError): return _TRANSLATIONS[None] -def activate(locale, path=None): +def activate( + locale: str, path: typing.Optional[str] = None +) -> gettext_module.NullTranslations: """Activate internationalisation. Set `locale` as current locale. Search for locale in directory `path`. @@ -55,12 +60,12 @@ def activate(locale, path=None): return _TRANSLATIONS[locale] -def deactivate(): +def deactivate() -> None: """Deactivate internationalisation.""" _CURRENT.locale = None -def gettext(message): +def gettext(message: str) -> str: """Get translation. Args: @@ -72,7 +77,7 @@ def gettext(message): return get_translation().gettext(message) -def pgettext(msgctxt, message): +def pgettext(msgctxt: str, message: str) -> str: """Fetches a particular translation. It works with `msgctxt` .po modifiers and allows duplicate keys with different @@ -97,13 +102,13 @@ def pgettext(msgctxt, message): return message if translation == key else translation -def ngettext(message, plural, num): +def ngettext(message: str, plural: str, num: int) -> str: """Plural version of gettext. Args: message (str): Singular text to translate. plural (str): Plural text to translate. - num (str): The number (e.g. item count) to determine translation for the + num (int): The number (e.g. item count) to determine translation for the respective grammatical number. Returns: @@ -112,7 +117,7 @@ def ngettext(message, plural, num): return get_translation().ngettext(message, plural, num) -def gettext_noop(message): +def gettext_noop(message: str) -> str: """Mark a string as a translation string without translating it. Example usage: diff --git a/src/humanize/number.py b/src/humanize/number.py index 0fef81f1..e46714d5 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -3,19 +3,24 @@ """Humanizing functions for numbers.""" import re +import typing from fractions import Fraction from .i18n import gettext as _ from .i18n import gettext_noop as N_ from .i18n import pgettext as P_ +# This type can be better defined by typing.SupportsInt, typing.SupportsFloat +# but that's a Python 3.8 only typing option. +NumberOrString = typing.Union[str, float, int] -def ordinal(value): + +def ordinal(value: NumberOrString) -> str: """Converts an integer to its ordinal as a string. For example, 1 is "1st", 2 is "2nd", 3 is "3rd", etc. Works for any integer or - anything `int()` will turn into an integer. Anything other value will have nothing - done to it. + anything `int()` will turn into an integer. Anything else will just return + the output of str(value). done to it. Examples: ```pycon @@ -35,7 +40,7 @@ def ordinal(value): '111th' >>> ordinal("something else") 'something else' - >>> ordinal(None) is None + >>> ordinal([1, 2, 3]) == "[1, 2, 3]" True ``` @@ -48,7 +53,7 @@ def ordinal(value): try: value = int(value) except (TypeError, ValueError): - return value + return str(value) t = ( P_("0", "th"), P_("1", "st"), @@ -66,7 +71,7 @@ def ordinal(value): return f"{value}{t[value % 10]}" -def intcomma(value, ndigits=None): +def intcomma(value: NumberOrString, ndigits: typing.Optional[int] = None) -> str: """Converts an integer to a string containing commas every three digits. For example, 3000 becomes "3,000" and 45000 becomes "45,000". To maintain some @@ -86,8 +91,8 @@ def intcomma(value, ndigits=None): '1,234.55' >>> intcomma(14308.40, 1) '14,308.4' - >>> intcomma(None) is None - True + >>> intcomma(None) + 'None' ``` Args: @@ -103,7 +108,7 @@ def intcomma(value, ndigits=None): else: float(value) except (TypeError, ValueError): - return value + return str(value) if ndigits: orig = "{0:.{1}f}".format(value, ndigits) @@ -117,8 +122,10 @@ def intcomma(value, ndigits=None): return intcomma(new) -powers = [10 ** x for x in (6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 100)] -human_powers = ( +powers: typing.List[int] = [ + 10 ** x for x in (6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 100) +] +human_powers: typing.Tuple[str, ...] = ( N_("million"), N_("billion"), N_("trillion"), @@ -133,7 +140,7 @@ def intcomma(value, ndigits=None): ) -def intword(value, format="%.1f"): +def intword(value: NumberOrString, format: str = "%.1f") -> str: """Converts a large integer to a friendly text representation. Works best for numbers over 1 million. For example, 1_000_000 becomes "1.0 million", @@ -150,8 +157,8 @@ def intword(value, format="%.1f"): '1.2 billion' >>> intword(8100000000000000000000000000000000) '8.1 decillion' - >>> intword(None) is None - True + >>> intword(None) + 'None' >>> intword("1234000", "%0.3f") '1.234 million' @@ -163,12 +170,12 @@ def intword(value, format="%.1f"): Returns: str: Friendly text representation as a string, unless the value passed could not - be coaxed into an `int`. + be coaxed into an `int`, in which case, `str(value)` is returned. """ try: value = int(value) except (TypeError, ValueError): - return value + return str(value) if value < powers[0]: return str(value) @@ -183,7 +190,7 @@ def intword(value, format="%.1f"): return str(value) -def apnumber(value): +def apnumber(value: NumberOrString) -> str: """Converts an integer to Associated Press style. Examples: @@ -198,8 +205,8 @@ def apnumber(value): 'seven' >>> apnumber("foo") 'foo' - >>> apnumber(None) is None - True + >>> apnumber(None) + 'None' ``` Args: @@ -207,12 +214,13 @@ def apnumber(value): Returns: str: For numbers 0-9, the number spelled out. Otherwise, the number. This always - returns a string unless the value was not `int`-able, unlike the Django filter. + returns a string. If the value was not `int`-able, then `str(value)` + is returned. """ try: value = int(value) except (TypeError, ValueError): - return value + return str(value) if not 0 <= value < 10: return str(value) return ( @@ -229,7 +237,7 @@ def apnumber(value): )[value] -def fractional(value): +def fractional(value: NumberOrString) -> str: """Convert to fractional number. There will be some cases where one might not want to show ugly decimal places for @@ -243,6 +251,7 @@ def fractional(value): * a string representation of a fraction * or a whole number * or a mixed fraction + * or the str output of the value, if it could not be converted Examples: ```pycon @@ -256,8 +265,8 @@ def fractional(value): '1' >>> fractional("ten") 'ten' - >>> fractional(None) is None - True + >>> fractional(None) + 'None' ``` Args: @@ -269,11 +278,11 @@ def fractional(value): try: number = float(value) except (TypeError, ValueError): - return value + return str(value) whole_number = int(number) frac = Fraction(number - whole_number).limit_denominator(1000) - numerator = frac._numerator - denominator = frac._denominator + numerator = frac.numerator + denominator = frac.denominator if whole_number and not numerator and denominator == 1: # this means that an integer was passed in # (or variants of that integer like 1.0000) @@ -284,7 +293,7 @@ def fractional(value): return f"{whole_number:.0f} {numerator:.0f}/{denominator:.0f}" -def scientific(value, precision=2): +def scientific(value: NumberOrString, precision: int = 2) -> str: """Return number in string scientific notation z.wq x 10ⁿ. Examples: @@ -303,8 +312,8 @@ def scientific(value, precision=2): '9.90 x 10¹' >>> scientific("foo") 'foo' - >>> scientific(None) is None - True + >>> scientific(None) + 'None' ``` @@ -342,7 +351,7 @@ def scientific(value, precision=2): n = fmt.format(value) except (ValueError, TypeError): - return value + return str(value) part1, part2 = n.split("e") if "-0" in part2: diff --git a/src/humanize/py.typed b/src/humanize/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/humanize/time.py b/src/humanize/time.py index 319d7e41..f7b9563a 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -7,12 +7,28 @@ import datetime as dt import math +import typing from enum import Enum from functools import total_ordering from .i18n import gettext as _ from .i18n import ngettext +if typing.TYPE_CHECKING: + from typing_extensions import Literal + + UnitString = Literal[ + "microseconds", + "milliseconds", + "seconds", + "minutes", + "hours", + "days", + "months", + "years", + ] + + __all__ = [ "naturaldelta", "naturaltime", @@ -33,17 +49,17 @@ class Unit(Enum): MONTHS = 6 YEARS = 7 - def __lt__(self, other): + def __lt__(self, other: typing.Any) -> typing.Any: if self.__class__ is other.__class__: return self.value < other.value return NotImplemented -def _now(): +def _now() -> dt.datetime: return dt.datetime.now() -def abs_timedelta(delta): +def abs_timedelta(delta: dt.timedelta) -> dt.timedelta: """Return an "absolute" value for a timedelta, always representing a time distance. Args: @@ -58,7 +74,9 @@ def abs_timedelta(delta): return delta -def date_and_delta(value, *, now=None): +def date_and_delta( + value, *, now: typing.Optional[dt.datetime] = None +) -> typing.Tuple[typing.Any, typing.Any]: """Turn a value into a date and a timedelta which represents how long ago it was. If that's not possible, return `(None, value)`. @@ -81,7 +99,12 @@ def date_and_delta(value, *, now=None): return date, abs_timedelta(delta) -def naturaldelta(value, months=True, minimum_unit="seconds", when=None): +def naturaldelta( + value: typing.Union[dt.timedelta, int], + months: bool = True, + minimum_unit: 'Literal["seconds", "milliseconds", "microseconds"]' = "seconds", + when: typing.Optional[dt.datetime] = None, +) -> str: """Return a natural representation of a timedelta or number of seconds. This is similar to `naturaltime`, but does not add tense to the result. @@ -91,7 +114,7 @@ def naturaldelta(value, months=True, minimum_unit="seconds", when=None): months (bool): If `True`, then a number of months (based on 30.5 days) will be used for fuzziness between years. minimum_unit (str): The lowest unit that can be used. - when (datetime.timedelta): Point in time relative to which _value_ is + when (datetime.datetime): Point in time relative to which _value_ is interpreted. Defaults to the current time in the local timezone. Returns: @@ -112,11 +135,11 @@ def naturaldelta(value, months=True, minimum_unit="seconds", when=None): tmp = Unit[minimum_unit.upper()] if tmp not in (Unit.SECONDS, Unit.MILLISECONDS, Unit.MICROSECONDS): raise ValueError(f"Minimum unit '{minimum_unit}' not supported") - minimum_unit = tmp + min_u = tmp date, delta = date_and_delta(value, now=when) if date is None: - return value + return str(value) use_months = months @@ -124,18 +147,17 @@ def naturaldelta(value, months=True, minimum_unit="seconds", when=None): days = abs(delta.days) years = days // 365 days = days % 365 - months = int(days // 30.5) + num_months = int(days // 30.5) if not years and days < 1: if seconds == 0: - if minimum_unit == Unit.MICROSECONDS and delta.microseconds < 1000: + if min_u == Unit.MICROSECONDS and delta.microseconds < 1000: return ( ngettext("%d microsecond", "%d microseconds", delta.microseconds) % delta.microseconds ) - elif minimum_unit == Unit.MILLISECONDS or ( - minimum_unit == Unit.MICROSECONDS - and 1000 <= delta.microseconds < 1_000_000 + elif min_u == Unit.MILLISECONDS or ( + min_u == Unit.MICROSECONDS and 1000 <= delta.microseconds < 1_000_000 ): milliseconds = delta.microseconds / 1000 return ( @@ -154,7 +176,7 @@ def naturaldelta(value, months=True, minimum_unit="seconds", when=None): return ngettext("%d minute", "%d minutes", minutes) % minutes elif 3600 <= seconds < 3600 * 2: return _("an hour") - elif 3600 < seconds: + else: hours = seconds // 3600 return ngettext("%d hour", "%d hours", hours) % hours elif years == 0: @@ -163,23 +185,24 @@ def naturaldelta(value, months=True, minimum_unit="seconds", when=None): if not use_months: return ngettext("%d day", "%d days", days) % days else: - if not months: + if not num_months: return ngettext("%d day", "%d days", days) % days - elif months == 1: + elif num_months == 1: return _("a month") else: - return ngettext("%d month", "%d months", months) % months + return ngettext("%d month", "%d months", num_months) % num_months elif years == 1: - if not months and not days: + if not num_months and not days: return _("a year") - elif not months: + elif not num_months: return ngettext("1 year, %d day", "1 year, %d days", days) % days elif use_months: - if months == 1: + if num_months == 1: return _("1 year, 1 month") else: return ( - ngettext("1 year, %d month", "1 year, %d months", months) % months + ngettext("1 year, %d month", "1 year, %d months", num_months) + % num_months ) else: return ngettext("1 year, %d day", "1 year, %d days", days) % days @@ -187,7 +210,13 @@ def naturaldelta(value, months=True, minimum_unit="seconds", when=None): return ngettext("%d year", "%d years", years) % years -def naturaltime(value, future=False, months=True, minimum_unit="seconds", when=None): +def naturaltime( + value: typing.Union[dt.datetime, int], + future: bool = False, + months: bool = True, + minimum_unit: 'Literal["seconds", "milliseconds", "microseconds"]' = "seconds", + when: typing.Optional[dt.datetime] = None, +) -> str: """Return a natural representation of a time in a resolution that makes sense. This is more or less compatible with Django's `naturaltime` filter. @@ -209,7 +238,7 @@ def naturaltime(value, future=False, months=True, minimum_unit="seconds", when=N now = when or _now() date, delta = date_and_delta(value, now=now) if date is None: - return value + return str(value) # determine tense by value only if datetime/timedelta were passed if isinstance(value, (dt.datetime, dt.timedelta)): future = date > now @@ -223,7 +252,7 @@ def naturaltime(value, future=False, months=True, minimum_unit="seconds", when=N return ago % delta -def naturalday(value, format="%b %d"): +def naturalday(value: typing.Union[dt.date, dt.datetime], format: str = "%b %d") -> str: """Return a natural day. For date values that are tomorrow, today or yesterday compared to @@ -235,10 +264,10 @@ def naturalday(value, format="%b %d"): value = dt.date(value.year, value.month, value.day) except AttributeError: # Passed value wasn't date-ish - return value + return str(value) except (OverflowError, ValueError): # Date arguments out of range - return value + return str(value) delta = value - dt.date.today() if delta.days == 0: return _("today") @@ -249,23 +278,25 @@ def naturalday(value, format="%b %d"): return value.strftime(format) -def naturaldate(value): +def naturaldate(value: typing.Union[dt.date, dt.datetime]) -> str: """Like `naturalday`, but append a year for dates more than ~five months away.""" try: value = dt.date(value.year, value.month, value.day) except AttributeError: # Passed value wasn't date-ish - return value + return str(value) except (OverflowError, ValueError): # Date arguments out of range - return value + return str(value) delta = abs_timedelta(value - dt.date.today()) if delta.days >= 5 * 365 / 12: return naturalday(value, "%b %d %Y") return naturalday(value) -def _quotient_and_remainder(value, divisor, unit, minimum_unit, suppress): +def _quotient_and_remainder( + value, divisor, unit, minimum_unit, suppress +) -> typing.Tuple[float, float]: """Divide `value` by `divisor` returning the quotient and remainder. If `unit` is `minimum_unit`, makes the quotient a float number and the remainder @@ -298,7 +329,9 @@ def _quotient_and_remainder(value, divisor, unit, minimum_unit, suppress): return divmod(value, divisor) -def _carry(value1, value2, ratio, unit, min_unit, suppress): +def _carry( + value1, value2, ratio, unit, min_unit, suppress +) -> typing.Tuple[float, float]: """Return a tuple with two values. If the unit is in `suppress`, multiply `value1` by `ratio` and add it to `value2` @@ -329,7 +362,7 @@ def _carry(value1, value2, ratio, unit, min_unit, suppress): return (value1, value2) -def _suitable_minimum_unit(min_unit, suppress): +def _suitable_minimum_unit(min_unit: Unit, suppress: typing.Iterable[Unit]) -> Unit: """Return a minimum unit suitable that is not suppressed. If not suppressed, return the same unit: @@ -347,9 +380,10 @@ def _suitable_minimum_unit(min_unit, suppress): >>> _suitable_minimum_unit(Unit.HOURS, [Unit.HOURS, Unit.DAYS]) """ - if min_unit in suppress: + suppress_set = set(suppress) + if min_unit in suppress_set: for unit in Unit: - if unit > min_unit and unit not in suppress: + if unit > min_unit and unit not in suppress_set: return unit raise ValueError( @@ -359,7 +393,9 @@ def _suitable_minimum_unit(min_unit, suppress): return min_unit -def _suppress_lower_units(min_unit, suppress): +def _suppress_lower_units( + min_unit: Unit, suppress: typing.Iterable[Unit] +) -> typing.Set[Unit]: """Extend suppressed units (if any) with all units lower than the minimum unit. >>> from humanize.time import _suppress_lower_units, Unit @@ -375,7 +411,12 @@ def _suppress_lower_units(min_unit, suppress): return suppress -def precisedelta(value, minimum_unit="seconds", suppress=(), format="%0.2f"): +def precisedelta( + value: typing.Union[dt.timedelta, int], + minimum_unit: "UnitString" = "seconds", + suppress: typing.Iterable["UnitString"] = (), + format: str = "%0.2f", +) -> str: """Return a precise representation of a timedelta. ```pycon @@ -430,19 +471,19 @@ def precisedelta(value, minimum_unit="seconds", suppress=(), format="%0.2f"): """ date, delta = date_and_delta(value) if date is None: - return value + return str(value) - suppress = [Unit[s.upper()] for s in suppress] + suppress_set = {Unit[s.upper()] for s in suppress} # Find a suitable minimum unit (it can be greater the one that the # user gave us if it is suppressed). min_unit = Unit[minimum_unit.upper()] - min_unit = _suitable_minimum_unit(min_unit, suppress) + min_unit = _suitable_minimum_unit(min_unit, suppress_set) del minimum_unit # Expand the suppressed units list/set to include all the units # that are below the minimum unit - suppress = _suppress_lower_units(min_unit, suppress) + suppress_set = _suppress_lower_units(min_unit, suppress_set) # handy aliases days = delta.days @@ -465,27 +506,27 @@ def precisedelta(value, minimum_unit="seconds", suppress=(), format="%0.2f"): # years, days = divmod(years, days) # # The same applies for months, hours, minutes and milliseconds below - years, days = _quotient_and_remainder(days, 365, YEARS, min_unit, suppress) - months, days = _quotient_and_remainder(days, 30.5, MONTHS, min_unit, suppress) + years, days = _quotient_and_remainder(days, 365, YEARS, min_unit, suppress_set) + months, days = _quotient_and_remainder(days, 30.5, MONTHS, min_unit, suppress_set) - # If DAYS is not in suppress, we can represent the days but + # If DAYS is not in suppress_set, we can represent the days but # if it is a suppressed unit, we need to carry it to a lower unit, # seconds in this case. # # The same applies for secs and usecs below - days, secs = _carry(days, secs, 24 * 3600, DAYS, min_unit, suppress) + days, secs = _carry(days, secs, 24 * 3600, DAYS, min_unit, suppress_set) - hours, secs = _quotient_and_remainder(secs, 3600, HOURS, min_unit, suppress) - minutes, secs = _quotient_and_remainder(secs, 60, MINUTES, min_unit, suppress) + hours, secs = _quotient_and_remainder(secs, 3600, HOURS, min_unit, suppress_set) + minutes, secs = _quotient_and_remainder(secs, 60, MINUTES, min_unit, suppress_set) - secs, usecs = _carry(secs, usecs, 1e6, SECONDS, min_unit, suppress) + secs, usecs = _carry(secs, usecs, 1e6, SECONDS, min_unit, suppress_set) msecs, usecs = _quotient_and_remainder( - usecs, 1000, MILLISECONDS, min_unit, suppress + usecs, 1000, MILLISECONDS, min_unit, suppress_set ) # if _unused != 0 we had lost some precision - usecs, _unused = _carry(usecs, 0, 1, MICROSECONDS, min_unit, suppress) + usecs, _unused = _carry(usecs, 0, 1, MICROSECONDS, min_unit, suppress_set) fmts = [ ("%d year", "%d years", years), @@ -500,13 +541,13 @@ def precisedelta(value, minimum_unit="seconds", suppress=(), format="%0.2f"): texts = [] for unit, fmt in zip(reversed(Unit), fmts): - singular_txt, plural_txt, value = fmt - if value > 0: - fmt_txt = ngettext(singular_txt, plural_txt, value) - if unit == min_unit and math.modf(value)[0] > 0: + singular_txt, plural_txt, fmt_value = fmt + if fmt_value > 0: + fmt_txt = ngettext(singular_txt, plural_txt, fmt_value) + if unit == min_unit and math.modf(fmt_value)[0] > 0: fmt_txt = fmt_txt.replace("%d", format) - texts.append(fmt_txt % value) + texts.append(fmt_txt % fmt_value) if unit == min_unit: break diff --git a/tests/test_number.py b/tests/test_number.py index 347217d2..bcead0a6 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -22,7 +22,7 @@ ("103", "103rd"), ("111", "111th"), ("something else", "something else"), - (None, None), + (None, "None"), ], ) def test_ordinal(test_input, expected): @@ -44,7 +44,7 @@ def test_ordinal(test_input, expected): (["10311"], "10,311"), (["1000000"], "1,000,000"), (["1234567.1234567"], "1,234,567.1234567"), - ([None], None), + ([None], "None"), ([14308.40], "14,308.4"), ([14308.40, None], "14,308.4"), ([14308.40, 1], "14,308.4"), @@ -85,7 +85,7 @@ def test_intword_powers(): (["1300000000000000"], "1.3 quadrillion"), (["3500000000000000000000"], "3.5 sextillion"), (["8100000000000000000000000000000000"], "8.1 decillion"), - ([None], None), + ([None], "None"), (["1230000", "%0.2f"], "1.23 million"), ([10 ** 101], "1" + "0" * 101), ], @@ -105,7 +105,7 @@ def test_intword(test_args, expected): (9, "nine"), (10, "10"), ("7", "seven"), - (None, None), + (None, "None"), ], ) def test_apnumber(test_input, expected): @@ -122,7 +122,7 @@ def test_apnumber(test_input, expected): ("7", "7"), ("8.9", "8 9/10"), ("ten", "ten"), - (None, None), + (None, "None"), (1 / 3, "1/3"), (1.5, "1 1/2"), (0.3, "3/10"), @@ -144,7 +144,7 @@ def test_fractional(test_input, expected): (["99"], "9.90 x 10¹"), ([float(0.3)], "3.00 x 10⁻¹"), (["foo"], "foo"), - ([None], None), + ([None], "None"), ([1000, 1], "1.0 x 10³"), ([float(0.3), 1], "3.0 x 10⁻¹"), ([1000, 0], "1 x 10³"), diff --git a/tests/test_time.py b/tests/test_time.py index 90c01300..a4bb257d 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -214,10 +214,10 @@ def test_naturaltime_nomonths(test_input, expected): ([dt.date(TODAY.year, 3, 5)], "Mar 05"), (["02/26/1984"], "02/26/1984"), ([dt.date(1982, 6, 27), "%Y.%m.%d"], "1982.06.27"), - ([None], None), + ([None], "None"), (["Not a date at all."], "Not a date at all."), - ([VALUE_ERROR_TEST], VALUE_ERROR_TEST), - ([OVERFLOW_ERROR_TEST], OVERFLOW_ERROR_TEST), + ([VALUE_ERROR_TEST], str(VALUE_ERROR_TEST)), + ([OVERFLOW_ERROR_TEST], str(OVERFLOW_ERROR_TEST)), ], ) def test_naturalday(test_args, expected): @@ -233,10 +233,10 @@ def test_naturalday(test_args, expected): (YESTERDAY, "yesterday"), (dt.date(TODAY.year, 3, 5), "Mar 05"), (dt.date(1982, 6, 27), "Jun 27 1982"), - (None, None), + (None, str(None)), ("Not a date at all.", "Not a date at all."), - (VALUE_ERROR_TEST, VALUE_ERROR_TEST), - (OVERFLOW_ERROR_TEST, OVERFLOW_ERROR_TEST), + (VALUE_ERROR_TEST, str(VALUE_ERROR_TEST)), + (OVERFLOW_ERROR_TEST, str(OVERFLOW_ERROR_TEST)), (dt.date(2019, 2, 2), "Feb 02 2019"), (dt.date(2019, 3, 2), "Mar 02 2019"), (dt.date(2019, 4, 2), "Apr 02 2019"), @@ -629,7 +629,8 @@ def test_precisedelta_suppress_units(val, min_unit, suppress, expected): def test_precisedelta_bogus_call(): - assert humanize.precisedelta(None) is None + bogus = object() + assert humanize.precisedelta(bogus) == str(bogus) with pytest.raises(ValueError): humanize.precisedelta(1, minimum_unit="years", suppress=["years"])