Merge pull request #789 from niccokunzmann/ruff-formmat

Format the source code with ruff
pull/787/head^2
Nicco Kunzmann 2025-03-28 11:43:45 +00:00 zatwierdzone przez GitHub
commit 4cbac76190
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
32 zmienionych plików z 5100 dodań i 2083 usunięć

Wyświetl plik

@ -6,7 +6,7 @@ Changelog
Minor changes:
- ...
- Use ``ruff`` to format the source code.
Breaking changes:

Wyświetl plik

@ -27,7 +27,7 @@ from optparse import OptionParser
tmpeggs = tempfile.mkdtemp()
usage = '''\
usage = """\
[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
Bootstraps a buildout-based project.
@ -37,28 +37,40 @@ Python that you want bin/buildout to use.
Note that by using --find-links to point to local resources, you can keep
this script from going over the network.
'''
"""
parser = OptionParser(usage=usage)
parser.add_option("-v", "--version", help="use a specific zc.buildout version")
parser.add_option("-t", "--accept-buildout-test-releases",
dest='accept_buildout_test_releases',
action="store_true", default=False,
help=("Normally, if you do not specify a --version, the "
"bootstrap script and buildout gets the newest "
"*final* versions of zc.buildout and its recipes and "
"extensions for you. If you use this flag, "
"bootstrap and buildout will get the newest releases "
"even if they are alphas or betas."))
parser.add_option("-c", "--config-file",
help=("Specify the path to the buildout configuration "
"file to be used."))
parser.add_option("-f", "--find-links",
help=("Specify a URL to search for buildout releases"))
parser.add_option("--allow-site-packages",
action="store_true", default=False,
help=("Let bootstrap.py use existing site packages"))
parser.add_option(
"-t",
"--accept-buildout-test-releases",
dest="accept_buildout_test_releases",
action="store_true",
default=False,
help=(
"Normally, if you do not specify a --version, the "
"bootstrap script and buildout gets the newest "
"*final* versions of zc.buildout and its recipes and "
"extensions for you. If you use this flag, "
"bootstrap and buildout will get the newest releases "
"even if they are alphas or betas."
),
)
parser.add_option(
"-c",
"--config-file",
help=("Specify the path to the buildout configuration " "file to be used."),
)
parser.add_option(
"-f", "--find-links", help=("Specify a URL to search for buildout releases")
)
parser.add_option(
"--allow-site-packages",
action="store_true",
default=False,
help=("Let bootstrap.py use existing site packages"),
)
options, args = parser.parse_args()
@ -75,21 +87,22 @@ except ImportError:
from urllib2 import urlopen
ez = {}
exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
exec(urlopen("https://bootstrap.pypa.io/ez_setup.py").read(), ez)
if not options.allow_site_packages:
# ez_setup imports site, which adds site packages
# this will remove them from the path to ensure that incompatible versions
# of setuptools are not in the path
import site
# inside a virtualenv, there is no 'getsitepackages'.
# We can't remove these reliably
if hasattr(site, 'getsitepackages'):
if hasattr(site, "getsitepackages"):
for sitepackage_path in site.getsitepackages():
sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
setup_args = {'to_dir': tmpeggs, 'download_delay': 0}
ez['use_setuptools'](**setup_args)
setup_args = {"to_dir": tmpeggs, "download_delay": 0}
ez["use_setuptools"](**setup_args)
import setuptools
import pkg_resources
@ -104,36 +117,43 @@ for path in sys.path:
ws = pkg_resources.working_set
cmd = [sys.executable, '-c',
'from setuptools.command.easy_install import main; main()',
'-mZqNxd', tmpeggs]
cmd = [
sys.executable,
"-c",
"from setuptools.command.easy_install import main; main()",
"-mZqNxd",
tmpeggs,
]
find_links = os.environ.get(
'bootstrap-testing-find-links',
options.find_links or
('https://downloads.buildout.org/'
if options.accept_buildout_test_releases else None)
)
"bootstrap-testing-find-links",
options.find_links
or (
"https://downloads.buildout.org/"
if options.accept_buildout_test_releases
else None
),
)
if find_links:
cmd.extend(['-f', find_links])
cmd.extend(["-f", find_links])
setuptools_path = ws.find(
pkg_resources.Requirement.parse('setuptools')).location
setuptools_path = ws.find(pkg_resources.Requirement.parse("setuptools")).location
requirement = 'zc.buildout'
requirement = "zc.buildout"
version = options.version
if version is None and not options.accept_buildout_test_releases:
# Figure out the most recent final version of zc.buildout.
import setuptools.package_index
_final_parts = '*final-', '*final'
_final_parts = "*final-", "*final"
def _final_version(parsed_version):
for part in parsed_version:
if (part.startswith('*')) and (part not in _final_parts):
if (part.startswith("*")) and (part not in _final_parts):
return False
return True
index = setuptools.package_index.PackageIndex(
search_path=[setuptools_path])
index = setuptools.package_index.PackageIndex(search_path=[setuptools_path])
if find_links:
index.add_find_links((find_links,))
req = pkg_resources.Requirement.parse(requirement)
@ -152,13 +172,13 @@ if version is None and not options.accept_buildout_test_releases:
best.sort()
version = best[-1].version
if version:
requirement = '=='.join((requirement, version))
requirement = "==".join((requirement, version))
cmd.append(requirement)
import subprocess
if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
raise Exception(
"Failed to execute command:\n%s" % repr(cmd)[1:-1])
raise Exception("Failed to execute command:\n%s" % repr(cmd)[1:-1])
######################################################################
# Import and run buildout
@ -167,12 +187,12 @@ ws.add_entry(tmpeggs)
ws.require(requirement)
import zc.buildout.buildout
if not [a for a in args if '=' not in a]:
args.append('bootstrap')
if not [a for a in args if "=" not in a]:
args.append("bootstrap")
# if -c was provided, we push it back into args for buildout' main function
if options.config_file is not None:
args[0:0] = ['-c', options.config_file]
args[0:0] = ["-c", options.config_file]
zc.buildout.buildout.main(args)
shutil.rmtree(tmpeggs)

Wyświetl plik

@ -3,46 +3,45 @@ import importlib.metadata
import datetime
import os
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
try:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
except ImportError:
html_theme = 'default'
html_theme = "default"
if not on_rtd:
print('-' * 74)
print('Warning: sphinx-rtd-theme not installed, building with default '
'theme.')
print('-' * 74)
print("-" * 74)
print(
"Warning: sphinx-rtd-theme not installed, building with default " "theme."
)
print("-" * 74)
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'sphinx_copybutton',
'sphinx.ext.intersphinx',
'sphinx.ext.autosectionlabel',
"sphinx.ext.autodoc",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
"sphinx_copybutton",
"sphinx.ext.intersphinx",
"sphinx.ext.autosectionlabel",
]
source_suffix = '.rst'
master_doc = 'index'
source_suffix = ".rst"
master_doc = "index"
project = 'icalendar'
project = "icalendar"
this_year = datetime.date.today().year
copyright = f'{this_year}, Plone Foundation'
version = importlib.metadata.version('icalendar')
copyright = f"{this_year}, Plone Foundation"
version = importlib.metadata.version("icalendar")
release = version
exclude_patterns = ['_build', 'lib', 'bin', 'include', 'local']
pygments_style = 'sphinx'
exclude_patterns = ["_build", "lib", "bin", "include", "local"]
pygments_style = "sphinx"
htmlhelp_basename = 'icalendardoc'
htmlhelp_basename = "icalendardoc"
man_pages = [
('index', 'icalendar', 'icalendar Documentation',
['Plone Foundation'], 1)
]
man_pages = [("index", "icalendar", "icalendar Documentation", ["Plone Foundation"], 1)]
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),

Wyświetl plik

@ -23,24 +23,27 @@ from icalendar.tools import is_date, normalize_pytz, to_datetime
if TYPE_CHECKING:
from datetime import datetime
Parent = Union[Event,Todo]
Parent = Union[Event, Todo]
class IncompleteAlarmInformation(ValueError):
"""The alarms cannot be calculated yet because information is missing."""
class ComponentStartMissing(IncompleteAlarmInformation):
"""We are missing the start of a component that the alarm is for.
Use Alarms.set_start().
"""
class ComponentEndMissing(IncompleteAlarmInformation):
"""We are missing the end of a component that the alarm is for.
Use Alarms.set_end().
"""
class LocalTimezoneMissing(IncompleteAlarmInformation):
"""We are missing the local timezone to compute the value.
@ -52,13 +55,13 @@ class AlarmTime:
"""An alarm time with all the information."""
def __init__(
self,
alarm: Alarm,
trigger : datetime,
acknowledged_until:Optional[datetime]=None,
snoozed_until:Optional[datetime]=None,
parent: Optional[Parent]=None,
):
self,
alarm: Alarm,
trigger: datetime,
acknowledged_until: Optional[datetime] = None,
snoozed_until: Optional[datetime] = None,
parent: Optional[Parent] = None,
):
"""Create a new AlarmTime.
alarm
@ -191,22 +194,22 @@ class Alarms:
This is not implemented yet.
"""
def __init__(self, component:Optional[Alarm|Event|Todo]=None):
def __init__(self, component: Optional[Alarm | Event | Todo] = None):
"""Start computing alarm times."""
self._absolute_alarms : list[Alarm] = []
self._start_alarms : list[Alarm] = []
self._end_alarms : list[Alarm] = []
self._start : Optional[date] = None
self._end : Optional[date] = None
self._parent : Optional[Parent] = None
self._last_ack : Optional[datetime] = None
self._snooze_until : Optional[datetime] = None
self._local_tzinfo : Optional[tzinfo] = None
self._absolute_alarms: list[Alarm] = []
self._start_alarms: list[Alarm] = []
self._end_alarms: list[Alarm] = []
self._start: Optional[date] = None
self._end: Optional[date] = None
self._parent: Optional[Parent] = None
self._last_ack: Optional[datetime] = None
self._snooze_until: Optional[datetime] = None
self._local_tzinfo: Optional[tzinfo] = None
if component is not None:
self.add_component(component)
def add_component(self, component:Alarm|Parent):
def add_component(self, component: Alarm | Parent):
"""Add a component.
If this is an alarm, it is added.
@ -263,7 +266,7 @@ class Alarms:
"""
self._end = dt
def _add(self, dt: date, td:timedelta):
def _add(self, dt: date, td: timedelta):
"""Add a timedelta to a datetime."""
if is_date(dt):
if td.seconds == 0:
@ -294,7 +297,7 @@ class Alarms:
"""
self._snooze_until = tzp.localize_utc(dt) if dt is not None else None
def set_local_timezone(self, tzinfo:Optional[tzinfo|str]):
def set_local_timezone(self, tzinfo: Optional[tzinfo | str]):
"""Set the local timezone.
Events are sometimes in local time.
@ -318,30 +321,32 @@ class Alarms:
If you forget to set the acknowledged times, that is not problem.
"""
return (
self._get_end_alarm_times() +
self._get_start_alarm_times() +
self._get_absolute_alarm_times()
self._get_end_alarm_times()
+ self._get_start_alarm_times()
+ self._get_absolute_alarm_times()
)
def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]:
"""The times when the alarm is triggered relative to start."""
yield first # we trigger at the start
yield first # we trigger at the start
repeat = alarm.REPEAT
duration = alarm.DURATION
if repeat and duration:
for i in range(1, repeat + 1):
yield self._add(first, duration * i)
def _alarm_time(self, alarm: Alarm, trigger:date):
def _alarm_time(self, alarm: Alarm, trigger: date):
"""Create an alarm time with the additional attributes."""
if getattr(trigger, "tzinfo", None) is None and self._local_tzinfo is not None:
trigger = normalize_pytz(trigger.replace(tzinfo=self._local_tzinfo))
return AlarmTime(alarm, trigger, self._last_ack, self._snooze_until, self._parent)
return AlarmTime(
alarm, trigger, self._last_ack, self._snooze_until, self._parent
)
def _get_absolute_alarm_times(self) -> list[AlarmTime]:
"""Return a list of absolute alarm times."""
return [
self._alarm_time(alarm , trigger)
self._alarm_time(alarm, trigger)
for alarm in self._absolute_alarms
for trigger in self._repeat(alarm.TRIGGER, alarm)
]
@ -349,9 +354,11 @@ class Alarms:
def _get_start_alarm_times(self) -> list[AlarmTime]:
"""Return a list of alarm times relative to the start of the component."""
if self._start is None and self._start_alarms:
raise ComponentStartMissing("Use Alarms.set_start because at least one alarm is relative to the start of a component.")
raise ComponentStartMissing(
"Use Alarms.set_start because at least one alarm is relative to the start of a component."
)
return [
self._alarm_time(alarm , trigger)
self._alarm_time(alarm, trigger)
for alarm in self._start_alarms
for trigger in self._repeat(self._add(self._start, alarm.TRIGGER), alarm)
]
@ -359,9 +366,11 @@ class Alarms:
def _get_end_alarm_times(self) -> list[AlarmTime]:
"""Return a list of alarm times relative to the start of the component."""
if self._end is None and self._end_alarms:
raise ComponentEndMissing("Use Alarms.set_end because at least one alarm is relative to the end of a component.")
raise ComponentEndMissing(
"Use Alarms.set_end because at least one alarm is relative to the end of a component."
)
return [
self._alarm_time(alarm , trigger)
self._alarm_time(alarm, trigger)
for alarm in self._end_alarms
for trigger in self._repeat(self._add(self._end, alarm.TRIGGER), alarm)
]
@ -378,11 +387,12 @@ class Alarms:
"""
return [alarm_time for alarm_time in self.times if alarm_time.is_active()]
__all__ = [
"Alarms",
"AlarmTime",
"IncompleteAlarmInformation",
"ComponentEndMissing",
"ComponentStartMissing",
"LocalTimezoneMissing"
"LocalTimezoneMissing",
]

Plik diff jest za duży Load Diff

Wyświetl plik

