Skip to content

Commit

Permalink
Merge pull request #650 from niccokunzmann/remove-pytz
Browse files Browse the repository at this point in the history
Remove pytz dependency
  • Loading branch information
stevepiercy committed Jun 25, 2024
2 parents 6604d02 + cc32aa6 commit 1bc9784
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 35 deletions.
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
config:
# [Python version, tox env]
- ["3.8", "py38"]
- ["3.8", "nopytz"]
- ["3.9", "py39"]
- ["3.10", "py310"]
- ["pypy-3.9", "pypy3"]
Expand Down
7 changes: 6 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ Breaking changes:
- Remove untested and broken ``LocalTimezone`` and ``FixedOffset`` tzinfo
sub-classes, see `Issue 67 <https://github.com/collective/icalendar/issues/67>`_

- Remove ``pytz`` as a dependency of ``icalendar``. If you require ``pytz``,
add it to your dependency list or install it additionally with::

pip install icalendar>=6.0.0 pytz

New features:

- ...
- Add function ``icalendar.use_pytz()``.

Bug fixes:

Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
tests_require = []
install_requires = [
'python-dateutil',
'pytz',
# install requirements depending on python version
# see https://www.python.org/dev/peps/pep-0508/#environment-markers
'backports.zoneinfo; python_version < "3.9"',
Expand Down
62 changes: 42 additions & 20 deletions src/icalendar/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import zoneinfo
import pytest
import icalendar
import pytz
try:
import pytz
except ImportError:
pytz = None
from datetime import datetime
from dateutil import tz
from icalendar.cal import Component, Calendar
from icalendar.timezone import tzp as _tzp
Expand All @@ -13,6 +17,21 @@
import itertools
import sys

HAS_PYTZ = pytz is not None
if HAS_PYTZ:
PYTZ_UTC = [
pytz.utc,
pytz.timezone('UTC'),
]
PYTZ_IN_TIMEZONE = [
lambda dt, tzname: pytz.timezone(tzname).localize(dt),
]
PYTZ_TZP = ["pytz"]
else:
PYTZ_UTC = []
PYTZ_IN_TIMEZONE = []
PYTZ_TZP = []


class DataSource:
'''A collection of parsed ICS elements (e.g calendars, timezones, events)'''
Expand Down Expand Up @@ -76,17 +95,14 @@ def timezones(tzp):
def events(tzp):
return DataSource(EVENTS_FOLDER, icalendar.Event.from_ical)

@pytest.fixture(params=[
pytz.utc,
@pytest.fixture(params=PYTZ_UTC + [
zoneinfo.ZoneInfo('UTC'),
pytz.timezone('UTC'),
tz.UTC,
tz.gettz('UTC')])
def utc(request, tzp):
return request.param

@pytest.fixture(params=[
lambda dt, tzname: pytz.timezone(tzname).localize(dt),
@pytest.fixture(params=PYTZ_IN_TIMEZONE + [
lambda dt, tzname: dt.replace(tzinfo=tz.gettz(tzname)),
lambda dt, tzname: dt.replace(tzinfo=zoneinfo.ZoneInfo(tzname))
])
Expand Down Expand Up @@ -193,7 +209,7 @@ def tzp(tzp_name):
_tzp.use_default()


@pytest.fixture(params=["pytz", "zoneinfo"])
@pytest.fixture(params=PYTZ_TZP + ["zoneinfo"])
def other_tzp(request, tzp):
"""This is annother timezone provider.
Expand All @@ -207,15 +223,13 @@ def other_tzp(request, tzp):
@pytest.fixture()
def pytz_only(tzp):
"""Skip tests that are not running under pytz."""
if not tzp.uses_pytz():
pytest.skip("Not using pytz. Skipping this test.")
assert tzp.uses_pytz()


@pytest.fixture()
def zoneinfo_only(tzp):
"""Skip tests that are not running under pytz."""
if not tzp.uses_zoneinfo():
pytest.skip("Not using zoneinfo. Skipping this test.")
def zoneinfo_only(tzp, request, tzp_name):
"""Skip tests that are not running under zoneinfo."""
assert tzp.uses_zoneinfo()


def pytest_generate_tests(metafunc):
Expand All @@ -228,12 +242,12 @@ def pytest_generate_tests(metafunc):
See https://docs.pytest.org/en/6.2.x/example/parametrize.html#deferring-the-setup-of-parametrized-resources
"""
if "tzp_name" in metafunc.fixturenames:
tzp_names = ["pytz", "zoneinfo"]
tzp_names = PYTZ_TZP + ["zoneinfo"]
if "zoneinfo_only" in metafunc.fixturenames:
tzp_names.remove("pytz")
if "pytz_only" in metafunc.fixturenames:
tzp_names.remove("zoneinfo")
assert tzp_names, "Use pytz_only or zoneinfo_only but not both!"
tzp_names = ["zoneinfo"]
if "pytz_only" in metafunc.fixturenames:
tzp_names = PYTZ_TZP
assert not ("zoneinfo_only" in metafunc.fixturenames and "pytz_only" in metafunc.fixturenames), "Use pytz_only or zoneinfo_only but not both!"
metafunc.parametrize("tzp_name", tzp_names, scope="module")


Expand All @@ -243,18 +257,26 @@ def __repr__(self):
return f"ZoneInfo(key={repr(self.key)})"


def test_print(obj):
def doctest_print(obj):
"""doctest print"""
if isinstance(obj, bytes):
obj = obj.decode("UTF-8")
print(str(obj).strip().replace("\r\n", "\n").replace("\r", "\n"))


def doctest_import(name, *args, **kw):
"""Replace the import mechanism to skip the whole doctest if we import pytz."""
if name == "pytz":
return pytz
return __import__(name, *args, **kw)

@pytest.fixture()
def env_for_doctest(monkeypatch):
"""Modify the environment to make doctests run."""
monkeypatch.setitem(sys.modules, "zoneinfo", zoneinfo)
monkeypatch.setattr(zoneinfo, "ZoneInfo", DoctestZoneInfo)
from icalendar.timezone.zoneinfo import ZONEINFO
monkeypatch.setattr(ZONEINFO, "utc", zoneinfo.ZoneInfo("UTC"))
return {"print": test_print}
return {
"print": doctest_print
}
8 changes: 6 additions & 2 deletions src/icalendar/tests/prop/test_identity_and_equality.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@
from icalendar import vDDDTypes
from datetime import datetime, date, time
from icalendar.timezone.zoneinfo import zoneinfo
import pytz
try:
import pytz
except ImportError:
pytz = None
from dateutil import tz
import pytest
from copy import deepcopy



vDDDTypes_list = [
vDDDTypes(pytz.timezone('EST').localize(datetime(year=2022, month=7, day=22, hour=12, minute=7))),
vDDDTypes(datetime(year=2022, month=7, day=22, hour=12, minute=7, tzinfo=zoneinfo.ZoneInfo("Europe/London"))),
vDDDTypes(datetime(year=2022, month=7, day=22, hour=12, minute=7)),
vDDDTypes(datetime(year=2022, month=7, day=22, hour=12, minute=7, tzinfo=tz.UTC)),
vDDDTypes(date(year=2022, month=7, day=22)),
vDDDTypes(date(year=2022, month=7, day=23)),
vDDDTypes(time(hour=22, minute=7, second=2))
]
if pytz:
vDDDTypes_list.append(vDDDTypes(pytz.timezone('EST').localize(datetime(year=2022, month=7, day=22, hour=12, minute=7))),)

def identity(x):
return x
Expand Down
5 changes: 4 additions & 1 deletion src/icalendar/tests/test_equality.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Test the equality and inequality of components."""
import copy
import pytz
try:
import pytz
except ImportError:
pytz = None
from icalendar.prop import *
from datetime import datetime, date, timedelta
import pytest
Expand Down
22 changes: 17 additions & 5 deletions src/icalendar/tests/test_pytz_zoneinfo_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,31 @@
These are mostly located in icalendar.timezone.
"""
import pytz
try:
import pytz
from icalendar.timezone.pytz import PYTZ
except ImportError:
pytz = None
from icalendar.timezone.zoneinfo import zoneinfo, ZONEINFO
from dateutil.tz.tz import _tzicalvtz
from icalendar.timezone.pytz import PYTZ
import pytest
import copy
import pickle
from dateutil.rrule import rrule, MONTHLY
from datetime import datetime

if pytz:
PYTZ_TIMEZONES = pytz.all_timezones
TZP_ = [PYTZ(), ZONEINFO()]
NEW_TZP_NAME = ["pytz", "zoneinfo"]
else:
PYTZ_TIMEZONES = []
TZP_ = [ZONEINFO()]
NEW_TZP_NAME = ["zoneinfo"]

@pytest.mark.parametrize("tz_name", pytz.all_timezones + list(zoneinfo.available_timezones()))
@pytest.mark.parametrize("tzp_", [PYTZ(), ZONEINFO()])

@pytest.mark.parametrize("tz_name", PYTZ_TIMEZONES + list(zoneinfo.available_timezones()))
@pytest.mark.parametrize("tzp_", TZP_)
def test_timezone_names_are_known(tz_name, tzp_):
"""Make sure that all timezones are understood."""
if tz_name in ("Factory", "localtime"):
Expand Down Expand Up @@ -55,7 +67,7 @@ def test_cache_reuse_timezone_cache(tzp, timezones):
assert tzp1 is tzp.timezone("custom_Pacific/Fiji"), "Cache is not replaced."


@pytest.mark.parametrize("new_tzp_name", ["pytz", "zoneinfo"])
@pytest.mark.parametrize("new_tzp_name", NEW_TZP_NAME)
def test_cache_is_emptied_when_tzp_is_switched(tzp, timezones, new_tzp_name):
"""Make sure we do not reuse the timezones created when we switch the provider."""
tzp.cache_timezone_component(timezones.pacific_fiji)
Expand Down
21 changes: 18 additions & 3 deletions src/icalendar/tests/test_with_doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import os
import pytest
import importlib
import sys
import re

HERE = os.path.dirname(__file__) or "."
ICALENDAR_PATH = os.path.dirname(HERE)
Expand All @@ -35,7 +37,12 @@ def test_this_module_is_among_them():
@pytest.mark.parametrize("module_name", MODULE_NAMES)
def test_docstring_of_python_file(module_name):
"""This test runs doctest on the Python module."""
module = importlib.import_module(module_name)
try:
module = importlib.import_module(module_name)
except ModuleNotFoundError as e:
if e.name == "pytz":
pytest.skip("pytz is not installed, skipping this module.")
raise
test_result = doctest.testmod(module, name=module_name)
assert test_result.failed == 0, f"{test_result.failed} errors in {module_name}"

Expand All @@ -62,14 +69,22 @@ def test_files_is_included(filename):


@pytest.mark.parametrize("document", DOCUMENT_PATHS)
def test_documentation_file(document, zoneinfo_only, env_for_doctest):
def test_documentation_file(document, zoneinfo_only, env_for_doctest, tzp):
"""This test runs doctest on a documentation file.
functions are also replaced to work.
"""
test_result = doctest.testfile(document, module_relative=False, globs=env_for_doctest)
try:
test_result = doctest.testfile(document, module_relative=False, globs=env_for_doctest, raise_on_error=True)
except doctest.UnexpectedException as e:
ty, err, tb = e.exc_info
if issubclass(ty, ModuleNotFoundError) and err.name == "pytz":
pytest.skip("pytz not installed, skipping this file.")
finally:
tzp.use_zoneinfo()
assert test_result.failed == 0, f"{test_result.failed} errors in {os.path.basename(document)}"


def test_can_import_zoneinfo(env_for_doctest):
"""Allow importing zoneinfo for tests."""
assert "zoneinfo" in sys.modules
2 changes: 1 addition & 1 deletion src/icalendar/timezone/tzp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from dateutil.rrule import rrule


DEFAULT_TIMEZONE_PROVIDER = "pytz"
DEFAULT_TIMEZONE_PROVIDER = "zoneinfo"


class TZP:
Expand Down
21 changes: 20 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# to run for a specific environment, use ``tox -e ENVNAME``
[tox]
envlist = py38,py39,py310,py311,312,pypy3,docs
envlist = py38,py39,py310,py311,312,pypy3,docs,nopytz
# Note: the 'docs' env creates a 'build' directory which may interfere in strange ways
# with the other environments. You might see this when you run the tests in parallel.
# See https://github.com/collective/icalendar/pull/359#issuecomment-1214150269
Expand All @@ -11,11 +11,30 @@ deps =
pytest
coverage
hypothesis
pytz
commands =
coverage run --source=src/icalendar --omit=*/tests/hypothesis/* --omit=*/tests/fuzzed/* --module pytest []
coverage report
coverage html

[testenv:nopytz]
# install with dependencies
usedevelop = False
# use lowest version
basepython = python3.8
allowlist_externals =
rm
deps =
setuptools>=70.1.0
pytest
coverage
hypothesis
commands =
rm -rf build # do not mess up import
coverage run --source=src/icalendar --omit=*/tests/hypothesis/* --omit=*/tests/fuzzed/* --module pytest []
coverage report
coverage html

[testenv:docs]
deps =
-r {toxinidir}/requirements_docs.txt
Expand Down

0 comments on commit 1bc9784

Please sign in to comment.