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

Deprecate Python 2.7 and Python 3.5 #877

Merged
merged 7 commits into from
Nov 10, 2020
Merged
Changes from 6 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
2 changes: 1 addition & 1 deletion .github/workflows/continuous_integration.yml
Original file line number Diff line number Diff line change
@@ -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
13 changes: 7 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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:
21 changes: 12 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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__
@@ -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
@@ -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
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
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

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
@@ -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
@@ -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)
@@ -84,7 +72,7 @@ def __init__(
second=0,
microsecond=0,
tzinfo=None,
**kwargs
**kwargs,
):
if tzinfo is None:
tzinfo = dateutil_tz.tzutc()
@@ -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)
@@ -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)
@@ -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)
@@ -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()
@@ -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.
@@ -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):
@@ -720,7 +697,7 @@ def int_timestamp(self):

"""

return calendar.timegm(self._datetime.utctimetuple())
return int(self.timestamp())
Comment on lines -723 to +700
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably best to stick with old way to guarantee compatibility.


@property
def float_timestamp(self):
@@ -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):
@@ -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)

@@ -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:
@@ -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
@@ -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):
@@ -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"]:
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