diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5ace4600 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index d60e4bd3..3b9c3804 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -9,18 +9,14 @@ on: - cron: "0 0 1 * *" jobs: - test: + unit-tests: name: ${{ matrix.os }} (${{ matrix.python-version }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ["pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["pypy-3.11", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] os: [ubuntu-latest, macos-latest, windows-latest] - exclude: - # pypy3 randomly fails on Windows builds - - os: windows-latest - python-version: "pypy-3.7" include: - os: ubuntu-latest path: ~/.cache/pip @@ -29,15 +25,15 @@ jobs: - os: windows-latest path: ~\AppData\Local\pip\Cache steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v5 with: path: ${{ matrix.path }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -47,30 +43,36 @@ jobs: - name: Test with tox run: tox - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + if: ${{ success() }} + uses: codecov/codecov-action@v5 + with: + files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 with: - file: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} - lint: + linting: + name: Linting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.10 - uses: actions/setup-python@v2 - with: - python-version: "3.10" - - name: Cache pip - uses: actions/cache@v2 + - uses: actions/checkout@v6 + - uses: actions/cache@v5 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - - name: Cache pre-commit - uses: actions/cache@v2 + - uses: actions/cache@v5 with: path: ~/.cache/pre-commit key: ${{ runner.os }}-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} restore-keys: ${{ runner.os }}-pre-commit- + - name: Set up Python ${{ runner.python-version }} + uses: actions/setup-python@v6 + with: + python-version: "3.11" - name: Install dependencies run: | pip install -U pip setuptools wheel diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..07cbd63a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: release + +on: + workflow_dispatch: # run manually + push: # run on matching tags + tags: + - '*.*.*' + +jobs: + release-to-pypi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/cache@v5 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} + restore-keys: ${{ runner.os }}-pip- + - uses: actions/setup-python@v6 + with: + python-version: "3.14" + - name: Install dependencies + run: | + pip install -U pip setuptools wheel + pip install -U tox + - name: Publish package to PyPI + env: + FLIT_USERNAME: __token__ + FLIT_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: tox -e publish diff --git a/.gitignore b/.gitignore index 0448d0cf..4c8153df 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +junit.xml *.cover .hypothesis/ .pytest_cache/ @@ -209,3 +210,10 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk + +# Claude Code configurations +CLAUDE*.md +.claude/ + +# Kiro +.kiro/** diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28b5f8a0..7c6bc401 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v6.0.0 hooks: - id: check-ast - id: check-yaml @@ -12,22 +12,20 @@ repos: - id: check-builtin-literals - id: debug-statements - id: end-of-file-fixer - - id: fix-encoding-pragma - args: [--remove] - id: requirements-txt-fixer args: [requirements/requirements.txt, requirements/requirements-docs.txt, requirements/requirements-tests.txt] - id: trailing-whitespace - repo: https://github.com/timothycrosley/isort - rev: 5.10.1 + rev: 7.0.0 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.30.1 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: python-no-eval - id: python-check-blanket-noqa @@ -38,17 +36,18 @@ repos: - id: rst-inline-touching-normal - id: text-unicode-replacement-char - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 25.12.0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] - repo: https://github.com/pycqa/flake8 - rev: 4.0.1 + rev: 7.3.0 hooks: - id: flake8 additional_dependencies: [flake8-bugbear,flake8-annotations] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.930' + rev: v1.19.0 hooks: - id: mypy + args: [--cache-fine-grained, --show-error-codes] additional_dependencies: [types-python-dateutil] diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..57f8dd08 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,29 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: requirements/requirements-docs.txt diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 12bc86a6..4324cdf1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,50 @@ Changelog ========= +1.4.0 (2025-10-18) +------------------ + +- [ADDED] Added ``week_start`` parameter to ``floor()`` and ``ceil()`` methods. `PR #1222 `_ +- [ADDED] Added ``FORMAT_RFC3339_STRICT`` with a T separator. `PR #1201 `_ +- [ADDED] Added Macedonian in Latin locale support. `PR #1200 `_ +- [ADDED] Added Persian/Farsi locale support. `PR #1190 `_ +- [ADDED] Added week and weeks to Thai locale timeframes. `PR #1218 `_ +- [ADDED] Added weeks to Catalan locale. `PR #1189 `_ +- [ADDED] Added Persian names of months, month-abbreviations and day-abbreviations in Gregorian calendar. `PR #1172 `_ +- [CHANGED] Migrated Arrow to use ZoneInfo for timezones instead of pytz. `PR #1217 `_ +- [FIXED] Fixed humanize month limits. `PR #1224 `_ +- [FIXED] Fixed type hint of ``Arrow.__getattr__``. `PR #1171 `_ +- [FIXED] Fixed spelling and removed poorly used expressions in Korean locale. `PR #1181 `_ +- [FIXED] Updated ``shift()`` method for issue #1145. `PR #1194 `_ +- [FIXED] Improved Greek locale translations (seconds, days, "ago", and month typo). `PR #1184 `_, `PR #1186 `_ +- [FIXED] Addressed ``datetime.utcnow`` deprecation warning. `PR #1182 `_ +- [INTERNAL] Added codecov test results. `PR #1223 `_ +- [INTERNAL] Updated CI dependencies (actions/setup-python, actions/checkout, codecov/codecov-action, actions/cache). +- [INTERNAL] Added docstrings to parser.py. `PR #1010 `_ +- [INTERNAL] Updated Python versions support and bumped CI dependencies. `PR #1177 `_ +- [INTERNAL] Added dependabot for GitHub actions. `PR #1193 `_ +- [INTERNAL] Moved dateutil types to test requirements. `PR #1183 `_ +- [INTERNAL] Added documentation link for ``arrow.format``. `PR #1180 `_ + +1.3.0 (2023-09-30) +------------------ + +- [ADDED] Added official support for Python 3.11 and 3.12. +- [ADDED] Added dependency on ``types-python-dateutil`` to improve Arrow mypy compatibility. `PR #1102 `_ +- [FIX] Updates to Italian, Romansh, Hungarian, Finish and Arabic locales. +- [FIX] Handling parsing of UTC prefix in timezone strings. +- [CHANGED] Update documentation to improve readability. +- [CHANGED] Dropped support for Python 3.6 and 3.7, which are end-of-life. +- [INTERNAL] Migrate from ``setup.py``/Twine to ``pyproject.toml``/Flit for packaging and distribution. +- [INTERNAL] Adopt ``.readthedocs.yaml`` configuration file for continued ReadTheDocs support. + +1.2.3 (2022-06-25) +------------------ + +- [NEW] Added Amharic, Armenian, Georgian, Laotian and Uzbek locales. +- [FIX] Updated Danish locale and associated tests. +- [INTERNAL] Small fixes to CI. + 1.2.2 (2022-01-19) ------------------ @@ -278,7 +322,7 @@ After 8 years we're pleased to announce Arrow v1.0. Thanks to the entire Python - [FIX] Consolidated and simplified German locales. - [INTERNAL] Moved testing suite from nosetest/Chai to pytest/pytest-mock. - [INTERNAL] Converted xunit-style setup and teardown functions in tests to pytest fixtures. -- [INTERNAL] Setup Github Actions for CI alongside Travis. +- [INTERNAL] Setup GitHub Actions for CI alongside Travis. - [INTERNAL] Help support Arrow's future development by donating to the project on `Open Collective `_. 0.15.5 (2020-01-03) @@ -652,7 +696,7 @@ The following will work in v0.15.0: - [NEW] Brazilian locale (Augusto2112) - [NEW] Dutch locale (OrangeTux) - [NEW] Italian locale (Pertux) -- [NEW] Austrain locale (LeChewbacca) +- [NEW] Austrian locale (LeChewbacca) - [NEW] Tagalog locale (Marksteve) - [FIX] Corrected spelling and day numbers in German locale (LeChewbacca) - [FIX] Factory ``get`` method should now handle unicode strings correctly (Bwells) diff --git a/LICENSE b/LICENSE index 4f9eea5d..ff864f3b 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2021 Chris Smith + Copyright 2023 Chris Smith Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 9abe9773..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include LICENSE CHANGELOG.rst README.rst Makefile tox.ini -recursive-include requirements *.txt -recursive-include tests *.py -recursive-include docs *.py *.rst *.bat Makefile diff --git a/Makefile b/Makefile index 5f885157..1435ac21 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,16 @@ .PHONY: auto test docs clean -auto: build310 +auto: build311 -build36: PYTHON_VER = python3.6 -build37: PYTHON_VER = python3.7 build38: PYTHON_VER = python3.8 build39: PYTHON_VER = python3.9 build310: PYTHON_VER = python3.10 +build311: PYTHON_VER = python3.11 +build312: PYTHON_VER = python3.12 +build313: PYTHON_VER = python3.13 +build314: PYTHON_VER = python3.14 -build36 build37 build38 build39 build310: clean +build36 build37 build38 build39 build310 build311 build312 build313 build314: clean $(PYTHON_VER) -m venv venv . venv/bin/activate; \ pip install -U pip setuptools wheel; \ @@ -23,7 +25,7 @@ test: lint: . venv/bin/activate; \ - pre-commit run --all-files + pre-commit run --all-files --show-diff-on-failure clean-docs: rm -rf docs/_build @@ -42,15 +44,9 @@ clean: clean-dist rm -f .coverage coverage.xml ./**/*.pyc clean-dist: - rm -rf dist build .egg .eggs arrow.egg-info + rm -rf dist build *.egg *.eggs *.egg-info -build-dist: +build-dist: clean-dist . venv/bin/activate; \ - pip install -U pip setuptools twine wheel; \ - python setup.py sdist bdist_wheel - -upload-dist: - . venv/bin/activate; \ - twine upload dist/* - -publish: test clean-dist build-dist upload-dist clean-dist + pip install -U flit; \ + flit build diff --git a/README.rst b/README.rst index 69f91fe5..11c44155 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ Features -------- - Fully-implemented, drop-in replacement for datetime -- Support for Python 3.6+ +- Support for Python 3.8+ - Timezone-aware and UTC by default - Super-simple creation options for many common input scenarios - ``shift`` method with support for relative offsets, including weeks diff --git a/arrow/__init__.py b/arrow/__init__.py index bc597097..9232b379 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -11,6 +11,7 @@ FORMAT_RFC1123, FORMAT_RFC2822, FORMAT_RFC3339, + FORMAT_RFC3339_STRICT, FORMAT_RSS, FORMAT_W3C, ) @@ -33,6 +34,7 @@ "FORMAT_RFC1123", "FORMAT_RFC2822", "FORMAT_RFC3339", + "FORMAT_RFC3339_STRICT", "FORMAT_RSS", "FORMAT_W3C", "ParserError", diff --git a/arrow/_version.py b/arrow/_version.py index bc86c944..3e8d9f94 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.2.2" +__version__ = "1.4.0" diff --git a/arrow/api.py b/arrow/api.py index d8ed24b9..6fd2640d 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -26,8 +26,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, -) -> Arrow: - ... # pragma: no cover +) -> Arrow: ... # pragma: no cover @overload @@ -36,8 +35,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, -) -> Arrow: - ... # pragma: no cover +) -> Arrow: ... # pragma: no cover @overload @@ -57,8 +55,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, -) -> Arrow: - ... # pragma: no cover +) -> Arrow: ... # pragma: no cover @overload @@ -69,8 +66,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, -) -> Arrow: - ... # pragma: no cover +) -> Arrow: ... # pragma: no cover @overload @@ -81,8 +77,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, -) -> Arrow: - ... # pragma: no cover +) -> Arrow: ... # pragma: no cover def get(*args: Any, **kwargs: Any) -> Arrow: diff --git a/arrow/arrow.py b/arrow/arrow.py index 21b0347f..eecf2326 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -4,23 +4,24 @@ """ - import calendar import re import sys -from datetime import date +from datetime import date as dt_date from datetime import datetime as dt_datetime from datetime import time as dt_time -from datetime import timedelta +from datetime import timedelta, timezone from datetime import tzinfo as dt_tzinfo from math import trunc from time import struct_time from typing import ( Any, ClassVar, + Final, Generator, Iterable, List, + Literal, Mapping, Optional, Tuple, @@ -36,12 +37,6 @@ from arrow.constants import DEFAULT_LOCALE, DEHUMANIZE_LOCALES from arrow.locales import TimeFrameLiteral -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Final, Literal -else: - from typing import Final, Literal # pragma: no cover - - TZ_EXPR = Union[dt_tzinfo, str] _T_FRAMES = Literal[ @@ -128,6 +123,7 @@ class Arrow: ] _ATTRS_PLURAL: Final[List[str]] = [f"{a}s" for a in _ATTRS] _MONTHS_PER_QUARTER: Final[int] = 3 + _MONTHS_PER_YEAR: Final[int] = 12 _SECS_PER_MINUTE: Final[int] = 60 _SECS_PER_HOUR: Final[int] = 60 * 60 _SECS_PER_DAY: Final[int] = 60 * 60 * 24 @@ -162,15 +158,15 @@ def __init__( **kwargs: Any, ) -> None: if tzinfo is None: - tzinfo = dateutil_tz.tzutc() + tzinfo = timezone.utc # detect that tzinfo is a pytz object (issue #626) elif ( isinstance(tzinfo, dt_tzinfo) and hasattr(tzinfo, "localize") and hasattr(tzinfo, "zone") - and tzinfo.zone # type: ignore[attr-defined] + and tzinfo.zone ): - tzinfo = parser.TzinfoParser.parse(tzinfo.zone) # type: ignore[attr-defined] + tzinfo = parser.TzinfoParser.parse(tzinfo.zone) elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) @@ -197,7 +193,7 @@ def now(cls, tzinfo: Optional[dt_tzinfo] = None) -> "Arrow": """ if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() + tzinfo = dt_datetime.now().astimezone().tzinfo dt = dt_datetime.now(tzinfo) @@ -225,7 +221,7 @@ def utcnow(cls) -> "Arrow": """ - dt = dt_datetime.now(dateutil_tz.tzutc()) + dt = dt_datetime.now(timezone.utc) return cls( dt.year, @@ -254,7 +250,7 @@ def fromtimestamp( """ if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() + tzinfo = dt_datetime.now().astimezone().tzinfo elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) @@ -288,7 +284,7 @@ def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow": raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") timestamp = util.normalize_timestamp(float(timestamp)) - dt = dt_datetime.utcfromtimestamp(timestamp) + dt = dt_datetime.fromtimestamp(timestamp, timezone.utc) return cls( dt.year, @@ -298,7 +294,7 @@ def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow": dt.minute, dt.second, dt.microsecond, - dateutil_tz.tzutc(), + timezone.utc, fold=getattr(dt, "fold", 0), ) @@ -322,7 +318,7 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr if tzinfo is None: if dt.tzinfo is None: - tzinfo = dateutil_tz.tzutc() + tzinfo = timezone.utc else: tzinfo = dt.tzinfo @@ -339,7 +335,7 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr ) @classmethod - def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": + def fromdate(cls, date: dt_date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": """Constructs an :class:`Arrow ` object from a ``date`` and optional replacement timezone. All time values are set to 0. @@ -349,7 +345,7 @@ def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": """ if tzinfo is None: - tzinfo = dateutil_tz.tzutc() + tzinfo = timezone.utc return cls(date.year, date.month, date.day, tzinfo=tzinfo) @@ -495,8 +491,8 @@ def range( yield current values = [getattr(current, f) for f in cls._ATTRS] - current = cls(*values, tzinfo=tzinfo).shift( # type: ignore - **{frame_relative: relative_steps} + current = cls(*values, tzinfo=tzinfo).shift( # type: ignore[misc] + check_imaginary=True, **{frame_relative: relative_steps} ) if frame in ["month", "quarter", "year"] and current.day < original_day: @@ -554,14 +550,14 @@ def span( (, ) """ - if not 1 <= week_start <= 7: - raise ValueError("week_start argument must be between 1 and 7.") util.validate_bounds(bounds) frame_absolute, frame_relative, relative_steps = self._get_frames(frame) if frame_absolute == "week": + if not 1 <= week_start <= 7: + raise ValueError("week_start argument must be between 1 and 7.") attr = "day" elif frame_absolute == "quarter": attr = "month" @@ -578,7 +574,7 @@ def span( for _ in range(3 - len(values)): values.append(1) - floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore + floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore[misc] if frame_absolute == "week": # if week_start is greater than self.isoweekday() go back one week by setting delta = 7 @@ -587,7 +583,9 @@ def span( elif frame_absolute == "quarter": floor = floor.shift(months=-((self.month - 1) % 3)) - ceil = floor.shift(**{frame_relative: count * relative_steps}) + ceil = floor.shift( + check_imaginary=True, **{frame_relative: count * relative_steps} + ) if bounds[0] == "(": floor = floor.shift(microseconds=+1) @@ -597,39 +595,49 @@ def span( return floor, ceil - def floor(self, frame: _T_FRAMES) -> "Arrow": + def floor(self, frame: _T_FRAMES, **kwargs: Any) -> "Arrow": """Returns a new :class:`Arrow ` object, representing the "floor" of the timespan of the :class:`Arrow ` object in a given timeframe. Equivalent to the first element in the 2-tuple returned by :func:`span `. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where + Monday is 1 and Sunday is 7. Usage:: >>> arrow.utcnow().floor('hour') + >>> arrow.utcnow().floor('week', week_start=7) + + """ - return self.span(frame)[0] + return self.span(frame, **kwargs)[0] - def ceil(self, frame: _T_FRAMES) -> "Arrow": + def ceil(self, frame: _T_FRAMES, **kwargs: Any) -> "Arrow": """Returns a new :class:`Arrow ` object, representing the "ceiling" of the timespan of the :class:`Arrow ` object in a given timeframe. Equivalent to the second element in the 2-tuple returned by :func:`span `. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where + Monday is 1 and Sunday is 7. Usage:: >>> arrow.utcnow().ceil('hour') + >>> arrow.utcnow().ceil('week', week_start=7) + + """ - return self.span(frame)[1] + return self.span(frame, **kwargs)[1] @classmethod def span_range( @@ -792,7 +800,6 @@ def __str__(self) -> str: return self._datetime.isoformat() def __format__(self, formatstr: str) -> str: - if len(formatstr) > 0: return self.format(formatstr) @@ -803,8 +810,7 @@ def __hash__(self) -> int: # attributes and properties - def __getattr__(self, name: str) -> int: - + def __getattr__(self, name: str) -> Any: if name == "week": return self.isocalendar()[1] @@ -812,7 +818,7 @@ def __getattr__(self, name: str) -> int: return int((self.month - 1) / self._MONTHS_PER_QUARTER) + 1 if not name.startswith("_"): - value: Optional[int] = getattr(self._datetime, name, None) + value: Optional[Any] = getattr(self._datetime, name, None) if value is not None: return value @@ -965,7 +971,6 @@ def replace(self, **kwargs: Any) -> "Arrow": absolute_kwargs = {} for key, value in kwargs.items(): - if key in self._ATTRS: absolute_kwargs[key] = value elif key in ["week", "quarter"]: @@ -988,10 +993,15 @@ def replace(self, **kwargs: Any) -> "Arrow": return self.fromdatetime(current) - def shift(self, **kwargs: Any) -> "Arrow": + def shift(self, check_imaginary: bool = True, **kwargs: Any) -> "Arrow": """Returns a new :class:`Arrow ` object with attributes updated according to inputs. + Parameters: + check_imaginary (bool): If True (default), will check for and resolve + imaginary times (like during DST transitions). If False, skips this check. + + Use pluralized property names to relatively shift their current value: >>> import arrow @@ -1022,7 +1032,6 @@ def shift(self, **kwargs: Any) -> "Arrow": additional_attrs = ["weeks", "quarters", "weekday"] for key, value in kwargs.items(): - if key in self._ATTRS_PLURAL or key in additional_attrs: relative_kwargs[key] = value else: @@ -1039,7 +1048,8 @@ def shift(self, **kwargs: Any) -> "Arrow": current = self._datetime + relativedelta(**relative_kwargs) - if not dateutil_tz.datetime_exists(current): + # If check_imaginary is True, perform the check for imaginary times (DST transitions) + if check_imaginary and not dateutil_tz.datetime_exists(current): current = dateutil_tz.resolve_imaginary(current) return self.fromdatetime(current) @@ -1096,7 +1106,8 @@ def format( self, fmt: str = "YYYY-MM-DD HH:mm:ssZZ", locale: str = DEFAULT_LOCALE ) -> str: """Returns a string representation of the :class:`Arrow ` object, - formatted according to the provided format string. + formatted according to the provided format string. For a list of formatting values, + see :ref:`supported-tokens` :param fmt: the format string. :param locale: the locale to format. @@ -1151,7 +1162,7 @@ def humanize( locale = locales.get_locale(locale) if other is None: - utc = dt_datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) + utc = dt_datetime.now(timezone.utc).replace(tzinfo=timezone.utc) dt = utc.astimezone(self._datetime.tzinfo) elif isinstance(other, Arrow): @@ -1189,6 +1200,7 @@ def humanize( elif diff < self._SECS_PER_MINUTE * 2: return locale.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( @@ -1197,36 +1209,54 @@ def humanize( elif diff < self._SECS_PER_HOUR * 2: return locale.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) - elif diff < self._SECS_PER_DAY * 2: + + calendar_diff = ( + relativedelta(dt, self._datetime) + if self._datetime < dt + else relativedelta(self._datetime, dt) + ) + calendar_months = ( + calendar_diff.years * self._MONTHS_PER_YEAR + calendar_diff.months + ) + + # For months, if more than 2 weeks, count as a full month + if calendar_diff.days > 14: + calendar_months += 1 + + calendar_months = min(calendar_months, self._MONTHS_PER_YEAR) + + if diff < self._SECS_PER_DAY * 2: return locale.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) + elif calendar_months >= 1 and diff < self._SECS_PER_YEAR: + if calendar_months == 1: + return locale.describe( + "month", sign, only_distance=only_distance + ) + else: + months = sign * calendar_months + return locale.describe( + "months", months, only_distance=only_distance + ) + elif diff < self._SECS_PER_WEEK * 2: return locale.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) - elif diff < self._SECS_PER_MONTH * 2: - return locale.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( - "months", months, only_distance=only_distance - ) - elif diff < self._SECS_PER_YEAR * 2: return locale.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) @@ -1259,11 +1289,10 @@ def humanize( ) if trunc(abs(delta)) != 1: - granularity += "s" # type: ignore + granularity += "s" # type: ignore[assignment] return locale.describe(granularity, delta, only_distance=only_distance) else: - if not granularity: raise ValueError( "Empty granularity list provided. " @@ -1314,7 +1343,7 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float: def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": """Returns a new :class:`Arrow ` object, that represents - the time difference relative to the attrbiutes of the + the time difference relative to the attributes of the :class:`Arrow ` object. :param timestring: a ``str`` representing a humanized relative time. @@ -1367,7 +1396,6 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # Search input string for each time unit within locale for unit, unit_object in locale_obj.timeframes.items(): - # Need to check the type of unit_object to create the correct dictionary if isinstance(unit_object, Mapping): strings_to_search = unit_object @@ -1378,13 +1406,12 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # Needs to cycle all through strings as some locales have strings that # could overlap in a regex match, since input validation isn't being performed. for time_delta, time_string in strings_to_search.items(): - # Replace {0} with regex \d representing digits search_string = str(time_string) search_string = search_string.format(r"\d+") # Create search pattern and find within string - pattern = re.compile(fr"(^|\b|\d){search_string}") + pattern = re.compile(rf"(^|\b|\d){search_string}") match = pattern.search(input_string) # If there is no match continue to next iteration @@ -1419,19 +1446,19 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # Assert error if string does not modify any units if not any([True for k, v in unit_visited.items() if v]): raise ValueError( - "Input string not valid. Note: Some locales do not support the week granulairty in Arrow. " + "Input string not valid. Note: Some locales do not support the week granularity in Arrow. " "If you are attempting to use the week granularity on an unsupported locale, this could be the cause of this error." ) # Sign logic future_string = locale_obj.future future_string = future_string.format(".*") - future_pattern = re.compile(fr"^{future_string}$") + future_pattern = re.compile(rf"^{future_string}$") future_pattern_match = future_pattern.findall(input_string) past_string = locale_obj.past past_string = past_string.format(".*") - past_pattern = re.compile(fr"^{past_string}$") + past_pattern = re.compile(rf"^{past_string}$") past_pattern_match = past_pattern.findall(input_string) # If a string contains the now unit, there will be no relative units, hence the need to check if the now unit @@ -1451,7 +1478,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": time_changes = {k: sign_val * v for k, v in time_object_info.items()} - return current_time.shift(**time_changes) + return current_time.shift(check_imaginary=True, **time_changes) # query functions @@ -1515,7 +1542,7 @@ def is_between( # datetime methods - def date(self) -> date: + def date(self) -> dt_date: """Returns a ``date`` object with the same year, month and day. Usage:: @@ -1718,7 +1745,6 @@ def for_json(self) -> str: # math def __add__(self, other: Any) -> "Arrow": - if isinstance(other, (timedelta, relativedelta)): return self.fromdatetime(self._datetime + other, self._datetime.tzinfo) @@ -1736,7 +1762,6 @@ def __sub__(self, other: Union[dt_datetime, "Arrow"]) -> timedelta: pass # pragma: no cover def __sub__(self, other: Any) -> Union[timedelta, "Arrow"]: - if isinstance(other, (timedelta, relativedelta)): return self.fromdatetime(self._datetime - other, self._datetime.tzinfo) @@ -1749,7 +1774,6 @@ def __sub__(self, other: Any) -> Union[timedelta, "Arrow"]: return NotImplemented def __rsub__(self, other: Any) -> timedelta: - if isinstance(other, dt_datetime): return other - self._datetime @@ -1758,42 +1782,36 @@ def __rsub__(self, other: Any) -> timedelta: # comparisons def __eq__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return False return self._datetime == self._get_datetime(other) def __ne__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return True return not self.__eq__(other) def __gt__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented return self._datetime > self._get_datetime(other) def __ge__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented return self._datetime >= self._get_datetime(other) def __lt__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented return self._datetime < self._get_datetime(other) def __le__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented @@ -1804,7 +1822,7 @@ def __le__(self, other: Any) -> bool: def _get_tzinfo(tz_expr: Optional[TZ_EXPR]) -> dt_tzinfo: """Get normalized tzinfo object from various inputs.""" if tz_expr is None: - return dateutil_tz.tzutc() + return timezone.utc if isinstance(tz_expr, dt_tzinfo): return tz_expr else: @@ -1865,7 +1883,6 @@ def _get_frames(cls, name: _T_FRAMES) -> Tuple[str, str, int]: def _get_iteration_params(cls, end: Any, limit: Optional[int]) -> Tuple[Any, int]: """Sets default end and limit values for range method.""" if end is None: - if limit is None: raise ValueError("One of 'end' or 'limit' is required.") @@ -1879,7 +1896,7 @@ def _get_iteration_params(cls, end: Any, limit: Optional[int]) -> Tuple[Any, int @staticmethod def _is_last_day_of_month(date: "Arrow") -> bool: """Returns a boolean indicating whether the datetime is the last day of the month.""" - return date.day == calendar.monthrange(date.year, date.month)[1] + return cast(int, date.day) == calendar.monthrange(date.year, date.month)[1] Arrow.min = Arrow.fromdatetime(dt_datetime.min) diff --git a/arrow/constants.py b/arrow/constants.py index 4c6fa5cb..532e9596 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -2,11 +2,7 @@ import sys from datetime import datetime - -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Final -else: - from typing import Final # pragma: no cover +from typing import Final # datetime.max.timestamp() errors on Windows, so we must hardcode # the highest possible datetime value that can output a timestamp. @@ -21,7 +17,7 @@ # Must get max value of ctime on Windows based on architecture (x32 vs x64) # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/ctime-ctime32-ctime64-wctime-wctime32-wctime64 # Note: this may occur on both 32-bit Linux systems (issue #930) along with Windows systems - is_64bits = sys.maxsize > 2 ** 32 + is_64bits = sys.maxsize > 2**32 _MAX_TIMESTAMP = ( datetime(3000, 1, 1, 23, 59, 59, 999999).timestamp() if is_64bits @@ -166,6 +162,12 @@ "ka-ge", "kk", "kk-kz", + # "lo", + # "lo-la", "am", "am-et", + "hy-am", + "hy", + "uz", + "uz-uz", } diff --git a/arrow/factory.py b/arrow/factory.py index aad4af8b..0913bfe1 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -5,16 +5,13 @@ """ - import calendar -from datetime import date, datetime +from datetime import date, datetime, timezone from datetime import tzinfo as dt_tzinfo from decimal import Decimal from time import struct_time from typing import Any, List, Optional, Tuple, Type, Union, overload -from dateutil import tz as dateutil_tz - from arrow import parser from arrow.arrow import TZ_EXPR, Arrow from arrow.constants import DEFAULT_LOCALE @@ -41,8 +38,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, - ) -> Arrow: - ... # pragma: no cover + ) -> Arrow: ... # pragma: no cover @overload def get( @@ -62,8 +58,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, - ) -> Arrow: - ... # pragma: no cover + ) -> Arrow: ... # pragma: no cover @overload def get( @@ -74,8 +69,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, - ) -> Arrow: - ... # pragma: no cover + ) -> Arrow: ... # pragma: no cover @overload def get( @@ -86,8 +80,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, - ) -> Arrow: - ... # pragma: no cover + ) -> Arrow: ... # pragma: no cover def get(self, *args: Any, **kwargs: Any) -> Arrow: """Returns an :class:`Arrow ` object based on flexible inputs. @@ -230,7 +223,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: elif not isinstance(arg, str) and is_timestamp(arg): if tz is None: # set to UTC by default - tz = dateutil_tz.tzutc() + tz = timezone.utc return self.type.fromtimestamp(arg, tzinfo=tz) # (Arrow) -> from the object's datetime @ tzinfo @@ -267,11 +260,9 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.") elif arg_count == 2: - arg_1, arg_2 = args[0], args[1] if isinstance(arg_1, datetime): - # (datetime, tzinfo/str) -> fromdatetime @ tzinfo if isinstance(arg_2, (dt_tzinfo, str)): return self.type.fromdatetime(arg_1, tzinfo=arg_2) @@ -281,7 +272,6 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: ) elif isinstance(arg_1, date): - # (date, tzinfo/str) -> fromdate @ tzinfo if isinstance(arg_2, (dt_tzinfo, str)): return self.type.fromdate(arg_1, tzinfo=arg_2) @@ -341,7 +331,7 @@ def now(self, tz: Optional[TZ_EXPR] = None) -> Arrow: """ if tz is None: - tz = dateutil_tz.tzlocal() + tz = datetime.now().astimezone().tzinfo elif not isinstance(tz, dt_tzinfo): tz = parser.TzinfoParser.parse(tz) diff --git a/arrow/formatter.py b/arrow/formatter.py index 728bea1a..6c6a718c 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -1,21 +1,12 @@ """Provides the :class:`Arrow ` class, an improved formatter for datetimes.""" import re -import sys -from datetime import datetime, timedelta -from typing import Optional, Pattern, cast - -from dateutil import tz as dateutil_tz +from datetime import datetime, timedelta, timezone +from typing import Final, Optional, Pattern, cast from arrow import locales from arrow.constants import DEFAULT_LOCALE -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Final -else: - from typing import Final # pragma: no cover - - FORMAT_ATOM: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" FORMAT_COOKIE: Final[str] = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ" FORMAT_RFC822: Final[str] = "ddd, DD MMM YY HH:mm:ss Z" @@ -24,12 +15,12 @@ FORMAT_RFC1123: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" FORMAT_RFC2822: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" FORMAT_RFC3339: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" +FORMAT_RFC3339_STRICT: Final[str] = "YYYY-MM-DDTHH:mm:ssZZ" FORMAT_RSS: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" FORMAT_W3C: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" 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 # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578 @@ -41,18 +32,15 @@ class DateTimeFormatter: locale: locales.Locale def __init__(self, locale: str = DEFAULT_LOCALE) -> None: - self.locale = locales.get_locale(locale) def format(cls, dt: datetime, fmt: str) -> str: - # FIXME: _format_token() is nullable return cls._FORMAT_RE.sub( lambda m: cast(str, cls._format_token(dt, m.group(0))), fmt ) def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]: - if token and token.startswith("[") and token.endswith("]"): return token[1:-1] @@ -132,7 +120,7 @@ def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]: if token in ["ZZ", "Z"]: separator = ":" if token == "ZZ" else "" - tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo + tz = timezone.utc if dt.tzinfo is None else dt.tzinfo # `dt` must be aware object. Otherwise, this line will raise AttributeError # https://github.com/arrow-py/arrow/pull/883#discussion_r529866834 # datetime awareness: https://docs.python.org/3/library/datetime.html#aware-and-naive-objects diff --git a/arrow/locales.py b/arrow/locales.py index d5652370..757df480 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1,12 +1,12 @@ """Provides internationalization for arrow in over 60 languages and dialects.""" -import sys from math import trunc from typing import ( Any, ClassVar, Dict, List, + Literal, Mapping, Optional, Sequence, @@ -16,11 +16,6 @@ cast, ) -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Literal -else: - from typing import Literal # pragma: no cover - TimeFrameLiteral = Literal[ "now", "second", @@ -45,7 +40,6 @@ str, Sequence[str], Mapping[str, str], Mapping[str, Sequence[str]] ] - _locale_map: Dict[str, Type["Locale"]] = {} @@ -130,7 +124,6 @@ def __init_subclass__(cls, **kwargs: Any) -> None: _locale_map[locale_name.lower().replace("_", "-")] = cls def __init__(self) -> None: - self._month_name_to_ordinal = None def describe( @@ -175,7 +168,7 @@ def describe_multi( # Needed to determine the correct relative string to use timeframe_value = 0 - for _unit_name, unit_value in timeframes: + for _, unit_value in timeframes: if trunc(unit_value) != 0: timeframe_value = trunc(unit_value) break @@ -286,7 +279,6 @@ def _format_relative( timeframe: TimeFrameLiteral, delta: Union[float, int], ) -> str: - if timeframe == "now": return humanized @@ -296,7 +288,6 @@ def _format_relative( class EnglishLocale(Locale): - names = [ "en", "en-us", @@ -427,7 +418,7 @@ class ItalianLocale(Locale): "hours": "{0} ore", "day": "un giorno", "days": "{0} giorni", - "week": "una settimana,", + "week": "una settimana", "weeks": "{0} settimane", "month": "un mese", "months": "{0} mesi", @@ -560,7 +551,6 @@ def _ordinal_number(self, n: int) -> str: class FrenchBaseLocale(Locale): - past = "il y a {0}" future = "dans {0}" and_word = "et" @@ -622,7 +612,6 @@ def _ordinal_number(self, n: int) -> str: class FrenchLocale(FrenchBaseLocale, Locale): - names = ["fr", "fr-fr"] month_abbreviations = [ @@ -643,7 +632,6 @@ class FrenchLocale(FrenchBaseLocale, Locale): class FrenchCanadianLocale(FrenchBaseLocale, Locale): - names = ["fr-ca"] month_abbreviations = [ @@ -664,23 +652,22 @@ class FrenchCanadianLocale(FrenchBaseLocale, Locale): class GreekLocale(Locale): - names = ["el", "el-gr"] - past = "{0} πριν" + past = "πριν από {0}" future = "σε {0}" and_word = "και" timeframes = { "now": "τώρα", - "second": "ένα δεύτερο", + "second": "ένα δευτερόλεπτο", "seconds": "{0} δευτερόλεπτα", "minute": "ένα λεπτό", "minutes": "{0} λεπτά", "hour": "μία ώρα", "hours": "{0} ώρες", - "day": "μία μέρα", - "days": "{0} μέρες", + "day": "μία ημέρα", + "days": "{0} ημέρες", "week": "μία εβδομάδα", "weeks": "{0} εβδομάδες", "month": "ένα μήνα", @@ -710,7 +697,7 @@ class GreekLocale(Locale): "Φεβ", "Μαρ", "Απρ", - "Μαϊ", + "Μαΐ", "Ιον", "Ιολ", "Αυγ", @@ -734,7 +721,6 @@ class GreekLocale(Locale): class JapaneseLocale(Locale): - names = ["ja", "ja-jp"] past = "{0}前" @@ -790,12 +776,20 @@ class JapaneseLocale(Locale): "12", ] - day_names = ["", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日"] + day_names = [ + "", + "月曜日", + "火曜日", + "水曜日", + "木曜日", + "金曜日", + "土曜日", + "日曜日", + ] day_abbreviations = ["", "月", "火", "水", "木", "金", "土", "日"] class SwedishLocale(Locale): - names = ["sv", "sv-se"] past = "för {0} sen" @@ -865,7 +859,6 @@ class SwedishLocale(Locale): class FinnishLocale(Locale): - names = ["fi", "fi-fi"] # The finnish grammar is very complex, and its hard to convert @@ -876,14 +869,16 @@ class FinnishLocale(Locale): timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "juuri nyt", - "second": "sekunti", - "seconds": {"past": "{0} muutama sekunti", "future": "{0} muutaman sekunnin"}, + "second": {"past": "sekunti", "future": "sekunnin"}, + "seconds": {"past": "{0} sekuntia", "future": "{0} sekunnin"}, "minute": {"past": "minuutti", "future": "minuutin"}, "minutes": {"past": "{0} minuuttia", "future": "{0} minuutin"}, "hour": {"past": "tunti", "future": "tunnin"}, "hours": {"past": "{0} tuntia", "future": "{0} tunnin"}, - "day": "päivä", + "day": {"past": "päivä", "future": "päivän"}, "days": {"past": "{0} päivää", "future": "{0} päivän"}, + "week": {"past": "viikko", "future": "viikon"}, + "weeks": {"past": "{0} viikkoa", "future": "{0} viikon"}, "month": {"past": "kuukausi", "future": "kuukauden"}, "months": {"past": "{0} kuukautta", "future": "{0} kuukauden"}, "year": {"past": "vuosi", "future": "vuoden"}, @@ -952,7 +947,6 @@ def _ordinal_number(self, n: int) -> str: class ChineseCNLocale(Locale): - names = ["zh", "zh-cn"] past = "{0}前" @@ -1007,12 +1001,20 @@ class ChineseCNLocale(Locale): "12", ] - day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] + day_names = [ + "", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期日", + ] day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] class ChineseTWLocale(Locale): - names = ["zh-tw"] past = "{0}前" @@ -1073,7 +1075,6 @@ class ChineseTWLocale(Locale): class HongKongLocale(Locale): - names = ["zh-hk"] past = "{0}前" @@ -1128,12 +1129,20 @@ class HongKongLocale(Locale): "12", ] - day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] + day_names = [ + "", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期日", + ] day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] class KoreanLocale(Locale): - names = ["ko", "ko-kr"] past = "{0} 전" @@ -1158,7 +1167,6 @@ class KoreanLocale(Locale): } special_dayframes = { - -3: "그끄제", -2: "그제", -1: "어제", 1: "내일", @@ -1167,7 +1175,7 @@ class KoreanLocale(Locale): 4: "그글피", } - special_yearframes = {-2: "제작년", -1: "작년", 1: "내년", 2: "내후년"} + special_yearframes = {-2: "재작년", -1: "작년", 1: "내년", 2: "내후년"} month_names = [ "", @@ -1200,11 +1208,32 @@ class KoreanLocale(Locale): "12", ] - day_names = ["", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"] + day_names = [ + "", + "월요일", + "화요일", + "수요일", + "목요일", + "금요일", + "토요일", + "일요일", + ] day_abbreviations = ["", "월", "화", "수", "목", "금", "토", "일"] def _ordinal_number(self, n: int) -> str: - ordinals = ["0", "첫", "두", "세", "네", "다섯", "여섯", "일곱", "여덟", "아홉", "열"] + ordinals = [ + "0", + "첫", + "두", + "세", + "네", + "다섯", + "여섯", + "일곱", + "여덟", + "아홉", + "열", + ] if n < len(ordinals): return f"{ordinals[n]}번째" return f"{n}번째" @@ -1229,7 +1258,6 @@ def _format_relative( # derived locale types & implementations. class DutchLocale(Locale): - names = ["nl", "nl-nl"] past = "{0} geleden" @@ -1318,7 +1346,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class BelarusianLocale(SlavicBaseLocale): - names = ["be", "be-by"] past = "{0} таму" @@ -1397,7 +1424,6 @@ class BelarusianLocale(SlavicBaseLocale): class PolishLocale(SlavicBaseLocale): - names = ["pl", "pl-pl"] past = "{0} temu" @@ -1488,7 +1514,6 @@ class PolishLocale(SlavicBaseLocale): class RussianLocale(SlavicBaseLocale): - names = ["ru", "ru-ru"] past = "{0} назад" @@ -1579,7 +1604,6 @@ class RussianLocale(SlavicBaseLocale): class AfrikaansLocale(Locale): - names = ["af", "af-nl"] past = "{0} gelede" @@ -1595,6 +1619,8 @@ class AfrikaansLocale(Locale): "hours": "{0} ure", "day": "een dag", "days": "{0} dae", + "week": "een week", + "weeks": "{0} weke", "month": "een maand", "months": "{0} maande", "year": "een jaar", @@ -1646,7 +1672,6 @@ class AfrikaansLocale(Locale): class BulgarianLocale(SlavicBaseLocale): - names = ["bg", "bg-bg"] past = "{0} назад" @@ -1725,7 +1750,6 @@ class BulgarianLocale(SlavicBaseLocale): class UkrainianLocale(SlavicBaseLocale): - names = ["ua", "uk", "uk-ua"] past = "{0} тому" @@ -1902,13 +1926,111 @@ class MacedonianLocale(SlavicBaseLocale): ] -class GermanBaseLocale(Locale): +class MacedonianLatinLocale(SlavicBaseLocale): + names = ["mk-latn", "mk-mk-latn"] + + past = "pred {0}" + future = "za {0}" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "sega", + "second": "edna sekunda", + "seconds": { + "singular": "{0} sekunda", + "dual": "{0} sekundi", + "plural": "{0} sekundi", + }, + "minute": "edna minuta", + "minutes": { + "singular": "{0} minuta", + "dual": "{0} minuti", + "plural": "{0} minuti", + }, + "hour": "eden saat", + "hours": {"singular": "{0} saat", "dual": "{0} saati", "plural": "{0} saati"}, + "day": "eden den", + "days": {"singular": "{0} den", "dual": "{0} dena", "plural": "{0} dena"}, + "week": "edna nedela", + "weeks": { + "singular": "{0} nedela", + "dual": "{0} nedeli", + "plural": "{0} nedeli", + }, + "month": "eden mesec", + "months": { + "singular": "{0} mesec", + "dual": "{0} meseci", + "plural": "{0} meseci", + }, + "year": "edna godina", + "years": { + "singular": "{0} godina", + "dual": "{0} godini", + "plural": "{0} godini", + }, + } + + meridians = {"am": "dp", "pm": "pp", "AM": "pretpladne", "PM": "popladne"} + + month_names = [ + "", + "Januari", + "Fevruari", + "Mart", + "April", + "Maj", + "Juni", + "Juli", + "Avgust", + "Septemvri", + "Oktomvri", + "Noemvri", + "Dekemvri", + ] + month_abbreviations = [ + "", + "Jan", + "Fev", + "Mar", + "Apr", + "Maj", + "Jun", + "Jul", + "Avg", + "Sep", + "Okt", + "Noe", + "Dek", + ] + + day_names = [ + "", + "Ponedelnik", + "Vtornik", + "Sreda", + "Chetvrtok", + "Petok", + "Sabota", + "Nedela", + ] + day_abbreviations = [ + "", + "Pon", + "Vt", + "Sre", + "Chet", + "Pet", + "Sab", + "Ned", + ] + +class GermanBaseLocale(Locale): past = "vor {0}" future = "in {0}" and_word = "und" - timeframes = { + timeframes: ClassVar[Dict[TimeFrameLiteral, str]] = { "now": "gerade eben", "second": "einer Sekunde", "seconds": "{0} Sekunden", @@ -2003,23 +2125,22 @@ def describe( 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))) + humanized: str = self.timeframes_only_distance[timeframe].format( + trunc(abs(delta)) + ) return humanized class GermanLocale(GermanBaseLocale, Locale): - names = ["de", "de-de"] class SwissLocale(GermanBaseLocale, Locale): - names = ["de-ch"] class AustrianLocale(GermanBaseLocale, Locale): - names = ["de-at"] month_names = [ @@ -2040,7 +2161,6 @@ class AustrianLocale(GermanBaseLocale, Locale): class NorwegianLocale(Locale): - names = ["nb", "nb-no"] past = "for {0} siden" @@ -2112,7 +2232,6 @@ def _ordinal_number(self, n: int) -> str: class NewNorwegianLocale(Locale): - names = ["nn", "nn-no"] past = "for {0} sidan" @@ -2259,7 +2378,6 @@ class BrazilianPortugueseLocale(PortugueseLocale): class TagalogLocale(Locale): - names = ["tl", "tl-ph"] past = "nakaraang {0}" @@ -2333,7 +2451,6 @@ def _ordinal_number(self, n: int) -> str: class VietnameseLocale(Locale): - names = ["vi", "vi-vn"] past = "{0} trước" @@ -2402,7 +2519,6 @@ class VietnameseLocale(Locale): class TurkishLocale(Locale): - names = ["tr", "tr-tr"] past = "{0} önce" @@ -2474,7 +2590,6 @@ class TurkishLocale(Locale): class AzerbaijaniLocale(Locale): - names = ["az", "az-az"] past = "{0} əvvəl" @@ -2577,6 +2692,8 @@ class ArabicLocale(Locale): "hours": {"2": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"}, "day": "يوم", "days": {"2": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"}, + "week": "اسبوع", + "weeks": {"2": "اسبوعين", "ten": "{0} أسابيع", "higher": "{0} اسبوع"}, "month": "شهر", "months": {"2": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"}, "year": "سنة", @@ -2862,7 +2979,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class DanishLocale(Locale): - names = ["da", "da-dk"] past = "for {0} siden" @@ -2935,7 +3051,6 @@ def _ordinal_number(self, n: int) -> str: class MalayalamLocale(Locale): - names = ["ml"] past = "{0} മുമ്പ്" @@ -3009,7 +3124,6 @@ class MalayalamLocale(Locale): class HindiLocale(Locale): - names = ["hi", "hi-in"] past = "{0} पहले" @@ -3338,7 +3452,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class FarsiLocale(Locale): - names = ["fa", "fa-ir"] past = "{0} قبل" @@ -3354,6 +3467,10 @@ class FarsiLocale(Locale): "hours": "{0} ساعت", "day": "یک روز", "days": "{0} روز", + "week": "یک هفته", + "weeks": "{0} هفته", + "quarter": "یک فصل", + "quarters": "{0} فصل", "month": "یک ماه", "months": "{0} ماه", "year": "یک سال", @@ -3369,33 +3486,33 @@ class FarsiLocale(Locale): month_names = [ "", - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", + "ژانویه", + "فوریه", + "مارس", + "آوریل", + "مه", + "ژوئن", + "ژوئیه", + "اوت", + "سپتامبر", + "اکتبر", + "نوامبر", + "دسامبر", ] month_abbreviations = [ "", - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", + "ژانویه", + "فوریه", + "مارس", + "آوریل", + "مه", + "ژوئن", + "ژوئیه", + "اوت", + "سپتامبر", + "اکتبر", + "نوامبر", + "دسامبر", ] day_names = [ @@ -3408,11 +3525,19 @@ class FarsiLocale(Locale): "شنبه", "یکشنبه", ] - day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + day_abbreviations = [ + "", + "دو شنبه", + "سه شنبه", + "چهارشنبه", + "پنجشنبه", + "جمعه", + "شنبه", + "یکشنبه", + ] class HebrewLocale(Locale): - names = ["he", "he-il"] past = "לפני {0}" @@ -3523,7 +3648,6 @@ def describe_multi( class MarathiLocale(Locale): - names = ["mr"] past = "{0} आधी" @@ -3607,6 +3731,8 @@ class CatalanLocale(Locale): "hours": "{0} hores", "day": "un dia", "days": "{0} dies", + "week": "una setmana", + "weeks": "{0} setmanes", "month": "un mes", "months": "{0} mesos", "year": "un any", @@ -3730,7 +3856,6 @@ class BasqueLocale(Locale): class HungarianLocale(Locale): - names = ["hu", "hu-hu"] past = "{0} ezelőtt" @@ -3746,6 +3871,8 @@ class HungarianLocale(Locale): "hours": {"past": "{0} órával", "future": "{0} óra"}, "day": {"past": "egy nappal", "future": "egy nap"}, "days": {"past": "{0} nappal", "future": "{0} nap"}, + "week": {"past": "egy héttel", "future": "egy hét"}, + "weeks": {"past": "{0} héttel", "future": "{0} hét"}, "month": {"past": "egy hónappal", "future": "egy hónap"}, "months": {"past": "{0} hónappal", "future": "{0} hónap"}, "year": {"past": "egy évvel", "future": "egy év"}, @@ -3882,7 +4009,6 @@ def _ordinal_number(self, n: int) -> str: class ThaiLocale(Locale): - names = ["th", "th-th"] past = "{0} ที่ผ่านมา" @@ -3891,16 +4017,18 @@ class ThaiLocale(Locale): timeframes = { "now": "ขณะนี้", "second": "วินาที", - "seconds": "{0} ไม่กี่วินาที", - "minute": "1 นาที", + "seconds": "{0} วินาที", + "minute": "นาที", "minutes": "{0} นาที", - "hour": "1 ชั่วโมง", + "hour": "ชั่วโมง", "hours": "{0} ชั่วโมง", - "day": "1 วัน", + "day": "วัน", "days": "{0} วัน", - "month": "1 เดือน", + "week": "สัปดาห์", + "weeks": "{0} สัปดาห์", + "month": "เดือน", "months": "{0} เดือน", - "year": "1 ปี", + "year": "ปี", "years": "{0} ปี", } @@ -3935,8 +4063,17 @@ class ThaiLocale(Locale): "ธ.ค.", ] - day_names = ["", "จันทร์", "อังคาร", "พุธ", "พฤหัสบดี", "ศุกร์", "เสาร์", "อาทิตย์"] - day_abbreviations = ["", "จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"] + day_names = [ + "", + "วันจันทร์", + "วันอังคาร", + "วันพุธ", + "วันพฤหัสบดี", + "วันศุกร์", + "วันเสาร์", + "วันอาทิตย์", + ] + day_abbreviations = ["", "จ.", "อ.", "พ.", "พฤ.", "ศ.", "ส.", "อา."] meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} @@ -3971,8 +4108,114 @@ def _format_relative( return relative_string -class BengaliLocale(Locale): +class LaotianLocale(Locale): + names = ["lo", "lo-la"] + + past = "{0} ກ່ອນຫນ້ານີ້" + future = "ໃນ {0}" + + timeframes = { + "now": "ດຽວນີ້", + "second": "ວິນາທີ", + "seconds": "{0} ວິນາທີ", + "minute": "ນາທີ", + "minutes": "{0} ນາທີ", + "hour": "ຊົ່ວໂມງ", + "hours": "{0} ຊົ່ວໂມງ", + "day": "ມື້", + "days": "{0} ມື້", + "week": "ອາທິດ", + "weeks": "{0} ອາທິດ", + "month": "ເດືອນ", + "months": "{0} ເດືອນ", + "year": "ປີ", + "years": "{0} ປີ", + } + + month_names = [ + "", + "ມັງກອນ", # mangkon + "ກຸມພາ", # kumpha + "ມີນາ", # mina + "ເມສາ", # mesa + "ພຶດສະພາ", # phudsapha + "ມິຖຸນາ", # mithuna + "ກໍລະກົດ", # kolakod + "ສິງຫາ", # singha + "ກັນຍາ", # knaia + "ຕຸລາ", # tula + "ພະຈິກ", # phachik + "ທັນວາ", # thanuaa + ] + month_abbreviations = [ + "", + "ມັງກອນ", + "ກຸມພາ", + "ມີນາ", + "ເມສາ", + "ພຶດສະພາ", + "ມິຖຸນາ", + "ກໍລະກົດ", + "ສິງຫາ", + "ກັນຍາ", + "ຕຸລາ", + "ພະຈິກ", + "ທັນວາ", + ] + + day_names = [ + "", + "ວັນຈັນ", # vanchan + "ວັນອັງຄານ", # vnoangkhan + "ວັນພຸດ", # vanphud + "ວັນພະຫັດ", # vanphahad + "ວັນ​ສຸກ", # vansuk + "ວັນເສົາ", # vansao + "ວັນອາທິດ", # vnoathid + ] + day_abbreviations = [ + "", + "ວັນຈັນ", + "ວັນອັງຄານ", + "ວັນພຸດ", + "ວັນພະຫັດ", + "ວັນ​ສຸກ", + "ວັນເສົາ", + "ວັນອາທິດ", + ] + + BE_OFFSET = 543 + + def year_full(self, year: int) -> str: + """Lao always use Buddhist Era (BE) which is CE + 543""" + year += self.BE_OFFSET + return f"{year:04d}" + + def year_abbreviation(self, year: int) -> str: + """Lao always use Buddhist Era (BE) which is CE + 543""" + year += self.BE_OFFSET + return f"{year:04d}"[2:] + def _format_relative( + self, + humanized: str, + timeframe: TimeFrameLiteral, + delta: Union[float, int], + ) -> str: + """Lao normally doesn't have any space between words""" + if timeframe == "now": + return humanized + + direction = self.past if delta < 0 else self.future + relative_string = direction.format(humanized) + + if timeframe == "seconds": + relative_string = relative_string.replace(" ", "") + + return relative_string + + +class BengaliLocale(Locale): names = ["bn", "bn-bd", "bn-in"] past = "{0} আগে" @@ -4050,10 +4293,10 @@ def _ordinal_number(self, n: int) -> str: return f"{n}র্থ" if n == 6: return f"{n}ষ্ঠ" + return "" class RomanshLocale(Locale): - names = ["rm", "rm-ch"] past = "avant {0}" @@ -4069,6 +4312,8 @@ class RomanshLocale(Locale): "hours": "{0} ura", "day": "in di", "days": "{0} dis", + "week": "in'emna", + "weeks": "{0} emnas", "month": "in mais", "months": "{0} mais", "year": "in onn", @@ -4260,7 +4505,6 @@ class SlovenianLocale(Locale): class IndonesianLocale(Locale): - names = ["id", "id-id"] past = "{0} yang lalu" @@ -4480,7 +4724,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class LatvianLocale(Locale): - names = ["lv", "lv-lv"] past = "pirms {0}" @@ -4561,7 +4804,6 @@ class LatvianLocale(Locale): class SwahiliLocale(Locale): - names = [ "sw", "sw-ke", @@ -4646,7 +4888,6 @@ class SwahiliLocale(Locale): class CroatianLocale(Locale): - names = ["hr", "hr-hr"] past = "prije {0}" @@ -4738,7 +4979,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class LatinLocale(Locale): - names = ["la", "la-va"] past = "ante {0}" @@ -4819,7 +5059,6 @@ class LatinLocale(Locale): class LithuanianLocale(Locale): - names = ["lt", "lt-lt"] past = "prieš {0}" @@ -4900,7 +5139,6 @@ class LithuanianLocale(Locale): class MalayLocale(Locale): - names = ["ms", "ms-my", "ms-bn"] past = "{0} yang lalu" @@ -4981,7 +5219,6 @@ class MalayLocale(Locale): class MalteseLocale(Locale): - names = ["mt", "mt-mt"] past = "{0} ilu" @@ -5073,7 +5310,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class SamiLocale(Locale): - names = ["se", "se-fi", "se-no", "se-se"] past = "{0} dassái" @@ -5153,7 +5389,6 @@ class SamiLocale(Locale): class OdiaLocale(Locale): - names = ["or", "or-in"] past = "{0} ପୂର୍ବେ" @@ -5244,7 +5479,6 @@ def _ordinal_number(self, n: int) -> str: class SerbianLocale(Locale): - names = ["sr", "sr-rs", "sr-sp"] past = "pre {0}" @@ -5336,14 +5570,13 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class LuxembourgishLocale(Locale): - names = ["lb", "lb-lu"] past = "virun {0}" future = "an {0}" and_word = "an" - timeframes = { + timeframes: ClassVar[Dict[TimeFrameLiteral, str]] = { "now": "just elo", "second": "enger Sekonn", "seconds": "{0} Sekonnen", @@ -5427,18 +5660,18 @@ def describe( delta: Union[int, float] = 0, only_distance: bool = False, ) -> str: - if not only_distance: return super().describe(timeframe, delta, only_distance) # Luxembourgish uses a different case without 'in' or 'ago' - humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) + humanized: str = self.timeframes_only_distance[timeframe].format( + trunc(abs(delta)) + ) return humanized class ZuluLocale(Locale): - names = ["zu", "zu-za"] past = "{0} edlule" @@ -5536,7 +5769,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class TamilLocale(Locale): - names = ["ta", "ta-in", "ta-lk"] past = "{0} நேரத்திற்கு முன்பு" @@ -5624,7 +5856,6 @@ def _ordinal_number(self, n: int) -> str: class AlbanianLocale(Locale): - names = ["sq", "sq-al"] past = "{0} më parë" @@ -5705,7 +5936,6 @@ class AlbanianLocale(Locale): class GeorgianLocale(Locale): - names = ["ka", "ka-ge"] past = "{0} წინ" # ts’in @@ -5790,7 +6020,6 @@ class GeorgianLocale(Locale): class SinhalaLocale(Locale): - names = ["si", "si-lk"] past = "{0}ට පෙර" @@ -5954,7 +6183,6 @@ def describe( class UrduLocale(Locale): - names = ["ur", "ur-pk"] past = "پہلے {0}" @@ -6035,7 +6263,6 @@ class UrduLocale(Locale): class KazakhLocale(Locale): - names = ["kk", "kk-kz"] past = "{0} бұрын" @@ -6271,3 +6498,157 @@ def describe( humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) return humanized + + +class ArmenianLocale(Locale): + names = ["hy", "hy-am"] + past = "{0} առաջ" + future = "{0}ից" + and_word = "Եվ" # Yev + + timeframes = { + "now": "հիմա", + "second": "վայրկյան", + "seconds": "{0} վայրկյան", + "minute": "րոպե", + "minutes": "{0} րոպե", + "hour": "ժամ", + "hours": "{0} ժամ", + "day": "օր", + "days": "{0} օր", + "month": "ամիս", + "months": "{0} ամիս", + "year": "տարին", + "years": "{0} տարին", + "week": "շաբաթ", + "weeks": "{0} շաբաթ", + } + + meridians = { + "am": "Ամ", + "pm": "պ.մ.", + "AM": "Ամ", + "PM": "պ.մ.", + } + + month_names = [ + "", + "հունվար", + "փետրվար", + "մարտ", + "ապրիլ", + "մայիս", + "հունիս", + "հուլիս", + "օգոստոս", + "սեպտեմբեր", + "հոկտեմբեր", + "նոյեմբեր", + "դեկտեմբեր", + ] + + month_abbreviations = [ + "", + "հունվար", + "փետրվար", + "մարտ", + "ապրիլ", + "մայիս", + "հունիս", + "հուլիս", + "օգոստոս", + "սեպտեմբեր", + "հոկտեմբեր", + "նոյեմբեր", + "դեկտեմբեր", + ] + + day_names = [ + "", + "երկուշաբթի", + "երեքշաբթի", + "չորեքշաբթի", + "հինգշաբթի", + "ուրբաթ", + "շաբաթ", + "կիրակի", + ] + + day_abbreviations = [ + "", + "երկ.", + "երեք.", + "չորեք.", + "հինգ.", + "ուրբ.", + "շաբ.", + "կիր.", + ] + + +class UzbekLocale(Locale): + names = ["uz", "uz-uz"] + past = "{0}dan avval" + future = "{0}dan keyin" + timeframes = { + "now": "hozir", + "second": "bir soniya", + "seconds": "{0} soniya", + "minute": "bir daqiqa", + "minutes": "{0} daqiqa", + "hour": "bir soat", + "hours": "{0} soat", + "day": "bir kun", + "days": "{0} kun", + "week": "bir hafta", + "weeks": "{0} hafta", + "month": "bir oy", + "months": "{0} oy", + "year": "bir yil", + "years": "{0} yil", + } + + month_names = [ + "", + "Yanvar", + "Fevral", + "Mart", + "Aprel", + "May", + "Iyun", + "Iyul", + "Avgust", + "Sentyabr", + "Oktyabr", + "Noyabr", + "Dekabr", + ] + + month_abbreviations = [ + "", + "Yan", + "Fev", + "Mar", + "Apr", + "May", + "Iyn", + "Iyl", + "Avg", + "Sen", + "Okt", + "Noy", + "Dek", + ] + + day_names = [ + "", + "Dushanba", + "Seshanba", + "Chorshanba", + "Payshanba", + "Juma", + "Shanba", + "Yakshanba", + ] + + day_abbreviations = ["", "Dush", "Sesh", "Chor", "Pay", "Jum", "Shan", "Yak"] diff --git a/arrow/parser.py b/arrow/parser.py index e95d78b0..fc3774b0 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -1,8 +1,7 @@ """Provides the :class:`Arrow ` class, a better way to parse datetime strings.""" import re -import sys -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from datetime import tzinfo as dt_tzinfo from functools import lru_cache from typing import ( @@ -11,30 +10,38 @@ Dict, Iterable, List, + Literal, Match, Optional, Pattern, SupportsFloat, SupportsInt, Tuple, + TypedDict, Union, cast, overload, ) -from dateutil import tz +try: + from zoneinfo import ZoneInfo, ZoneInfoNotFoundError +except ImportError: + from backports.zoneinfo import ZoneInfo, ZoneInfoNotFoundError # type: ignore[import-not-found, no-redef] from arrow import locales from arrow.constants import DEFAULT_LOCALE from arrow.util import next_weekday, normalize_timestamp -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Literal, TypedDict -else: - from typing import Literal, TypedDict # pragma: no cover - class ParserError(ValueError): + """ + A custom exception class for handling parsing errors in the parser. + + Notes: + This class inherits from the built-in `ValueError` class and is used to raise exceptions + when an error occurs during the parsing process. + """ + pass @@ -44,6 +51,14 @@ class ParserError(ValueError): # _parse_multiformat() and the appropriate error message was not # transmitted to the user. class ParserMatchError(ParserError): + """ + This class is a subclass of the ParserError class and is used to raise errors that occur during the matching process. + + Notes: + This class is part of the Arrow parser and is used to provide error handling when a parsing match fails. + + """ + pass @@ -85,6 +100,29 @@ class ParserMatchError(ParserError): class _Parts(TypedDict, total=False): + """ + A dictionary that represents different parts of a datetime. + + :class:`_Parts` is a TypedDict that represents various components of a date or time, + such as year, month, day, hour, minute, second, microsecond, timestamp, expanded_timestamp, tzinfo, + am_pm, day_of_week, and weekdate. + + :ivar year: The year, if present, as an integer. + :ivar month: The month, if present, as an integer. + :ivar day_of_year: The day of the year, if present, as an integer. + :ivar day: The day, if present, as an integer. + :ivar hour: The hour, if present, as an integer. + :ivar minute: The minute, if present, as an integer. + :ivar second: The second, if present, as an integer. + :ivar microsecond: The microsecond, if present, as an integer. + :ivar timestamp: The timestamp, if present, as a float. + :ivar expanded_timestamp: The expanded timestamp, if present, as an integer. + :ivar tzinfo: The timezone info, if present, as a :class:`dt_tzinfo` object. + :ivar am_pm: The AM/PM indicator, if present, as a string literal "am" or "pm". + :ivar day_of_week: The day of the week, if present, as an integer. + :ivar weekdate: The week date, if present, as a tuple of three integers or None. + """ + year: int month: int day_of_year: int @@ -102,6 +140,16 @@ class _Parts(TypedDict, total=False): class DateTimeParser: + """A :class:`DateTimeParser ` object + + Contains the regular expressions and functions to parse and split the input strings into tokens and eventually + produce a datetime that is used by :class:`Arrow ` internally. + + :param locale: the locale string + :param cache_size: the size of the LRU cache used for regular expressions. Defaults to 0. + + """ + _FORMAT_RE: ClassVar[Pattern[str]] = 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)" ) @@ -159,7 +207,15 @@ class DateTimeParser: _input_re_map: Dict[_FORMAT_TYPE, Pattern[str]] def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None: - + """ + Contains the regular expressions and functions to parse and split the input strings into tokens and eventually + produce a datetime that is used by :class:`Arrow ` internally. + + :param locale: the locale string + :type locale: str + :param cache_size: the size of the LRU cache used for regular expressions. Defaults to 0. + :type cache_size: int + """ self.locale = locales.get_locale(locale) self._input_re_map = self._BASE_INPUT_RE_MAP.copy() self._input_re_map.update( @@ -196,7 +252,23 @@ def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None: def parse_iso( self, datetime_string: str, normalize_whitespace: bool = False ) -> datetime: - + """ + Parses a datetime string using a ISO 8601-like format. + + :param datetime_string: The datetime string to parse. + :param normalize_whitespace: Whether to normalize whitespace in the datetime string (default is False). + :type datetime_string: str + :type normalize_whitespace: bool + :returns: The parsed datetime object. + :rtype: datetime + :raises ParserError: If the datetime string is not in a valid ISO 8601-like format. + + Usage:: + >>> import arrow.parser + >>> arrow.parser.DateTimeParser().parse_iso('2021-10-12T14:30:00') + datetime.datetime(2021, 10, 12, 14, 30) + + """ if normalize_whitespace: datetime_string = re.sub(r"\s+", " ", datetime_string.strip()) @@ -236,13 +308,14 @@ def parse_iso( ] if has_time: - if has_space_divider: date_string, time_string = datetime_string.split(" ", 1) else: date_string, time_string = datetime_string.split("T", 1) - time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE) + time_parts = re.split( + r"[\+\-Z]", time_string, maxsplit=1, flags=re.IGNORECASE + ) time_components: Optional[Match[str]] = self._TIME_RE.match(time_parts[0]) @@ -303,7 +376,27 @@ def parse( fmt: Union[List[str], str], normalize_whitespace: bool = False, ) -> datetime: + """ + Parses a datetime string using a specified format. + + :param datetime_string: The datetime string to parse. + :param fmt: The format string or list of format strings to use for parsing. + :param normalize_whitespace: Whether to normalize whitespace in the datetime string (default is False). + :type datetime_string: str + :type fmt: Union[List[str], str] + :type normalize_whitespace: bool + :returns: The parsed datetime object. + :rtype: datetime + :raises ParserMatchError: If the datetime string does not match the specified format. + Usage:: + + >>> import arrow.parser + >>> arrow.parser.DateTimeParser().parse('2021-10-12 14:30:00', 'YYYY-MM-DD HH:mm:ss') + datetime.datetime(2021, 10, 12, 14, 30) + + + """ if normalize_whitespace: datetime_string = re.sub(r"\s+", " ", datetime_string) @@ -341,12 +434,20 @@ def parse( f"Unable to find a match group for the specified token {token!r}." ) - self._parse_token(token, value, parts) # type: ignore + self._parse_token(token, value, parts) # type: ignore[arg-type] return self._build_datetime(parts) def _generate_pattern_re(self, fmt: str) -> Tuple[List[_FORMAT_TYPE], Pattern[str]]: - + """ + Generates a regular expression pattern from a format string. + + :param fmt: The format string to convert into a regular expression pattern. + :type fmt: str + :returns: A tuple containing a list of format tokens and the corresponding regular expression pattern. + :rtype: Tuple[List[_FORMAT_TYPE], Pattern[str]] + :raises ParserError: If an unrecognized token is encountered in the format string. + """ # fmt is a string of tokens like 'YYYY-MM-DD' # we construct a new string by replacing each # token by its pattern: @@ -453,8 +554,7 @@ def _parse_token( ], value: Union[str, bytes, SupportsInt, bytearray], parts: _Parts, - ) -> None: - ... # pragma: no cover + ) -> None: ... # pragma: no cover @overload def _parse_token( @@ -462,8 +562,7 @@ def _parse_token( token: Literal["X"], value: Union[str, bytes, SupportsFloat, bytearray], parts: _Parts, - ) -> None: - ... # pragma: no cover + ) -> None: ... # pragma: no cover @overload def _parse_token( @@ -471,8 +570,7 @@ def _parse_token( token: Literal["MMMM", "MMM", "dddd", "ddd", "S"], value: Union[str, bytes, bytearray], parts: _Parts, - ) -> None: - ... # pragma: no cover + ) -> None: ... # pragma: no cover @overload def _parse_token( @@ -480,8 +578,7 @@ def _parse_token( token: Literal["a", "A", "ZZZ", "ZZ", "Z"], value: Union[str, bytes], parts: _Parts, - ) -> None: - ... # pragma: no cover + ) -> None: ... # pragma: no cover @overload def _parse_token( @@ -489,8 +586,7 @@ def _parse_token( token: Literal["W"], value: Tuple[_WEEKDATE_ELEMENT, _WEEKDATE_ELEMENT, Optional[_WEEKDATE_ELEMENT]], parts: _Parts, - ) -> None: - ... # pragma: no cover + ) -> None: ... # pragma: no cover def _parse_token( self, @@ -498,7 +594,20 @@ def _parse_token( value: Any, parts: _Parts, ) -> None: + """ + Parse a token and its value, and update the `_Parts` dictionary with the parsed values. + + The function supports several tokens, including "YYYY", "YY", "MMMM", "MMM", "MM", "M", "DDDD", "DDD", "DD", "D", "Do", "dddd", "ddd", "HH", "H", "mm", "m", "ss", "s", "S", "X", "x", "ZZZ", "ZZ", "Z", "a", "A", and "W". Each token is matched and the corresponding value is parsed and added to the `_Parts` dictionary. + :param token: The token to parse. + :type token: Any + :param value: The value of the token. + :type value: Any + :param parts: A dictionary to update with the parsed values. + :type parts: _Parts + :raises ParserMatchError: If the hour token value is not between 0 and 12 inclusive for tokens "a" or "A". + + """ if token == "YYYY": parts["year"] = int(value) @@ -508,7 +617,7 @@ def _parse_token( elif token in ["MMMM", "MMM"]: # FIXME: month_number() is nullable - parts["month"] = self.locale.month_number(value.lower()) # type: ignore + parts["month"] = self.locale.month_number(value.lower()) # type: ignore[typeddict-item] elif token in ["MM", "M"]: parts["month"] = int(value) @@ -585,10 +694,17 @@ def _parse_token( @staticmethod def _build_datetime(parts: _Parts) -> datetime: + """ + Build a datetime object from a dictionary of date parts. + + :param parts: A dictionary containing the date parts extracted from a date string. + :type parts: dict + :return: A datetime object representing the date and time. + :rtype: datetime.datetime + """ weekdate = parts.get("weekdate") if weekdate is not None: - year, week = int(weekdate[0]), int(weekdate[1]) if weekdate[2] is not None: @@ -609,14 +725,14 @@ def _build_datetime(parts: _Parts) -> datetime: timestamp = parts.get("timestamp") if timestamp is not None: - return datetime.fromtimestamp(timestamp, tz=tz.tzutc()) + return datetime.fromtimestamp(timestamp, tz=timezone.utc) expanded_timestamp = parts.get("expanded_timestamp") if expanded_timestamp is not None: return datetime.fromtimestamp( normalize_timestamp(expanded_timestamp), - tz=tz.tzutc(), + tz=timezone.utc, ) day_of_year = parts.get("day_of_year") @@ -712,7 +828,21 @@ def _build_datetime(parts: _Parts) -> datetime: ) def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime: - + """ + Parse a date and time string using multiple formats. + + Tries to parse the provided string with each format in the given `formats` + iterable, returning the resulting `datetime` object if a match is found. If no + format matches the string, a `ParserError` is raised. + + :param string: The date and time string to parse. + :type string: str + :param formats: An iterable of date and time format strings to try, in order. + :type formats: Iterable[str] + :returns: The parsed date and time. + :rtype: datetime.datetime + :raises ParserError: If no format matches the input string. + """ _datetime: Optional[datetime] = None for fmt in formats: @@ -735,27 +865,52 @@ def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime: def _generate_choice_re( choices: Iterable[str], flags: Union[int, re.RegexFlag] = 0 ) -> Pattern[str]: + """ + Generate a regular expression pattern that matches a choice from an iterable. + + Takes an iterable of strings (`choices`) and returns a compiled regular expression + pattern that matches any of the choices. The pattern is created by joining the + choices with the '|' (OR) operator, which matches any of the enclosed patterns. + + :param choices: An iterable of strings to match. + :type choices: Iterable[str] + :param flags: Optional regular expression flags. Default is 0. + :type flags: Union[int, re.RegexFlag], optional + :returns: A compiled regular expression pattern that matches any of the choices. + :rtype: re.Pattern[str] + """ return re.compile(r"({})".format("|".join(choices)), flags=flags) class TzinfoParser: + """ + Parser for timezone information. + """ + _TZINFO_RE: ClassVar[Pattern[str]] = re.compile( - r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$" + r"^(?:\(UTC)*([\+\-])?(\d{2})(?:\:?(\d{2}))?" ) @classmethod def parse(cls, tzinfo_string: str) -> dt_tzinfo: - + """ + Parse a timezone string and return a datetime timezone object. + + :param tzinfo_string: The timezone string to parse. + :type tzinfo_string: str + :returns: The parsed datetime timezone object. + :rtype: datetime.timezone + :raises ParserError: If the timezone string cannot be parsed. + """ tzinfo: Optional[dt_tzinfo] = None if tzinfo_string == "local": - tzinfo = tz.tzlocal() + tzinfo = datetime.now().astimezone().tzinfo elif tzinfo_string in ["utc", "UTC", "Z"]: - tzinfo = tz.tzutc() + tzinfo = timezone.utc else: - iso_match = cls._TZINFO_RE.match(tzinfo_string) if iso_match: @@ -768,10 +923,13 @@ def parse(cls, tzinfo_string: str) -> dt_tzinfo: if sign == "-": seconds *= -1 - tzinfo = tz.tzoffset(None, seconds) + tzinfo = timezone(timedelta(seconds=seconds)) else: - tzinfo = tz.gettz(tzinfo_string) + try: + tzinfo = ZoneInfo(tzinfo_string) + except ZoneInfoNotFoundError: + tzinfo = None if tzinfo is None: raise ParserError(f"Could not parse timezone expression {tzinfo_string!r}.") diff --git a/arrow/util.py b/arrow/util.py index f3eaa21c..7171d92c 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,7 +1,7 @@ """Helpful functions used internally within arrow.""" import datetime -from typing import Any, Optional, cast +from typing import Any, Optional from dateutil.rrule import WEEKLY, rrule @@ -39,10 +39,7 @@ def next_weekday( """ if weekday < 0 or weekday > 6: raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).") - return cast( - datetime.datetime, - rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0], - ) + return rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0] def is_timestamp(value: Any) -> bool: diff --git a/docs/api-guide.rst b/docs/api-guide.rst new file mode 100644 index 00000000..3cf4d394 --- /dev/null +++ b/docs/api-guide.rst @@ -0,0 +1,28 @@ +*************************************** +API Guide +*************************************** + +:mod:`arrow.arrow` +===================== + +.. automodule:: arrow.arrow + :members: + +:mod:`arrow.factory` +===================== + +.. automodule:: arrow.factory + :members: + +:mod:`arrow.api` +===================== + +.. automodule:: arrow.api + :members: + +:mod:`arrow.locale` +===================== + +.. automodule:: arrow.locales + :members: + :undoc-members: diff --git a/docs/conf.py b/docs/conf.py index f106cb7f..aa6fb444 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,14 +13,18 @@ # -- Project information ----------------------------------------------------- project = "Arrow 🏹" -copyright = "2021, Chris Smith" +copyright = "2023, Chris Smith" author = "Chris Smith" release = about["__version__"] # -- General configuration --------------------------------------------------- -extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx_autodoc_typehints", + "sphinx_rtd_theme", +] templates_path = [] @@ -30,11 +34,11 @@ source_suffix = ".rst" pygments_style = "sphinx" -language = None +language = "en" # -- Options for HTML output ------------------------------------------------- -html_theme = "alabaster" +html_theme = "sphinx_rtd_theme" html_theme_path = [] html_static_path = [] @@ -42,21 +46,21 @@ html_show_sphinx = False html_show_copyright = True -# https://alabaster.readthedocs.io/en/latest/customization.html -html_theme_options = { - "description": "Arrow is a sensible and human-friendly approach to dates, times and timestamps.", +html_context = { + "display_github": True, "github_user": "arrow-py", "github_repo": "arrow", - "github_banner": True, - "show_related": False, - "show_powered_by": False, - "github_button": True, - "github_type": "star", - "github_count": "true", # must be a string + "github_version": "master/docs/", } -html_sidebars = { - "**": ["about.html", "localtoc.html", "relations.html", "searchbox.html"] +# https://sphinx-rtd-theme.readthedocs.io/en/stable/index.html +html_theme_options = { + "logo_only": False, + "prev_next_buttons_location": "both", + "style_nav_header_background": "grey", + # TOC options + "collapse_navigation": False, + "navigation_depth": 3, } # Generate PDFs with unicode characters diff --git a/docs/getting-started.rst b/docs/getting-started.rst new file mode 100644 index 00000000..6ebd3346 --- /dev/null +++ b/docs/getting-started.rst @@ -0,0 +1,9 @@ +*************************************** +Getting started +*************************************** + +Assuming you have Python already, follow the guidelines below to get started with Arrow. + +.. include:: ../README.rst + :start-after: Quick Start + :end-before: end-inclusion-marker-do-not-remove diff --git a/docs/guide.rst b/docs/guide.rst new file mode 100644 index 00000000..5e0e697a --- /dev/null +++ b/docs/guide.rst @@ -0,0 +1,565 @@ +*************************************** +User’s Guide +*************************************** + + +Creation +~~~~~~~~ + +Get 'now' easily: + +.. code-block:: python + + >>> arrow.utcnow() + + + >>> arrow.now() + + + >>> arrow.now('US/Pacific') + + +Create from timestamps (:code:`int` or :code:`float`): + +.. code-block:: python + + >>> arrow.get(1367900664) + + + >>> arrow.get(1367900664.152325) + + +Use a naive or timezone-aware datetime, or flexibly specify a timezone: + +.. code-block:: python + + >>> arrow.get(datetime.utcnow()) + + + >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') + + + >>> from dateutil import tz + >>> arrow.get(datetime(2013, 5, 5), tz.gettz('US/Pacific')) + + + >>> arrow.get(datetime.now(tz.gettz('US/Pacific'))) + + +Parse from a string: + +.. code-block:: python + + >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss') + + +Search a date in a string: + +.. code-block:: python + + >>> arrow.get('June was born in May 1980', 'MMMM YYYY') + + +Some ISO 8601 compliant strings are recognized and parsed without a format string: + + >>> arrow.get('2013-09-30T15:34:00.000-07:00') + + +Arrow objects can be instantiated directly too, with the same arguments as a datetime: + +.. code-block:: python + + >>> arrow.get(2013, 5, 5) + + + >>> arrow.Arrow(2013, 5, 5) + + +Properties +~~~~~~~~~~ + +Get a datetime or timestamp representation: + +.. code-block:: python + + >>> a = arrow.utcnow() + >>> a.datetime + datetime.datetime(2013, 5, 7, 4, 38, 15, 447644, tzinfo=tzutc()) + +Get a naive datetime, and tzinfo: + +.. code-block:: python + + >>> a.naive + datetime.datetime(2013, 5, 7, 4, 38, 15, 447644) + + >>> a.tzinfo + tzutc() + +Get any datetime value: + +.. code-block:: python + + >>> a.year + 2013 + +Call datetime functions that return properties: + +.. code-block:: python + + >>> a.date() + datetime.date(2013, 5, 7) + + >>> a.time() + datetime.time(4, 38, 15, 447644) + +Replace & Shift +~~~~~~~~~~~~~~~ + +Get a new :class:`Arrow ` object, with altered attributes, just as you would with a datetime: + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw + + + >>> arw.replace(hour=4, minute=40) + + +Or, get one with attributes shifted forward or backward: + +.. code-block:: python + + >>> arw.shift(weeks=+3) + + +Even replace the timezone without altering other attributes: + +.. code-block:: python + + >>> arw.replace(tzinfo='US/Pacific') + + +Move between the earlier and later moments of an ambiguous time: + +.. code-block:: python + + >>> paris_transition = arrow.Arrow(2019, 10, 27, 2, tzinfo="Europe/Paris", fold=0) + >>> paris_transition + + >>> paris_transition.ambiguous + True + >>> paris_transition.replace(fold=1) + + +Format +~~~~~~ + +For a list of formatting values, see :ref:`supported-tokens` + +.. code-block:: python + + >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') + '2013-05-07 05:23:16 -00:00' + +Convert +~~~~~~~ + +Convert from UTC to other timezones by name or tzinfo: + +.. code-block:: python + + >>> utc = arrow.utcnow() + >>> utc + + + >>> utc.to('US/Pacific') + + + >>> utc.to(tz.gettz('US/Pacific')) + + +Or using shorthand: + +.. code-block:: python + + >>> utc.to('local') + + + >>> utc.to('local').to('utc') + + + +Humanize +~~~~~~~~ + +Humanize relative to now: + +.. code-block:: python + + >>> past = arrow.utcnow().shift(hours=-1) + >>> past.humanize() + 'an hour ago' + +Or another Arrow, or datetime: + +.. code-block:: python + + >>> present = arrow.utcnow() + >>> future = present.shift(hours=2) + >>> future.humanize(present) + 'in 2 hours' + +Indicate time as relative or include only the distance + +.. code-block:: python + + >>> present = arrow.utcnow() + >>> future = present.shift(hours=2) + >>> future.humanize(present) + 'in 2 hours' + >>> future.humanize(present, only_distance=True) + '2 hours' + + +Indicate a specific time granularity (or multiple): + +.. code-block:: python + + >>> present = arrow.utcnow() + >>> future = present.shift(minutes=66) + >>> future.humanize(present, granularity="minute") + 'in 66 minutes' + >>> future.humanize(present, granularity=["hour", "minute"]) + 'in an hour and 6 minutes' + >>> present.humanize(future, granularity=["hour", "minute"]) + 'an hour and 6 minutes ago' + >>> future.humanize(present, only_distance=True, granularity=["hour", "minute"]) + 'an hour and 6 minutes' + +Support for a growing number of locales (see ``locales.py`` for supported languages): + +.. code-block:: python + + + >>> future = arrow.utcnow().shift(hours=1) + >>> future.humanize(a, locale='ru') + 'через 2 час(а,ов)' + +Dehumanize +~~~~~~~~~~ + +Take a human readable string and use it to shift into a past time: + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw + + >>> earlier = arw.dehumanize("2 days ago") + >>> earlier + + +Or use it to shift into a future time: + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw + + >>> later = arw.dehumanize("in a month") + >>> later + + +Support for a growing number of locales (see ``constants.py`` for supported languages): + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw + + >>> later = arw.dehumanize("एक माह बाद", locale="hi") + >>> later + + +Ranges & Spans +~~~~~~~~~~~~~~ + +Get the time span of any unit: + +.. code-block:: python + + >>> arrow.utcnow().span('hour') + (, ) + +Or just get the floor and ceiling: + +.. code-block:: python + + >>> arrow.utcnow().floor('hour') + + + >>> arrow.utcnow().ceil('hour') + + + >>> arrow.utcnow().floor('week', week_start=7) + + + >>> arrow.utcnow().ceil('week', week_start=7) + + +You can also get a range of time spans: + +.. code-block:: python + + >>> start = datetime(2013, 5, 5, 12, 30) + >>> end = datetime(2013, 5, 5, 17, 15) + >>> for r in arrow.Arrow.span_range('hour', start, end): + ... print(r) + ... + (, ) + (, ) + (, ) + (, ) + (, ) + +Or just iterate over a range of time: + +.. code-block:: python + + >>> start = datetime(2013, 5, 5, 12, 30) + >>> end = datetime(2013, 5, 5, 17, 15) + >>> for r in arrow.Arrow.range('hour', start, end): + ... print(repr(r)) + ... + + + + + + +.. toctree:: + :maxdepth: 2 + +Factories +~~~~~~~~~ + +Use factories to harness Arrow's module API for a custom Arrow-derived type. First, derive your type: + +.. code-block:: python + + >>> class CustomArrow(arrow.Arrow): + ... + ... def days_till_xmas(self): + ... + ... xmas = arrow.Arrow(self.year, 12, 25) + ... + ... if self > xmas: + ... xmas = xmas.shift(years=1) + ... + ... return (xmas - self).days + + +Then get and use a factory for it: + +.. code-block:: python + + >>> factory = arrow.ArrowFactory(CustomArrow) + >>> custom = factory.utcnow() + >>> custom + >>> + + >>> custom.days_till_xmas() + >>> 211 + +.. _supported-tokens: + +Supported Tokens +~~~~~~~~~~~~~~~~ + +Use the following tokens for parsing and formatting. Note that they are **not** the same as the tokens for `strptime `_: + ++--------------------------------+--------------+-------------------------------------------+ +| |Token |Output | ++================================+==============+===========================================+ +|**Year** |YYYY |2000, 2001, 2002 ... 2012, 2013 | ++--------------------------------+--------------+-------------------------------------------+ +| |YY |00, 01, 02 ... 12, 13 | ++--------------------------------+--------------+-------------------------------------------+ +|**Month** |MMMM |January, February, March ... [#t1]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |MMM |Jan, Feb, Mar ... [#t1]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |MM |01, 02, 03 ... 11, 12 | ++--------------------------------+--------------+-------------------------------------------+ +| |M |1, 2, 3 ... 11, 12 | ++--------------------------------+--------------+-------------------------------------------+ +|**Day of Year** |DDDD |001, 002, 003 ... 364, 365 | ++--------------------------------+--------------+-------------------------------------------+ +| |DDD |1, 2, 3 ... 364, 365 | ++--------------------------------+--------------+-------------------------------------------+ +|**Day of Month** |DD |01, 02, 03 ... 30, 31 | ++--------------------------------+--------------+-------------------------------------------+ +| |D |1, 2, 3 ... 30, 31 | ++--------------------------------+--------------+-------------------------------------------+ +| |Do |1st, 2nd, 3rd ... 30th, 31st | ++--------------------------------+--------------+-------------------------------------------+ +|**Day of Week** |dddd |Monday, Tuesday, Wednesday ... [#t2]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |ddd |Mon, Tue, Wed ... [#t2]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |d |1, 2, 3 ... 6, 7 | ++--------------------------------+--------------+-------------------------------------------+ +|**ISO week date** |W |2011-W05-4, 2019-W17 | ++--------------------------------+--------------+-------------------------------------------+ +|**Hour** |HH |00, 01, 02 ... 23, 24 | ++--------------------------------+--------------+-------------------------------------------+ +| |H |0, 1, 2 ... 23, 24 | ++--------------------------------+--------------+-------------------------------------------+ +| |hh |01, 02, 03 ... 11, 12 | ++--------------------------------+--------------+-------------------------------------------+ +| |h |1, 2, 3 ... 11, 12 | ++--------------------------------+--------------+-------------------------------------------+ +|**AM / PM** |A |AM, PM, am, pm [#t1]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |a |am, pm [#t1]_ | ++--------------------------------+--------------+-------------------------------------------+ +|**Minute** |mm |00, 01, 02 ... 58, 59 | ++--------------------------------+--------------+-------------------------------------------+ +| |m |0, 1, 2 ... 58, 59 | ++--------------------------------+--------------+-------------------------------------------+ +|**Second** |ss |00, 01, 02 ... 58, 59 | ++--------------------------------+--------------+-------------------------------------------+ +| |s |0, 1, 2 ... 58, 59 | ++--------------------------------+--------------+-------------------------------------------+ +|**Sub-second** |S... |0, 02, 003, 000006, 123123123123... [#t3]_ | ++--------------------------------+--------------+-------------------------------------------+ +|**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t4]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |ZZ |-07:00, -06:00 ... +06:00, +07:00, +08, Z | ++--------------------------------+--------------+-------------------------------------------+ +| |Z |-0700, -0600 ... +0600, +0700, +08, Z | ++--------------------------------+--------------+-------------------------------------------+ +|**Seconds Timestamp** |X |1381685817, 1381685817.915482 ... [#t5]_ | ++--------------------------------+--------------+-------------------------------------------+ +|**ms or µs Timestamp** |x |1569980330813, 1569980330813221 | ++--------------------------------+--------------+-------------------------------------------+ + +.. rubric:: Footnotes + +.. [#t1] localization support for parsing and formatting +.. [#t2] localization support only for formatting +.. [#t3] the result is truncated to microseconds, with `half-to-even rounding `_. +.. [#t4] timezone names from `tz database `_ provided via dateutil package, note that abbreviations such as MST, PDT, BRST are unlikely to parse due to ambiguity. Use the full IANA zone name instead (Asia/Shanghai, Europe/London, America/Chicago etc). +.. [#t5] this token cannot be used for parsing timestamps out of natural language strings due to compatibility reasons + +Built-in Formats +++++++++++++++++ + +There are several formatting standards that are provided as built-in tokens. + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw.format(arrow.FORMAT_ATOM) + '2020-05-27 10:30:35+00:00' + >>> arw.format(arrow.FORMAT_COOKIE) + 'Wednesday, 27-May-2020 10:30:35 UTC' + >>> arw.format(arrow.FORMAT_RSS) + 'Wed, 27 May 2020 10:30:35 +0000' + >>> arw.format(arrow.FORMAT_RFC822) + 'Wed, 27 May 20 10:30:35 +0000' + >>> arw.format(arrow.FORMAT_RFC850) + 'Wednesday, 27-May-20 10:30:35 UTC' + >>> arw.format(arrow.FORMAT_RFC1036) + 'Wed, 27 May 20 10:30:35 +0000' + >>> arw.format(arrow.FORMAT_RFC1123) + 'Wed, 27 May 2020 10:30:35 +0000' + >>> arw.format(arrow.FORMAT_RFC2822) + 'Wed, 27 May 2020 10:30:35 +0000' + >>> arw.format(arrow.FORMAT_RFC3339) + '2020-05-27 10:30:35+00:00' + >>> arw.format(arrow.FORMAT_W3C) + '2020-05-27 10:30:35+00:00' + +Escaping Formats +~~~~~~~~~~~~~~~~ + +Tokens, phrases, and regular expressions in a format string can be escaped when parsing and formatting by enclosing them within square brackets. + +Tokens & Phrases +++++++++++++++++ + +Any `token `_ or phrase can be escaped as follows: + +.. code-block:: python + + >>> fmt = "YYYY-MM-DD h [h] m" + >>> arw = arrow.get("2018-03-09 8 h 40", fmt) + + >>> arw.format(fmt) + '2018-03-09 8 h 40' + + >>> fmt = "YYYY-MM-DD h [hello] m" + >>> arw = arrow.get("2018-03-09 8 hello 40", fmt) + + >>> arw.format(fmt) + '2018-03-09 8 hello 40' + + >>> fmt = "YYYY-MM-DD h [hello world] m" + >>> arw = arrow.get("2018-03-09 8 hello world 40", fmt) + + >>> arw.format(fmt) + '2018-03-09 8 hello world 40' + +This can be useful for parsing dates in different locales such as French, in which it is common to format time strings as "8 h 40" rather than "8:40". + +Regular Expressions ++++++++++++++++++++ + +You can also escape regular expressions by enclosing them within square brackets. In the following example, we are using the regular expression :code:`\s+` to match any number of whitespace characters that separate the tokens. This is useful if you do not know the number of spaces between tokens ahead of time (e.g. in log files). + +.. code-block:: python + + >>> fmt = r"ddd[\s+]MMM[\s+]DD[\s+]HH:mm:ss[\s+]YYYY" + >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) + + + >>> arrow.get("Mon \tSep 08 16:41:45 2014", fmt) + + + >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) + + +Punctuation +~~~~~~~~~~~ + +Date and time formats may be fenced on either side by one punctuation character from the following list: ``, . ; : ? ! " \` ' [ ] { } ( ) < >`` + +.. code-block:: python + + >>> arrow.get("Cool date: 2019-10-31T09:12:45.123456+04:30.", "YYYY-MM-DDTHH:mm:ss.SZZ") + + + >>> arrow.get("Tomorrow (2019-10-31) is Halloween!", "YYYY-MM-DD") + + + >>> arrow.get("Halloween is on 2019.10.31.", "YYYY.MM.DD") + + + >>> arrow.get("It's Halloween tomorrow (2019-10-31)!", "YYYY-MM-DD") + # Raises exception because there are multiple punctuation marks following the date + +Redundant Whitespace +~~~~~~~~~~~~~~~~~~~~ + +Redundant whitespace characters (spaces, tabs, and newlines) can be normalized automatically by passing in the ``normalize_whitespace`` flag to ``arrow.get``: + +.. code-block:: python + + >>> arrow.get('\t \n 2013-05-05T12:30:45.123456 \t \n', normalize_whitespace=True) + + + >>> arrow.get('2013-05-05 T \n 12:30:45\t123456', 'YYYY-MM-DD T HH:mm:ss S', normalize_whitespace=True) + diff --git a/docs/index.rst b/docs/index.rst index d4f9ec2a..0ad2fdba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,593 +3,31 @@ Arrow: Better dates & times for Python Release v\ |release| (`Installation`_) (`Changelog `_) +`Go to repository `_ + .. include:: ../README.rst :start-after: start-inclusion-marker-do-not-remove :end-before: end-inclusion-marker-do-not-remove -User's Guide ------------- - -Creation -~~~~~~~~ - -Get 'now' easily: - -.. code-block:: python - - >>> arrow.utcnow() - - - >>> arrow.now() - - - >>> arrow.now('US/Pacific') - - -Create from timestamps (:code:`int` or :code:`float`): - -.. code-block:: python - - >>> arrow.get(1367900664) - - - >>> arrow.get(1367900664.152325) - - -Use a naive or timezone-aware datetime, or flexibly specify a timezone: - -.. code-block:: python - - >>> arrow.get(datetime.utcnow()) - - - >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') - - - >>> from dateutil import tz - >>> arrow.get(datetime(2013, 5, 5), tz.gettz('US/Pacific')) - - - >>> arrow.get(datetime.now(tz.gettz('US/Pacific'))) - - -Parse from a string: - -.. code-block:: python - - >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss') - - -Search a date in a string: - -.. code-block:: python - - >>> arrow.get('June was born in May 1980', 'MMMM YYYY') - - -Some ISO 8601 compliant strings are recognized and parsed without a format string: - - >>> arrow.get('2013-09-30T15:34:00.000-07:00') - - -Arrow objects can be instantiated directly too, with the same arguments as a datetime: - -.. code-block:: python - - >>> arrow.get(2013, 5, 5) - - - >>> arrow.Arrow(2013, 5, 5) - - -Properties -~~~~~~~~~~ - -Get a datetime or timestamp representation: - -.. code-block:: python - - >>> a = arrow.utcnow() - >>> a.datetime - datetime.datetime(2013, 5, 7, 4, 38, 15, 447644, tzinfo=tzutc()) - -Get a naive datetime, and tzinfo: - -.. code-block:: python - - >>> a.naive - datetime.datetime(2013, 5, 7, 4, 38, 15, 447644) - - >>> a.tzinfo - tzutc() - -Get any datetime value: - -.. code-block:: python - - >>> a.year - 2013 - -Call datetime functions that return properties: - -.. code-block:: python - - >>> a.date() - datetime.date(2013, 5, 7) - - >>> a.time() - datetime.time(4, 38, 15, 447644) - -Replace & Shift -~~~~~~~~~~~~~~~ - -Get a new :class:`Arrow ` object, with altered attributes, just as you would with a datetime: - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw - - - >>> arw.replace(hour=4, minute=40) - - -Or, get one with attributes shifted forward or backward: - -.. code-block:: python - - >>> arw.shift(weeks=+3) - - -Even replace the timezone without altering other attributes: - -.. code-block:: python - - >>> arw.replace(tzinfo='US/Pacific') - - -Move between the earlier and later moments of an ambiguous time: - -.. code-block:: python - - >>> paris_transition = arrow.Arrow(2019, 10, 27, 2, tzinfo="Europe/Paris", fold=0) - >>> paris_transition - - >>> paris_transition.ambiguous - True - >>> paris_transition.replace(fold=1) - - -Format -~~~~~~ - -.. code-block:: python - - >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') - '2013-05-07 05:23:16 -00:00' - -Convert -~~~~~~~ - -Convert from UTC to other timezones by name or tzinfo: - -.. code-block:: python - - >>> utc = arrow.utcnow() - >>> utc - - - >>> utc.to('US/Pacific') - - - >>> utc.to(tz.gettz('US/Pacific')) - - -Or using shorthand: - -.. code-block:: python - - >>> utc.to('local') - - - >>> utc.to('local').to('utc') - - - -Humanize -~~~~~~~~ - -Humanize relative to now: - -.. code-block:: python - - >>> past = arrow.utcnow().shift(hours=-1) - >>> past.humanize() - 'an hour ago' - -Or another Arrow, or datetime: - -.. code-block:: python - - >>> present = arrow.utcnow() - >>> future = present.shift(hours=2) - >>> future.humanize(present) - 'in 2 hours' - -Indicate time as relative or include only the distance - -.. code-block:: python - - >>> present = arrow.utcnow() - >>> future = present.shift(hours=2) - >>> future.humanize(present) - 'in 2 hours' - >>> future.humanize(present, only_distance=True) - '2 hours' - - -Indicate a specific time granularity (or multiple): - -.. code-block:: python - - >>> present = arrow.utcnow() - >>> future = present.shift(minutes=66) - >>> future.humanize(present, granularity="minute") - 'in 66 minutes' - >>> future.humanize(present, granularity=["hour", "minute"]) - 'in an hour and 6 minutes' - >>> present.humanize(future, granularity=["hour", "minute"]) - 'an hour and 6 minutes ago' - >>> future.humanize(present, only_distance=True, granularity=["hour", "minute"]) - 'an hour and 6 minutes' - -Support for a growing number of locales (see ``locales.py`` for supported languages): - -.. code-block:: python - - - >>> future = arrow.utcnow().shift(hours=1) - >>> future.humanize(a, locale='ru') - 'через 2 час(а,ов)' - -Dehumanize -~~~~~~~~~~ - -Take a human readable string and use it to shift into a past time: - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw - - >>> earlier = arw.dehumanize("2 days ago") - >>> earlier - - -Or use it to shift into a future time: - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw - - >>> later = arw.dehumanize("in a month") - >>> later - - -Support for a growing number of locales (see ``constants.py`` for supported languages): - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw - - >>> later = arw.dehumanize("एक माह बाद", locale="hi") - >>> later - - -Ranges & Spans -~~~~~~~~~~~~~~ - -Get the time span of any unit: - -.. code-block:: python - - >>> arrow.utcnow().span('hour') - (, ) - -Or just get the floor and ceiling: - -.. code-block:: python - - >>> arrow.utcnow().floor('hour') - - - >>> arrow.utcnow().ceil('hour') - - -You can also get a range of time spans: - -.. code-block:: python - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.span_range('hour', start, end): - ... print(r) - ... - (, ) - (, ) - (, ) - (, ) - (, ) - -Or just iterate over a range of time: - -.. code-block:: python - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.range('hour', start, end): - ... print(repr(r)) - ... - - - - - - .. toctree:: - :maxdepth: 2 - -Factories -~~~~~~~~~ - -Use factories to harness Arrow's module API for a custom Arrow-derived type. First, derive your type: - -.. code-block:: python - - >>> class CustomArrow(arrow.Arrow): - ... - ... def days_till_xmas(self): - ... - ... xmas = arrow.Arrow(self.year, 12, 25) - ... - ... if self > xmas: - ... xmas = xmas.shift(years=1) - ... - ... return (xmas - self).days - - -Then get and use a factory for it: - -.. code-block:: python - - >>> factory = arrow.ArrowFactory(CustomArrow) - >>> custom = factory.utcnow() - >>> custom - >>> - - >>> custom.days_till_xmas() - >>> 211 - -Supported Tokens -~~~~~~~~~~~~~~~~ + :maxdepth: 2 -Use the following tokens for parsing and formatting. Note that they are **not** the same as the tokens for `strptime `_: + getting-started -+--------------------------------+--------------+-------------------------------------------+ -| |Token |Output | -+================================+==============+===========================================+ -|**Year** |YYYY |2000, 2001, 2002 ... 2012, 2013 | -+--------------------------------+--------------+-------------------------------------------+ -| |YY |00, 01, 02 ... 12, 13 | -+--------------------------------+--------------+-------------------------------------------+ -|**Month** |MMMM |January, February, March ... [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |MMM |Jan, Feb, Mar ... [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |MM |01, 02, 03 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -| |M |1, 2, 3 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -|**Day of Year** |DDDD |001, 002, 003 ... 364, 365 | -+--------------------------------+--------------+-------------------------------------------+ -| |DDD |1, 2, 3 ... 364, 365 | -+--------------------------------+--------------+-------------------------------------------+ -|**Day of Month** |DD |01, 02, 03 ... 30, 31 | -+--------------------------------+--------------+-------------------------------------------+ -| |D |1, 2, 3 ... 30, 31 | -+--------------------------------+--------------+-------------------------------------------+ -| |Do |1st, 2nd, 3rd ... 30th, 31st | -+--------------------------------+--------------+-------------------------------------------+ -|**Day of Week** |dddd |Monday, Tuesday, Wednesday ... [#t2]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |ddd |Mon, Tue, Wed ... [#t2]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |d |1, 2, 3 ... 6, 7 | -+--------------------------------+--------------+-------------------------------------------+ -|**ISO week date** |W |2011-W05-4, 2019-W17 | -+--------------------------------+--------------+-------------------------------------------+ -|**Hour** |HH |00, 01, 02 ... 23, 24 | -+--------------------------------+--------------+-------------------------------------------+ -| |H |0, 1, 2 ... 23, 24 | -+--------------------------------+--------------+-------------------------------------------+ -| |hh |01, 02, 03 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -| |h |1, 2, 3 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -|**AM / PM** |A |AM, PM, am, pm [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |a |am, pm [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -|**Minute** |mm |00, 01, 02 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -| |m |0, 1, 2 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -|**Second** |ss |00, 01, 02 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -| |s |0, 1, 2 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -|**Sub-second** |S... |0, 02, 003, 000006, 123123123123... [#t3]_ | -+--------------------------------+--------------+-------------------------------------------+ -|**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t4]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |ZZ |-07:00, -06:00 ... +06:00, +07:00, +08, Z | -+--------------------------------+--------------+-------------------------------------------+ -| |Z |-0700, -0600 ... +0600, +0700, +08, Z | -+--------------------------------+--------------+-------------------------------------------+ -|**Seconds Timestamp** |X |1381685817, 1381685817.915482 ... [#t5]_ | -+--------------------------------+--------------+-------------------------------------------+ -|**ms or µs Timestamp** |x |1569980330813, 1569980330813221 | -+--------------------------------+--------------+-------------------------------------------+ - -.. rubric:: Footnotes - -.. [#t1] localization support for parsing and formatting -.. [#t2] localization support only for formatting -.. [#t3] the result is truncated to microseconds, with `half-to-even rounding `_. -.. [#t4] timezone names from `tz database `_ provided via dateutil package, note that abbreviations such as MST, PDT, BRST are unlikely to parse due to ambiguity. Use the full IANA zone name instead (Asia/Shanghai, Europe/London, America/Chicago etc). -.. [#t5] this token cannot be used for parsing timestamps out of natural language strings due to compatibility reasons - -Built-in Formats -++++++++++++++++ - -There are several formatting standards that are provided as built-in tokens. - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw.format(arrow.FORMAT_ATOM) - '2020-05-27 10:30:35+00:00' - >>> arw.format(arrow.FORMAT_COOKIE) - 'Wednesday, 27-May-2020 10:30:35 UTC' - >>> arw.format(arrow.FORMAT_RSS) - 'Wed, 27 May 2020 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC822) - 'Wed, 27 May 20 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC850) - 'Wednesday, 27-May-20 10:30:35 UTC' - >>> arw.format(arrow.FORMAT_RFC1036) - 'Wed, 27 May 20 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC1123) - 'Wed, 27 May 2020 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC2822) - 'Wed, 27 May 2020 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC3339) - '2020-05-27 10:30:35+00:00' - >>> arw.format(arrow.FORMAT_W3C) - '2020-05-27 10:30:35+00:00' - -Escaping Formats -~~~~~~~~~~~~~~~~ - -Tokens, phrases, and regular expressions in a format string can be escaped when parsing and formatting by enclosing them within square brackets. - -Tokens & Phrases -++++++++++++++++ - -Any `token `_ or phrase can be escaped as follows: - -.. code-block:: python - - >>> fmt = "YYYY-MM-DD h [h] m" - >>> arw = arrow.get("2018-03-09 8 h 40", fmt) - - >>> arw.format(fmt) - '2018-03-09 8 h 40' - - >>> fmt = "YYYY-MM-DD h [hello] m" - >>> arw = arrow.get("2018-03-09 8 hello 40", fmt) - - >>> arw.format(fmt) - '2018-03-09 8 hello 40' - - >>> fmt = "YYYY-MM-DD h [hello world] m" - >>> arw = arrow.get("2018-03-09 8 hello world 40", fmt) - - >>> arw.format(fmt) - '2018-03-09 8 hello world 40' - -This can be useful for parsing dates in different locales such as French, in which it is common to format time strings as "8 h 40" rather than "8:40". - -Regular Expressions -+++++++++++++++++++ - -You can also escape regular expressions by enclosing them within square brackets. In the following example, we are using the regular expression :code:`\s+` to match any number of whitespace characters that separate the tokens. This is useful if you do not know the number of spaces between tokens ahead of time (e.g. in log files). - -.. code-block:: python - - >>> fmt = r"ddd[\s+]MMM[\s+]DD[\s+]HH:mm:ss[\s+]YYYY" - >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) - - - >>> arrow.get("Mon \tSep 08 16:41:45 2014", fmt) - - - >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) - - -Punctuation -~~~~~~~~~~~ - -Date and time formats may be fenced on either side by one punctuation character from the following list: ``, . ; : ? ! " \` ' [ ] { } ( ) < >`` - -.. code-block:: python - - >>> arrow.get("Cool date: 2019-10-31T09:12:45.123456+04:30.", "YYYY-MM-DDTHH:mm:ss.SZZ") - - - >>> arrow.get("Tomorrow (2019-10-31) is Halloween!", "YYYY-MM-DD") - - - >>> arrow.get("Halloween is on 2019.10.31.", "YYYY.MM.DD") - - - >>> arrow.get("It's Halloween tomorrow (2019-10-31)!", "YYYY-MM-DD") - # Raises exception because there are multiple punctuation marks following the date - -Redundant Whitespace -~~~~~~~~~~~~~~~~~~~~ - -Redundant whitespace characters (spaces, tabs, and newlines) can be normalized automatically by passing in the ``normalize_whitespace`` flag to ``arrow.get``: - -.. code-block:: python - - >>> arrow.get('\t \n 2013-05-05T12:30:45.123456 \t \n', normalize_whitespace=True) - - - >>> arrow.get('2013-05-05 T \n 12:30:45\t123456', 'YYYY-MM-DD T HH:mm:ss S', normalize_whitespace=True) - - -API Guide ---------- - -arrow.arrow -~~~~~~~~~~~ - -.. automodule:: arrow.arrow - :members: - -arrow.factory -~~~~~~~~~~~~~ +--------------- -.. automodule:: arrow.factory - :members: +.. toctree:: + :maxdepth: 2 -arrow.api -~~~~~~~~~ + guide -.. automodule:: arrow.api - :members: +--------------- -arrow.locale -~~~~~~~~~~~~ +.. toctree:: + :maxdepth: 2 -.. automodule:: arrow.locales - :members: - :undoc-members: + api-guide -Release History --------------- .. toctree:: diff --git a/docs/releases.rst b/docs/releases.rst index 22e1e59c..ed21b487 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -1,3 +1,7 @@ +*************************************** +Release History +*************************************** + .. _releases: .. include:: ../CHANGELOG.rst diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..d7d176a7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,69 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "arrow" +authors = [{name = "Chris Smith", email = "crsmithdev@gmail.com"}] +readme = "README.rst" +license = {file = "LICENSE"} +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: Apache Software License", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", +] +dependencies = [ + "python-dateutil>=2.7.0", + "backports.zoneinfo==0.2.1;python_version<'3.9'", + "tzdata;python_version>='3.9'", +] +requires-python = ">=3.8" +description = "Better dates & times for Python" +keywords = [ + "arrow", + "date", + "time", + "datetime", + "timestamp", + "timezone", + "humanize", +] +dynamic = ["version"] + +[project.optional-dependencies] +test = [ + "dateparser==1.*", + "pre-commit", + "pytest", + "pytest-cov", + "pytest-mock", + "pytz==2025.2", + "simplejson==3.*", +] +doc = [ + "doc8", + "sphinx>=7.0.0", + "sphinx-autobuild", + "sphinx-autodoc-typehints", + "sphinx_rtd_theme>=1.3.0", +] + +[project.urls] +Documentation = "https://arrow.readthedocs.io" +Source = "https://github.com/arrow-py/arrow" +Issues = "https://github.com/arrow-py/arrow/issues" + +[tool.flit.module] +name = "arrow" diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt index de59f1a3..35ca4ded 100644 --- a/requirements/requirements-docs.txt +++ b/requirements/requirements-docs.txt @@ -1,5 +1,6 @@ -r requirements.txt doc8 -sphinx +sphinx>=7.0.0 sphinx-autobuild sphinx-autodoc-typehints +sphinx_rtd_theme>=1.3.0 diff --git a/requirements/requirements-tests.txt b/requirements/requirements-tests.txt index 7e9fbe3f..d9f660e3 100644 --- a/requirements/requirements-tests.txt +++ b/requirements/requirements-tests.txt @@ -4,7 +4,6 @@ pre-commit pytest pytest-cov pytest-mock -python-dateutil>=2.7.0 -pytz==2021.1 +pytz==2025.2 simplejson==3.* -typing_extensions; python_version < '3.8' +types-python-dateutil>=2.8.10 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index bcdff0e8..fa333102 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,2 +1,3 @@ +backports.zoneinfo==0.2.1;python_version<'3.9' python-dateutil>=2.7.0 -typing_extensions; python_version < '3.8' +tzdata;python_version>='3.9' diff --git a/setup.cfg b/setup.cfg index 3add2419..916477e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,8 @@ [mypy] -python_version = 3.6 +python_version = 3.11 + +show_error_codes = True +pretty = True allow_any_expr = True allow_any_decorated = True diff --git a/setup.py b/setup.py deleted file mode 100644 index 350a5a0f..00000000 --- a/setup.py +++ /dev/null @@ -1,48 +0,0 @@ -# mypy: ignore-errors -from pathlib import Path - -from setuptools import setup - -readme = Path("README.rst").read_text(encoding="utf-8") -version = Path("arrow/_version.py").read_text(encoding="utf-8") -about = {} -exec(version, about) - -setup( - name="arrow", - version=about["__version__"], - description="Better dates & times for Python", - long_description=readme, - long_description_content_type="text/x-rst", - url="https://arrow.readthedocs.io", - author="Chris Smith", - author_email="crsmithdev@gmail.com", - license="Apache 2.0", - packages=["arrow"], - package_data={"arrow": ["py.typed"]}, - zip_safe=False, - python_requires=">=3.6", - install_requires=[ - "python-dateutil>=2.7.0", - "typing_extensions; python_version<'3.8'", - ], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Topic :: Software Development :: Libraries :: Python Modules", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - ], - keywords="arrow date time datetime timestamp timezone humanize", - project_urls={ - "Repository": "https://github.com/arrow-py/arrow", - "Bug Reports": "https://github.com/arrow-py/arrow/issues", - "Documentation": "https://arrow.readthedocs.io", - }, -) diff --git a/tests/conftest.py b/tests/conftest.py index 5d5b9980..b6127073 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,11 @@ from datetime import datetime import pytest -from dateutil import tz as dateutil_tz + +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo from arrow import arrow, factory, formatter, locales, parser @@ -32,7 +36,7 @@ def time_2013_02_15(request): @pytest.fixture(scope="class") def time_1975_12_25(request): request.cls.datetime = datetime( - 1975, 12, 25, 14, 15, 16, tzinfo=dateutil_tz.gettz("America/New_York") + 1975, 12, 25, 14, 15, 16, tzinfo=ZoneInfo("America/New_York") ) request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 2e2ffe91..b595e4e2 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1,7 +1,12 @@ +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo + import pickle import sys import time -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from typing import List import dateutil @@ -18,7 +23,6 @@ class TestTestArrowInit: def test_init_bad_input(self): - with pytest.raises(TypeError): arrow.Arrow(2013) @@ -29,7 +33,6 @@ def test_init_bad_input(self): arrow.Arrow(2013, 2, 2, 12, 30, 45, 9999999) def test_init(self): - result = arrow.Arrow(2013, 2, 2) self.expected = datetime(2013, 2, 2, tzinfo=tz.tzutc()) assert result._datetime == self.expected @@ -51,21 +54,40 @@ def test_init(self): assert result._datetime == self.expected result = arrow.Arrow( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") ) self.expected = datetime( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") ) assert result._datetime == self.expected # regression tests for issue #626 def test_init_pytz_timezone(self): - result = arrow.Arrow( 2013, 2, 2, 12, 30, 45, 999999, tzinfo=pytz.timezone("Europe/Paris") ) self.expected = datetime( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") + ) + assert result._datetime == self.expected + assert_datetime_equality(result._datetime, self.expected, 1) + + def test_init_zoneinfo_timezone(self): + result = arrow.Arrow( + 2024, 7, 10, 18, 55, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") + ) + self.expected = datetime( + 2024, 7, 10, 18, 55, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") + ) + assert result._datetime == self.expected + assert_datetime_equality(result._datetime, self.expected, 1) + + def test_init_dateutil_timezone(self): + result = arrow.Arrow( + 2024, 7, 10, 18, 55, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + ) + self.expected = datetime( + 2024, 7, 10, 18, 55, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") ) assert result._datetime == self.expected assert_datetime_equality(result._datetime, self.expected, 1) @@ -84,61 +106,52 @@ def test_init_with_fold(self): class TestTestArrowFactory: def test_now(self): - result = arrow.Arrow.now() - assert_datetime_equality( - result._datetime, datetime.now().replace(tzinfo=tz.tzlocal()) - ) + assert_datetime_equality(result._datetime, datetime.now().astimezone()) def test_utcnow(self): - result = arrow.Arrow.utcnow() assert_datetime_equality( - result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc()) + result._datetime, datetime.now(timezone.utc).replace(tzinfo=timezone.utc) ) assert result.fold == 0 def test_fromtimestamp(self): - timestamp = time.time() result = arrow.Arrow.fromtimestamp(timestamp) - assert_datetime_equality( - result._datetime, datetime.now().replace(tzinfo=tz.tzlocal()) - ) + assert_datetime_equality(result._datetime, datetime.now().astimezone()) - result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.gettz("Europe/Paris")) + result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=ZoneInfo("Europe/Paris")) assert_datetime_equality( result._datetime, - datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), + datetime.fromtimestamp(timestamp, ZoneInfo("Europe/Paris")), ) result = arrow.Arrow.fromtimestamp(timestamp, tzinfo="Europe/Paris") assert_datetime_equality( result._datetime, - datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), + datetime.fromtimestamp(timestamp, ZoneInfo("Europe/Paris")), ) with pytest.raises(ValueError): arrow.Arrow.fromtimestamp("invalid timestamp") def test_utcfromtimestamp(self): - timestamp = time.time() result = arrow.Arrow.utcfromtimestamp(timestamp) assert_datetime_equality( - result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc()) + result._datetime, datetime.now(timezone.utc).replace(tzinfo=timezone.utc) ) with pytest.raises(ValueError): arrow.Arrow.utcfromtimestamp("invalid timestamp") def test_fromdatetime(self): - dt = datetime(2013, 2, 3, 12, 30, 45, 1) result = arrow.Arrow.fromdatetime(dt) @@ -146,45 +159,40 @@ def test_fromdatetime(self): assert result._datetime == dt.replace(tzinfo=tz.tzutc()) def test_fromdatetime_dt_tzinfo(self): - - dt = datetime(2013, 2, 3, 12, 30, 45, 1, tzinfo=tz.gettz("US/Pacific")) + dt = datetime(2013, 2, 3, 12, 30, 45, 1, tzinfo=ZoneInfo("US/Pacific")) result = arrow.Arrow.fromdatetime(dt) - assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == dt.replace(tzinfo=ZoneInfo("US/Pacific")) def test_fromdatetime_tzinfo_arg(self): - dt = datetime(2013, 2, 3, 12, 30, 45, 1) - result = arrow.Arrow.fromdatetime(dt, tz.gettz("US/Pacific")) + result = arrow.Arrow.fromdatetime(dt, ZoneInfo("US/Pacific")) - assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == dt.replace(tzinfo=ZoneInfo("US/Pacific")) def test_fromdate(self): - dt = date(2013, 2, 3) - result = arrow.Arrow.fromdate(dt, tz.gettz("US/Pacific")) + result = arrow.Arrow.fromdate(dt, ZoneInfo("US/Pacific")) - assert result._datetime == datetime(2013, 2, 3, tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == datetime(2013, 2, 3, tzinfo=ZoneInfo("US/Pacific")) def test_strptime(self): - formatted = datetime(2013, 2, 3, 12, 30, 45).strftime("%Y-%m-%d %H:%M:%S") result = arrow.Arrow.strptime(formatted, "%Y-%m-%d %H:%M:%S") assert result._datetime == datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.tzutc()) result = arrow.Arrow.strptime( - formatted, "%Y-%m-%d %H:%M:%S", tzinfo=tz.gettz("Europe/Paris") + formatted, "%Y-%m-%d %H:%M:%S", tzinfo=ZoneInfo("Europe/Paris") ) assert result._datetime == datetime( - 2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris") + 2013, 2, 3, 12, 30, 45, tzinfo=ZoneInfo("Europe/Paris") ) def test_fromordinal(self): - timestamp = 1607066909.937968 with pytest.raises(TypeError): arrow.Arrow.fromordinal(timestamp) @@ -205,43 +213,36 @@ def test_fromordinal(self): @pytest.mark.usefixtures("time_2013_02_03") class TestTestArrowRepresentation: def test_repr(self): - result = self.arrow.__repr__() assert result == f"" def test_str(self): - result = self.arrow.__str__() assert result == self.arrow._datetime.isoformat() def test_hash(self): - result = self.arrow.__hash__() assert result == self.arrow._datetime.__hash__() def test_format(self): - result = f"{self.arrow:YYYY-MM-DD}" assert result == "2013-02-03" def test_bare_format(self): - result = self.arrow.format() assert result == "2013-02-03 12:30:45+00:00" def test_format_no_format_string(self): - result = f"{self.arrow}" assert result == str(self.arrow) def test_clone(self): - result = self.arrow.clone() assert result is not self.arrow @@ -251,12 +252,10 @@ def test_clone(self): @pytest.mark.usefixtures("time_2013_01_01") class TestArrowAttribute: def test_getattr_base(self): - with pytest.raises(AttributeError): self.arrow.prop def test_getattr_week(self): - assert self.arrow.week == 1 def test_getattr_quarter(self): @@ -281,31 +280,24 @@ def test_getattr_quarter(self): assert q4.quarter == 4 def test_getattr_dt_value(self): - assert self.arrow.year == 2013 def test_tzinfo(self): - - assert self.arrow.tzinfo == tz.tzutc() + assert self.arrow.tzinfo == timezone.utc def test_naive(self): - assert self.arrow.naive == self.arrow._datetime.replace(tzinfo=None) def test_timestamp(self): - assert self.arrow.timestamp() == self.arrow._datetime.timestamp() def test_int_timestamp(self): - assert self.arrow.int_timestamp == int(self.arrow._datetime.timestamp()) def test_float_timestamp(self): - assert self.arrow.float_timestamp == self.arrow._datetime.timestamp() def test_getattr_fold(self): - # UTC is always unambiguous assert self.now.fold == 0 @@ -318,7 +310,6 @@ def test_getattr_fold(self): ambiguous_dt.fold = 0 def test_getattr_ambiguous(self): - assert not self.now.ambiguous ambiguous_dt = arrow.Arrow(2017, 10, 29, 2, 0, tzinfo="Europe/Stockholm") @@ -326,7 +317,6 @@ def test_getattr_ambiguous(self): assert ambiguous_dt.ambiguous def test_getattr_imaginary(self): - assert not self.now.imaginary imaginary_dt = arrow.Arrow(2013, 3, 31, 2, 30, tzinfo="Europe/Paris") @@ -337,19 +327,16 @@ def test_getattr_imaginary(self): @pytest.mark.usefixtures("time_utcnow") class TestArrowComparison: def test_eq(self): - assert self.arrow == self.arrow assert self.arrow == self.arrow.datetime assert not (self.arrow == "abc") def test_ne(self): - assert not (self.arrow != self.arrow) assert not (self.arrow != self.arrow.datetime) assert self.arrow != "abc" def test_gt(self): - arrow_cmp = self.arrow.shift(minutes=1) assert not (self.arrow > self.arrow) @@ -362,7 +349,6 @@ def test_gt(self): assert self.arrow < arrow_cmp.datetime def test_ge(self): - with pytest.raises(TypeError): self.arrow >= "abc" # noqa: B015 @@ -370,7 +356,6 @@ def test_ge(self): assert self.arrow >= self.arrow.datetime def test_lt(self): - arrow_cmp = self.arrow.shift(minutes=1) assert not (self.arrow < self.arrow) @@ -383,7 +368,6 @@ def test_lt(self): assert self.arrow < arrow_cmp.datetime def test_le(self): - with pytest.raises(TypeError): self.arrow <= "abc" # noqa: B015 @@ -394,53 +378,44 @@ def test_le(self): @pytest.mark.usefixtures("time_2013_01_01") class TestArrowMath: def test_add_timedelta(self): - result = self.arrow.__add__(timedelta(days=1)) assert result._datetime == datetime(2013, 1, 2, tzinfo=tz.tzutc()) def test_add_other(self): - with pytest.raises(TypeError): self.arrow + 1 def test_radd(self): - result = self.arrow.__radd__(timedelta(days=1)) assert result._datetime == datetime(2013, 1, 2, tzinfo=tz.tzutc()) def test_sub_timedelta(self): - result = self.arrow.__sub__(timedelta(days=1)) assert result._datetime == datetime(2012, 12, 31, tzinfo=tz.tzutc()) def test_sub_datetime(self): - result = self.arrow.__sub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) assert result == timedelta(days=11) def test_sub_arrow(self): - result = self.arrow.__sub__(arrow.Arrow(2012, 12, 21, tzinfo=tz.tzutc())) assert result == timedelta(days=11) def test_sub_other(self): - with pytest.raises(TypeError): self.arrow - object() def test_rsub_datetime(self): - result = self.arrow.__rsub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) assert result == timedelta(days=-11) def test_rsub_other(self): - with pytest.raises(TypeError): timedelta(days=1) - self.arrow @@ -448,87 +423,73 @@ def test_rsub_other(self): @pytest.mark.usefixtures("time_utcnow") class TestArrowDatetimeInterface: def test_date(self): - result = self.arrow.date() assert result == self.arrow._datetime.date() def test_time(self): - result = self.arrow.time() assert result == self.arrow._datetime.time() def test_timetz(self): - result = self.arrow.timetz() assert result == self.arrow._datetime.timetz() def test_astimezone(self): - - other_tz = tz.gettz("US/Pacific") + other_tz = ZoneInfo("US/Pacific") result = self.arrow.astimezone(other_tz) assert result == self.arrow._datetime.astimezone(other_tz) def test_utcoffset(self): - result = self.arrow.utcoffset() assert result == self.arrow._datetime.utcoffset() def test_dst(self): - result = self.arrow.dst() assert result == self.arrow._datetime.dst() def test_timetuple(self): - result = self.arrow.timetuple() assert result == self.arrow._datetime.timetuple() def test_utctimetuple(self): - result = self.arrow.utctimetuple() assert result == self.arrow._datetime.utctimetuple() def test_toordinal(self): - result = self.arrow.toordinal() assert result == self.arrow._datetime.toordinal() def test_weekday(self): - result = self.arrow.weekday() assert result == self.arrow._datetime.weekday() def test_isoweekday(self): - result = self.arrow.isoweekday() assert result == self.arrow._datetime.isoweekday() def test_isocalendar(self): - result = self.arrow.isocalendar() assert result == self.arrow._datetime.isocalendar() def test_isoformat(self): - result = self.arrow.isoformat() assert result == self.arrow._datetime.isoformat() def test_isoformat_timespec(self): - result = self.arrow.isoformat(timespec="hours") assert result == self.arrow._datetime.isoformat(timespec="hours") @@ -542,19 +503,16 @@ def test_isoformat_timespec(self): assert result == self.arrow._datetime.isoformat(sep="x", timespec="seconds") def test_simplejson(self): - result = json.dumps({"v": self.arrow.for_json()}, for_json=True) assert json.loads(result)["v"] == self.arrow._datetime.isoformat() def test_ctime(self): - result = self.arrow.ctime() assert result == self.arrow._datetime.ctime() def test_strftime(self): - result = self.arrow.strftime("%Y") assert result == self.arrow._datetime.strftime("%Y") @@ -586,20 +544,20 @@ class TestArrowFalsePositiveDst: def test_dst(self): self.before_1 = arrow.Arrow( - 2016, 11, 6, 3, 59, tzinfo=tz.gettz("America/New_York") + 2016, 11, 6, 3, 59, tzinfo=ZoneInfo("America/New_York") ) - self.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York")) - self.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York")) + self.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=ZoneInfo("America/New_York")) + self.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=ZoneInfo("America/New_York")) self.after_2 = arrow.Arrow( - 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") + 2016, 11, 6, 23, 59, tzinfo=ZoneInfo("America/New_York") ) self.before_3 = arrow.Arrow( - 2018, 11, 4, 3, 59, tzinfo=tz.gettz("America/New_York") + 2018, 11, 4, 3, 59, tzinfo=ZoneInfo("America/New_York") ) - self.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York")) - self.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York")) + self.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=ZoneInfo("America/New_York")) + self.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=ZoneInfo("America/New_York")) self.after_4 = arrow.Arrow( - 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") + 2018, 11, 4, 23, 59, tzinfo=ZoneInfo("America/New_York") ) assert self.before_1.day == self.before_2.day assert self.after_1.day == self.after_2.day @@ -609,11 +567,10 @@ def test_dst(self): class TestArrowConversion: def test_to(self): - dt_from = datetime.now() - arrow_from = arrow.Arrow.fromdatetime(dt_from, tz.gettz("US/Pacific")) + arrow_from = arrow.Arrow.fromdatetime(dt_from, ZoneInfo("US/Pacific")) - self.expected = dt_from.replace(tzinfo=tz.gettz("US/Pacific")).astimezone( + self.expected = dt_from.replace(tzinfo=ZoneInfo("US/Pacific")).astimezone( tz.tzutc() ) @@ -632,7 +589,6 @@ def test_to_amsterdam_then_utc(self): # regression test for #690 def test_to_israel_same_offset(self): - result = arrow.Arrow(2019, 10, 27, 2, 21, 1, tzinfo="+03:00").to("Israel") expected = arrow.Arrow(2019, 10, 27, 1, 21, 1, tzinfo="Israel") @@ -642,13 +598,12 @@ def test_to_israel_same_offset(self): # issue 315 def test_anchorage_dst(self): before = arrow.Arrow(2016, 3, 13, 1, 59, tzinfo="America/Anchorage") - after = arrow.Arrow(2016, 3, 13, 2, 1, tzinfo="America/Anchorage") + after = arrow.Arrow(2016, 3, 13, 3, 1, tzinfo="America/Anchorage") assert before.utcoffset() != after.utcoffset() # issue 476 def test_chicago_fall(self): - result = arrow.Arrow(2017, 11, 5, 2, 1, tzinfo="-05:00").to("America/Chicago") expected = arrow.Arrow(2017, 11, 5, 1, 1, tzinfo="America/Chicago") @@ -656,7 +611,6 @@ def test_chicago_fall(self): assert result.utcoffset() != expected.utcoffset() def test_toronto_gap(self): - before = arrow.Arrow(2011, 3, 13, 6, 30, tzinfo="UTC").to("America/Toronto") after = arrow.Arrow(2011, 3, 13, 7, 30, tzinfo="UTC").to("America/Toronto") @@ -666,7 +620,6 @@ def test_toronto_gap(self): assert before.utcoffset() != after.utcoffset() def test_sydney_gap(self): - before = arrow.Arrow(2012, 10, 6, 15, 30, tzinfo="UTC").to("Australia/Sydney") after = arrow.Arrow(2012, 10, 6, 16, 30, tzinfo="UTC").to("Australia/Sydney") @@ -678,7 +631,6 @@ def test_sydney_gap(self): class TestArrowPickling: def test_pickle_and_unpickle(self): - dt = arrow.Arrow.utcnow() pickled = pickle.dumps(dt) @@ -690,12 +642,10 @@ def test_pickle_and_unpickle(self): class TestArrowReplace: def test_not_attr(self): - with pytest.raises(ValueError): arrow.Arrow.utcnow().replace(abc=1) def test_replace(self): - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assert arw.replace(year=2012) == arrow.Arrow(2012, 5, 5, 12, 30, 45) @@ -706,15 +656,13 @@ def test_replace(self): assert arw.replace(second=1) == arrow.Arrow(2013, 5, 5, 12, 30, 1) def test_replace_tzinfo(self): - arw = arrow.Arrow.utcnow().to("US/Eastern") - result = arw.replace(tzinfo=tz.gettz("US/Pacific")) + result = arw.replace(tzinfo=ZoneInfo("US/Pacific")) - assert result == arw.datetime.replace(tzinfo=tz.gettz("US/Pacific")) + assert result == arw.datetime.replace(tzinfo=ZoneInfo("US/Pacific")) def test_replace_fold(self): - before = arrow.Arrow(2017, 11, 5, 1, tzinfo="America/New_York") after = before.replace(fold=1) @@ -724,19 +672,16 @@ def test_replace_fold(self): assert before.utcoffset() != after.utcoffset() def test_replace_fold_and_other(self): - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assert arw.replace(fold=1, minute=50) == arrow.Arrow(2013, 5, 5, 12, 50, 45) assert arw.replace(minute=50, fold=1) == arrow.Arrow(2013, 5, 5, 12, 50, 45) def test_replace_week(self): - with pytest.raises(ValueError): arrow.Arrow.utcnow().replace(week=1) def test_replace_quarter(self): - with pytest.raises(ValueError): arrow.Arrow.utcnow().replace(quarter=1) @@ -748,14 +693,12 @@ def test_replace_quarter_and_fold(self): arrow.utcnow().replace(quarter=1, fold=1) def test_replace_other_kwargs(self): - with pytest.raises(AttributeError): arrow.utcnow().replace(abc="def") class TestArrowShift: def test_not_attr(self): - now = arrow.Arrow.utcnow() with pytest.raises(ValueError): @@ -765,7 +708,6 @@ def test_not_attr(self): now.shift(week=1) def test_shift(self): - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assert arw.shift(years=1) == arrow.Arrow(2014, 5, 5, 12, 30, 45) @@ -822,7 +764,6 @@ def test_shift(self): assert arw.shift(weekday=SU(2)) == arrow.Arrow(2013, 5, 12, 12, 30, 45) def test_shift_negative(self): - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assert arw.shift(years=-1) == arrow.Arrow(2012, 5, 5, 12, 30, 45) @@ -858,7 +799,6 @@ def test_shift_negative(self): assert arw.shift(weekday=SU(-2)) == arrow.Arrow(2013, 4, 28, 12, 30, 45) def test_shift_quarters_bug(self): - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) # The value of the last-read argument was used instead of the ``quarters`` argument. @@ -876,7 +816,6 @@ def test_shift_quarters_bug(self): ) def test_shift_positive_imaginary(self): - # Avoid shifting into imaginary datetimes, take into account DST and other timezone changes. new_york = arrow.Arrow(2017, 3, 12, 1, 30, tzinfo="America/New_York") @@ -907,7 +846,6 @@ def test_shift_positive_imaginary(self): ) def test_shift_negative_imaginary(self): - new_york = arrow.Arrow(2011, 3, 13, 3, 30, tzinfo="America/New_York") assert new_york.shift(hours=-1) == arrow.Arrow( 2011, 3, 13, 3, 30, tzinfo="America/New_York" @@ -930,6 +868,16 @@ def test_shift_negative_imaginary(self): 2011, 12, 31, 23, tzinfo="Pacific/Apia" ) + def test_shift_with_imaginary_check(self): + dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=ZoneInfo("US/Eastern")) + shifted = dt.shift(hours=1) + assert shifted.datetime.hour == 3 + + def test_shift_without_imaginary_check(self): + dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=ZoneInfo("US/Eastern")) + shifted = dt.shift(hours=1, check_imaginary=False) + assert shifted.datetime.hour == 3 + @pytest.mark.skipif( dateutil.__version__ < "2.7.1", reason="old tz database (2018d needed)" ) @@ -951,7 +899,6 @@ def shift_imaginary_seconds(self): class TestArrowRange: def test_year(self): - result = list( arrow.Arrow.range( "year", datetime(2013, 1, 2, 3, 4, 5), datetime(2016, 4, 5, 6, 7, 8) @@ -966,7 +913,6 @@ def test_year(self): ] def test_quarter(self): - result = list( arrow.Arrow.range( "quarter", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) @@ -979,7 +925,6 @@ def test_quarter(self): ] def test_month(self): - result = list( arrow.Arrow.range( "month", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) @@ -994,7 +939,6 @@ def test_month(self): ] def test_week(self): - result = list( arrow.Arrow.range( "week", datetime(2013, 9, 1, 2, 3, 4), datetime(2013, 10, 1, 2, 3, 4) @@ -1010,7 +954,6 @@ def test_week(self): ] def test_day(self): - result = list( arrow.Arrow.range( "day", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 5, 6, 7, 8) @@ -1025,7 +968,6 @@ def test_day(self): ] def test_hour(self): - result = list( arrow.Arrow.range( "hour", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 6, 7, 8) @@ -1048,7 +990,6 @@ def test_hour(self): assert result == [arrow.Arrow(2013, 1, 2, 3, 4, 5)] def test_minute(self): - result = list( arrow.Arrow.range( "minute", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 7, 8) @@ -1063,7 +1004,6 @@ def test_minute(self): ] def test_second(self): - result = list( arrow.Arrow.range( "second", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 4, 8) @@ -1078,7 +1018,6 @@ def test_second(self): ] def test_arrow(self): - result = list( arrow.Arrow.range( "day", @@ -1095,47 +1034,43 @@ def test_arrow(self): ] def test_naive_tz(self): - result = arrow.Arrow.range( "year", datetime(2013, 1, 2, 3), datetime(2016, 4, 5, 6), "US/Pacific" ) for r in result: - assert r.tzinfo == tz.gettz("US/Pacific") + assert r.tzinfo == ZoneInfo("US/Pacific") def test_aware_same_tz(self): - result = arrow.Arrow.range( "day", - arrow.Arrow(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")), - arrow.Arrow(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), + arrow.Arrow(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")), + arrow.Arrow(2013, 1, 3, tzinfo=ZoneInfo("US/Pacific")), ) for r in result: - assert r.tzinfo == tz.gettz("US/Pacific") + assert r.tzinfo == ZoneInfo("US/Pacific") def test_aware_different_tz(self): - result = arrow.Arrow.range( "day", - datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")), - datetime(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), + datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Eastern")), + datetime(2013, 1, 3, tzinfo=ZoneInfo("US/Pacific")), ) for r in result: - assert r.tzinfo == tz.gettz("US/Eastern") + assert r.tzinfo == ZoneInfo("US/Eastern") def test_aware_tz(self): - result = arrow.Arrow.range( "day", - datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")), - datetime(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), - tz=tz.gettz("US/Central"), + datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Eastern")), + datetime(2013, 1, 3, tzinfo=ZoneInfo("US/Pacific")), + tz=ZoneInfo("US/Central"), ) for r in result: - assert r.tzinfo == tz.gettz("US/Central") + assert r.tzinfo == ZoneInfo("US/Central") def test_imaginary(self): # issue #72, avoid duplication in utc column @@ -1150,9 +1085,12 @@ def test_imaginary(self): assert len(utc_range) == len(set(utc_range)) def test_unsupported(self): - with pytest.raises(ValueError): - next(arrow.Arrow.range("abc", datetime.utcnow(), datetime.utcnow())) + next( + arrow.Arrow.range( + "abc", datetime.now(timezone.utc), datetime.now(timezone.utc) + ) + ) def test_range_over_months_ending_on_different_days(self): # regression test for issue #842 @@ -1206,7 +1144,6 @@ def test_range_over_year_maintains_end_date_across_leap_year(self): class TestArrowSpanRange: def test_year(self): - result = list( arrow.Arrow.span_range("year", datetime(2013, 2, 1), datetime(2016, 3, 31)) ) @@ -1231,7 +1168,6 @@ def test_year(self): ] def test_quarter(self): - result = list( arrow.Arrow.span_range( "quarter", datetime(2013, 2, 2), datetime(2013, 5, 15) @@ -1244,7 +1180,6 @@ def test_quarter(self): ] def test_month(self): - result = list( arrow.Arrow.span_range("month", datetime(2013, 1, 2), datetime(2013, 4, 15)) ) @@ -1257,7 +1192,6 @@ def test_month(self): ] def test_week(self): - result = list( arrow.Arrow.span_range("week", datetime(2013, 2, 2), datetime(2013, 2, 28)) ) @@ -1277,7 +1211,6 @@ def test_week(self): ] def test_day(self): - result = list( arrow.Arrow.span_range( "day", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) @@ -1304,7 +1237,6 @@ def test_day(self): ] def test_days(self): - result = list( arrow.Arrow.span_range( "days", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) @@ -1331,7 +1263,6 @@ def test_days(self): ] def test_hour(self): - result = list( arrow.Arrow.span_range( "hour", datetime(2013, 1, 1, 0, 30), datetime(2013, 1, 1, 3, 30) @@ -1368,7 +1299,6 @@ def test_hour(self): ] def test_minute(self): - result = list( arrow.Arrow.span_range( "minute", datetime(2013, 1, 1, 0, 0, 30), datetime(2013, 1, 1, 0, 3, 30) @@ -1395,7 +1325,6 @@ def test_minute(self): ] def test_second(self): - result = list( arrow.Arrow.span_range( "second", datetime(2013, 1, 1), datetime(2013, 1, 1, 0, 0, 3) @@ -1422,8 +1351,7 @@ def test_second(self): ] def test_naive_tz(self): - - tzinfo = tz.gettz("US/Pacific") + tzinfo = ZoneInfo("US/Pacific") result = arrow.Arrow.span_range( "hour", datetime(2013, 1, 1, 0), datetime(2013, 1, 1, 3, 59), "US/Pacific" @@ -1434,8 +1362,7 @@ def test_naive_tz(self): assert c.tzinfo == tzinfo def test_aware_same_tz(self): - - tzinfo = tz.gettz("US/Pacific") + tzinfo = ZoneInfo("US/Pacific") result = arrow.Arrow.span_range( "hour", @@ -1448,9 +1375,8 @@ def test_aware_same_tz(self): assert c.tzinfo == tzinfo def test_aware_different_tz(self): - - tzinfo1 = tz.gettz("US/Pacific") - tzinfo2 = tz.gettz("US/Eastern") + tzinfo1 = ZoneInfo("US/Pacific") + tzinfo2 = ZoneInfo("US/Eastern") result = arrow.Arrow.span_range( "hour", @@ -1463,20 +1389,18 @@ def test_aware_different_tz(self): assert c.tzinfo == tzinfo1 def test_aware_tz(self): - result = arrow.Arrow.span_range( "hour", - datetime(2013, 1, 1, 0, tzinfo=tz.gettz("US/Eastern")), - datetime(2013, 1, 1, 2, 59, tzinfo=tz.gettz("US/Eastern")), + datetime(2013, 1, 1, 0, tzinfo=ZoneInfo("US/Eastern")), + datetime(2013, 1, 1, 2, 59, tzinfo=ZoneInfo("US/Eastern")), tz="US/Central", ) for f, c in result: - assert f.tzinfo == tz.gettz("US/Central") - assert c.tzinfo == tz.gettz("US/Central") + assert f.tzinfo == ZoneInfo("US/Central") + assert c.tzinfo == ZoneInfo("US/Central") def test_bounds_param_is_passed(self): - result = list( arrow.Arrow.span_range( "quarter", datetime(2013, 2, 2), datetime(2013, 5, 15), bounds="[]" @@ -1489,7 +1413,6 @@ def test_bounds_param_is_passed(self): ] def test_exact_bound_exclude(self): - result = list( arrow.Arrow.span_range( "hour", @@ -1709,40 +1632,34 @@ def test_exact(self): @pytest.mark.usefixtures("time_2013_02_15") class TestArrowSpan: def test_span_attribute(self): - with pytest.raises(ValueError): self.arrow.span("span") def test_span_year(self): - floor, ceil = self.arrow.span("year") assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_quarter(self): - floor, ceil = self.arrow.span("quarter") assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 3, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_quarter_count(self): - floor, ceil = self.arrow.span("quarter", 2) assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 6, 30, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_year_count(self): - floor, ceil = self.arrow.span("year", 2) assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2014, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_month(self): - floor, ceil = self.arrow.span("month") assert floor == datetime(2013, 2, 1, tzinfo=tz.tzutc()) @@ -1775,75 +1692,220 @@ def test_span_week(self): assert ceil == datetime(2013, 2, 16, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_day(self): - floor, ceil = self.arrow.span("day") assert floor == datetime(2013, 2, 15, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_hour(self): - floor, ceil = self.arrow.span("hour") assert floor == datetime(2013, 2, 15, 3, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_minute(self): - floor, ceil = self.arrow.span("minute") assert floor == datetime(2013, 2, 15, 3, 41, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 41, 59, 999999, tzinfo=tz.tzutc()) def test_span_second(self): - floor, ceil = self.arrow.span("second") assert floor == datetime(2013, 2, 15, 3, 41, 22, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 41, 22, 999999, tzinfo=tz.tzutc()) def test_span_microsecond(self): - floor, ceil = self.arrow.span("microsecond") assert floor == datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) def test_floor(self): - floor, ceil = self.arrow.span("month") assert floor == self.arrow.floor("month") assert ceil == self.arrow.ceil("month") - def test_span_inclusive_inclusive(self): + def test_floor_week_start(self): + """ + Test floor method with week_start parameter for different week starts. + """ + # Test with default week_start=1 (Monday) + floor_default = self.arrow.floor("week") + floor_span_default, _ = self.arrow.span("week") + assert floor_default == floor_span_default + + # Test with week_start=1 (Monday) - explicit + floor_monday = self.arrow.floor("week", week_start=1) + floor_span_monday, _ = self.arrow.span("week", week_start=1) + assert floor_monday == floor_span_monday + + # Test with week_start=7 (Sunday) + floor_sunday = self.arrow.floor("week", week_start=7) + floor_span_sunday, _ = self.arrow.span("week", week_start=7) + assert floor_sunday == floor_span_sunday + + # Test with week_start=6 (Saturday) + floor_saturday = self.arrow.floor("week", week_start=6) + floor_span_saturday, _ = self.arrow.span("week", week_start=6) + assert floor_saturday == floor_span_saturday + + # Test with week_start=2 (Tuesday) + floor_tuesday = self.arrow.floor("week", week_start=2) + floor_span_tuesday, _ = self.arrow.span("week", week_start=2) + assert floor_tuesday == floor_span_tuesday + + def test_ceil_week_start(self): + """ + Test ceil method with week_start parameter for different week starts. + """ + # Test with default week_start=1 (Monday) + ceil_default = self.arrow.ceil("week") + _, ceil_span_default = self.arrow.span("week") + assert ceil_default == ceil_span_default + + # Test with week_start=1 (Monday) - explicit + ceil_monday = self.arrow.ceil("week", week_start=1) + _, ceil_span_monday = self.arrow.span("week", week_start=1) + assert ceil_monday == ceil_span_monday + + # Test with week_start=7 (Sunday) + ceil_sunday = self.arrow.ceil("week", week_start=7) + _, ceil_span_sunday = self.arrow.span("week", week_start=7) + assert ceil_sunday == ceil_span_sunday + + # Test with week_start=6 (Saturday) + ceil_saturday = self.arrow.ceil("week", week_start=6) + _, ceil_span_saturday = self.arrow.span("week", week_start=6) + assert ceil_saturday == ceil_span_saturday + + # Test with week_start=2 (Tuesday) + ceil_tuesday = self.arrow.ceil("week", week_start=2) + _, ceil_span_tuesday = self.arrow.span("week", week_start=2) + assert ceil_tuesday == ceil_span_tuesday + + def test_floor_ceil_week_start_values(self): + """ + Test specific date values for floor and ceil with different week_start values. + The test arrow is 2013-02-15 (Friday, isoweekday=5). + """ + # Test Monday start (week_start=1) + # Friday should floor to previous Monday (2013-02-11) + floor_mon = self.arrow.floor("week", week_start=1) + assert floor_mon == datetime(2013, 2, 11, tzinfo=tz.tzutc()) + # Friday should ceil to next Sunday (2013-02-17) + ceil_mon = self.arrow.ceil("week", week_start=1) + assert ceil_mon == datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + + # Test Sunday start (week_start=7) + # Friday should floor to previous Sunday (2013-02-10) + floor_sun = self.arrow.floor("week", week_start=7) + assert floor_sun == datetime(2013, 2, 10, tzinfo=tz.tzutc()) + # Friday should ceil to next Saturday (2013-02-16) + ceil_sun = self.arrow.ceil("week", week_start=7) + assert ceil_sun == datetime(2013, 2, 16, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + + # Test Saturday start (week_start=6) + # Friday should floor to previous Saturday (2013-02-09) + floor_sat = self.arrow.floor("week", week_start=6) + assert floor_sat == datetime(2013, 2, 9, tzinfo=tz.tzutc()) + # Friday should ceil to next Friday (2013-02-15) + ceil_sat = self.arrow.ceil("week", week_start=6) + assert ceil_sat == datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + + def test_floor_ceil_week_start_backward_compatibility(self): + """ + Test that floor and ceil methods maintain backward compatibility + when called without the week_start parameter. + """ + # Test that calling floor/ceil without parameters works the same as before + floor_old = self.arrow.floor("week") + floor_new = self.arrow.floor("week", week_start=1) # default value + assert floor_old == floor_new + + ceil_old = self.arrow.ceil("week") + ceil_new = self.arrow.ceil("week", week_start=1) # default value + assert ceil_old == ceil_new + def test_floor_ceil_week_start_ignored_for_non_week_frames(self): + """ + Test that week_start parameter is ignored for non-week frames. + """ + # Test that week_start parameter is ignored for different frames + for frame in ["hour", "day", "month", "year"]: + # floor should work the same with or without week_start for non-week frames + floor_without = self.arrow.floor(frame) + floor_with = self.arrow.floor(frame, week_start=7) # should be ignored + assert floor_without == floor_with + + # ceil should work the same with or without week_start for non-week frames + ceil_without = self.arrow.ceil(frame) + ceil_with = self.arrow.ceil(frame, week_start=7) # should be ignored + assert ceil_without == ceil_with + + def test_floor_ceil_week_start_validation(self): + """ + Test that week_start parameter validation works correctly for week frames. + """ + # Valid values should work for week frames + for week_start in range(1, 8): + self.arrow.floor("week", week_start=week_start) + self.arrow.ceil("week", week_start=week_start) + + # Invalid values should raise ValueError for week frames + with pytest.raises( + ValueError, match="week_start argument must be between 1 and 7" + ): + self.arrow.floor("week", week_start=0) + + with pytest.raises( + ValueError, match="week_start argument must be between 1 and 7" + ): + self.arrow.floor("week", week_start=8) + + with pytest.raises( + ValueError, match="week_start argument must be between 1 and 7" + ): + self.arrow.ceil("week", week_start=0) + + with pytest.raises( + ValueError, match="week_start argument must be between 1 and 7" + ): + self.arrow.ceil("week", week_start=8) + + # Invalid week_start values should be ignored for non-week frames (no validation) + # This ensures the parameter doesn't cause errors for other frames + for frame in ["hour", "day", "month", "year"]: + # These should not raise errors even though week_start is invalid + self.arrow.floor(frame, week_start=0) + self.arrow.floor(frame, week_start=8) + self.arrow.ceil(frame, week_start=0) + self.arrow.ceil(frame, week_start=8) + + def test_span_inclusive_inclusive(self): floor, ceil = self.arrow.span("hour", bounds="[]") assert floor == datetime(2013, 2, 15, 3, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 4, tzinfo=tz.tzutc()) def test_span_exclusive_inclusive(self): - floor, ceil = self.arrow.span("hour", bounds="(]") assert floor == datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 4, tzinfo=tz.tzutc()) def test_span_exclusive_exclusive(self): - floor, ceil = self.arrow.span("hour", bounds="()") assert floor == datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) def test_bounds_are_validated(self): - with pytest.raises(ValueError): floor, ceil = self.arrow.span("hour", bounds="][") def test_exact(self): - result_floor, result_ceil = self.arrow.span("hour", exact=True) expected_floor = datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) @@ -1853,28 +1915,24 @@ def test_exact(self): assert result_ceil == expected_ceil def test_exact_inclusive_inclusive(self): - floor, ceil = self.arrow.span("minute", bounds="[]", exact=True) assert floor == datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 42, 22, 8923, tzinfo=tz.tzutc()) def test_exact_exclusive_inclusive(self): - floor, ceil = self.arrow.span("day", bounds="(]", exact=True) assert floor == datetime(2013, 2, 15, 3, 41, 22, 8924, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 16, 3, 41, 22, 8923, tzinfo=tz.tzutc()) def test_exact_exclusive_exclusive(self): - floor, ceil = self.arrow.span("second", bounds="()", exact=True) assert floor == datetime(2013, 2, 15, 3, 41, 22, 8924, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 41, 23, 8922, tzinfo=tz.tzutc()) def test_all_parameters_specified(self): - floor, ceil = self.arrow.span("week", bounds="()", exact=True, count=2) assert floor == datetime(2013, 2, 15, 3, 41, 22, 8924, tzinfo=tz.tzutc()) @@ -1884,7 +1942,6 @@ def test_all_parameters_specified(self): @pytest.mark.usefixtures("time_2013_01_01") class TestArrowHumanize: def test_granularity(self): - assert self.now.humanize(granularity="second") == "just now" later1 = self.now.shift(seconds=1) @@ -1909,7 +1966,7 @@ def test_granularity(self): assert self.now.humanize(later4000, granularity="day") == "0 days ago" assert later4000.humanize(self.now, granularity="day") == "in 0 days" - later105 = self.now.shift(seconds=10 ** 5) + later105 = self.now.shift(seconds=10**5) assert self.now.humanize(later105, granularity="hour") == "27 hours ago" assert later105.humanize(self.now, granularity="hour") == "in 27 hours" assert self.now.humanize(later105, granularity="day") == "a day ago" @@ -1921,7 +1978,7 @@ def test_granularity(self): assert self.now.humanize(later105, granularity=["month"]) == "0 months ago" assert later105.humanize(self.now, granularity=["month"]) == "in 0 months" - later106 = self.now.shift(seconds=3 * 10 ** 6) + later106 = self.now.shift(seconds=3 * 10**6) assert self.now.humanize(later106, granularity="day") == "34 days ago" assert later106.humanize(self.now, granularity="day") == "in 34 days" assert self.now.humanize(later106, granularity="week") == "4 weeks ago" @@ -1931,7 +1988,7 @@ def test_granularity(self): assert self.now.humanize(later106, granularity="year") == "0 years ago" assert later106.humanize(self.now, granularity="year") == "in 0 years" - later506 = self.now.shift(seconds=50 * 10 ** 6) + later506 = self.now.shift(seconds=50 * 10**6) assert self.now.humanize(later506, granularity="week") == "82 weeks ago" assert later506.humanize(self.now, granularity="week") == "in 82 weeks" assert self.now.humanize(later506, granularity="month") == "18 months ago" @@ -1943,27 +2000,27 @@ def test_granularity(self): assert self.now.humanize(later1, granularity="quarter") == "0 quarters ago" assert later1.humanize(self.now, granularity="quarter") == "in 0 quarters" - later107 = self.now.shift(seconds=10 ** 7) + later107 = self.now.shift(seconds=10**7) assert self.now.humanize(later107, granularity="quarter") == "a quarter ago" assert later107.humanize(self.now, granularity="quarter") == "in a quarter" - later207 = self.now.shift(seconds=2 * 10 ** 7) + later207 = self.now.shift(seconds=2 * 10**7) assert self.now.humanize(later207, granularity="quarter") == "2 quarters ago" assert later207.humanize(self.now, granularity="quarter") == "in 2 quarters" - later307 = self.now.shift(seconds=3 * 10 ** 7) + later307 = self.now.shift(seconds=3 * 10**7) assert self.now.humanize(later307, granularity="quarter") == "3 quarters ago" assert later307.humanize(self.now, granularity="quarter") == "in 3 quarters" - later377 = self.now.shift(seconds=3.7 * 10 ** 7) + later377 = self.now.shift(seconds=3.7 * 10**7) assert self.now.humanize(later377, granularity="quarter") == "4 quarters ago" assert later377.humanize(self.now, granularity="quarter") == "in 4 quarters" - later407 = self.now.shift(seconds=4 * 10 ** 7) + later407 = self.now.shift(seconds=4 * 10**7) assert self.now.humanize(later407, granularity="quarter") == "5 quarters ago" assert later407.humanize(self.now, granularity="quarter") == "in 5 quarters" - later108 = self.now.shift(seconds=10 ** 8) + later108 = self.now.shift(seconds=10**8) assert self.now.humanize(later108, granularity="year") == "3 years ago" assert later108.humanize(self.now, granularity="year") == "in 3 years" - later108onlydistance = self.now.shift(seconds=10 ** 8) + later108onlydistance = self.now.shift(seconds=10**8) assert ( self.now.humanize( later108onlydistance, only_distance=True, granularity="year" @@ -2012,7 +2069,7 @@ def test_multiple_granularity(self): == "0 days an hour and 6 minutes ago" ) - later105 = self.now.shift(seconds=10 ** 5) + later105 = self.now.shift(seconds=10**5) assert ( self.now.humanize(later105, granularity=["hour", "day", "minute"]) == "a day 3 hours and 46 minutes ago" @@ -2020,7 +2077,7 @@ def test_multiple_granularity(self): with pytest.raises(ValueError): self.now.humanize(later105, granularity=["error", "second"]) - later108onlydistance = self.now.shift(seconds=10 ** 8) + later108onlydistance = self.now.shift(seconds=10**8) assert ( self.now.humanize( later108onlydistance, only_distance=True, granularity=["year"] @@ -2054,7 +2111,6 @@ def test_multiple_granularity(self): ) def test_seconds(self): - later = self.now.shift(seconds=10) # regression test for issue #727 @@ -2065,7 +2121,6 @@ def test_seconds(self): assert later.humanize(self.now, only_distance=True) == "10 seconds" def test_minute(self): - later = self.now.shift(minutes=1) assert self.now.humanize(later) == "a minute ago" @@ -2075,7 +2130,6 @@ def test_minute(self): assert later.humanize(self.now, only_distance=True) == "a minute" def test_minutes(self): - later = self.now.shift(minutes=2) assert self.now.humanize(later) == "2 minutes ago" @@ -2085,7 +2139,6 @@ def test_minutes(self): assert later.humanize(self.now, only_distance=True) == "2 minutes" def test_hour(self): - later = self.now.shift(hours=1) assert self.now.humanize(later) == "an hour ago" @@ -2095,7 +2148,6 @@ def test_hour(self): assert later.humanize(self.now, only_distance=True) == "an hour" def test_hours(self): - later = self.now.shift(hours=2) assert self.now.humanize(later) == "2 hours ago" @@ -2105,7 +2157,6 @@ def test_hours(self): assert later.humanize(self.now, only_distance=True) == "2 hours" def test_day(self): - later = self.now.shift(days=1) assert self.now.humanize(later) == "a day ago" @@ -2127,7 +2178,6 @@ def test_day(self): assert later.humanize(self.now, only_distance=True) == "a day" def test_days(self): - later = self.now.shift(days=2) assert self.now.humanize(later) == "2 days ago" @@ -2147,7 +2197,6 @@ def test_days(self): assert later.humanize(self.now) == "in 4 days" def test_week(self): - later = self.now.shift(weeks=1) assert self.now.humanize(later) == "a week ago" @@ -2157,7 +2206,6 @@ def test_week(self): assert later.humanize(self.now, only_distance=True) == "a week" def test_weeks(self): - later = self.now.shift(weeks=2) assert self.now.humanize(later) == "2 weeks ago" @@ -2166,29 +2214,16 @@ def test_weeks(self): assert self.now.humanize(later, only_distance=True) == "2 weeks" assert later.humanize(self.now, only_distance=True) == "2 weeks" - @pytest.mark.xfail(reason="known issue with humanize month limits") def test_month(self): - later = self.now.shift(months=1) - # TODO this test now returns "4 weeks ago", we need to fix this to be correct on a per month basis assert self.now.humanize(later) == "a month ago" assert later.humanize(self.now) == "in a month" assert self.now.humanize(later, only_distance=True) == "a month" assert later.humanize(self.now, only_distance=True) == "a month" - def test_month_plus_4_days(self): - - # TODO needed for coverage, remove when month limits are fixed - later = self.now.shift(months=1, days=4) - - assert self.now.humanize(later) == "a month ago" - assert later.humanize(self.now) == "in a month" - - @pytest.mark.xfail(reason="known issue with humanize month limits") def test_months(self): - later = self.now.shift(months=2) earlier = self.now.shift(months=-2) @@ -2199,7 +2234,6 @@ def test_months(self): assert later.humanize(self.now, only_distance=True) == "2 months" def test_year(self): - later = self.now.shift(years=1) assert self.now.humanize(later) == "a year ago" @@ -2209,7 +2243,6 @@ def test_year(self): assert later.humanize(self.now, only_distance=True) == "a year" def test_years(self): - later = self.now.shift(years=2) assert self.now.humanize(later) == "2 years ago" @@ -2225,7 +2258,6 @@ def test_years(self): assert result == "in a year" def test_arrow(self): - arw = arrow.Arrow.fromdatetime(self.datetime) result = arw.humanize(arrow.Arrow.fromdatetime(self.datetime)) @@ -2233,7 +2265,6 @@ def test_arrow(self): assert result == "just now" def test_datetime_tzinfo(self): - arw = arrow.Arrow.fromdatetime(self.datetime) result = arw.humanize(self.datetime.replace(tzinfo=tz.tzutc())) @@ -2241,21 +2272,18 @@ def test_datetime_tzinfo(self): assert result == "just now" def test_other(self): - arw = arrow.Arrow.fromdatetime(self.datetime) with pytest.raises(TypeError): arw.humanize(object()) def test_invalid_locale(self): - arw = arrow.Arrow.fromdatetime(self.datetime) with pytest.raises(ValueError): arw.humanize(locale="klingon") def test_none(self): - arw = arrow.Arrow.utcnow() result = arw.humanize() @@ -2277,7 +2305,6 @@ def test_week_limit(self): assert result == "a week ago" def test_untranslated_granularity(self, mocker): - arw = arrow.Arrow.utcnow() later = arw.shift(weeks=1) @@ -2295,8 +2322,8 @@ def test_empty_granularity_list(self): arw.humanize(later, granularity=[]) # Bulgarian is an example of a language that overrides _format_timeframe - # Applicabale to all locales. Note: Contributors need to make sure - # that if they override describe or describe_mutli, that delta + # Applicable to all locales. Note: Contributors need to make sure + # that if they override describe or describe_multi, that delta # is truncated on call def test_no_floats(self): @@ -2317,7 +2344,6 @@ def test_no_floats_multi_gran(self): @pytest.mark.usefixtures("time_2013_01_01") class TestArrowHumanizeTestsWithLocale: def test_now(self): - arw = arrow.Arrow(2013, 1, 1, 0, 0, 0) result = arw.humanize(self.datetime, locale="ru") @@ -2331,7 +2357,6 @@ def test_seconds(self): assert result == "через 44 секунды" def test_years(self): - arw = arrow.Arrow(2011, 7, 2) result = arw.humanize(self.datetime, locale="ru") @@ -2470,6 +2495,12 @@ def locale_list_no_weeks() -> List[str]: "ka-ge", "kk", "kk-kz", + "hy", + "hy-am", + "uz", + "uz-uz", + # "lo", + # "lo-la", ] return tested_langs @@ -2544,6 +2575,10 @@ def locale_list_with_weeks() -> List[str]: "ta-lk", "kk", "kk-kz", + "hy", + "hy-am", + "uz", + "uz-uz", ] return tested_langs @@ -2572,9 +2607,7 @@ def slavic_locales() -> List[str]: class TestArrowDehumanize: def test_now(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-1) second_future = arw.shift(seconds=1) @@ -2590,9 +2623,7 @@ def test_now(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(second_future_string, locale=lang) == arw def test_seconds(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-5) second_future = arw.shift(seconds=5) @@ -2608,9 +2639,7 @@ def test_seconds(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(second_future_string, locale=lang) == second_future def test_minute(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2001, 6, 18, 5, 55, 0) minute_ago = arw.shift(minutes=-1) minute_future = arw.shift(minutes=1) @@ -2626,9 +2655,7 @@ def test_minute(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(minute_future_string, locale=lang) == minute_future def test_minutes(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2007, 1, 10, 5, 55, 0) minute_ago = arw.shift(minutes=-5) minute_future = arw.shift(minutes=5) @@ -2644,9 +2671,7 @@ def test_minutes(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(minute_future_string, locale=lang) == minute_future def test_hour(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2009, 4, 20, 5, 55, 0) hour_ago = arw.shift(hours=-1) hour_future = arw.shift(hours=1) @@ -2660,9 +2685,7 @@ def test_hour(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(hour_future_string, locale=lang) == hour_future def test_hours(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2010, 2, 16, 7, 55, 0) hour_ago = arw.shift(hours=-3) hour_future = arw.shift(hours=3) @@ -2676,9 +2699,7 @@ def test_hours(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(hour_future_string, locale=lang) == hour_future def test_week(self, locale_list_with_weeks: List[str]): - for lang in locale_list_with_weeks: - arw = arrow.Arrow(2012, 2, 18, 1, 52, 0) week_ago = arw.shift(weeks=-1) week_future = arw.shift(weeks=1) @@ -2692,9 +2713,7 @@ def test_week(self, locale_list_with_weeks: List[str]): assert arw.dehumanize(week_future_string, locale=lang) == week_future def test_weeks(self, locale_list_with_weeks: List[str]): - for lang in locale_list_with_weeks: - arw = arrow.Arrow(2020, 3, 18, 5, 3, 0) week_ago = arw.shift(weeks=-7) week_future = arw.shift(weeks=7) @@ -2708,9 +2727,7 @@ def test_weeks(self, locale_list_with_weeks: List[str]): assert arw.dehumanize(week_future_string, locale=lang) == week_future def test_year(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) year_ago = arw.shift(years=-1) year_future = arw.shift(years=1) @@ -2724,9 +2741,7 @@ def test_year(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(year_future_string, locale=lang) == year_future def test_years(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) year_ago = arw.shift(years=-10) year_future = arw.shift(years=10) @@ -2740,9 +2755,7 @@ def test_years(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(year_future_string, locale=lang) == year_future def test_gt_than_10_years(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) year_ago = arw.shift(years=-25) year_future = arw.shift(years=25) @@ -2756,9 +2769,7 @@ def test_gt_than_10_years(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(year_future_string, locale=lang) == year_future def test_mixed_granularity(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) past = arw.shift(hours=-1, minutes=-1, seconds=-1) future = arw.shift(hours=1, minutes=1, seconds=1) @@ -2774,9 +2785,7 @@ def test_mixed_granularity(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(future_string, locale=lang) == future def test_mixed_granularity_hours(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) past = arw.shift(hours=-3, minutes=-1, seconds=-15) future = arw.shift(hours=3, minutes=1, seconds=15) @@ -2792,9 +2801,7 @@ def test_mixed_granularity_hours(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(future_string, locale=lang) == future def test_mixed_granularity_day(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) past = arw.shift(days=-3, minutes=-1, seconds=-15) future = arw.shift(days=3, minutes=1, seconds=15) @@ -2810,9 +2817,7 @@ def test_mixed_granularity_day(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(future_string, locale=lang) == future def test_mixed_granularity_day_hour(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) past = arw.shift(days=-3, hours=-23, seconds=-15) future = arw.shift(days=3, hours=23, seconds=15) @@ -2829,7 +2834,6 @@ def test_mixed_granularity_day_hour(self, locale_list_no_weeks: List[str]): # Test to make sure unsupported locales error out def test_unsupported_locale(self): - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-5) second_future = arw.shift(seconds=5) @@ -2850,7 +2854,6 @@ def test_unsupported_locale(self): # Test to ensure old style locale strings are supported def test_normalized_locale(self): - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-5) second_future = arw.shift(seconds=5) @@ -2867,9 +2870,7 @@ def test_normalized_locale(self): # Ensures relative units are required in string def test_require_relative_unit(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-5) second_future = arw.shift(seconds=5) @@ -2889,9 +2890,7 @@ def test_require_relative_unit(self, locale_list_no_weeks: List[str]): # Test for scrambled input def test_scrambled_input(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-5) second_future = arw.shift(seconds=5) @@ -2917,9 +2916,7 @@ def test_scrambled_input(self, locale_list_no_weeks: List[str]): arw.dehumanize(second_future_string, locale=lang) def test_no_units_modified(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) # Ensures we pass the first stage of checking whether relative units exist @@ -2934,7 +2931,6 @@ def test_no_units_modified(self, locale_list_no_weeks: List[str]): arw.dehumanize(empty_future_string, locale=lang) def test_slavic_locales(self, slavic_locales: List[str]): - # Relevant units for Slavic locale plural logic units = [ 0, @@ -2965,7 +2961,6 @@ def test_slavic_locales(self, slavic_locales: List[str]): assert arw.dehumanize(future_string, locale=lang) == future def test_czech_slovak(self): - # Relevant units for Slavic locale plural logic units = [ 0, @@ -3072,11 +3067,10 @@ def test_value_error_exception(self): class TestArrowUtil: def test_get_datetime(self): - get_datetime = arrow.Arrow._get_datetime arw = arrow.Arrow.utcnow() - dt = datetime.utcnow() + dt = datetime.now(timezone.utc) timestamp = time.time() assert get_datetime(arw) == arw.datetime @@ -3090,7 +3084,6 @@ def test_get_datetime(self): assert "not recognized as a datetime or timestamp" in str(raise_ctx.value) def test_get_tzinfo(self): - get_tzinfo = arrow.Arrow._get_tzinfo with pytest.raises(ValueError) as raise_ctx: @@ -3098,7 +3091,6 @@ def test_get_tzinfo(self): assert "not recognized as a timezone" in str(raise_ctx.value) def test_get_iteration_params(self): - assert arrow.Arrow._get_iteration_params("end", None) == ("end", sys.maxsize) assert arrow.Arrow._get_iteration_params(None, 100) == (arrow.Arrow.max, 100) assert arrow.Arrow._get_iteration_params(100, 120) == (100, 120) diff --git a/tests/test_factory.py b/tests/test_factory.py index 53bba20d..056cee41 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,10 +1,15 @@ import time -from datetime import date, datetime +from datetime import date, datetime, timezone from decimal import Decimal import pytest from dateutil import tz +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo + from arrow import Arrow from arrow.parser import ParserError @@ -14,13 +19,11 @@ @pytest.mark.usefixtures("arrow_factory") class TestGet: def test_no_args(self): - assert_datetime_equality( - self.factory.get(), datetime.utcnow().replace(tzinfo=tz.tzutc()) + self.factory.get(), datetime.now(timezone.utc).replace(tzinfo=timezone.utc) ) def test_timestamp_one_arg_no_arg(self): - no_arg = self.factory.get(1406430900).timestamp() one_arg = self.factory.get("1406430900", "X").timestamp() @@ -31,16 +34,14 @@ def test_one_arg_none(self): self.factory.get(None) def test_struct_time(self): - assert_datetime_equality( self.factory.get(time.gmtime()), - datetime.utcnow().replace(tzinfo=tz.tzutc()), + datetime.now(timezone.utc).replace(tzinfo=timezone.utc), ) def test_one_arg_timestamp(self): - int_timestamp = int(time.time()) - timestamp_dt = datetime.utcfromtimestamp(int_timestamp).replace( + timestamp_dt = datetime.fromtimestamp(int_timestamp, timezone.utc).replace( tzinfo=tz.tzutc() ) @@ -50,7 +51,7 @@ def test_one_arg_timestamp(self): self.factory.get(str(int_timestamp)) float_timestamp = time.time() - timestamp_dt = datetime.utcfromtimestamp(float_timestamp).replace( + timestamp_dt = datetime.fromtimestamp(float_timestamp, timezone.utc).replace( tzinfo=tz.tzutc() ) @@ -66,60 +67,54 @@ def test_one_arg_timestamp(self): self.factory.get(timestamp) def test_one_arg_expanded_timestamp(self): - millisecond_timestamp = 1591328104308 microsecond_timestamp = 1591328104308505 # Regression test for issue #796 - assert self.factory.get(millisecond_timestamp) == datetime.utcfromtimestamp( - 1591328104.308 + assert self.factory.get(millisecond_timestamp) == datetime.fromtimestamp( + 1591328104.308, timezone.utc ).replace(tzinfo=tz.tzutc()) - assert self.factory.get(microsecond_timestamp) == datetime.utcfromtimestamp( - 1591328104.308505 + assert self.factory.get(microsecond_timestamp) == datetime.fromtimestamp( + 1591328104.308505, timezone.utc ).replace(tzinfo=tz.tzutc()) def test_one_arg_timestamp_with_tzinfo(self): - timestamp = time.time() timestamp_dt = datetime.fromtimestamp(timestamp, tz=tz.tzutc()).astimezone( - tz.gettz("US/Pacific") + ZoneInfo("US/Pacific") ) - timezone = tz.gettz("US/Pacific") + timezone = ZoneInfo("US/Pacific") assert_datetime_equality( self.factory.get(timestamp, tzinfo=timezone), timestamp_dt ) def test_one_arg_arrow(self): - arw = self.factory.utcnow() result = self.factory.get(arw) assert arw == result def test_one_arg_datetime(self): - - dt = datetime.utcnow().replace(tzinfo=tz.tzutc()) + dt = datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()) assert self.factory.get(dt) == dt def test_one_arg_date(self): - d = date.today() dt = datetime(d.year, d.month, d.day, tzinfo=tz.tzutc()) assert self.factory.get(d) == dt def test_one_arg_tzinfo(self): - self.expected = ( - datetime.utcnow() + datetime.now(timezone.utc) .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) + .astimezone(ZoneInfo("US/Pacific")) ) assert_datetime_equality( - self.factory.get(tz.gettz("US/Pacific")), self.expected + self.factory.get(ZoneInfo("US/Pacific")), self.expected ) # regression test for issue #658 @@ -132,23 +127,21 @@ def test_one_arg_dateparser_datetime(self): assert dt_output == expected def test_kwarg_tzinfo(self): - self.expected = ( - datetime.utcnow() + datetime.now(timezone.utc) .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) + .astimezone(ZoneInfo("US/Pacific")) ) assert_datetime_equality( - self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected + self.factory.get(tzinfo=ZoneInfo("US/Pacific")), self.expected ) def test_kwarg_tzinfo_string(self): - self.expected = ( - datetime.utcnow() + datetime.now(timezone.utc) .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) + .astimezone(ZoneInfo("US/Pacific")) ) assert_datetime_equality(self.factory.get(tzinfo="US/Pacific"), self.expected) @@ -176,38 +169,34 @@ def test_kwarg_normalize_whitespace(self): # regression test for #944 def test_one_arg_datetime_tzinfo_kwarg(self): - dt = datetime(2021, 4, 29, 6) result = self.factory.get(dt, tzinfo="America/Chicago") - expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago")) + expected = datetime(2021, 4, 29, 6, tzinfo=ZoneInfo("America/Chicago")) assert_datetime_equality(result._datetime, expected) def test_one_arg_arrow_tzinfo_kwarg(self): - arw = Arrow(2021, 4, 29, 6) result = self.factory.get(arw, tzinfo="America/Chicago") - expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago")) + expected = datetime(2021, 4, 29, 6, tzinfo=ZoneInfo("America/Chicago")) assert_datetime_equality(result._datetime, expected) def test_one_arg_date_tzinfo_kwarg(self): - da = date(2021, 4, 29) result = self.factory.get(da, tzinfo="America/Chicago") - expected = Arrow(2021, 4, 29, tzinfo=tz.gettz("America/Chicago")) + expected = Arrow(2021, 4, 29, tzinfo=ZoneInfo("America/Chicago")) assert result.date() == expected.date() assert result.tzinfo == expected.tzinfo def test_one_arg_iso_calendar_tzinfo_kwarg(self): - result = self.factory.get((2004, 1, 7), tzinfo="America/Chicago") expected = Arrow(2004, 1, 4, tzinfo="America/Chicago") @@ -215,15 +204,11 @@ def test_one_arg_iso_calendar_tzinfo_kwarg(self): assert_datetime_equality(result, expected) def test_one_arg_iso_str(self): + dt = datetime.now(timezone.utc) - dt = datetime.utcnow() - - assert_datetime_equality( - self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc()) - ) + assert_datetime_equality(self.factory.get(dt.isoformat()), dt) def test_one_arg_iso_calendar(self): - pairs = [ (datetime(2004, 1, 4), (2004, 1, 7)), (datetime(2008, 12, 30), (2009, 1, 2)), @@ -252,12 +237,10 @@ def test_one_arg_iso_calendar(self): self.factory.get((2014, 7, 10)) def test_one_arg_other(self): - with pytest.raises(TypeError): self.factory.get(object()) def test_one_arg_bool(self): - with pytest.raises(TypeError): self.factory.get(False) @@ -272,55 +255,46 @@ def test_one_arg_decimal(self): ) def test_two_args_datetime_tzinfo(self): + result = self.factory.get(datetime(2013, 1, 1), ZoneInfo("US/Pacific")) - result = self.factory.get(datetime(2013, 1, 1), tz.gettz("US/Pacific")) - - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")) def test_two_args_datetime_tz_str(self): - result = self.factory.get(datetime(2013, 1, 1), "US/Pacific") - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")) def test_two_args_date_tzinfo(self): + result = self.factory.get(date(2013, 1, 1), ZoneInfo("US/Pacific")) - result = self.factory.get(date(2013, 1, 1), tz.gettz("US/Pacific")) - - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")) def test_two_args_date_tz_str(self): - result = self.factory.get(date(2013, 1, 1), "US/Pacific") - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")) def test_two_args_datetime_other(self): - with pytest.raises(TypeError): - self.factory.get(datetime.utcnow(), object()) + self.factory.get(datetime.now(timezone.utc), object()) def test_two_args_date_other(self): - with pytest.raises(TypeError): self.factory.get(date.today(), object()) def test_two_args_str_str(self): - result = self.factory.get("2013-01-01", "YYYY-MM-DD") assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_two_args_str_tzinfo(self): - - result = self.factory.get("2013-01-01", tzinfo=tz.gettz("US/Pacific")) + result = self.factory.get("2013-01-01", tzinfo=ZoneInfo("US/Pacific")) assert_datetime_equality( - result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + result._datetime, datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")) ) def test_two_args_twitter_format(self): - # format returned by twitter API for created_at: twitter_date = "Fri Apr 08 21:08:54 +0000 2016" result = self.factory.get(twitter_date, "ddd MMM DD HH:mm:ss Z YYYY") @@ -328,24 +302,20 @@ def test_two_args_twitter_format(self): assert result._datetime == datetime(2016, 4, 8, 21, 8, 54, tzinfo=tz.tzutc()) def test_two_args_str_list(self): - result = self.factory.get("2013-01-01", ["MM/DD/YYYY", "YYYY-MM-DD"]) assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_two_args_unicode_unicode(self): - result = self.factory.get("2013-01-01", "YYYY-MM-DD") assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_two_args_other(self): - with pytest.raises(TypeError): self.factory.get(object(), object()) def test_three_args_with_tzinfo(self): - timefmt = "YYYYMMDD" d = "20150514" @@ -354,26 +324,20 @@ def test_three_args_with_tzinfo(self): ) def test_three_args(self): - assert self.factory.get(2013, 1, 1) == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_full_kwargs(self): - - assert ( - self.factory.get( - year=2016, - month=7, - day=14, - hour=7, - minute=16, - second=45, - microsecond=631092, - ) - == datetime(2016, 7, 14, 7, 16, 45, 631092, tzinfo=tz.tzutc()) - ) + assert self.factory.get( + year=2016, + month=7, + day=14, + hour=7, + minute=16, + second=45, + microsecond=631092, + ) == datetime(2016, 7, 14, 7, 16, 45, 631092, tzinfo=tz.tzutc()) def test_three_kwargs(self): - assert self.factory.get(year=2016, month=7, day=14) == datetime( 2016, 7, 14, 0, 0, tzinfo=tz.tzutc() ) @@ -383,7 +347,6 @@ def test_tzinfo_string_kwargs(self): assert result._datetime == datetime(2019, 7, 28, 7, 0, 0, 0, tzinfo=tz.tzutc()) def test_insufficient_kwargs(self): - with pytest.raises(TypeError): self.factory.get(year=2016) @@ -402,35 +365,31 @@ def test_locale(self): def test_locale_kwarg_only(self): res = self.factory.get(locale="ja") - assert res.tzinfo == tz.tzutc() + assert res.tzinfo == timezone.utc def test_locale_with_tzinfo(self): - res = self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo")) - assert res.tzinfo == tz.gettz("Asia/Tokyo") + res = self.factory.get(locale="ja", tzinfo=ZoneInfo("Asia/Tokyo")) + assert res.tzinfo == ZoneInfo("Asia/Tokyo") @pytest.mark.usefixtures("arrow_factory") class TestUtcNow: def test_utcnow(self): - assert_datetime_equality( self.factory.utcnow()._datetime, - datetime.utcnow().replace(tzinfo=tz.tzutc()), + datetime.now(timezone.utc), ) @pytest.mark.usefixtures("arrow_factory") class TestNow: def test_no_tz(self): - - assert_datetime_equality(self.factory.now(), datetime.now(tz.tzlocal())) + assert_datetime_equality(self.factory.now(), datetime.now().astimezone()) def test_tzinfo(self): - assert_datetime_equality( - self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST")) + self.factory.now(ZoneInfo("EST")), datetime.now(ZoneInfo("EST")) ) def test_tz_str(self): - - assert_datetime_equality(self.factory.now("EST"), datetime.now(tz.gettz("EST"))) + assert_datetime_equality(self.factory.now("EST"), datetime.now(ZoneInfo("EST"))) diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 06831f1e..ff3fea28 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -1,7 +1,11 @@ -from datetime import datetime +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo + +from datetime import datetime, timezone import pytest -import pytz from dateutil import tz as dateutil_tz from arrow import ( @@ -13,6 +17,7 @@ FORMAT_RFC1123, FORMAT_RFC2822, FORMAT_RFC3339, + FORMAT_RFC3339_STRICT, FORMAT_RSS, FORMAT_W3C, ) @@ -23,7 +28,6 @@ @pytest.mark.usefixtures("arrow_formatter") class TestFormatterFormatToken: def test_format(self): - dt = datetime(2013, 2, 5, 12, 32, 51) result = self.formatter.format(dt, "MM-DD-YYYY hh:mm:ss a") @@ -31,13 +35,11 @@ def test_format(self): assert result == "02-05-2013 12:32:51 pm" def test_year(self): - dt = datetime(2013, 1, 1) assert self.formatter._format_token(dt, "YYYY") == "2013" assert self.formatter._format_token(dt, "YY") == "13" def test_month(self): - dt = datetime(2013, 1, 1) assert self.formatter._format_token(dt, "MMMM") == "January" assert self.formatter._format_token(dt, "MMM") == "Jan" @@ -45,7 +47,6 @@ def test_month(self): assert self.formatter._format_token(dt, "M") == "1" def test_day(self): - dt = datetime(2013, 2, 1) assert self.formatter._format_token(dt, "DDDD") == "032" assert self.formatter._format_token(dt, "DDD") == "32" @@ -58,7 +59,6 @@ def test_day(self): assert self.formatter._format_token(dt, "d") == "5" def test_hour(self): - dt = datetime(2013, 1, 1, 2) assert self.formatter._format_token(dt, "HH") == "02" assert self.formatter._format_token(dt, "H") == "2" @@ -81,19 +81,16 @@ def test_hour(self): assert self.formatter._format_token(dt, "h") == "12" def test_minute(self): - dt = datetime(2013, 1, 1, 0, 1) assert self.formatter._format_token(dt, "mm") == "01" assert self.formatter._format_token(dt, "m") == "1" def test_second(self): - dt = datetime(2013, 1, 1, 0, 0, 1) assert self.formatter._format_token(dt, "ss") == "01" assert self.formatter._format_token(dt, "s") == "1" def test_sub_second(self): - dt = datetime(2013, 1, 1, 0, 0, 0, 123456) assert self.formatter._format_token(dt, "SSSSSS") == "123456" assert self.formatter._format_token(dt, "SSSSS") == "12345" @@ -111,7 +108,6 @@ def test_sub_second(self): assert self.formatter._format_token(dt, "S") == "0" def test_timestamp(self): - dt = datetime.now(tz=dateutil_tz.UTC) expected = str(dt.timestamp()) assert self.formatter._format_token(dt, "X") == expected @@ -122,8 +118,7 @@ def test_timestamp(self): assert self.formatter._format_token(dt, "x") == expected def test_timezone(self): - - dt = datetime.utcnow().replace(tzinfo=dateutil_tz.gettz("US/Pacific")) + dt = datetime.now(timezone.utc).replace(tzinfo=ZoneInfo("US/Pacific")) result = self.formatter._format_token(dt, "ZZ") assert result == "-07:00" or result == "-08:00" @@ -133,10 +128,9 @@ def test_timezone(self): @pytest.mark.parametrize("full_tz_name", make_full_tz_list()) def test_timezone_formatter(self, full_tz_name): - # This test will fail if we use "now" as date as soon as we change from/to DST - dt = datetime(1986, 2, 14, tzinfo=pytz.timezone("UTC")).replace( - tzinfo=dateutil_tz.gettz(full_tz_name) + dt = datetime(1986, 2, 14, tzinfo=timezone.utc).replace( + tzinfo=ZoneInfo(full_tz_name) ) abbreviation = dt.tzname() @@ -144,7 +138,6 @@ def test_timezone_formatter(self, full_tz_name): assert result == abbreviation def test_am_pm(self): - dt = datetime(2012, 1, 1, 11) assert self.formatter._format_token(dt, "a") == "am" assert self.formatter._format_token(dt, "A") == "AM" @@ -167,7 +160,6 @@ def test_nonsense(self): assert self.formatter._format_token(dt, "NONSENSE") is None def test_escape(self): - assert ( self.formatter.format( datetime(2015, 12, 10, 17, 9), "MMMM D, YYYY [at] h:mma" @@ -267,6 +259,12 @@ def test_rfc3339(self): == "1975-12-25 14:15:16-05:00" ) + def test_rfc3339_strict(self): + assert ( + self.formatter.format(self.datetime, FORMAT_RFC3339_STRICT) + == "1975-12-25T14:15:16-05:00" + ) + def test_rss(self): assert ( self.formatter.format(self.datetime, FORMAT_RSS) diff --git a/tests/test_locales.py b/tests/test_locales.py index 0e42074d..3d7120e3 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -8,7 +8,6 @@ class TestLocaleValidation: """Validate locales to ensure that translations are valid and complete""" def test_locale_validation(self): - for locale_cls in self.locales.values(): # 7 days + 1 spacer to allow for 1-indexing of months assert len(locale_cls.day_names) == 8 @@ -34,15 +33,13 @@ def test_locale_validation(self): assert locale_cls.future is not None def test_locale_name_validation(self): + import re for locale_cls in self.locales.values(): for locale_name in locale_cls.names: - assert len(locale_name) == 2 or len(locale_name) == 5 assert locale_name.islower() - # Not a two-letter code - if len(locale_name) > 2: - assert "-" in locale_name - assert locale_name.count("-") == 1 + pattern = r"^[a-z]{2}(-[a-z]{2})?(?:-latn|-cyrl)?$" + assert re.match(pattern, locale_name) def test_duplicated_locale_name(self): with pytest.raises(LookupError): @@ -90,7 +87,6 @@ def test_get_locale_by_class_name(self, mocker): assert result == mock_locale_obj def test_locales(self): - assert len(locales._locale_map) > 0 @@ -116,24 +112,20 @@ def test_describe(self): assert self.locale.describe("now", only_distance=False) == "just now" def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 hours" assert self.locale._format_timeframe("hour", 0) == "an hour" def test_format_relative_now(self): - result = self.locale._format_relative("just now", "now", 0) assert result == "just now" def test_format_relative_past(self): - result = self.locale._format_relative("an hour", "hour", 1) assert result == "in an hour" def test_format_relative_future(self): - result = self.locale._format_relative("an hour", "hour", -1) assert result == "an hour ago" @@ -438,7 +430,6 @@ def test_plurals2(self): @pytest.mark.usefixtures("lang_locale") class TestPolishLocale: def test_plurals(self): - assert self.locale._format_timeframe("seconds", 0) == "0 sekund" assert self.locale._format_timeframe("second", 1) == "sekundę" assert self.locale._format_timeframe("seconds", 2) == "2 sekundy" @@ -491,7 +482,6 @@ def test_plurals(self): @pytest.mark.usefixtures("lang_locale") class TestIcelandicLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("now", 0) == "rétt í þessu" assert self.locale._format_timeframe("second", -1) == "sekúndu" @@ -534,23 +524,19 @@ def test_format_timeframe(self): @pytest.mark.usefixtures("lang_locale") class TestMalayalamLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 മണിക്കൂർ" assert self.locale._format_timeframe("hour", 0) == "ഒരു മണിക്കൂർ" def test_format_relative_now(self): - result = self.locale._format_relative("ഇപ്പോൾ", "now", 0) assert result == "ഇപ്പോൾ" def test_format_relative_past(self): - result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", 1) assert result == "ഒരു മണിക്കൂർ ശേഷം" def test_format_relative_future(self): - result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", -1) assert result == "ഒരു മണിക്കൂർ മുമ്പ്" @@ -585,22 +571,18 @@ def test_weekday(self): @pytest.mark.usefixtures("lang_locale") class TestHindiLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 घंटे" assert self.locale._format_timeframe("hour", 0) == "एक घंटा" def test_format_relative_now(self): - result = self.locale._format_relative("अभी", "now", 0) assert result == "अभी" def test_format_relative_past(self): - result = self.locale._format_relative("एक घंटा", "hour", 1) assert result == "एक घंटा बाद" def test_format_relative_future(self): - result = self.locale._format_relative("एक घंटा", "hour", -1) assert result == "एक घंटा पहले" @@ -675,17 +657,14 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", 5) == "5 let" def test_format_relative_now(self): - result = self.locale._format_relative("Teď", "now", 0) assert result == "Teď" def test_format_relative_future(self): - result = self.locale._format_relative("hodinu", "hour", 1) assert result == "Za hodinu" def test_format_relative_past(self): - result = self.locale._format_relative("hodinou", "hour", -1) assert result == "Před hodinou" @@ -693,7 +672,6 @@ def test_format_relative_past(self): @pytest.mark.usefixtures("lang_locale") class TestSlovakLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("seconds", -5) == "5 sekundami" assert self.locale._format_timeframe("seconds", -2) == "2 sekundami" assert self.locale._format_timeframe("second", -1) == "sekundou" @@ -753,17 +731,14 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "Teraz" def test_format_relative_now(self): - result = self.locale._format_relative("Teraz", "now", 0) assert result == "Teraz" def test_format_relative_future(self): - result = self.locale._format_relative("hodinu", "hour", 1) assert result == "O hodinu" def test_format_relative_past(self): - result = self.locale._format_relative("hodinou", "hour", -1) assert result == "Pred hodinou" @@ -933,6 +908,148 @@ def test_multi_describe_mk(self): assert describe(seconds60, only_distance=True) == "1 секунда" +@pytest.mark.usefixtures("lang_locale") +class TestMacedonianLatinLocale: + def test_singles_mk(self): + assert self.locale._format_timeframe("second", 1) == "edna sekunda" + assert self.locale._format_timeframe("minute", 1) == "edna minuta" + assert self.locale._format_timeframe("hour", 1) == "eden saat" + assert self.locale._format_timeframe("day", 1) == "eden den" + assert self.locale._format_timeframe("week", 1) == "edna nedela" + assert self.locale._format_timeframe("month", 1) == "eden mesec" + assert self.locale._format_timeframe("year", 1) == "edna godina" + + def test_meridians_mk(self): + assert self.locale.meridian(7, "A") == "pretpladne" + assert self.locale.meridian(18, "A") == "popladne" + assert self.locale.meridian(10, "a") == "dp" + assert self.locale.meridian(22, "a") == "pp" + + def test_describe_mk(self): + assert self.locale.describe("second", only_distance=True) == "edna sekunda" + assert self.locale.describe("second", only_distance=False) == "za edna sekunda" + assert self.locale.describe("minute", only_distance=True) == "edna minuta" + assert self.locale.describe("minute", only_distance=False) == "za edna minuta" + assert self.locale.describe("hour", only_distance=True) == "eden saat" + assert self.locale.describe("hour", only_distance=False) == "za eden saat" + assert self.locale.describe("day", only_distance=True) == "eden den" + assert self.locale.describe("day", only_distance=False) == "za eden den" + assert self.locale.describe("week", only_distance=True) == "edna nedela" + assert self.locale.describe("week", only_distance=False) == "za edna nedela" + assert self.locale.describe("month", only_distance=True) == "eden mesec" + assert self.locale.describe("month", only_distance=False) == "za eden mesec" + assert self.locale.describe("year", only_distance=True) == "edna godina" + assert self.locale.describe("year", only_distance=False) == "za edna godina" + + def test_relative_mk(self): + # time + assert self.locale._format_relative("sega", "now", 0) == "sega" + assert self.locale._format_relative("1 sekunda", "seconds", 1) == "za 1 sekunda" + assert self.locale._format_relative("1 minuta", "minutes", 1) == "za 1 minuta" + assert self.locale._format_relative("1 saat", "hours", 1) == "za 1 saat" + assert self.locale._format_relative("1 den", "days", 1) == "za 1 den" + assert self.locale._format_relative("1 nedela", "weeks", 1) == "za 1 nedela" + assert self.locale._format_relative("1 mesec", "months", 1) == "za 1 mesec" + assert self.locale._format_relative("1 godina", "years", 1) == "za 1 godina" + assert ( + self.locale._format_relative("1 sekunda", "seconds", -1) == "pred 1 sekunda" + ) + assert ( + self.locale._format_relative("1 minuta", "minutes", -1) == "pred 1 minuta" + ) + assert self.locale._format_relative("1 saat", "hours", -1) == "pred 1 saat" + assert self.locale._format_relative("1 den", "days", -1) == "pred 1 den" + assert self.locale._format_relative("1 nedela", "weeks", -1) == "pred 1 nedela" + assert self.locale._format_relative("1 mesec", "months", -1) == "pred 1 mesec" + assert self.locale._format_relative("1 godina", "years", -1) == "pred 1 godina" + + def test_plurals_mk(self): + # Seconds + assert self.locale._format_timeframe("seconds", 0) == "0 sekundi" + assert self.locale._format_timeframe("seconds", 1) == "1 sekunda" + assert self.locale._format_timeframe("seconds", 2) == "2 sekundi" + assert self.locale._format_timeframe("seconds", 4) == "4 sekundi" + assert self.locale._format_timeframe("seconds", 5) == "5 sekundi" + assert self.locale._format_timeframe("seconds", 21) == "21 sekunda" + assert self.locale._format_timeframe("seconds", 22) == "22 sekundi" + assert self.locale._format_timeframe("seconds", 25) == "25 sekundi" + + # Minutes + assert self.locale._format_timeframe("minutes", 0) == "0 minuti" + assert self.locale._format_timeframe("minutes", 1) == "1 minuta" + assert self.locale._format_timeframe("minutes", 2) == "2 minuti" + assert self.locale._format_timeframe("minutes", 4) == "4 minuti" + assert self.locale._format_timeframe("minutes", 5) == "5 minuti" + assert self.locale._format_timeframe("minutes", 21) == "21 minuta" + assert self.locale._format_timeframe("minutes", 22) == "22 minuti" + assert self.locale._format_timeframe("minutes", 25) == "25 minuti" + + # Hours + assert self.locale._format_timeframe("hours", 0) == "0 saati" + assert self.locale._format_timeframe("hours", 1) == "1 saat" + assert self.locale._format_timeframe("hours", 2) == "2 saati" + assert self.locale._format_timeframe("hours", 4) == "4 saati" + assert self.locale._format_timeframe("hours", 5) == "5 saati" + assert self.locale._format_timeframe("hours", 21) == "21 saat" + assert self.locale._format_timeframe("hours", 22) == "22 saati" + assert self.locale._format_timeframe("hours", 25) == "25 saati" + + # Days + assert self.locale._format_timeframe("days", 0) == "0 dena" + assert self.locale._format_timeframe("days", 1) == "1 den" + assert self.locale._format_timeframe("days", 2) == "2 dena" + assert self.locale._format_timeframe("days", 3) == "3 dena" + assert self.locale._format_timeframe("days", 21) == "21 den" + + # Weeks + assert self.locale._format_timeframe("weeks", 0) == "0 nedeli" + assert self.locale._format_timeframe("weeks", 1) == "1 nedela" + assert self.locale._format_timeframe("weeks", 2) == "2 nedeli" + assert self.locale._format_timeframe("weeks", 4) == "4 nedeli" + assert self.locale._format_timeframe("weeks", 5) == "5 nedeli" + assert self.locale._format_timeframe("weeks", 21) == "21 nedela" + assert self.locale._format_timeframe("weeks", 22) == "22 nedeli" + assert self.locale._format_timeframe("weeks", 25) == "25 nedeli" + + # Months + assert self.locale._format_timeframe("months", 0) == "0 meseci" + assert self.locale._format_timeframe("months", 1) == "1 mesec" + assert self.locale._format_timeframe("months", 2) == "2 meseci" + assert self.locale._format_timeframe("months", 4) == "4 meseci" + assert self.locale._format_timeframe("months", 5) == "5 meseci" + assert self.locale._format_timeframe("months", 21) == "21 mesec" + assert self.locale._format_timeframe("months", 22) == "22 meseci" + assert self.locale._format_timeframe("months", 25) == "25 meseci" + + # Years + assert self.locale._format_timeframe("years", 1) == "1 godina" + assert self.locale._format_timeframe("years", 2) == "2 godini" + assert self.locale._format_timeframe("years", 5) == "5 godini" + + def test_multi_describe_mk(self): + describe = self.locale.describe_multi + + fulltest = [("years", 5), ("weeks", 1), ("hours", 1), ("minutes", 6)] + assert describe(fulltest) == "za 5 godini 1 nedela 1 saat 6 minuti" + seconds4000_0days = [("days", 0), ("hours", 1), ("minutes", 6)] + assert describe(seconds4000_0days) == "za 0 dena 1 saat 6 minuti" + seconds4000 = [("hours", 1), ("minutes", 6)] + assert describe(seconds4000) == "za 1 saat 6 minuti" + assert describe(seconds4000, only_distance=True) == "1 saat 6 minuti" + seconds3700 = [("hours", 1), ("minutes", 1)] + assert describe(seconds3700) == "za 1 saat 1 minuta" + seconds300_0hours = [("hours", 0), ("minutes", 5)] + assert describe(seconds300_0hours) == "za 0 saati 5 minuti" + seconds300 = [("minutes", 5)] + assert describe(seconds300) == "za 5 minuti" + seconds60 = [("minutes", 1)] + assert describe(seconds60) == "za 1 minuta" + assert describe(seconds60, only_distance=True) == "1 minuta" + seconds60 = [("seconds", 1)] + assert describe(seconds60) == "za 1 sekunda" + assert describe(seconds60, only_distance=True) == "1 sekunda" + + @pytest.mark.usefixtures("time_2013_01_01") @pytest.mark.usefixtures("lang_locale") class TestHebrewLocale: @@ -1111,9 +1228,9 @@ def test_format_timeframe(self): # Second(s) assert self.locale._format_timeframe("second", -1) == "sekunti" - assert self.locale._format_timeframe("second", 1) == "sekunti" - assert self.locale._format_timeframe("seconds", -2) == "2 muutama sekunti" - assert self.locale._format_timeframe("seconds", 2) == "2 muutaman sekunnin" + assert self.locale._format_timeframe("second", 1) == "sekunnin" + assert self.locale._format_timeframe("seconds", -2) == "2 sekuntia" + assert self.locale._format_timeframe("seconds", 2) == "2 sekunnin" # Minute(s) assert self.locale._format_timeframe("minute", -1) == "minuutti" @@ -1129,7 +1246,7 @@ def test_format_timeframe(self): # Day(s) assert self.locale._format_timeframe("day", -1) == "päivä" - assert self.locale._format_timeframe("day", 1) == "päivä" + assert self.locale._format_timeframe("day", 1) == "päivän" assert self.locale._format_timeframe("days", -2) == "2 päivää" assert self.locale._format_timeframe("days", 2) == "2 päivän" @@ -1269,6 +1386,12 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("days", -2) == "2 nappal" assert self.locale._format_timeframe("days", 2) == "2 nap" + # Week(s) + assert self.locale._format_timeframe("week", -1) == "egy héttel" + assert self.locale._format_timeframe("week", 1) == "egy hét" + assert self.locale._format_timeframe("weeks", -2) == "2 héttel" + assert self.locale._format_timeframe("weeks", 2) == "2 hét" + # Month(s) assert self.locale._format_timeframe("month", -1) == "egy hónappal" assert self.locale._format_timeframe("month", 1) == "egy hónap" @@ -1294,6 +1417,63 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1a" +@pytest.mark.usefixtures("lang_locale") +class TestLaotianLocale: + def test_year_full(self): + assert self.locale.year_full(2015) == "2558" + + def test_year_abbreviation(self): + assert self.locale.year_abbreviation(2015) == "58" + + def test_format_relative_now(self): + result = self.locale._format_relative("ດຽວນີ້", "now", 0) + assert result == "ດຽວນີ້" + + def test_format_relative_past(self): + result = self.locale._format_relative("1 ຊົ່ວໂມງ", "hour", 1) + assert result == "ໃນ 1 ຊົ່ວໂມງ" + result = self.locale._format_relative("{0} ຊົ່ວໂມງ", "hours", 2) + assert result == "ໃນ {0} ຊົ່ວໂມງ" + result = self.locale._format_relative("ວິນາທີ", "seconds", 42) + assert result == "ໃນວິນາທີ" + + def test_format_relative_future(self): + result = self.locale._format_relative("1 ຊົ່ວໂມງ", "hour", -1) + assert result == "1 ຊົ່ວໂມງ ກ່ອນຫນ້ານີ້" + + def test_format_timeframe(self): + # minute(s) + assert self.locale._format_timeframe("minute", 1) == "ນາທີ" + assert self.locale._format_timeframe("minute", -1) == "ນາທີ" + assert self.locale._format_timeframe("minutes", 7) == "7 ນາທີ" + assert self.locale._format_timeframe("minutes", -20) == "20 ນາທີ" + # day(s) + assert self.locale._format_timeframe("day", 1) == "ມື້" + assert self.locale._format_timeframe("day", -1) == "ມື້" + assert self.locale._format_timeframe("days", 7) == "7 ມື້" + assert self.locale._format_timeframe("days", -20) == "20 ມື້" + # week(s) + assert self.locale._format_timeframe("week", 1) == "ອາທິດ" + assert self.locale._format_timeframe("week", -1) == "ອາທິດ" + assert self.locale._format_timeframe("weeks", 7) == "7 ອາທິດ" + assert self.locale._format_timeframe("weeks", -20) == "20 ອາທິດ" + # month(s) + assert self.locale._format_timeframe("month", 1) == "ເດືອນ" + assert self.locale._format_timeframe("month", -1) == "ເດືອນ" + assert self.locale._format_timeframe("months", 7) == "7 ເດືອນ" + assert self.locale._format_timeframe("months", -20) == "20 ເດືອນ" + # year(s) + assert self.locale._format_timeframe("year", 1) == "ປີ" + assert self.locale._format_timeframe("year", -1) == "ປີ" + assert self.locale._format_timeframe("years", 7) == "7 ປີ" + assert self.locale._format_timeframe("years", -20) == "20 ປີ" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "ວັນເສົາ" + assert self.locale.day_abbreviation(dt.isoweekday()) == "ວັນເສົາ" + + @pytest.mark.usefixtures("lang_locale") class TestThaiLocale: def test_year_full(self): @@ -1318,6 +1498,44 @@ def test_format_relative_future(self): result = self.locale._format_relative("1 ชั่วโมง", "hour", -1) assert result == "1 ชั่วโมง ที่ผ่านมา" + def test_format_timeframe(self): + # Now + assert self.locale._format_timeframe("now", 0) == "ขณะนี้" + # Second(s) + assert self.locale._format_timeframe("second", 1) == "วินาที" + assert self.locale._format_timeframe("seconds", 2) == "2 วินาที" + # Minute(s) + assert self.locale._format_timeframe("minute", 1) == "นาที" + assert self.locale._format_timeframe("minutes", 5) == "5 นาที" + # Hour(s) + assert self.locale._format_timeframe("hour", 1) == "ชั่วโมง" + assert self.locale._format_timeframe("hours", 3) == "3 ชั่วโมง" + # Day(s) + assert self.locale._format_timeframe("day", 1) == "วัน" + assert self.locale._format_timeframe("days", 7) == "7 วัน" + # Week(s) + assert self.locale._format_timeframe("week", 1) == "สัปดาห์" + assert self.locale._format_timeframe("weeks", 2) == "2 สัปดาห์" + # Month(s) + assert self.locale._format_timeframe("month", 1) == "เดือน" + assert self.locale._format_timeframe("months", 4) == "4 เดือน" + # Year(s) + assert self.locale._format_timeframe("year", 1) == "ปี" + assert self.locale._format_timeframe("years", 10) == "10 ปี" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 0) + # These values depend on the actual Thai locale implementation + # Replace with correct Thai names if available + assert self.locale.day_name(dt.isoweekday()) == "วันเสาร์" + assert self.locale.day_abbreviation(dt.isoweekday()) == "ส." + + def test_ordinal_number(self): + # Thai ordinal numbers are not commonly used, but test for fallback + assert self.locale.ordinal_number(1) == "1" + assert self.locale.ordinal_number(10) == "10" + assert self.locale.ordinal_number(0) == "0" + @pytest.mark.usefixtures("lang_locale") class TestBengaliLocale: @@ -1331,13 +1549,12 @@ def test_ordinal_number(self): assert self.locale._ordinal_number(10) == "10ম" assert self.locale._ordinal_number(11) == "11তম" assert self.locale._ordinal_number(42) == "42তম" - assert self.locale._ordinal_number(-1) is None + assert self.locale._ordinal_number(-1) == "" @pytest.mark.usefixtures("lang_locale") class TestRomanianLocale: def test_timeframes(self): - assert self.locale._format_timeframe("hours", 2) == "2 ore" assert self.locale._format_timeframe("months", 2) == "2 luni" @@ -1372,11 +1589,11 @@ def test_relative_timeframes(self): @pytest.mark.usefixtures("lang_locale") class TestArabicLocale: def test_timeframes(self): - # single assert self.locale._format_timeframe("minute", 1) == "دقيقة" assert self.locale._format_timeframe("hour", 1) == "ساعة" assert self.locale._format_timeframe("day", 1) == "يوم" + assert self.locale._format_timeframe("week", 1) == "اسبوع" assert self.locale._format_timeframe("month", 1) == "شهر" assert self.locale._format_timeframe("year", 1) == "سنة" @@ -1384,6 +1601,7 @@ def test_timeframes(self): assert self.locale._format_timeframe("minutes", 2) == "دقيقتين" assert self.locale._format_timeframe("hours", 2) == "ساعتين" assert self.locale._format_timeframe("days", 2) == "يومين" + assert self.locale._format_timeframe("weeks", 2) == "اسبوعين" assert self.locale._format_timeframe("months", 2) == "شهرين" assert self.locale._format_timeframe("years", 2) == "سنتين" @@ -1391,17 +1609,47 @@ def test_timeframes(self): assert self.locale._format_timeframe("minutes", 3) == "3 دقائق" assert self.locale._format_timeframe("hours", 4) == "4 ساعات" assert self.locale._format_timeframe("days", 5) == "5 أيام" + assert self.locale._format_timeframe("weeks", 7) == "7 أسابيع" assert self.locale._format_timeframe("months", 6) == "6 أشهر" assert self.locale._format_timeframe("years", 10) == "10 سنوات" # more than ten assert self.locale._format_timeframe("minutes", 11) == "11 دقيقة" assert self.locale._format_timeframe("hours", 19) == "19 ساعة" + assert self.locale._format_timeframe("weeks", 20) == "20 اسبوع" assert self.locale._format_timeframe("months", 24) == "24 شهر" assert self.locale._format_timeframe("days", 50) == "50 يوم" assert self.locale._format_timeframe("years", 115) == "115 سنة" +@pytest.mark.usefixtures("lang_locale") +class TestFarsiLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "اکنون" + # single + assert self.locale._format_timeframe("minute", 1) == "یک دقیقه" + assert self.locale._format_timeframe("hour", 1) == "یک ساعت" + assert self.locale._format_timeframe("day", 1) == "یک روز" + assert self.locale._format_timeframe("week", 1) == "یک هفته" + assert self.locale._format_timeframe("month", 1) == "یک ماه" + assert self.locale._format_timeframe("year", 1) == "یک سال" + + # double + assert self.locale._format_timeframe("minutes", 2) == "2 دقیقه" + assert self.locale._format_timeframe("hours", 2) == "2 ساعت" + assert self.locale._format_timeframe("days", 2) == "2 روز" + assert self.locale._format_timeframe("weeks", 2) == "2 هفته" + assert self.locale._format_timeframe("months", 2) == "2 ماه" + assert self.locale._format_timeframe("years", 2) == "2 سال" + + def test_weekday(self): + fa = arrow.Arrow(2024, 10, 25, 17, 30, 00) + assert self.locale.day_name(fa.isoweekday()) == "جمعه" + assert self.locale.day_abbreviation(fa.isoweekday()) == "جمعه" + assert self.locale.month_name(fa.month) == "اکتبر" + assert self.locale.month_abbreviation(fa.month) == "اکتبر" + + @pytest.mark.usefixtures("lang_locale") class TestNepaliLocale: def test_format_timeframe(self): @@ -2349,14 +2597,14 @@ def test_format_relative(self): assert self.locale._format_relative("2시간", "hours", -2) == "2시간 전" assert self.locale._format_relative("하루", "day", -1) == "어제" assert self.locale._format_relative("2일", "days", -2) == "그제" - assert self.locale._format_relative("3일", "days", -3) == "그끄제" + assert self.locale._format_relative("3일", "days", -3) == "3일 전" assert self.locale._format_relative("4일", "days", -4) == "4일 전" assert self.locale._format_relative("1주", "week", -1) == "1주 전" assert self.locale._format_relative("2주", "weeks", -2) == "2주 전" assert self.locale._format_relative("한달", "month", -1) == "한달 전" assert self.locale._format_relative("2개월", "months", -2) == "2개월 전" assert self.locale._format_relative("1년", "year", -1) == "작년" - assert self.locale._format_relative("2년", "years", -2) == "제작년" + assert self.locale._format_relative("2년", "years", -2) == "재작년" assert self.locale._format_relative("3년", "years", -3) == "3년 전" def test_ordinal_number(self): @@ -2376,6 +2624,26 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(100) == "100번째" +@pytest.mark.usefixtures("lang_locale") +class TestDutchLocale: + def test_plurals(self): + assert self.locale._format_timeframe("now", 0) == "nu" + assert self.locale._format_timeframe("second", 1) == "een seconde" + assert self.locale._format_timeframe("seconds", 30) == "30 seconden" + assert self.locale._format_timeframe("minute", 1) == "een minuut" + assert self.locale._format_timeframe("minutes", 40) == "40 minuten" + assert self.locale._format_timeframe("hour", 1) == "een uur" + assert self.locale._format_timeframe("hours", 23) == "23 uur" + assert self.locale._format_timeframe("day", 1) == "een dag" + assert self.locale._format_timeframe("days", 12) == "12 dagen" + assert self.locale._format_timeframe("week", 1) == "een week" + assert self.locale._format_timeframe("weeks", 38) == "38 weken" + assert self.locale._format_timeframe("month", 1) == "een maand" + assert self.locale._format_timeframe("months", 11) == "11 maanden" + assert self.locale._format_timeframe("year", 1) == "een jaar" + assert self.locale._format_timeframe("years", 12) == "12 jaar" + + @pytest.mark.usefixtures("lang_locale") class TestJapaneseLocale: def test_format_timeframe(self): @@ -2431,22 +2699,18 @@ def test_ordinal_number(self): assert self.locale._ordinal_number(-1) == "" def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 ଘଣ୍ଟା" assert self.locale._format_timeframe("hour", 0) == "ଏକ ଘଣ୍ଟା" def test_format_relative_now(self): - result = self.locale._format_relative("ବର୍ତ୍ତମାନ", "now", 0) assert result == "ବର୍ତ୍ତମାନ" def test_format_relative_past(self): - result = self.locale._format_relative("ଏକ ଘଣ୍ଟା", "hour", 1) assert result == "ଏକ ଘଣ୍ଟା ପରେ" def test_format_relative_future(self): - result = self.locale._format_relative("ଏକ ଘଣ୍ଟା", "hour", -1) assert result == "ଏକ ଘଣ୍ଟା ପୂର୍ବେ" @@ -2675,13 +2939,11 @@ def test_format_relative_now(self): assert result == "දැන්" def test_format_relative_future(self): - result = self.locale._format_relative("පැයකින්", "පැය", 1) assert result == "පැයකින්" # (in) one hour def test_format_relative_past(self): - result = self.locale._format_relative("පැයක", "පැය", -1) assert result == "පැයකට පෙර" # an hour ago @@ -2785,24 +3047,20 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1." def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 timer" assert self.locale._format_timeframe("hour", 0) == "en time" def test_format_relative_now(self): - result = self.locale._format_relative("nå nettopp", "now", 0) assert result == "nå nettopp" def test_format_relative_past(self): - result = self.locale._format_relative("en time", "hour", 1) assert result == "om en time" def test_format_relative_future(self): - result = self.locale._format_relative("en time", "hour", -1) assert result == "for en time siden" @@ -2841,24 +3099,20 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1." def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 timar" assert self.locale._format_timeframe("hour", 0) == "ein time" def test_format_relative_now(self): - result = self.locale._format_relative("no nettopp", "now", 0) assert result == "no nettopp" def test_format_relative_past(self): - result = self.locale._format_relative("ein time", "hour", 1) assert result == "om ein time" def test_format_relative_future(self): - result = self.locale._format_relative("ein time", "hour", -1) assert result == "for ein time sidan" @@ -2980,13 +3234,11 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1ኛ" def test_format_relative_future(self): - result = self.locale._format_relative("በአንድ ሰዓት", "hour", 1) assert result == "በአንድ ሰዓት ውስጥ" # (in) one hour def test_format_relative_past(self): - result = self.locale._format_relative("ከአንድ ሰዓት", "hour", -1) assert result == "ከአንድ ሰዓት በፊት" # an hour ago @@ -2995,3 +3247,538 @@ def test_weekday(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) assert self.locale.day_name(dt.isoweekday()) == "ቅዳሜ" assert self.locale.day_abbreviation(dt.isoweekday()) == "ዓ" + + +@pytest.mark.usefixtures("lang_locale") +class TestArmenianLocale: + def test_describe(self): + assert self.locale.describe("now", only_distance=True) == "հիմա" + assert self.locale.describe("now", only_distance=False) == "հիմա" + + def test_meridians_hy(self): + assert self.locale.meridian(7, "A") == "Ամ" + assert self.locale.meridian(18, "A") == "պ.մ." + assert self.locale.meridian(10, "a") == "Ամ" + assert self.locale.meridian(22, "a") == "պ.մ." + + def test_format_timeframe(self): + # Second(s) + assert self.locale._format_timeframe("second", -1) == "վայրկյան" + assert self.locale._format_timeframe("second", 1) == "վայրկյան" + assert self.locale._format_timeframe("seconds", -3) == "3 վայրկյան" + assert self.locale._format_timeframe("seconds", 3) == "3 վայրկյան" + assert self.locale._format_timeframe("seconds", 30) == "30 վայրկյան" + + # Minute(s) + assert self.locale._format_timeframe("minute", -1) == "րոպե" + assert self.locale._format_timeframe("minute", 1) == "րոպե" + assert self.locale._format_timeframe("minutes", -4) == "4 րոպե" + assert self.locale._format_timeframe("minutes", 4) == "4 րոպե" + assert self.locale._format_timeframe("minutes", 40) == "40 րոպե" + + # Hour(s) + assert self.locale._format_timeframe("hour", -1) == "ժամ" + assert self.locale._format_timeframe("hour", 1) == "ժամ" + assert self.locale._format_timeframe("hours", -23) == "23 ժամ" + assert self.locale._format_timeframe("hours", 23) == "23 ժամ" + + # Day(s) + assert self.locale._format_timeframe("day", -1) == "օր" + assert self.locale._format_timeframe("day", 1) == "օր" + assert self.locale._format_timeframe("days", -12) == "12 օր" + assert self.locale._format_timeframe("days", 12) == "12 օր" + + # Month(s) + assert self.locale._format_timeframe("month", -1) == "ամիս" + assert self.locale._format_timeframe("month", 1) == "ամիս" + assert self.locale._format_timeframe("months", -2) == "2 ամիս" + assert self.locale._format_timeframe("months", 2) == "2 ամիս" + assert self.locale._format_timeframe("months", 11) == "11 ամիս" + + # Year(s) + assert self.locale._format_timeframe("year", -1) == "տարին" + assert self.locale._format_timeframe("year", 1) == "տարին" + assert self.locale._format_timeframe("years", -2) == "2 տարին" + assert self.locale._format_timeframe("years", 2) == "2 տարին" + assert self.locale._format_timeframe("years", 12) == "12 տարին" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "շաբաթ" + assert self.locale.day_abbreviation(dt.isoweekday()) == "շաբ." + + +@pytest.mark.usefixtures("lang_locale") +class TestUzbekLocale: + def test_singles_mk(self): + assert self.locale._format_timeframe("second", 1) == "bir soniya" + assert self.locale._format_timeframe("minute", 1) == "bir daqiqa" + assert self.locale._format_timeframe("hour", 1) == "bir soat" + assert self.locale._format_timeframe("day", 1) == "bir kun" + assert self.locale._format_timeframe("week", 1) == "bir hafta" + assert self.locale._format_timeframe("month", 1) == "bir oy" + assert self.locale._format_timeframe("year", 1) == "bir yil" + + def test_describe_mk(self): + assert self.locale.describe("second", only_distance=True) == "bir soniya" + assert ( + self.locale.describe("second", only_distance=False) == "bir soniyadan keyin" + ) + assert self.locale.describe("minute", only_distance=True) == "bir daqiqa" + assert ( + self.locale.describe("minute", only_distance=False) == "bir daqiqadan keyin" + ) + assert self.locale.describe("hour", only_distance=True) == "bir soat" + assert self.locale.describe("hour", only_distance=False) == "bir soatdan keyin" + assert self.locale.describe("day", only_distance=True) == "bir kun" + assert self.locale.describe("day", only_distance=False) == "bir kundan keyin" + assert self.locale.describe("week", only_distance=True) == "bir hafta" + assert self.locale.describe("week", only_distance=False) == "bir haftadan keyin" + assert self.locale.describe("month", only_distance=True) == "bir oy" + assert self.locale.describe("month", only_distance=False) == "bir oydan keyin" + assert self.locale.describe("year", only_distance=True) == "bir yil" + assert self.locale.describe("year", only_distance=False) == "bir yildan keyin" + + def test_relative_mk(self): + assert self.locale._format_relative("hozir", "now", 0) == "hozir" + assert ( + self.locale._format_relative("1 soniya", "seconds", 1) + == "1 soniyadan keyin" + ) + assert ( + self.locale._format_relative("1 soniya", "seconds", -1) + == "1 soniyadan avval" + ) + assert ( + self.locale._format_relative("1 daqiqa", "minutes", 1) + == "1 daqiqadan keyin" + ) + assert ( + self.locale._format_relative("1 daqiqa", "minutes", -1) + == "1 daqiqadan avval" + ) + assert self.locale._format_relative("1 soat", "hours", 1) == "1 soatdan keyin" + assert self.locale._format_relative("1 soat", "hours", -1) == "1 soatdan avval" + assert self.locale._format_relative("1 kun", "days", 1) == "1 kundan keyin" + assert self.locale._format_relative("1 kun", "days", -1) == "1 kundan avval" + assert self.locale._format_relative("1 hafta", "weeks", 1) == "1 haftadan keyin" + assert ( + self.locale._format_relative("1 hafta", "weeks", -1) == "1 haftadan avval" + ) + assert self.locale._format_relative("1 oy", "months", 1) == "1 oydan keyin" + assert self.locale._format_relative("1 oy", "months", -1) == "1 oydan avval" + assert self.locale._format_relative("1 yil", "years", 1) == "1 yildan keyin" + assert self.locale._format_relative("1 yil", "years", -1) == "1 yildan avval" + + def test_plurals_mk(self): + assert self.locale._format_timeframe("now", 0) == "hozir" + assert self.locale._format_timeframe("second", 1) == "bir soniya" + assert self.locale._format_timeframe("seconds", 30) == "30 soniya" + assert self.locale._format_timeframe("minute", 1) == "bir daqiqa" + assert self.locale._format_timeframe("minutes", 40) == "40 daqiqa" + assert self.locale._format_timeframe("hour", 1) == "bir soat" + assert self.locale._format_timeframe("hours", 23) == "23 soat" + assert self.locale._format_timeframe("days", 12) == "12 kun" + assert self.locale._format_timeframe("week", 1) == "bir hafta" + assert self.locale._format_timeframe("weeks", 38) == "38 hafta" + assert self.locale._format_timeframe("month", 1) == "bir oy" + assert self.locale._format_timeframe("months", 11) == "11 oy" + assert self.locale._format_timeframe("year", 1) == "bir yil" + assert self.locale._format_timeframe("years", 12) == "12 yil" + + +@pytest.mark.usefixtures("lang_locale") +class TestGreekLocale: + def test_format_relative_future(self): + result = self.locale._format_relative("μία ώρα", "ώρα", -1) + + assert result == "πριν από μία ώρα" # an hour ago + + def test_month_abbreviation(self): + assert self.locale.month_abbreviations[5] == "Μαΐ" + + def test_format_timeframe(self): + assert self.locale._format_timeframe("second", 1) == "ένα δευτερόλεπτο" + assert self.locale._format_timeframe("seconds", 3) == "3 δευτερόλεπτα" + assert self.locale._format_timeframe("day", 1) == "μία ημέρα" + assert self.locale._format_timeframe("days", 6) == "6 ημέρες" + + +@pytest.mark.usefixtures("lang_locale") +class TestAfrikaansLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "nou" + + # singular + assert self.locale._format_timeframe("second", 1) == "n sekonde" + assert self.locale._format_timeframe("minute", 1) == "minuut" + assert self.locale._format_timeframe("hour", 1) == "uur" + assert self.locale._format_timeframe("day", 1) == "een dag" + assert self.locale._format_timeframe("week", 1) == "een week" + assert self.locale._format_timeframe("month", 1) == "een maand" + assert self.locale._format_timeframe("year", 1) == "een jaar" + + # plural + assert self.locale._format_timeframe("seconds", 2) == "2 sekondes" + assert self.locale._format_timeframe("minutes", 2) == "2 minute" + assert self.locale._format_timeframe("hours", 2) == "2 ure" + assert self.locale._format_timeframe("days", 2) == "2 dae" + assert self.locale._format_timeframe("weeks", 2) == "2 weke" + assert self.locale._format_timeframe("months", 2) == "2 maande" + assert self.locale._format_timeframe("years", 2) == "2 jaar" + + assert self.locale._format_timeframe("seconds", 5) == "5 sekondes" + assert self.locale._format_timeframe("minutes", 5) == "5 minute" + assert self.locale._format_timeframe("hours", 5) == "5 ure" + assert self.locale._format_timeframe("days", 5) == "5 dae" + assert self.locale._format_timeframe("months", 5) == "5 maande" + assert self.locale._format_timeframe("years", 5) == "5 jaar" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Saterdag" + assert self.locale.day_abbreviation(dt.isoweekday()) == "Za" + + assert self.locale.month_name(dt.month) == "April" + assert self.locale.month_abbreviation(dt.month) == "Apr" + + def test_format_relative_past(self): + assert self.locale._format_relative("uur", "hour", -1) == "uur gelede" + + def test_format_relative_future(self): + assert self.locale._format_relative("uur", "hour", 1) == "in uur" + + +@pytest.mark.usefixtures("lang_locale") +class TestAlgeriaTunisiaArabicLocale: + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "السبت" + assert self.locale.day_abbreviation(dt.isoweekday()) == "سبت" + + assert self.locale.month_name(dt.month) == "أفريل" + assert self.locale.month_abbreviation(dt.month) == "أفريل" + + def test_format_relative_past(self): + assert self.locale._format_relative("ساعة", "hour", -1) == "منذ ساعة" + + def test_format_relative_future(self): + assert self.locale._format_relative("ساعة", "hour", 1) == "خلال ساعة" + + +@pytest.mark.usefixtures("lang_locale") +class TestAustrianLocale: + def test_weekday(self): + dt = arrow.Arrow(2015, 1, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Sonntag" + assert self.locale.day_abbreviation(dt.isoweekday()) == "So" + + assert self.locale.month_name(dt.month) == "Jänner" + assert self.locale.month_abbreviation(dt.month) == "Jan" + + +@pytest.mark.usefixtures("lang_locale") +class TestBasqueLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "Orain" + assert self.locale._format_timeframe("second", 1) == "segundo bat" + assert self.locale._format_timeframe("minute", 1) == "minutu bat" + assert self.locale._format_timeframe("hour", 1) == "ordu bat" + assert self.locale._format_timeframe("day", 1) == "egun bat" + assert self.locale._format_timeframe("month", 1) == "hilabete bat" + assert self.locale._format_timeframe("year", 1) == "urte bat" + + assert self.locale._format_timeframe("seconds", 2) == "2 segundu" + assert self.locale._format_timeframe("minutes", 2) == "2 minutu" + assert self.locale._format_timeframe("hours", 2) == "2 ordu" + assert self.locale._format_timeframe("days", 2) == "2 egun" + assert self.locale._format_timeframe("months", 2) == "2 hilabet" + assert self.locale._format_timeframe("years", 2) == "2 urte" + + assert self.locale._format_timeframe("seconds", 5) == "5 segundu" + assert self.locale._format_timeframe("minutes", 5) == "5 minutu" + assert self.locale._format_timeframe("hours", 5) == "5 ordu" + assert self.locale._format_timeframe("days", 5) == "5 egun" + assert self.locale._format_timeframe("months", 5) == "5 hilabet" + assert self.locale._format_timeframe("years", 5) == "5 urte" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "larunbata" + assert self.locale.day_abbreviation(dt.isoweekday()) == "lr" + + assert self.locale.month_name(dt.month) == "apirilak" + assert self.locale.month_abbreviation(dt.month) == "api" + + def test_format_relative_past(self): + assert self.locale._format_relative("ordu bat", "hour", -1) == "duela ordu bat" + + def test_format_relative_future(self): + assert self.locale._format_relative("ordu bat", "hour", 1) == "ordu bat" + + +@pytest.mark.usefixtures("lang_locale") +class TestBelarusianLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "зараз" + assert self.locale._format_timeframe("second", 1) == "секунду" + assert self.locale._format_timeframe("minute", 1) == "хвіліну" + assert self.locale._format_timeframe("hour", 1) == "гадзіну" + assert self.locale._format_timeframe("day", 1) == "дзень" + assert self.locale._format_timeframe("month", 1) == "месяц" + assert self.locale._format_timeframe("year", 1) == "год" + + assert self.locale._format_timeframe("seconds", 2) == "2 некалькі секунд" + assert self.locale._format_timeframe("minutes", 2) == "2 хвіліны" + assert self.locale._format_timeframe("hours", 2) == "2 гадзіны" + assert self.locale._format_timeframe("days", 2) == "2 дні" + assert self.locale._format_timeframe("months", 2) == "2 месяцы" + assert self.locale._format_timeframe("years", 2) == "2 гады" + + assert self.locale._format_timeframe("seconds", 5) == "5 некалькі секунд" + assert self.locale._format_timeframe("minutes", 5) == "5 хвілін" + assert self.locale._format_timeframe("hours", 5) == "5 гадзін" + assert self.locale._format_timeframe("days", 5) == "5 дзён" + assert self.locale._format_timeframe("months", 5) == "5 месяцаў" + assert self.locale._format_timeframe("years", 5) == "5 гадоў" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "субота" + assert self.locale.day_abbreviation(dt.isoweekday()) == "сб" + + assert self.locale.month_name(dt.month) == "красавіка" + assert self.locale.month_abbreviation(dt.month) == "крас" + + def test_format_relative_past(self): + assert self.locale._format_relative("гадзіну", "hour", -1) == "гадзіну таму" + + def test_format_relative_future(self): + assert self.locale._format_relative("гадзіну", "hour", 1) == "праз гадзіну" + + +@pytest.mark.usefixtures("lang_locale") +class TestLevantArabicLocale: + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.month_name(dt.month) == "نيسان" + assert self.locale.month_abbreviation(dt.month) == "نيسان" + + +@pytest.mark.usefixtures("lang_locale") +class TestMauritaniaArabicLocale: + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.month_name(dt.month) == "إبريل" + assert self.locale.month_abbreviation(dt.month) == "إبريل" + + +@pytest.mark.usefixtures("lang_locale") +class TestMoroccoArabicLocale: + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.month_name(dt.month) == "أبريل" + assert self.locale.month_abbreviation(dt.month) == "أبريل" + + +@pytest.mark.usefixtures("lang_locale") +class TestRomanshLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "en quest mument" + assert self.locale._format_timeframe("second", 1) == "in secunda" + assert self.locale._format_timeframe("minute", 1) == "ina minuta" + assert self.locale._format_timeframe("hour", 1) == "in'ura" + assert self.locale._format_timeframe("day", 1) == "in di" + assert self.locale._format_timeframe("month", 1) == "in mais" + assert self.locale._format_timeframe("year", 1) == "in onn" + + assert self.locale._format_timeframe("seconds", 2) == "2 secundas" + assert self.locale._format_timeframe("minutes", 2) == "2 minutas" + assert self.locale._format_timeframe("hours", 2) == "2 ura" + assert self.locale._format_timeframe("days", 2) == "2 dis" + assert self.locale._format_timeframe("months", 2) == "2 mais" + assert self.locale._format_timeframe("years", 2) == "2 onns" + + assert self.locale._format_timeframe("seconds", 5) == "5 secundas" + assert self.locale._format_timeframe("minutes", 5) == "5 minutas" + assert self.locale._format_timeframe("hours", 5) == "5 ura" + assert self.locale._format_timeframe("days", 5) == "5 dis" + assert self.locale._format_timeframe("months", 5) == "5 mais" + assert self.locale._format_timeframe("years", 5) == "5 onns" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "sonda" + assert self.locale.day_abbreviation(dt.isoweekday()) == "so" + + assert self.locale.month_name(dt.month) == "avrigl" + assert self.locale.month_abbreviation(dt.month) == "avr" + + def test_format_relative_past(self): + assert self.locale._format_relative("in'ura", "hour", -1) == "avant in'ura" + + def test_format_relative_future(self): + assert self.locale._format_relative("in'ura", "hour", 1) == "en in'ura" + + +@pytest.mark.usefixtures("lang_locale") +class TestSlovenianLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "zdaj" + assert self.locale._format_timeframe("second", 1) == "sekundo" + assert self.locale._format_timeframe("minute", 1) == "minuta" + assert self.locale._format_timeframe("hour", 1) == "uro" + assert self.locale._format_timeframe("day", 1) == "dan" + assert self.locale._format_timeframe("month", 1) == "mesec" + assert self.locale._format_timeframe("year", 1) == "leto" + + assert self.locale._format_timeframe("seconds", 2) == "2 sekund" + assert self.locale._format_timeframe("minutes", 2) == "2 minutami" + assert self.locale._format_timeframe("hours", 2) == "2 ur" + assert self.locale._format_timeframe("days", 2) == "2 dni" + assert self.locale._format_timeframe("months", 2) == "2 mesecev" + assert self.locale._format_timeframe("years", 2) == "2 let" + + assert self.locale._format_timeframe("seconds", 5) == "5 sekund" + assert self.locale._format_timeframe("minutes", 5) == "5 minutami" + assert self.locale._format_timeframe("hours", 5) == "5 ur" + assert self.locale._format_timeframe("days", 5) == "5 dni" + assert self.locale._format_timeframe("months", 5) == "5 mesecev" + assert self.locale._format_timeframe("years", 5) == "5 let" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Sobota" + assert self.locale.day_abbreviation(dt.isoweekday()) == "Sob" + + assert self.locale.month_name(dt.month) == "April" + assert self.locale.month_abbreviation(dt.month) == "Apr" + + def test_format_relative_past(self): + assert self.locale._format_relative("in'ura", "hour", -1) == "pred in'ura" + + def test_format_relative_future(self): + assert self.locale._format_relative("in'ura", "hour", 1) == "čez in'ura" + + +@pytest.mark.usefixtures("lang_locale") +class TestUkrainianLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "зараз" + assert self.locale._format_timeframe("second", 1) == "секунда" + assert self.locale._format_timeframe("minute", 1) == "хвилину" + assert self.locale._format_timeframe("hour", 1) == "годину" + assert self.locale._format_timeframe("day", 1) == "день" + assert self.locale._format_timeframe("month", 1) == "місяць" + assert self.locale._format_timeframe("year", 1) == "рік" + + assert self.locale._format_timeframe("seconds", 2) == "2 кілька секунд" + assert self.locale._format_timeframe("minutes", 2) == "2 хвилини" + assert self.locale._format_timeframe("hours", 2) == "2 години" + assert self.locale._format_timeframe("days", 2) == "2 дні" + assert self.locale._format_timeframe("months", 2) == "2 місяці" + assert self.locale._format_timeframe("years", 2) == "2 роки" + + assert self.locale._format_timeframe("seconds", 5) == "5 кілька секунд" + assert self.locale._format_timeframe("minutes", 5) == "5 хвилин" + assert self.locale._format_timeframe("hours", 5) == "5 годин" + assert self.locale._format_timeframe("days", 5) == "5 днів" + assert self.locale._format_timeframe("months", 5) == "5 місяців" + assert self.locale._format_timeframe("years", 5) == "5 років" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "субота" + assert self.locale.day_abbreviation(dt.isoweekday()) == "сб" + + assert self.locale.month_name(dt.month) == "квітня" + assert self.locale.month_abbreviation(dt.month) == "квіт" + + def test_format_relative_past(self): + assert self.locale._format_relative("годину", "hour", -1) == "годину тому" + + def test_format_relative_future(self): + assert self.locale._format_relative("годину", "hour", 1) == "за годину" + + +@pytest.mark.usefixtures("lang_locale") +class TestVietnameseLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "hiện tại" + assert self.locale._format_timeframe("second", 1) == "một giây" + assert self.locale._format_timeframe("minute", 1) == "một phút" + assert self.locale._format_timeframe("hour", 1) == "một giờ" + assert self.locale._format_timeframe("day", 1) == "một ngày" + assert self.locale._format_timeframe("week", 1) == "một tuần" + assert self.locale._format_timeframe("month", 1) == "một tháng" + assert self.locale._format_timeframe("year", 1) == "một năm" + + assert self.locale._format_timeframe("seconds", 2) == "2 giây" + assert self.locale._format_timeframe("minutes", 2) == "2 phút" + assert self.locale._format_timeframe("hours", 2) == "2 giờ" + assert self.locale._format_timeframe("days", 2) == "2 ngày" + assert self.locale._format_timeframe("weeks", 2) == "2 tuần" + assert self.locale._format_timeframe("months", 2) == "2 tháng" + assert self.locale._format_timeframe("years", 2) == "2 năm" + + assert self.locale._format_timeframe("seconds", 5) == "5 giây" + assert self.locale._format_timeframe("minutes", 5) == "5 phút" + assert self.locale._format_timeframe("hours", 5) == "5 giờ" + assert self.locale._format_timeframe("days", 5) == "5 ngày" + assert self.locale._format_timeframe("weeks", 5) == "5 tuần" + assert self.locale._format_timeframe("months", 5) == "5 tháng" + assert self.locale._format_timeframe("years", 5) == "5 năm" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Thứ Bảy" + assert self.locale.day_abbreviation(dt.isoweekday()) == "Thứ 7" + + assert self.locale.month_name(dt.month) == "Tháng Tư" + assert self.locale.month_abbreviation(dt.month) == "Tháng 4" + + def test_format_relative_past(self): + assert self.locale._format_relative("một giờ", "hour", -1) == "một giờ trước" + + def test_format_relative_future(self): + assert self.locale._format_relative("một giờ", "hour", 1) == "một giờ nữa" + + +@pytest.mark.usefixtures("lang_locale") +class TestCatalanLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "Ara mateix" + assert self.locale._format_timeframe("second", 1) == "un segon" + assert self.locale._format_timeframe("minute", 1) == "un minut" + assert self.locale._format_timeframe("hour", 1) == "una hora" + assert self.locale._format_timeframe("day", 1) == "un dia" + assert self.locale._format_timeframe("month", 1) == "un mes" + assert self.locale._format_timeframe("year", 1) == "un any" + + assert self.locale._format_timeframe("seconds", 2) == "2 segons" + assert self.locale._format_timeframe("minutes", 2) == "2 minuts" + assert self.locale._format_timeframe("hours", 2) == "2 hores" + assert self.locale._format_timeframe("days", 2) == "2 dies" + assert self.locale._format_timeframe("months", 2) == "2 mesos" + assert self.locale._format_timeframe("years", 2) == "2 anys" + + assert self.locale._format_timeframe("seconds", 5) == "5 segons" + assert self.locale._format_timeframe("minutes", 5) == "5 minuts" + assert self.locale._format_timeframe("hours", 5) == "5 hores" + assert self.locale._format_timeframe("days", 5) == "5 dies" + assert self.locale._format_timeframe("months", 5) == "5 mesos" + assert self.locale._format_timeframe("years", 5) == "5 anys" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "dissabte" + assert self.locale.day_abbreviation(dt.isoweekday()) == "ds." + + assert self.locale.month_name(dt.month) == "abril" + assert self.locale.month_abbreviation(dt.month) == "abr." + + def test_format_relative_past(self): + assert self.locale._format_relative("una hora", "hour", -1) == "Fa una hora" + + def test_format_relative_future(self): + assert self.locale._format_relative("una hora", "hour", 1) == "En una hora" diff --git a/tests/test_parser.py b/tests/test_parser.py index 4a4cfe41..7038d880 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,11 +1,16 @@ import calendar import os import time -from datetime import datetime +from datetime import datetime, timedelta, timezone import pytest from dateutil import tz +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo + import arrow from arrow import formatter, parser from arrow.constants import MAX_TIMESTAMP_US @@ -83,7 +88,6 @@ def test_parse_token_invalid_meridians(self): assert parts == {} def test_parser_no_caching(self, mocker): - mocked_parser = mocker.patch( "arrow.parser.DateTimeParser._generate_pattern_re", fmt="fmt_a" ) @@ -135,7 +139,6 @@ def test_parser_multiple_line_caching(self, mocker): assert mocked_parser.call_args_list[1] == mocker.call(fmt="fmt_b") def test_YY_and_YYYY_format_list(self): - assert self.parser.parse("15/01/19", ["DD/MM/YY", "DD/MM/YYYY"]) == datetime( 2019, 1, 15 ) @@ -145,24 +148,18 @@ def test_YY_and_YYYY_format_list(self): 2019, 1, 15 ) - assert ( - self.parser.parse( - "15/01/2019T04:05:06.789120Z", - ["D/M/YYThh:mm:ss.SZ", "D/M/YYYYThh:mm:ss.SZ"], - ) - == datetime(2019, 1, 15, 4, 5, 6, 789120, tzinfo=tz.tzutc()) - ) + assert self.parser.parse( + "15/01/2019T04:05:06.789120Z", + ["D/M/YYThh:mm:ss.SZ", "D/M/YYYYThh:mm:ss.SZ"], + ) == datetime(2019, 1, 15, 4, 5, 6, 789120, tzinfo=tz.tzutc()) # regression test for issue #447 def test_timestamp_format_list(self): # should not match on the "X" token - assert ( - self.parser.parse( - "15 Jul 2000", - ["MM/DD/YYYY", "YYYY-MM-DD", "X", "DD-MMMM-YYYY", "D MMM YYYY"], - ) - == datetime(2000, 7, 15) - ) + assert self.parser.parse( + "15 Jul 2000", + ["MM/DD/YYYY", "YYYY-MM-DD", "X", "DD-MMMM-YYYY", "D MMM YYYY"], + ) == datetime(2000, 7, 15) with pytest.raises(ParserError): self.parser.parse("15 Jul", "X") @@ -171,7 +168,6 @@ def test_timestamp_format_list(self): @pytest.mark.usefixtures("dt_parser") class TestDateTimeParserParse: def test_parse_list(self, mocker): - mocker.patch( "arrow.parser.DateTimeParser._parse_multiformat", string="str", @@ -183,7 +179,6 @@ def test_parse_list(self, mocker): assert result == "result" def test_parse_unrecognized_token(self, mocker): - mocker.patch.dict("arrow.parser.DateTimeParser._BASE_INPUT_RE_MAP") del arrow.parser.DateTimeParser._BASE_INPUT_RE_MAP["YYYY"] @@ -193,17 +188,14 @@ def test_parse_unrecognized_token(self, mocker): _parser.parse("2013-01-01", "YYYY-MM-DD") def test_parse_parse_no_match(self): - with pytest.raises(ParserError): self.parser.parse("01-01", "YYYY-MM-DD") def test_parse_separators(self): - with pytest.raises(ParserError): self.parser.parse("1403549231", "YYYY-MM-DD") def test_parse_numbers(self): - self.expected = datetime(2012, 1, 1, 12, 5, 10) assert ( self.parser.parse("2012-01-01 12:05:10", "YYYY-MM-DD HH:mm:ss") @@ -211,19 +203,16 @@ def test_parse_numbers(self): ) def test_parse_am(self): - with pytest.raises(ParserMatchError): self.parser.parse("2021-01-30 14:00:00 AM", "YYYY-MM-DD HH:mm:ss A") def test_parse_year_two_digit(self): - self.expected = datetime(1979, 1, 1, 12, 5, 10) assert ( self.parser.parse("79-01-01 12:05:10", "YY-MM-DD HH:mm:ss") == self.expected ) def test_parse_timestamp(self): - tz_utc = tz.tzutc() float_timestamp = time.time() int_timestamp = int(float_timestamp) @@ -302,14 +291,12 @@ def test_parse_expanded_timestamp(self): self.parser.parse(f"{timestamp:f}", "x") def test_parse_names(self): - self.expected = datetime(2012, 1, 1) assert self.parser.parse("January 1, 2012", "MMMM D, YYYY") == self.expected assert self.parser.parse("Jan 1, 2012", "MMM D, YYYY") == self.expected def test_parse_pm(self): - self.expected = datetime(1, 1, 1, 13, 0, 0) assert self.parser.parse("1 pm", "H a") == self.expected assert self.parser.parse("1 pm", "h a") == self.expected @@ -327,20 +314,17 @@ def test_parse_pm(self): assert self.parser.parse("12 pm", "h A") == self.expected def test_parse_tz_hours_only(self): - self.expected = datetime(2025, 10, 17, 5, 30, 10, tzinfo=tz.tzoffset(None, 0)) parsed = self.parser.parse("2025-10-17 05:30:10+00", "YYYY-MM-DD HH:mm:ssZ") assert parsed == self.expected def test_parse_tz_zz(self): - self.expected = datetime(2013, 1, 1, tzinfo=tz.tzoffset(None, -7 * 3600)) assert self.parser.parse("2013-01-01 -07:00", "YYYY-MM-DD ZZ") == self.expected @pytest.mark.parametrize("full_tz_name", make_full_tz_list()) def test_parse_tz_name_zzz(self, full_tz_name): - - self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(full_tz_name)) + self.expected = datetime(2013, 1, 1, tzinfo=ZoneInfo(full_tz_name)) assert ( self.parser.parse(f"2013-01-01 {full_tz_name}", "YYYY-MM-DD ZZZ") == self.expected @@ -503,21 +487,15 @@ def test_parse_with_extra_words_at_start_and_end_valid(self): "2016-05-16T04:05:06.789120 blah", "YYYY-MM-DDThh:mm:ss.S" ) == datetime(2016, 5, 16, 4, 5, 6, 789120) - assert ( - self.parser.parse( - "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", - "YYYY-MM-DDThh:mm:ss.S", - ) - == datetime(2016, 5, 16, 4, 5, 6, 789120) - ) + assert self.parser.parse( + "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", + "YYYY-MM-DDThh:mm:ss.S", + ) == datetime(2016, 5, 16, 4, 5, 6, 789120) - assert ( - self.parser.parse( - "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", - "YYYY-MM-DD hh:mm:ss.S", - ) - == datetime(2016, 5, 16, 4, 5, 6, 789120) - ) + assert self.parser.parse( + "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", + "YYYY-MM-DD hh:mm:ss.S", + ) == datetime(2016, 5, 16, 4, 5, 6, 789120) # regression test for issue #701 # tests cases of a partial match surrounded by punctuation @@ -739,7 +717,6 @@ def test_parse_HH_24(self): self.parser.parse("2019-12-31T24:00:00.999999", "YYYY-MM-DDTHH:mm:ss.S") def test_parse_W(self): - assert self.parser.parse("2011-W05-4", "W") == datetime(2011, 2, 3) assert self.parser.parse("2011W054", "W") == datetime(2011, 2, 3) assert self.parser.parse("2011-W05", "W") == datetime(2011, 1, 31) @@ -783,14 +760,11 @@ def test_parse_normalize_whitespace(self): with pytest.raises(ParserError): self.parser.parse("Jun 1 2005 1:33PM", "MMM D YYYY H:mmA") - assert ( - self.parser.parse( - "\t 2013-05-05 T \n 12:30:45\t123456 \t \n", - "YYYY-MM-DD T HH:mm:ss S", - normalize_whitespace=True, - ) - == datetime(2013, 5, 5, 12, 30, 45, 123456) - ) + assert self.parser.parse( + "\t 2013-05-05 T \n 12:30:45\t123456 \t \n", + "YYYY-MM-DD T HH:mm:ss S", + normalize_whitespace=True, + ) == datetime(2013, 5, 5, 12, 30, 45, 123456) with pytest.raises(ParserError): self.parser.parse( @@ -809,31 +783,24 @@ def test_parse_normalize_whitespace(self): @pytest.mark.usefixtures("dt_parser_regex") class TestDateTimeParserRegex: def test_format_year(self): - assert self.format_regex.findall("YYYY-YY") == ["YYYY", "YY"] def test_format_month(self): - assert self.format_regex.findall("MMMM-MMM-MM-M") == ["MMMM", "MMM", "MM", "M"] def test_format_day(self): - assert self.format_regex.findall("DDDD-DDD-DD-D") == ["DDDD", "DDD", "DD", "D"] def test_format_hour(self): - assert self.format_regex.findall("HH-H-hh-h") == ["HH", "H", "hh", "h"] def test_format_minute(self): - assert self.format_regex.findall("mm-m") == ["mm", "m"] def test_format_second(self): - assert self.format_regex.findall("ss-s") == ["ss", "s"] def test_format_subsecond(self): - assert self.format_regex.findall("SSSSSS-SSSSS-SSSS-SSS-SS-S") == [ "SSSSSS", "SSSSS", @@ -844,23 +811,18 @@ def test_format_subsecond(self): ] def test_format_tz(self): - assert self.format_regex.findall("ZZZ-ZZ-Z") == ["ZZZ", "ZZ", "Z"] def test_format_am_pm(self): - assert self.format_regex.findall("A-a") == ["A", "a"] def test_format_timestamp(self): - assert self.format_regex.findall("X") == ["X"] def test_format_timestamp_milli(self): - assert self.format_regex.findall("x") == ["x"] def test_escape(self): - escape_regex = parser.DateTimeParser._ESCAPE_RE assert escape_regex.findall("2018-03-09 8 [h] 40 [hello]") == ["[h]", "[hello]"] @@ -884,7 +846,6 @@ def test_month_abbreviations(self): assert result == calendar.month_abbr[1:] def test_digits(self): - assert parser.DateTimeParser._ONE_OR_TWO_DIGIT_RE.findall("4-56") == ["4", "56"] assert parser.DateTimeParser._ONE_OR_TWO_OR_THREE_DIGIT_RE.findall( "4-56-789" @@ -932,9 +893,9 @@ def test_timestamp_milli(self): def test_time(self): time_re = parser.DateTimeParser._TIME_RE - time_seperators = [":", ""] + time_separators = [":", ""] - for sep in time_seperators: + for sep in time_separators: assert time_re.findall("12") == [("12", "", "", "", "")] assert time_re.findall(f"12{sep}35") == [("12", "35", "", "", "")] assert time_re.findall("12{sep}35{sep}46".format(sep=sep)) == [ @@ -955,7 +916,6 @@ def test_time(self): @pytest.mark.usefixtures("dt_parser") class TestDateTimeParserISO: def test_YYYY(self): - assert self.parser.parse_iso("2013") == datetime(2013, 1, 1) def test_YYYY_DDDD(self): @@ -983,7 +943,6 @@ def test_YYYY_DDDD(self): assert self.parser.parse_iso("2017-366") == datetime(2018, 1, 1) def test_YYYY_DDDD_HH_mm_ssZ(self): - assert self.parser.parse_iso("2013-036 04:05:06+01:00") == datetime( 2013, 2, 5, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600) ) @@ -997,67 +956,55 @@ def test_YYYY_MM_DDDD(self): self.parser.parse_iso("2014-05-125") def test_YYYY_MM(self): - for separator in DateTimeParser.SEPARATORS: assert self.parser.parse_iso(separator.join(("2013", "02"))) == datetime( 2013, 2, 1 ) def test_YYYY_MM_DD(self): - for separator in DateTimeParser.SEPARATORS: assert self.parser.parse_iso( separator.join(("2013", "02", "03")) ) == datetime(2013, 2, 3) def test_YYYY_MM_DDTHH_mmZ(self): - assert self.parser.parse_iso("2013-02-03T04:05+01:00") == datetime( 2013, 2, 3, 4, 5, tzinfo=tz.tzoffset(None, 3600) ) def test_YYYY_MM_DDTHH_mm(self): - assert self.parser.parse_iso("2013-02-03T04:05") == datetime(2013, 2, 3, 4, 5) def test_YYYY_MM_DDTHH(self): - assert self.parser.parse_iso("2013-02-03T04") == datetime(2013, 2, 3, 4) def test_YYYY_MM_DDTHHZ(self): - assert self.parser.parse_iso("2013-02-03T04+01:00") == datetime( 2013, 2, 3, 4, tzinfo=tz.tzoffset(None, 3600) ) def test_YYYY_MM_DDTHH_mm_ssZ(self): - assert self.parser.parse_iso("2013-02-03T04:05:06+01:00") == datetime( 2013, 2, 3, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600) ) def test_YYYY_MM_DDTHH_mm_ss(self): - assert self.parser.parse_iso("2013-02-03T04:05:06") == datetime( 2013, 2, 3, 4, 5, 6 ) def test_YYYY_MM_DD_HH_mmZ(self): - assert self.parser.parse_iso("2013-02-03 04:05+01:00") == datetime( 2013, 2, 3, 4, 5, tzinfo=tz.tzoffset(None, 3600) ) def test_YYYY_MM_DD_HH_mm(self): - assert self.parser.parse_iso("2013-02-03 04:05") == datetime(2013, 2, 3, 4, 5) def test_YYYY_MM_DD_HH(self): - assert self.parser.parse_iso("2013-02-03 04") == datetime(2013, 2, 3, 4) def test_invalid_time(self): - with pytest.raises(ParserError): self.parser.parse_iso("2013-02-03T") @@ -1068,19 +1015,16 @@ def test_invalid_time(self): self.parser.parse_iso("2013-02-03 04:05:06.") def test_YYYY_MM_DD_HH_mm_ssZ(self): - assert self.parser.parse_iso("2013-02-03 04:05:06+01:00") == datetime( 2013, 2, 3, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600) ) def test_YYYY_MM_DD_HH_mm_ss(self): - assert self.parser.parse_iso("2013-02-03 04:05:06") == datetime( 2013, 2, 3, 4, 5, 6 ) def test_YYYY_MM_DDTHH_mm_ss_S(self): - assert self.parser.parse_iso("2013-02-03T04:05:06.7") == datetime( 2013, 2, 3, 4, 5, 6, 700000 ) @@ -1115,7 +1059,6 @@ def test_YYYY_MM_DDTHH_mm_ss_S(self): ) def test_YYYY_MM_DDTHH_mm_ss_SZ(self): - assert self.parser.parse_iso("2013-02-03T04:05:06.7+01:00") == datetime( 2013, 2, 3, 4, 5, 6, 700000, tzinfo=tz.tzoffset(None, 3600) ) @@ -1141,7 +1084,6 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): ) def test_W(self): - assert self.parser.parse_iso("2011-W05-4") == datetime(2011, 2, 3) assert self.parser.parse_iso("2011-W05-4T14:17:01") == datetime( @@ -1155,7 +1097,6 @@ def test_W(self): ) def test_invalid_Z(self): - with pytest.raises(ParserError): self.parser.parse_iso("2013-02-03T04:05:06.78912z") @@ -1213,8 +1154,7 @@ def test_gnu_date(self): ) def test_isoformat(self): - - dt = datetime.utcnow() + dt = datetime.now(timezone.utc) assert self.parser.parse_iso(dt.isoformat()) == dt @@ -1374,35 +1314,42 @@ def test_midnight_end_day(self): @pytest.mark.usefixtures("tzinfo_parser") class TestTzinfoParser: def test_parse_local(self): + from datetime import datetime - assert self.parser.parse("local") == tz.tzlocal() + assert self.parser.parse("local") == datetime.now().astimezone().tzinfo def test_parse_utc(self): + assert self.parser.parse("utc") == timezone.utc + assert self.parser.parse("UTC") == timezone.utc - assert self.parser.parse("utc") == tz.tzutc() - assert self.parser.parse("UTC") == tz.tzutc() + def test_parse_utc_withoffset(self): + assert self.parser.parse("(UTC+01:00") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("(UTC-01:00") == timezone(timedelta(seconds=-3600)) + assert self.parser.parse("(UTC+01:00") == timezone(timedelta(seconds=3600)) + assert self.parser.parse( + "(UTC+01:00) Amsterdam, Berlin, Bern, Rom, Stockholm, Wien" + ) == timezone(timedelta(seconds=3600)) def test_parse_iso(self): + assert self.parser.parse("01:00") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("11:35") == timezone( + timedelta(seconds=11 * 3600 + 2100) + ) + assert self.parser.parse("+01:00") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("-01:00") == timezone(timedelta(seconds=-3600)) - assert self.parser.parse("01:00") == tz.tzoffset(None, 3600) - assert self.parser.parse("11:35") == tz.tzoffset(None, 11 * 3600 + 2100) - assert self.parser.parse("+01:00") == tz.tzoffset(None, 3600) - assert self.parser.parse("-01:00") == tz.tzoffset(None, -3600) - - assert self.parser.parse("0100") == tz.tzoffset(None, 3600) - assert self.parser.parse("+0100") == tz.tzoffset(None, 3600) - assert self.parser.parse("-0100") == tz.tzoffset(None, -3600) + assert self.parser.parse("0100") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("+0100") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("-0100") == timezone(timedelta(seconds=-3600)) - assert self.parser.parse("01") == tz.tzoffset(None, 3600) - assert self.parser.parse("+01") == tz.tzoffset(None, 3600) - assert self.parser.parse("-01") == tz.tzoffset(None, -3600) + assert self.parser.parse("01") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("+01") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("-01") == timezone(timedelta(seconds=-3600)) def test_parse_str(self): - - assert self.parser.parse("US/Pacific") == tz.gettz("US/Pacific") + assert self.parser.parse("US/Pacific") == ZoneInfo("US/Pacific") def test_parse_fails(self): - with pytest.raises(parser.ParserError): self.parser.parse("fail") @@ -1410,31 +1357,25 @@ def test_parse_fails(self): @pytest.mark.usefixtures("dt_parser") class TestDateTimeParserMonthName: def test_shortmonth_capitalized(self): - assert self.parser.parse("2013-Jan-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) def test_shortmonth_allupper(self): - assert self.parser.parse("2013-JAN-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) def test_shortmonth_alllower(self): - assert self.parser.parse("2013-jan-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) def test_month_capitalized(self): - assert self.parser.parse("2013-January-01", "YYYY-MMMM-DD") == datetime( 2013, 1, 1 ) def test_month_allupper(self): - assert self.parser.parse("2013-JANUARY-01", "YYYY-MMMM-DD") == datetime( 2013, 1, 1 ) def test_month_alllower(self): - assert self.parser.parse("2013-january-01", "YYYY-MMMM-DD") == datetime( 2013, 1, 1 ) @@ -1581,13 +1522,11 @@ def test_french(self): @pytest.mark.usefixtures("dt_parser") class TestDateTimeParserSearchDate: def test_parse_search(self): - assert self.parser.parse( "Today is 25 of September of 2003", "DD of MMMM of YYYY" ) == datetime(2003, 9, 25) def test_parse_search_with_numbers(self): - assert self.parser.parse( "2000 people met the 2012-01-01 12:05:10", "YYYY-MM-DD HH:mm:ss" ) == datetime(2012, 1, 1, 12, 5, 10) @@ -1597,7 +1536,6 @@ def test_parse_search_with_numbers(self): ) == datetime(1979, 1, 1, 12, 5, 10) def test_parse_search_with_names(self): - assert self.parser.parse("June was born in May 1980", "MMMM YYYY") == datetime( 1980, 5, 1 ) @@ -1614,12 +1552,10 @@ def test_parse_search_locale_with_names(self): ) def test_parse_search_fails(self): - with pytest.raises(parser.ParserError): self.parser.parse("Jag föddes den 25 Augusti 1975", "DD MMMM YYYY") def test_escape(self): - format = "MMMM D, YYYY [at] h:mma" assert self.parser.parse( "Thursday, December 10, 2015 at 5:09pm", format diff --git a/tests/test_util.py b/tests/test_util.py index 3b32e1bc..2454dac5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,5 +1,5 @@ import time -from datetime import datetime +from datetime import datetime, timezone import pytest @@ -87,7 +87,7 @@ def test_validate_ordinal(self): except (ValueError, TypeError) as exp: pytest.fail(f"Exception raised when shouldn't have ({type(exp)}).") - ordinal = datetime.utcnow().toordinal() + ordinal = datetime.now(timezone.utc).toordinal() ordinal_str = str(ordinal) ordinal_float = float(ordinal) + 0.5 diff --git a/tests/utils.py b/tests/utils.py index 95b47c16..834aee05 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,13 +1,36 @@ -import pytz +try: + import zoneinfo +except ImportError: + from backports import zoneinfo from dateutil.zoneinfo import get_zonefile_instance def make_full_tz_list(): dateutil_zones = set(get_zonefile_instance().zones) - pytz_zones = set(pytz.all_timezones) - return dateutil_zones.union(pytz_zones) + zoneinfo_zones = set(zoneinfo.available_timezones()) + # Since the tests create ZoneInfo objects, we can only use timezones + # that are available in zoneinfo. Filter out any dateutil-only timezones + # that are not available in zoneinfo (like Asia/Hanoi which was renamed to Asia/Ho_Chi_Minh) + all_zones = dateutil_zones.union(zoneinfo_zones) + return {tz for tz in all_zones if tz in zoneinfo_zones} def assert_datetime_equality(dt1, dt2, within=10): - assert dt1.tzinfo == dt2.tzinfo + # Compare timezone behavior instead of object identity for cross-platform compatibility + assert_timezone_equivalence(dt1.tzinfo, dt2.tzinfo, dt1) assert abs((dt1 - dt2).total_seconds()) < within + + +def assert_timezone_equivalence(tz1, tz2, dt): + # Timezone objects are equivalent + if tz1 == tz2: + return + + # Compare timezone names + assert tz1.tzname(dt) == tz2.tzname(dt) + + # Compare UTC offset and DST behavior at the given datetime + assert tz1.utcoffset(dt) == tz2.utcoffset( + dt + ), f"UTC offset mismatch: {tz1.utcoffset(dt)} != {tz2.utcoffset(dt)}" + assert tz1.dst(dt) == tz2.dst(dt), f"DST mismatch: {tz1.dst(dt)} != {tz2.dst(dt)}" diff --git a/tox.ini b/tox.ini index fefa3e7e..77f02052 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,18 @@ [tox] minversion = 3.18.0 -envlist = py{py3,36,37,38,39,310} +envlist = py{py3,38,39,310,311,312,313,314} skip_missing_interpreters = true [gh-actions] python = - pypy-3.7: pypy3 - 3.6: py36 - 3.7: py37 + pypy-3.11: pypy3 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + 3.14: py314 [testenv] deps = -r requirements/requirements-tests.txt @@ -34,8 +36,17 @@ commands = doc8 index.rst ../README.rst --extension .rst --ignore D001 make html SPHINXOPTS="-W --keep-going" +[testenv:publish] +passenv = * +skip_install = true +deps = + -r requirements/requirements.txt + flit +allowlist_externals = flit +commands = flit publish --setup-py + [pytest] -addopts = -v --cov-branch --cov=arrow --cov-fail-under=99 --cov-report=term-missing --cov-report=xml +addopts = -v --cov-branch --cov=arrow --cov-fail-under=99 --cov-report=term-missing --cov-report=xml --junitxml=junit.xml -o junit_family=legacy testpaths = tests [isort] @@ -45,4 +56,4 @@ include_trailing_comma = true [flake8] per-file-ignores = arrow/__init__.py:F401,tests/*:ANN001,ANN201 -ignore = E203,E501,W503,ANN101,ANN102 +extend-ignore = E203,E501,W503,ANN101,ANN102,ANN401