pax_global_header00006660000000000000000000000064136724356130014524gustar00rootroot0000000000000052 comment=4d65efa80d09bd42ff59e4919e90484f897027ba pytz-deprecation-shim-0.1.0.post0/000077500000000000000000000000001367243561300170255ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/.github/000077500000000000000000000000001367243561300203655ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/.github/workflows/000077500000000000000000000000001367243561300224225ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/.github/workflows/publish.yml000066400000000000000000000032101367243561300246070ustar00rootroot00000000000000# This workflow is triggered two ways: # # 1. When a tag is created, the workflow will upload the package to # test.pypi.org. # 2. When a release is made, the workflow will upload the package to pypi.org. # # It is done this way until PyPI has draft reviews, to allow for a two-stage # upload with a chance for manual intervention before the final publication. name: Upload package on: push: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install -U tox - name: Create tox environments run: | tox -p -e build,build-check,release --notest - name: Build package run: | tox -e build - name: Check build run: | tox -e build-check - name: Publish package if: >- (github.event_name == 'push' && startsWith(github.ref, 'refs/tags')) || (github.event_name == 'release') env: TWINE_USERNAME: "__token__" run: | if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then export TWINE_REPOSITORY_URL="https://test.pypi.org/legacy/" export TWINE_PASSWORD="${{ secrets.TEST_PYPI_UPLOAD_TOKEN }}" elif [[ "$GITHUB_EVENT_NAME" == "release" ]]; then export TWINE_REPOSITORY="pypi" export TWINE_PASSWORD="${{ secrets.PYPI_UPLOAD_TOKEN }}" else echo "Unknown event name: ${GITHUB_EVENT_NAME}" exit 1 fi tox -e release pytz-deprecation-shim-0.1.0.post0/.github/workflows/python-tests.yml000066400000000000000000000046251367243561300256350ustar00rootroot00000000000000name: Python package on: [push] jobs: tests: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python-version: ['2.7', '3.6', '3.7', '3.8', 'pypy2', 'pypy3'] os: ["ubuntu-latest", "windows-latest"] env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} TOXENV: py HYPOTHESIS_PROFILE: ci steps: - uses: actions/checkout@v2 - name: ${{ matrix.python-version }} - ${{ matrix.os }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip tox - name: Run tests shell: bash run: | tz_path_options=('/usr/share/zoneinfo' \ '/usr/lib/zoneinfo' \ '/usr/share/lib/zoneinfo' \ '/etc/zoneinfo') for option in ${tz_path_options[@]}; do if [[ -d "$option" ]]; then export PYTZ_TZDATADIR="$option" export PYTHONTZPATH="$option" break fi done echo "PYTZ_TZDATADIR: $PYTZ_TZDATADIR" echo "PYTHONTZPATH: $PYTHONTZPATH" if [[ ! -z "$PYTZ_TZDATADIR" ]]; then tzdata_zi="$PYTZ_TZDATADIR/tzdata.zi" if [[ -f "$tzdata_zi" ]]; then head -n 2 "$tzdata_zi" else echo "$tzdata_zi not found" fi fi # Disable coverage on pypy if [[ ${{ matrix.python-version }} = pypy* ]]; then export DEFAULT_TEST_POSARGS='' fi python -m tox - name: Report coverage if: ${{ ! startsWith(matrix.python-version, 'pypy') }} run: | tox -e coverage-report,codecov other: runs-on: "ubuntu-latest" strategy: matrix: toxenv: ["lint", "docs", "build", "precommit"] env: TOXENV: ${{ matrix.toxenv }} steps: - uses: actions/checkout@v2 - name: ${{ matrix.toxenv }} uses: actions/setup-python@v1 with: python-version: 3.8 - name: Install tox run: python -m pip install --upgrade pip tox - name: Run action run: | if [[ $TOXENV == "build" ]]; then TOXENV="build,build-check" fi if [[ $TOXENV == "docs" ]]; then tox -- -j auto -bhtml -W -n -a --keep-going else tox fi pytz-deprecation-shim-0.1.0.post0/.gitignore000066400000000000000000000005201367243561300210120ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *.so # Distribution / packaging build/ dist/ *.egg-info/ .eggs # Sphinx documentation docs/_build/ docs/_output/ # Testing and coverage .cache .hypothesis_cache .hypothesis .mypy_cache .pytest_cache .tox .pytype *.gcda *.gcno *.gcov # Virtual environments venv/ .venv/ pytz-deprecation-shim-0.1.0.post0/.pre-commit-config.yaml000066400000000000000000000013171367243561300233100ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: stable hooks: - id: black language_version: python3.8 - repo: https://github.com/pre-commit/mirrors-isort rev: v4.3.21 hooks: - id: isort additional_dependencies: [toml] language_version: python3.8 - repo: https://github.com/pycqa/pylint rev: pylint-2.5.2 hooks: - id: pylint - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements - repo: https://github.com/asottile/setup-cfg-fmt rev: v1.9.0 hooks: - id: setup-cfg-fmt args: ["--max-py-version", "3.9"] pytz-deprecation-shim-0.1.0.post0/.readthedocs.yml000066400000000000000000000002161367243561300221120ustar00rootroot00000000000000version: 2 # Required in order to install with pip python: version: 3.8 install: - path: . - requirements: docs/requirements.txt pytz-deprecation-shim-0.1.0.post0/CHANGELOG.rst000066400000000000000000000004151367243561300210460ustar00rootroot00000000000000Version 0.1.0 (2020-06-16) ========================== This is the first release of ``pytz-deprecation-shim``. Post-releases: -------------- - ``0.1.0.post0`` (2020-06-17): Fixes the ``project_urls`` metadata to point to the correct bug tracker and documentation. pytz-deprecation-shim-0.1.0.post0/LICENSE000066400000000000000000000011201367243561300200240ustar00rootroot00000000000000Apache Software License 2.0 Copyright (c) 2020, Paul Ganssle (Google) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. pytz-deprecation-shim-0.1.0.post0/MANIFEST.in000066400000000000000000000011221367243561300205570ustar00rootroot00000000000000# All the stuff to include include VERSION include tox.ini include LICENSE *.rst *.toml *.yml *.yaml *.ini *.sh *.cfg recursive-include licenses * recursive-include src/pytz_deprecation_shim * recursive-include templates * graft .github recursive-include tests *.py recursive-include tests *.json # Documentation recursive-include docs *.png recursive-include docs *.svg recursive-include docs *.py recursive-include docs *.rst prune docs/_build prune docs/_output # Files and directories incidentally here prune build/ prune dist/ prune tmp/ prune src/*.egg-info global-exclude *.pyc *.pyo pytz-deprecation-shim-0.1.0.post0/README.rst000066400000000000000000000075441367243561300205260ustar00rootroot00000000000000pytz_deprecation_shim: Shims to help you safely remove pytz =========================================================== ``pytz`` has served the Python community well for many years, but it is no longer the best option for providing time zones. ``pytz`` has a non-standard interface that is `very easy to misuse `_; this interface was necessary when ``pytz`` was created, because ``datetime`` had no way to represent ambiguous datetimes, but this was solved in in Python 3.6, which added a ``fold`` attribute to datetimes in `PEP 495 `_. With the addition of the ``zoneinfo`` module in Python 3.9 (`PEP 615 `_), there has never been a better time to migrate away from ``pytz``. However, since ``pytz`` time zones are used very differently from a standard ``tzinfo``, and many libraries have built ``pytz`` zones into their standard time zone interface (and thus may have users relying on the existence of the ``localize`` and ``normalize`` methods); this library provides shim classes that are compatible with both PEP 495 and ``pytz``'s interface, to make it easier for libraries to deprecate ``pytz``. Usage ===== This library is intended for *temporary usage only*, and should allow you to drop your dependency on ``pytz`` while also giving your users notice that eventually you will remove support for the ``pytz``-specific interface. Within your own code, use ``pytz_deprecation_shim.timezone`` shims as if they were ``zoneinfo`` or ``dateutil.tz`` zones — do not use ``localize`` or ``normalize``: .. code-block:: pycon >>> import pytz_deprecation_shim as pds >>> from datetime import datetime, timedelta >>> LA = pds.timezone("America/Los_Angeles") >>> dt = datetime(2020, 10, 31, 12, tzinfo=LA) >>> print(dt) 2020-10-31 12:00:00-07:00 >>> dt.tzname() 'PDT' Datetime addition will work `like normal Python datetime arithmetic `_, even across a daylight saving time transition: .. code-block:: pycon >>> dt_add = dt + timedelta(days=1) >>> print(dt_add) 2020-11-01 12:00:00-08:00 >>> dt_add.tzname() 'PST' However, if you have exposed a time zone to end users who are using ``localize`` and/or ``normalize`` or any other ``pytz``-specific features (or if you've failed to convert some of your own code all the way), those users will see a warning (rather than an exception) when they use those features: .. code-block:: pycon >>> dt = LA.localize(datetime(2020, 10, 31, 12)) :1: PytzUsageWarning: The localize method is no longer necessary, as this time zone supports the fold attribute (PEP 495). For more details on migrating to a PEP 495-compliant implementation, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html >>> print(dt) 2020-10-31 12:00:00-07:00 >>> dt.tzname() 'PDT' >>> dt_add = LA.normalize(dt + timedelta(days=1)) :1: PytzUsageWarning: The normalize method is no longer necessary, as this time zone supports the fold attribute (PEP 495). For more details on migrating to a PEP 495-compliant implementation, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html >>> print(dt_add) 2020-11-01 12:00:00-08:00 >>> dt_add.tzname() 'PST' For IANA time zones, calling ``str()`` on the shim zones (and indeed on ``pytz`` and ``zoneinfo`` zones as well) returns the IANA key, so end users who would like to actively migrate to a ``zoneinfo`` (or ``backports.zoneinfo``) can do so: .. code-block:: pycon >>> from zoneinfo import ZoneInfo >>> LA = pds.timezone("America/Los_Angeles") >>> LA_zi = ZoneInfo(str(LA)) >>> print(LA_zi) zoneinfo.ZoneInfo(key='America/Los_Angeles') pytz-deprecation-shim-0.1.0.post0/VERSION000066400000000000000000000000141367243561300200700ustar00rootroot000000000000000.1.0.post0 pytz-deprecation-shim-0.1.0.post0/codecov.yml000066400000000000000000000005151367243561300211730ustar00rootroot00000000000000--- comment: false coverage: status: patch: default: target: "100" paths: - "tests/" - "src/" project: default: target: "100" paths: - "tests/" - "src/" pytz-deprecation-shim-0.1.0.post0/conftest.py000066400000000000000000000004571367243561300212320ustar00rootroot00000000000000import os from datetime import timedelta import hypothesis hypothesis.settings.register_profile("long", max_examples=5000) hypothesis.settings.register_profile( "ci", max_examples=2000, deadline=timedelta(seconds=1) ) hypothesis.settings.load_profile(os.getenv(u"HYPOTHESIS_PROFILE", "default")) pytz-deprecation-shim-0.1.0.post0/docs/000077500000000000000000000000001367243561300177555ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/docs/_static/000077500000000000000000000000001367243561300214035ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/docs/_static/images/000077500000000000000000000000001367243561300226505ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/docs/_static/images/fold_495.svg000066400000000000000000000626131367243561300247260ustar00rootroot00000000000000 image/svg+xml pytz-deprecation-shim-0.1.0.post0/docs/_static/images/fold_gap_labels.svg000066400000000000000000000502051367243561300264700ustar00rootroot00000000000000 image/svg+xml pytz-deprecation-shim-0.1.0.post0/docs/_static/images/fold_pytz.svg000066400000000000000000000706251367243561300254150ustar00rootroot00000000000000 image/svg+xml pytz-deprecation-shim-0.1.0.post0/docs/api.rst000066400000000000000000000022001367243561300212520ustar00rootroot00000000000000.. module:: pytz_deprecation_shim API Reference ============= .. data:: UTC The UTC singleton (with an alias at ``utc``) is a shim class wrapping either ``datetime.timezone.utc`` or ``dateutil.tz.UTC``. .. autofunction:: timezone(key) .. autofunction:: fixed_offset_timezone(offset) .. autofunction:: build_tzinfo(zone, fp) .. autofunction:: wrap_zone(tz, key=...) Exceptions ---------- .. autoexception:: PytzUsageWarning ``pytz`` shim exceptions ######################## These exceptions are intended to mirror at least some of ``pytz``'s exception hierarchy. The shim classes are designed in such a way that the exception classes they raise can be caught *either* by catching the value in ``pytz_deprecation_shim`` or the equivalent exception in ``pytz`` (the actual exception type raised will depend on whether or not ``pytz`` has been imported, but the value will either be one of the shim exceptions or a subclass of both the shim exception and its ``pytz`` equivalent). .. autoexception:: InvalidTimeError .. autoexception:: AmbiguousTimeError .. autoexception:: NonExistentTimeError .. autoexception:: UnknownTimeZoneError pytz-deprecation-shim-0.1.0.post0/docs/changelog.rst000066400000000000000000000001731367243561300224370ustar00rootroot00000000000000.. Changelog transcluded from the changelog at the repo root ========= Changelog ========= .. include:: ../CHANGELOG.rst pytz-deprecation-shim-0.1.0.post0/docs/conf.py000066400000000000000000000046561367243561300212670ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- import os def read_version(): here = os.path.split(__file__)[0] version_file = os.path.join(here, "../VERSION") with open(version_file, "rt") as f: return f.read().strip() VERSION = read_version() # -- Project information ----------------------------------------------------- project = "pytz_deprecation_shim" copyright = "2020, Paul Ganssle" author = "Paul Ganssle" # The full version, including alpha/beta/rc tags release = VERSION # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.extlinks", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # For cross-links to other documentation intersphinx_mapping = { "python": ("https://docs.python.org/3.9", None), "dateutil": ("https://dateutil.readthedocs.io/en/stable/", None), } _repo = "https://github.com/pganssle/pytz-deprecation-shim/" extlinks = { "gh": (_repo + "issues/%s", "GH-"), "gh-pr": (_repo + "pull/%s", "GH-"), "pypi": ("https://pypi.org/project/%s", ""), "bpo": ("https://bugs.python.org/issue%s", "bpo-"), "cpython-pr": ("https://github.com/python/cpython/pull/%s", "CPython PR #"), } pytz-deprecation-shim-0.1.0.post0/docs/helpers.rst000066400000000000000000000001611367243561300221470ustar00rootroot00000000000000Helper functions ================ .. automodule:: pytz_deprecation_shim.helpers :members: :undoc-members: pytz-deprecation-shim-0.1.0.post0/docs/index.rst000066400000000000000000000003041367243561300216130ustar00rootroot00000000000000.. include:: ../README.rst .. toctree:: :maxdepth: 2 migration api helpers changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pytz-deprecation-shim-0.1.0.post0/docs/migration.rst000066400000000000000000000340261367243561300225050ustar00rootroot00000000000000Migration Guide =============== This guide explains how to migrate from using ``pytz`` to a :pep:`495`-compatible library, either the standard library module :mod:`zoneinfo` (or its backport, :pypi:`backports.zoneinfo`), or :pypi:`python-dateutil`. Which replacement to choose? ---------------------------- If you only need to support Python 3, you should use :mod:`zoneinfo`. If you need to support Python 2 and Python 3, you should either use :mod:`dateutil.tz` or a combination of ``zoneinfo`` and ``dateutil``. The shim classes use ``dateutil`` in Python 2 and ``zoneinfo`` in Python 3. You can also directly use the shim classes if desired; the shim classes are, themselves, :pep:`495`-compatible time zone implementations. As long as you do not use any part of the ``pytz``-specific interface, they will not raise any warnings. There is, however, some performance overhead associated with this as compared to using the underlying libraries directly. If your time zones are coming from a source that returns ``pytz_deprecation_shim`` shim time zones, you can upgrade them to the PEP 495-compatible zone that they are a wrapper around using :func:`pytz_deprecation_shim.helpers.upgrade_tzinfo`. For more information about how to get time zones from the replacement you've chosen, see the section on :ref:`acquiring a tzinfo object `. Creating an aware datetime ("localizing" datetimes) --------------------------------------------------- With :pep:`495`-compatible zones, there is no longer any need to use ``localize``, you can directly attach a time zone to your ``datetime``: .. code-block:: python import zoneinfo from datetime import datetime NYC = zoneinfo.ZoneInfo("America/New_York") datetime(2020, 1, 1, tzinfo=NYC) If you have a naïve ``datetime`` (or one you'd like to "reinterpret" an aware ``datetime`` as being in another zone), use :meth:`~datetime.datetime.replace`: .. code-block:: python datetime(2020, 1, 1).replace(tzinfo=NYC) Ambiguous and imaginary times ############################# Whenever a time zone's UTC offset changes (such as during a Daylight Saving Time / Summer Time transition), this creates either a gap ("imaginary" times) of times that have been skipped over in local time or a fold (ambiguous times), where there are two possible local times with the same "wall time". This is the problem that :pep:`495` was created to address. When using a PEP 495-compatible time zone, use the ``fold`` attribute to select the behavior you expect during these times. ``fold=0`` (the default) corresponds to the offset that applied *before* the transition, while ``fold=1`` corresponds to the offset that applies *after* the transition: .. code-block:: pycon >>> dt = datetime(2020, 11, 1, 1, tzinfo=ZoneInfo("America/Los_Angeles")) >>> print(dt) 2020-11-01 01:00:00-07:00 >>> dt.tzname() 'PDT' >>> dt_enfolded = dt.replace(fold=1) >>> print(dt_enfolded) 2020-11-01 01:00:00-08:00 >>> dt_enfolded.tzname() 'PST' Since PEP 495 was introduced in Python 3.6, the ``fold`` attribute is not available in earlier versions of Python. However, ``dateutil`` provides a backport for this feature via :func:`dateutil.tz.enfold`. If you are still supporting Python 2, you can use ``tz.enfold``: .. code-block:: pycon >>> dt = datetime(2020, 11, 1, 1, tzinfo=ZoneInfo("America/Los_Angeles")) >>> print(dt) 2020-11-01 01:00:00-07:00 >>> dt.tzname() 'PDT' >>> from dateutil import tz >>> dt = datetime(2020, 11, 1, 1, tzinfo=tz.gettz("America/Los_Angeles")) >>> dt.tzname() 'PDT' >>> dt_enfolded = tz.enfold(dt) >>> dt_enfolded.tzname() 'PST' The ``tz.enfold`` function is also compatible with the ``zoneinfo`` module, and can be used unconditionally in 2/3 compatible code that uses different time zone providers in Python 2 and 3. Semantic differences between ``is_dst`` and ``fold`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ As mentioned in the previous section, during a fold or a gap, the offset information that applies is ill-defined. With ``pytz`` you disambiguate between these choices by using ``is_dst`` to select which side of the transition you want to interpret your naïve datetime as. With :pep:`495`, you choose that by setting the ``fold`` attribute of the ``datetime``. Unfortunately, ``is_dst`` and ``fold`` do not cleanly map onto one another, because ``is_dst`` intends to choose whether to interpret the time as "daylight saving time" vs "standard time", whereas ``fold`` selects between "the first offset" and "the second offset". PEP 495 made the choice to avoid any explicit reference to DST because not all folds and gaps are created by DST-related transitions. To demonstrate the difference, consider the timeline of a year with a standard time (STD) → daylight saving time (DST) transition in spring and its inverse in fall: .. image:: _static/images/fold_gap_labels.svg :align: center During a fold or a gap, ``.utcoffset()``, ``.dst()`` and ``tzname()`` for a given datetime are ill-defined, and so a disambiguation method like ``is_dst`` or ``fold`` needs to be introduced. With ``pytz``'s ``is_dst``, the user is selecting whether to choose the DST or the STD offset when more than one answer is possible. The offsets that apply at each time during the year are illustrated below, with "returns DST offsets" shown in red, and "returns STD offsets" shown in blue: .. image:: _static/images/fold_pytz.svg :align: center With :pep:`495`'s ``fold``, however, the user selects between whether to apply the offset from *before* the transition (``fold = 0``) or *after* the transition (``fold = 1``), as illustrated below: .. image:: _static/images/fold_495.svg :align: center These two don't map onto one another perfectly. Most people likely care about the behavior during folds rather than gaps, because each ambiguous time during a fold represents a real time that occurred, whereas during gaps the primary ambiguity is due to the fact that in a sense both offsets are equally wrong, since no such time occurred. During the folds, the ``is_dst`` behavior can be approximated by setting ``fold = not is_dst``, which will be valid except in in cases of `negative daylight saving time (Winter time) `_, such as occurs in ``Europe/Dublin`` zone (the behavior of ``is_dst`` during offset shifts unrelated to daylight saving time doesn't seem like it would be well-defined, but a spot check of 1969-09-30T12 in the ``Pacific/Kwajalein`` zone indicates that ``fold = not is_dst`` has the same behavior). Detecting ambiguous and imaginary times ####################################### ``pytz`` provides the option to raise an exception if the user attempts to localize a ``datetime`` that falls during a gap or a fold. Since ``zoneinfo`` and ``dateutil.tz`` don't have an explicit localization step, there is no analogous option to throw an error, but it can be re-created using the ``dateutil`` functions :func:`dateutil.tz.datetime_ambiguous` and :func:`dateutil.tz.datetime_exists`, which work independent of the time zone provider in both Python 2 and 3. So, if your ``pytz`` code looks like the following: .. code-block:: python import pytz try: zone.localize(dt, is_dst=None) except pytz.NonExistentTimeError: handle_non_existent_time(dt) except pytz.AmbiguousTimeError: handle_ambiguous_time(dt) You can replace it with the following: .. code-block:: python from dateutil import tz dt = dt.replace(tzinfo=zone) # Only needed if `dt` is naive if not tz.datetime_exists(dt): handle_non_existent_time(dt) elif tz.datetime_ambiguous(dt): handle_ambiguous_time(dt) If you are using ``zoneinfo`` and do not want to take on a ``dateutil`` dependency for this purpose, these functions can be approximated easily enough: .. code-block:: python from datetime import timezone def datetime_exists(dt): """Check if a datetime exists.""" # There are no non-existent times in UTC, and comparisons between # aware time zones always compare absolute times; if a datetime is # not equal to the same datetime represented in UTC, it is imaginary. return dt.astimezone(timezone.utc) == dt def datetime_ambiguous(dt): """Check whether a datetime is ambiguous.""" # If a datetime exists and its UTC offset changes in response to # changing `fold`, it is ambiguous in the zone specified. return datetime_exists(dt) and ( dt.replace(fold=not dt.fold).utcoffset() != dt.utcoffset()) Handling datetime arithmetic ("normalizing" datetimes) ------------------------------------------------------ With ``pytz``, after any arithmetical operation on an aware ``datetime``, it needs to be "normalized", in case the addition has resulted in a ``datetime`` with a different offset from the originally-localized datetime. This is not the case with :pep:`495`-compatible datetimes, and arithmetic that crosses a transition boundary will have the correct offset values. For example: .. code-block:: pycon >>> from zoneinfo import ZoneInfo >>> from datetime import datetime >>> dt = datetime(1992, 3, 1, tzinfo=ZoneInfo("Europe/Minsk")) >>> print(dt) 1992-03-01 00:00:00+02:00 >>> print(dt.utcoffset()) 2:00:00 >>> dt.tzname() 'EET' >>> dt += timedelta(days=90) >>> print(dt) 1992-05-30 00:00:00+03:00 >>> print(dt.utcoffset()) 3:00:00 >>> dt.tzname() 'EEST' However, because this is using standard ``datetime`` mechanisms, the semantics are slightly different (see `Semantics of timezone-aware datetime arithmetic `_ for a more in-depth article on the subject). With a ``pytz`` "add-and-normalize" workflow, all addition is "absolute time" arithmetic (i.e. as if it were performed in UTC), whereas standard ``datetime`` arithmetic is "wall time" arithmetic. So, an example of addition across a DST boundary using ``pytz``: .. code-block:: pycon >>> NYC = pytz.timezone("America/New_York") >>> dt1 = NYC.localize(datetime(2018, 3, 10, 13)) >>> print(dt1) 2018-03-10 13:00:00-05:00 >>> dt2 = dt1 + timedelta(days=1) >>> print(dt2) # Note the offset has not changed! 2018-03-11 13:00:00-05:00 >>> print(NYC.normalize(dt2)) # Note the offset and time both change 2018-03-11 14:00:00-04:00 With a :pep:`495` workflow, the default is to use "wall time" arithmetic, so ``timedelta(days=1)`` will produce the same time of day on the following day, regardless of whether 24 hours will have elapsed in local time or not. So code similar to the operation above instead gives you: .. code-block:: pycon >>> NYC = ZoneInfo("America/New_York") >>> dt1 = datetime(2018, 3, 10, 13, tzinfo=NYC) >>> print(dt1) 2018-03-10 13:00:00-05:00 >>> dt2 = dt1 + timedelta(days=1) >>> print(dt2) 2018-03-11 13:00:00-04:00 It is worth noting that this "wall time" arithmetic may produce an imaginary or ambiguous time. To handle that situation, see `Detecting ambiguous and imaginary times`_. If you want "absolute time" rather than "wall time" arithmetic, the best option is to perform the arithmetic in UTC. Here is a simple helper function for that purpose (in Python 2 or 2/3 compatible code, replace ``datetime.timezone.utc`` with ``dateutil.tz.UTC``): .. code-block:: python from datetime import timezone def absolute_add(dt, td): dt_utc = dt.astimezone(timezone.utc) return (dt_utc + td).astimezone(dt.tzinfo) This will have the same semantics as "add and normalize" in ``pytz``, and similarly guarantees that the result exists. Getting a time zone's name -------------------------- ``pytz`` zones have a ``.zone`` attribute that exposes the key used to created it from the IANA time zone database. The equivalent attribute on ``zoneinfo.ZoneInfo`` objects is :attr:`zoneinfo.ZoneInfo.key`. There is currently no equivalent for this in ``dateutil`` zones. You can also recover this information by calling ``str`` on a ``pytz`` zone, a shim class zone (even in Python 2), or a ``zoneinfo.ZoneInfo`` zone, e.g.: .. code-block:: pycon >>> LA = zoneinfo.ZoneInfo("America/Los_Angeles") >>> str(LA) 'America/Los_Angeles' .. _acquiring-tzinfo: Acquiring a ``tzinfo`` object ----------------------------- Most of this guide assumes that you already have a time zone object, because it is aimed at people who were using ``pytz``-specific features of a time zone returned by a library that is switching over to use a PEP 495-compatible time zone provider. However, if you are *also* creating your own ``pytz`` objects, or you want to switch to directly creating ``tzinfo`` objects yourself, this section covers creating PEP 495-compatible ``tzinfo`` objects. IANA zones ########## With ``pytz``, one creates an IANA / Olson time zone object via the ``pytz.timezone`` function, like so: .. code-block:: python import pytz LA = pytz.timezone("America/Los_Angeles") # When using :mod:`zoneinfo`, instead use the :class:`zoneinfo.ZoneInfo` constructor. Note: in Python 3.6-3.8, replace ``import zoneinfo`` with ``from backports import zoneinfo``: .. code-block:: python import zoneinfo LA = zoneinfo.ZoneInfo("America/Los_Angeles") # zoneinfo.ZoneInfo(key='America/Los_Angeles') When using :mod:`dateutil.tz`, use :func:`dateutil.tz.gettz`: .. code-block:: python from dateutil import tz LA = tz.gettz("America/Los_Angeles") # tzfile('/usr/share/zoneinfo/America/Los_Angeles') UTC and fixed offset zones ########################## ``pytz`` provides a convenience singleton ``pytz.UTC``, as well as a ``FixedOffset`` function, for constructing a value with a fixed offset in minutes. To get an object representing UTC, in Python 3+, use the standard library-provided :attr:`datetime.timezone.utc` singleton. When using ``dateutil``, use ``dateutil.tz.UTC``. To construct a fixed offset zone, use :class:`datetime.timezone` in Python 3 and :class:`dateutil.tz.tzoffset` in Python 2. pytz-deprecation-shim-0.1.0.post0/docs/requirements.txt000066400000000000000000000000371367243561300232410ustar00rootroot00000000000000sphinx>=3.0.0 sphinx_rtd_theme pytz-deprecation-shim-0.1.0.post0/licenses/000077500000000000000000000000001367243561300206325ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/licenses/LICENSE_APACHE000066400000000000000000000261351367243561300226470ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. pytz-deprecation-shim-0.1.0.post0/pyproject.toml000066400000000000000000000014251367243561300217430ustar00rootroot00000000000000[build-system] requires = ["setuptools>=40.8.0", "wheel"] build-backend = "setuptools.build_meta" [tool.black] line-length = 80 [tool.coverage.paths] source = ["src", ".tox/*/site-packages"] [tool.coverage.report] show_missing = true skip_covered = true [tool.isort] atomic=true force_grid_wrap=0 include_trailing_comma=true known_first_party = ["pytz_deprecation_shim"] known_third_party=[ "dateutil", "hypothesis", "pytest", "pytz", ] multi_line_output=3 not_skip="__init__.py" use_parentheses=true [tool.pylint.'MESSAGES CONTROL'] disable="all" enable=""" unused-import, unused-variable, unpacking-non-sequence, invalid-all-object, used-before-assignment, no-else-raise, bad-format-character, bad-format-string, bare-except, """ pytz-deprecation-shim-0.1.0.post0/scripts/000077500000000000000000000000001367243561300205145ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/scripts/update_test_data.py000066400000000000000000000062131367243561300244020ustar00rootroot00000000000000""" Script to automatically generate a JSON file containing time zone information. This is done to allow "pinning" a small subset of the tzdata in the tests, since we are testing properties of a file that may be subject to change. For example, the behavior in the far future of any given zone is likely to change, but "does this give the right answer for this file in 2040" is still an important property to test. This must be run from a computer with zoneinfo data installed. """ import base64 import functools import json import pathlib import textwrap import typing import zlib import backports.zoneinfo as zoneinfo KEYS = [ "Africa/Abidjan", "Africa/Casablanca", "America/Los_Angeles", "America/Santiago", "Asia/Tokyo", "Australia/Sydney", "Europe/Dublin", "Europe/Lisbon", "Europe/London", "Pacific/Kiritimati", "UTC", ] REPO_ROOT = pathlib.Path(__file__).parent.parent.absolute() TEST_DATA_LOC = REPO_ROOT / "tests" / "data" @functools.lru_cache(maxsize=None) def get_zoneinfo_path() -> pathlib.Path: """Get the first zoneinfo directory on TZPATH containing the "UTC" zone.""" key = "UTC" for path in map(pathlib.Path, zoneinfo.TZPATH): if (path / key).exists(): return path else: raise OSError("Cannot find time zone data.") def get_zoneinfo_metadata() -> typing.Dict[str, str]: path = get_zoneinfo_path() tzdata_zi = path / "tzdata.zi" if not tzdata_zi.exists(): # tzdata.zi is necessary to get the version information raise OSError("Time zone data does not include tzdata.zi.") with open(tzdata_zi, "r") as f: version_line = next(f) _, version = version_line.strip().rsplit(" ", 1) if ( not version[0:4].isdigit() or len(version) < 5 or not version[4:].isalpha() ): raise ValueError( "Version string should be YYYYx, " + "where YYYY is the year and x is a letter; " + f"found: {version}" ) return {"version": version} def get_zoneinfo(key: str) -> bytes: path = get_zoneinfo_path() with open(path / key, "rb") as f: return f.read() def encode_compressed(data: bytes) -> typing.List[str]: compressed_zone = zlib.compress(data) raw = base64.b64encode(compressed_zone) raw_data_str = raw.decode("utf-8") data_str = textwrap.wrap(raw_data_str, width=70) return data_str def load_compressed_keys() -> typing.Dict[str, typing.List[str]]: output = {key: encode_compressed(get_zoneinfo(key)) for key in KEYS} return output def update_test_data(fname: str = "zoneinfo_data.json") -> None: TEST_DATA_LOC.mkdir(exist_ok=True, parents=True) # Annotation required: https://github.com/python/mypy/issues/8772 json_kwargs: typing.Dict[str, typing.Any] = dict( indent=2, sort_keys=True, ) compressed_keys = load_compressed_keys() metadata = get_zoneinfo_metadata() output = { "metadata": metadata, "data": compressed_keys, } with open(TEST_DATA_LOC / fname, "w") as f: json.dump(output, f, **json_kwargs) if __name__ == "__main__": update_test_data() pytz-deprecation-shim-0.1.0.post0/setup.cfg000066400000000000000000000026221367243561300206500ustar00rootroot00000000000000[metadata] name = pytz_deprecation_shim version = file:VERSION description = Shims to make deprecation of pytz easier long_description = file: README.rst long_description_content_type = text/x-rst url = https://github.com/pganssle/pytz-deprecation-shim author = Paul Ganssle author_email = paul@ganssle.io license = Apache-2.0 license_file = LICENSE license_files = LICENSE licenses/LICENSE_APACHE classifiers = Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 project_urls = Source = https://github.com/pganssle/pytz-deprecation-shim Bug Reports = https://github.com/pganssle/pytz-deprecation-shim/issues Documentation = https://pytz-deprecation-shim.readthedocs.io [options] packages = pytz_deprecation_shim install_requires = backports.zoneinfo;python_version>="3.6" and python_version<"3.9" python-dateutil;python_version<"3.6" tzdata;python_version>="3.6" python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.* package_dir = =src [tool:pytest] xfail_strict = True [bdist_wheel] universal = 1 pytz-deprecation-shim-0.1.0.post0/src/000077500000000000000000000000001367243561300176145ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/src/pytz_deprecation_shim/000077500000000000000000000000001367243561300242175ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/src/pytz_deprecation_shim/__init__.py000066400000000000000000000011501367243561300263250ustar00rootroot00000000000000__all__ = [ "AmbiguousTimeError", "NonExistentTimeError", "InvalidTimeError", "UnknownTimeZoneError", "PytzUsageWarning", "FixedOffset", "UTC", "utc", "build_tzinfo", "timezone", "fixed_offset_timezone", "wrap_zone", ] from . import helpers from ._exceptions import ( AmbiguousTimeError, InvalidTimeError, NonExistentTimeError, PytzUsageWarning, UnknownTimeZoneError, ) from ._impl import ( UTC, build_tzinfo, fixed_offset_timezone, timezone, wrap_zone, ) # Compatibility aliases utc = UTC FixedOffset = fixed_offset_timezone pytz-deprecation-shim-0.1.0.post0/src/pytz_deprecation_shim/_common.py000066400000000000000000000004231367243561300262170ustar00rootroot00000000000000import sys _PYTZ_IMPORTED = False def pytz_imported(): """Detects whether or not pytz has been imported without importing pytz.""" global _PYTZ_IMPORTED if not _PYTZ_IMPORTED and "pytz" in sys.modules: _PYTZ_IMPORTED = True return _PYTZ_IMPORTED pytz-deprecation-shim-0.1.0.post0/src/pytz_deprecation_shim/_compat.py000066400000000000000000000007121367243561300262130ustar00rootroot00000000000000import sys if sys.version_info[0] == 2: from . import _compat_py2 as _compat_impl else: from . import _compat_py3 as _compat_impl UTC = _compat_impl.UTC get_timezone = _compat_impl.get_timezone get_timezone_file = _compat_impl.get_timezone_file get_fixed_offset_zone = _compat_impl.get_fixed_offset_zone is_ambiguous = _compat_impl.is_ambiguous is_imaginary = _compat_impl.is_imaginary enfold = _compat_impl.enfold get_fold = _compat_impl.get_fold pytz-deprecation-shim-0.1.0.post0/src/pytz_deprecation_shim/_compat_py2.py000066400000000000000000000013311367243561300270030ustar00rootroot00000000000000from datetime import timedelta from dateutil import tz UTC = tz.UTC def get_timezone(key): if not key: raise KeyError("Unknown time zone: %s" % key) try: rv = tz.gettz(key) except Exception: rv = None if rv is None or not isinstance(rv, (tz.tzutc, tz.tzfile)): raise KeyError("Unknown time zone: %s" % key) return rv def get_timezone_file(f, key=None): return tz.tzfile(f) def get_fixed_offset_zone(offset): return tz.tzoffset(None, timedelta(minutes=offset)) def is_ambiguous(dt): return tz.datetime_ambiguous(dt) def is_imaginary(dt): return not tz.datetime_exists(dt) enfold = tz.enfold def get_fold(dt): return getattr(dt, "fold", 0) pytz-deprecation-shim-0.1.0.post0/src/pytz_deprecation_shim/_compat_py3.py000066400000000000000000000025401367243561300270070ustar00rootroot00000000000000# Note: This file could use Python 3-only syntax, but at the moment this breaks # the coverage job on Python 2. Until we make it so that coverage can ignore # this file only on Python 2, we'll have to stick to 2/3-compatible syntax. try: import zoneinfo except ImportError: from backports import zoneinfo import datetime UTC = datetime.timezone.utc def get_timezone(key): try: return zoneinfo.ZoneInfo(key) except (ValueError, OSError): # TODO: Use `from e` when this file can use Python 3 syntax raise KeyError(key) def get_timezone_file(f, key=None): return zoneinfo.ZoneInfo.from_file(f, key=key) def get_fixed_offset_zone(offset): return datetime.timezone(datetime.timedelta(minutes=offset)) def is_imaginary(dt): dt_rt = dt.astimezone(UTC).astimezone(dt.tzinfo) return not (dt == dt_rt) def is_ambiguous(dt): if is_imaginary(dt): return False wall_0 = dt wall_1 = dt.replace(fold=not dt.fold) # Ambiguous datetimes can only exist if the offset changes, so we don't # actually have to check whether dst() or tzname() are different. same_offset = wall_0.utcoffset() == wall_1.utcoffset() return not same_offset def enfold(dt, fold=1): if dt.fold != fold: return dt.replace(fold=fold) else: return dt def get_fold(dt): return dt.fold pytz-deprecation-shim-0.1.0.post0/src/pytz_deprecation_shim/_exceptions.py000066400000000000000000000040271367243561300271140ustar00rootroot00000000000000from ._common import pytz_imported class PytzUsageWarning(RuntimeWarning): """Warning raised when accessing features specific to ``pytz``'s interface. This warning is used to direct users of ``pytz``-specific features like the ``localize`` and ``normalize`` methods towards using the standard ``tzinfo`` interface, so that these shims can be replaced with one of the underlying libraries they are wrapping. """ class UnknownTimeZoneError(KeyError): """Raised when no time zone is found for a specified key.""" class InvalidTimeError(Exception): """The base class for exceptions related to folds and gaps.""" class AmbiguousTimeError(InvalidTimeError): """Exception raised when ``is_dst=None`` for an ambiguous time (fold).""" class NonExistentTimeError(InvalidTimeError): """Exception raised when ``is_dst=None`` for a non-existent time (gap).""" PYTZ_BASE_ERROR_MAPPING = {} def _make_pytz_derived_errors( InvalidTimeError_=InvalidTimeError, AmbiguousTimeError_=AmbiguousTimeError, NonExistentTimeError_=NonExistentTimeError, UnknownTimeZoneError_=UnknownTimeZoneError, ): if PYTZ_BASE_ERROR_MAPPING or not pytz_imported(): return import pytz class InvalidTimeError(InvalidTimeError_, pytz.InvalidTimeError): pass class AmbiguousTimeError(AmbiguousTimeError_, pytz.AmbiguousTimeError): pass class NonExistentTimeError( NonExistentTimeError_, pytz.NonExistentTimeError ): pass class UnknownTimeZoneError( UnknownTimeZoneError_, pytz.UnknownTimeZoneError ): pass PYTZ_BASE_ERROR_MAPPING.update( { InvalidTimeError_: InvalidTimeError, AmbiguousTimeError_: AmbiguousTimeError, NonExistentTimeError_: NonExistentTimeError, UnknownTimeZoneError_: UnknownTimeZoneError, } ) def get_exception(exc_type, msg): _make_pytz_derived_errors() out_exc_type = PYTZ_BASE_ERROR_MAPPING.get(exc_type, exc_type) return out_exc_type(msg) pytz-deprecation-shim-0.1.0.post0/src/pytz_deprecation_shim/_impl.py000066400000000000000000000222441367243561300256750ustar00rootroot00000000000000# -*- coding: utf-8 -*- import warnings from datetime import tzinfo from . import _compat from ._exceptions import ( AmbiguousTimeError, NonExistentTimeError, PytzUsageWarning, UnknownTimeZoneError, get_exception, ) IS_DST_SENTINEL = object() KEY_SENTINEL = object() def timezone(key, _cache={}): """Builds an IANA database time zone shim. This is the equivalent of ``pytz.timezone``. :param key: A valid key from the IANA time zone database. :raises UnknownTimeZoneError: If an unknown value is passed, this will raise an exception that can be caught by :exc:`pytz_deprecation_shim.UnknownTimeZoneError` or ``pytz.UnknownTimeZoneError``. Like :exc:`zoneinfo.ZoneInfoNotFoundError`, both of those are subclasses of :exc:`KeyError`. """ instance = _cache.get(key, None) if instance is None: if len(key) == 3 and key.lower() == "utc": instance = _cache.setdefault(key, UTC) else: try: zone = _compat.get_timezone(key) except KeyError: raise get_exception(UnknownTimeZoneError, key) instance = _cache.setdefault(key, wrap_zone(zone, key=key)) return instance def fixed_offset_timezone(offset, _cache={}): """Builds a fixed offset time zone shim. This is the equivalent of ``pytz.FixedOffset``. An alias is available as ``pytz_deprecation_shim.FixedOffset`` as well. :param offset: A fixed offset from UTC, in minutes. This must be in the range ``-1439 <= offset <= 1439``. :raises ValueError: For offsets whose absolute value is greater than or equal to 24 hours. :return: A shim time zone. """ if not (-1440 < offset < 1440): raise ValueError("absolute offset is too large", offset) instance = _cache.get(offset, None) if instance is None: if offset == 0: instance = _cache.setdefault(offset, UTC) else: zone = _compat.get_fixed_offset_zone(offset) instance = _cache.setdefault(offset, wrap_zone(zone, key=None)) return instance def build_tzinfo(zone, fp): """Builds a shim object from a TZif file. This is a shim for ``pytz.build_tzinfo``. Given a value to use as the zone IANA key and a file-like object containing a valid TZif file (i.e. conforming to :rfc:`8536`), this builds a time zone object and wraps it in a shim class. The argument names are chosen to match those in ``pytz.build_tzinfo``. :param zone: A string to be used as the time zone object's IANA key. :param fp: A readable file-like object emitting bytes, pointing to a valid TZif file. :return: A shim time zone. """ zone_file = _compat.get_timezone_file(fp) return wrap_zone(zone_file, key=zone) def wrap_zone(tz, key=KEY_SENTINEL, _cache={}): """Wrap an existing time zone object in a shim class. This is likely to be useful if you would like to work internally with non-``pytz`` zones, but you expose an interface to callers relying on ``pytz``'s interface. It may also be useful for passing non-``pytz`` zones to libraries expecting to use ``pytz``'s interface. :param tz: A :pep:`495`-compatible time zone, such as those provided by :mod:`dateutil.tz` or :mod:`zoneinfo`. :param key: The value for the IANA time zone key. This is optional for ``zoneinfo`` zones, but required for ``dateutil.tz`` zones. :return: A shim time zone. """ if key is KEY_SENTINEL: key = getattr(tz, "key", KEY_SENTINEL) if key is KEY_SENTINEL: raise TypeError( "The `key` argument is required when wrapping zones that do not " + "have a `key` attribute." ) instance = _cache.get((id(tz), key), None) if instance is None: instance = _cache.setdefault((id(tz), key), _PytzShimTimezone(tz, key)) return instance class _PytzShimTimezone(tzinfo): # Add instance variables for _zone and _key because this will make error # reporting with partially-initialized _BasePytzShimTimezone objects # work better. _zone = None _key = None def __init__(self, zone, key): self._key = key self._zone = zone def utcoffset(self, dt): return self._zone.utcoffset(dt) def dst(self, dt): return self._zone.dst(dt) def tzname(self, dt): return self._zone.tzname(dt) def fromutc(self, dt): # The default fromutc implementation only works if tzinfo is "self" dt_base = dt.replace(tzinfo=self._zone) dt_out = self._zone.fromutc(dt_base) return dt_out.replace(tzinfo=self) def __str__(self): if self._key is not None: return str(self._key) else: return repr(self) def __repr__(self): return "%s(%s, %s)" % ( self.__class__.__name__, repr(self._zone), repr(self._key), ) def unwrap_shim(self): """Returns the underlying class that the shim is a wrapper for. This is a shim-specific method equivalent to :func:`pytz_deprecation_shim.helpers.upgrade_tzinfo`. It is provided as a method to allow end-users to upgrade shim timezones without requiring an explicit dependency on ``pytz_deprecation_shim``, e.g.: .. code-block:: python if getattr(tz, "unwrap_shim", None) is None: tz = tz.unwrap_shim() """ return self._zone @property def zone(self): warnings.warn( "The zone attribute is specific to pytz's interface; " + "please migrate to a new time zone provider. " + "For more details on how to do so, see %s" % PYTZ_MIGRATION_GUIDE_URL, PytzUsageWarning, stacklevel=2, ) return self._key def localize(self, dt, is_dst=IS_DST_SENTINEL): warnings.warn( "The localize method is no longer necessary, as this " + "time zone supports the fold attribute (PEP 495). " + "For more details on migrating to a PEP 495-compliant " + "implementation, see %s" % PYTZ_MIGRATION_GUIDE_URL, PytzUsageWarning, stacklevel=2, ) if dt.tzinfo is not None: raise ValueError("Not naive datetime (tzinfo is already set)") dt_out = dt.replace(tzinfo=self) if is_dst is IS_DST_SENTINEL: return dt_out dt_ambiguous = _compat.is_ambiguous(dt_out) dt_imaginary = ( _compat.is_imaginary(dt_out) if not dt_ambiguous else False ) if is_dst is None: if dt_imaginary: raise get_exception( NonExistentTimeError, dt.replace(tzinfo=None) ) if dt_ambiguous: raise get_exception(AmbiguousTimeError, dt.replace(tzinfo=None)) elif dt_ambiguous or dt_imaginary: # Start by normalizing the folds; dt_out may have fold=0 or fold=1, # but we need to know the DST offset on both sides anyway, so we # will get one datetime representing each side of the fold, then # decide which one we're going to return. if _compat.get_fold(dt_out): dt_enfolded = dt_out dt_out = _compat.enfold(dt_out, fold=0) else: dt_enfolded = _compat.enfold(dt_out, fold=1) # Now we want to decide whether the fold=0 or fold=1 represents # what pytz would return for `is_dst=True` enfolded_dst = bool(dt_enfolded.dst()) if bool(dt_out.dst()) == enfolded_dst: # If this is not a transition between standard time and # daylight saving time, pytz will consider the larger offset # the DST offset. enfolded_dst = dt_enfolded.utcoffset() > dt_out.utcoffset() # The default we've established is that dt_out is fold=0; swap it # for the fold=1 datetime if is_dst == True and the enfolded side # is DST or if is_dst == False and the enfolded side is *not* DST. if is_dst == enfolded_dst: dt_out = dt_enfolded return dt_out def normalize(self, dt): warnings.warn( "The normalize method is no longer necessary, as this " + "time zone supports the fold attribute (PEP 495). " + "For more details on migrating to a PEP 495-compliant " + "implementation, see %s" % PYTZ_MIGRATION_GUIDE_URL, PytzUsageWarning, stacklevel=2, ) if dt.tzinfo is None: raise ValueError("Naive time - no tzinfo set") if dt.tzinfo is self: return dt return dt.astimezone(self) def __copy__(self): return self def __deepcopy__(self, memo=None): return self def __reduce__(self): return wrap_zone, (self._zone, self._key) UTC = wrap_zone(_compat.UTC, "UTC") PYTZ_MIGRATION_GUIDE_URL = ( "https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html" ) pytz-deprecation-shim-0.1.0.post0/src/pytz_deprecation_shim/helpers.py000066400000000000000000000056221367243561300262400ustar00rootroot00000000000000""" This module contains helper functions to ease the transition from ``pytz`` to another :pep:`495`-compatible library. """ from . import _common, _compat from ._impl import _PytzShimTimezone _PYTZ_BASE_CLASSES = None def is_pytz_zone(tz): """Check if a time zone is a ``pytz`` time zone. This will only import ``pytz`` if it has already been imported, and does not rely on the existence of the ``localize`` or ``normalize`` methods (since the shim classes also have these methods, but are not ``pytz`` zones). """ # If pytz is not in sys.modules, then we will assume the time zone is not a # pytz zone. It is possible that someone has manipulated sys.modules to # remove pytz, but that's the kind of thing that causes all kinds of other # problems anyway, so we'll call that an unsupported configuration. if not _common.pytz_imported(): return False if _PYTZ_BASE_CLASSES is None: _populate_pytz_base_classes() return isinstance(tz, _PYTZ_BASE_CLASSES) def upgrade_tzinfo(tz): """Convert a ``pytz`` or shim timezone into its modern equivalent. The shim classes are thin wrappers around :mod:`zoneinfo` or :mod:`dateutil.tz` implementations of the :class:`datetime.tzinfo` base class. This function removes the shim and returns the underlying "upgraded" time zone. When passed a ``pytz`` zone (not a shim), this returns the non-``pytz`` equivalent. This may fail if ``pytz`` is using a data source incompatible with the upgraded provider's data source, or if the ``pytz`` zone was built from a file rather than an IANA key. When passed an object that is not a shim or a ``pytz`` zone, this returns the original object. :param tz: A :class:`datetime.tzinfo` object. :raises KeyError: If a ``pytz`` zone is passed to the function with no equivalent in the :pep:`495`-compatible library's version of the Olson database. :return: A :pep:`495`-compatible equivalent of any ``pytz`` or shim class, or the original object. """ if isinstance(tz, _PytzShimTimezone): return tz._zone if is_pytz_zone(tz): if tz.zone is None: # This is a fixed offset zone offset = tz.utcoffset(None) offset_minutes = offset.total_seconds() / 60 return _compat.get_fixed_offset_zone(offset_minutes) if tz.zone == "UTC": return _compat.UTC return _compat.get_timezone(tz.zone) return tz def _populate_pytz_base_classes(): import pytz from pytz.tzinfo import BaseTzInfo base_classes = (BaseTzInfo, pytz._FixedOffset) # In releases prior to 2018.4, pytz.UTC was not a subclass of BaseTzInfo if not isinstance(pytz.UTC, BaseTzInfo): # pragma: nocover base_classes = base_classes + (type(pytz.UTC),) global _PYTZ_BASE_CLASSES _PYTZ_BASE_CLASSES = base_classes pytz-deprecation-shim-0.1.0.post0/tag_release.sh000077500000000000000000000002361367243561300216400ustar00rootroot00000000000000#!/usr/bin/bash set -e VERSION=$( self.offset_after.utcoffset @property def gap(self): """Whether this introduces a gap""" return self.offset_before.utcoffset < self.offset_after.utcoffset @property def delta(self): return self.offset_after.utcoffset - self.offset_before.utcoffset @property def anomaly_start(self): if get_fold(self): return self.transition + self.delta else: return self.transition @property def anomaly_end(self): if not get_fold(self): return self.transition + self.delta else: return self.transition pytz-deprecation-shim-0.1.0.post0/tests/data/000077500000000000000000000000001367243561300211005ustar00rootroot00000000000000pytz-deprecation-shim-0.1.0.post0/tests/data/zoneinfo_data.json000066400000000000000000000370631367243561300246240ustar00rootroot00000000000000{ "data": { "Africa/Abidjan": [ "eJwLicpMM2LACxiBmAmIOSY9m+TB+P//HwuoBIuPbwiDOxCHkGDIfyDAaRAXkDDgAgB0WB", "dZ" ], "Africa/Casablanca": [ "eJy9lX1M1HUcxw8FR4J6NSJHq74ZpEkOcT5EJeRgpQYI3oEHIhozHuIY5+WADMddKjJBkj", "kk5UbfRGKAAjOSpxIfeDRD5caARMdMo62og+nQYaPv+/NZ/1Qr/6n77fa69+ev9+1eb9DH", "piQu0/zja6t6u6i3+5HIB7bOaQ9Ll+GkqWtozNL32TLH9RUnNNd/WeiY8jG/MXXhV5Nr6l", "6La2eaw+1Hd437O2855vQEWeYWTpjm2SMt2oXlJq/GzzXPzk93rF3zg2Vt6wLT+he8NOsP", "9pveHj5nCd1yxxRW0mQJX17g2GCTMsK1VUb4HxMRSZUyMvsjGTn4jdzoUiY3Gt+XulW7pK", "72eakbflPoQ6Nk1FNxMip7RETVOMvol1fJ6MlAuUm3X26ynhSGJ/XCMCBkjLgrYww6GXNv", "pYhtnCFjJ21is885ubl/vojrDZZxD5NFfGm9jLfPk9vMBrFNZsuEoA6RkOkttnsa5faQIv", "HumIdIfPoVmdRiFUnjIyKlaJFMaQsTqUlXRWrxXZm2YrVIM9pEutttmf7qY8I0FCl3aOul", "ue6BMN98Ru7MlXJnQ4fI0M+RGflGmbX4S5FlGJIf3F8gdvlZxYe9PTJ71iK5u2KH2G0vlj", "nmmyKnerWY4fSnZ+ZfLn/zzHR+xGd6+v5t/NZztU7O9KO74rOGPju5hobpNb5L/dV7qUb/", "78J0/CHMtHpBGhDigJCHqAQCIREIkYhKJhBCEZVUVEiJRVRygRAML0gGQjT6Bko2EMKBkA", "6EeHhBPhACEpWEIEQkKhlBCAlCShBigpAThKAgJCUqUYlKVhDCEpW0IMQlKnlBCExUEhOV", "yCBkBiE0UUlNVGKDkJuoBAchOVGJDkJ2ohIehPREJT5RyQ9iAEQ1AhBDIKoxgBgEUY0CxD", "CIahwgBkJUIwExFKIaC4jBENVoQAwHxHhADIg4zt8PQyKqMYEYFLGY+2NYRCP3xcCIamQg", "hgZibCAGR1SjAzE8YgP3xACJ+dwTQySqMYIYJIhRghgmiHGCGChRjRTEUInV3NO6bh/drZ", "nVlPeIJcxw7rl3jH3Z5809c9vX0D13IpFyXlEt5bwe7nkgOZruBz7lngWBv9G9II17Frq/", "R7kwmHt+/P0TdD/0+ErKRc3NzFHueXj/i5QPn+WexVt66V6cP0G5xPc1yiUJ3POo0y26Hw", "3gnscGD9C9dBb3tFVMUrbd4J5l2ex7WfVFylLHfksr9zwuvqB83MA9yydj6X7CJ4dyRXsX", "5YqH3LOyNJ3ulX3cs8rsSfcqyT1rgvZQrsngnqc8fel+KmQD5dqf+4h1XtyzviWIcv049z", "x9aJTup9vcKDfE844airnnmeVTdD9j5J5NbuV0bwrgns1DWsotWu7ZWtdI99Ybw5S/zt3K", "bOCebbrLlNvyuef5xfx34bzhE8oXJkcoX/Tjnu29eXTvcOGenRV+dO+0c89us41ydxX3vL", "QuhO6XMpMpX37uJ2Y49/x2zEC515t7XmnX0P3KhDfla0VplK91c097sgfd7UcCKPcHfsU0", "cs8B95coDwRzz8FbV+k+pL1H+bvmLOYo9xzOvUN5+GzIf/3P7f95Hv1f6OzX1afAJf6zfw", "fffWU8" ], "America/Los_Angeles": [ "eJzl1VtQVVUcx3FQFEHRlYkSYa3URBAQUBQvZd7mgIqoIIkZXhIYTEOFUMuQJsqsMf5kXr", "rR0qzJKUNzbCyL0CanprEMGsdbRJbKqHhBS7x0Tuu78q2nHpvWmb0/e855PLO/v8wZBXmJ", "fv847W5dnE9vPXcrs7eqd1PMW3tCxTwXbTa9dlW+XR1u6npfWVQ3p6865C2eebhsmxz1fi", "nHakUdb3hHftr6gTTUrJKfV78kjVUF8kvBIjlROkF+TZ0qv+XEysmoQXLKM16d7jpFNUXG", "qKaWgepMYGd1pi5UnT11UZ3bcVU1769T5yuPqAvV5+Xi4j3qUsVBaVm5Vy4XfiRXZr0tv6", "e/In+MfFauDiiR1n6b1bUu0+V6h3J1vfUZfaMpT908nKv//DpFeXd7tO+9aOW3MUr7P99J", "tVnWUbdd3E8C5kSadlOCpf2oIBMYf04C14wxQeo7Cdr2hQ6+uF06NlabTkcOqJDaStP5k2", "rVZeuHWr1aqW5bLbrr8iJ1e8ES3S17mgpNzdTdhw5RPaIG67BeGXJHUJgOb5Mk4S2DzJ0n", "ekhEXXfTc+81uWtHq7l70zHRJf31PU9/Lr2yQnTv+UdVn6QL+t7xn6m+oT/oyOgqFeltNl", "HBZSqq4XsTfWau6l+zw8TUl0ps1VoTt3O2DCh93MSvHS0J+S/rxKI+MtBTrAdltJekyCw9", "OLFJhgQm6+SIdir5dLgeeuO0Grb/ph5+7Bs1YnuYuW/P++r+iutm5Otr1AOFx82o8q0yOr", "3GjJn3oowdYMy4sQtl3OV67Yl9QXnqd+nUkAUqdecGPb55kprwxk4z8dB8SVuxzkz6eKKk", "z1xqJq+Pkykjss3UEiUZEcNN5rQWybzZ02Ql/ShZJ4fp6XddUtO/itDZ3no1Y4tXP9SwS8", "0sb9QP12xQOfP26VlVy9XstAYzZ9U6mRtbax7JXyrzQjabXE+25DaXm/zI4ZJ/IM8UBPaU", "gm0pZv4pnzy6PlcvOBihFpZ49GPVXlWYFaUXVTSqxYM76iWF+1RRaLMuTt+iin1BpiS5Vk", "oazpplYZtlWc0Bs7y1XJ6oqjZPHs6TFaWV5qndKVKaU2RWboyWNv720zbA8d/6+HyVff38", "fL43xT/A56so8+vgnju5u5qYluk3eay9Muz1oL0mZ7oX3N/dMv9FCXz25ASE1yJFQKqAlA", "GpA1IIpBJOWwqkFkgxkGog5UDqgRQEqQhSEqQmSFGQqiBlQeqCFAapDFIapDZOWxykOk5b", "HqQ+SIGQCiElQmqEFAmpElImpE5IoZBKIaVCaoUUC6kWUi6nrRdSMKRiSMnQ1YxD0Ti2au", "6PtGVD6oYUDqmc+0Nt6ZDaIcVz2uoh5XPa+iEFRCqIlBCpIVJEpIpIGZE6IoVEKomUEqkl", "Ukykmkg5kXoiBXXaiiIlRWqKFBWpKlJWpK5IYZHKIqVFaosUF6kuUl6nrS9SYKetMFJipM", "ZIkZEqI2VG6owUGqk0Umqk1kixkWoj5UbqjRTcaSuOlBypOVJ0pOpI2ZG6I4VHKo+UHqk9", "Unyk+kj5nbb+yAI47QogS+C0a4AsArIKyDIg64AsBLISyFIga4EsBrIayHI47XogC+K0K4", "IsidOuCbIoyKogy4KsC7IwyMogS4OsDbI4yOogy4OsD7JATrtCyBI57Rohi+S0q4QsE7JO", "yEIhK4UsFbJWyGIhq4UsF7JeyII5fX+/ryyZ064ZsmhOu2rIsiHrhiwcsnLI0iFrh//fxQ", "u2Xyfbn2PSBsYlxsXHpCUkxCXExQf/Ba/xl0M=" ], "America/Santiago": [ "eJzl1XtMlWUAx/H3nMMtLglCpqiFFGaB+CBHuQT4SCKQgqfEa0cyddw0QOJSSqFupdYWyW", "itaTO7qmv6gBXQZD7rAllUsGJkk7U1sdWajG6Wpqfn+w7+6q/+a+s9e9/PGWf8826/71O0", "vrwk2frHFTR+c708/j1ql3k0i9ylB70jnsON3/UcsbZrVdzQ09aQKdsXW/2n1lbrt2Pdnn", "fSXpPvukr7O6KHdMeFg57Oa17d1Rvj6ZtRKfu2fd//5flm/dX0s2oweZUerG+R1wYy5PWW", "K8rX95m2as4rR51HO2W+cq2ar/1i4pV/1goZYIWpwJDTMvDbURV0PVLecGZABQ//JkOOXh", "Kh3UMybF+/uPGlLjmprF2EPzWoI/JaxeSSDh05p15ELX1R3xTRoqbM3qmnjNWqqQEb9NSB", "1WraxUU6ui1VTf/CK2cciFYzT2bJW6r/Urc2x8iYJpeYVeWUsWlXxW2FF+Tt04ZFXKql4/", "48I2b//Ie+49wRMefyh/rO97vVXUNv6PhXDquEjn167p7dKvGFCj1v02Yldu2VSdk5av7G", "cpmcOSTccplcELJELAzrlCkBgyrVmiRTPw0SaT/E6/TjP4q7z4bpjKf7RObRUZ1VcUIs2j", "eg5cpP1OKyUzo7+S11T97zeknksyonqV3m/FKtciNaZe6oU+SN1cv801Xi3oG1clmnpZa3", "pcuCnkBR6NmjVxw6oTzZ5/R9jc+p++N/lyu9Naoo5Gu5JuWiWHPyJ73u5o/FuubP9frLx8", "UDVUp7j5WqDbWNunh/vnqwqFhvLI9XD7ml3pQfpjZHxeotc0bVll/9dMnkUFEyPEuWjl0S", "Zd0uWT7QLyoOjcitbe1iW2OvfPhAq6j0HpNVO5SqznhGbl/fomri3tSPpNeqWv/9ui56ta", "ob2arrr6Sqho8K9KPfRKvHXp+nd3yQInY+GaEbX50mHq9MlE/suSqaCsNlk8/nczgdLoef", "c+Ljcrr8zR0Q+B/5BPh83e9Zlv308/m6lBXk8/VqK5SnwzwH+x3hE0/+vrygyFpp7rligb", "nd5p5YosMxYdG/2Kd5Rb7yvTN7kJ0iW0X2imwW2S2yXWS/yIaRHSNbRvaMbNrW7BrZNrJv", "ZOO2ZufI1pG9I5u3NbtHto/2/u1/NA3gMh2w34BpAdIDpAlIF5A2IH1AGmFrOmG/KNMKpB", "dIM5BuIO1A+oE0BOkI0hKkJ0hTkK4gbUH6gjTG1nQGaY2t6Q3SHKQ7SHuQ/iANQjqEtAjp", "EdIkpEtIm5A+IY2yNZ1CWoX0CmkW0i2kXUi/kIYhHUNahvQMaRrSNaRtSN+QxiGdQ1qH9A", "5pnq3pHtI+pH9IA5EOIi1Eeog0Eeki0kakj0gjkU4irbQ1vUSaaWu6ibQT6SfSUKSjSEuR", "niJNRbqKtBXpK9JYpLNIa5He2prmIt21Ne1F+os0GOkw0mKkx0iTkS4jbUb6jDQa6bStaT", "XSa1vTbKTbSLuRfiMNRzqOtBzpOdJ0pOtI25G+I41HOo+0Hum9rWk+0n2k/Uj/kTMAOQeQ", "swA5D5AzATkXkLPB9n96PgRnmF+y3OaZnJVQkJaYlLhw3nx3QoF7/Fvw39le+wo=" ], "Asia/Tokyo": [ "eJwLicpMM2LAACxQDAKcUDZPA5C4bsdUcP1t5IcbP34V3Dxr/eE2O8OH22tlP9x59ujD3Z", "7/H5gZmSCQgaGZGaS7ZwEjUHPdBAYOCOnjG8Lg5QLEwSEgaUYQDiHBEf+BIPXQkgIQDXIM", "mAY6CESDHAWiQQ4D0SDHgWmgA0E0yJEgmmyHcgE5upZcAG/EZO8=" ], "Australia/Sydney": [ "eJzdlF1MlXUcx78nYQSpZDlelVAzwRA4KgaF5gsgpBjK4bU0AkSxFw+QipX486VRW2rahc", "zODGlGFxVGa5pjEcuNFQKri15Ic3TTsDbyJmNm9nweuOiim+5an2d7Ps/Ozp5z8T/fj6+8", "tmah/pHgCR+eeJ5qzi2w7u1AoCv+1z7fh+rrLfFfPFts/e0Z/oHWBg02x/on+TMsaMoMBR", "fEWvBv6RaSctNCvo6x0PArFvrRHxY22mN3vBmtyd/+oClNNzT13KcK33RZd7a0adqybt3V", "eFB3x53S9NIaTf/zgCLScxU5+RWLmlVtUb37LMaTYzHtVRY7nGAzmrNtZk+Yxe0O2D3HOx", "Vf1mSzbMBmr3xDc2p/1r2z92huXr/mjl7VvMQOzRu8qMTQo0rseF/zRxp0f8sXlvTVEVvQ", "+K4ld9ZZSulhSz1WaN6N5bawPs0WLc20xRsiLW1mnC3xjtmSm7L02AilX/rRMtpH7MGuC/", "bQ0JAyTw9r6fkuLXvpMz184qSWbz6tFQfO28rsZq2qClhW8luWvarJcoLXaPWcClt9vU25", "P72uvG8O6pHeHVpztkZr20uUfzxX65oz9Oiu+SrwF9n6ihzbUPCAFS5PMF9qtBXFh1lx+A", "0rvvWLlYxettIrA1Y22G3l3WfssXOX9Pg7/drY8ok2vdyhJxpbVbH1qJ4s3a/K3AZVpVer", "OqFIm6NyVDOtzrZ4Km3LtULbOpxltV+m2bae++ypDyLt6VO32zOvjdmzdtW2+783/7YQ1Z", "X9rvq8ETVkDOm5xD7tiO7SztD3tHPspHaNHFHjd6bdn9fp+Y8r9ULnIXuxrcn2HNtuTfsr", "bG/9ervNM3FNCvoPXNKhC/ytT5zxBEmvtir0789r831akZ3FrdDH1zwe37+YyC2Hhoi9mZ", "ipuHbmgpmMa2c2mOlg5oOZEGZGvJQpYebk/pgzKdfOrDDTcu3MCzMx187MMFPDzA2YHDA7", "YHrA/IAJAjMEpgjMEZika2eWwDSBeWIm6tqZKWaqrp25YiaLmS1mupj5YiYMzBgzZcycgU", "kDswam7dqZNzBx187Mgam7duYOTB6YPWb6mPljEoDJACYFmBxgkoDJAiYNmDxgEuHayQQm", "Fa6dXGCSgckGJh1APoCEABkBUgLkBEgKJitAWjB5wSQGkxkgNa6vj58ryQGyA6QHyA+QIC", "BDQIqAHAFJwmQJkyZMnjCJwmQKkyrXTq4wycJkC5MuTL4wCQMyBqQMyBmQNCBrQNqAvAGJ", "AzIHpA7IHSZ5rq+Nnyvpw+QPk0BMBjEpxOQQk0RMFjFpBPIIJBLIJJBKIJdAMl2PjZ8r6Q", "TyCSQUyCiQUkxOMUnFZBX/D9IaxuMCbyofJ+V7U5O9yalJ+YtRyqKwvwAmYQJp" ], "Europe/Dublin": [ "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", "tTaBLq9wfGY+1n" ], "Europe/Lisbon": [ "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", "cvTz45/gI6LhGv" ], "Europe/London": [ "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", "==" ], "Pacific/Kiritimati": [ "eJwLicpMM2LAC1igWKgBRIR+YtBn1V5Q////f0YmZub//3OAwv//ZwHV/P9fnMDAxcBw5A", "EDn49vCIOuoYGJAYhk0DY0YQghwSag4f/rzD1AFoJtBAGQrSCadJu5bICkna6hCRcAUwcw", "tg==" ], "UTC": [ "eJwLicpMM2IgCBiBmAXCDA1xZgghSxcXkDDgAgBn0wZx" ] }, "metadata": { "version": "2020a" } } pytz-deprecation-shim-0.1.0.post0/tests/test_errors.py000066400000000000000000000025761367243561300231260ustar00rootroot00000000000000from datetime import datetime, timedelta import hypothesis import pytest import pytz import pytz_deprecation_shim as pds from ._common import ( invalid_offset_minute_strategy, invalid_zone_strategy, valid_zone_strategy, ) # On Python 2.7 when running 5 test suites in parallel, this is reliably flaky # without increasing the deadline. When running 1 or 2 in parallel it's only # intermittently flaky. @hypothesis.settings(deadline=timedelta(seconds=30)) @hypothesis.given(key=invalid_zone_strategy) @hypothesis.example(key="") def test_invalid_zones(key): with pytest.raises(pds.UnknownTimeZoneError) as exc_info: pds.timezone(key) assert issubclass(exc_info.type, pytz.UnknownTimeZoneError) @hypothesis.given(key=valid_zone_strategy) def test_localize_aware_datetime(key): zone = pds.timezone(key) dt = datetime(2020, 1, 1, tzinfo=pds.UTC) with pytest.warns(pds.PytzUsageWarning), pytest.raises(ValueError): zone.localize(dt) @hypothesis.given(key=valid_zone_strategy) def test_normalize_naive_datetime(key): zone = pds.timezone(key) dt = datetime(2020, 1, 1) with pytest.warns(pds.PytzUsageWarning), pytest.raises(ValueError): zone.normalize(dt) @hypothesis.given(offset=invalid_offset_minute_strategy) def test_invalid_fixed_offsets(offset): with pytest.raises(ValueError): pds.fixed_offset_timezone(offset) pytz-deprecation-shim-0.1.0.post0/tests/test_helpers.py000066400000000000000000000120021367243561300232350ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys import threading import pytest import pytz import pytz_deprecation_shim as pds from pytz_deprecation_shim import helpers as pds_helpers from ._common import UTC get_timezone = pds._compat.get_timezone get_fixed_timezone = pds._compat.get_fixed_offset_zone SYS_MODULES_LOCK = threading.Lock() HELPER_LOCK = threading.Lock() ZONE_LIST = ( "America/New_York", "Asia/Tokyo", "Australia/Sydney", "Africa/Abidjan", "America/Santiago", "Europe/London", "Europe/Dublin", ) OFFSET_MINUTES_LIST = (0, -1439, 1439, 0, 60, -60, 11) def _timezones(tz_constructor): return tuple(map(tz_constructor, ZONE_LIST)) def _fixed_offsets(tz_constructor): return tuple(map(tz_constructor, OFFSET_MINUTES_LIST)) NON_PYTZ_ZONES = ( (UTC, pds.timezone("UTC")) + _timezones(pds.timezone) + _timezones(get_timezone) + _fixed_offsets(pds.fixed_offset_timezone) + _fixed_offsets(get_fixed_timezone) ) PYTZ_ZONES = ( (pytz.UTC, pytz.timezone("UTC")) + _timezones(pytz.timezone) + _fixed_offsets(pytz.FixedOffset) ) @pytest.fixture(autouse=True) def helper_lock(): """Lock around anything using the helper module.""" with HELPER_LOCK: yield @pytest.fixture def no_pytz(): """Fixture to remove pytz from sys.modules for the duration of the test.""" with SYS_MODULES_LOCK: pds._common._PYTZ_IMPORTED = False base_classes = pds_helpers._PYTZ_BASE_CLASSES pds_helpers._PYTZ_BASE_CLASSES = None pytz_modules = {} for modname in list(sys.modules): if modname.split(".", 1)[0] != "pytz": # pragma: nocover continue pytz_modules[modname] = sys.modules.pop(modname) try: yield finally: sys.modules.update(pytz_modules) pds_helpers._PYTZ_BASE_CLASSES = base_classes @pytest.mark.parametrize("tz", NON_PYTZ_ZONES) def test_not_pytz_zones(tz): """Tests is_pytz_zone for non-pytz zones.""" assert not pds_helpers.is_pytz_zone(tz) @pytest.mark.parametrize("tz", NON_PYTZ_ZONES) def test_not_pytz_no_pytz(tz, no_pytz): """Tests is_pytz_zone when pytz has not been imported.""" assert not pds_helpers.is_pytz_zone(tz) # Ensure that the test didn't import `pytz`. assert "pytz" not in sys.modules @pytest.mark.parametrize("tz", PYTZ_ZONES) def test_pytz_zone(tz): assert pds_helpers.is_pytz_zone(tz) @pytest.mark.parametrize("key", ("UTC",) + ZONE_LIST) def test_pytz_zones_before_after(key, no_pytz): """Tests is_pytz_zone when pytz is imported after first use. We want to make sure that is_pytz_zone doesn't inappropriately cache the fact that pytz hasn't been imported, and just always return ``False``, even if pytz is imported after the first call to is_pytz_zone. """ non_pytz_zone = pds.timezone(key) assert not pds_helpers.is_pytz_zone(non_pytz_zone) assert "pytz" not in sys.modules import pytz pytz_zone = pytz.timezone(key) assert pds_helpers.is_pytz_zone(pytz_zone) @pytest.mark.parametrize("key", ZONE_LIST) def test_upgrade_shim_timezone(key): shim_zone = pds.timezone(key) actual = pds_helpers.upgrade_tzinfo(shim_zone) expected = get_timezone(key) assert actual is expected @pytest.mark.parametrize("key", ZONE_LIST) def test_upgrade_pytz_timezone(key): pytz_zone = pytz.timezone(key) actual = pds_helpers.upgrade_tzinfo(pytz_zone) expected = get_timezone(key) assert actual is expected @pytest.mark.parametrize("offset_minutes", OFFSET_MINUTES_LIST) def test_upgrade_shim_fixed_offset(offset_minutes): shim_zone = pds.fixed_offset_timezone(offset_minutes) actual = pds_helpers.upgrade_tzinfo(shim_zone) expected = get_fixed_timezone(offset_minutes) # get_fixed_timezone doesn't do any caching, so the fixed offsets aren't # necessarily going to be singletons. assert actual == expected @pytest.mark.parametrize("offset_minutes", OFFSET_MINUTES_LIST) def test_upgrade_pytz_fixed_offset(offset_minutes): pytz_zone = pytz.FixedOffset(offset_minutes) actual = pds_helpers.upgrade_tzinfo(pytz_zone) expected = get_fixed_timezone(offset_minutes) # get_fixed_timezone doesn't do any caching, so the fixed offsets aren't # necessarily going to be singletons. assert actual == expected @pytest.mark.parametrize("utc", (pds.UTC, pytz.UTC)) def test_upgrade_utc(utc): """Tests that upgrade_zone called on UTC objects returns _compat.UTC. This is relevant because tz.gettz("UTC") or zoneinfo.ZoneInfo("UTC") may each return a tzfile-based zone, not their respective UTC singletons. """ actual = pds_helpers.upgrade_tzinfo(utc) assert actual is UTC @pytest.mark.parametrize( "tz", [UTC] + list(map(get_timezone, ZONE_LIST)) + list(map(get_fixed_timezone, OFFSET_MINUTES_LIST)), ) def test_upgrade_tz_noop(tz): """Tests that non-shim, non-pytz zones are unaffected by upgrade_tzinfo.""" actual = pds_helpers.upgrade_tzinfo(tz) assert actual is tz pytz-deprecation-shim-0.1.0.post0/tests/test_pytz_equivalent.py000066400000000000000000000260051367243561300250460ustar00rootroot00000000000000# -*- coding: utf-8 -*- from datetime import datetime, timedelta import hypothesis import pytest import pytz from hypothesis import strategies as hst import pytz_deprecation_shim as pds from pytz_deprecation_shim import PytzUsageWarning from ._common import ( MAX_DATETIME, MIN_DATETIME, PY2, UTC, ZERO, assert_dt_equivalent, conditional_examples, dt_strategy, enfold, get_fold, offset_minute_strategy, round_timedelta, valid_zone_strategy, ) _ARGENTINA_CITIES = [ "Buenos_Aires", "Catamarca", "ComodRivadavia", "Cordoba", "Jujuy", "La_Rioja", "Mendoza", "Rio_Gallegos", "Rosario", "Salta", "San_Juan", "San_Luis", "Tucuman", "Ushuaia", ] _ARGENTINA_ZONES = {"America/%s" % city for city in _ARGENTINA_CITIES} _ARGENTINA_ZONES |= { "America/Argentina/%s" % city for city in _ARGENTINA_CITIES } @hypothesis.given( dt=dt_strategy, key=valid_zone_strategy, is_dst=hst.booleans() ) @hypothesis.example( dt=enfold(datetime(2010, 11, 7, 1, 30), fold=1), key="America/New_York", is_dst=True, ) @hypothesis.example( dt=enfold(datetime(2010, 11, 7, 1, 30), fold=1), key="America/New_York", is_dst=False, ) @hypothesis.example( dt=datetime(2010, 11, 7, 1, 30), key="America/New_York", is_dst=True ) @hypothesis.example( dt=datetime(2010, 11, 7, 1, 30), key="America/New_York", is_dst=False ) @conditional_examples( not PY2, [ hypothesis.example( dt=datetime(2009, 3, 29, 2), key="Europe/Amsterdam", is_dst=True ), hypothesis.example( dt=enfold(datetime(1933, 1, 1), fold=0), key="Asia/Kuching", is_dst=True, ), hypothesis.example( dt=enfold(datetime(1933, 1, 1), fold=1), key="Asia/Kuching", is_dst=True, ), hypothesis.example( dt=enfold(datetime(1933, 1, 1), fold=0), key="Asia/Kuching", is_dst=False, ), hypothesis.example( dt=enfold(datetime(1933, 1, 1), fold=1), key="Asia/Kuching", is_dst=False, ), ], ) def test_localize_explicit_is_dst(dt, key, is_dst): pytz_zone = pytz.timezone(key) shim_zone = pds.timezone(key) dt_pytz = pytz_zone.localize(dt, is_dst=is_dst) with pytest.warns(PytzUsageWarning): dt_shim = shim_zone.localize(dt, is_dst=is_dst) assume_no_dst_inconsistency_bug(dt, key) assert_dt_equivalent(dt_shim, dt_pytz) @hypothesis.given(dt=dt_strategy, key=valid_zone_strategy) @hypothesis.example(dt=datetime(2018, 3, 25, 1, 30), key="Europe/London") @hypothesis.example(dt=datetime(2018, 10, 28, 1, 30), key="Europe/London") @hypothesis.example(dt=datetime(2004, 4, 4, 2, 30), key="America/New_York") @hypothesis.example(dt=datetime(2004, 10, 31, 1, 30), key="America/New_York") @conditional_examples( not PY2, examples=[ hypothesis.example( dt=datetime(2018, 3, 25, 1, 30), key="Europe/Dublin" ), hypothesis.example( dt=datetime(2018, 10, 28, 1, 30), key="Europe/Dublin" ), ], ) def test_localize_is_dst_none(dt, key): pytz_zone = pytz.timezone(key) shim_zone = pds.timezone(key) pytz_exc = shim_exc = dt_pytz = dt_shim = None try: dt_pytz = pytz_zone.localize(dt, is_dst=None) except pytz.InvalidTimeError as e: pytz_exc = e with pytest.warns(PytzUsageWarning): try: dt_shim = shim_zone.localize(dt, is_dst=None) except pds.InvalidTimeError as e: shim_exc = e # This section is triggered rarely. It currently occurs in Python 3 when # key = "Africa/Abidjan" and dt is 1912-01-01. if (dt_pytz is None) != (dt_shim is None): # pragma: nocover uz = pds._compat.get_timezone(key) utc_off = uz.utcoffset(dt) hypothesis.assume(utc_off == round_timedelta(utc_off)) utc_off_folded = uz.utcoffset(enfold(dt, fold=not get_fold(dt))) hypothesis.assume(utc_off_folded == round_timedelta(utc_off_folded)) if dt_pytz: assume_no_dst_inconsistency_bug(dt, key, is_dst=False) assert_dt_equivalent(dt_pytz, dt_shim) if pytz_exc: assert repr(shim_exc) == repr(pytz_exc) assert str(shim_exc) == str(pytz_exc) assert isinstance(shim_exc, type(pytz_exc)) @hypothesis.given( dt=dt_strategy, delta=hst.timedeltas( min_value=timedelta(days=-730), max_value=timedelta(days=730) ), key=valid_zone_strategy, ) def test_normalize_same_zone(dt, delta, key): """Test normalization after arithmetic. NOTE: There is actually a difference in semantics here, because with pytz zones, adding a timedelta and normalizing gives you absolute time arithmetic, not wall-time arithmetic. To emulate this, we do the addition of the shimmed zone in UTC. """ pytz_zone = pytz.timezone(key) shim_zone = pds.timezone(key) dt_pytz = pytz_zone.localize(dt, is_dst=not get_fold(dt)) dt_shim = dt.replace(tzinfo=shim_zone) hypothesis.assume(dt_pytz == dt_shim) dt_pytz_after_nn = dt_pytz + delta dt_shim_after_nn = (dt_shim.astimezone(UTC) + delta).astimezone(shim_zone) hypothesis.assume( MIN_DATETIME < dt_pytz_after_nn.replace(tzinfo=None) < MAX_DATETIME ) dt_pytz_after = pytz_zone.normalize(dt_pytz_after_nn) with pytest.warns(PytzUsageWarning): dt_shim_after = shim_zone.normalize(dt_shim_after_nn) assert dt_shim_after_nn is dt_shim_after assume_no_dst_inconsistency_bug(dt + delta, key) assert_dt_equivalent(dt_pytz_after, dt_shim_after, round_dates=True) @hypothesis.given(dt=dt_strategy, offset=offset_minute_strategy) @hypothesis.example(dt=datetime(2020, 1, 1), offset=0) @hypothesis.example(dt=datetime(2020, 2, 29), offset=0) @hypothesis.example(dt=datetime(2020, 2, 29), offset=-1439) @hypothesis.example(dt=datetime(2020, 2, 29), offset=1439) def test_localize_fixed_offset(dt, offset): pytz_zone = pytz.FixedOffset(offset) shim_zone = pds.fixed_offset_timezone(offset) dt_pytz = pytz_zone.localize(dt) with pytest.warns(pds.PytzUsageWarning): dt_shim = shim_zone.localize(dt) assert dt_pytz == dt_shim assert dt_pytz.utcoffset() == dt_shim.utcoffset() @hypothesis.given(key=valid_zone_strategy) def test_zone_attribute(key): pytz_zone = pytz.timezone(key) shim_zone = pds.timezone(key) pytz_zone_value = pytz_zone.zone with pytest.warns(PytzUsageWarning): shim_zone_value = shim_zone.zone assert pytz_zone_value == shim_zone_value @hypothesis.given(key=valid_zone_strategy) def test_str(key): pytz_zone = pytz.timezone(key) shim_zone = pds.timezone(key) assert str(pytz_zone) == str(shim_zone) def assume_no_dst_inconsistency_bug(dt, key, is_dst=False): # prama: nocover # pytz and zoneinfo have bugs around the correct value for dst(), see, e.g. # Until those are fixed, we'll try to avoid these "sore spots" with a # combination of one-offs and rough heuristics. if len(key) == 3 and key.lower() == "utc": uz = pds._compat.UTC else: uz = pds._compat.get_timezone(key) ########### # One-offs if PY2: # https://github.com/dateutil/dateutil/issues/1048 hypothesis.assume( uz.dst(dt) or not ((uz.dst(dt + timedelta(hours=24)) or ZERO) < ZERO) ) # https://github.com/dateutil/dateutil/issues/1049 hypothesis.assume( not ( key == "Asia/Colombo" and datetime(1942, 1, 5) <= dt <= datetime(1945, 10, 17) ) ) # https://github.com/dateutil/dateutil/issues/1050 hypothesis.assume( not ( key == "America/Iqaluit" and datetime(1942, 7, 31) <= dt <= datetime(1945, 10, 1) ) ) # Possibly another manifestation of dateutil/dateutil#1050 hypothesis.assume( not ( (key == "MET" or key == "CET") and datetime(1916, 5, 1) <= dt <= datetime(1916, 10, 2) ) ) # dateutil isn't currently up to PEP 495's spec during ambiguous times, # which means occasionally it's an ambiguous time and neither side is # DST. from dateutil import tz hypothesis.assume(tz.datetime_exists(dt, uz)) # bpo-40930: https://bugs.python.org/issue40930 hypothesis.assume( not ( key == "Pacific/Rarotonga" and datetime(1978, 11, 11) <= dt <= datetime(1991, 3, 2) and uz.dst(dt) ) ) hypothesis.assume( not ( key == "America/Montevideo" and ( datetime(1923, 1, 1) <= dt <= datetime(1927, 1, 1) or datetime(1942, 12, 14) <= dt <= datetime(1943, 3, 13) ) and uz.dst(dt) ) ) # Argentina switched from -03 (STD) to -03 (DST) to -03 (STD) during this # interval, for whatever reason. pytz calls this dst() == 0, zoneinfo calls # this dst() == 1:00. hypothesis.assume( not ( key in _ARGENTINA_ZONES and datetime(1999, 10, 3) <= dt <= datetime(2000, 3, 4) ) ) # bpo-40933: https://bugs.python.org/issue40933 hypothesis.assume( not ( key == "Europe/Minsk" and datetime(1941, 6, 27) <= dt <= datetime(1943, 3, 30) ) ) # Issue with pytz: America/Louisville transitioned from EST→CDT→EST in # 1974, but `pytz` returns timedelta(0) for CDT. hypothesis.assume( not ( ( key == "America/Louisville" or key == "America/Kentucky/Louisville" or key == "America/Indiana/Marengo" ) and datetime(1974, 1, 6) <= dt <= datetime(1974, 10, 28) ) ) # Same deal with the RussiaAsia rule in 1991, a transition to DST with no # corresponding change in the offset, then a transition bac hypothesis.assume( not ( key == "Asia/Qyzylorda" and datetime(1991, 3, 31) <= dt <= datetime(1991, 9, 30) ) ) # Issue with pytz: Europe/Paris went from CEST (+2, STD) → CEST (+2, DST) → # WEMT (+2, DST) → WEST (+1, DST) → WEMT (+2, DST) → CET (+1, STD) between # 3 April 1944 and 16 September 1945. pytz doesn't detect that WEMT is a # DST zone, though. hypothesis.assume( not ( key == "Europe/Paris" and datetime(1944, 10, 8) <= dt <= datetime(1945, 4, 3) ) ) ########### # Bugs around incorrect double DST # https://github.com/stub42/pytz/issues/44 # bpo-40931: https://bugs.python.org/issue40931 pz = pytz.timezone(key) dt_pytz = pz.localize(dt, is_dst=is_dst) dt_uz = dt.replace(tzinfo=uz) dt_uz = enfold(dt_uz, fold=not is_dst) if abs(dt_pytz.dst()) <= timedelta(hours=1) and abs( dt_uz.dst() or timedelta(0) ) <= timedelta(hours=1): return hypothesis.assume(dt_uz.dst() == dt_pytz.dst()) pytz-deprecation-shim-0.1.0.post0/tests/test_shims.py000066400000000000000000000257661367243561300227430ustar00rootroot00000000000000import copy import pickle from datetime import datetime, timedelta, tzinfo import hypothesis import hypothesis.strategies as hst import pytest import pytz import pytz_deprecation_shim as pds from . import _zoneinfo_data from ._common import ( MAX_DATETIME, MIN_DATETIME, PY2, assert_dt_equivalent, assert_dt_offset, conditional_examples, datetime_unambiguous, dt_strategy, enfold, get_fold, offset_minute_strategy, round_normalized, valid_zone_strategy, ) ONE_SECOND = timedelta(seconds=1) ARBITRARY_KEY_STRATEGY = hst.from_regex("[a-zA-Z][a-zA-Z_]+(/[a-zA-Z_]+)+") def _make_no_cache_timezone(): if PY2: import dateutil.tz def no_cache_timezone(key, tz=dateutil.tz): return pds.wrap_zone(tz.gettz.nocache(key), key) else: try: import zoneinfo except ImportError: from backports import zoneinfo def no_cache_timezone(key, zoneinfo=zoneinfo): return pds.wrap_zone(zoneinfo.ZoneInfo.no_cache(key), key) return no_cache_timezone no_cache_timezone = _make_no_cache_timezone() del _make_no_cache_timezone def test_fixed_offset_utc(): """Tests that fixed_offset_timezone(0) always returns UTC.""" assert pds.fixed_offset_timezone(0) is pds.UTC @pytest.mark.parametrize("key", ["utc", "UTC"]) def test_timezone_utc_singleton(key): assert pds.timezone(key) is pds.UTC @hypothesis.given(minutes=offset_minute_strategy.filter(lambda m: m != 0)) def test_str_fixed_offset(minutes): shim_zone = pds.fixed_offset_timezone(minutes) assert str(shim_zone) == repr(shim_zone) @hypothesis.given(key=valid_zone_strategy) def test_timezone_repr(key): zone = pds.timezone(key) assert key in repr(zone) @hypothesis.given(dt=dt_strategy, key=valid_zone_strategy) def test_localize_default_is_dst(dt, key): """Tests localize when is_dst is not specified. With pytz, is_dst defaults to False, but with the shims, if is_dst is unspecified we will assume that the user is indifferent about what to do and do whatever the underlying behavior of the time zone we're shimming around does. """ shim_zone = pds.timezone(key) with pytest.warns(pds.PytzUsageWarning): dt_localized = shim_zone.localize(dt) dt_replaced = dt.replace(tzinfo=shim_zone) assert_dt_equivalent(dt_localized, dt_replaced) @hypothesis.given( dt=dt_strategy, delta=hst.timedeltas( min_value=timedelta(days=-730), max_value=timedelta(days=730) ), key=valid_zone_strategy, ) def test_normalize_pytz_zone(dt, delta, key): """Test that pds.normalize works on pytz-zoned datetimes. This allows you to take a zone you've normalized via pytz and convert it to the shim zone. """ pytz_zone = pytz.timezone(key) shim_zone = pds.timezone(key) dt_pytz = pytz_zone.localize(dt) + delta dt_pytz_normalized = pytz_zone.normalize(dt_pytz) with pytest.warns(pds.PytzUsageWarning): dt_shim = shim_zone.normalize(dt_pytz) rounded_shim = round_normalized(dt_shim) rounded_pytz = round_normalized(dt_pytz_normalized) rounded_shim_naive = rounded_shim.replace(tzinfo=None) rounded_pytz_naive = rounded_pytz.replace(tzinfo=None) hypothesis.assume(MIN_DATETIME <= rounded_shim_naive <= MAX_DATETIME) assert rounded_shim_naive == rounded_pytz_naive @pytest.mark.parametrize( "key, dt, offset", _zoneinfo_data.get_unambiguous_cases() ) def test_localize_unambiguous_build_tzinfo(key, dt, offset): zone = pds.build_tzinfo(key, _zoneinfo_data.get_zone_file_obj(key)) with pytest.warns(pds.PytzUsageWarning): dt_localized = zone.localize(dt) assert_dt_offset(dt_localized, offset) def _skip_transition(key, zt): if not PY2: return False if key == "Europe/Dublin" and ( zt.transition > datetime(1968, 1, 1) or zt.transition < datetime(1917, 1, 1) ): return True if key == "Africa/Casablanca" and zt.transition > datetime(2019, 1, 1): return True if key == "America/Santiago" and zt.transition < datetime(1912, 1, 1): return True return False def _localize_fold_cases(): cases = [] def _add_case(key, dt, fold, offset, skip=False): case = (key, dt, fold, offset) if skip: case = pytest.param(*case, marks=pytest.mark.skip) cases.append(case) for key, zt in _zoneinfo_data.get_fold_cases(): dt = zt.anomaly_start skip = _skip_transition(key, zt) _add_case(key, dt, 0, zt.offset_before, skip=skip) _add_case(key, dt, 1, zt.offset_after, skip=skip) dt = zt.anomaly_end - ONE_SECOND _add_case(key, dt, 0, zt.offset_before, skip=skip) _add_case(key, dt, 1, zt.offset_after, skip=skip) return tuple(cases) @pytest.mark.parametrize("key, dt, fold, offset", _localize_fold_cases()) def test_localize_folds(key, dt, fold, offset): zone = pds.build_tzinfo(key, _zoneinfo_data.get_zone_file_obj(key)) with pytest.warns(pds.PytzUsageWarning): dt_localized = zone.localize(enfold(dt, fold=fold)) assert_dt_offset(dt_localized, offset) def _localize_gap_cases(): cases = [] for key, zt in _zoneinfo_data.get_gap_cases(): dt = zt.anomaly_start cases.append((key, dt, 0, zt.offset_before)) cases.append((key, dt, 1, zt.offset_after)) dt = zt.anomaly_end - ONE_SECOND cases.append((key, dt, 0, zt.offset_before)) cases.append((key, dt, 1, zt.offset_after)) return tuple(cases) @pytest.mark.parametrize("key, dt, fold, offset", _localize_gap_cases()) @pytest.mark.skipif( PY2, reason="dateutil.tz doesn't properly support fold during gaps" ) def test_localize_gap(key, dt, fold, offset): zone = pds.build_tzinfo(key, _zoneinfo_data.get_zone_file_obj(key)) with pytest.warns(pds.PytzUsageWarning): dt_localized = zone.localize(enfold(dt, fold=fold)) assert_dt_offset(dt_localized, offset) def _folds_from_utc_cases(): cases = [] def _add_case(key, dt_utc, expected_fold, skip=False): case = (key, dt_utc, expected_fold) if skip: case = pytest.param(*case, marks=pytest.mark.skip) cases.append(case) for key, zt in _zoneinfo_data.get_fold_cases(): dt_utc = zt.transition_utc dt_before_utc = dt_utc - ONE_SECOND dt_after_utc = dt_utc + ONE_SECOND skip = _skip_transition(key, zt) _add_case(key, dt_before_utc, 0, skip=skip) _add_case(key, dt_after_utc, 1, skip=skip) return tuple(cases) @pytest.mark.parametrize("key, dt_utc, expected_fold", _folds_from_utc_cases()) def test_folds_from_utc(key, dt_utc, expected_fold): zone = pds.build_tzinfo(key, _zoneinfo_data.get_zone_file_obj(key)) dt = dt_utc.astimezone(zone) assert get_fold(dt) == expected_fold SHIM_ZONE_STRATEGY = hst.one_of( [ valid_zone_strategy.map(pds.timezone), valid_zone_strategy.map(no_cache_timezone), offset_minute_strategy.map(pds.fixed_offset_timezone), hst.just(pds.UTC), ] ) @hypothesis.given(shim_zone=SHIM_ZONE_STRATEGY) def test_unwrap_shim(shim_zone): """Tests that .unwrap(_shim) always does the same as upgrade_tzinfo().""" assert shim_zone.unwrap_shim() is pds.helpers.upgrade_tzinfo(shim_zone) @pytest.mark.skipif( PY2, reason="Currently dateutil does not have a .key attribute" ) def test_wrap_zone_default_key(): key = "America/New_York" zone = pds._compat.get_timezone(key) wrapped_zone = pds.wrap_zone(zone) assert str(wrapped_zone) == key def test_wrap_zone_no_key(): class TzInfo(tzinfo): def __init__(self): pass zone = TzInfo() with pytest.raises(TypeError): pds.wrap_zone(zone) @hypothesis.given(shim_zone=SHIM_ZONE_STRATEGY) def test_wrap_zone_same_object(shim_zone): """Tests unwrapping and rewrapping a shim zone. This should return the original shim object. """ unwrapped = shim_zone.unwrap_shim() with pytest.warns(pds.PytzUsageWarning): key = shim_zone.zone rewrapped = pds.wrap_zone(unwrapped, key=key) assert rewrapped is shim_zone @hypothesis.given(shim_zone=SHIM_ZONE_STRATEGY, key=ARBITRARY_KEY_STRATEGY) def test_wrap_zone_new_key(shim_zone, key): """Test that wrap_zone can set arbitrary keys. There are a bunch of layers of caching here, so we want to make sure that if we wrap a zone that may already live in a cache with a new shim that has a different key, the new wrapper will reflect that, and the existing shim class won't be affected. """ original_key = str(shim_zone) unwrapped = shim_zone.unwrap_shim() new_shim = pds.wrap_zone(unwrapped, key=key) assert str(new_shim) == key assert str(shim_zone) == original_key @hypothesis.given(shim_zone=SHIM_ZONE_STRATEGY) @pytest.mark.parametrize("copy_func", [copy.copy, copy.deepcopy,]) def test_copy(copy_func, shim_zone): shim_copy = copy_func(shim_zone) assert shim_copy is shim_zone @hypothesis.given(shim_zone=SHIM_ZONE_STRATEGY, dt=dt_strategy) @hypothesis.example( shim_zone=pds.timezone("America/New_York"), dt=datetime(2020, 11, 1, 1, 30) ) @hypothesis.example( shim_zone=no_cache_timezone("America/New_York"), dt=datetime(2020, 11, 1, 1, 30), ) @hypothesis.example( shim_zone=pds.timezone("America/New_York"), dt=enfold(datetime(2020, 11, 1, 1, 30), fold=1), ) @conditional_examples( not PY2, examples=[ hypothesis.example( shim_zone=no_cache_timezone("America/New_York"), dt=datetime(2020, 3, 8, 2, 30), ), hypothesis.example( shim_zone=no_cache_timezone("America/New_York"), dt=enfold(datetime(2020, 3, 8, 2, 30), fold=1), ), ], ) def test_pickle_round_trip(shim_zone, dt): """Test that the results of a pickle round trip are identical to inputs. Ideally we would want some metric of equality on the pickled objects themselves, but with time zones object equality is usually equivalent to object identity, and that is not universally preserved in pickle round tripping on all Python versions and for all zones. """ shim_copy = pickle.loads(pickle.dumps(shim_zone)) dt = dt.replace(tzinfo=shim_zone) dt_rt = dt.replace(tzinfo=shim_copy) # PEP 495 says that the result of an inter-zone comparison between # datetimes where the offset depends on the fold is always False. if shim_zone is not shim_copy and dt != dt_rt: assert dt.dst() == dt_rt.dst() assert dt.utcoffset() == dt_rt.utcoffset() assert not datetime_unambiguous(dt) assert not datetime_unambiguous(dt_rt) return assert_dt_equivalent(dt, dt_rt) @hypothesis.given(key=valid_zone_strategy) @pytest.mark.skipif(PY2, reason="Singleton behavior is different in Python 2") def test_pickle_returns_same_object(key): shim_zone = pds.timezone(key) shim_copy = pickle.loads(pickle.dumps(shim_zone)) assert shim_zone is shim_copy def test_utc_alias(): assert pds.utc is pds.UTC pytz-deprecation-shim-0.1.0.post0/tox.ini000066400000000000000000000054111367243561300203410ustar00rootroot00000000000000[tox] minversion = 3.3.0 isolated_build = True skip_missing_interpreters = true [testenv] description = Run the tests deps = attrs coverage[toml] hypothesis>=5.7.0; python_version>="3.6" hypothesis; python_version<="2.7" pytz; python_version!="2.7" pytz==2019.3; python_version=="2.7" pytest pytest-cov pytest-randomly pytest-xdist passenv = HYPOTHESIS_PROFILE PYTZ_TZDATADIR PYTHONTZPATH setenv = COVERAGE_FILE={toxworkdir}/.coverage/.coverage.{envname} commands = pytest {toxinidir} {posargs: {env:DEFAULT_TEST_POSARGS:--cov=pytz_deprecation_shim --cov=tests}} [testenv:coverage-report] skip_install = true deps = coverage[toml]>=5.0.2 depends = py38 setenv=COVERAGE_FILE=.coverage changedir = {toxworkdir}/.coverage commands = coverage combine coverage report coverage xml [testenv:codecov] description = [only run on CI]: upload coverage data to codecov (depends on coverage running first) deps = codecov depends = coverage-report passenv = CODECOV_TOKEN skip_install = True commands = python -m codecov --file {toxworkdir}/.coverage/coverage.xml [testenv:format] description = Run auto formatters skip_install = True whitelist_externals = bash deps = black isort commands = black . isort -rc docs src tests [testenv:precommit] description = Run the pre-commit hooks on all files skip_install = True deps = pre-commit commands = pre-commit install -f --install-hooks pre-commit run --all-files [testenv:update-test-data] description = Update the test data JSON file skip_install = True deps = backports.zoneinfo commands = python scripts/update_test_data.py [testenv:lint] description = Run linting checks skip_install = True deps = black isort pylint commands = black --check . isort --check-only --recursive docs src tests pylint src tests [testenv:docs] description = Build the documentation deps = -rdocs/requirements.txt commands = sphinx-build -d "{toxworkdir}/docs_doctree" "{toxinidir}/docs" \ "{toxinidir}/docs/_output" {posargs: -j auto --color -bhtml} [testenv:build] description = Build a wheel and source distribution skip_install = True passenv = * deps = pep517 commands = python -m pep517.build {posargs} {toxinidir} -o {toxinidir}/dist [testenv:build-check] description = Build a wheel and source distribution skip_install = True deps = twine depends = build commands = twine check dist/* [testenv:release] description = Make a release; must be called after "build" skip_install = True deps = twine depends = build passenv = TWINE_* commands = twine check {toxinidir}/dist/* twine upload {toxinidir}/dist/* \ {posargs:-r {env:TWINE_REPOSITORY:testpypi} --non-interactive}