@ -14,10 +14,8 @@ def canonsort_keys(keys, canonical_order=None):
def canonsort_items(dict1, canonical_order=None):
"""Returns a list of items from dict1, sorted by canonical_order.
"""
return [(k, dict1[k]) for k
in canonsort_keys(dict1.keys(), canonical_order)]
"""Returns a list of items from dict1, sorted by canonical_order."""
return [(k, dict1[k]) for k in canonsort_keys(dict1.keys(), canonical_order)]
class CaselessDict(OrderedDict):
@ -26,8 +24,7 @@ class CaselessDict(OrderedDict):
"""
def __init__(self, *args, **kwargs):
"""Set keys to upper for initial dict.
"""
"""Set keys to upper for initial dict."""
super().__init__(*args, **kwargs)
for key, value in self.items():
key_upper = to_unicode(key).upper()
@ -74,7 +71,7 @@ class CaselessDict(OrderedDict):
# Multiple keys where key1.upper() == key2.upper() will be lost.
mappings = list(args) + [kwargs]
for mapping in mappings:
if hasattr(mapping, 'items'):
if hasattr(mapping, "items"):
mapping = iter(mapping.items())
for key, value in mapping:
self[key] = value
@ -83,7 +80,7 @@ class CaselessDict(OrderedDict):
return type(self)(super().copy())
def __repr__(self):
return f'{type(self).__name__}({dict(self)})'
return f"{type(self).__name__}({dict(self)})"
def __eq__(self, other):
return self is other or dict(self.items()) == dict(other.items())

Wyświetl plik

@ -1,5 +1,6 @@
#!/usr/bin/env python3
"""utility program that allows user to preview calendar's events"""
import sys
import pathlib
import argparse
@ -7,6 +8,7 @@ from datetime import datetime
from icalendar import Calendar, __version__
def _format_name(address):
"""Retrieve the e-mail and the name from an address.
@ -14,10 +16,10 @@ def _format_name(address):
:returns str: The name and the e-mail address.
"""
email = address.split(':')[-1]
name = email.split('@')[0]
email = address.split(":")[-1]
name = email.split("@")[0]
if not email:
return ''
return ""
return f"{name} <{email}>"
@ -30,33 +32,34 @@ def _format_attendees(attendees):
"""
if isinstance(attendees, str):
attendees = [attendees]
return '\n'.join(map(lambda s: s.rjust(len(s) + 5), map(_format_name, attendees)))
return "\n".join(map(lambda s: s.rjust(len(s) + 5), map(_format_name, attendees)))
def view(event):
"""Make a human readable summary of an iCalendar file.
:returns str: Human readable summary.
"""
summary = event.get('summary', default='')
organizer = _format_name(event.get('organizer', default=''))
attendees = _format_attendees(event.get('attendee', default=[]))
location = event.get('location', default='')
comment = event.get('comment', '')
description = event.get('description', '').split('\n')
description = '\n'.join(map(lambda s: s.rjust(len(s) + 5), description))
summary = event.get("summary", default="")
organizer = _format_name(event.get("organizer", default=""))
attendees = _format_attendees(event.get("attendee", default=[]))
location = event.get("location", default="")
comment = event.get("comment", "")
description = event.get("description", "").split("\n")
description = "\n".join(map(lambda s: s.rjust(len(s) + 5), description))
start = event.decoded('dtstart')
if 'duration' in event:
end = event.decoded('dtend', default=start + event.decoded('duration'))
start = event.decoded("dtstart")
if "duration" in event:
end = event.decoded("dtend", default=start + event.decoded("duration"))
else:
end = event.decoded('dtend', default=start)
duration = event.decoded('duration', default=end - start)
end = event.decoded("dtend", default=start)
duration = event.decoded("duration", default=end - start)
if isinstance(start, datetime):
start = start.astimezone()
start = start.strftime('%c')
start = start.strftime("%c")
if isinstance(end, datetime):
end = end.astimezone()
end = end.strftime('%c')
end = end.strftime("%c")
return f""" Organizer: {organizer}
Attendees:
@ -70,21 +73,33 @@ def view(event):
Description:
{description}"""
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('calendar_files', nargs='+', type=pathlib.Path)
parser.add_argument('--output', '-o', type=argparse.FileType('w'), default=sys.stdout, help='output file')
parser.add_argument('-v', '--version', action='version', version=f'{parser.prog} version {__version__}')
parser.add_argument("calendar_files", nargs="+", type=pathlib.Path)
parser.add_argument(
"--output",
"-o",
type=argparse.FileType("w"),
default=sys.stdout,
help="output file",
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"{parser.prog} version {__version__}",
)
argv = parser.parse_args()
for calendar_file in argv.calendar_files:
with open(calendar_file, encoding='utf-8-sig') as f:
with open(calendar_file, encoding="utf-8-sig") as f:
calendar = Calendar.from_ical(f.read())
for event in calendar.walk('vevent'):
argv.output.write(view(event) + '\n\n')
for event in calendar.walk("vevent"):
argv.output.write(view(event) + "\n\n")
__all__ = ["main", "view"]
if __name__ == '__main__':
if __name__ == "__main__":
main()

Wyświetl plik

@ -23,12 +23,30 @@ with atheris.instrument_imports():
from icalendar.tests.fuzzed import fuzz_calendar_v1
_value_error_matches = [
"component", "parse", "Expected", "Wrong date format", "END encountered",
"vDDD", 'recurrence', 'Offset must', 'Invalid iCalendar',
'alue MUST', 'Key name', 'Invalid content line', 'does not exist',
'base 64', 'must use datetime', 'Unknown date type', 'Wrong',
'Start time', 'iCalendar', 'recurrence', 'float, float', 'utc offset',
'parent', 'MUST be a datetime'
"component",
"parse",
"Expected",
"Wrong date format",
"END encountered",
"vDDD",
"recurrence",
"Offset must",
"Invalid iCalendar",
"alue MUST",
"Key name",
"Invalid content line",
"does not exist",
"base 64",
"must use datetime",
"Unknown date type",
"Wrong",
"Start time",
"iCalendar",
"recurrence",
"float, float",
"utc offset",
"parent",
"MUST be a datetime",
]
@ -43,11 +61,18 @@ def TestOneInput(data):
try:
# print the ICS file for the test case extraction
# see https://stackoverflow.com/a/27367173/1320237
print(base64.b64encode(calendar_string.encode("UTF-8", "surrogateescape")).decode("ASCII"))
except UnicodeEncodeError: pass
print(
base64.b64encode(
calendar_string.encode("UTF-8", "surrogateescape")
).decode("ASCII")
)
except UnicodeEncodeError:
pass
print("--- end calendar ---")
fuzz_calendar_v1(icalendar.Calendar.from_ical, calendar_string, multiple, should_walk)
fuzz_calendar_v1(
icalendar.Calendar.from_ical, calendar_string, multiple, should_walk
)
except ValueError as e:
if any(m in str(e) for m in _value_error_matches):
return -1

Wyświetl plik

@ -15,38 +15,43 @@ import re
def escape_char(text):
"""Format value according to iCalendar TEXT escaping rules.
"""
"""Format value according to iCalendar TEXT escaping rules."""
assert isinstance(text, (str, bytes))
# NOTE: ORDER MATTERS!
return text.replace(r'\N', '\n')\
.replace('\\', '\\\\')\
.replace(';', r'\;')\
.replace(',', r'\,')\
.replace('\r\n', r'\n')\
.replace('\n', r'\n')
return (
text.replace(r"\N", "\n")
.replace("\\", "\\\\")
.replace(";", r"\;")
.replace(",", r"\,")
.replace("\r\n", r"\n")
.replace("\n", r"\n")
)
def unescape_char(text):
assert isinstance(text, (str, bytes))
# NOTE: ORDER MATTERS!
if isinstance(text, str):
return text.replace('\\N', '\\n')\
.replace('\r\n', '\n')\
.replace('\\n', '\n')\
.replace('\\,', ',')\
.replace('\\;', ';')\
.replace('\\\\', '\\')
return (
text.replace("\\N", "\\n")
.replace("\r\n", "\n")
.replace("\\n", "\n")
.replace("\\,", ",")
.replace("\\;", ";")
.replace("\\\\", "\\")
)
elif isinstance(text, bytes):
return text.replace(b'\\N', b'\\n')\
.replace(b'\r\n', b'\n')\
.replace(b'\\n', b'\n')\
.replace(b'\\,', b',')\
.replace(b'\\;', b';')\
.replace(b'\\\\', b'\\')
return (
text.replace(b"\\N", b"\\n")
.replace(b"\r\n", b"\n")
.replace(b"\\n", b"\n")
.replace(b"\\,", b",")
.replace(b"\\;", b";")
.replace(b"\\\\", b"\\")
)
def foldline(line, limit=75, fold_sep='\r\n '):
def foldline(line, limit=75, fold_sep="\r\n "):
"""Make a string folded as defined in RFC5545
Lines of text SHOULD NOT be longer than 75 octets, excluding the line
break. Long content lines SHOULD be split into a multiple line
@ -56,16 +61,16 @@ def foldline(line, limit=75, fold_sep='\r\n '):
SPACE or HTAB).
"""
assert isinstance(line, str)
assert '\n' not in line
assert "\n" not in line
# Use a fast and simple variant for the common case that line is all ASCII.
try:
line.encode('ascii')
line.encode("ascii")
except (UnicodeEncodeError, UnicodeDecodeError):
pass
else:
return fold_sep.join(
line[i:i + limit - 1] for i in range(0, len(line), limit - 1)
line[i : i + limit - 1] for i in range(0, len(line), limit - 1)
)
ret_chars = []
@ -78,15 +83,15 @@ def foldline(line, limit=75, fold_sep='\r\n '):
byte_count = char_byte_len
ret_chars.append(char)
return ''.join(ret_chars)
return "".join(ret_chars)
#################################################################
# Property parameter stuff
def param_value(value):
"""Returns a parameter value.
"""
"""Returns a parameter value."""
if isinstance(value, SEQUENCE_TYPES):
return q_join(value)
elif isinstance(value, str):
@ -99,13 +104,13 @@ def param_value(value):
# [\w-] because of the iCalendar RFC
# . because of the vCard RFC
NAME = re.compile(r'[\w.-]+')
NAME = re.compile(r"[\w.-]+")
UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F",:;]')
QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F"]')
FOLD = re.compile(b'(\r?\n)+[ \t]')
uFOLD = re.compile('(\r?\n)+[ \t]')
NEWLINE = re.compile(r'\r?\n')
UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f",:;]')
QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f"]')
FOLD = re.compile(b"(\r?\n)+[ \t]")
uFOLD = re.compile("(\r?\n)+[ \t]")
NEWLINE = re.compile(r"\r?\n")
def validate_token(name):
@ -127,8 +132,7 @@ QUOTABLE = re.compile("[,;: ']")
def dquote(val):
"""Enclose parameter values containing [,;:] in double quotes.
"""
"""Enclose parameter values containing [,;:] in double quotes."""
# a double-quote character is forbidden to appear in a parameter value
# so replace it with a single-quote character
val = val.replace('"', "'")
@ -138,9 +142,8 @@ def dquote(val):
# parsing helper
def q_split(st, sep=',', maxsplit=-1):
"""Splits a string on char, taking double (q)uotes into considderation.
"""
def q_split(st, sep=",", maxsplit=-1):
"""Splits a string on char, taking double (q)uotes into considderation."""
if maxsplit == 0:
return [st]
@ -162,9 +165,8 @@ def q_split(st, sep=',', maxsplit=-1):
return result
def q_join(lst, sep=','):
"""Joins a list on sep, quoting strings with QUOTABLE chars.
"""
def q_join(lst, sep=","):
"""Joins a list on sep, quoting strings with QUOTABLE chars."""
return sep.join(dquote(itm) for itm in lst)
@ -179,24 +181,24 @@ class Parameters(CaselessDict):
"""
return self.keys()
# TODO?
# Later, when I get more time... need to finish this off now. The last major
# thing missing.
# def _encode(self, name, value, cond=1):
# # internal, for conditional convertion of values.
# if cond:
# klass = types_factory.for_property(name)
# return klass(value)
# return value
#
# def add(self, name, value, encode=0):
# "Add a parameter value and optionally encode it."
# if encode:
# value = self._encode(name, value, encode)
# self[name] = value
#
# def decoded(self, name):
# "returns a decoded value, or list of same"
# TODO?
# Later, when I get more time... need to finish this off now. The last major
# thing missing.
# def _encode(self, name, value, cond=1):
# # internal, for conditional convertion of values.
# if cond:
# klass = types_factory.for_property(name)
# return klass(value)
# return value
#
# def add(self, name, value, encode=0):
# "Add a parameter value and optionally encode it."
# if encode:
# value = self._encode(name, value, encode)
# self[name] = value
#
# def decoded(self, name):
# "returns a decoded value, or list of same"
def to_ical(self, sorted=True):
result = []
@ -210,8 +212,8 @@ class Parameters(CaselessDict):
value = value.encode(DEFAULT_ENCODING)
# CaselessDict keys are always unicode
key = key.upper().encode(DEFAULT_ENCODING)
result.append(key + b'=' + value)
return b';'.join(result)
result.append(key + b"=" + value)
return b";".join(result)
@classmethod
def from_ical(cls, st, strict=False):
@ -219,14 +221,14 @@ class Parameters(CaselessDict):
# parse into strings
result = cls()
for param in q_split(st, ';'):
for param in q_split(st, ";"):
try:
key, val = q_split(param, '=', maxsplit=1)
key, val = q_split(param, "=", maxsplit=1)
validate_token(key)
# Property parameter values that are not in quoted
# strings are case insensitive.
vals = []
for v in q_split(val, ','):
for v in q_split(val, ","):
if v.startswith('"') and v.endswith('"'):
v = v.strip('"')
validate_param_value(v, quoted=True)
@ -245,20 +247,27 @@ class Parameters(CaselessDict):
else:
result[key] = vals
except ValueError as exc:
raise ValueError(
f'{param!r} is not a valid parameter string: {exc}')
raise ValueError(f"{param!r} is not a valid parameter string: {exc}")
return result
def escape_string(val):
# f'{i:02X}'
return val.replace(r'\,', '%2C').replace(r'\:', '%3A')\
.replace(r'\;', '%3B').replace(r'\\', '%5C')
return (
val.replace(r"\,", "%2C")
.replace(r"\:", "%3A")
.replace(r"\;", "%3B")
.replace(r"\\", "%5C")
)
def unescape_string(val):
return val.replace('%2C', ',').replace('%3A', ':')\
.replace('%3B', ';').replace('%5C', '\\')
return (
val.replace("%2C", ",")
.replace("%3A", ":")
.replace("%3B", ";")
.replace("%5C", "\\")
)
def unescape_list_or_string(val):
@ -271,24 +280,26 @@ def unescape_list_or_string(val):
#########################################
# parsing and generation of content lines
class Contentline(str):
"""A content line is basically a string that can be folded and parsed into
parts.
"""
def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING):
value = to_unicode(value, encoding=encoding)
assert '\n' not in value, ('Content line can not contain unescaped '
'new line characters.')
assert "\n" not in value, (
"Content line can not contain unescaped " "new line characters."
)
self = super().__new__(cls, value)
self.strict = strict
return self
@classmethod
def from_parts(cls, name, params, values, sorted=True):
"""Turn a parts into a content line.
"""
"""Turn a parts into a content line."""
assert isinstance(params, Parameters)
if hasattr(values, 'to_ical'):
if hasattr(values, "to_ical"):
values = values.to_ical()
else:
values = vText(values).to_ical()
@ -301,12 +312,11 @@ class Contentline(str):
values = to_unicode(values)
if params:
params = to_unicode(params.to_ical(sorted=sorted))
return cls(f'{name};{params}:{values}')
return cls(f'{name}:{values}')
return cls(f"{name};{params}:{values}")
return cls(f"{name}:{values}")
def parts(self):
"""Split the content line up into (name, parameters, values) parts.
"""
"""Split the content line up into (name, parameters, values) parts."""
try:
st = escape_string(self)
name_split = None
@ -314,39 +324,40 @@ class Contentline(str):
in_quotes = False
for i, ch in enumerate(st):
if not in_quotes:
if ch in ':;' and not name_split:
if ch in ":;" and not name_split:
name_split = i
if ch == ':' and not value_split:
if ch == ":" and not value_split:
value_split = i
if ch == '"':
in_quotes = not in_quotes
name = unescape_string(st[:name_split])
if not name:
raise ValueError('Key name is required')
raise ValueError("Key name is required")
validate_token(name)
if not value_split:
value_split = i + 1
if not name_split or name_split + 1 == value_split:
raise ValueError('Invalid content line')
params = Parameters.from_ical(st[name_split + 1: value_split],
strict=self.strict)
raise ValueError("Invalid content line")
params = Parameters.from_ical(
st[name_split + 1 : value_split], strict=self.strict
)
params = Parameters(
(unescape_string(key), unescape_list_or_string(value))
for key, value in iter(params.items())
)
values = unescape_string(st[value_split + 1:])
values = unescape_string(st[value_split + 1 :])
return (name, params, values)
except ValueError as exc:
raise ValueError(
f"Content line could not be parsed into parts: '{self}': {exc}")
f"Content line could not be parsed into parts: '{self}': {exc}"
)
@classmethod
def from_ical(cls, ical, strict=False):
"""Unfold the content lines in an iCalendar into long content lines.
"""
"""Unfold the content lines in an iCalendar into long content lines."""
ical = to_unicode(ical)
# a fold is carriage return followed by either a space or a tab
return cls(uFOLD.sub('', ical), strict=strict)
return cls(uFOLD.sub("", ical), strict=strict)
def to_ical(self):
"""Long content lines are folded so they are less than 75 characters
@ -362,33 +373,48 @@ class Contentlines(list):
"""
def to_ical(self):
"""Simply join self.
"""
return b'\r\n'.join(line.to_ical() for line in self if line) + b'\r\n'
"""Simply join self."""
return b"\r\n".join(line.to_ical() for line in self if line) + b"\r\n"
@classmethod
def from_ical(cls, st):
"""Parses a string into content lines.
"""
"""Parses a string into content lines."""
st = to_unicode(st)
try:
# a fold is carriage return followed by either a space or a tab
unfolded = uFOLD.sub('', st)
lines = cls(Contentline(line) for
line in NEWLINE.split(unfolded) if line)
lines.append('') # '\r\n' at the end of every content line
unfolded = uFOLD.sub("", st)
lines = cls(Contentline(line) for line in NEWLINE.split(unfolded) if line)
lines.append("") # '\r\n' at the end of every content line
return lines
except Exception:
raise ValueError('Expected StringType with content lines')
raise ValueError("Expected StringType with content lines")
# XXX: what kind of hack is this? import depends to be at end
from icalendar.prop import vText
__all__ = ["Contentline", "Contentlines", "FOLD", "NAME", "NEWLINE",
"Parameters", "QUNSAFE_CHAR", "QUOTABLE", "UNSAFE_CHAR", "dquote",
"escape_char", "escape_string", "foldline", "param_value", "q_join",
"q_split", "uFOLD", "unescape_char",
"unescape_list_or_string", "unescape_string", "validate_param_value",
"validate_token"]
__all__ = [
"Contentline",
"Contentlines",
"FOLD",
"NAME",
"NEWLINE",
"Parameters",
"QUNSAFE_CHAR",
"QUOTABLE",
"UNSAFE_CHAR",
"dquote",
"escape_char",
"escape_string",
"foldline",
"param_value",
"q_join",
"q_split",
"uFOLD",
"unescape_char",
"unescape_list_or_string",
"unescape_string",
"validate_param_value",
"validate_token",
]

