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

Add Cython Support to Arrow #1143

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/continuous_integration.yml
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Install dependencies
run: |
pip install -U pip setuptools wheel
pip install -U tox tox-gh-actions
pip install -U tox tox-gh-actions "Cython>=3.0.0a11"
- name: Test with tox
run: tox
- name: Upload coverage to Codecov
Expand Down
16 changes: 12 additions & 4 deletions Makefile
@@ -1,4 +1,4 @@
.PHONY: auto test docs clean
.PHONY: auto test docs clean compile-cython

auto: build310

Expand All @@ -15,7 +15,15 @@ build36 build37 build38 build39 build310: clean
pip install -U pip setuptools wheel; \
pip install -r requirements/requirements-tests.txt; \
pip install -r requirements/requirements-docs.txt; \
pre-commit install
pre-commit install; \
python setup.py build_ext --inplace

compile-cython:
. venv/bin/activate; \
python setup.py build_ext --inplace

clean-cython:
rm -f ./arrow/*.c ./arrow/*.so ./arrow/*.pyd

test:
rm -f .coverage coverage.xml
Expand All @@ -38,14 +46,14 @@ live-docs: clean-docs
. venv/bin/activate; \
sphinx-autobuild docs docs/_build/html

clean: clean-dist
clean: clean-dist clean-cython
rm -rf venv .pytest_cache ./**/__pycache__
rm -f .coverage coverage.xml ./**/*.pyc

clean-dist:
rm -rf dist build .egg .eggs arrow.egg-info

