Skip to content

Commit 72cfdbb

Browse files
committed
Preserve URI parameters on jCal round-trip
1 parent b32cbd7 commit 72cfdbb

3 files changed

Lines changed: 76 additions & 1 deletion

File tree

news/1524.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Preserve ``URI`` value parameters on a jCal round-trip. :meth:`vUri.from_jcal <icalendar.prop.uri.vUri.from_jcal>` passed the parameters positionally, but ``vUri.__new__`` takes ``encoding`` as its second positional argument, so the parameters were consumed as the encoding and dropped (for example ``ATTACH;FMTTYPE=...`` or ``CONFERENCE;FEATURE=...`` lost their parameters). They are now passed as the ``params`` keyword. Prepared with the assistance of an AI coding agent (Anthropic's Claude Opus 4.8). @gaoflow

src/icalendar/prop/uri.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def from_jcal(cls, jcal_property: list) -> Self:
102102
JCalParsingError.validate_property(jcal_property, cls)
103103
return cls(
104104
jcal_property[3],
105-
Parameters.from_jcal_property(jcal_property),
105+
params=Parameters.from_jcal_property(jcal_property),
106106
)
107107

108108
@property
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Property parameters on URI-typed properties must survive a jCal round-trip.
2+
3+
``vUri.from_jcal`` used to pass the parsed parameters as the second *positional*
4+
argument. Unlike every other value type, ``vUri.__new__`` takes an ``encoding``
5+
argument in that slot (``vUri(value, encoding, /, params=None)``), so the
6+
:class:`~icalendar.parser.Parameters` object was bound to ``encoding`` and
7+
silently discarded, leaving ``params`` at its default of ``None``.
8+
9+
As a result parameters such as ``FMTTYPE`` (ATTACH), ``FEATURE``/``LABEL``
10+
(CONFERENCE) or ``RELTYPE`` (RELATED-TO) were dropped whenever a URI property was
11+
read back from jCal, breaking the round-trip required by :rfc:`7265`.
12+
"""
13+
14+
from icalendar import Calendar, Component, Event, vUri
15+
from icalendar.parser import Parameters
16+
17+
18+
def _jcal_round_trip(component: Component) -> Component:
19+
return Component.from_jcal(component.to_jcal())
20+
21+
22+
def test_vuri_from_jcal_keeps_parameters():
23+
"""The root cause: ``vUri.from_jcal`` must attach the parsed parameters."""
24+
jcal_property = ["attach", {"fmttype": "text/plain"}, "uri", "http://example.com"]
25+
uri = vUri.from_jcal(jcal_property)
26+
assert uri.params == Parameters({"FMTTYPE": "text/plain"})
27+
# The value (and the ``encoding`` slot the parameters used to be bound to)
28+
# is still parsed correctly.
29+
assert str(uri) == "http://example.com"
30+
31+
32+
def test_attach_fmttype_survives_jcal_round_trip():
33+
event = Event()
34+
event.add("UID", "1")
35+
event.add(
36+
"ATTACH",
37+
vUri("http://example.com/report.txt", params={"FMTTYPE": "text/plain"}),
38+
)
39+
restored = _jcal_round_trip(event)
40+
assert restored["ATTACH"].params["FMTTYPE"] == "text/plain"
41+
assert b"ATTACH;FMTTYPE=text/plain:" in restored.to_ical()
42+
43+
44+
def test_conference_multiple_parameters_survive_jcal_round_trip():
45+
"""Several parameters on the same URI property are all preserved."""
46+
event = Event()
47+
event.add("UID", "1")
48+
event.add(
49+
"CONFERENCE",
50+
vUri("tel:+1-555-0100", params={"FEATURE": "AUDIO", "LABEL": "Call in"}),
51+
)
52+
restored = _jcal_round_trip(event)
53+
params = restored["CONFERENCE"].params
54+
assert params["FEATURE"] == "AUDIO"
55+
assert params["LABEL"] == "Call in"
56+
57+
58+
def test_jcal_round_trip_is_idempotent_for_uri_parameters():
59+
"""``to_jcal`` of the reparsed component equals the original jCal."""
60+
calendar = Calendar()
61+
calendar.add("VERSION", "2.0")
62+
calendar.add("PRODID", "-//test//EN")
63+
event = Event()
64+
event.add("UID", "1")
65+
event.add(
66+
"ATTACH", vUri("http://example.com/x.txt", params={"FMTTYPE": "text/plain"})
67+
)
68+
event.add(
69+
"CONFERENCE", vUri("https://chat.example.com/", params={"FEATURE": "VIDEO"})
70+
)
71+
calendar.add_component(event)
72+
73+
jcal = calendar.to_jcal()
74+
assert Component.from_jcal(jcal).to_jcal() == jcal

0 commit comments

Comments
 (0)