Wyświetl plik

@ -1,11 +1,11 @@
from typing import List, Union
SEQUENCE_TYPES = (list, tuple)
DEFAULT_ENCODING = 'utf-8'
DEFAULT_ENCODING = "utf-8"
ICAL_TYPE = Union[str, bytes]
def from_unicode(value: ICAL_TYPE, encoding='utf-8') -> bytes:
def from_unicode(value: ICAL_TYPE, encoding="utf-8") -> bytes:
"""
Converts a value to bytes, even if it already is bytes
:param value: The value to convert
@ -18,27 +18,26 @@ def from_unicode(value: ICAL_TYPE, encoding='utf-8') -> bytes:
try:
return value.encode(encoding)
except UnicodeEncodeError:
return value.encode('utf-8', 'replace')
return value.encode("utf-8", "replace")
else:
return value
def to_unicode(value: ICAL_TYPE, encoding='utf-8-sig') -> str:
"""Converts a value to unicode, even if it is already a unicode string.
"""
def to_unicode(value: ICAL_TYPE, encoding="utf-8-sig") -> str:
"""Converts a value to unicode, even if it is already a unicode string."""
if isinstance(value, str):
return value
elif isinstance(value, bytes):
try:
return value.decode(encoding)
except UnicodeDecodeError:
return value.decode('utf-8-sig', 'replace')
return value.decode("utf-8-sig", "replace")
else:
return value
def data_encode(
data: Union[ICAL_TYPE, dict, list], encoding=DEFAULT_ENCODING
data: Union[ICAL_TYPE, dict, list], encoding=DEFAULT_ENCODING
) -> Union[bytes, List[bytes], dict]:
"""Encode all datastructures to the given encoding.
Currently unicode strings, dicts and lists are supported.
@ -54,5 +53,11 @@ def data_encode(
return data
__all__ = ["DEFAULT_ENCODING", "SEQUENCE_TYPES", "ICAL_TYPE", "data_encode", "from_unicode",
"to_unicode"]
__all__ = [
"DEFAULT_ENCODING",
"SEQUENCE_TYPES",
"ICAL_TYPE",
"data_encode",
"from_unicode",
"to_unicode",
]

Plik diff jest za duży Load Diff

Wyświetl plik

@ -265,24 +265,28 @@ def pytz_only(tzp, tzp_name) -> str:
assert tzp.uses_pytz()
return tzp_name
@pytest.fixture
def zoneinfo_only(tzp, request, tzp_name) -> str:
"""Skip tests that are not running under zoneinfo."""
assert tzp.uses_zoneinfo()
return tzp_name
@pytest.fixture
def no_pytz(tzp_name) -> str:
"""Do not run tests with pytz."""
assert tzp_name != "pytz"
return tzp_name
@pytest.fixture
def no_zoneinfo(tzp_name) -> str:
"""Do not run tests with zoneinfo."""
assert tzp_name != "zoneinfo"
return tzp_name
def pytest_generate_tests(metafunc):
"""Parametrize without skipping:
@ -343,7 +347,7 @@ def env_for_doctest(monkeypatch):
@pytest.fixture(params=timezone_ids.TZIDS)
def tzid(request:pytest.FixtureRequest) -> str:
def tzid(request: pytest.FixtureRequest) -> str:
"""Return a timezone id to be used with pytz or zoneinfo.
This goes through all the different timezones possible.

Wyświetl plik

@ -1,6 +1,5 @@
"""Test the properties of the alarm."""
import pytest
from icalendar.cal import Alarm, InvalidCalendar
@ -52,6 +51,3 @@ def test_alarm_to_string():
a = Alarm()
a.REPEAT = 11
assert a.to_ical() == b"BEGIN:VALARM\r\nREPEAT:11\r\nEND:VALARM\r\n"

Wyświetl plik

@ -19,20 +19,21 @@ def test_no_dtstamp(dtstamp_comp):
assert dtstamp_comp.DTSTAMP is None
def set_dtstamp_attribute(component:Component, value:date):
def set_dtstamp_attribute(component: Component, value: date):
"""Use the setter."""
component.DTSTAMP = value
def set_dtstamp_item(component: Component, value:date):
def set_dtstamp_item(component: Component, value: date):
"""Use setitem."""
component["DTSTAMP"] = vDDDTypes(value)
def set_dtstamp_add(component: Component, value:date):
def set_dtstamp_add(component: Component, value: date):
"""Use add."""
component.add("DTSTAMP", value)
@pytest.mark.parametrize(
("value", "timezone", "expected"),
[
@ -40,12 +41,14 @@ def set_dtstamp_add(component: Component, value:date):
(datetime(2024, 10, 11, 23, 1), "Europe/Berlin", datetime(2024, 10, 11, 21, 1)),
(datetime(2024, 10, 11, 22, 1), "UTC", datetime(2024, 10, 11, 22, 1)),
(date(2024, 10, 10), None, datetime(2024, 10, 10)),
]
],
)
@pytest.mark.parametrize(
"set_dtstamp", [set_dtstamp_add, set_dtstamp_attribute, set_dtstamp_item]
)
def test_set_value_and_get_it(dtstamp_comp, value, timezone, expected, tzp, set_dtstamp):
def test_set_value_and_get_it(
dtstamp_comp, value, timezone, expected, tzp, set_dtstamp
):
"""Set and get the DTSTAMP value."""
dtstamp = value if timezone is None else tzp.localize(value, timezone)
set_dtstamp(dtstamp_comp, dtstamp)
@ -55,13 +58,7 @@ def test_set_value_and_get_it(dtstamp_comp, value, timezone, expected, tzp, set_
assert in_utc == dtstamp_comp.DTSTAMP
@pytest.mark.parametrize(
"invalid_value",
[
None,
timedelta()
]
)
@pytest.mark.parametrize("invalid_value", [None, timedelta()])
def test_set_invalid_value(invalid_value, dtstamp_comp):
"""Check handling of invalid values."""
with pytest.raises(TypeError) as e:
@ -69,19 +66,16 @@ def test_set_invalid_value(invalid_value, dtstamp_comp):
assert e.value.args[0] == f"DTSTAMP takes a datetime in UTC, not {invalid_value}"
@pytest.mark.parametrize(
"invalid_value",
[
None,
vDDDTypes(timedelta())
]
)
@pytest.mark.parametrize("invalid_value", [None, vDDDTypes(timedelta())])
def test_get_invalid_value(invalid_value, dtstamp_comp):
"""Check handling of invalid values."""
dtstamp_comp["DTSTAMP"] = invalid_value
with pytest.raises(InvalidCalendar) as e:
dtstamp_comp.DTSTAMP # noqa: B018
assert e.value.args[0] == f"DTSTAMP must be a datetime in UTC, not {getattr(invalid_value, 'dt', invalid_value)}"
assert (
e.value.args[0]
== f"DTSTAMP must be a datetime in UTC, not {getattr(invalid_value, 'dt', invalid_value)}"
)
def test_set_twice(dtstamp_comp, tzp):

Wyświetl plik

@ -1,87 +1,120 @@
from icalendar.prop import vBoolean,vInline , vUTCOffset, vCategory, vCalAddress, vWeekday, vDuration, vFloat, vGeo, vInt, vText, vMonth, vUTCOffset, vFrequency, vRecur, vDatetime, vUri
from icalendar.prop import (
vBoolean,
vInline,
vUTCOffset,
vCategory,
vCalAddress,
vWeekday,
vDuration,
vFloat,
vGeo,
vInt,
vText,
vMonth,
vUTCOffset,
vFrequency,
vRecur,
vDatetime,
vUri,
)
import datetime
def test_param_vCategory():
obj = vCategory(["Work", "Personal"], params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vCategory)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vCategory(["Work", "Personal"], params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vCategory)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vCalAddress():
obj = vCalAddress('mailto:jane_doe@example.com',params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vCalAddress)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vCalAddress("mailto:jane_doe@example.com", params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vCalAddress)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vWeekday():
obj = vWeekday("2FR",params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vWeekday)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vWeekday("2FR", params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vWeekday)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vBoolean():
obj = vBoolean(True, params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vBoolean)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vBoolean(True, params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vBoolean)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vDuration():
td = datetime.timedelta(days=15, seconds=18020)
obj = vDuration(td, params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vDuration)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vDuration(td, params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vDuration)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vFloat():
obj = vFloat('1.333',params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vFloat)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vFloat("1.333", params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vFloat)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vGeo():
obj = vGeo((37.386013, -122.082932),params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vGeo)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vGeo((37.386013, -122.082932), params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vGeo)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vInt():
obj = vInt('87',params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vInt)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vInt("87", params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vInt)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vInline():
obj = vInline("sometxt", params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vInline)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vInline("sometxt", params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vInline)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vText():
obj = vText("sometxt", params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vText)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vText("sometxt", params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vText)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vMonth():
obj = vMonth(1,params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vMonth)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vMonth(1, params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vMonth)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vUTCOffset():
obj = vUTCOffset(datetime.timedelta(days=-1, seconds=68400),params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vUTCOffset)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vUTCOffset(
datetime.timedelta(days=-1, seconds=68400), params={"SOME_PARAM": "VALUE"}
)
assert isinstance(obj, vUTCOffset)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vFrequency():
obj = vFrequency("DAILY",params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vFrequency)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vFrequency("DAILY", params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vFrequency)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vRecur():
obj = vRecur({'FREQ': ['DAILY'], 'COUNT': [10]}, params={"SOME_PARAM":"VALUE"})
assert isinstance(obj,vRecur)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vRecur({"FREQ": ["DAILY"], "COUNT": [10]}, params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vRecur)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vDatetime():
dt = datetime.datetime(2025, 3, 16, 14, 30, 0, tzinfo=datetime.timezone.utc)
obj = vDatetime(dt,params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vDatetime)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vDatetime(dt, params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vDatetime)
assert obj.params["SOME_PARAM"] == "VALUE"
def test_param_vUri():
obj = vUri("WWW.WESBITE.COM",params={"SOME_PARAM":"VALUE"})
assert isinstance(obj, vUri)
assert obj.params["SOME_PARAM"]=="VALUE"
obj = vUri("WWW.WESBITE.COM", params={"SOME_PARAM": "VALUE"})
assert isinstance(obj, vUri)
assert obj.params["SOME_PARAM"] == "VALUE"

Wyświetl plik

@ -6,8 +6,11 @@ import copy
try:
from pytz import UnknownTimeZoneError
except ImportError:
class UnknownTimeZoneError(Exception):
pass
from datetime import date, datetime, time, timedelta
import pytest
@ -83,7 +86,10 @@ def test_deep_copies_are_equal(ics_file, tzp):
Ignore errors when a custom time zone is used.
This is still covered by the parsing test.
"""
if ics_file.source_file == "issue_722_timezone_transition_ambiguity.ics" and tzp.uses_zoneinfo():
if (
ics_file.source_file == "issue_722_timezone_transition_ambiguity.ics"
and tzp.uses_zoneinfo()
):
pytest.skip("This test fails for now.")
with contextlib.suppress(UnknownTimeZoneError):
assert_equal(copy.deepcopy(ics_file), copy.deepcopy(ics_file))

Wyświetl plik

@ -1,4 +1,5 @@
"""This tests the properties of components and their types."""
from __future__ import annotations
from datetime import date, datetime, timedelta
@ -23,7 +24,7 @@ from icalendar import (
from icalendar.prop import vDuration
def prop(component: Event|Todo, prop:str) -> str:
def prop(component: Event | Todo, prop: str) -> str:
"""Translate the end property.
This allows us to run the same tests on Event and Todo.
@ -32,16 +33,20 @@ def prop(component: Event|Todo, prop:str) -> str:
return "DUE"
return prop
@pytest.fixture(params=[Event, Todo])
def start_end_component(request):
"""The event to test."""
return request.param()
@pytest.fixture(params=[
@pytest.fixture(
params=[
datetime(2022, 7, 22, 12, 7),
date(2022, 7, 22),
datetime(2022, 7, 22, 13, 7, tzinfo=ZoneInfo("Europe/Paris")),
])
]
)
def dtstart(request, set_component_start, start_end_component):
"""Start of the event."""
set_component_start(start_end_component, request.param)
@ -55,14 +60,17 @@ def _set_component_start_init(component, start):
component.clear()
component.update(type(component)(d))
def _set_component_dtstart(component, start):
"""Create the event with the dtstart property."""
component.DTSTART = start
def _set_component_start_attr(component, start):
"""Create the event with the dtstart property."""
component.start = start
def _set_component_start_ics(component, start):
"""Create the event with the start property."""
component.add("dtstart", start)
@ -71,11 +79,20 @@ def _set_component_start_ics(component, start):
component.clear()
component.update(type(component).from_ical(ics))
@pytest.fixture(params=[_set_component_start_init, _set_component_start_ics, _set_component_dtstart, _set_component_start_attr])
@pytest.fixture(
params=[
_set_component_start_init,
_set_component_start_ics,
_set_component_dtstart,
_set_component_start_attr,
]
)
def set_component_start(request):
"""Create a new event."""
return request.param
def test_component_dtstart(dtstart, start_end_component):
"""Test the start of events."""
assert start_end_component.DTSTART == dtstart
@ -96,15 +113,17 @@ invalid_start_todo_1 = Todo(invalid_start_event_1)
invalid_start_todo_2 = Todo(invalid_start_event_2)
invalid_start_todo_3 = Todo(invalid_start_event_3)
@pytest.mark.parametrize(
"invalid_event", [
"invalid_event",
[
invalid_start_event_1,
invalid_start_event_2,
invalid_start_event_3,
invalid_start_todo_1,
invalid_start_todo_2,
invalid_start_todo_3,
]
],
)
def test_multiple_dtstart(invalid_event):
"""Check that we get the right error."""
@ -128,11 +147,13 @@ def test_no_dtstart(start_end_component):
start_end_component.start # noqa: B018
@pytest.fixture(params=[
@pytest.fixture(
params=[
datetime(2022, 7, 22, 12, 8),
date(2022, 7, 23),
datetime(2022, 7, 22, 14, 7, tzinfo=ZoneInfo("Europe/Paris")),
])
]
)
def dtend(request, set_component_end, start_end_component):
"""end of the event."""
set_component_end(start_end_component, request.param)
@ -146,14 +167,17 @@ def _set_component_end_init(component, end):
component.clear()
component.update(type(component)(d))
def _set_component_end_property(component, end):
"""Create the event with the dtend property."""
setattr(component, prop(component, "DTEND"), end)
def _set_component_end_attr(component, end):
"""Create the event with the dtend property."""
component.end = end
def _set_component_end_ics(component, end):
"""Create the event with the end property."""
component.add(prop(component, "DTEND"), end)
@ -162,11 +186,20 @@ def _set_component_end_ics(component, end):
component.clear()
component.update(type(component).from_ical(ics))
@pytest.fixture(params=[_set_component_end_init, _set_component_end_ics, _set_component_end_property, _set_component_end_attr])
@pytest.fixture(
params=[
_set_component_end_init,
_set_component_end_ics,
_set_component_end_property,
_set_component_end_attr,
]
)
def set_component_end(request):
"""Create a new event."""
return request.param
def test_component_end_property(dtend, start_end_component):
"""Test the end of events."""
attr = prop(start_end_component, "DTEND")
@ -186,34 +219,43 @@ def test_delete_attr(start_end_component, dtstart, dtend, attr):
delattr(start_end_component, attr)
def _set_duration_vdddtypes(event:Event, duration:timedelta):
def _set_duration_vdddtypes(event: Event, duration: timedelta):
"""Set the vDDDTypes value"""
event["DURATION"] = vDDDTypes(duration)
def _set_duration_add(event:Event, duration:timedelta):
def _set_duration_add(event: Event, duration: timedelta):
"""Set the vDDDTypes value"""
event.add("DURATION", duration)
def _set_duration_vduration(event:Event, duration:timedelta):
def _set_duration_vduration(event: Event, duration: timedelta):
"""Set the vDDDTypes value"""
event["DURATION"] = vDuration(duration)
@pytest.fixture(params=[_set_duration_vdddtypes, _set_duration_add, _set_duration_vduration])
@pytest.fixture(
params=[_set_duration_vdddtypes, _set_duration_add, _set_duration_vduration]
)
def duration(start_end_component, dtstart, request):
"""... events have a DATE value type for the "DTSTART" property ...
If such a "VEVENT" has a "DURATION"
property, it MUST be specified as a "dur-day" or "dur-week" value.
"""
duration = timedelta(hours=1) if isinstance(dtstart, datetime) else timedelta(days=2)
duration = (
timedelta(hours=1) if isinstance(dtstart, datetime) else timedelta(days=2)
)
request.param(start_end_component, duration)
return duration
def test_start_and_duration(start_end_component, dtstart, duration):
"""Check calculation of end with duration."""
dur = start_end_component.end - start_end_component.start
assert dur == duration
assert start_end_component.duration == duration
# The "VEVENT" is also the calendar component used to specify an
# anniversary or daily reminder within a calendar. These events
# have a DATE value type for the "DTSTART" property instead of the
@ -247,18 +289,43 @@ invalid_todo_end_4 = Todo()
invalid_todo_end_4.add("DTSTART", date(2024, 1, 1))
invalid_todo_end_4.add("DURATION", timedelta(hours=1))
@pytest.mark.parametrize(
("invalid_component", "message"),
[
(invalid_event_end_1, "DTSTART and DTEND must be of the same type, either date or datetime."),
(invalid_event_end_2, "DTSTART and DTEND must be of the same type, either date or datetime."),
(invalid_event_end_3, "Only one of DTEND and DURATION may be in a VEVENT, not both."),
(invalid_event_end_4, "When DTSTART is a date, DURATION must be of days or weeks."),
(invalid_todo_end_1, "DTSTART and DUE must be of the same type, either date or datetime."),
(invalid_todo_end_2, "DTSTART and DUE must be of the same type, either date or datetime."),
(invalid_todo_end_3, "Only one of DUE and DURATION may be in a VTODO, not both."),
(invalid_todo_end_4, "When DTSTART is a date, DURATION must be of days or weeks."),
]
(
invalid_event_end_1,
"DTSTART and DTEND must be of the same type, either date or datetime.",
),
(
invalid_event_end_2,
"DTSTART and DTEND must be of the same type, either date or datetime.",
),
(
invalid_event_end_3,
"Only one of DTEND and DURATION may be in a VEVENT, not both.",
),
(
invalid_event_end_4,
"When DTSTART is a date, DURATION must be of days or weeks.",
),
(
invalid_todo_end_1,
"DTSTART and DUE must be of the same type, either date or datetime.",
),
(
invalid_todo_end_2,
"DTSTART and DUE must be of the same type, either date or datetime.",
),
(
invalid_todo_end_3,
"Only one of DUE and DURATION may be in a VTODO, not both.",
),
(
invalid_todo_end_4,
"When DTSTART is a date, DURATION must be of days or weeks.",
),
],
)
@pytest.mark.parametrize("attr", ["start", "end"])
def test_invalid_event(invalid_component, message, attr):
@ -267,6 +334,7 @@ def test_invalid_event(invalid_component, message, attr):
getattr(invalid_component, attr)
assert e.value.args[0] == message
def test_event_duration_zero():
"""
For cases where a "VEVENT" calendar component
@ -300,8 +368,9 @@ def test_todo_duration_zero():
assert todo.end == todo.start
assert todo.duration == timedelta(days=0)
def test_todo_duration_one_day():
""" The end is at the end of the day, excluding midnight.
"""The end is at the end of the day, excluding midnight.
RFC 5545:
The following is an example of a "VTODO" calendar
@ -314,7 +383,6 @@ def test_todo_duration_one_day():
assert event.duration == timedelta(days=1)
incomplete_event_1 = Event()
incomplete_event_2 = Event()
incomplete_event_2.add("DURATION", timedelta(hours=1))
@ -323,7 +391,6 @@ incomplete_todo_2 = Todo()
incomplete_todo_2.add("DURATION", timedelta(hours=1))
@pytest.mark.parametrize(
"incomplete_event_end",
[
@ -331,7 +398,7 @@ incomplete_todo_2.add("DURATION", timedelta(hours=1))
incomplete_event_2,
incomplete_todo_1,
incomplete_todo_2,
]
],
)
@pytest.mark.parametrize("attr", ["start", "end", "duration"])
def test_incomplete_event(incomplete_event_end, attr):
@ -346,23 +413,23 @@ def test_incomplete_event(incomplete_event_end, attr):
object(),
timedelta(days=1),
(datetime(2024, 10, 11, 10, 20), timedelta(days=1)),
]
],
)
@pytest.mark.parametrize(
("Component", "attr"),
[
(Event,"start"),
(Event,"end"),
(Event,"DTSTART"),
(Event,"DTEND"),
(Journal,"start"),
(Journal,"end"),
(Journal,"DTSTART"),
(Todo,"start"),
(Todo,"end"),
(Todo,"DTSTART"),
(Todo,"DUE"),
]
(Event, "start"),
(Event, "end"),
(Event, "DTSTART"),
(Event, "DTEND"),
(Journal, "start"),
(Journal, "end"),
(Journal, "DTSTART"),
(Todo, "start"),
(Todo, "end"),
(Todo, "DTSTART"),
(Todo, "DUE"),
],
)
def test_set_invalid_start(invalid_value, attr, Component):
"""Check that we get the right error.
@ -373,12 +440,15 @@ def test_set_invalid_start(invalid_value, attr, Component):
component = Component()
with pytest.raises(TypeError) as e:
setattr(component, attr, invalid_value)
assert e.value.args[0] == f"Use datetime or date, not {type(invalid_value).__name__}."
assert (
e.value.args[0] == f"Use datetime or date, not {type(invalid_value).__name__}."
)
def setitem(d:dict, key, value):
def setitem(d: dict, key, value):
d[key] = value
@pytest.mark.parametrize(
"invalid_value",
[
@ -387,14 +457,17 @@ def setitem(d:dict, key, value):
(datetime(2024, 10, 11, 10, 20), timedelta(days=1)),
date(2012, 2, 2),
datetime(2022, 2, 2),
]
],
)
def test_check_invalid_duration(start_end_component, invalid_value):
"""Check that we get the right error."""
start_end_component["DURATION"] = invalid_value
with pytest.raises(InvalidCalendar) as e:
start_end_component.DURATION # noqa: B018
assert e.value.args[0] == f"DURATION must be a timedelta, not {type(invalid_value).__name__}."
assert (
e.value.args[0]
== f"DURATION must be a timedelta, not {type(invalid_value).__name__}."
)
def test_setting_the_end_deletes_the_duration(start_end_component):
@ -419,14 +492,17 @@ def test_setting_duration_deletes_the_end(start_end_component):
assert getattr(start_end_component, DTEND) is None
assert start_end_component.DURATION == timedelta(days=1)
valid_values = pytest.mark.parametrize(
("attr", "value"),
[
("DTSTART", datetime(2024, 10, 11, 10, 20)),
("DTEND", datetime(2024, 10, 11, 10, 20)),
("DURATION", timedelta(days=1)),
]
],
)
@valid_values
def test_setting_to_none_deletes_value(start_end_component, attr, value):
"""Setting attributes to None deletes them."""
@ -462,12 +538,16 @@ def test_delete_duration(start_end_component):
del start_end_component.DURATION
assert start_end_component.DURATION is None
@pytest.mark.parametrize("attr", ["DTSTART", "end", "start"])
@pytest.mark.parametrize("start", [
datetime(2024, 10, 11, 10, 20),
date(2024, 10, 11),
datetime(2024, 10, 11, 10, 20, tzinfo=ZoneInfo("Europe/Paris")),
])
@pytest.mark.parametrize(
"start",
[
datetime(2024, 10, 11, 10, 20),
date(2024, 10, 11),
datetime(2024, 10, 11, 10, 20, tzinfo=ZoneInfo("Europe/Paris")),
],
)
def test_journal_start(start, attr):
"""Test that we can set the start of a journal."""
j = Journal()
@ -477,6 +557,7 @@ def test_journal_start(start, attr):
assert j.end == start
assert j.duration == timedelta(0)
@pytest.mark.parametrize("attr", ["start", "end"])
def test_delete_journal_start(attr):
"""Delete the start of the journal."""
@ -488,9 +569,10 @@ def test_delete_journal_start(attr):
with pytest.raises(IncompleteComponent):
getattr(j, attr)
def setting_twice_does_not_duplicate_the_entry():
j = Journal()
j.DTSTART = date(2024, 1,1 )
j.DTSTART = date(2024, 1, 1)
j.DTSTART = date(2024, 1, 3)
assert date(2024, 1, 3) == j.DTSTART
assert j.start == date(2024, 1, 3)
@ -500,10 +582,14 @@ def setting_twice_does_not_duplicate_the_entry():
@pytest.mark.parametrize(
("file", "trigger", "related"),
[
("rfc_5545_absolute_alarm_example", vDatetime.from_ical("19970317T133000Z"), "START"),
(
"rfc_5545_absolute_alarm_example",
vDatetime.from_ical("19970317T133000Z"),
"START",
),
("rfc_5545_end", timedelta(days=-2), "END"),
("start_date", timedelta(days=-2), "START"),
]
],
)
def test_get_alarm_trigger_property(alarms, file, trigger, related):
"""Get the trigger property."""
@ -533,21 +619,38 @@ def test_get_related_without_trigger():
"""The default is start"""
assert Alarm().TRIGGER_RELATED == "START"
def test_cannot_set_related_without_trigger():
"""TRIGGER must be set to set the parameter."""
with pytest.raises(ValueError) as e:
a = Alarm()
a.TRIGGER_RELATED = "END"
assert e.value.args[0] == "You must set a TRIGGER before setting the RELATED parameter."
assert (
e.value.args[0]
== "You must set a TRIGGER before setting the RELATED parameter."
)
@pytest.mark.parametrize(
("file", "triggers"),
[
("rfc_5545_absolute_alarm_example", ((), (), (vDatetime.from_ical("19970317T133000Z"), vDatetime.from_ical("19970317T134500Z"),vDatetime.from_ical("19970317T140000Z"),vDatetime.from_ical("19970317T141500Z"),vDatetime.from_ical("19970317T143000Z"),))),
(
"rfc_5545_absolute_alarm_example",
(
(),
(),
(
vDatetime.from_ical("19970317T133000Z"),
vDatetime.from_ical("19970317T134500Z"),
vDatetime.from_ical("19970317T140000Z"),
vDatetime.from_ical("19970317T141500Z"),
vDatetime.from_ical("19970317T143000Z"),
),
),
),
("rfc_5545_end", ((), (timedelta(days=-2),), ())),
("start_date", ((timedelta(days=-2),), (), ())),
]
],
)
def test_get_alarm_triggers(alarms, file, triggers):
"""Get the trigger property."""
@ -561,8 +664,10 @@ def test_triggers_emtpy_alarm():
"""An alarm with no trigger has no triggers."""
assert Alarm().triggers == ((), (), ())
h1 = timedelta(hours=1)
def test_triggers_emtpy_with_no_repeat():
"""Check incomplete values."""
a = Alarm()
@ -570,6 +675,7 @@ def test_triggers_emtpy_with_no_repeat():
a.DURATION = h1
assert a.triggers == ((h1,), (), ())
def test_triggers_emtpy_with_no_duration():
"""Check incomplete values."""
a = Alarm()
@ -581,10 +687,13 @@ def test_triggers_emtpy_with_no_duration():
@pytest.mark.parametrize(
("file", "triggers"),
[
("rfc_5545_absolute_alarm_example", ((), (), (vDatetime.from_ical("19970317T133000Z"),))),
(
"rfc_5545_absolute_alarm_example",
((), (), (vDatetime.from_ical("19970317T133000Z"),)),
),
("rfc_5545_end", ((), (timedelta(days=-2),), ())),
("start_date", ((timedelta(days=-2),), (), ())),
]
],
)
@pytest.mark.parametrize("duration", [timedelta(days=-1), h1])
@pytest.mark.parametrize("repeat", [1, 3])

