diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 3cf3ee040..350bbb4f5 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cbbc42e80..d456b221b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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: diff --git a/Makefile b/Makefile index 41b351912..18a7fbc5e 100644 --- a/Makefile +++ b/Makefile @@ -2,15 +2,13 @@ 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; \ @@ -18,14 +16,18 @@ build27 build35 build36 build37 build38 build39: clean 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 diff --git a/README.rst b/README.rst index 69f6c50d8..3571c71e4 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/arrow/__init__.py b/arrow/__init__.py index 2883527be..117c9e8a0 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from ._version import __version__ from .api import get, now, utcnow from .arrow import Arrow diff --git a/arrow/api.py b/arrow/api.py index a6b7be3de..13a369f82 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- """ Provides the default implementation of :class:`ArrowFactory ` methods for use as a module API. """ -from __future__ import absolute_import from arrow.factory import ArrowFactory diff --git a/arrow/arrow.py b/arrow/arrow.py index f3706b5b2..899c680c9 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1,15 +1,12 @@ -# -*- coding: utf-8 -*- """ Provides the :class:`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 ` 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 ` 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()) @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"]: diff --git a/arrow/constants.py b/arrow/constants.py index 81e37b26d..d63698448 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -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 diff --git a/arrow/factory.py b/arrow/factory.py index 05933e815..9bae80c58 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Implements the :class:`ArrowFactory ` class, providing factory methods for common :class:`Arrow ` @@ -6,7 +5,6 @@ """ -from __future__ import absolute_import import calendar from datetime import date, datetime @@ -17,10 +15,10 @@ from arrow import parser from arrow.arrow import Arrow -from arrow.util import is_timestamp, iso_to_gregorian, isstr +from arrow.util import is_timestamp, iso_to_gregorian -class ArrowFactory(object): +class ArrowFactory: """A factory for generating :class:`Arrow ` objects. :param type: (optional) the :class:`Arrow `-based class to construct from. @@ -155,7 +153,7 @@ def get(self, *args, **kwargs): # () -> now, @ utc. if arg_count == 0: - if isstr(tz): + if isinstance(tz, str): tz = parser.TzinfoParser.parse(tz) return self.type.now(tz) @@ -172,7 +170,7 @@ def get(self, *args, **kwargs): return self.type.utcnow() # try (int, float) -> from timestamp with tz - elif not isstr(arg) and is_timestamp(arg): + elif not isinstance(arg, str) and is_timestamp(arg): if tz is None: # set to UTC by default tz = dateutil_tz.tzutc() @@ -195,7 +193,7 @@ def get(self, *args, **kwargs): return self.type.now(arg) # (str) -> parse. - elif isstr(arg): + elif isinstance(arg, str): dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace) return self.type.fromdatetime(dt, tz) @@ -220,7 +218,7 @@ def get(self, *args, **kwargs): if isinstance(arg_1, datetime): # (datetime, tzinfo/str) -> fromdatetime replace tzinfo. - if isinstance(arg_2, dt_tzinfo) or isstr(arg_2): + if isinstance(arg_2, dt_tzinfo) or isinstance(arg_2, str): return self.type.fromdatetime(arg_1, arg_2) else: raise TypeError( @@ -232,7 +230,7 @@ def get(self, *args, **kwargs): elif isinstance(arg_1, date): # (date, tzinfo/str) -> fromdate replace tzinfo. - if isinstance(arg_2, dt_tzinfo) or isstr(arg_2): + if isinstance(arg_2, dt_tzinfo) or isinstance(arg_2, str): return self.type.fromdate(arg_1, tzinfo=arg_2) else: raise TypeError( @@ -242,7 +240,9 @@ def get(self, *args, **kwargs): ) # (str, format) -> parse. - elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)): + elif isinstance(arg_1, str) and ( + isinstance(arg_2, str) or isinstance(arg_2, list) + ): dt = parser.DateTimeParser(locale).parse( args[0], args[1], normalize_whitespace ) diff --git a/arrow/formatter.py b/arrow/formatter.py index 9f9d7a44d..6bd61c89d 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division - -import calendar import re from dateutil import tz as dateutil_tz @@ -20,7 +16,7 @@ FORMAT_W3C = "YYYY-MM-DD HH:mm:ssZZ" -class DateTimeFormatter(object): +class DateTimeFormatter: # This pattern matches characters enclosed in square brackets are matched as # an atomic group. For more info on atomic groups and how to they are @@ -53,16 +49,16 @@ def _format_token(self, dt, token): if token == "MMM": return self.locale.month_abbreviation(dt.month) if token == "MM": - return "{:02d}".format(dt.month) + return f"{dt.month:02d}" if token == "M": return str(dt.month) if token == "DDDD": - return "{:03d}".format(dt.timetuple().tm_yday) + return f"{dt.timetuple().tm_yday:03d}" if token == "DDD": return str(dt.timetuple().tm_yday) if token == "DD": - return "{:02d}".format(dt.day) + return f"{dt.day:02d}" if token == "D": return str(dt.day) @@ -77,7 +73,7 @@ def _format_token(self, dt, token): return str(dt.isoweekday()) if token == "HH": - return "{:02d}".format(dt.hour) + return f"{dt.hour:02d}" if token == "H": return str(dt.hour) if token == "hh": @@ -86,12 +82,12 @@ def _format_token(self, dt, token): return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) if token == "mm": - return "{:02d}".format(dt.minute) + return f"{dt.minute:02d}" if token == "m": return str(dt.minute) if token == "ss": - return "{:02d}".format(dt.second) + return f"{dt.second:02d}" if token == "s": return str(dt.second) @@ -109,13 +105,10 @@ def _format_token(self, dt, token): return str(int(dt.microsecond / 100000)) if token == "X": - # TODO: replace with a call to dt.timestamp() when we drop Python 2.7 - return str(calendar.timegm(dt.utctimetuple())) + return str(dt.timestamp()) if token == "x": - # TODO: replace with a call to dt.timestamp() when we drop Python 2.7 - ts = calendar.timegm(dt.utctimetuple()) + (dt.microsecond / 1000000) - return str(int(ts * 1000000)) + return str(int(dt.timestamp() * 1000000)) if token == "ZZZ": return dt.tzname() @@ -129,11 +122,11 @@ def _format_token(self, dt, token): total_minutes = abs(total_minutes) hour, minute = divmod(total_minutes, 60) - return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute) + return f"{sign}{hour:02d}{separator}{minute:02d}" if token in ("a", "A"): return self.locale.meridian(dt.hour, token) if token == "W": year, week, day = dt.isocalendar() - return "{}-W{:02d}-{}".format(year, week, day) + return f"{year}-W{week:02d}-{day}" diff --git a/arrow/locales.py b/arrow/locales.py index c441fbd2c..3d0ca4637 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - import inspect import sys from math import trunc @@ -17,7 +14,7 @@ def get_locale(name): locale_cls = _locales.get(name.lower()) if locale_cls is None: - raise ValueError("Unsupported locale '{}'".format(name)) + raise ValueError(f"Unsupported locale '{name}'") return locale_cls() @@ -32,7 +29,7 @@ def get_locale_by_class_name(name): locale_cls = globals().get(name) if locale_cls is None: - raise ValueError("Unsupported locale '{}'".format(name)) + raise ValueError(f"Unsupported locale '{name}'") return locale_cls() @@ -40,7 +37,7 @@ def get_locale_by_class_name(name): # base locale type. -class Locale(object): +class Locale: """ Represents locale-specific data and functionality. """ names = [] @@ -171,14 +168,14 @@ def year_full(self, year): :param name: the ``int`` year (4-digit) """ - return "{:04d}".format(year) + return f"{year:04d}" def year_abbreviation(self, year): """Returns the year for specific locale if available :param name: the ``int`` year (4-digit) """ - return "{:04d}".format(year)[2:] + return f"{year:04d}"[2:] def meridian(self, hour, token): """Returns the meridian indicator for a specified hour and format token. @@ -200,7 +197,7 @@ def ordinal_number(self, n): return self._ordinal_number(n) def _ordinal_number(self, n): - return "{}".format(n) + return f"{n}" def _name_to_ordinal(self, lst): return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:]))) @@ -308,12 +305,12 @@ def _ordinal_number(self, n): if n % 100 not in (11, 12, 13): remainder = abs(n) % 10 if remainder == 1: - return "{}st".format(n) + return f"{n}st" elif remainder == 2: - return "{}nd".format(n) + return f"{n}nd" elif remainder == 3: - return "{}rd".format(n) - return "{}th".format(n) + return f"{n}rd" + return f"{n}th" def describe(self, timeframe, delta=0, only_distance=False): """Describes a delta within a timeframe in plain language. @@ -323,7 +320,7 @@ def describe(self, timeframe, delta=0, only_distance=False): :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords """ - humanized = super(EnglishLocale, self).describe(timeframe, delta, only_distance) + humanized = super().describe(timeframe, delta, only_distance) if only_distance and timeframe == "now": humanized = "instantly" @@ -400,7 +397,7 @@ class ItalianLocale(Locale): ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" def _ordinal_number(self, n): - return "{}º".format(n) + return f"{n}º" class SpanishLocale(Locale): @@ -475,7 +472,7 @@ class SpanishLocale(Locale): ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" def _ordinal_number(self, n): - return "{}º".format(n) + return f"{n}º" class FrenchBaseLocale(Locale): @@ -536,8 +533,8 @@ class FrenchBaseLocale(Locale): def _ordinal_number(self, n): if abs(n) == 1: - return "{}er".format(n) - return "{}e".format(n) + return f"{n}er" + return f"{n}e" class FrenchLocale(FrenchBaseLocale, Locale): @@ -869,7 +866,7 @@ def _format_relative(self, humanized, timeframe, delta): return direction.format(humanized[which]) def _ordinal_number(self, n): - return "{}.".format(n) + return f"{n}." class ChineseCNLocale(Locale): @@ -1128,7 +1125,7 @@ def _ordinal_number(self, n): ordinals = ["0", "첫", "두", "세", "네", "다섯", "여섯", "일곱", "여덟", "아홉", "열"] if n < len(ordinals): return "{}번째".format(ordinals[n]) - return "{}번째".format(n) + return f"{n}번째" def _format_relative(self, humanized, timeframe, delta): if timeframe in ("day", "days"): @@ -1140,7 +1137,7 @@ def _format_relative(self, humanized, timeframe, delta): if special: return special - return super(KoreanLocale, self)._format_relative(humanized, timeframe, delta) + return super()._format_relative(humanized, timeframe, delta) # derived locale types & implementations. @@ -1798,7 +1795,7 @@ class GermanBaseLocale(Locale): day_abbreviations = ["", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] def _ordinal_number(self, n): - return "{}.".format(n) + return f"{n}." def describe(self, timeframe, delta=0, only_distance=False): """Describes a delta within a timeframe in plain language. @@ -1809,9 +1806,7 @@ def describe(self, timeframe, delta=0, only_distance=False): """ if not only_distance: - return super(GermanBaseLocale, self).describe( - timeframe, delta, only_distance - ) + return super().describe(timeframe, delta, only_distance) # German uses a different case without 'in' or 'ago' humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) @@ -2130,7 +2125,7 @@ class TagalogLocale(Locale): meridians = {"am": "nu", "pm": "nh", "AM": "ng umaga", "PM": "ng hapon"} def _ordinal_number(self, n): - return "ika-{}".format(n) + return f"ika-{n}" class VietnameseLocale(Locale): @@ -3186,7 +3181,7 @@ class HebrewLocale(Locale): def _format_timeframe(self, timeframe, delta): """Hebrew couple of aware""" - couple = "2-{}".format(timeframe) + couple = f"2-{timeframe}" single = timeframe.rstrip("s") if abs(delta) == 2 and couple in self.timeframes: key = couple @@ -3592,7 +3587,7 @@ class EsperantoLocale(Locale): ordinal_day_re = r"((?P[1-3]?[0-9](?=a))a)" def _ordinal_number(self, n): - return "{}a".format(n) + return f"{n}a" class ThaiLocale(Locale): @@ -3659,12 +3654,12 @@ class ThaiLocale(Locale): def year_full(self, year): """Thai always use Buddhist Era (BE) which is CE + 543""" year += self.BE_OFFSET - return "{:04d}".format(year) + return f"{year:04d}" def year_abbreviation(self, year): """Thai always use Buddhist Era (BE) which is CE + 543""" year += self.BE_OFFSET - return "{:04d}".format(year)[2:] + return f"{year:04d}"[2:] def _format_relative(self, humanized, timeframe, delta): """Thai normally doesn't have any space between words""" @@ -3746,15 +3741,15 @@ class BengaliLocale(Locale): def _ordinal_number(self, n): if n > 10 or n == 0: - return "{}তম".format(n) + return f"{n}তম" if n in [1, 5, 7, 8, 9, 10]: - return "{}ম".format(n) + return f"{n}ম" if n in [2, 3]: - return "{}য়".format(n) + return f"{n}য়" if n == 4: - return "{}র্থ".format(n) + return f"{n}র্থ" if n == 6: - return "{}ষ্ঠ".format(n) + return f"{n}ষ্ঠ" class RomanshLocale(Locale): diff --git a/arrow/parser.py b/arrow/parser.py index 50cdcf4d2..21ff3335a 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -1,19 +1,12 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - import re from datetime import datetime, timedelta +from functools import lru_cache from dateutil import tz from arrow import locales from arrow.util import iso_to_gregorian, next_weekday, normalize_timestamp -try: - from functools import lru_cache -except ImportError: # pragma: no cover - from backports.functools_lru_cache import lru_cache # pragma: no cover - class ParserError(ValueError): pass @@ -28,7 +21,7 @@ class ParserMatchError(ParserError): pass -class DateTimeParser(object): +class DateTimeParser: _FORMAT_RE = re.compile( r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|x|X|W)" @@ -200,19 +193,19 @@ def parse_iso(self, datetime_string, normalize_whitespace=False): elif has_seconds: time_string = "HH{time_sep}mm{time_sep}ss".format(time_sep=time_sep) elif has_minutes: - time_string = "HH{time_sep}mm".format(time_sep=time_sep) + time_string = f"HH{time_sep}mm" else: time_string = "HH" if has_space_divider: - formats = ["{} {}".format(f, time_string) for f in formats] + formats = [f"{f} {time_string}" for f in formats] else: - formats = ["{}T{}".format(f, time_string) for f in formats] + formats = [f"{f}T{time_string}" for f in formats] if has_time and has_tz: # Add "Z" or "ZZ" to the format strings to indicate to # _parse_token() that a timezone needs to be parsed - formats = ["{}{}".format(f, tz_format) for f in formats] + formats = [f"{f}{tz_format}" for f in formats] return self._parse_multiformat(datetime_string, formats) @@ -226,17 +219,16 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): try: fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) - # TODO: remove pragma when we drop 2.7 - except re.error as e: # pragma: no cover + except re.error as e: raise ParserMatchError( - "Failed to generate regular expression pattern: {}".format(e) + f"Failed to generate regular expression pattern: {e}" ) match = fmt_pattern_re.search(datetime_string) if match is None: raise ParserMatchError( - "Failed to match '{}' when parsing '{}'".format(fmt, datetime_string) + f"Failed to match '{fmt}' when parsing '{datetime_string}'" ) parts = {} @@ -248,8 +240,7 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): else: value = match.group(token) - # TODO: remove pragma when we drop 2.7 - if value is None: # pragma: no cover + if value is None: raise ParserMatchError( "Unable to find a match group for the specified token '{}'.".format( token @@ -288,8 +279,8 @@ def _generate_pattern_re(self, fmt): try: input_re = self._input_re_map[token] except KeyError: - raise ParserError("Unrecognized token '{}'".format(token)) - input_pattern = "(?P<{}>{})".format(token, input_re.pattern) + raise ParserError(f"Unrecognized token '{token}'") + input_pattern = f"(?P<{token}>{input_re.pattern})" tokens.append(token) # a pattern doesn't have the same length as the token # it replaces! We keep the difference in the offset variable. @@ -389,7 +380,7 @@ def _parse_token(self, token, value, parts): # We have the *most significant* digits of an arbitrary-precision integer. # We want the six most significant digits as an integer, rounded. # IDEA: add nanosecond support somehow? Need datetime support for it first. - value = value.ljust(7, str("0")) + value = value.ljust(7, "0") # floating-point (IEEE-754) defaults to half-to-even rounding seventh_digit = int(value[6]) @@ -468,12 +459,12 @@ def _build_datetime(parts): "Month component is not allowed with the DDD and DDDD tokens." ) - date_string = "{}-{}".format(year, day_of_year) + date_string = f"{year}-{day_of_year}" try: dt = datetime.strptime(date_string, "%Y-%j") except ValueError: raise ParserError( - "The provided day of year '{}' is invalid.".format(day_of_year) + f"The provided day of year '{day_of_year}' is invalid." ) parts["year"] = dt.year @@ -571,7 +562,7 @@ def _generate_choice_re(choices, flags=0): return re.compile(r"({})".format("|".join(choices)), flags=flags) -class TzinfoParser(object): +class TzinfoParser: _TZINFO_RE = re.compile(r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$") @classmethod @@ -604,8 +595,6 @@ def parse(cls, tzinfo_string): tzinfo = tz.gettz(tzinfo_string) if tzinfo is None: - raise ParserError( - 'Could not parse timezone expression "{}"'.format(tzinfo_string) - ) + raise ParserError(f'Could not parse timezone expression "{tzinfo_string}"') return tzinfo diff --git a/arrow/util.py b/arrow/util.py index acce8878d..7053a8863 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,8 +1,4 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - import datetime -import numbers from dateutil.rrule import WEEKLY, rrule @@ -45,9 +41,7 @@ def is_timestamp(value): if isinstance(value, bool): return False if not ( - isinstance(value, numbers.Integral) - or isinstance(value, float) - or isinstance(value, str) + isinstance(value, int) or isinstance(value, float) or isinstance(value, str) ): return False try: @@ -65,9 +59,7 @@ def normalize_timestamp(timestamp): elif timestamp < MAX_TIMESTAMP_US: timestamp /= 1e6 else: - raise ValueError( - "The specified timestamp '{}' is too large.".format(timestamp) - ) + raise ValueError(f"The specified timestamp '{timestamp}' is too large.") return timestamp @@ -97,19 +89,4 @@ def validate_bounds(bounds): ) -# Python 2.7 / 3.0+ definitions for isstr function. - -try: # pragma: no cover - basestring - - def isstr(s): - return isinstance(s, basestring) # noqa: F821 - - -except NameError: # pragma: no cover - - def isstr(s): - return isinstance(s, str) - - -__all__ = ["next_weekday", "total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"] +__all__ = ["next_weekday", "total_seconds", "is_timestamp", "iso_to_gregorian"] diff --git a/docs/conf.py b/docs/conf.py index aaf3c5082..ce16c080e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,20 +1,17 @@ -# -*- coding: utf-8 -*- - # -- Path setup -------------------------------------------------------------- -import io import os import sys sys.path.insert(0, os.path.abspath("..")) about = {} -with io.open("../arrow/_version.py", "r", encoding="utf-8") as f: +with open("../arrow/_version.py", encoding="utf-8") as f: exec(f.read(), about) # -- Project information ----------------------------------------------------- -project = u"Arrow 🏹" +project = "Arrow 🏹" copyright = "2020, Chris Smith" author = "Chris Smith" diff --git a/requirements.txt b/requirements.txt index bf4415764..bcf86d346 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,9 @@ -backports.functools_lru_cache==1.6.1; python_version == "2.7" -dateparser==0.7.* -pre-commit==1.21.*; python_version <= "3.5" -pre-commit==2.7.*; python_version >= "3.6" -pytest==4.6.*; python_version == "2.7" -pytest==6.1.*; python_version >= "3.5" +dateparser==1.0.* +pre-commit==2.8.* +pytest==6.1.* pytest-cov==2.10.* -pytest-mock==2.0.*; python_version == "2.7" -pytest-mock==3.3.*; python_version >= "3.5" +pytest-mock==3.3.* python-dateutil==2.8.* pytz==2019.* simplejson==3.17.* -sphinx==1.8.*; python_version == "2.7" -sphinx==3.2.*; python_version >= "3.5" +sphinx==3.3.* diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2a9acf13d..000000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py index dc4f0e77d..703f1c548 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,10 @@ -# -*- coding: utf-8 -*- -import io - from setuptools import setup -with io.open("README.rst", "r", encoding="utf-8") as f: +with open("README.rst", encoding="utf-8") as f: readme = f.read() about = {} -with io.open("arrow/_version.py", "r", encoding="utf-8") as f: +with open("arrow/_version.py", encoding="utf-8") as f: exec(f.read(), about) setup( @@ -22,20 +19,15 @@ license="Apache 2.0", packages=["arrow"], zip_safe=False, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", - install_requires=[ - "python-dateutil>=2.7.0", - "backports.functools_lru_cache>=1.2.1;python_version=='2.7'", - ], + python_requires=">=3.6", + install_requires=["python-dateutil>=2.7.0"], classifiers=[ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Topic :: Software Development :: Libraries :: Python Modules", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/tests/conftest.py b/tests/conftest.py index 5bc8a4af2..4043bc3b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from datetime import datetime import pytest diff --git a/tests/test_api.py b/tests/test_api.py index 9b19a27cd..5576aaf84 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import arrow diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 872edc017..fe583d98b 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import calendar import pickle import sys import time @@ -193,7 +189,7 @@ def test_repr(self): result = self.arrow.__repr__() - assert result == "".format(self.arrow._datetime.isoformat()) + assert result == f"" def test_str(self): @@ -209,7 +205,7 @@ def test_hash(self): def test_format(self): - result = "{:YYYY-MM-DD}".format(self.arrow) + result = f"{self.arrow:YYYY-MM-DD}" assert result == "2013-02-03" @@ -221,7 +217,7 @@ def test_bare_format(self): def test_format_no_format_string(self): - result = "{}".format(self.arrow) + result = f"{self.arrow}" assert result == str(self.arrow) @@ -280,24 +276,15 @@ def test_naive(self): def test_timestamp(self): - assert self.arrow.timestamp == calendar.timegm( - self.arrow._datetime.utctimetuple() - ) - - with pytest.warns(DeprecationWarning): - self.arrow.timestamp + assert self.arrow.timestamp() == self.arrow._datetime.timestamp() def test_int_timestamp(self): - assert self.arrow.int_timestamp == calendar.timegm( - self.arrow._datetime.utctimetuple() - ) + assert self.arrow.int_timestamp == int(self.arrow._datetime.timestamp()) def test_float_timestamp(self): - result = self.arrow.float_timestamp - self.arrow.timestamp - - assert result == self.arrow.microsecond + assert self.arrow.float_timestamp == self.arrow._datetime.timestamp() def test_getattr_fold(self): @@ -922,9 +909,6 @@ def test_shift_kiritimati(self): 1995, 1, 1, 12, 30, tzinfo="Pacific/Kiritimati" ) - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="unsupported before python 3.6" - ) def shift_imaginary_seconds(self): # offset has a seconds component monrovia = arrow.Arrow(1972, 1, 6, 23, tzinfo="Africa/Monrovia") diff --git a/tests/test_factory.py b/tests/test_factory.py index 2b8df5168..b954d1620 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import time from datetime import date, datetime @@ -20,8 +19,8 @@ def test_no_args(self): def test_timestamp_one_arg_no_arg(self): - no_arg = self.factory.get(1406430900).timestamp - one_arg = self.factory.get("1406430900", "X").timestamp + no_arg = self.factory.get(1406430900).timestamp() + one_arg = self.factory.get("1406430900", "X").timestamp() assert no_arg == one_arg @@ -289,7 +288,7 @@ def test_two_args_str_list(self): def test_two_args_unicode_unicode(self): - result = self.factory.get(u"2013-01-01", u"YYYY-MM-DD") + result = self.factory.get("2013-01-01", "YYYY-MM-DD") assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) diff --git a/tests/test_formatter.py b/tests/test_formatter.py index e97aeb5dc..06831f1e0 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from datetime import datetime import pytest @@ -113,14 +112,13 @@ def test_sub_second(self): def test_timestamp(self): - timestamp = 1588437009.8952794 - dt = datetime.utcfromtimestamp(timestamp) - expected = str(int(timestamp)) + dt = datetime.now(tz=dateutil_tz.UTC) + expected = str(dt.timestamp()) assert self.formatter._format_token(dt, "X") == expected # Must round because time.time() may return a float with greater # than 6 digits of precision - expected = str(int(timestamp * 1000000)) + expected = str(int(dt.timestamp() * 1000000)) assert self.formatter._format_token(dt, "x") == expected def test_timezone(self): diff --git a/tests/test_locales.py b/tests/test_locales.py index 76323f379..642013ba5 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import pytest from arrow import arrow, locales diff --git a/tests/test_parser.py b/tests/test_parser.py index 9e497f504..8170a2f1d 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,9 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import calendar import os -import sys import time from datetime import datetime @@ -226,24 +222,19 @@ def test_parse_timestamp(self): tz_utc = tz.tzutc() int_timestamp = int(time.time()) self.expected = datetime.fromtimestamp(int_timestamp, tz=tz_utc) - assert self.parser.parse("{:d}".format(int_timestamp), "X") == self.expected + assert self.parser.parse(f"{int_timestamp:d}", "X") == self.expected float_timestamp = time.time() self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - assert self.parser.parse("{:f}".format(float_timestamp), "X") == self.expected + assert self.parser.parse(f"{float_timestamp:f}", "X") == self.expected # test handling of ns timestamp (arrow will round to 6 digits regardless) self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - assert ( - self.parser.parse("{:f}123".format(float_timestamp), "X") == self.expected - ) + assert self.parser.parse(f"{float_timestamp:f}123", "X") == self.expected # test ps timestamp (arrow will round to 6 digits regardless) self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - assert ( - self.parser.parse("{:f}123456".format(float_timestamp), "X") - == self.expected - ) + assert self.parser.parse(f"{float_timestamp:f}123456", "X") == self.expected # NOTE: negative timestamps cannot be handled by datetime on Window # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 @@ -252,15 +243,13 @@ def test_parse_timestamp(self): negative_int_timestamp = -int_timestamp self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) assert ( - self.parser.parse("{:d}".format(negative_int_timestamp), "X") - == self.expected + self.parser.parse(f"{negative_int_timestamp:d}", "X") == self.expected ) negative_float_timestamp = -float_timestamp self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) assert ( - self.parser.parse("{:f}".format(negative_float_timestamp), "X") - == self.expected + self.parser.parse(f"{negative_float_timestamp:f}", "X") == self.expected ) # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will @@ -292,10 +281,10 @@ def test_parse_expanded_timestamp(self): assert self.parser.parse("{:d}".format(int(timestamp)), "x") == self.expected self.expected = datetime.fromtimestamp(round(timestamp, 3), tz=tz_utc) - assert self.parser.parse("{:d}".format(timestamp_milli), "x") == self.expected + assert self.parser.parse(f"{timestamp_milli:d}", "x") == self.expected self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) - assert self.parser.parse("{:d}".format(timestamp_micro), "x") == self.expected + assert self.parser.parse(f"{timestamp_micro:d}", "x") == self.expected # anything above max µs timestamp should fail with pytest.raises(ValueError): @@ -303,7 +292,7 @@ def test_parse_expanded_timestamp(self): # floats are not allowed with the "x" token with pytest.raises(ParserMatchError): - self.parser.parse("{:f}".format(timestamp), "x") + self.parser.parse(f"{timestamp:f}", "x") def test_parse_names(self): @@ -346,7 +335,7 @@ def test_parse_tz_name_zzz(self, full_tz_name): self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(full_tz_name)) assert ( - self.parser.parse("2013-01-01 {}".format(full_tz_name), "YYYY-MM-DD ZZZ") + self.parser.parse(f"2013-01-01 {full_tz_name}", "YYYY-MM-DD ZZZ") == self.expected ) @@ -940,9 +929,7 @@ def test_time(self): for sep in time_seperators: assert time_re.findall("12") == [("12", "", "", "", "")] - assert time_re.findall("12{sep}35".format(sep=sep)) == [ - ("12", "35", "", "", "") - ] + assert time_re.findall(f"12{sep}35") == [("12", "35", "", "", "")] assert time_re.findall("12{sep}35{sep}46".format(sep=sep)) == [ ("12", "35", "46", "", "") ] @@ -1658,7 +1645,6 @@ def test_escape(self): ) == datetime(2017, 12, 31, 2, 0) -@pytest.mark.skipif(sys.version_info < (3, 5), reason="requires python3.5 or higher") @pytest.mark.usefixtures("dt_parser") class TestFuzzInput: # Regression test for issue #860 diff --git a/tests/test_util.py b/tests/test_util.py index e48b4de06..2d4bd1921 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import time from datetime import datetime diff --git a/tests/utils.py b/tests/utils.py index 2a048feb3..05ac5af4c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import pytz from dateutil.zoneinfo import get_zonefile_instance diff --git a/tox.ini b/tox.ini index c8e1a3e45..492b09eb7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,11 @@ [tox] minversion = 3.18.0 -envlist = py{py3,27,35,36,37,38,39} +envlist = py{py3,36,37,38,39} skip_missing_interpreters = true [gh-actions] python = pypy3: pypy3 - 2.7: py27 - 3.5: py35 3.6: py36 3.7: py37 3.8: py38