Skip to content

Commit

Permalink
Deprecate Python 2.7 and Python 3.5 (#877)
Browse files Browse the repository at this point in the history
* Begin new branch for py27/35 changes. Drop 27/35 from CI.

* Run pre-commit hooks

* Deprecate timestamp property and fix tests

* More removals for 36+

* More timestamp updates and cleaned up Makefile
  • Loading branch information
jadchaar authored Nov 10, 2020
1 parent 609b630 commit 30d16c1
Show file tree
Hide file tree
Showing 27 changed files with 155 additions and 293 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/continuous_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["pypy3", "2.7", "3.5", "3.6", "3.7", "3.8", "3.9"]
python-version: ["pypy3", "3.6", "3.7", "3.8", "3.9"]
os: [ubuntu-latest, macos-latest, windows-latest]
exclude:
# pypy3 randomly fails on Windows builds
Expand Down
13 changes: 7 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ default_language_version:
python: python3
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
rev: v3.3.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: fix-encoding-pragma
exclude: ^arrow/_version.py
args: [--remove]
- id: requirements-txt-fixer
- id: check-ast
- id: check-yaml
Expand All @@ -16,15 +16,16 @@ repos:
- id: check-merge-conflict
- id: debug-statements
- repo: https://github.com/timothycrosley/isort
rev: 5.5.4
rev: 5.6.4
hooks:
- id: isort
- repo: https://github.com/asottile/pyupgrade
rev: v2.7.2
rev: v2.7.3
hooks:
- id: pyupgrade
args: [--py36-plus]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.6.0
rev: v1.7.0
hooks:
- id: python-no-eval
- id: python-check-blanket-noqa
Expand All @@ -33,7 +34,7 @@ repos:
rev: 20.8b1
hooks:
- id: black
args: [--safe, --quiet]
args: [--safe, --quiet, --target-version=py36]
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.4
hooks:
Expand Down
21 changes: 12 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,32 @@

auto: build38

build27: PYTHON_VER = python2.7
build35: PYTHON_VER = python3.5
build36: PYTHON_VER = python3.6
build37: PYTHON_VER = python3.7
build38: PYTHON_VER = python3.8
build39: PYTHON_VER = python3.9

build27 build35 build36 build37 build38 build39: clean
virtualenv venv --python=$(PYTHON_VER)
build36 build37 build38 build39: clean
$(PYTHON_VER) -m venv venv
. venv/bin/activate; \
pip install -U pip setuptools wheel; \
pip install -r requirements.txt; \
pre-commit install

test:
rm -f .coverage coverage.xml
. venv/bin/activate; pytest
. venv/bin/activate; \
pytest

lint:
. venv/bin/activate; pre-commit run --all-files --show-diff-on-failure
. venv/bin/activate; \
pre-commit run --all-files --show-diff-on-failure

docs:
rm -rf docs/_build
. venv/bin/activate; cd docs; make html
. venv/bin/activate; \
cd docs; \
make html

clean: clean-dist
rm -rf venv .pytest_cache ./**/__pycache__
Expand All @@ -36,10 +38,11 @@ clean-dist:

build-dist:
. venv/bin/activate; \
pip install -U setuptools twine wheel; \
pip install -U pip setuptools twine wheel; \
python setup.py sdist bdist_wheel

upload-dist:
. venv/bin/activate; twine upload dist/*
. venv/bin/activate; \
twine upload dist/*

publish: test clean-dist build-dist upload-dist clean-dist
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Features
--------

- Fully-implemented, drop-in replacement for datetime
- Supports Python 2.7, 3.5, 3.6, 3.7, 3.8 and 3.9
- Supports Python 3.6+
- Timezone-aware and UTC by default
- Provides super-simple creation options for many common input scenarios
- :code:`shift` method with support for relative offsets, including weeks
Expand Down
1 change: 0 additions & 1 deletion arrow/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from ._version import __version__
from .api import get, now, utcnow
from .arrow import Arrow
Expand Down
2 changes: 0 additions & 2 deletions arrow/api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
"""
Provides the default implementation of :class:`ArrowFactory <arrow.factory.ArrowFactory>`
methods for use as a module API.
"""

from __future__ import absolute_import

from arrow.factory import ArrowFactory

Expand Down
70 changes: 17 additions & 53 deletions arrow/arrow.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
# -*- coding: utf-8 -*-
"""
Provides the :class:`Arrow <arrow.arrow.Arrow>` class, an enhanced ``datetime``
replacement.
"""

from __future__ import absolute_import

import calendar
import sys
import warnings
from datetime import datetime, timedelta
from datetime import tzinfo as dt_tzinfo
from math import trunc
Expand All @@ -19,17 +16,8 @@

from arrow import formatter, locales, parser, util

if sys.version_info[:2] < (3, 6): # pragma: no cover
with warnings.catch_warnings():
warnings.simplefilter("default", DeprecationWarning)
warnings.warn(
"Arrow will drop support for Python 2.7 and 3.5 in the upcoming v1.0.0 release. Please upgrade to "
"Python 3.6+ to continue receiving updates for Arrow.",
DeprecationWarning,
)


class Arrow(object):
class Arrow:
"""An :class:`Arrow <arrow.arrow.Arrow>` object.
Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing
Expand Down Expand Up @@ -65,7 +53,7 @@ class Arrow(object):
resolution = datetime.resolution

_ATTRS = ["year", "month", "day", "hour", "minute", "second", "microsecond"]
_ATTRS_PLURAL = ["{}s".format(a) for a in _ATTRS]
_ATTRS_PLURAL = [f"{a}s" for a in _ATTRS]
_MONTHS_PER_QUARTER = 3
_SECS_PER_MINUTE = float(60)
_SECS_PER_HOUR = float(60 * 60)
Expand All @@ -84,7 +72,7 @@ def __init__(
second=0,
microsecond=0,
tzinfo=None,
**kwargs
**kwargs,
):
if tzinfo is None:
tzinfo = dateutil_tz.tzutc()
Expand All @@ -96,7 +84,7 @@ def __init__(
and tzinfo.zone
):
tzinfo = parser.TzinfoParser.parse(tzinfo.zone)
elif util.isstr(tzinfo):
elif isinstance(tzinfo, str):
tzinfo = parser.TzinfoParser.parse(tzinfo)

fold = kwargs.get("fold", 0)
Expand Down Expand Up @@ -177,13 +165,11 @@ def fromtimestamp(cls, timestamp, tzinfo=None):

if tzinfo is None:
tzinfo = dateutil_tz.tzlocal()
elif util.isstr(tzinfo):
elif isinstance(tzinfo, str):
tzinfo = parser.TzinfoParser.parse(tzinfo)

if not util.is_timestamp(timestamp):
raise ValueError(
"The provided timestamp '{}' is invalid.".format(timestamp)
)
raise ValueError(f"The provided timestamp '{timestamp}' is invalid.")

timestamp = util.normalize_timestamp(float(timestamp))
dt = datetime.fromtimestamp(timestamp, tzinfo)
Expand All @@ -209,9 +195,7 @@ def utcfromtimestamp(cls, timestamp):
"""

if not util.is_timestamp(timestamp):
raise ValueError(
"The provided timestamp '{}' is invalid.".format(timestamp)
)
raise ValueError(f"The provided timestamp '{timestamp}' is invalid.")

timestamp = util.normalize_timestamp(float(timestamp))
dt = datetime.utcfromtimestamp(timestamp)
Expand Down Expand Up @@ -604,7 +588,7 @@ def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"):
# representations

def __repr__(self):
return "<{} [{}]>".format(self.__class__.__name__, self.__str__())
return f"<{self.__class__.__name__} [{self.__str__()}]>"

def __str__(self):
return self._datetime.isoformat()
Expand Down Expand Up @@ -688,7 +672,6 @@ def naive(self):

return self._datetime.replace(tzinfo=None)

@property
def timestamp(self):
"""Returns a timestamp representation of the :class:`Arrow <arrow.arrow.Arrow>` object, in
UTC time.
Expand All @@ -700,13 +683,7 @@ def timestamp(self):
"""

warnings.warn(
"For compatibility with the datetime.timestamp() method this property will be replaced with a method in "
"the 1.0.0 release, please switch to the .int_timestamp property for identical behaviour as soon as "
"possible.",
DeprecationWarning,
)
return calendar.timegm(self._datetime.utctimetuple())
return self._datetime.timestamp()

@property
def int_timestamp(self):
Expand All @@ -720,7 +697,7 @@ def int_timestamp(self):
"""

return calendar.timegm(self._datetime.utctimetuple())
return int(self.timestamp())

@property
def float_timestamp(self):
Expand All @@ -734,11 +711,7 @@ def float_timestamp(self):
"""

# IDEA get rid of this in 1.0.0 and wrap datetime.timestamp()
# Or for compatibility retain this but make it call the timestamp method
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
return self.timestamp + float(self.microsecond) / 1000000
return self.timestamp()

@property
def fold(self):
Expand Down Expand Up @@ -802,9 +775,9 @@ def replace(self, **kwargs):
if key in self._ATTRS:
absolute_kwargs[key] = value
elif key in ["week", "quarter"]:
raise AttributeError("setting absolute {} is not supported".format(key))
raise AttributeError(f"setting absolute {key} is not supported")
elif key not in ["tzinfo", "fold"]:
raise AttributeError('unknown attribute: "{}"'.format(key))
raise AttributeError(f'unknown attribute: "{key}"')

current = self._datetime.replace(**absolute_kwargs)

Expand Down Expand Up @@ -1062,7 +1035,7 @@ def humanize(
years = sign * int(max(delta / self._SECS_PER_YEAR, 2))
return locale.describe("years", years, only_distance=only_distance)

elif util.isstr(granularity):
elif isinstance(granularity, str):
if granularity == "second":
delta = sign * delta
if abs(delta) < 2:
Expand Down Expand Up @@ -1491,13 +1464,6 @@ def __le__(self, other):

return self._datetime <= self._get_datetime(other)

def __cmp__(self, other):
if sys.version_info[0] < 3: # pragma: no cover
if not isinstance(other, (Arrow, datetime)):
raise TypeError(
"can't compare '{}' to '{}'".format(type(self), type(other))
)

# internal methods

@staticmethod
Expand All @@ -1511,7 +1477,7 @@ def _get_tzinfo(tz_expr):
try:
return parser.TzinfoParser.parse(tz_expr)
except parser.ParserError:
raise ValueError("'{}' not recognized as a timezone".format(tz_expr))
raise ValueError(f"'{tz_expr}' not recognized as a timezone")

@classmethod
def _get_datetime(cls, expr):
Expand All @@ -1524,15 +1490,13 @@ def _get_datetime(cls, expr):
timestamp = float(expr)
return cls.utcfromtimestamp(timestamp).datetime
else:
raise ValueError(
"'{}' not recognized as a datetime or timestamp.".format(expr)
)
raise ValueError(f"'{expr}' not recognized as a datetime or timestamp.")

@classmethod
def _get_frames(cls, name):

if name in cls._ATTRS:
return name, "{}s".format(name), 1
return name, f"{name}s", 1
elif name[-1] == "s" and name[:-1] in cls._ATTRS:
return name[:-1], name, 1
elif name in ["week", "weeks"]:
Expand Down
15 changes: 9 additions & 6 deletions arrow/constants.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
import os
from datetime import datetime

# Output of time.mktime(datetime.max.timetuple()) on macOS
# This value must be hardcoded for compatibility with Windows
# Platform-independent max timestamps are hard to form
# https://stackoverflow.com/q/46133223
MAX_TIMESTAMP = 253402318799.0
# datetime.max.timestamp() errors on Windows, so we must hardcode
# the highest possible datetime value that can output a timestamp.
# tl;dr platform-independent max timestamps are hard to form
# See: https://stackoverflow.com/q/46133223
MAX_TIMESTAMP = (
datetime(3001, 1, 18, 23, 59, 59, 999999) if os.name == "nt" else datetime.max
).timestamp()
MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000
MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1000000
Loading

0 comments on commit 30d16c1

Please sign in to comment.