Wyświetl plik

@ -12,7 +12,6 @@ DTSTART, DTEND, and DURATION.
"""
from datetime import date, datetime, timedelta, timezone
import pytest
@ -46,12 +45,8 @@ alarm_1.add("TRIGGER", EXAMPLE_TRIGGER)
alarm_2 = Alarm()
alarm_2["TRIGGER"] = vDatetime(EXAMPLE_TRIGGER)
@pytest.mark.parametrize(
"alarm",
[
alarm_1, alarm_2
]
)
@pytest.mark.parametrize("alarm", [alarm_1, alarm_2])
def test_absolute_alarm_time_with_vDatetime(alarm):
"""Check that the absolute alarm is recognized.
@ -72,6 +67,7 @@ alarm_incomplete_2 = Alarm()
alarm_incomplete_2.TRIGGER = timedelta(hours=2)
alarm_incomplete_2.REPEAT = 100
@pytest.mark.parametrize("alarm", [alarm_incomplete_1, alarm_incomplete_2])
def test_alarm_has_only_one_of_repeat_or_duration(alarm):
"""This is an edge case and we should ignore the repetition."""
@ -95,21 +91,34 @@ def test_cannot_compute_relative_alarm_without_start(alarm_before_start):
"""We have an alarm without a start of a component."""
with pytest.raises(IncompleteAlarmInformation) as e:
Alarms(alarm_before_start).times # noqa: B018
assert e.value.args[0] == f"Use {Alarms.__name__}.{Alarms.set_start.__name__} because at least one alarm is relative to the start of a component."
assert (
e.value.args[0]
== f"Use {Alarms.__name__}.{Alarms.set_start.__name__} because at least one alarm is relative to the start of a component."
)
@pytest.mark.parametrize(
("dtstart", "timezone", "trigger"),
[
(datetime(2024, 10, 29, 13, 10), "UTC", datetime(2024, 10, 29, 13, 10, tzinfo=UTC)),
(
datetime(2024, 10, 29, 13, 10),
"UTC",
datetime(2024, 10, 29, 13, 10, tzinfo=UTC),
),
(date(2024, 11, 16), None, datetime(2024, 11, 16, 0, 0)),
(datetime(2024, 10, 29, 13, 10), "Asia/Singapore", datetime(2024, 10, 29, 5, 10, tzinfo=UTC)),
(
datetime(2024, 10, 29, 13, 10),
"Asia/Singapore",
datetime(2024, 10, 29, 5, 10, tzinfo=UTC),
),
(datetime(2024, 10, 29, 13, 20), None, datetime(2024, 10, 29, 13, 20)),
]
],
)
def test_can_complete_relative_calculation_if_a_start_is_given(alarm_before_start, dtstart, timezone, trigger, tzp):
def test_can_complete_relative_calculation_if_a_start_is_given(
alarm_before_start, dtstart, timezone, trigger, tzp
):
"""The start is given and required."""
start = (dtstart if timezone is None else tzp.localize(dtstart, timezone))
start = dtstart if timezone is None else tzp.localize(dtstart, timezone)
alarms = Alarms(alarm_before_start)
alarms.set_start(start)
assert len(alarms.times) == 1
@ -131,21 +140,32 @@ def test_cannot_compute_relative_alarm_without_end(alarms):
"""We have an alarm without an end of a component."""
with pytest.raises(IncompleteAlarmInformation) as e:
Alarms(alarms.rfc_5545_end).times # noqa: B018
assert e.value.args[0] == f"Use {Alarms.__name__}.{Alarms.set_end.__name__} because at least one alarm is relative to the end of a component."
assert (
e.value.args[0]
== f"Use {Alarms.__name__}.{Alarms.set_end.__name__} because at least one alarm is relative to the end of a component."
)
@pytest.mark.parametrize(
("dtend", "timezone", "trigger"),
[
(datetime(2024, 10, 29, 13, 10), "UTC", datetime(2024, 10, 29, 13, 10, tzinfo=UTC)),
(
datetime(2024, 10, 29, 13, 10),
"UTC",
datetime(2024, 10, 29, 13, 10, tzinfo=UTC),
),
(date(2024, 11, 16), None, date(2024, 11, 16)),
(datetime(2024, 10, 29, 13, 10), "Asia/Singapore", datetime(2024, 10, 29, 5, 10, tzinfo=UTC)),
(
datetime(2024, 10, 29, 13, 10),
"Asia/Singapore",
datetime(2024, 10, 29, 5, 10, tzinfo=UTC),
),
(datetime(2024, 10, 29, 13, 20), None, datetime(2024, 10, 29, 13, 20)),
]
],
)
def test_can_complete_relative_calculation(alarms, dtend, timezone, trigger, tzp):
"""The start is given and required."""
start = (dtend if timezone is None else tzp.localize(dtend, timezone))
start = dtend if timezone is None else tzp.localize(dtend, timezone)
alarms = Alarms(alarms.rfc_5545_end)
alarms.set_end(start)
assert len(alarms.times) == 1
@ -163,7 +183,6 @@ def test_end_as_date_with_delta_as_date_stays_date(alarms, dtend):
assert a.times[0].trigger == dtend - timedelta(days=2)
def test_add_multiple_alarms(alarms):
"""We can add multiple alarms."""
a = Alarms()
@ -202,31 +221,82 @@ def test_cannot_set_the_event_twice(calendars):
[
("alarm_etar_future", -1, 3, "Etar (1): we just created the alarm"),
("alarm_etar_notification", -1, 2, "Etar (2): the notification popped up"),
("alarm_etar_notification_clicked", -1, 0, "Etar (3): the notification was dismissed"), # TODO: check that that is really true
("alarm_google_future", -1, 4, "Google (1): we just created the event with alarms"),
("alarm_google_acknowledged", -1, 2, "Google (2): 2 alarms happened at the same time"),
(
"alarm_etar_notification_clicked",
-1,
0,
"Etar (3): the notification was dismissed",
), # TODO: check that that is really true
(
"alarm_google_future",
-1,
4,
"Google (1): we just created the event with alarms",
),
(
"alarm_google_acknowledged",
-1,
2,
"Google (2): 2 alarms happened at the same time",
),
("alarm_thunderbird_future", -1, 2, "Thunderbird (1.1): 2 alarms are set"),
("alarm_thunderbird_snoozed_until_1457", -1, 2, "Thunderbird (1.2): 2 alarms are snoozed to another time"),
("alarm_thunderbird_closed", -1, 0, "Thunderbird (1.3): all alarms are dismissed (closed)"),
(
"alarm_thunderbird_snoozed_until_1457",
-1,
2,
"Thunderbird (1.2): 2 alarms are snoozed to another time",
),
(
"alarm_thunderbird_closed",
-1,
0,
"Thunderbird (1.3): all alarms are dismissed (closed)",
),
("alarm_thunderbird_2_future", -1, 2, "Thunderbird (2.1): 2 alarms active"),
("alarm_thunderbird_2_notification_popped_up", -1, 2, "Thunderbird (2.2): one alarm popped up as a notification"),
("alarm_thunderbird_2_notification_5_min_postponed", -1, 2, "Thunderbird (2.3): 1 alarm active and one postponed by 5 minutes"),
("alarm_thunderbird_2_notification_5_min_postponed_and_popped_up", -1, 2, "Thunderbird (2.4): 1 alarm active and one postponed by 5 minutes and now popped up"),
("alarm_thunderbird_2_notification_5_min_postponed_and_closed", -1, 1, "Thunderbird (2.5): 1 alarm active and one postponed by 5 minutes and is now acknowledged"),
]
(
"alarm_thunderbird_2_notification_popped_up",
-1,
2,
"Thunderbird (2.2): one alarm popped up as a notification",
),
(
"alarm_thunderbird_2_notification_5_min_postponed",
-1,
2,
"Thunderbird (2.3): 1 alarm active and one postponed by 5 minutes",
),
(
"alarm_thunderbird_2_notification_5_min_postponed_and_popped_up",
-1,
2,
"Thunderbird (2.4): 1 alarm active and one postponed by 5 minutes and now popped up",
),
(
"alarm_thunderbird_2_notification_5_min_postponed_and_closed",
-1,
1,
"Thunderbird (2.5): 1 alarm active and one postponed by 5 minutes and is now acknowledged",
),
],
)
def test_number_of_active_alarms_from_calendar_software(calendars, calendar, index, count, message):
def test_number_of_active_alarms_from_calendar_software(
calendars, calendar, index, count, message
):
"""Check that we extract calculate the correct amount of active alarms."""
event = calendars[calendar].subcomponents[index]
a = Alarms(event)
active_alarms = a.active # We do not need to pass a timezone because the events have a timezone
assert len(active_alarms) == count, f"{message} - I expect {count} alarms active but got {len(active_alarms)}."
active_alarms = (
a.active
) # We do not need to pass a timezone because the events have a timezone
assert (
len(active_alarms) == count
), f"{message} - I expect {count} alarms active but got {len(active_alarms)}."
three_alarms = Alarm()
three_alarms.REPEAT = 2
three_alarms.add("DURATION", timedelta(hours=1)) # 2 hours & 1 hour before
three_alarms.add("TRIGGER", -timedelta(hours=3)) # 3 hours before
three_alarms.add("DURATION", timedelta(hours=1)) # 2 hours & 1 hour before
three_alarms.add("TRIGGER", -timedelta(hours=3)) # 3 hours before
@pytest.mark.parametrize(
@ -236,10 +306,17 @@ three_alarms.add("TRIGGER", -timedelta(hours=3)) # 3 hours before
(datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 9, 1), "UTC", 2),
(datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 10, 1), "UTC", 1),
(datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 11, 1), "UTC", 0),
(datetime(2024, 10, 10, 12, tzinfo=timezone.utc), datetime(2024, 10, 10, 11, 1), None, 0),
]
(
datetime(2024, 10, 10, 12, tzinfo=timezone.utc),
datetime(2024, 10, 10, 11, 1),
None,
0,
),
],
)
def test_number_of_active_alarms_with_moving_time(start, acknowledged, count, tzp, timezone):
def test_number_of_active_alarms_with_moving_time(
start, acknowledged, count, tzp, timezone
):
"""Check how many alarms are active after a time they are acknowledged."""
a = Alarms()
a.add_alarm(three_alarms)
@ -258,7 +335,10 @@ def test_incomplete_alarm_information_for_active_state(tzp):
a.acknowledge_until(tzp.localize_utc(datetime(2012, 10, 10, 12)))
with pytest.raises(IncompleteAlarmInformation) as e:
a.active # noqa: B018
assert e.value.args[0] == f"A local timezone is required to check if the alarm is still active. Use Alarms.{Alarms.set_local_timezone.__name__}()."
assert (
e.value.args[0]
== f"A local timezone is required to check if the alarm is still active. Use Alarms.{Alarms.set_local_timezone.__name__}()."
)
@pytest.mark.parametrize(
@ -269,7 +349,7 @@ def test_incomplete_alarm_information_for_active_state(tzp):
"alarm_thunderbird_closed",
"alarm_thunderbird_future",
"alarm_thunderbird_snoozed_until_1457",
]
],
)
def test_thunderbird_recognition(calendars, calendar_name):
"""Check if we correctly discover Thunderbird's alarm algorithm."""
@ -282,12 +362,12 @@ def test_thunderbird_recognition(calendars, calendar_name):
@pytest.mark.parametrize(
"snooze",
[
datetime(2012, 10, 10, 11, 1), # before everything
datetime(2012, 10, 10, 11, 1), # before everything
datetime(2017, 12, 1, 10, 1),
datetime(2017, 12, 1, 11, 1),
datetime(2017, 12, 1, 12, 1),
datetime(2017, 12, 1, 13, 1), # snooze until after the start of the event
]
datetime(2017, 12, 1, 13, 1), # snooze until after the start of the event
],
)
def test_snoozed_alarm_has_trigger_at_snooze_time(tzp, snooze):
"""When an alarm is snoozed, it pops up after the snooze time."""
@ -322,7 +402,7 @@ def test_snoozed_alarm_has_trigger_at_snooze_time(tzp, snooze):
# When the second "snooze" alarm is triggered, the user decides to dismiss it.
# The client acknowledges both the original alarm and the second "snooze" alarm:
(4, ()),
]
],
)
def test_rfc_9074_alarm_times(events, event_index, alarm_times):
"""Test the examples from the RFC and their timing.
@ -331,7 +411,9 @@ def test_rfc_9074_alarm_times(events, event_index, alarm_times):
"""
a = events[f"rfc_9074_example_{event_index}"].alarms
assert len(a.active) == len(alarm_times)
expected_alarm_times = {vDatetime.from_ical(t, "America/New_York") for t in alarm_times}
expected_alarm_times = {
vDatetime.from_ical(t, "America/New_York") for t in alarm_times
}
computed_alarm_times = {alarm.trigger for alarm in a.active}
assert expected_alarm_times == computed_alarm_times
@ -344,4 +426,4 @@ def test_set_to_None():
a.set_local_timezone(None)
a.acknowledge_until(None)
a.snooze_until(None)
assert vars(a) == vars(Alarms())
assert vars(a) == vars(Alarms())

