From 24c4426e3ef915a388545357ba924738b15950ce Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Nov 2023 18:37:55 -0800 Subject: [PATCH] Add exception catching for pyparsing errors --- ical/calendar_stream.py | 18 +++++++++++++----- tests/test_calendar.py | 7 +++++++ tests/test_calendar_stream.py | 8 ++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/ical/calendar_stream.py b/ical/calendar_stream.py index 2327b39..4892ad5 100644 --- a/ical/calendar_stream.py +++ b/ical/calendar_stream.py @@ -29,16 +29,18 @@ from __future__ import annotations import logging +import pyparsing try: - from pydantic.v1 import Field + from pydantic.v1 import Field, ValidationError except ImportError: - from pydantic import Field + from pydantic import Field, ValidationError from .calendar import Calendar from .component import ComponentModel from .parsing.component import encode_content, parse_content from .types.data_types import DATA_TYPE +from .exceptions import CalendarParseError _LOGGER = logging.getLogger(__name__) @@ -55,13 +57,19 @@ class CalendarStream(ComponentModel): @classmethod def from_ics(cls, content: str) -> "CalendarStream": """Factory method to create a new instance from an rfc5545 iCalendar content.""" - components = parse_content(content) + try: + components = parse_content(content) + except pyparsing.ParseException as err: + raise CalendarParseError(f"Failed to parse calendar stream: {err}") from err result: dict[str, list] = {"vcalendar": []} for component in components: result.setdefault(component.name, []) result[component.name].append(component.as_dict()) _LOGGER.debug("Parsing object %s", result) - return cls.parse_obj(result) + try: + return cls.parse_obj(result) + except ValidationError as err: + raise CalendarParseError(f"Failed to parse calendar stream: {err}") from err def ics(self) -> str: """Encode the calendar stream as an rfc5545 iCalendar Stream content.""" @@ -79,7 +87,7 @@ def calendar_from_ics(cls, content: str) -> Calendar: return stream.calendars[0] if len(stream.calendars) == 0: return Calendar() - raise ValueError("Calendar Stream had more than one calendar") + raise CalendarParseError("Calendar Stream had more than one calendar") @classmethod def calendar_to_ics(cls, calendar: Calendar) -> str: diff --git a/tests/test_calendar.py b/tests/test_calendar.py index ca58c49..9f9a4c1 100644 --- a/tests/test_calendar.py +++ b/tests/test_calendar.py @@ -13,6 +13,7 @@ from ical.calendar import Calendar from ical.calendar_stream import IcsCalendarStream +from ical.exceptions import CalendarParseError from ical.event import Event from ical.types.recur import Recur @@ -458,3 +459,9 @@ def test_floating_time_with_timezone_propagation() -> None: it = iter(cal.timeline_tz(zoneinfo.ZoneInfo("Europe/Brussels"))) for i in range(0, 30): next(it) + + +def test_invalid_ics() -> None: + """Test a parse failure for ics content.""" + with pytest.raises(CalendarParseError, match="Failed to parse calendar stream"): + IcsCalendarStream.calendar_from_ics("invalid") diff --git a/tests/test_calendar_stream.py b/tests/test_calendar_stream.py index e23ff3f..cd3887e 100644 --- a/tests/test_calendar_stream.py +++ b/tests/test_calendar_stream.py @@ -6,6 +6,7 @@ import pytest from pytest_golden.plugin import GoldenTestFixture +from ical.exceptions import CalendarParseError from ical.calendar_stream import CalendarStream, IcsCalendarStream @@ -58,3 +59,10 @@ def test_serialize(golden: GoldenTestFixture) -> None: """Fixture to read golden file and compare to golden output.""" cal = IcsCalendarStream.from_ics(golden["input"]) assert cal.ics() == golden.get("encoded", golden["input"]) + + + +def test_invalid_ics(mock_prodid: Generator[None, None, None]) -> None: + """Test parsing failures for ics content.""" + with pytest.raises(CalendarParseError, match="Failed to parse calendar stream"): + IcsCalendarStream.calendar_from_ics("invalid") \ No newline at end of file