Repository navigation
Conversation
|
Thanks for the pull request, @EyalHefer! A maintainer will review it shortly. |
Insufficient dataNot enough activity yet to make a reliable assessment. This is an automated analysis by AgentScan |
|
This pull request did not pass quality checks and AI use is suspected. Please review Contribute and make any necessary amendments. |
Detect the format from the first non-whitespace character and call the matching parser. Accept bytes, a Path or a binary file. Add tests and docs. Closes PyCalendar#1917 AI: Claude Opus 5.5 (Claude Code) explained the codebase, reviewed my parse() code, and drafted the tests and docs. I reviewed and tested all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6bc58f3 to
816f9c5
Compare
|
This pull request did not pass quality checks and AI use is suspected. Please review Contribute and make any necessary amendments. |
|
Thanks for the automated feedback! I shortened the description and the commit message, and ran the full test suite on Linux (21,329 passed) so I could check every item in the checklist. I also added a test for the "no calendars found" case. The one remaining failure, |
niccokunzmann
left a comment
There was a problem hiding this comment.
Thanks, this looks really nice! I added some suggestions.
| ======================= | ||
|
|
||
| By default, :func:`~icalendar.cal.calendar.parse` returns one calendar. | ||
| If the data contains more than one calendar, it raises a :class:`ValueError`. |
There was a problem hiding this comment.
I think as an interface, it would be nice to raise a subclass of ValueError: CalendarInformationLost with a nice error message.
Also, this should explain that iCalendar and xCal may both contain several calendars. jCal can only contain one. But mostly there is only one calendar anyway.
There was a problem hiding this comment.
Done. parse now raises CalendarInformationLost (a ValueError subclass), and the docs explain that iCalendar and xCal data may contain several calendars and jCal only one.
|
|
||
| - iCalendar, as specified in :rfc:`5545` | ||
| - jCal, a JSON representation, as specified in :rfc:`7265` | ||
| - xCal, an XML representation, as specified in :rfc:`6321` |
There was a problem hiding this comment.
Would it work to list the from_... functions next to the RFC?
There was a problem hiding this comment.
Done, each RFC now lists its parser.
| The function :func:`~icalendar.cal.calendar.parse` detects the format for you and calls the matching parser. | ||
|
|
||
|
|
||
| Parse data |
There was a problem hiding this comment.
| Parse data | |
| Parse :class:`bytes` |
Does that work?
| from pathlib import Path | ||
| from icalendar import parse | ||
|
|
||
| calendar = parse(Path("calendar.ics")) |
There was a problem hiding this comment.
I think, you can convert this into a >>> pycon block if you use this.
| calendar = parse(Path("calendar.ics")) | |
| calendar = parse(Path("example.ics")) |
We have the example.ics file at the root of the repo to make that possible.
There was a problem hiding this comment.
Changed to pycon blocks. The root example.ics is in .gitignore (a tutorial doctest creates it), so the doctest failed on a fresh clone. I used src/icalendar/tests/calendars/example.ics instead, like usage.rst does.
| A calendar, or a list of calendars if ``multiple`` is ``True``. | ||
|
|
||
| Raises: | ||
| ValueError: If the format cannot be detected, or if ``multiple`` is |
There was a problem hiding this comment.
I would value a subclass for the format problem here - then it is clear from the error type what the problem is.
Can also be a subclass: NoCalendarsFound.
Let's make the front pretty!
There was a problem hiding this comment.
Added UnknownCalendarFormat and NoCalendarsFound. Both inherit from InvalidCalendar, because the data itself is the problem. CalendarInformationLost inherits from ValueError directly, because there the data is valid.
| data = source.read_bytes() | ||
| else: | ||
| data = source.read() | ||
| data = data.lstrip() |
There was a problem hiding this comment.
It is relatively costly to perform that operation. It might be faster to use a regex.
\w*([bB\[<])
I am not sure if begin would also be accepted...
That is a speed add-on.
There was a problem hiding this comment.
I measured this before changing it. lstrip() returns the same object when there is nothing to strip, so in the usual case nothing is copied. With leading whitespace, a 50 MB file takes about 0.02 s, which is small compared to parsing. A regex would only find the first character, and the whitespace would still need to be cut before from_xcal, which is also a copy. So I kept lstrip().
About begin: from_ical accepts lowercase, so parse now does too. I also skip a UTF-8 BOM, which from_ical already accepts. Both have tests.
|
|
||
| def test_empty_data_raises_error(): | ||
| """Data with no detectable format raises a ValueError.""" | ||
| with pytest.raises(ValueError): |
There was a problem hiding this comment.
That should check which error and the message should be descriptive.
There was a problem hiding this comment.
Each error test now checks the error class and part of the message.
| assert calendars[0]["PRODID"] == "test" | ||
|
|
||
|
|
||
| def test_parse_open_file(tmp_path): |
There was a problem hiding this comment.
This could be parametrized with a fixture for all valid kinds of data.
There was a problem hiding this comment.
Done, the path and file tests use the data_path and data_file fixtures, so they run for all three formats.
| ) | ||
|
|
||
|
|
||
| ALL_FORMATS = pytest.mark.parametrize( |
There was a problem hiding this comment.
Please transform this into a fixture and use it throughout the tests.
Since there is an if/else statement, this ensures propert handling in the future when others edit this.
@pytest.fixture(params=..., ids=...)
def data(request):
return request.param
@pytest.fixture()
def data_path(data, tmp_path):
path = tmp_path / "calendar.ics"
path.write_bytes(ICAL)
return path
@pytest.fixture()
def data_file(data_path):
return data_path.open("rb")
There was a problem hiding this comment.
Done. One small change from your example: data_path writes data instead of ICAL.
| data = source.read_bytes() | ||
| else: | ||
| data = source.read() | ||
| data = data.lstrip() |
There was a problem hiding this comment.
Before this line, you need to check that data is actually bytes. If not, that justifies a TypeError.
Tested e.g. by passing data_path.open("r")
There was a problem hiding this comment.
Added. A str, or a file opened with "r", now raises a TypeError that says to open files with "rb". Both cases have tests.
Raise UnknownCalendarFormat, NoCalendarsFound and CalendarInformationLost instead of ValueError, and TypeError for non-bytes data. Accept a UTF-8 BOM and lowercase "begin". Use fixtures for all formats in the tests and update the docs. AI: Claude Opus 5.5 (Claude Code) found edge cases and wrote the tests and docs. I wrote the parse() changes and error classes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review! I addressed all comments and the full test suite passes. Ready for another look. |
niccokunzmann
left a comment
There was a problem hiding this comment.
Thanks! I left another comment!
| Parse any calendar format | ||
| ========================= | ||
|
|
||
| This chapter describes how to parse calendar data when you do not know in advance whether it is iCalendar, jCal, or xCal. |
There was a problem hiding this comment.
| This chapter describes how to parse calendar data when you do not know in advance whether it is iCalendar, jCal, or xCal. | |
| This chapter describes how to parse calendar data when you do not know in advance whether it is :term:`iCalendar`, :term:`jCal`, or :term:`xCal`. |
We can link the now !
There was a problem hiding this comment.
Done. I merged main so that the jCal and xCal glossary terms resolve.
| >>> len(calendar.events) | ||
| 3 | ||
|
|
||
| A file opened in text mode returns :class:`str` instead of :class:`bytes`, so :func:`~icalendar.cal.calendar.parse` raises a :class:`TypeError`. |
There was a problem hiding this comment.
I opened an issue - since from_ical also works with string, jCal also does and for XML it just need encoding with the <xml...> prefix, it should work. Have a look: #2045
There was a problem hiding this comment.
Thanks, I read #2045. For now parse still raises a TypeError for text, and I left the input types for that issue.
|
|
||
| To get all calendars as a list, pass ``multiple=True``. | ||
|
|
||
| .. code-block:: pycon |
There was a problem hiding this comment.
| .. code-block:: pycon | |
| The example below demonstrates the iCalendar stream. | |
| The ``ical`` variable contains only one calendar. | |
| ``icale + ical`` parses into two calendars. | |
| .. code-block:: pycon |
There was a problem hiding this comment.
Added, with ical + ical in the last line.
| f"Expected bytes, but got {type(data).__name__}. " | ||
| 'Open files in binary mode with "rb".' | ||
| ) | ||
| data = data.removeprefix(b"\xef\xbb\xbf").lstrip() |
There was a problem hiding this comment.
If you remove the byte order mark - can you check that UTF-8 with a different order still works?
It might be better to do both:
- use a regex to find the first important character
- pass the bytes to the parsing function and not altering them here
You could add another example calendar with the BOM but in the other direction.
There was a problem hiding this comment.
Good catch. parse now finds the first important character with a regex and passes the data to the parser unchanged.
I tested the byte order marks: a UTF-8 BOM works for all three formats (jCal is now decoded with utf-8-sig), and UTF-16 xCal works in both byte orders. UTF-16 iCal and jCal still raise an error, because from_ical and from_jcal don't support them.
One behavior change: since the data is not altered, leading whitespace now behaves as in the parsers. jCal accepts it, iCal and xCal raise an error. The tests cover this.
Find the first important character with a regex and pass the data to the parser unaltered. This adds UTF-16 xCal support. Link glossary terms in the docs and update the tests. AI: Claude Opus 5.5 (Claude Code) wrote the regex, tests and docs and guided my parse() changes. I reviewed and tested all of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks! I addressed the new comments and merged main. The full test suite passes. Ready for another look. |
stevepiercy
left a comment
There was a problem hiding this comment.
This is nice work. I'd like to see a docstring added for the regular expression, and made a few nitpicky suggestions. Would you please take care?
Also @niccokunzmann needs to do another review and work through his unresolved suggestions.
|
|
||
|
|
||
| __all__ = ["Calendar"] | ||
| FORMAT_START = re.compile(rb"(?:\xef\xbb\xbf|\xff\xfe|\xfe\xff)?[\s\x00]*([Bb\[<])") |
There was a problem hiding this comment.
This should have a docstring to explain its purpose and cite the source of the regular expression.
There was a problem hiding this comment.
I want to check that this renders OK once this branch is updated with main and the RTD preview can build.
Apply the review suggestions: Oxford commas, no "please" in the error message, wording in the how-to guide, the news entry and the test module docstring. Move the link to the how-to guide into a seealso block after the examples. Add a docstring to FORMAT_START that explains what the regular expression skips and cites the review comment it is based on. AI: Claude Opus 5.5 (Claude Code) applied the review suggestions and drafted the FORMAT_START docstring at my request. I reviewed the changes and ran the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @stevepiercy! I applied all your suggestions and added a docstring to |
|
@EyalHefer please update your branch against |
stevepiercy
left a comment
There was a problem hiding this comment.
Changes look good, but I can't verify since the branch is not current with main and RTD can't build the docs. Would you please take care?
Also need a review from @niccokunzmann to go over his unresolved comments.
Linked issue
Description
Adds a parse function that detects whether data is iCalendar, jCal, or xCal from its first non-whitespace character and calls the matching parser.
Checklist