Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compare test calendars to find components that do not equal #575

Merged
merged 12 commits into from
Nov 2, 2023
8 changes: 5 additions & 3 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ 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.
Ref: #568
- 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:
Expand Down Expand Up @@ -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:

Expand All @@ -104,7 +106,7 @@ Bug fixes:
Minor changes:

- Improved documentation
Ref: #503, #504
Ref: #503, #504

Bug fixes:

Expand Down
3 changes: 1 addition & 2 deletions src/icalendar/cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ def __repr__(self):
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)
Expand All @@ -465,7 +465,6 @@ def __eq__(self, other):

return True


#######################################
# components defined in RFC 5545

Expand Down
3 changes: 3 additions & 0 deletions src/icalendar/caselessdict.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ def __repr__(self):
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
Expand Down
78 changes: 49 additions & 29 deletions src/icalendar/prop.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,20 @@ def _isdst(self, dt):
return tt.tm_isdst > 0


class vBinary:
class EqualityMixin:
"""A class to share equality functions of properties."""

def __eq__(self, other):
"""Check the equality between this property and the other.

You can override this to make comparing faster.
"""
return isinstance(other, EqualityMixin) and \
self.params == other.params and \
self.to_ical() == other.to_ical()


class vBinary(EqualityMixin):
"""Binary property values are base 64 encoded.
"""

Expand Down Expand Up @@ -263,12 +276,13 @@ def __eq__(self, other):
return self.dts == other.dts


class vCategory:
class vCategory(EqualityMixin):

def __init__(self, c_list):
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])
Expand All @@ -280,7 +294,19 @@ def from_ical(ical):
return out


class vDDDTypes:
class DtEqualityMixin:
jacadzaca marked this conversation as resolved.
Show resolved Hide resolved
"""Make classes with a datetime/date comparable."""

def __eq__(self, other):
if isinstance(other, DtEqualityMixin):
return self.params == other.params and self.dt == other.dt
return False

def __hash__(self):
return hash(self.dt)


class vDDDTypes(DtEqualityMixin):
"""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.
Expand All @@ -290,7 +316,7 @@ def __init__(self, dt):
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'})
Expand Down Expand Up @@ -320,14 +346,6 @@ def to_ical(self):
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):
Expand All @@ -349,8 +367,11 @@ def from_ical(cls, ical, timezone=None):
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(DtEqualityMixin):
"""Render and generates iCalendar date format.
"""

Expand All @@ -377,7 +398,7 @@ def from_ical(ical):
raise ValueError(f'Wrong date format {ical}')


class vDatetime:
class vDatetime(DtEqualityMixin):
"""Render and generates icalendar datetime format.

vDatetime is timezone aware and uses the pytz library, an implementation of
Expand Down Expand Up @@ -438,7 +459,7 @@ def from_ical(ical, timezone=None):
raise ValueError(f'Wrong datetime format: {ical}')


class vDuration:
class vDuration(DtEqualityMixin):
"""Subclass of timedelta that renders itself in the iCalendar DURATION
format.
"""
Expand Down Expand Up @@ -495,8 +516,12 @@ def from_ical(ical):

return value

@property
def dt(self):
"""The time delta for compatibility."""
return self.td

class vPeriod:
class vPeriod(DtEqualityMixin):
"""A precise period of time.
"""

Expand All @@ -520,7 +545,7 @@ def __init__(self, per):
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)
Expand All @@ -532,17 +557,6 @@ def __init__(self, per):
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)
Expand Down Expand Up @@ -574,6 +588,10 @@ def __repr__(self):
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.
Expand Down Expand Up @@ -740,7 +758,7 @@ def from_ical(cls, ical):
return cls(ical_unesc)


class vTime:
class vTime(DtEqualityMixin):
"""Render and generates iCalendar time format.
"""

Expand Down Expand Up @@ -814,6 +832,8 @@ def from_ical(ical):
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
File renamed without changes.
37 changes: 31 additions & 6 deletions src/icalendar/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ def __init__(self, data_source_folder, parser):
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)
Expand All @@ -40,20 +46,23 @@ def multiple(self):

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,
Expand All @@ -71,3 +80,19 @@ def utc(request):
])
def in_timezone(request):
return request.param


@pytest.fixture(params=[
angatha marked this conversation as resolved.
Show resolved Hide resolved
(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]
Loading