Wyświetl plik

@ -14,6 +14,7 @@ from re import findall
import pytest
from dateutil.tz import gettz
try:
from zoneinfo import available_timezones
except ImportError:
@ -22,25 +23,36 @@ except ImportError:
from icalendar import Calendar, Component, Event, Timezone
from icalendar.timezone import tzid_from_tzinfo, tzids_from_tzinfo
tzids = pytest.mark.parametrize("tzid", [
"Europe/Berlin",
"Asia/Singapore",
"America/New_York",
])
tzids = pytest.mark.parametrize(
"tzid",
[
"Europe/Berlin",
"Asia/Singapore",
"America/New_York",
],
)
def assert_components_equal(c1:Component, c2:Component):
def assert_components_equal(c1: Component, c2: Component):
"""Print the diff of two components."""
ML = 32
ll1 = c1.to_ical().decode().splitlines()
ll2 = c2.to_ical().decode().splitlines()
pad = max(len(l) for l in ll1 if len(l) <=ML)
pad = max(len(l) for l in ll1 if len(l) <= ML)
diff = 0
for l1, l2 in zip(ll1, ll2):
a = len(l1) > 32 or len(l2) > 32
print(a * " " + l1, " " * (pad - len(l1)), a* "\n->" + l2, " "*(pad - len(l2)), "\tdiff!" if l1 != l2 else "")
print(
a * " " + l1,
" " * (pad - len(l1)),
a * "\n->" + l2,
" " * (pad - len(l2)),
"\tdiff!" if l1 != l2 else "",
)
diff += l1 != l2
assert not diff, f"{diff} lines differ"
@tzids
def test_conversion_converges(tzp, tzid):
"""tzinfo -> VTIMEZONE -> tzinfo -> VTIMEZONE
@ -48,11 +60,15 @@ def test_conversion_converges(tzp, tzid):
We can assume that both generated VTIMEZONEs are equivalent.
"""
if tzp.uses_pytz():
pytest.skip("pytz will not converge on the first run. This is problematic. PYTZ-TODO")
pytest.skip(
"pytz will not converge on the first run. This is problematic. PYTZ-TODO"
)
tzinfo1 = tzp.timezone(tzid)
assert tzinfo1 is not None
generated1 = Timezone.from_tzinfo(tzinfo1)
generated1["TZID"] = "test-generated" # change the TZID so we do not use an existing one
generated1["TZID"] = (
"test-generated" # change the TZID so we do not use an existing one
)
tzinfo2 = generated1.to_tz()
generated2 = Timezone.from_tzinfo(tzinfo2, "test-generated")
tzinfo3 = generated2.to_tz()
@ -70,7 +86,6 @@ def test_conversion_converges(tzp, tzid):
assert generated1 == generated2
@tzids
def both_tzps_generate_the_same_info(tzid, tzp):
"""We want to make sure that we get the same info for all timezone implementations.
@ -81,7 +96,7 @@ def both_tzps_generate_the_same_info(tzid, tzp):
"""
# default generation
tz1 = Timezone.from_tzid(tzid, tzp, last_date=date(2024, 1, 1))
tzp.use_zoneinfo() # we compare to zoneinfo
tzp.use_zoneinfo() # we compare to zoneinfo
tz2 = Timezone.from_tzid(tzid, tzp, last_date=date(2024, 1, 1))
assert_components_equal(tz1, tz2)
assert tz1 == tz2
@ -90,7 +105,7 @@ def both_tzps_generate_the_same_info(tzid, tzp):
@tzids
def test_tzid_matches(tzid, tzp):
"""Check the TZID."""
tz = Timezone.from_tzinfo(tzp.timezone(tzid))
tz = Timezone.from_tzinfo(tzp.timezone(tzid))
assert tz["TZID"] == tzid
@ -127,8 +142,10 @@ def test_berlin_time(tzp):
def test_range_is_not_crossed():
first_date = datetime(2023, 1, 1)
last_date = datetime(2024, 1, 1)
def check(dt):
assert first_date <= dt <= last_date
tz = Timezone.from_tzid("Europe/Berlin", last_date=last_date, first_date=first_date)
for sub in tz.standard + tz.daylight:
check(sub.DTSTART)
@ -147,6 +164,7 @@ def test_use_the_original_timezone(tzid, tzp):
assert type(tzinfo1) == type(tzinfo2)
assert tzinfo1 == tzinfo2
@pytest.mark.parametrize(
("tzid", "dt", "tzname"),
[
@ -155,19 +173,16 @@ def test_use_the_original_timezone(tzid, tzp):
("Asia/Singapore", datetime(1981, 12, 31, 23, 10), "+0730"),
("Asia/Singapore", datetime(1981, 12, 31, 23, 34), "+0730"),
("Asia/Singapore", datetime(1981, 12, 31, 23, 59, 59), "+0730"),
("Asia/Singapore", datetime(1982, 1, 1), "+08"),
("Asia/Singapore", datetime(1982, 1, 1, 0, 1), "+08"),
("Asia/Singapore", datetime(1982, 1, 1, 0, 34), "+08"),
("Asia/Singapore", datetime(1982, 1, 1, 1, 0), "+08"),
("Asia/Singapore", datetime(1982, 1, 1, 1, 1), "+08"),
("Europe/Berlin", datetime(1970, 1, 1), "CET"),
("Europe/Berlin", datetime(2024, 3, 31, 0, 0), "CET"),
("Europe/Berlin", datetime(2024, 3, 31, 1, 0), "CET"),
("Europe/Berlin", datetime(2024, 3, 31, 2, 0), "CET"),
("Europe/Berlin", datetime(2024, 3, 31, 2, 59, 59), "CET"),
("Europe/Berlin", datetime(2024, 3, 31, 3, 0), "CEST"),
("Europe/Berlin", datetime(2024, 3, 31, 3, 0, 1), "CEST"),
("Europe/Berlin", datetime(2024, 3, 31, 4, 0), "CEST"),
@ -176,11 +191,9 @@ def test_use_the_original_timezone(tzid, tzp):
("Europe/Berlin", datetime(2024, 10, 27, 2, 0), "CEST"),
("Europe/Berlin", datetime(2024, 10, 27, 2, 30), "CEST"),
("Europe/Berlin", datetime(2024, 10, 27, 2, 59, 59), "CEST"),
("Europe/Berlin", datetime(2024, 10, 27, 3, 0), "CET"),
("Europe/Berlin", datetime(2024, 10, 27, 3, 0, 1), "CET"),
("Europe/Berlin", datetime(2024, 10, 27, 4, 0), "CET"),
# transition times from https://www.zeitverschiebung.net/de/timezone/america--new_york
("America/New_York", datetime(1970, 1, 1), "EST"),
# Daylight Saving Time
@ -198,7 +211,7 @@ def test_use_the_original_timezone(tzid, tzp):
("America/New_York", datetime(2025, 3, 9, 3, 0), "EDT"),
("America/New_York", datetime(2025, 3, 9, 3, 1, 1), "EDT"),
("America/New_York", datetime(2025, 3, 9, 4, 0), "EDT"),
]
],
)
def test_check_datetimes_around_transition_times(tzp, tzid, dt, tzname):
"""We should make sure than the datetimes with the generated timezones
@ -214,24 +227,22 @@ def test_check_datetimes_around_transition_times(tzp, tzid, dt, tzname):
# generated_dt = generated_tzinfo.localize(dt)
generated_dt = generated_tzinfo.normalize(generated_dt)
if dt in (
datetime(2024, 10, 27, 1, 0),
datetime(2024, 11, 3, 1, 59, 59),
datetime(2024, 11, 3, 1, 0),
datetime(2024, 11, 3, 0, 0),
datetime(2024, 10, 27, 2, 59, 59),
datetime(2024, 10, 27, 2, 30),
datetime(2024, 10, 27, 2, 0),
datetime(2024, 10, 27, 1, 0)
):
datetime(2024, 10, 27, 1, 0),
datetime(2024, 11, 3, 1, 59, 59),
datetime(2024, 11, 3, 1, 0),
datetime(2024, 11, 3, 0, 0),
datetime(2024, 10, 27, 2, 59, 59),
datetime(2024, 10, 27, 2, 30),
datetime(2024, 10, 27, 2, 0),
datetime(2024, 10, 27, 1, 0),
):
pytest.skip("We do not know how to do this. PYTZ-TODO")
assert generated_dt.tzname() == expected_dt.tzname() == tzname, message
assert generated_dt.dst() == expected_dt.dst(), message
assert generated_dt.utcoffset() == expected_dt.utcoffset(), message
@pytest.mark.parametrize(
"uid", [0, 1, 2, 3]
)
@pytest.mark.parametrize("uid", [0, 1, 2, 3])
def test_dateutil_timezone_when_time_is_going_backwards(calendars, tzp, uid):
"""When going from Daylight Saving Time to Standard Time, times can be ambiguous.
For example, at 3:00 AM, the time falls back to 2:00 AM, repeating a full hour of times from 2:00 AM to 3:00 AM on the same date.
@ -242,20 +253,21 @@ def test_dateutil_timezone_when_time_is_going_backwards(calendars, tzp, uid):
Each event has its timezone saved in it.
"""
cal : Calendar = calendars.issue_722_timezone_transition_ambiguity
event : Event = cal.events[uid]
cal: Calendar = calendars.issue_722_timezone_transition_ambiguity
event: Event = cal.events[uid]
expected_tzname = str(event["X-TZNAME"])
actual_tzname = event.start.tzname()
assert actual_tzname == expected_tzname, event["SUMMARY"]
def query_tzid(query:str, cal:Calendar) -> str:
def query_tzid(query: str, cal: Calendar) -> str:
"""The tzid from the query."""
try:
tzinfo = eval(query, {"cal": cal}) # noqa: S307
except Exception as e:
raise ValueError(query) from e
return tzid_from_tzinfo(tzinfo)
return tzid_from_tzinfo(tzinfo)
# these are queries for all the places that have a TZID
# according to RFC 5545
@ -270,6 +282,7 @@ queries = [
"cal.events[2].get('RDATE')[1].dts[0].dt.tzinfo", # RDATE multiple
]
@pytest.mark.parametrize("query", queries)
def test_add_missing_timezones_to_example(calendars, query):
"""Add the missing timezones to the calendar."""
@ -278,6 +291,7 @@ def test_add_missing_timezones_to_example(calendars, query):
tzs = cal.get_missing_tzids()
assert tzid in tzs
def test_queries_yield_unique_tzids(calendars):
"""We make sure each query tests a unique place to find for the algorithm."""
cal = calendars.issue_722_missing_timezones
@ -288,6 +302,7 @@ def test_queries_yield_unique_tzids(calendars):
tzids.add(tzid)
assert len(tzids) == len(queries)
def test_we_do_not_miss_to_add_a_query(calendars):
"""Make sure all tzids are actually queried."""
cal = calendars.issue_722_missing_timezones
@ -296,35 +311,40 @@ def test_we_do_not_miss_to_add_a_query(calendars):
assert cal.get_used_tzids() == ids, "We find all tzids and they are unique."
assert len(ids) == len(queries), "We query all the tzids."
def test_unknown_tzid(calendars):
"""If we have an unknown tzid with no timezone component."""
cal = calendars.issue_722_missing_VTIMEZONE_custom
assert "CUSTOM_tzid" in cal.get_used_tzids()
assert "CUSTOM_tzid" in cal.get_missing_tzids()
def test_custom_timezone_is_found_and_used(calendars):
"""Check the custom timezone component is not missing."""
cal = calendars.america_new_york
assert "custom_America/New_York" in cal.get_used_tzids()
assert "custom_America/New_York" not in cal.get_missing_tzids()
def test_not_missing_anything():
"""Check that no timezone is missing."""
cal = Calendar()
assert cal.get_missing_tzids() == set()
def test_utc_is_not_missing(calendars):
"""UTC should not be found missing."""
cal = calendars.issue_722_missing_timezones
assert "UTC" not in cal.get_missing_tzids()
assert "UTC" not in cal.get_used_tzids()
def test_dateutil_timezone_is_not_found_with_tzname(calendars, no_pytz):
"""dateutil is an example of a timezone that has no tzid.
In this test we make sure that the timezone is said to be missing.
"""
cal : Calendar = calendars.america_new_york
cal: Calendar = calendars.america_new_york
cal.subcomponents.remove(cal.timezones[0])
assert cal.get_missing_tzids() == {"custom_America/New_York"}
assert "dateutil" in repr(cal.events[0].start.tzinfo.__class__)
@ -354,35 +374,38 @@ def test_dateutil_timezone_is_also_added(calendars):
This is important as we use those in the zoneinfo implementation.
"""
@pytest.mark.parametrize(
"calendar",
[
"example",
"america_new_york", # custom
"timezone_same_start", # known tzid
"period_with_timezone", # known tzid
]
"america_new_york", # custom
"timezone_same_start", # known tzid
"period_with_timezone", # known tzid
],
)
def test_timezone_is_not_missing(calendars, calendar):
"""Check that these calendars have no timezone missing."""
cal :Calendar= calendars[calendar]
cal: Calendar = calendars[calendar]
timezones = cal.timezones[:]
assert set() == cal.get_missing_tzids()
cal.add_missing_timezones()
assert set() == cal.get_missing_tzids()
assert cal.timezones == timezones
def test_add_missing_known_timezones(calendars):
"""Add all timezones specified."""
cal :Calendar= calendars.issue_722_missing_timezones
cal: Calendar = calendars.issue_722_missing_timezones
assert len(cal.timezones) == 0
cal.add_missing_timezones()
assert len(cal.timezones) == len(queries), "all timezones are known"
assert len(cal.get_missing_tzids()) == 0
def test_cannot_add_unknown_timezone(calendars):
"""I cannot add a timezone that is unknown."""
cal :Calendar= calendars.issue_722_missing_VTIMEZONE_custom
cal: Calendar = calendars.issue_722_missing_VTIMEZONE_custom
assert len(cal.timezones) == 0
assert cal.get_missing_tzids() == {"CUSTOM_tzid"}
cal.add_missing_timezones()
@ -394,6 +417,7 @@ def test_cannot_create_a_timezone_from_an_invalid_tzid():
with pytest.raises(ValueError):
Timezone.from_tzid("invalid/tzid")
def test_dates_before_and_after_are_considered():
"""When we add the timezones, we should check the calendar to see
if all dates really occur in the span we use.
@ -403,7 +427,7 @@ def test_dates_before_and_after_are_considered():
pytest.skip("todo")
@pytest.mark.parametrize("tzid", available_timezones() - {"Factory", "localtime"})
@pytest.mark.parametrize("tzid", available_timezones() - {"Factory", "localtime"})
def test_we_can_identify_dateutil_timezones(tzid):
"""dateutil and others were badly supported.

