-
-
Notifications
You must be signed in to change notification settings - Fork 175
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
Add from_file and to_file method #757
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
from datetime import date, datetime, timedelta, tzinfo | ||
from typing import List, Optional, Tuple | ||
from datetime import date, datetime, timedelta | ||
from pathlib import Path | ||
from typing import TYPE_CHECKING, List, NamedTuple, Optional, Tuple, Union | ||
|
||
import dateutil.rrule | ||
|
@@ -587,6 +588,76 @@ def is_thunderbird(self) -> bool: | |
return any(attr.startswith("X-MOZ-") for attr in self.keys()) | ||
|
||
|
||
@classmethod | ||
def from_file(cls, file: Union[str, Path], multiple: bool = False): | ||
"""Create a Component from a file. | ||
|
||
This class method can be used by any Component subclass (Calendar, Event, etc.) | ||
to read their data from a file. | ||
|
||
Args: | ||
file: The file to read from. Can be: | ||
- A string path to a file | ||
- A Path object | ||
multiple: If True, allows parsing multiple components from the file. | ||
|
||
Returns: | ||
If multiple=False (default): | ||
A single Component instance of the appropriate type | ||
If multiple=True: | ||
A list of Component instances | ||
|
||
Raises: | ||
FileNotFoundError: If the file path doesn't exist | ||
ValueError: If the file contents are not valid iCalendar format | ||
|
||
Example: | ||
>>> from icalendar import Calendar | ||
>>> from pathlib import Path | ||
>>> # Read a calendar file | ||
>>> cal = Calendar.from_file("src/icalendar/tests/calendars/example.ics") | ||
>>> # Read multiple calendars | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can remove the >>> here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you suggestion to remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if I remove the
This does work:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It needs a new line:
Otherwise the comment is considered output. |
||
>>> cals = Calendar.from_file("src/icalendar/tests/calendars/multiple_calendar_components.ics", multiple=True) | ||
>>> # or pass a Path object | ||
>>> path = Path("src/icalendar/tests/calendars/example.ics") | ||
>>> cal = Calendar.from_file(path) | ||
""" | ||
# Handle string path by converting to Path | ||
if isinstance(file, str): | ||
file = Path(file) | ||
|
||
return cls.from_ical(file.read_bytes(), multiple=multiple) | ||
|
||
def to_file(self, file: Union[str, Path], sorted: bool = True) -> None: | ||
"""Write the component to a file. | ||
|
||
This method can be used by any Component subclass (Calendar, Event, etc.) | ||
to write their data to a file. | ||
|
||
Args: | ||
file: Where to write the component. Can be: | ||
- A string path to a file | ||
- A Path object | ||
sorted: Whether parameters and properties should be lexicographically sorted. | ||
|
||
Example: | ||
>>> from icalendar import Calendar, Event | ||
>>> # Write a calendar | ||
>>> cal = Calendar() | ||
>>> cal.to_file("calendar.ics") | ||
>>> # Write an event | ||
>>> event = Event() | ||
>>> event.to_file("event.ics") | ||
|
||
""" | ||
|
||
# Handle string path | ||
if isinstance(file, str): | ||
file = Path(file) | ||
|
||
file.write_bytes(self.to_ical(sorted=sorted)) | ||
|
||
|
||
|
||
####################################### | ||
# components defined in RFC 5545 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import pytest | ||
from datetime import datetime | ||
from pathlib import Path | ||
from icalendar import Calendar, Event, Todo, Journal | ||
|
||
|
||
@pytest.fixture | ||
def temp_path(tmp_path): | ||
"""Create a temp directory and change to it for tests""" | ||
return tmp_path / "test.ics" | ||
|
||
|
||
@pytest.fixture | ||
def multiple_calendars_path(): | ||
"""Path to test file containing multiple calendar components""" | ||
return Path("src/icalendar/tests/calendars/multiple_calendar_components.ics") | ||
|
||
|
||
def test_from_file_str_path(calendars): | ||
"""Test reading from file using string path""" | ||
path = "src/icalendar/tests/calendars/example.ics" | ||
cal = Calendar.from_file(path) | ||
assert cal == calendars.example | ||
|
||
|
||
def test_from_file_path_object(calendars): | ||
"""Test reading from file using Path object""" | ||
path = Path("src/icalendar/tests/calendars/example.ics") | ||
cal = Calendar.from_file(path) | ||
assert cal == calendars.example | ||
|
||
|
||
def test_from_file_multiple(multiple_calendars_path): | ||
"""Test reading multiple components from a file""" | ||
cals = Calendar.from_file(multiple_calendars_path, multiple=True) | ||
assert isinstance(cals, list) | ||
assert len(cals) > 1 | ||
assert all(isinstance(cal, Calendar) for cal in cals) | ||
|
||
|
||
def test_from_file_non_existent(): | ||
"""Test attempting to read from non-existent file""" | ||
with pytest.raises(FileNotFoundError): | ||
Calendar.from_file("non_existent.ics") | ||
|
||
|
||
def test_to_file_str_path(temp_path, calendars): | ||
"""Test writing to file using string path""" | ||
cal = calendars.example | ||
cal.to_file(str(temp_path)) | ||
assert temp_path.exists() | ||
# Verify contents by reading back | ||
cal2 = Calendar.from_file(temp_path) | ||
assert cal == cal2 | ||
|
||
|
||
def test_to_file_path_object(temp_path, calendars): | ||
"""Test writing to file using Path object""" | ||
cal = calendars.example | ||
cal.to_file(temp_path) | ||
assert temp_path.exists() | ||
# Verify contents by reading back | ||
cal2 = Calendar.from_file(temp_path) | ||
assert cal == cal2 | ||
|
||
|
||
def test_other_components(temp_path): | ||
"""Test file I/O with other component types""" | ||
components = [Event(), Todo(), Journal()] | ||
|
||
for comp in components: | ||
comp.add("summary", "Test") | ||
comp.to_file(temp_path) | ||
assert temp_path.exists() | ||
# Read back and verify it's the correct type | ||
comp2 = type(comp).from_file(temp_path) | ||
assert isinstance(comp2, type(comp)) | ||
assert comp == comp2 | ||
|
||
|
||
def test_component_roundtrip(temp_path): | ||
"""Test that a component survives a write/read cycle preserving all data""" | ||
# Create a complex calendar with nested components | ||
cal = Calendar() | ||
event = Event() | ||
event.add("summary", "Test Event") | ||
|
||
dt = datetime(2024, 1, 1, 12, 0, 0) | ||
event.add("dtstart", dt) | ||
cal.add_component(event) | ||
|
||
# Write and read back | ||
cal.to_file(temp_path) | ||
cal2 = Calendar.from_file(temp_path) | ||
|
||
# Verify equality | ||
assert cal == cal2 | ||
assert len(cal2.subcomponents) == len(cal.subcomponents) | ||
assert cal2.subcomponents[0]["summary"] == "Test Event" | ||
assert cal2.subcomponents[0]["dtstart"].dt == dt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.