build-dist:
build-dist: compile-cython
. venv/bin/activate; \
pip install -U pip setuptools twine wheel; \
python setup.py sdist bdist_wheel
Expand Down
65 changes: 43 additions & 22 deletions arrow/arrow.py
Expand Up @@ -1148,7 +1148,7 @@ def humanize(
"""

locale_name = locale
locale = locales.get_locale(locale)
locale_cls = locales.get_locale(locale)

if other is None:
utc = dt_datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc())
Expand Down Expand Up @@ -1179,65 +1179,83 @@ def humanize(
try:
if granularity == "auto":
if diff < 10:
return locale.describe("now", only_distance=only_distance)
return locale_cls.describe("now", only_distance=only_distance)

if diff < self._SECS_PER_MINUTE:
seconds = sign * delta_second
return locale.describe(
return locale_cls.describe(
"seconds", seconds, only_distance=only_distance
)

elif diff < self._SECS_PER_MINUTE * 2:
return locale.describe("minute", sign, only_distance=only_distance)
return locale_cls.describe(
"minute", sign, only_distance=only_distance
)
elif diff < self._SECS_PER_HOUR:
minutes = sign * max(delta_second // self._SECS_PER_MINUTE, 2)
return locale.describe(
return locale_cls.describe(
"minutes", minutes, only_distance=only_distance
)

elif diff < self._SECS_PER_HOUR * 2:
return locale.describe("hour", sign, only_distance=only_distance)
return locale_cls.describe(
"hour", sign, only_distance=only_distance
)
elif diff < self._SECS_PER_DAY:
hours = sign * max(delta_second // self._SECS_PER_HOUR, 2)
return locale.describe("hours", hours, only_distance=only_distance)
return locale_cls.describe(
"hours", hours, only_distance=only_distance
)
elif diff < self._SECS_PER_DAY * 2:
return locale.describe("day", sign, only_distance=only_distance)
return locale_cls.describe("day", sign, only_distance=only_distance)
elif diff < self._SECS_PER_WEEK:
days = sign * max(delta_second // self._SECS_PER_DAY, 2)
return locale.describe("days", days, only_distance=only_distance)
return locale_cls.describe(
"days", days, only_distance=only_distance
)

elif diff < self._SECS_PER_WEEK * 2:
return locale.describe("week", sign, only_distance=only_distance)
return locale_cls.describe(
"week", sign, only_distance=only_distance
)
elif diff < self._SECS_PER_MONTH:
weeks = sign * max(delta_second // self._SECS_PER_WEEK, 2)
return locale.describe("weeks", weeks, only_distance=only_distance)
return locale_cls.describe(
"weeks", weeks, only_distance=only_distance
)

elif diff < self._SECS_PER_MONTH * 2:
return locale.describe("month", sign, only_distance=only_distance)
return locale_cls.describe(
"month", sign, only_distance=only_distance
)
elif diff < self._SECS_PER_YEAR:
# TODO revisit for humanization during leap years
self_months = self._datetime.year * 12 + self._datetime.month
other_months = dt.year * 12 + dt.month

months = sign * max(abs(other_months - self_months), 2)

return locale.describe(
return locale_cls.describe(
"months", months, only_distance=only_distance
)

elif diff < self._SECS_PER_YEAR * 2:
return locale.describe("year", sign, only_distance=only_distance)
return locale_cls.describe(
"year", sign, only_distance=only_distance
)
else:
years = sign * max(delta_second // self._SECS_PER_YEAR, 2)
return locale.describe("years", years, only_distance=only_distance)
return locale_cls.describe(
"years", years, only_distance=only_distance
)

elif isinstance(granularity, str):
granularity = cast(TimeFrameLiteral, granularity) # type: ignore[assignment]

if granularity == "second":
delta = sign * float(delta_second)
if abs(delta) < 2:
return locale.describe("now", only_distance=only_distance)
return locale_cls.describe("now", only_distance=only_distance)
elif granularity == "minute":
delta = sign * delta_second / self._SECS_PER_MINUTE
elif granularity == "hour":
Expand All @@ -1260,7 +1278,9 @@ def humanize(

if trunc(abs(delta)) != 1:
granularity += "s" # type: ignore[assignment]
return locale.describe(granularity, delta, only_distance=only_distance)
return locale_cls.describe(
granularity, delta, only_distance=only_distance
)

else:

Expand Down Expand Up @@ -1304,7 +1324,9 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float:
"Please select between 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter' or 'year'."
)

return locale.describe_multi(timeframes, only_distance=only_distance)
return locale_cls.describe_multi(
timeframes, only_distance=only_distance
)

except KeyError as e:
raise ValueError(
Expand Down Expand Up @@ -1410,9 +1432,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow":

# Add change value to the correct unit (incorporates the plurality that exists within timeframe i.e second v.s seconds)
time_unit_to_change = str(unit)
time_unit_to_change += (
"s" if (str(time_unit_to_change)[-1] != "s") else ""
)
time_unit_to_change += "s" if time_unit_to_change[-1] != "s" else ""
time_object_info[time_unit_to_change] = change_value
unit_visited[time_unit_to_change] = True

Expand Down Expand Up @@ -1663,7 +1683,8 @@ def isocalendar(self) -> Tuple[int, int, int]:

"""

return self._datetime.isocalendar()
cal = tuple(self._datetime.isocalendar())
return (cal[0], cal[1], cal[2])

def isoformat(self, sep: str = "T", timespec: str = "auto") -> str:
"""Returns an ISO 8601 formatted representation of the date and time.
Expand Down
1 change: 1 addition & 0 deletions requirements/requirements.txt
@@ -1,2 +1,3 @@
Cython>=3.0.0a11
python-dateutil>=2.7.0
typing_extensions; python_version < '3.8'
15 changes: 14 additions & 1 deletion setup.py
@@ -1,13 +1,22 @@
# mypy: ignore-errors
from pathlib import Path

from setuptools import setup
from Cython.Build import build_ext, cythonize
from setuptools import Extension, setup

readme = Path("README.rst").read_text(encoding="utf-8")
version = Path("arrow/_version.py").read_text(encoding="utf-8")
about = {}
exec(version, about)

extensions = [
Extension(
"*",
["arrow/*.py"],
define_macros=[("CYTHON_TRACE", "1")],
)
]

setup(
name="arrow",
version=about["__version__"],
Expand Down Expand Up @@ -46,4 +55,8 @@
"Bug Reports": "https://github.com/arrow-py/arrow/issues",
"Documentation": "https://arrow.readthedocs.io",
},
ext_modules=cythonize(
extensions, language_level="3", compiler_directives={"linetrace": True}
),
cmdclass={"build_ext": build_ext},
)
5 changes: 1 addition & 4 deletions tests/test_locales.py
Expand Up @@ -77,13 +77,10 @@ def test_get_locale_by_class_name(self, mocker):
mock_locale_cls = mocker.Mock()
mock_locale_obj = mock_locale_cls.return_value = mocker.Mock()

globals_fn = mocker.Mock()
globals_fn.return_value = {"NonExistentLocale": mock_locale_cls}

with pytest.raises(ValueError):
arrow.locales.get_locale_by_class_name("NonExistentLocale")

mocker.patch.object(locales, "globals", globals_fn)
mocker.patch.object(locales, "NonExistentLocale", mock_locale_cls, create=True)
result = arrow.locales.get_locale_by_class_name("NonExistentLocale")

mock_locale_cls.assert_called_once_with()
Expand Down
3 changes: 3 additions & 0 deletions tox.ini
Expand Up @@ -47,3 +47,6 @@ include_trailing_comma = true
[flake8]
per-file-ignores = arrow/__init__.py:F401,tests/*:ANN001,ANN201
ignore = E203,E501,W503,ANN101,ANN102,ANN401

[coverage:run]
plugins = Cython.Coverage