Wyświetl plik

@ -100,12 +100,12 @@ def test_tzid_is_part_of_the_period_values(calendars, tzp):
def test_period_overlaps():
# 30 minute increments
datetime_1 = datetime.datetime(2024, 11, 20, 12, 0) # 12:00
datetime_2 = datetime.datetime(2024, 11, 20, 12, 30) # 12:30
datetime_2 = datetime.datetime(2024, 11, 20, 12, 30) # 12:30
datetime_3 = datetime.datetime(2024, 11, 20, 13, 0) # 13:00
period_1 = vPeriod((datetime_1,datetime_2))
period_2 = vPeriod((datetime_1,datetime_3))
period_3 = vPeriod((datetime_2,datetime_3))
period_1 = vPeriod((datetime_1, datetime_2))
period_2 = vPeriod((datetime_1, datetime_3))
period_3 = vPeriod((datetime_2, datetime_3))
assert period_1.overlaps(period_2)
assert period_3.overlaps(period_2)

Wyświetl plik

@ -3,6 +3,7 @@
See https://www.rfc-editor.org/rfc/rfc9074.html
and also https://github.com/collective/icalendar/issues/657
"""
import pytest
from icalendar.prop import vDDDTypes, vText
@ -15,7 +16,7 @@ from icalendar.prop import vDDDTypes, vText
("RELATED-TO", vText, "rfc_9074_example_2", 1),
("ACKNOWLEDGED", vDDDTypes, "rfc_9074_example_3", 0),
("PROXIMITY", vText, "rfc_9074_example_proximity", 0),
]
],
)
def test_calendar_types(events, prop, cls, file, alarm_index):
"""Check the types of the properties."""

Wyświetl plik

@ -510,7 +510,8 @@ TZIDS = [
"Indian/Mauritius",
"Indian/Mayotte",
"Indian/Reunion",
"Iran", "Asia/Tehran",
"Iran",
"Asia/Tehran",
"Israel",
"Jamaica",
"Japan",
@ -524,7 +525,8 @@ TZIDS = [
"MST7MDT",
"Navajo",
"NZ",
"NZ-CHAT", "Pacific/Chatham",
"NZ-CHAT",
"Pacific/Chatham",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
@ -594,4 +596,4 @@ TZIDS = [
"WET",
"W-SU",
"Zulu",
]
]

Wyświetl plik

@ -1,9 +1,11 @@
"""This package contains all functionality for timezones."""
from .tzid import tzid_from_dt, tzid_from_tzinfo, tzids_from_tzinfo
from .tzp import TZP
tzp = TZP()
def use_pytz():
"""Use pytz as the implementation that looks up and creates timezones."""
tzp.use_pytz()
@ -13,6 +15,7 @@ def use_zoneinfo():
"""Use zoneinfo as the implementation that looks up and creates timezones."""
tzp.use_zoneinfo()
__all__ = [
"TZP",
"tzp",
@ -20,5 +23,5 @@ __all__ = [
"use_zoneinfo",
"tzid_from_tzinfo",
"tzid_from_dt",
"tzids_from_tzinfo"
"tzids_from_tzinfo",
]

Wyświetl plik

@ -15,6 +15,7 @@ Run this module:
python -m icalendar.timezone.equivalent_timezone_ids
"""
from __future__ import annotations
from collections import defaultdict
@ -34,13 +35,16 @@ DTS = []
dt = START
while dt <= END:
DTS.append(dt)
dt += timedelta(hours=25) # This must be big enough to be fast and small enough to identify the timeszones before it is the present year
dt += timedelta(
hours=25
) # This must be big enough to be fast and small enough to identify the timeszones before it is the present year
del dt
def main(
create_timezones:list[Callable[[str], tzinfo]],
name:str,
):
create_timezones: list[Callable[[str], tzinfo]],
name: str,
):
"""Generate a lookup table for timezone information if unknown timezones.
We cannot create one lookup for all because they seem to be all equivalent
@ -50,24 +54,24 @@ def main(
unsorted_tzids = available_timezones()
unsorted_tzids.remove("localtime")
unsorted_tzids.remove("Factory")
class TZ(NamedTuple):
tz: tzinfo
id:str
id: str
tzs = [
TZ(create_timezone(tzid), tzid)
for create_timezone in create_timezones
for tzid in unsorted_tzids
]
def generate_tree(
tzs: list[TZ],
step:timedelta=timedelta(hours=1),
start:datetime=START,
end:datetime=END,
todo:Optional[set[str]]=None
) -> tuple[datetime, dict[timedelta, set[str]]] | set[str]: # should be recursive
tzs: list[TZ],
step: timedelta = timedelta(hours=1),
start: datetime = START,
end: datetime = END,
todo: Optional[set[str]] = None,
) -> tuple[datetime, dict[timedelta, set[str]]] | set[str]: # should be recursive
"""Generate a lookup tree."""
if todo is None:
todo = [tz.id for tz in tzs]
@ -79,14 +83,14 @@ def main(
todo.remove(tzs[0].id)
return {tzs[0].id}
while start < end:
offsets : dict[timedelta, list[TZ]] = defaultdict(list)
offsets: dict[timedelta, list[TZ]] = defaultdict(list)
try:
# if we are around a timezone change, we must move on
# see https://github.com/collective/icalendar/issues/776
around_tz_change = not all(
tz.tz.utcoffset(start) ==
tz.tz.utcoffset(start - DISTANCE_FROM_TIMEZONE_CHANGE) ==
tz.tz.utcoffset(start + DISTANCE_FROM_TIMEZONE_CHANGE)
tz.tz.utcoffset(start)
== tz.tz.utcoffset(start - DISTANCE_FROM_TIMEZONE_CHANGE)
== tz.tz.utcoffset(start + DISTANCE_FROM_TIMEZONE_CHANGE)
for tz in tzs
)
except (NonExistentTimeError, AmbiguousTimeError):
@ -101,7 +105,9 @@ def main(
continue
lookup = {}
for offset, tzs in offsets.items():
lookup[offset] = generate_tree(tzs=tzs, step=step, start=start + step, end=end, todo=todo)
lookup[offset] = generate_tree(
tzs=tzs, step=step, start=start + step, end=end, todo=todo
)
return start, lookup
print(f"reached end with {len(tzs)} timezones - assuming they are equivalent.")
result = set()
@ -110,7 +116,6 @@ def main(
todo.remove(tz.id)
return result
lookup = generate_tree(tzs, step=timedelta(hours=33))
file = Path(__file__).parent / f"equivalent_timezone_ids_{name}.py"
@ -118,7 +123,9 @@ def main(
print("lookup = ", end="")
pprint(lookup)
with file.open("w") as f:
f.write(f"'''This file is automatically generated by {Path(__file__).name}'''\n")
f.write(
f"'''This file is automatically generated by {Path(__file__).name}'''\n"
)
f.write("import datetime\n\n")
f.write("\nlookup = ")
pprint(lookup, stream=f)
@ -126,12 +133,16 @@ def main(
return lookup
__all__ = ["main"]
if __name__ == "__main__":
from dateutil.tz import gettz
from pytz import timezone
from zoneinfo import ZoneInfo
# add more timezone implementations if you like
main([ZoneInfo, timezone, gettz], "result",
main(
[ZoneInfo, timezone, gettz],
"result",
)

Wyświetl plik

@ -1,4 +1,5 @@
"""The interface for timezone implementations."""
from __future__ import annotations
from abc import ABC, abstractmethod
@ -33,7 +34,7 @@ class TZProvider(ABC):
"""Whether the timezone is already cached by the implementation."""
@abstractmethod
def fix_rrule_until(self, rrule:rrule, ical_rrule:prop.vRecur) -> None:
def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None:
"""Make sure the until value works for the rrule generated from the ical_rrule."""
@abstractmethod
@ -52,4 +53,5 @@ class TZProvider(ABC):
def uses_zoneinfo(self) -> bool:
"""Whether we use zoneinfo."""
__all__ = ["TZProvider"]

Wyświetl plik

@ -1,4 +1,5 @@
"""Use pytz timezones."""
from __future__ import annotations
import pytz
from .. import cal
@ -10,7 +11,6 @@ from icalendar import prop
from dateutil.rrule import rrule
class PYTZ(TZProvider):
"""Provide icalendar with timezones from pytz."""
@ -18,7 +18,7 @@ class PYTZ(TZProvider):
def localize_utc(self, dt: datetime) -> datetime:
"""Return the datetime in UTC."""
if getattr(dt, 'tzinfo', False) and dt.tzinfo is not None:
if getattr(dt, "tzinfo", False) and dt.tzinfo is not None:
return dt.astimezone(pytz.utc)
# assume UTC for naive datetime instances
return pytz.utc.localize(dt)
@ -31,9 +31,9 @@ class PYTZ(TZProvider):
"""Whether the timezone is already cached by the implementation."""
return id in pytz.all_timezones
def fix_rrule_until(self, rrule:rrule, ical_rrule:prop.vRecur) -> None:
def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None:
"""Make sure the until value works for the rrule generated from the ical_rrule."""
if not {'UNTIL', 'COUNT'}.intersection(ical_rrule.keys()):
if not {"UNTIL", "COUNT"}.intersection(ical_rrule.keys()):
# pytz.timezones don't know any transition dates after 2038
# either
rrule._until = datetime(2038, 12, 31, tzinfo=pytz.UTC)
@ -42,11 +42,15 @@ class PYTZ(TZProvider):
"""Create a pytz timezone from the given information."""
transition_times, transition_info = tz.get_transitions()
name = tz.tz_name
cls = type(name, (DstTzInfo,), {
'zone': name,
'_utc_transition_times': transition_times,
'_transition_info': transition_info
})
cls = type(
name,
(DstTzInfo,),
{
"zone": name,
"_utc_transition_times": transition_times,
"_transition_info": transition_info,
},
)
return cls()
def timezone(self, name: str) -> Optional[tzinfo]:

Wyświetl plik

@ -4,6 +4,7 @@ Normally, timezones have ids.
This is a way to access the ids if you have a
datetime.tzinfo object.
"""
from __future__ import annotations
from collections import defaultdict
@ -34,7 +35,7 @@ def tzids_from_tzinfo(tzinfo: Optional[tzinfo]) -> tuple[str]:
>>> tzids_from_tzinfo(gettz("Europe/Berlin"))
('Europe/Berlin', 'Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna')
""" # The example might need to change if you recreate the lookup tree
""" # The example might need to change if you recreate the lookup tree
if tzinfo is None:
return ()
if hasattr(tzinfo, "zone"):
@ -71,6 +72,7 @@ def tzid_from_tzinfo(tzinfo: Optional[tzinfo]) -> Optional[str]:
return None
return tzids[0]
def tzid_from_dt(dt: datetime) -> Optional[str]:
"""Retrieve the timezone id from the datetime object."""
tzid = tzid_from_tzinfo(dt.tzinfo)
@ -106,4 +108,5 @@ def get_equivalent_tzids(tzid: str) -> tuple[str]:
ids = _EQUIVALENT_IDS.get(tzid, set())
return (tzid,) + tuple(sorted(ids - {tzid}))
__all__ = ["tzid_from_tzinfo", "tzid_from_dt", "tzids_from_tzinfo"]

