Merge pull request #575 from niccokunzmann/comparing

Compare test calendars to find components that do not equal
pull/576/head
Jaca 2023-11-02 21:26:30 +01:00 zatwierdzone przez GitHub
commit 9ce1fa8c8f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
17 zmienionych plików z 226 dodań i 58 usunięć

Wyświetl plik

@ -29,6 +29,8 @@ Bug fixes:
- Multivalue FREEBUSY property is now parsed properly
Ref: #27
[jacadzaca]
- Compare equality and inequality of calendars more completely
Ref: #570
- Use non legacy timezone name.
Ref: #567
- Add some compare functions.
@ -36,7 +38,7 @@ Bug fixes:
- Change OSS Fuzz build script to point to harnesses in fuzzing directory
Ref: #574
5.0.10 (unreleased)
5.0.10 (2023-09-26)
-------------------
Bug fixes:
@ -92,7 +94,7 @@ Minor changes:
Minor changes:
- Added support for BYWEEKDAY in vRecur ref: #268
- Added support for BYWEEKDAY in vRecur ref: #268
Bug fixes:
@ -104,7 +106,7 @@ Bug fixes:
Minor changes:
- Improved documentation
Ref: #503, #504
Ref: #503, #504
Bug fixes:

Wyświetl plik

@ -447,7 +447,7 @@ class Component(CaselessDict):
return f"{self.name or type(self).__name__}({dict(self)}{', ' + subs if subs else ''})"
def __eq__(self, other):
if not len(self.subcomponents) == len(other.subcomponents):
if len(self.subcomponents) != len(other.subcomponents):
return False
properties_equal = super().__eq__(other)
@ -465,7 +465,6 @@ class Component(CaselessDict):
return True
#######################################
# components defined in RFC 5545

Wyświetl plik

@ -88,6 +88,9 @@ class CaselessDict(OrderedDict):
def __eq__(self, other):
return self is other or dict(self.items()) == dict(other.items())
def __ne__(self, other):
return not self == other
# A list of keys that must appear first in sorted_keys and sorted_items;
# must be uppercase.
canonical_order = None

Wyświetl plik

