From 9e587f19825e2d6101a83e8a2ed36559645ebd5f Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 9 Sep 2024 13:21:43 +0100 Subject: [PATCH 1/9] test how to extend collection --- recurring_ical_events.py | 14 ++++++++++++++ test/test_extend_classes.py | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 test/test_extend_classes.py diff --git a/recurring_ical_events.py b/recurring_ical_events.py index e459189..d205b34 100644 --- a/recurring_ical_events.py +++ b/recurring_ical_events.py @@ -909,6 +909,19 @@ def __eq__(self, other: Occurrence) -> bool: return self.id == other.id +class CollectComponents(ABC): + """Class to collect components from a calendar.""" + + @abstractmethod + def collect_components( + self, source: Component, suppress_errors: tuple[Exception] + ) -> Sequence[Series]: + """Collect all components from the source component. + + suppress_errors - a list of errors that should be suppressed. + A Series of events with such an error is removed from all results. + """ + class CalendarQuery: """A calendar that can unfold its events at a certain time. @@ -1142,4 +1155,5 @@ def of( "Time", "RecurrenceID", "RecurrenceIDs", + "CollectComponents", ] diff --git a/test/test_extend_classes.py b/test/test_extend_classes.py new file mode 100644 index 0000000..a1c3c0b --- /dev/null +++ b/test/test_extend_classes.py @@ -0,0 +1,38 @@ +"""This tests extneding and modifying the behaviour of recurring ical events.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Sequence + +from recurring_ical_events import ( + CollectComponents, + EventAdapter, + JournalAdapter, + Series, + TodoAdapter, + of, +) + +if TYPE_CHECKING: + from icalendar.cal import Component + + +class CollectOneUIDEvent(CollectComponents): + """Collect only one UID.""" + + def __init__(self, uid:str) -> None: + self.uid = uid + + def collect_components(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + return [ + series + for adapter in [EventAdapter, JournalAdapter, TodoAdapter] + for series in adapter.collect_components(source, suppress_errors) + if series.uid == self.uid + ] + + +def test_collect_only_one_uid(calendars): + """Test that only one UID is used.""" + one_uid = CollectOneUIDEvent("4mm2ak3in2j3pllqdk1ubtbp9p@google.com") + query = of(calendars.raw.machbar_16_feb_2019, components=[one_uid]) + assert query.count() == 1 From 5a43904b9522ff54ac1ac9fecff98db34d0392df Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 9 Sep 2024 13:40:50 +0100 Subject: [PATCH 2/9] create an example of how to extend functionality --- README.rst | 72 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 9f7e124..e8285b5 100644 --- a/README.rst +++ b/README.rst @@ -345,6 +345,61 @@ Passing ``skip_bad_series=True`` as ``of()`` argument will totally skip theses e >>> recurring_ical_events.of(calendar_with_bad_event, skip_bad_series=True).count() 0 +Architecture +------------ + +.. image:: img/architecture.png + :alt: Architecture Diagram showing the components interacting + +Each icalendar **Calendar** can contain Events, Journal entries, +TODOs and others, called **Components**. +Those entries are grouped by their ``UID``. +Such a ``UID`` defines a **Series** of **Occurrences** that take place at +a given time. +Since each **Component** is different, the **ComponentAdapter** offers a unified +interface to interact with them. +The **Calendar** gets filtered and for each ``UID``, +a **Series** can use one or more **ComponentAdapters** to create +**Occurrences** of what happens in a time span. +These **Occurrences** are used internally and convert to **Components** for further use. + +Extending ``recurring-ical-events`` +*********************************** + +All the functionality of ``recurring-ical-events`` can be extended and modified. +To understand where to extend, have a look at the `Architecture`_. + +The first place for extending is the collection of components. +Components are collected into a ``Series`` that belongs together because it has the same ``UID``. +In this example, we collect any Journal, Event or TODO which matches a certain UID: + +.. code:: Python + + >>> from recurring_ical_events import CollectComponents, EventAdapter, JournalAdapter, TodoAdapter, Series + >>> from icalendar.cal import Component + >>> from typing import Sequence + >>> class CollectOneUIDEvent(CollectComponents): + ... """Collect only one UID.""" + ... def __init__(self, uid:str) -> None: + ... self.uid = uid + ... def collect_components(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + ... return [ + ... series + ... for adapter in [EventAdapter, JournalAdapter, TodoAdapter] + ... for series in adapter.collect_components(source, suppress_errors) + ... if series.uid == self.uid + ... ] + + # create the calendar + >>> calendar_file = CALENDARS / "machbar_16_feb_2019.ics" + >>> machbar_calendar = icalendar.Calendar.from_ical(calendar_file.read_bytes()) + + # collect only one UID: 4mm2ak3in2j3pllqdk1ubtbp9p@google.com + >>> one_uid = CollectOneUIDEvent("4mm2ak3in2j3pllqdk1ubtbp9p@google.com") + >>> query = recurring_ical_events.of(machbar_calendar, components=[one_uid]) + >>> query.count() # the event has no recurrence and thus there is only one + 1 + Version Fixing ************** @@ -429,23 +484,6 @@ To release new versions, 6. notify the issues about their release -Architecture ------------- - -.. image:: img/architecture.png - :alt: Architecture Diagram showing the components interacting - -Each icalendar **Calendar** can contain Events, Journal entries, -TODOs and others, called **Components**. -Those entries are grouped by their ``UID``. -Such a ``UID`` defines a **Series** of **Occurrences** that take place at -a given time. -Since each **Component** is different, the **ComponentAdapter** offers a unified -interface to interact with them. -The **Calendar** gets filtered and for each ``UID``, -a **Series** can use one or more **ComponentAdapters** to create -**Occurrences** of what happens in a time span. -These **Occurrences** are used internally and convert to **Components** for further use. Changelog --------- From e92f7574c81e5083f9d134424b94b7ff14dd554f Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 9 Sep 2024 13:48:40 +0100 Subject: [PATCH 3/9] document in sentences --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index e8285b5..878f80d 100644 --- a/README.rst +++ b/README.rst @@ -332,16 +332,17 @@ Passing ``skip_bad_series=True`` as ``of()`` argument will totally skip theses e .. code:: Python + # Create a calendar that contains broken events. >>> calendar_file = CALENDARS / "bad_rrule_missing_until_event.ics" >>> calendar_with_bad_event = icalendar.Calendar.from_ical(calendar_file.read_bytes()) - # default: error + # By default, broken events result in errors. >>> recurring_ical_events.of(calendar_with_bad_event, skip_bad_series=False).count() Traceback (most recent call last): ... recurring_ical_events.BadRuleStringFormat: UNTIL parameter is missing: FREQ=WEEKLY;BYDAY=TH;WKST=SU;UNTL=20191023 - # skip the bad events + # With skip_bad_series=True we skip the series that we cannot handle. >>> recurring_ical_events.of(calendar_with_bad_event, skip_bad_series=True).count() 0 From 019b7eaf5bae978ad512b6678bc42ec8ed6a9159 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 9 Sep 2024 15:07:04 +0100 Subject: [PATCH 4/9] add a way to override classes during collection --- recurring_ical_events.py | 147 +++++++++++++++++++++++++++--------- test/test_extend_classes.py | 22 +++++- 2 files changed, 131 insertions(+), 38 deletions(-) diff --git a/recurring_ical_events.py b/recurring_ical_events.py index d205b34..fb9ae0c 100644 --- a/recurring_ical_events.py +++ b/recurring_ical_events.py @@ -21,7 +21,9 @@ from collections import defaultdict from functools import wraps from typing import TYPE_CHECKING, Callable, Generator, Optional, Sequence, Union +import sys +from icalendar.cal import Component import x_wr_timezone from dateutil.rrule import rruleset, rrulestr from icalendar.prop import vDDDTypes @@ -304,6 +306,11 @@ def get_any(dictionary: dict, keys: Sequence[object], default: object = None): class Series: """Base class for components that result in a series of occurrences.""" + + @property + def occurrence(self) -> type[Occurrence]: + """A way to override the occurrence class.""" + return Occurrence def __init__(self, components: Sequence[ComponentAdapter]): """Create an component which may have repetitions in it.""" @@ -530,7 +537,7 @@ def between(self, span_start: Time, span_stop: Time) -> Generator[Occurrence]: recurrence_ids, normalize_pytz(start + self.core.duration), ) - occurrence = Occurrence( + occurrence = self.occurrence( self.core, self.convert_to_original_type(start), self.convert_to_original_type(stop), @@ -541,7 +548,7 @@ def between(self, span_start: Time, span_stop: Time) -> Generator[Occurrence]: if adapter in returned_modifications: continue returned_modifications.add(adapter) - occurrence = Occurrence(adapter) + occurrence = self.occurrence(adapter) if occurrence.is_in_span(span_start, span_stop): yield occurrence for modification in self.modifications: @@ -553,7 +560,7 @@ def between(self, span_start: Time, span_stop: Time) -> Generator[Occurrence]: continue if modification.is_in_span(span_start, span_stop): returned_modifications.add(modification) - yield Occurrence(modification) + yield self.occurrence(modification) def convert_to_original_type(self, date): """Convert a date back if this is possible. @@ -622,23 +629,12 @@ def uid(self) -> UID: return self._component.get("UID", str(id(self._component))) @classmethod - def collect_components( - cls, source: Component, suppress_errors: tuple[Exception] - ) -> Sequence[Series]: - """Collect all components from the source component. + def collect_components(cls, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + """Collect all components for this adapter. - suppress_errors - a list of errors that should be suppressed. - A Series of events with such an error is removed from all results. + This is a shortcut. """ - components: dict[str, list[Component]] = defaultdict(list) # UID -> components - for component in source.walk(cls.component_name()): - adapter = cls(component) - components[adapter.uid].append(adapter) - result = [] - for components in components.values(): - with contextlib.suppress(suppress_errors): - result.append(Series(components)) - return result + return CollectComponentsByName(cls.component_name(), cls).collect_components(source, suppress_errors) def as_component(self, start: Time, stop: Time, keep_recurrence_attributes: bool): # noqa: FBT001 """Create a shallow copy of the source event and modify some attributes.""" @@ -910,7 +906,7 @@ def __eq__(self, other: Occurrence) -> bool: class CollectComponents(ABC): - """Class to collect components from a calendar.""" + """Abstract class to collect components from a calendar.""" @abstractmethod def collect_components( @@ -922,6 +918,96 @@ def collect_components( A Series of events with such an error is removed from all results. """ +class CollectComponentsByName(CollectComponents): + """This is a component collecttion strategy. + + Components can be collected in different ways. + This class allows extension of the functionality by + - subclassing to filter the resulting components + - composition to combine collection behavior + """ + + component_adapters = [EventAdapter, TodoAdapter, JournalAdapter] + + @cached_property + def _component_adapters(self) -> dict[str : type[ComponentAdapter]]: + """A mapping of component adapters.""" + return { + adapter.component_name(): adapter for adapter in self.component_adapters + } + + def __init__(self, name: str, adapter : type[ComponentAdapter]|None=None, series: type[Series] = Series, occurrence: type[Occurrence] = Occurrence) -> None: + """Create a new way of collecting components.""" + if adapter is None: + if name not in self._component_adapters: + raise ValueError( + f'"{name}" is an unknown name for a ' + 'recurring component. ' + f"I only know these: { ', '.join(self._component_adapters)}." + ) + adapter = self._component_adapters[name] + if occurrence is not Occurrence: + _occurrence = occurrence + class series(series): # noqa: N801 + occurrence = _occurrence + self._name = name + self._series = series + self._adapter = adapter + + def collect_components(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + """Collect all components from the source component. + + suppress_errors - a list of errors that should be suppressed. + A Series of events with such an error is removed from all results. + """ + components: dict[str, list[Component]] = defaultdict(list) # UID -> components + for component in source.walk(self._name): + adapter = self._adapter(component) + components[adapter.uid].append(adapter) + result = [] + for components in components.values(): + with contextlib.suppress(suppress_errors): + result.append(self._series(components)) + return result + + +class CollectKnownComponents(CollectComponents): + """Collect all known components.""" + + @property + def component_adapters(self) -> Sequence[ComponentAdapter]: + """Return all known component adapters.""" + return CollectComponentsByName.component_adapters + + @property + def names(self) -> list[str]: + """Return the names of the components to collect.""" + return [adapter.component_name() for adapter in self.component_adapters] + + def __init__(self, + series: type[Series] = Series, + occurrence: type[Occurrence] = Occurrence, + collector:type[CollectComponentsByName] = CollectComponentsByName, + ) -> None: + """Collect all known components and overide the series and occurrence.""" + self._series = series + self._occurrence = occurrence + self._collector = collector + + def collect_components(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + """Collect the components.""" + result = [] + for name in self.names: + collector = self._collector(name, series = self._series, occurrence = self._occurrence) + result.extend(collector.collect_components(source, suppress_errors)) + return result + +if sys.version_info >= (3, 10): + T_COMPONENTS = Sequence[str | type[ComponentAdapter] | CollectComponents] +else: + # see https://github.com/python/cpython/issues/86399#issuecomment-1093889925 + T_COMPONENTS = Sequence[str] + class CalendarQuery: """A calendar that can unfold its events at a certain time. @@ -936,22 +1022,13 @@ class CalendarQuery: component_adapters - a list of component adapters """ - component_adapters = [EventAdapter, TodoAdapter, JournalAdapter] - - @cached_property - def _component_adapters(self) -> dict[str : type[ComponentAdapter]]: - """A mapping of component adapters.""" - return { - adapter.component_name(): adapter for adapter in self.component_adapters - } - suppressed_errors = [BadRuleStringFormat, PeriodEndBeforeStart] def __init__( self, calendar: Component, keep_recurrence_attributes: bool = False, # noqa: FBT001 - components: Sequence[str | type[ComponentAdapter]] = ("VEVENT",), + components: T_COMPONENTS = ("VEVENT",), skip_bad_series: bool = False, # noqa: FBT001 ): """Create an unfoldable calendar from a given calendar. @@ -972,13 +1049,7 @@ def __init__( self._skip_errors = tuple(self.suppressed_errors) if skip_bad_series else () for component_adapter_id in components: if isinstance(component_adapter_id, str): - if component_adapter_id not in self._component_adapters: - raise ValueError( - f'"{component_adapter_id}" is an unknown name for a ' - 'recurring component. ' - f"I only know these: { ', '.join(self._component_adapters)}." - ) - component_adapter = self._component_adapters[component_adapter_id] + component_adapter = CollectComponentsByName(component_adapter_id) else: component_adapter = component_adapter_id self.series.extend( @@ -1122,7 +1193,7 @@ def count(self) -> int: def of( a_calendar: Component, keep_recurrence_attributes=False, - components: Sequence[str | type[ComponentAdapter]] = ("VEVENT",), + components: T_COMPONENTS = ("VEVENT",), skip_bad_series: bool = False, # noqa: FBT001 ) -> CalendarQuery: """Unfold recurring events of a_calendar @@ -1156,4 +1227,6 @@ def of( "RecurrenceID", "RecurrenceIDs", "CollectComponents", + "CollectComponentsByName", + "T_COMPONENTS", ] diff --git a/test/test_extend_classes.py b/test/test_extend_classes.py index a1c3c0b..64413f0 100644 --- a/test/test_extend_classes.py +++ b/test/test_extend_classes.py @@ -1,12 +1,16 @@ """This tests extneding and modifying the behaviour of recurring ical events.""" from __future__ import annotations -from typing import TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING, Generator, Sequence + +from icalendar.cal import Component from recurring_ical_events import ( CollectComponents, + CollectKnownComponents, EventAdapter, JournalAdapter, + Occurrence, Series, TodoAdapter, of, @@ -36,3 +40,19 @@ def test_collect_only_one_uid(calendars): one_uid = CollectOneUIDEvent("4mm2ak3in2j3pllqdk1ubtbp9p@google.com") query = of(calendars.raw.machbar_16_feb_2019, components=[one_uid]) assert query.count() == 1 + + +class MyOccurrence(Occurrence): + """An occurrence that modifies the component.""" + + def as_component(self, keep_recurrence_attributes: bool) -> Component: # noqa: FBT001 + """Return a shallow copy of the source component and modify some attributes.""" + component = super().as_component(keep_recurrence_attributes) + component["X-MY-ATTRIBUTE"] = "my occurrence" + return component + +def test_added_attributes(calendars): + """Test that attributes are added.""" + query = of(calendars.raw.one_event, components=[CollectKnownComponents(occurrence=MyOccurrence)]) + event = next(query.all()) + assert event["X-MY-ATTRIBUTE"] == "my occurrence" From 61056eb895c7f2b9a8aa6ac8b3a3be52d60423c9 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 9 Sep 2024 18:44:44 +0100 Subject: [PATCH 5/9] Add and document extensibility --- README.rst | 100 +++++++++++++++++++++++++++++------- recurring_ical_events.py | 67 ++++++++++++++---------- test/test_extend_classes.py | 49 ++++++++++++++---- 3 files changed, 162 insertions(+), 54 deletions(-) diff --git a/README.rst b/README.rst index 878f80d..dbf3872 100644 --- a/README.rst +++ b/README.rst @@ -305,9 +305,7 @@ Here is a template code for choosing the supported types of components: If a type of component is not listed here, it can be added. Please create an issue for this in the source code repository. -You can create subclasses and customizations. - - +For further customization, please refer to the section on how to extend the default functionality. Speed ***** @@ -371,35 +369,99 @@ All the functionality of ``recurring-ical-events`` can be extended and modified. To understand where to extend, have a look at the `Architecture`_. The first place for extending is the collection of components. -Components are collected into a ``Series`` that belongs together because it has the same ``UID``. -In this example, we collect any Journal, Event or TODO which matches a certain UID: +Components are collected into a ``Series``. +A series belongs together because all components have the same ``UID``. +In this example, we collect one VEVENT which matches a certain UID: .. code:: Python - >>> from recurring_ical_events import CollectComponents, EventAdapter, JournalAdapter, TodoAdapter, Series + >>> from recurring_ical_events import SelectComponents, EventAdapter, Series >>> from icalendar.cal import Component >>> from typing import Sequence - >>> class CollectOneUIDEvent(CollectComponents): - ... """Collect only one UID.""" - ... def __init__(self, uid:str) -> None: - ... self.uid = uid - ... def collect_components(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: - ... return [ - ... series - ... for adapter in [EventAdapter, JournalAdapter, TodoAdapter] - ... for series in adapter.collect_components(source, suppress_errors) - ... if series.uid == self.uid - ... ] # create the calendar >>> calendar_file = CALENDARS / "machbar_16_feb_2019.ics" >>> machbar_calendar = icalendar.Calendar.from_ical(calendar_file.read_bytes()) + # Create a collector of components that searches for an event with a specific UID + >>> class CollectOneUIDEvent(SelectComponents): + ... def __init__(self, uid:str) -> None: + ... self.uid = uid + ... def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + ... components : list[Component] = [] + ... for component in source.walk("VEVENT"): + ... if component.get("UID") == self.uid: + ... components.append(EventAdapter(component)) + ... return [Series(components)] if components else [] + # collect only one UID: 4mm2ak3in2j3pllqdk1ubtbp9p@google.com >>> one_uid = CollectOneUIDEvent("4mm2ak3in2j3pllqdk1ubtbp9p@google.com") - >>> query = recurring_ical_events.of(machbar_calendar, components=[one_uid]) - >>> query.count() # the event has no recurrence and thus there is only one + >>> uid_query = recurring_ical_events.of(machbar_calendar, components=[one_uid]) + >>> uid_query.count() # the event has no recurrence and thus there is only one + 1 + +Several ways of extending the functionality have been created to override internals. +These can be subclassed or composed. + +Below, you can choose to collect all components. Subclasses can be created for the +``Series`` and the ``Occurrence``. + +.. code:: Python + + >>> from recurring_ical_events import AllKnownComponents, Series, Occurrence + + # we create a calendar with one event + >>> calendar_file = CALENDARS / "one_event.ics" + >>> one_event = icalendar.Calendar.from_ical(calendar_file.read_bytes()) + + # You can override the Occurrence and Series classes for all computable components + >>> select_all_known = AllKnownComponents(series=Series, occurrence=Occurrence) + >>> query_all_known = recurring_ical_events.of(one_event, components=[select_all_known]) + + # There should be exactly one event. + >>> query_all_known.count() + 1 + +This example shows that the behavior for specific types of components can be extended. +Additional to the series, you can change the ``ComponentAdapter`` that provides +a unified interface for all the components with the same name (``VEVENT`` for example). + +.. code:: Python + + >>> from recurring_ical_events import ComponentsWithName, EventAdapter, JournalAdapter, TodoAdapter + + # You can also choose to select only specific subcomponents by their name. + # The default arguments are added to show the extensibility. + >>> select_events = ComponentsWithName("VEVENT", adapter=EventAdapter, series=Series, occurrence=Occurrence) + >>> select_todos = ComponentsWithName("VTODO", adapter=TodoAdapter, series=Series, occurrence=Occurrence) + >>> select_journals = ComponentsWithName("VJOURNAL", adapter=JournalAdapter, series=Series, occurrence=Occurrence) + + # There should be one event happening and nothing else + >>> recurring_ical_events.of(one_event, components=[select_events]).count() 1 + >>> recurring_ical_events.of(one_event, components=[select_todos]).count() + 0 + >>> recurring_ical_events.of(one_event, components=[select_journals]).count() + 0 + +So, if you for example would like to modify all events that are returned by the query, +you can do that subclassing the ``Occurrence`` class. + + +.. code:: Python + + # This occurence changes adds a new attribute to the resulting events + >>> class MyOccurrence(Occurrence): + ... """An occurrence that modifies the component.""" + ... def as_component(self, keep_recurrence_attributes: bool) -> Component: + ... """Return a shallow copy of the source component and modify some attributes.""" + ... component = super().as_component(keep_recurrence_attributes) + ... component["X-MY-ATTRIBUTE"] = "my occurrence" + ... return component + >>> query = recurring_ical_events.of(one_event, components=[ComponentsWithName("VEVENT", occurrence=MyOccurrence)]) + >>> event = next(query.all()) + >>> event["X-MY-ATTRIBUTE"] + 'my occurrence' Version Fixing diff --git a/recurring_ical_events.py b/recurring_ical_events.py index fb9ae0c..2141def 100644 --- a/recurring_ical_events.py +++ b/recurring_ical_events.py @@ -503,7 +503,10 @@ def rrule_between(self, span_start: Time, span_stop: Time) -> Generator[Time]: yield start def between(self, span_start: Time, span_stop: Time) -> Generator[Occurrence]: - """components between the start (inclusive) and end (exclusive)""" + """Components between the start (inclusive) and end (exclusive). + + The result does not need to be ordered. + """ # make dates comparable, rrule converts them to datetimes span_start_dt = convert_to_datetime(span_start, self.tzinfo) span_stop_dt = convert_to_datetime(span_stop, self.tzinfo) @@ -629,12 +632,12 @@ def uid(self) -> UID: return self._component.get("UID", str(id(self._component))) @classmethod - def collect_components(cls, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + def collect_series_from(cls, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: """Collect all components for this adapter. This is a shortcut. """ - return CollectComponentsByName(cls.component_name(), cls).collect_components(source, suppress_errors) + return ComponentsWithName(cls.component_name(), cls).collect_series_from(source, suppress_errors) def as_component(self, start: Time, stop: Time, keep_recurrence_attributes: bool): # noqa: FBT001 """Create a shallow copy of the source event and modify some attributes.""" @@ -905,26 +908,26 @@ def __eq__(self, other: Occurrence) -> bool: return self.id == other.id -class CollectComponents(ABC): - """Abstract class to collect components from a calendar.""" +class SelectComponents(ABC): + """Abstract class to select components from a calendar.""" @abstractmethod - def collect_components( + def collect_series_from( self, source: Component, suppress_errors: tuple[Exception] ) -> Sequence[Series]: - """Collect all components from the source component. + """Collect all components from the source grouped together into a series. suppress_errors - a list of errors that should be suppressed. A Series of events with such an error is removed from all results. """ -class CollectComponentsByName(CollectComponents): +class ComponentsWithName(SelectComponents): """This is a component collecttion strategy. Components can be collected in different ways. This class allows extension of the functionality by - subclassing to filter the resulting components - - composition to combine collection behavior + - composition to combine collection behavior (see AllKnownComponents) """ component_adapters = [EventAdapter, TodoAdapter, JournalAdapter] @@ -937,7 +940,13 @@ def _component_adapters(self) -> dict[str : type[ComponentAdapter]]: } def __init__(self, name: str, adapter : type[ComponentAdapter]|None=None, series: type[Series] = Series, occurrence: type[Occurrence] = Occurrence) -> None: - """Create a new way of collecting components.""" + """Create a new way of collecting components. + + name - the name of the component to collect ("VEVENT", "VTODO", "VJOURNAL") + adapter - the adapter to use for these components with that name + series - the series class that hold a series of components + occurrence - the occurrence class that creates the resulting components + """ if adapter is None: if name not in self._component_adapters: raise ValueError( @@ -954,7 +963,7 @@ class series(series): # noqa: N801 self._series = series self._adapter = adapter - def collect_components(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: """Collect all components from the source component. suppress_errors - a list of errors that should be suppressed. @@ -971,39 +980,44 @@ def collect_components(self, source: Component, suppress_errors: tuple[Exception return result -class CollectKnownComponents(CollectComponents): - """Collect all known components.""" +class AllKnownComponents(SelectComponents): + """Group all known components into series.""" @property - def component_adapters(self) -> Sequence[ComponentAdapter]: + def _component_adapters(self) -> Sequence[ComponentAdapter]: """Return all known component adapters.""" - return CollectComponentsByName.component_adapters + return ComponentsWithName.component_adapters @property def names(self) -> list[str]: """Return the names of the components to collect.""" - return [adapter.component_name() for adapter in self.component_adapters] + return [adapter.component_name() for adapter in self._component_adapters] def __init__(self, series: type[Series] = Series, occurrence: type[Occurrence] = Occurrence, - collector:type[CollectComponentsByName] = CollectComponentsByName, + collector:type[ComponentsWithName] = ComponentsWithName, ) -> None: - """Collect all known components and overide the series and occurrence.""" + """Collect all known components and overide the series and occurrence. + + series - the Series class to override that is queried for Occurrences + occurrence - the occurrence class that creates the resulting components + collector - if you want to override the SelectComponentsByName class + """ self._series = series self._occurrence = occurrence self._collector = collector - def collect_components(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: - """Collect the components.""" + def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + """Collect the components from the source groups into a series.""" result = [] for name in self.names: collector = self._collector(name, series = self._series, occurrence = self._occurrence) - result.extend(collector.collect_components(source, suppress_errors)) + result.extend(collector.collect_series_from(source, suppress_errors)) return result if sys.version_info >= (3, 10): - T_COMPONENTS = Sequence[str | type[ComponentAdapter] | CollectComponents] + T_COMPONENTS = Sequence[str | type[ComponentAdapter] | SelectComponents] else: # see https://github.com/python/cpython/issues/86399#issuecomment-1093889925 T_COMPONENTS = Sequence[str] @@ -1023,6 +1037,7 @@ class CalendarQuery: """ suppressed_errors = [BadRuleStringFormat, PeriodEndBeforeStart] + ComponentsWithName = ComponentsWithName def __init__( self, @@ -1049,11 +1064,11 @@ def __init__( self._skip_errors = tuple(self.suppressed_errors) if skip_bad_series else () for component_adapter_id in components: if isinstance(component_adapter_id, str): - component_adapter = CollectComponentsByName(component_adapter_id) + component_adapter = self.ComponentsWithName(component_adapter_id) else: component_adapter = component_adapter_id self.series.extend( - component_adapter.collect_components(calendar, self._skip_errors) + component_adapter.collect_series_from(calendar, self._skip_errors) ) @staticmethod @@ -1226,7 +1241,7 @@ def of( "Time", "RecurrenceID", "RecurrenceIDs", - "CollectComponents", - "CollectComponentsByName", + "SelectComponents", + "ComponentsWithName", "T_COMPONENTS", ] diff --git a/test/test_extend_classes.py b/test/test_extend_classes.py index 64413f0..8c3c3ed 100644 --- a/test/test_extend_classes.py +++ b/test/test_extend_classes.py @@ -3,14 +3,15 @@ from typing import TYPE_CHECKING, Generator, Sequence +import pytest from icalendar.cal import Component from recurring_ical_events import ( - CollectComponents, - CollectKnownComponents, + AllKnownComponents, EventAdapter, JournalAdapter, Occurrence, + SelectComponents, Series, TodoAdapter, of, @@ -20,25 +21,55 @@ from icalendar.cal import Component -class CollectOneUIDEvent(CollectComponents): +class SelectUID1(SelectComponents): """Collect only one UID.""" def __init__(self, uid:str) -> None: self.uid = uid - def collect_components(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: return [ series for adapter in [EventAdapter, JournalAdapter, TodoAdapter] - for series in adapter.collect_components(source, suppress_errors) + for series in adapter.collect_series_from(source, suppress_errors) if series.uid == self.uid ] -def test_collect_only_one_uid(calendars): +class SelectUID2(AllKnownComponents): + + def __init__(self, uid:str) -> None: + super().__init__() + self.uid = uid + + def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + return [ + series + for series in super().collect_series_from(source, suppress_errors) + if series.uid == self.uid + ] + + +class SelectUID3(SelectComponents): + + def __init__(self, uid:str) -> None: + self.uid = uid + + def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + components : list[Component] = [] + for component in source.walk("VEVENT"): + if component.get("UID") == self.uid: + components.append(EventAdapter(component)) + return [Series(components)] if components else [] + + +@pytest.mark.parametrize( + "collector", [SelectUID1, SelectUID2, SelectUID3] +) +def test_collect_only_one_uid(calendars, collector): """Test that only one UID is used.""" - one_uid = CollectOneUIDEvent("4mm2ak3in2j3pllqdk1ubtbp9p@google.com") - query = of(calendars.raw.machbar_16_feb_2019, components=[one_uid]) + uid = "4mm2ak3in2j3pllqdk1ubtbp9p@google.com" + query = of(calendars.raw.machbar_16_feb_2019, components=[collector(uid)]) assert query.count() == 1 @@ -53,6 +84,6 @@ def as_component(self, keep_recurrence_attributes: bool) -> Component: # noqa: def test_added_attributes(calendars): """Test that attributes are added.""" - query = of(calendars.raw.one_event, components=[CollectKnownComponents(occurrence=MyOccurrence)]) + query = of(calendars.raw.one_event, components=[AllKnownComponents(occurrence=MyOccurrence)]) event = next(query.all()) assert event["X-MY-ATTRIBUTE"] == "my occurrence" From 49932009b7882121dcc143767217a63cbd05288a Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 9 Sep 2024 18:59:23 +0100 Subject: [PATCH 6/9] Allow the extension of a calendar query --- README.rst | 16 +++++++++++++++- recurring_ical_events.py | 8 ++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index dbf3872..2da85dc 100644 --- a/README.rst +++ b/README.rst @@ -416,6 +416,8 @@ Below, you can choose to collect all components. Subclasses can be created for t # You can override the Occurrence and Series classes for all computable components >>> select_all_known = AllKnownComponents(series=Series, occurrence=Occurrence) + >>> select_all_known.names # these are the supported types of components + ['VEVENT', 'VTODO', 'VJOURNAL'] >>> query_all_known = recurring_ical_events.of(one_event, components=[select_all_known]) # There should be exactly one event. @@ -444,7 +446,7 @@ a unified interface for all the components with the same name (``VEVENT`` for ex >>> recurring_ical_events.of(one_event, components=[select_journals]).count() 0 -So, if you for example would like to modify all events that are returned by the query, +So, if you would like to modify all events that are returned by the query, you can do that subclassing the ``Occurrence`` class. @@ -463,6 +465,18 @@ you can do that subclassing the ``Occurrence`` class. >>> event["X-MY-ATTRIBUTE"] 'my occurrence' +This library allows extension of functionality during the selection of components to calculate using these classes: + +* ``ComponentsWithName`` - for components of a certain name +* ``AllKnownComponents`` - for all components known +* ``SelectComponents`` - the interface to provide + +You can further customize behaviour by subclassing these: + +* ``ComponentAdapter`` such as ``EventAdapter``, ``JournalAdapter`` or ``TodoAdapter``. +* ``Series`` +* ``Occurrence`` +* ``CalendarQuery`` Version Fixing ************** diff --git a/recurring_ical_events.py b/recurring_ical_events.py index 2141def..de01684 100644 --- a/recurring_ical_events.py +++ b/recurring_ical_events.py @@ -1210,6 +1210,7 @@ def of( keep_recurrence_attributes=False, components: T_COMPONENTS = ("VEVENT",), skip_bad_series: bool = False, # noqa: FBT001 + calendar_query : type[CalendarQuery] = CalendarQuery, ) -> CalendarQuery: """Unfold recurring events of a_calendar @@ -1217,10 +1218,13 @@ def of( - keep_recurrence_attributes - whether to keep attributes that are only used to calculate the recurrence. - components is a list of component type names of which the recurrences - should be returned. + should be returned. This can also be instances of SelectComponents. + - skip_bad_series - whether to skip a series of components that contains + errors. + - calendar_query - The calendar query class to use. """ a_calendar = x_wr_timezone.to_standard(a_calendar) - return CalendarQuery( + return calendar_query( a_calendar, keep_recurrence_attributes, components, skip_bad_series ) From e9b42db1e814efc7515e68ef3df0043398cf1f53 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 9 Sep 2024 19:04:15 +0100 Subject: [PATCH 7/9] fix Python3.8 --- README.rst | 2 +- test/conftest.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2da85dc..271d342 100644 --- a/README.rst +++ b/README.rst @@ -387,7 +387,7 @@ In this example, we collect one VEVENT which matches a certain UID: >>> class CollectOneUIDEvent(SelectComponents): ... def __init__(self, uid:str) -> None: ... self.uid = uid - ... def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + ... def collect_series_from(self, source: Component, suppress_errors: tuple) -> Sequence[Series]: ... components : list[Component] = [] ... for component in source.walk("VEVENT"): ... if component.get("UID") == self.uid: diff --git a/test/conftest.py b/test/conftest.py index 5e1cdaf..20385f5 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -171,7 +171,8 @@ def env_for_doctest(monkeypatch): monkeypatch.setattr(_zoneinfo, "ZoneInfo", DoctestZoneInfo) from icalendar.timezone.zoneinfo import ZONEINFO monkeypatch.setattr(ZONEINFO, "utc", _zoneinfo.ZoneInfo("UTC")) - return { + env = { "print": doctest_print, "CALENDARS": CALENDARS_FOLDER, } + return env From 383d5b47f3210fd849071901aff0dd53c1baaa1a Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 9 Sep 2024 19:18:31 +0100 Subject: [PATCH 8/9] ruff formatting --- recurring_ical_events.py | 62 +++++++++++++++++++++++++------------ test/conftest.py | 11 +++++-- test/test_extend_classes.py | 39 ++++++++++++++--------- test/test_with_doctest.py | 11 +++++-- 4 files changed, 83 insertions(+), 40 deletions(-) diff --git a/recurring_ical_events.py b/recurring_ical_events.py index de01684..368dae1 100644 --- a/recurring_ical_events.py +++ b/recurring_ical_events.py @@ -17,15 +17,15 @@ import contextlib import datetime import re +import sys from abc import ABC, abstractmethod from collections import defaultdict from functools import wraps from typing import TYPE_CHECKING, Callable, Generator, Optional, Sequence, Union -import sys -from icalendar.cal import Component import x_wr_timezone from dateutil.rrule import rruleset, rrulestr +from icalendar.cal import Component from icalendar.prop import vDDDTypes if TYPE_CHECKING: @@ -306,7 +306,7 @@ def get_any(dictionary: dict, keys: Sequence[object], default: object = None): class Series: """Base class for components that result in a series of occurrences.""" - + @property def occurrence(self) -> type[Occurrence]: """A way to override the occurrence class.""" @@ -504,7 +504,7 @@ def rrule_between(self, span_start: Time, span_stop: Time) -> Generator[Time]: def between(self, span_start: Time, span_stop: Time) -> Generator[Occurrence]: """Components between the start (inclusive) and end (exclusive). - + The result does not need to be ordered. """ # make dates comparable, rrule converts them to datetimes @@ -632,12 +632,16 @@ def uid(self) -> UID: return self._component.get("UID", str(id(self._component))) @classmethod - def collect_series_from(cls, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + def collect_series_from( + cls, source: Component, suppress_errors: tuple[Exception] + ) -> Sequence[Series]: """Collect all components for this adapter. This is a shortcut. """ - return ComponentsWithName(cls.component_name(), cls).collect_series_from(source, suppress_errors) + return ComponentsWithName(cls.component_name(), cls).collect_series_from( + source, suppress_errors + ) def as_component(self, start: Time, stop: Time, keep_recurrence_attributes: bool): # noqa: FBT001 """Create a shallow copy of the source event and modify some attributes.""" @@ -921,6 +925,7 @@ def collect_series_from( A Series of events with such an error is removed from all results. """ + class ComponentsWithName(SelectComponents): """This is a component collecttion strategy. @@ -939,9 +944,15 @@ def _component_adapters(self) -> dict[str : type[ComponentAdapter]]: adapter.component_name(): adapter for adapter in self.component_adapters } - def __init__(self, name: str, adapter : type[ComponentAdapter]|None=None, series: type[Series] = Series, occurrence: type[Occurrence] = Occurrence) -> None: + def __init__( + self, + name: str, + adapter: type[ComponentAdapter] | None = None, + series: type[Series] = Series, + occurrence: type[Occurrence] = Occurrence, + ) -> None: """Create a new way of collecting components. - + name - the name of the component to collect ("VEVENT", "VTODO", "VJOURNAL") adapter - the adapter to use for these components with that name series - the series class that hold a series of components @@ -957,13 +968,17 @@ def __init__(self, name: str, adapter : type[ComponentAdapter]|None=None, series adapter = self._component_adapters[name] if occurrence is not Occurrence: _occurrence = occurrence + class series(series): # noqa: N801 occurrence = _occurrence + self._name = name self._series = series self._adapter = adapter - def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + def collect_series_from( + self, source: Component, suppress_errors: tuple[Exception] + ) -> Sequence[Series]: """Collect all components from the source component. suppress_errors - a list of errors that should be suppressed. @@ -992,14 +1007,15 @@ def _component_adapters(self) -> Sequence[ComponentAdapter]: def names(self) -> list[str]: """Return the names of the components to collect.""" return [adapter.component_name() for adapter in self._component_adapters] - - def __init__(self, - series: type[Series] = Series, - occurrence: type[Occurrence] = Occurrence, - collector:type[ComponentsWithName] = ComponentsWithName, - ) -> None: + + def __init__( + self, + series: type[Series] = Series, + occurrence: type[Occurrence] = Occurrence, + collector: type[ComponentsWithName] = ComponentsWithName, + ) -> None: """Collect all known components and overide the series and occurrence. - + series - the Series class to override that is queried for Occurrences occurrence - the occurrence class that creates the resulting components collector - if you want to override the SelectComponentsByName class @@ -1007,21 +1023,27 @@ def __init__(self, self._series = series self._occurrence = occurrence self._collector = collector - - def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + + def collect_series_from( + self, source: Component, suppress_errors: tuple[Exception] + ) -> Sequence[Series]: """Collect the components from the source groups into a series.""" result = [] for name in self.names: - collector = self._collector(name, series = self._series, occurrence = self._occurrence) + collector = self._collector( + name, series=self._series, occurrence=self._occurrence + ) result.extend(collector.collect_series_from(source, suppress_errors)) return result + if sys.version_info >= (3, 10): T_COMPONENTS = Sequence[str | type[ComponentAdapter] | SelectComponents] else: # see https://github.com/python/cpython/issues/86399#issuecomment-1093889925 T_COMPONENTS = Sequence[str] + class CalendarQuery: """A calendar that can unfold its events at a certain time. @@ -1210,7 +1232,7 @@ def of( keep_recurrence_attributes=False, components: T_COMPONENTS = ("VEVENT",), skip_bad_series: bool = False, # noqa: FBT001 - calendar_query : type[CalendarQuery] = CalendarQuery, + calendar_query: type[CalendarQuery] = CalendarQuery, ) -> CalendarQuery: """Unfold recurring events of a_calendar diff --git a/test/conftest.py b/test/conftest.py index 20385f5..01d8874 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -106,11 +106,12 @@ def walk(*args, **kw): return self._of(calendar) -if hasattr(icalendar, 'use_pytz') and hasattr(icalendar, 'use_zoneinfo'): +if hasattr(icalendar, "use_pytz") and hasattr(icalendar, "use_zoneinfo"): tzps = [icalendar.use_pytz, icalendar.use_zoneinfo] else: tzps = [lambda: ...] + @pytest.fixture(params=tzps, scope="module") def tzp(request): """The timezone provider supported by icalendar.""" @@ -153,26 +154,30 @@ def utc(request): """Return all the UTC implementations.""" return request.param + class DoctestZoneInfo(_zoneinfo.ZoneInfo): """Constent ZoneInfo representation for tests.""" + def __repr__(self): return f"ZoneInfo(key={self.key!r})" + def doctest_print(obj): """doctest print""" if isinstance(obj, bytes): obj = obj.decode("UTF-8") print(str(obj).strip().replace("\r\n", "\n").replace("\r", "\n")) + @pytest.fixture() def env_for_doctest(monkeypatch): """Modify the environment to make doctests run.""" monkeypatch.setitem(sys.modules, "zoneinfo", _zoneinfo) monkeypatch.setattr(_zoneinfo, "ZoneInfo", DoctestZoneInfo) from icalendar.timezone.zoneinfo import ZONEINFO + monkeypatch.setattr(ZONEINFO, "utc", _zoneinfo.ZoneInfo("UTC")) - env = { + return { "print": doctest_print, "CALENDARS": CALENDARS_FOLDER, } - return env diff --git a/test/test_extend_classes.py b/test/test_extend_classes.py index 8c3c3ed..2b990e6 100644 --- a/test/test_extend_classes.py +++ b/test/test_extend_classes.py @@ -1,7 +1,8 @@ """This tests extneding and modifying the behaviour of recurring ical events.""" + from __future__ import annotations -from typing import TYPE_CHECKING, Generator, Sequence +from typing import TYPE_CHECKING, Sequence import pytest from icalendar.cal import Component @@ -24,10 +25,12 @@ class SelectUID1(SelectComponents): """Collect only one UID.""" - def __init__(self, uid:str) -> None: + def __init__(self, uid: str) -> None: self.uid = uid - def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + def collect_series_from( + self, source: Component, suppress_errors: tuple[Exception] + ) -> Sequence[Series]: return [ series for adapter in [EventAdapter, JournalAdapter, TodoAdapter] @@ -37,12 +40,13 @@ def collect_series_from(self, source: Component, suppress_errors: tuple[Exceptio class SelectUID2(AllKnownComponents): - - def __init__(self, uid:str) -> None: + def __init__(self, uid: str) -> None: super().__init__() self.uid = uid - def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: + def collect_series_from( + self, source: Component, suppress_errors: tuple[Exception] + ) -> Sequence[Series]: return [ series for series in super().collect_series_from(source, suppress_errors) @@ -51,21 +55,22 @@ def collect_series_from(self, source: Component, suppress_errors: tuple[Exceptio class SelectUID3(SelectComponents): - - def __init__(self, uid:str) -> None: + def __init__(self, uid: str) -> None: self.uid = uid - def collect_series_from(self, source: Component, suppress_errors: tuple[Exception]) -> Sequence[Series]: - components : list[Component] = [] + def collect_series_from( + self, + source: Component, + suppress_errors: tuple[Exception], # noqa: ARG002 + ) -> Sequence[Series]: + components: list[Component] = [] for component in source.walk("VEVENT"): if component.get("UID") == self.uid: - components.append(EventAdapter(component)) + components.append(EventAdapter(component)) # noqa: PERF401 return [Series(components)] if components else [] -@pytest.mark.parametrize( - "collector", [SelectUID1, SelectUID2, SelectUID3] -) +@pytest.mark.parametrize("collector", [SelectUID1, SelectUID2, SelectUID3]) def test_collect_only_one_uid(calendars, collector): """Test that only one UID is used.""" uid = "4mm2ak3in2j3pllqdk1ubtbp9p@google.com" @@ -82,8 +87,12 @@ def as_component(self, keep_recurrence_attributes: bool) -> Component: # noqa: component["X-MY-ATTRIBUTE"] = "my occurrence" return component + def test_added_attributes(calendars): """Test that attributes are added.""" - query = of(calendars.raw.one_event, components=[AllKnownComponents(occurrence=MyOccurrence)]) + query = of( + calendars.raw.one_event, + components=[AllKnownComponents(occurrence=MyOccurrence)], + ) event = next(query.all()) assert event["X-MY-ATTRIBUTE"] == "my occurrence" diff --git a/test/test_with_doctest.py b/test/test_with_doctest.py index 5bdf15b..290788b 100644 --- a/test/test_with_doctest.py +++ b/test/test_with_doctest.py @@ -10,6 +10,7 @@ Hello World! """ + import doctest import importlib import pathlib @@ -26,6 +27,7 @@ "recurring_ical_events", ] + @pytest.mark.parametrize("module_name", MODULE_NAMES) def test_docstring_of_python_file(module_name): """This test runs doctest on the Python module.""" @@ -44,10 +46,15 @@ def test_documentation_file(document, env_for_doctest): functions are also replaced to work. """ - test_result = doctest.testfile(str(document), module_relative=False, globs=env_for_doctest, raise_on_error=False) + test_result = doctest.testfile( + str(document), + module_relative=False, + globs=env_for_doctest, + raise_on_error=False, + ) assert test_result.failed == 0, f"{test_result.failed} errors in {document.name}" -def test_can_import_zoneinfo(env_for_doctest): +def test_can_import_zoneinfo(env_for_doctest): # noqa: ARG001 """Allow importing zoneinfo for tests.""" assert "zoneinfo" in sys.modules From 0a3f5a997dac207d7009bc4f6eeae38ec419b62c Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Tue, 10 Sep 2024 16:21:53 +0100 Subject: [PATCH 9/9] Test series and correct UID --- recurring_ical_events.py | 3 ++- test/test_extend_classes.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/recurring_ical_events.py b/recurring_ical_events.py index 86ed5c4..648215b 100644 --- a/recurring_ical_events.py +++ b/recurring_ical_events.py @@ -561,6 +561,7 @@ def __init__(self, components: Sequence[ComponentAdapter]): self.recurrence_id_to_modification: dict[ RecurrenceID, ComponentAdapter ] = {} # RECURRENCE-ID -> adapter + self._uid = components[0].uid core: ComponentAdapter | None = None for component in components: if component.is_modification(): @@ -632,7 +633,7 @@ def between(self, span_start: Time, span_stop: Time) -> Generator[Occurrence]: @property def uid(self): """The UID that identifies this series.""" - return getattr(self.recurrence, "uid", "invalid") + return self._uid def __repr__(self): """A string representation.""" diff --git a/test/test_extend_classes.py b/test/test_extend_classes.py index 2b990e6..2a49ed6 100644 --- a/test/test_extend_classes.py +++ b/test/test_extend_classes.py @@ -16,6 +16,7 @@ Series, TodoAdapter, of, + ComponentsWithName ) if TYPE_CHECKING: @@ -67,6 +68,7 @@ def collect_series_from( for component in source.walk("VEVENT"): if component.get("UID") == self.uid: components.append(EventAdapter(component)) # noqa: PERF401 + print(components) return [Series(components)] if components else [] @@ -96,3 +98,31 @@ def test_added_attributes(calendars): ) event = next(query.all()) assert event["X-MY-ATTRIBUTE"] == "my occurrence" + + +@pytest.mark.parametrize( + ("calendar", "count", "collector"), + [ + # all + ("one_event", 1, AllKnownComponents()), + ("issue_97_simple_todo", 1, AllKnownComponents()), + ("issue_97_simple_journal", 1, AllKnownComponents()), + # events + ("one_event", 1, ComponentsWithName("VEVENT")), + ("issue_97_simple_todo", 0, ComponentsWithName("VEVENT")), + ("issue_97_simple_journal", 0, ComponentsWithName("VEVENT")), + # todos + ("one_event", 0, ComponentsWithName("VTODO")), + ("issue_97_simple_todo", 1, ComponentsWithName("VTODO")), + ("issue_97_simple_journal", 0, ComponentsWithName("VTODO")), + # journals + ("one_event", 0, ComponentsWithName("VJOURNAL")), + ("issue_97_simple_todo", 0, ComponentsWithName("VJOURNAL")), + ("issue_97_simple_journal", 1, ComponentsWithName("VJOURNAL")), + ] +) +def test_we_collect_all_components(calendars, calendar, count, collector:SelectComponents): + """Check that the calendars have the right amount of series collected.""" + series = collector.collect_series_from(calendars.raw[calendar], []) + print(series) + assert len(series) == count