Wyświetl plik

@ -26,31 +26,35 @@ class TZP:
All of icalendar will then use this timezone implementation.
"""
def __init__(self, provider:Union[str, TZProvider]=DEFAULT_TIMEZONE_PROVIDER):
def __init__(self, provider: Union[str, TZProvider] = DEFAULT_TIMEZONE_PROVIDER):
"""Create a new timezone implementation proxy."""
self.use(provider)
def use_pytz(self) -> None:
"""Use pytz as the timezone provider."""
from .pytz import PYTZ
self._use(PYTZ())
def use_zoneinfo(self) -> None:
"""Use zoneinfo as the timezone provider."""
from .zoneinfo import ZONEINFO
self._use(ZONEINFO())
def _use(self, provider:TZProvider) -> None:
def _use(self, provider: TZProvider) -> None:
"""Use a timezone implementation."""
self.__tz_cache = {}
self.__provider = provider
def use(self, provider:Union[str, TZProvider]):
def use(self, provider: Union[str, TZProvider]):
"""Switch to a different timezone provider."""
if isinstance(provider, str):
use_provider = getattr(self, f"use_{provider}", None)
if use_provider is None:
raise ValueError(f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'.")
raise ValueError(
f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'."
)
use_provider()
else:
self._use(provider)
@ -66,7 +70,9 @@ class TZP:
"""
return self.__provider.localize_utc(to_datetime(dt))
def localize(self, dt: datetime.date, tz: Union[datetime.tzinfo, str]) -> datetime.datetime:
def localize(
self, dt: datetime.date, tz: Union[datetime.tzinfo, str]
) -> datetime.datetime:
"""Localize a datetime to a timezone."""
if isinstance(tz, str):
tz = self.timezone(tz)
@ -79,14 +85,16 @@ class TZP:
This can influence the result from timezone(): Once cached, the
custom timezone is returned from timezone().
"""
_unclean_id = timezone_component['TZID']
_unclean_id = timezone_component["TZID"]
_id = self.clean_timezone_id(_unclean_id)
if not self.__provider.knows_timezone_id(_id) \
and not self.__provider.knows_timezone_id(_unclean_id) \
and _id not in self.__tz_cache:
if (
not self.__provider.knows_timezone_id(_id)
and not self.__provider.knows_timezone_id(_unclean_id)
and _id not in self.__tz_cache
):
self.__tz_cache[_id] = timezone_component.to_tz(self, lookup_tzid=False)
def fix_rrule_until(self, rrule:rrule, ical_rrule:prop.vRecur) -> None:
def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None:
"""Make sure the until value works."""
self.__provider.fix_rrule_until(rrule, ical_rrule)
@ -132,4 +140,5 @@ class TZP:
def __repr__(self) -> str:
return f"{self.__class__.__name__}({repr(self.name)})"
__all__ = ["TZP"]

Wyświetl plik

@ -10,110 +10,110 @@ for this mapping is also available at the unicode consortium [1].
"""
WINDOWS_TO_OLSON = {
'AUS Central Standard Time': 'Australia/Darwin',
'AUS Eastern Standard Time': 'Australia/Sydney',
'Afghanistan Standard Time': 'Asia/Kabul',
'Alaskan Standard Time': 'America/Anchorage',
'Arab Standard Time': 'Asia/Riyadh',
'Arabian Standard Time': 'Asia/Dubai',
'Arabic Standard Time': 'Asia/Baghdad',
'Argentina Standard Time': 'America/Argentina/Buenos_Aires',
'Atlantic Standard Time': 'America/Halifax',
'Azerbaijan Standard Time': 'Asia/Baku',
'Azores Standard Time': 'Atlantic/Azores',
'Bahia Standard Time': 'America/Bahia',
'Bangladesh Standard Time': 'Asia/Dhaka',
'Belarus Standard Time': 'Europe/Minsk',
'Canada Central Standard Time': 'America/Regina',
'Cape Verde Standard Time': 'Atlantic/Cape_Verde',
'Caucasus Standard Time': 'Asia/Yerevan',
'Cen. Australia Standard Time': 'Australia/Adelaide',
'Central America Standard Time': 'America/Guatemala',
'Central Asia Standard Time': 'Asia/Almaty',
'Central Brazilian Standard Time': 'America/Cuiaba',
'Central Europe Standard Time': 'Europe/Budapest',
'Central European Standard Time': 'Europe/Warsaw',
'Central Pacific Standard Time': 'Pacific/Guadalcanal',
'Central Standard Time': 'America/Chicago',
'Central Standard Time (Mexico)': 'America/Mexico_City',
'China Standard Time': 'Asia/Shanghai',
'Dateline Standard Time': 'Etc/GMT+12',
'E. Africa Standard Time': 'Africa/Nairobi',
'E. Australia Standard Time': 'Australia/Brisbane',
'E. Europe Standard Time': 'Europe/Chisinau',
'E. South America Standard Time': 'America/Sao_Paulo',
'Eastern Standard Time': 'America/New_York',
'Eastern Standard Time (Mexico)': 'America/Cancun',
'Egypt Standard Time': 'Africa/Cairo',
'Ekaterinburg Standard Time': 'Asia/Yekaterinburg',
'FLE Standard Time': 'Europe/Kyiv',
'Fiji Standard Time': 'Pacific/Fiji',
'GMT Standard Time': 'Europe/London',
'GTB Standard Time': 'Europe/Bucharest',
'Georgian Standard Time': 'Asia/Tbilisi',
'Greenland Standard Time': 'America/Nuuk',
'Greenwich Standard Time': 'Atlantic/Reykjavik',
'Hawaiian Standard Time': 'Pacific/Honolulu',
'India Standard Time': 'Asia/Kolkata',
'Iran Standard Time': 'Asia/Tehran',
'Israel Standard Time': 'Asia/Jerusalem',
'Jordan Standard Time': 'Asia/Amman',
'Kaliningrad Standard Time': 'Europe/Kaliningrad',
'Korea Standard Time': 'Asia/Seoul',
'Libya Standard Time': 'Africa/Tripoli',
'Line Islands Standard Time': 'Pacific/Kiritimati',
'Magadan Standard Time': 'Asia/Magadan',
'Mauritius Standard Time': 'Indian/Mauritius',
'Middle East Standard Time': 'Asia/Beirut',
'Montevideo Standard Time': 'America/Montevideo',
'Morocco Standard Time': 'Africa/Casablanca',
'Mountain Standard Time': 'America/Denver',
'Mountain Standard Time (Mexico)': 'America/Chihuahua',
'Myanmar Standard Time': 'Asia/Yangon',
'N. Central Asia Standard Time': 'Asia/Novosibirsk',
'Namibia Standard Time': 'Africa/Windhoek',
'Nepal Standard Time': 'Asia/Kathmandu',
'New Zealand Standard Time': 'Pacific/Auckland',
'Newfoundland Standard Time': 'America/St_Johns',
'North Asia East Standard Time': 'Asia/Irkutsk',
'North Asia Standard Time': 'Asia/Krasnoyarsk',
'North Korea Standard Time': 'Asia/Pyongyang',
'Pacific SA Standard Time': 'America/Santiago',
'Pacific Standard Time': 'America/Los_Angeles',
'Pakistan Standard Time': 'Asia/Karachi',
'Paraguay Standard Time': 'America/Asuncion',
'Romance Standard Time': 'Europe/Paris',
'Russia Time Zone 10': 'Asia/Srednekolymsk',
'Russia Time Zone 11': 'Asia/Kamchatka',
'Russia Time Zone 3': 'Europe/Samara',
'Russian Standard Time': 'Europe/Moscow',
'SA Eastern Standard Time': 'America/Cayenne',
'SA Pacific Standard Time': 'America/Bogota',
'SA Western Standard Time': 'America/La_Paz',
'SE Asia Standard Time': 'Asia/Bangkok',
'Samoa Standard Time': 'Pacific/Apia',
'Singapore Standard Time': 'Asia/Singapore',
'South Africa Standard Time': 'Africa/Johannesburg',
'Sri Lanka Standard Time': 'Asia/Colombo',
'Syria Standard Time': 'Asia/Damascus',
'Taipei Standard Time': 'Asia/Taipei',
'Tasmania Standard Time': 'Australia/Hobart',
'Tokyo Standard Time': 'Asia/Tokyo',
'Tonga Standard Time': 'Pacific/Tongatapu',
'Turkey Standard Time': 'Europe/Istanbul',
'US Eastern Standard Time': 'America/Indiana/Indianapolis',
'US Mountain Standard Time': 'America/Phoenix',
'UTC': 'Etc/GMT',
'UTC+12': 'Etc/GMT-12',
'UTC-02': 'Etc/GMT+2',
'UTC-11': 'Etc/GMT+11',
'Ulaanbaatar Standard Time': 'Asia/Ulaanbaatar',
'Venezuela Standard Time': 'America/Caracas',
'Vladivostok Standard Time': 'Asia/Vladivostok',
'W. Australia Standard Time': 'Australia/Perth',
'W. Central Africa Standard Time': 'Africa/Lagos',
'W. Europe Standard Time': 'Europe/Berlin',
'West Asia Standard Time': 'Asia/Tashkent',
'West Pacific Standard Time': 'Pacific/Port_Moresby',
'Yakutsk Standard Time': 'Asia/Yakutsk',
"AUS Central Standard Time": "Australia/Darwin",
"AUS Eastern Standard Time": "Australia/Sydney",
"Afghanistan Standard Time": "Asia/Kabul",
"Alaskan Standard Time": "America/Anchorage",
"Arab Standard Time": "Asia/Riyadh",
"Arabian Standard Time": "Asia/Dubai",
"Arabic Standard Time": "Asia/Baghdad",
"Argentina Standard Time": "America/Argentina/Buenos_Aires",
"Atlantic Standard Time": "America/Halifax",
"Azerbaijan Standard Time": "Asia/Baku",
"Azores Standard Time": "Atlantic/Azores",
"Bahia Standard Time": "America/Bahia",
"Bangladesh Standard Time": "Asia/Dhaka",
"Belarus Standard Time": "Europe/Minsk",
"Canada Central Standard Time": "America/Regina",
"Cape Verde Standard Time": "Atlantic/Cape_Verde",
"Caucasus Standard Time": "Asia/Yerevan",
"Cen. Australia Standard Time": "Australia/Adelaide",
"Central America Standard Time": "America/Guatemala",
"Central Asia Standard Time": "Asia/Almaty",
"Central Brazilian Standard Time": "America/Cuiaba",
"Central Europe Standard Time": "Europe/Budapest",
"Central European Standard Time": "Europe/Warsaw",
"Central Pacific Standard Time": "Pacific/Guadalcanal",
"Central Standard Time": "America/Chicago",
"Central Standard Time (Mexico)": "America/Mexico_City",
"China Standard Time": "Asia/Shanghai",
"Dateline Standard Time": "Etc/GMT+12",
"E. Africa Standard Time": "Africa/Nairobi",
"E. Australia Standard Time": "Australia/Brisbane",
"E. Europe Standard Time": "Europe/Chisinau",
"E. South America Standard Time": "America/Sao_Paulo",
"Eastern Standard Time": "America/New_York",
"Eastern Standard Time (Mexico)": "America/Cancun",
"Egypt Standard Time": "Africa/Cairo",
"Ekaterinburg Standard Time": "Asia/Yekaterinburg",
"FLE Standard Time": "Europe/Kyiv",
"Fiji Standard Time": "Pacific/Fiji",
"GMT Standard Time": "Europe/London",
"GTB Standard Time": "Europe/Bucharest",
"Georgian Standard Time": "Asia/Tbilisi",
"Greenland Standard Time": "America/Nuuk",
"Greenwich Standard Time": "Atlantic/Reykjavik",
"Hawaiian Standard Time": "Pacific/Honolulu",
"India Standard Time": "Asia/Kolkata",
"Iran Standard Time": "Asia/Tehran",
"Israel Standard Time": "Asia/Jerusalem",
"Jordan Standard Time": "Asia/Amman",
"Kaliningrad Standard Time": "Europe/Kaliningrad",
"Korea Standard Time": "Asia/Seoul",
"Libya Standard Time": "Africa/Tripoli",
"Line Islands Standard Time": "Pacific/Kiritimati",
"Magadan Standard Time": "Asia/Magadan",
"Mauritius Standard Time": "Indian/Mauritius",
"Middle East Standard Time": "Asia/Beirut",
"Montevideo Standard Time": "America/Montevideo",
"Morocco Standard Time": "Africa/Casablanca",
"Mountain Standard Time": "America/Denver",
"Mountain Standard Time (Mexico)": "America/Chihuahua",
"Myanmar Standard Time": "Asia/Yangon",
"N. Central Asia Standard Time": "Asia/Novosibirsk",
"Namibia Standard Time": "Africa/Windhoek",
"Nepal Standard Time": "Asia/Kathmandu",
"New Zealand Standard Time": "Pacific/Auckland",
"Newfoundland Standard Time": "America/St_Johns",
"North Asia East Standard Time": "Asia/Irkutsk",
"North Asia Standard Time": "Asia/Krasnoyarsk",
"North Korea Standard Time": "Asia/Pyongyang",
"Pacific SA Standard Time": "America/Santiago",
"Pacific Standard Time": "America/Los_Angeles",
"Pakistan Standard Time": "Asia/Karachi",
"Paraguay Standard Time": "America/Asuncion",
"Romance Standard Time": "Europe/Paris",
"Russia Time Zone 10": "Asia/Srednekolymsk",
"Russia Time Zone 11": "Asia/Kamchatka",
"Russia Time Zone 3": "Europe/Samara",
"Russian Standard Time": "Europe/Moscow",
"SA Eastern Standard Time": "America/Cayenne",
"SA Pacific Standard Time": "America/Bogota",
"SA Western Standard Time": "America/La_Paz",
"SE Asia Standard Time": "Asia/Bangkok",
"Samoa Standard Time": "Pacific/Apia",
"Singapore Standard Time": "Asia/Singapore",
"South Africa Standard Time": "Africa/Johannesburg",
"Sri Lanka Standard Time": "Asia/Colombo",
"Syria Standard Time": "Asia/Damascus",
"Taipei Standard Time": "Asia/Taipei",
"Tasmania Standard Time": "Australia/Hobart",
"Tokyo Standard Time": "Asia/Tokyo",
"Tonga Standard Time": "Pacific/Tongatapu",
"Turkey Standard Time": "Europe/Istanbul",
"US Eastern Standard Time": "America/Indiana/Indianapolis",
"US Mountain Standard Time": "America/Phoenix",
"UTC": "Etc/GMT",
"UTC+12": "Etc/GMT-12",
"UTC-02": "Etc/GMT+2",
"UTC-11": "Etc/GMT+11",
"Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
"Venezuela Standard Time": "America/Caracas",
"Vladivostok Standard Time": "Asia/Vladivostok",
"W. Australia Standard Time": "Australia/Perth",
"W. Central Africa Standard Time": "Africa/Lagos",
"W. Europe Standard Time": "Europe/Berlin",
"West Asia Standard Time": "Asia/Tashkent",
"West Pacific Standard Time": "Pacific/Port_Moresby",
"Yakutsk Standard Time": "Asia/Yakutsk",
}

