niccokunzmann / python-recurring-ical-events

Python library to calculate recurrence times of events, todos and journals based on icalendar RFC5545
https://pypi.org/project/recurring-ical-events/
GNU Lesser General Public License v3.0
90 stars 20 forks source link

feat: easy way to allow customizing classes #133

Open fabien-michel opened 5 months ago

fabien-michel commented 5 months ago

Add a mechanism to allow overriding classes.

Permit it would allow users to implement custom behavior without having to monkey-patch the code.

Example:

class MyCustomRepetition(Repetition):
    def __init__(self, *args, **kwargs):
        # custom behavior
        pass

class MyCustomRepeatedEvent(Repetition):
    def __init__(self, *args, **kwargs):
        # custom behavior
        pass

# Pass classes to `of` method (which will propagate options)
recurring_ical_events.of(calendar, repetition_cls=MyCustomRepetition, repeated_event_cls=MyCustomRepeatedEvent)

# or maybe with a dict
class_overrides = {
  Repetition: MyCustomRepetition,
  RepeatedEvent: MyCustomRepeatedEvent,
}

recurring_ical_events.of(calendar, class_overrides=class_overrides)

I'm not sure what is the good to way to do this kind of library customization.


We're using Polar.sh so you can upvote and help fund this issue. We receive the funding once the issue is completed & confirmed by you. Thank you in advance for helping prioritize & fund our work.

Fund with Polar

niccokunzmann commented 5 months ago

recurring_ical_events.of(calendar, class_overrides=class_overrides)

I like that one as it is simpler.

We have a start already. Maybe that can be incorporated. How to do it is now known to me at the moment. https://github.com/niccokunzmann/python-recurring-ical-events/blob/2428c3f2cdb1ef2250a89fdef407cb93e8e2ce3d/recurring_ical_events.py#L484

On a general note, I see that the amount of arguments being dragged through the code increases: https://github.com/niccokunzmann/python-recurring-ical-events/blob/2428c3f2cdb1ef2250a89fdef407cb93e8e2ce3d/recurring_ical_events.py#L664 This might be a point where a Strategy Pattern is a nicer solution. I think, people are fine with the odd argument and are unlikely to patch everything.

A Strategy pattern might also allow customization in the case of #132.

fabien-michel commented 5 months ago

I'm not sure what you mean by using the Strategy Pattern. Would something like this will match your expectation ?

Customization

import recurring_ical_events

class CustomRepeatedEvent(recurring_ical_events.RepeatedEvent):
    def _get_component_end(self):
        # Customized method
        return super()._get_component_end()

class CustomUnfoldableCalendar(recurring_ical_events.UnfoldableCalendar):
    keep_recurrence_attributes = True
    recurrence_calculators = {
        **recurring_ical_events.UnfoldableCalendar.recurrence_calculators,
        "VEVENT": CustomRepeatedEvent,
    }

calendar_events = recurring_ical_events.of(calendar, unfoldable_calendar_cls=CustomUnfoldableCalendar).between(...)

Possible implementation

def of(
    a_calendar,
    keep_recurrence_attributes=False,
    components=None,
    unfoldable_calendar_cls = UnfoldableCalendar,
) -> UnfoldableCalendar,:
    ...
    return unfoldable_calendar_cls(a_calendar, keep_recurrence_attributes, components)
class UnfoldableCalendar:
    recurrence_calculators = {
        "VEVENT": RepeatedEvent,
        "VTODO": RepeatedTodo,
        "VJOURNAL": RepeatedJournal,
    }

    keep_recurrence_attributes = False  # Add this
    components = None   # Add this

    def __init__(
        self,
        calendar,
        keep_recurrence_attributes=None,
        components=None,
    ):
        # Keep existing behavior
        if keep_recurrence_attributes is not None:
            self.keep_recurrence_attributes = keep_recurrence_attributes
        self.components = components or ["VEVENT"]
        ...
niccokunzmann commented 2 months ago

@fabien-michel, sorry for taking so long. I must have missed your comment. It looks good to me what you propose. There are also other classes that are used further inside. It would be nice to have a consistent way to patch those.

If you like, you could create an example file that overrides all the classes so that people have it easier and consistent and I can make sure that it works in the future.

niccokunzmann commented 2 weeks ago

In Version 3.0.0, I added a new way: the ComponentAdapter.

You can pass the ComponentAdapter to the components=[...] argument and it will be used to collect icalendar Components, create a Series and calculate the Occurrences. Thus, you can change all the components and classes if you like.

I would say that it is necessary to document the public interface a bit better for this: Which classes/methods/functions are public how to override the classes, more documentation in the methods.

Does this answer your question? What would you like to see to customize? Would documentation help? Let me know what you think, please.

fabien-michel commented 3 days ago

Thanks for all the works on version 3 ! They way you did it seems nice ! I'm just wondering how you would override Series class if needed. It's seems possible to override collect_components to return a new Series-like class, but I'm not sure if there is better way.

niccokunzmann commented 2 days ago

I should add an example that filters, and shows how to do it...