@ -145,6 +145,10 @@ class vBinary:
except UnicodeError:
raise ValueError('Not valid base 64 encoding.')
def __eq__(self, other):
"""self == other"""
return isinstance(other, vBinary) and self.obj == other.obj
class vBoolean(int):
"""Returns specific string according to state.
@ -269,6 +273,7 @@ class vCategory:
if not hasattr(c_list, '__iter__') or isinstance(c_list, str):
c_list = [c_list]
self.cats = [vText(c) for c in c_list]
self.params = Parameters()
def to_ical(self):
return b",".join([c.to_ical() for c in self.cats])
@ -279,8 +284,24 @@ class vCategory:
out = unescape_char(ical).split(',')
return out
def __eq__(self, other):
"""self == other"""
return isinstance(other, vCategory) and self.cats == other.cats
class vDDDTypes:
class TimeBase:
"""Make classes with a datetime/date comparable."""
def __eq__(self, other):
"""self == other"""
if isinstance(other, TimeBase):
return self.params == other.params and self.dt == other.dt
return False
def __hash__(self):
return hash(self.dt)
class vDDDTypes(TimeBase):
"""A combined Datetime, Date or Duration parser/generator. Their format
cannot be confused, and often values can be of either types.
So this is practical.
@ -290,7 +311,7 @@ class vDDDTypes:
if not isinstance(dt, (datetime, date, timedelta, time, tuple)):
raise ValueError('You must use datetime, date, timedelta, '
'time or tuple (for periods)')
if isinstance(dt, datetime):
if isinstance(dt, (datetime, timedelta)):
self.params = Parameters()
elif isinstance(dt, date):
self.params = Parameters({'value': 'DATE'})
@ -320,14 +341,6 @@ class vDDDTypes:
else:
raise ValueError(f'Unknown date type: {type(dt)}')
def __eq__(self, other):
if isinstance(other, vDDDTypes):
return self.params == other.params and self.dt == other.dt
return False
def __hash__(self):
return hash(self.dt)
@classmethod
def from_ical(cls, ical, timezone=None):
if isinstance(ical, cls):
@ -349,8 +362,11 @@ class vDDDTypes:
f"Expected datetime, date, or time, got: '{ical}'"
)
def __repr__(self):
"""repr(self)"""
return f"{self.__class__.__name__}({self.dt}, {self.params})"
class vDate:
class vDate(TimeBase):
"""Render and generates iCalendar date format.
"""
@ -377,7 +393,7 @@ class vDate:
raise ValueError(f'Wrong date format {ical}')
class vDatetime:
class vDatetime(TimeBase):
"""Render and generates icalendar datetime format.
vDatetime is timezone aware and uses the pytz library, an implementation of
@ -438,7 +454,7 @@ class vDatetime:
raise ValueError(f'Wrong datetime format: {ical}')
class vDuration:
class vDuration(TimeBase):
"""Subclass of timedelta that renders itself in the iCalendar DURATION
format.
"""
@ -495,8 +511,12 @@ class vDuration:
return value
@property
def dt(self):
"""The time delta for compatibility."""
return self.td
class vPeriod:
class vPeriod(TimeBase):
"""A precise period of time.
"""
@ -520,7 +540,7 @@ class vPeriod:
if start > end:
raise ValueError("Start time is greater than end time")
self.params = Parameters()
self.params = Parameters({'value': 'PERIOD'})
# set the timezone identifier
# does not support different timezones for start and end
tzid = tzid_from_dt(start)
@ -532,17 +552,6 @@ class vPeriod:
self.by_duration = by_duration
self.duration = duration
def __cmp__(self, other):
if not isinstance(other, vPeriod):
raise NotImplementedError(
f'Cannot compare vPeriod with {other!r}')
return cmp((self.start, self.end), (other.start, other.end))
def __eq__(self, other):
if not isinstance(other, vPeriod):
return False
return (self.start, self.end) == (other.start, other.end)
def overlaps(self, other):
if self.start > other.start:
return other.overlaps(self)
@ -574,6 +583,10 @@ class vPeriod:
p = (self.start, self.end)
return f'vPeriod({p!r})'
@property
def dt(self):
"""Make this cooperate with the other vDDDTypes."""
return (self.start, (self.duration if self.by_duration else self.end))
class vWeekday(str):
"""This returns an unquoted weekday abbrevation.
@ -740,7 +753,7 @@ class vText(str):
return cls(ical_unesc)
class vTime:
class vTime(TimeBase):
"""Render and generates iCalendar time format.
"""
@ -814,6 +827,8 @@ class vGeo:
except Exception:
raise ValueError(f"Expected 'float;float' , got: {ical}")
def __eq__(self, other):
return self.to_ical() == other.to_ical()
class vUTCOffset:
"""Renders itself as a utc offset.

Wyświetl plik

@ -4,4 +4,4 @@ SUMMARY:Example calendar with a ': ' in the summary
END:VEVENT
BEGIN:VEVENT
SUMMARY:Another event with a ': ' in the summary
END:VEVENT
END:VEVENT

Wyświetl plik

@ -15,10 +15,16 @@ class DataSource:
self._parser = parser
self._data_source_folder = data_source_folder
def keys(self):
"""Return all the files that could be used."""
return [file[:-4] for file in os.listdir(self._data_source_folder) if file.lower().endswith(".ics")]
def __getattr__(self, attribute):
"""Parse a file and return the result stored in the attribute."""
source_file = attribute.replace('-', '_') + '.ics'
source_path = os.path.join(self._data_source_folder, source_file)
if not os.path.isfile(source_path):
raise AttributeError(f"{source_path} does not exist.")
with open(source_path, 'rb') as f:
raw_ics = f.read()
source = self._parser(raw_ics)
@ -40,20 +46,23 @@ class DataSource:
HERE = os.path.dirname(__file__)
CALENDARS_FOLDER = os.path.join(HERE, 'calendars')
CALENDARS = DataSource(CALENDARS_FOLDER, icalendar.Calendar.from_ical)
TIMEZONES_FOLDER = os.path.join(HERE, 'timezones')
TIMEZONES = DataSource(TIMEZONES_FOLDER, icalendar.Timezone.from_ical)
EVENTS_FOLDER = os.path.join(HERE, 'events')
EVENTS = DataSource(EVENTS_FOLDER, icalendar.Event.from_ical)
@pytest.fixture
@pytest.fixture()
def calendars():
return DataSource(CALENDARS_FOLDER, icalendar.Calendar.from_ical)
return CALENDARS
@pytest.fixture
@pytest.fixture()
def timezones():
return DataSource(TIMEZONES_FOLDER, icalendar.Timezone.from_ical)
return TIMEZONES
@pytest.fixture
@pytest.fixture()
def events():
return DataSource(EVENTS_FOLDER, icalendar.Event.from_ical)
return EVENTS
@pytest.fixture(params=[
pytz.utc,
@ -71,3 +80,19 @@ def utc(request):
])
def in_timezone(request):
return request.param
@pytest.fixture(params=[
(data, key)
for data in [CALENDARS, TIMEZONES, EVENTS]
for key in data.keys() if key not in
( # exclude broken calendars here
"big_bad_calendar", "issue_104_broken_calendar", "small_bad_calendar",
"multiple_calendar_components", "pr_480_summary_with_colon"
)
])
def ics_file(request):
"""An example ICS file."""
data, key = request.param
print(key)
return data[key]

Wyświetl plik

@ -0,0 +1,131 @@
"""Test the equality and inequality of components."""
import copy
import pytz
from icalendar.prop import *
from datetime import datetime, date, timedelta
import pytest
def test_parsed_calendars_are_equal_if_parsed_again(ics_file):
"""Ensure that a calendar equals the same calendar."""
copy_of_calendar = ics_file.__class__.from_ical(ics_file.to_ical())
assert copy_of_calendar == ics_file
assert not copy_of_calendar != ics_file
def test_parsed_calendars_are_equal_if_from_same_source(ics_file):
"""Ensure that a calendar equals the same calendar."""
cal1 = ics_file.__class__.from_ical(ics_file.raw_ics)
cal2 = ics_file.__class__.from_ical(ics_file.raw_ics)
assert cal1 == cal2
assert not cal1 != cal2
def test_copies_are_equal(ics_file):
"""Ensure that copies are equal."""
copy1 = ics_file.copy(); copy1.subcomponents = ics_file.subcomponents
copy2 = ics_file.copy(); copy2.subcomponents = ics_file.subcomponents[:]
assert copy1 == copy2
assert copy1 == ics_file
assert copy2 == ics_file
assert not copy1 != copy2
assert not copy1 != ics_file
assert not copy2 != ics_file
def test_copy_does_not_copy_subcomponents(calendars):
"""If we copy the subcomponents, assumptions around copies will be broken."""
assert calendars.timezoned.subcomponents
assert not calendars.timezoned.copy().subcomponents
def test_deep_copies_are_equal(ics_file):
"""Ensure that deep copies are equal."""
try:
assert copy.deepcopy(ics_file) == copy.deepcopy(ics_file)
assert copy.deepcopy(ics_file) == ics_file
assert not copy.deepcopy(ics_file) != copy.deepcopy(ics_file)
assert not copy.deepcopy(ics_file) != ics_file
except pytz.UnknownTimeZoneError:
# Ignore errors when a custom time zone is used.
# This is still covered by the parsing test.
pass
def test_vGeo():
"""Check the equality of vGeo."""
assert vGeo(("100", "12.33")) == vGeo(("100.00", "12.330"))
assert vGeo(("100", "12.331")) != vGeo(("100.00", "12.330"))
assert vGeo(("10", "12.33")) != vGeo(("100.00", "12.330"))
def test_vBinary():
assert vBinary('asd') == vBinary('asd')
assert vBinary('asdf') != vBinary('asd')
def test_vBoolean():
assert vBoolean.from_ical('TRUE') == vBoolean.from_ical('TRUE')
assert vBoolean.from_ical('FALSE') == vBoolean.from_ical('FALSE')
assert vBoolean.from_ical('TRUE') != vBoolean.from_ical('FALSE')
def test_vCategory():
assert vCategory("HELLO") == vCategory("HELLO")
assert vCategory(["a","b"]) == vCategory(["a","b"])
assert vCategory(["a","b"]) != vCategory(["a","b", "c"])
def test_vText():
assert vText("HELLO") == vText("HELLO")
assert not vText("HELLO") != vText("HELLO")
assert vText("HELLO1") != vText("HELLO")
assert not vText("HELLO1") == vText("HELLO")
@pytest.mark.parametrize(
"vType,v1,v2",
[
(vDatetime, datetime(2023, 11, 1, 10, 11), datetime(2023, 11, 1, 10, 10)),
(vDate, date(2023, 11, 1), date(2023, 10, 31)),
(vDuration, timedelta(3, 11, 1), timedelta(23, 10, 31)),
(vPeriod, (datetime(2023, 11, 1, 10, 11), timedelta(3, 11, 1)), (datetime(2023, 11, 1, 10, 11), timedelta(23, 10, 31))),
(vPeriod, (datetime(2023, 11, 1, 10, 1), timedelta(3, 11, 1)), (datetime(2023, 11, 1, 10, 11), timedelta(3, 11, 1))),
(vPeriod, (datetime(2023, 11, 1, 10, 1), datetime(2023, 11, 1, 10, 3)), (datetime(2023, 11, 1, 10, 1), datetime(2023, 11, 1, 10, 2))),
(vTime, time(10, 10, 10), time(10, 10, 11)),
]
)
@pytest.mark.parametrize("eq", ["==", "!="])
@pytest.mark.parametrize("cls1", [0, 1])
@pytest.mark.parametrize("cls2", [0, 1])
@pytest.mark.parametrize("hash", [lambda x:x, hash])
def test_vDDDTypes_and_others(vType, v1, v2, cls1, cls2, eq, hash):
"""Check equality and inequality."""
t1 = (vType, vDDDTypes)[cls1]
t2 = (vType, vDDDTypes)[cls2]
if eq == "==":
assert hash(v1) == hash(v1)
assert hash(t1(v1)) == hash(t2(v1))
assert not hash(t1(v1)) != hash(t2(v1))
else:
assert hash(v1) != hash(v2)
assert hash(t1(v1)) != hash(t2(v2))
def test_repr_vDDDTypes():
assert "vDDDTypes" in repr(vDDDTypes(timedelta(3, 11, 1)))
vDDDLists_examples = [
vDDDLists([]),
vDDDLists([datetime(2023, 11, 1, 10, 1)]),
vDDDLists([datetime(2023, 11, 1, 10, 1), date(2023, 11, 1)]),
]
@pytest.mark.parametrize("l1", vDDDLists_examples)
@pytest.mark.parametrize("l2", vDDDLists_examples)
def test_vDDDLists(l1, l2):
"""Check the equality functions of vDDDLists."""
equal = l1 is l2
l2 = copy.deepcopy(l2)
assert equal == (l1 == l2)
assert equal != (l1 != l2)

Wyświetl plik

@ -1,11 +1,10 @@
'''tests ensuring that *the* way of doing things works'''
import datetime
from icalendar import Calendar, Event
import pytest
def test_creating_calendar_with_unicode_fields(calendars, utc):
''' create a calendar with events that contain unicode characters in their fields '''
cal = Calendar()
@ -38,4 +37,3 @@ def test_creating_calendar_with_unicode_fields(calendars, utc):
cal.add_component(event2)
assert cal.to_ical() == calendars.created_calendar_with_unicode_fields.raw_ics

Wyświetl plik

@ -15,7 +15,7 @@ class TestTime(unittest.TestCase):
def test_create_from_ical(self):
directory = os.path.dirname(__file__)
ics = open(os.path.join(directory, 'time.ics'), 'rb')
ics = open(os.path.join(directory, 'calendars', 'time.ics'), 'rb')
cal = icalendar.Calendar.from_ical(ics.read())
ics.close()

Wyświetl plik

@ -10,11 +10,13 @@ try:
except:
from backports import zoneinfo
HERE = os.path.dirname(__file__)
CALENDARS_DIRECTORY = os.path.join(HERE, 'calendars')
class TestTimezoned(unittest.TestCase):
def test_create_from_ical_zoneinfo(self):
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp:
with open(os.path.join(CALENDARS_DIRECTORY, 'timezoned.ics'), 'rb') as fp:
data = fp.read()
cal = icalendar.Calendar.from_ical(data)
@ -46,8 +48,7 @@ class TestTimezoned(unittest.TestCase):
)
def test_create_from_ical_pytz(self):
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp:
with open(os.path.join(CALENDARS_DIRECTORY, 'timezoned.ics'), 'rb') as fp:
data = fp.read()
cal = icalendar.Calendar.from_ical(data)
@ -269,9 +270,7 @@ class TestTimezoneCreation(unittest.TestCase):
def test_create_america_new_york(self):
"""testing America/New_York, the most complex example from the
RFC"""
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'america_new_york.ics'), 'rb') as fp:
with open(os.path.join(CALENDARS_DIRECTORY, 'america_new_york.ics'), 'rb') as fp:
data = fp.read()
cal = icalendar.Calendar.from_ical(data)
@ -307,8 +306,7 @@ class TestTimezoneCreation(unittest.TestCase):
one RDATE property per subcomponent"""
self.maxDiff = None
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'pacific_fiji.ics'), 'rb') as fp:
with open(os.path.join(CALENDARS_DIRECTORY, 'pacific_fiji.ics'), 'rb') as fp:
data = fp.read()
cal = icalendar.Calendar.from_ical(data)
@ -442,8 +440,7 @@ class TestTimezoneCreation(unittest.TestCase):
def test_same_start_date(self):
"""testing if we can handle VTIMEZONEs whose different components
have the same start DTIMEs."""
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'timezone_same_start.ics'), 'rb') as fp:
with open(os.path.join(CALENDARS_DIRECTORY, 'timezone_same_start.ics'), 'rb') as fp:
data = fp.read()
cal = icalendar.Calendar.from_ical(data)
d = cal.subcomponents[1]['DTSTART'].dt
@ -452,8 +449,7 @@ class TestTimezoneCreation(unittest.TestCase):
def test_same_start_date_and_offset(self):
"""testing if we can handle VTIMEZONEs whose different components
have the same DTSTARTs, TZOFFSETFROM, and TZOFFSETTO."""
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'timezone_same_start_and_offset.ics'), 'rb') as fp:
with open(os.path.join(CALENDARS_DIRECTORY, 'timezone_same_start_and_offset.ics'), 'rb') as fp:
data = fp.read()
cal = icalendar.Calendar.from_ical(data)
d = cal.subcomponents[1]['DTSTART'].dt
@ -462,8 +458,7 @@ class TestTimezoneCreation(unittest.TestCase):
def test_rdate(self):
"""testing if we can handle VTIMEZONEs who only have an RDATE, not RRULE
"""
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'timezone_rdate.ics'), 'rb') as fp:
with open(os.path.join(CALENDARS_DIRECTORY, 'timezone_rdate.ics'), 'rb') as fp:
data = fp.read()
cal = icalendar.Calendar.from_ical(data)
vevent = cal.walk('VEVENT')[0]