Skip to content

Commit

Permalink
Merge pull request #575 from niccokunzmann/comparing
Browse files Browse the repository at this point in the history
Compare test calendars to find components that do not equal
  • Loading branch information
jacadzaca committed Nov 2, 2023
2 parents da887c1 + 7f4b213 commit 9ce1fa8
Show file tree
Hide file tree
Showing 17 changed files with 226 additions and 58 deletions.
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
69 changes: 42 additions & 27 deletions src/icalendar/prop.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ def from_ical(ical):
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.
Expand Down Expand Up @@ -269,6 +273,7 @@ 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 @@ -279,8 +284,24 @@ def from_ical(ical):
out = unescape_char(ical).split(',')
return out

def __eq__(self, other):
"""self == other"""
return isinstance(other, vCategory) and self.cats == other.cats

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:
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.
Expand All @@ -290,7 +311,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 +341,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 +362,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(TimeBase):
"""Render and generates iCalendar date format.
"""

Expand All @@ -377,7 +393,7 @@ def from_ical(ical):
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
Expand Down Expand Up @@ -438,7 +454,7 @@ def from_ical(ical, timezone=None):
raise ValueError(f'Wrong datetime format: {ical}')


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

return value

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

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

Expand All @@ -520,7 +540,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 +552,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 +583,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 +753,7 @@ def from_ical(cls, ical):
return cls(ical_unesc)


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

Expand Down Expand Up @@ -814,6 +827,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
File renamed without changes.
File renamed without changes.
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.
File renamed without changes.
File renamed without changes.
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=[
(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

0 comments on commit 9ce1fa8

Please sign in to comment.