Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop support for EOL Python 2 #102

Merged
merged 10 commits into from
Feb 11, 2020
Merged
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
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v1.26.2
hooks:
- id: pyupgrade
args: ["--py3-plus"]

- repo: https://github.com/psf/black
rev: 19.10b0
hooks:
- id: black
args: ["--target-version", "py35"]

- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
Expand All @@ -29,4 +36,5 @@ repos:
rev: v2.5.0
hooks:
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
4 changes: 1 addition & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ language: python
cache: pip

python:
- 2.7
- pypy3
- 3.5
- 3.6
- 3.7
- 3.8
- 3.9-dev
- pypy
- pypy3

install:
- pip install -U pip
Expand Down
37 changes: 21 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
[![MIT License](https://img.shields.io/github/license/jmoiron/humanize.svg)](LICENSE)

This modest package contains various common humanization utilities, like turning
a number into a fuzzy human readable duration ('3 minutes ago') or into a human
readable size or throughput. It works with Python 2.7 and 3.5+ and is localized
to:
a number into a fuzzy human readable duration ("3 minutes ago") or into a human
readable size or throughput. It is localized to:

* Brazilian Portuguese
* Dutch
* Finnish
* French
Expand All @@ -22,7 +22,6 @@ to:
* Japanese
* Korean
* Persian
* Portuguese Brazilian
* Russian
* Simplified Chinese
* Slovak
Expand Down Expand Up @@ -50,26 +49,28 @@ Integer humanization:
Date & time humanization:

```pycon
>>> import datetime
>>> humanize.naturalday(datetime.datetime.now())
>>> import humanize
>>> import datetime as dt
>>> humanize.naturalday(dt.datetime.now())
'today'
>>> humanize.naturaldelta(datetime.timedelta(seconds=1001))
>>> humanize.naturaldelta(dt.timedelta(seconds=1001))
'16 minutes'
>>> humanize.naturalday(datetime.datetime.now() - datetime.timedelta(days=1))
>>> humanize.naturalday(dt.datetime.now() - dt.timedelta(days=1))
'yesterday'
>>> humanize.naturalday(datetime.date(2007, 6, 5))
>>> humanize.naturalday(dt.date(2007, 6, 5))
'Jun 05'
>>> humanize.naturaldate(datetime.date(2007, 6, 5))
>>> humanize.naturaldate(dt.date(2007, 6, 5))
'Jun 05 2007'
>>> humanize.naturaltime(datetime.datetime.now() - datetime.timedelta(seconds=1))
>>> humanize.naturaltime(dt.datetime.now() - dt.timedelta(seconds=1))
'a second ago'
>>> humanize.naturaltime(datetime.datetime.now() - datetime.timedelta(seconds=3600))
>>> humanize.naturaltime(dt.datetime.now() - dt.timedelta(seconds=3600))
'an hour ago'
```

File size humanization:

```pycon
>>> import humanize
>>> humanize.naturalsize(1000000)
'1.0 MB'
>>> humanize.naturalsize(1000000, binary=True)
Expand All @@ -81,6 +82,7 @@ File size humanization:
Human readable floating point numbers:

```pycon
>>> import humanize
>>> humanize.fractional(1/3)
'1/3'
>>> humanize.fractional(1.5)
Expand All @@ -98,20 +100,23 @@ Human readable floating point numbers:
How to change locale at runtime:

```pycon
>>> print(humanize.naturaltime(datetime.timedelta(seconds=3)))
>>> import humanize
>>> import datetime as dt
>>> humanize.naturaltime(dt.timedelta(seconds=3))
3 seconds ago
>>> _t = humanize.i18n.activate('ru_RU')
>>> print(humanize.naturaltime(datetime.timedelta(seconds=3)))
>>> _t = humanize.i18n.activate("ru_RU")
>>> humanize.naturaltime(dt.timedelta(seconds=3))
3 секунды назад
>>> humanize.i18n.deactivate()
>>> print(humanize.naturaltime(datetime.timedelta(seconds=3)))
>>> humanize.naturaltime(dt.timedelta(seconds=3))
3 seconds ago
```

You can pass additional parameter `path` to `activate` to specify a path to search
locales in.

```pycon
>>> import humanize
>>> humanize.i18n.activate("pt_BR")
IOError: [Errno 2] No translation file found for domain: 'humanize'
>>> humanize.i18n.activate("pt_BR", path="path/to/my/portuguese/translation/")
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tool.black]
target_version = ["py35"]
3 changes: 0 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[bdist_wheel]
universal = 1

[flake8]
max_line_length = 88

Expand Down
14 changes: 4 additions & 10 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import io

from setuptools import find_packages, setup

with io.open("README.md", encoding="UTF-8") as f:
with open("README.md", encoding="UTF-8") as f:
long_description = f.read()


Expand All @@ -29,25 +27,21 @@ def local_scheme(version):
zip_safe=False,
use_scm_version={"local_scheme": local_scheme},
setup_requires=["setuptools_scm"],
extras_require={
"tests": ["freezegun", "pytest", "pytest-cov"],
"tests:python_version < '3.4'": ["mock"],
},
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
extras_require={"tests": ["freezegun", "pytest", "pytest-cov"]},
python_requires=">=3.5",
# Get strings from https://pypi.org/pypi?%3Aaction=list_classifiers
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Text Processing",
Expand Down
6 changes: 0 additions & 6 deletions src/humanize/compat.py

This file was deleted.

1 change: 0 additions & 1 deletion src/humanize/filesize.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Bits & Bytes related humanization."""

Expand Down
17 changes: 11 additions & 6 deletions src/humanize/i18n.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import gettext as gettext_module
import os.path
from threading import local
Expand All @@ -20,7 +19,8 @@ def get_translation():

def activate(locale, path=None):
"""Set 'locale' as current locale. Search for locale in directory 'path'
@param locale: language name, eg 'en_GB'"""
@param locale: language name, eg 'en_GB'
@param path: path to search for locales"""
if path is None:
path = _DEFAULT_LOCALE_PATH
if locale not in _TRANSLATIONS:
Expand All @@ -42,11 +42,16 @@ def pgettext(msgctxt, message):
"""'Particular gettext' function.
It works with 'msgctxt' .po modifiers and allow duplicate keys with
different translations.
Python 2 don't have support for this GNU gettext function, so we
This GNU gettext function was added in Python 3.8, so for older versions we
reimplement it. It works by joining msgctx and msgid by '4' byte."""
key = msgctxt + "\x04" + message
translation = get_translation().gettext(key)
return message if translation == key else translation
try:
# Python 3.8+
return get_translation().pgettext(msgctxt, message)
except AttributeError:
# Python 3.7 and older
key = msgctxt + "\x04" + message
translation = get_translation().gettext(key)
return message if translation == key else translation


def ngettext(message, plural, num):
Expand Down
18 changes: 8 additions & 10 deletions src/humanize/number.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Humanizing functions for numbers."""

import re
from fractions import Fraction

from . import compat
from .i18n import gettext as _
from .i18n import gettext_noop as N_
from .i18n import pgettext as P_
Expand Down Expand Up @@ -43,7 +41,7 @@ def intcomma(value):
some compatibility with Django's intcomma, this function also accepts
floats."""
try:
if isinstance(value, compat.string_types):
if isinstance(value, str):
float(value.replace(",", ""))
else:
float(value)
Expand Down Expand Up @@ -138,15 +136,15 @@ def fractional(value):
number = float(value)
except (TypeError, ValueError):
return value
wholeNumber = int(number)
frac = Fraction(number - wholeNumber).limit_denominator(1000)
whole_number = int(number)
frac = Fraction(number - whole_number).limit_denominator(1000)
numerator = frac._numerator
denominator = frac._denominator
if wholeNumber and not numerator and denominator == 1:
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)
return "%.0f" % wholeNumber
elif not wholeNumber:
return "%.0f/%.0f" % (numerator, denominator)
return "%.0f" % whole_number
elif not whole_number:
return "{:.0f}/{:.0f}".format(numerator, denominator)
else:
return "%.0f %.0f/%.0f" % (wholeNumber, numerator, denominator)
return "{:.0f} {:.0f}/{:.0f}".format(whole_number, numerator, denominator)
23 changes: 11 additions & 12 deletions src/humanize/time.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Time humanizing functions. These are largely borrowed from Django's
``contrib.humanize``."""

from datetime import date, datetime, timedelta
import datetime as dt

from .i18n import gettext as _
from .i18n import ngettext
Expand All @@ -13,7 +12,7 @@


def _now():
return datetime.now()
return dt.datetime.now()


def abs_timedelta(delta):
Expand All @@ -29,19 +28,19 @@ def date_and_delta(value):
"""Turn a value into a date and a timedelta which represents how long ago
it was. If that's not possible, return (None, value)."""
now = _now()
if isinstance(value, datetime):
if isinstance(value, dt.datetime):
date = value
delta = now - value
elif isinstance(value, timedelta):
elif isinstance(value, dt.timedelta):
date = now - value
delta = value
else:
try:
value = int(value)
delta = timedelta(seconds=value)
delta = dt.timedelta(seconds=value)
date = now - delta
except (ValueError, TypeError):
return (None, value)
return None, value
return date, abs_timedelta(delta)


Expand Down Expand Up @@ -122,7 +121,7 @@ def naturaltime(value, future=False, months=True):
if date is None:
return value
# determine tense by value only if datetime/timedelta were passed
if isinstance(value, (datetime, timedelta)):
if isinstance(value, (dt.datetime, dt.timedelta)):
future = date > now

ago = _("%s from now") if future else _("%s ago")
Expand All @@ -139,14 +138,14 @@ def naturalday(value, format="%b %d"):
present day returns representing string. Otherwise, returns a string
formatted according to ``format``."""
try:
value = date(value.year, value.month, value.day)
value = dt.date(value.year, value.month, value.day)
except AttributeError:
# Passed value wasn't date-ish
return value
except (OverflowError, ValueError):
# Date arguments out of range
return value
delta = value - date.today()
delta = value - dt.date.today()
if delta.days == 0:
return _("today")
elif delta.days == 1:
Expand All @@ -160,14 +159,14 @@ def naturaldate(value):
"""Like naturalday, but will append a year for dates that are a year
ago or more."""
try:
value = date(value.year, value.month, value.day)
value = dt.date(value.year, value.month, value.day)
except AttributeError:
# Passed value wasn't date-ish
return value
except (OverflowError, ValueError):
# Date arguments out of range
return value
delta = abs_timedelta(value - date.today())
delta = abs_timedelta(value - dt.date.today())
if delta.days >= 365:
return naturalday(value, "%b %d %Y")
return naturalday(value)
1 change: 0 additions & 1 deletion tests/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Tests base classes."""

Expand Down
1 change: 0 additions & 1 deletion tests/test_filesize.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Tests for filesize humanizing."""

Expand Down
15 changes: 9 additions & 6 deletions tests/test_i18n.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import datetime as dt

import humanize
Expand All @@ -8,9 +7,13 @@ def test_i18n():
three_seconds = dt.timedelta(seconds=3)

assert humanize.naturaltime(three_seconds) == "3 seconds ago"
assert humanize.ordinal(5) == "5th"

humanize.i18n.activate("ru_RU")
assert humanize.naturaltime(three_seconds) == "3 секунды назад"

humanize.i18n.deactivate()
assert humanize.naturaltime(three_seconds) == "3 seconds ago"
try:
humanize.i18n.activate("ru_RU")
assert humanize.naturaltime(three_seconds) == "3 секунды назад"
assert humanize.ordinal(5) == "5ый"
finally:
humanize.i18n.deactivate()
assert humanize.naturaltime(three_seconds) == "3 seconds ago"
assert humanize.ordinal(5) == "5th"
Loading