Wyświetl plik

@ -1,4 +1,5 @@
"""Use zoneinfo timezones"""
from __future__ import annotations
try:
@ -35,7 +36,7 @@ class ZONEINFO(TZProvider):
def localize_utc(self, dt: datetime) -> datetime:
"""Return the datetime in UTC."""
if getattr(dt, 'tzinfo', False) and dt.tzinfo is not None:
if getattr(dt, "tzinfo", False) and dt.tzinfo is not None:
return dt.astimezone(self.utc)
return self.localize(dt, self.utc)
@ -53,9 +54,9 @@ class ZONEINFO(TZProvider):
"""Whether the timezone is already cached by the implementation."""
return id in self._available_timezones
def fix_rrule_until(self, rrule:rrule, ical_rrule:prop.vRecur) -> None:
def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None:
"""Make sure the until value works for the rrule generated from the ical_rrule."""
if not {'UNTIL', 'COUNT'}.intersection(ical_rrule.keys()):
if not {"UNTIL", "COUNT"}.intersection(ical_rrule.keys()):
# zoninfo does not know any transition dates after 2038
rrule._until = datetime(2038, 12, 31, tzinfo=self.utc)
@ -87,10 +88,11 @@ class ZONEINFO(TZProvider):
return True
def pickle_tzicalvtz(tzicalvtz:tz._tzicalvtz):
def pickle_tzicalvtz(tzicalvtz: tz._tzicalvtz):
"""Because we use dateutil.tzical, we need to make it pickle-able."""
return _tzicalvtz, (tzicalvtz._tzid, tzicalvtz._comps)
copyreg.pickle(_tzicalvtz, pickle_tzicalvtz)
@ -99,19 +101,23 @@ def pickle_rrule_with_cache(self: rrule):
This is mainly copied from rrule.replace.
"""
new_kwargs = {"interval": self._interval,
"count": self._count,
"dtstart": self._dtstart,
"freq": self._freq,
"until": self._until,
"wkst": self._wkst,
"cache": False if self._cache is None else True }
new_kwargs = {
"interval": self._interval,
"count": self._count,
"dtstart": self._dtstart,
"freq": self._freq,
"until": self._until,
"wkst": self._wkst,
"cache": False if self._cache is None else True,
}
new_kwargs.update(self._original_rule)
# from https://stackoverflow.com/a/64915638/1320237
return functools.partial(rrule, new_kwargs.pop("freq"), **new_kwargs), ()
copyreg.pickle(rrule, pickle_rrule_with_cache)
def pickle_rruleset_with_cache(rs: rruleset):
"""Pickle an rruleset."""
# self._rrule = []
@ -119,19 +125,28 @@ def pickle_rruleset_with_cache(rs: rruleset):
# self._exrule = []
# self._exdate = []
return unpickle_rruleset_with_cache, (
rs._rrule, rs._rdate, rs._exrule,
rs._exdate, False if rs._cache is None else True
rs._rrule,
rs._rdate,
rs._exrule,
rs._exdate,
False if rs._cache is None else True,
)
def unpickle_rruleset_with_cache(rrule, rdate, exrule, exdate, cache):
"""unpickling the rruleset."""
rs = rruleset(cache)
for o in rrule: rs.rrule(o)
for o in rdate: rs.rdate(o)
for o in exrule: rs.exrule(o)
for o in exdate: rs.exdate(o)
for o in rrule:
rs.rrule(o)
for o in rdate:
rs.rdate(o)
for o in exrule:
rs.exrule(o)
for o in exdate:
rs.exdate(o)
return rs
copyreg.pickle(rruleset, pickle_rruleset_with_cache)
__all__ = ["ZONEINFO"]

Wyświetl plik

@ -7,15 +7,13 @@ from icalendar.parser_tools import to_unicode
class UIDGenerator:
"""If you are too lazy to create real uid's.
"""If you are too lazy to create real uid's."""
"""
chars = list(ascii_letters + digits)
@staticmethod
def rnd_string(length=16):
"""Generates a string with random characters of length.
"""
"""Generates a string with random characters of length."""
return "".join([random.choice(UIDGenerator.chars) for _ in range(length)])
@staticmethod
@ -26,6 +24,7 @@ class UIDGenerator:
20050105T225746Z-HKtJMqUgdO0jDUwm@example.com
"""
from icalendar.prop import vDatetime, vText
host_name = to_unicode(host_name)
unique = unique or UIDGenerator.rnd_string()
today = to_unicode(vDatetime(datetime.today()).to_ical())
@ -70,4 +69,12 @@ def normalize_pytz(dt: date):
return dt
__all__ = ["UIDGenerator", "is_date", "is_datetime", "to_datetime", "is_pytz", "is_pytz_dt", "normalize_pytz"]
__all__ = [
"UIDGenerator",
"is_date",
"is_datetime",
"to_datetime",
"is_pytz",
"is_pytz_dt",
"normalize_pytz",
]