Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/cal/component.py: 68%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

292 statements  

1"""The base for :rfc:`5545` components.""" 

2 

3from __future__ import annotations 

4 

5from datetime import date, datetime, time, timezone 

6from typing import TYPE_CHECKING, ClassVar 

7 

8from icalendar.attr import comments_property, single_utc_property, uid_property 

9from icalendar.cal.component_factory import ComponentFactory 

10from icalendar.caselessdict import CaselessDict 

11from icalendar.error import InvalidCalendar 

12from icalendar.parser import Contentline, Contentlines, Parameters, q_join, q_split 

13from icalendar.parser_tools import DEFAULT_ENCODING 

14from icalendar.prop import TypesFactory, vDDDLists, vText 

15from icalendar.timezone import tzp 

16from icalendar.tools import is_date, is_datetime 

17 

18if TYPE_CHECKING: 

19 from icalendar.compatibility import Self 

20 

21_marker = [] 

22 

23 

24class Component(CaselessDict): 

25 """Base class for calendar components. 

26 

27 Component is the base object for calendar, Event and the other 

28 components defined in :rfc:`5545`. Normally you will not use this class 

29 directly, but rather one of the subclasses. 

30 

31 Attributes: 

32 name: The name of the component. Example: ``VCALENDAR``. 

33 required: These properties are required. 

34 singletons: These properties must only appear once. 

35 multiple: These properties may occur more than once. 

36 exclusive: These properties are mutually exclusive. 

37 inclusive: If the first in a tuple occurs, the second one must also occur. 

38 ignore_exceptions: If True, and we cannot parse this 

39 component, we will silently ignore it, rather than let the 

40 exception propagate upwards. 

41 types_factory: Factory for property types 

42 """ 

43 

44 name = None # should be defined in each component 

45 required = () # These properties are required 

46 singletons = () # These properties must only appear once 

47 multiple = () # may occur more than once 

48 exclusive = () # These properties are mutually exclusive 

49 inclusive: ( 

50 tuple[str] | tuple[tuple[str, str]] 

51 ) = () # if any occurs the other(s) MUST occur 

52 # ('duration', 'repeat') 

53 ignore_exceptions = False # if True, and we cannot parse this 

54 # component, we will silently ignore 

55 # it, rather than let the exception 

56 # propagate upwards 

57 # not_compliant = [''] # List of non-compliant properties. 

58 

59 types_factory = TypesFactory() 

60 _components_factory: ClassVar[ComponentFactory | None] = None 

61 

62 @classmethod 

63 def get_component_class(cls, name: str) -> type[Component]: 

64 """Return a component with this name. 

65 

66 Arguments: 

67 name: Name of the component, i.e. ``VCALENDAR`` 

68 """ 

69 if cls._components_factory is None: 

70 cls._components_factory = ComponentFactory() 

71 return cls._components_factory.get(name, Component) 

72 

73 @staticmethod 

74 def _infer_value_type(value: date | datetime | timedelta | time | tuple | list) -> str | None: 

75 """Infer the ``VALUE`` parameter from a Python type. 

76 

77 Args: 

78 value: Python native type, one of :py:class:`date`, :py:mod:`datetime`, :py:class:`timedelta`, :py:mod:`time`, :py:class:`tuple`, or :py:class:`list`. 

79 

80 Returns: 

81 str or None: The ``VALUE`` parameter string, for example, "DATE", "TIME", or other string, or ``None`` 

82 if no specific ``VALUE`` is needed. 

83 """ 

84 if isinstance(value, list): 

85 if not value: 

86 return None 

87 # Check if ALL items are date (but not datetime) 

88 if all(is_date(item) for item in value): 

89 return "DATE" 

90 # Check if ALL items are time 

91 if all(isinstance(item, time) for item in value): 

92 return "TIME" 

93 # Mixed types or other types - don't infer 

94 return None 

95 if is_date(value): 

96 return "DATE" 

97 if isinstance(value, time): 

98 return "TIME" 

99 # Don't infer PERIOD - it's too risky and vPeriod already handles it 

100 return None 

101 

102 def __init__(self, *args, **kwargs): 

103 """Set keys to upper for initial dict.""" 

104 super().__init__(*args, **kwargs) 

105 # set parameters here for properties that use non-default values 

106 self.subcomponents: list[Component] = [] # Components can be nested. 

107 self.errors = [] # If we ignored exception(s) while 

108 # parsing a property, contains error strings 

109 

110 def __bool__(self): 

111 """Returns True, CaselessDict would return False if it had no items.""" 

112 return True 

113 

114 def is_empty(self): 

115 """Returns True if Component has no items or subcomponents, else False.""" 

116 return bool(not list(self.values()) + self.subcomponents) 

117 

118 ############################# 

119 # handling of property values 

120 

121 @classmethod 

122 def _encode(cls, name, value, parameters=None, encode=1): 

123 """Encode values to icalendar property values. 

124 

125 :param name: Name of the property. 

126 :type name: string 

127 

128 :param value: Value of the property. Either of a basic Python type of 

129 any of the icalendar's own property types. 

130 :type value: Python native type or icalendar property type. 

131 

132 :param parameters: Property parameter dictionary for the value. Only 

133 available, if encode is set to True. 

134 :type parameters: Dictionary 

135 

136 :param encode: True, if the value should be encoded to one of 

137 icalendar's own property types (Fallback is "vText") 

138 or False, if not. 

139 :type encode: Boolean 

140 

141 :returns: icalendar property value 

142 """ 

143 if not encode: 

144 return value 

145 if isinstance(value, cls.types_factory.all_types): 

146 # Don't encode already encoded values. 

147 obj = value 

148 else: 

149 # Extract VALUE parameter if present, or infer it from the Python type 

150 value_param = None 

151 if parameters and "VALUE" in parameters: 

152 value_param = parameters["VALUE"] 

153 elif not isinstance(value, cls.types_factory.all_types): 

154 inferred = cls._infer_value_type(value) 

155 if inferred: 

156 value_param = inferred 

157 # Auto-set the VALUE parameter 

158 if parameters is None: 

159 parameters = {} 

160 if "VALUE" not in parameters: 

161 parameters["VALUE"] = inferred 

162 

163 klass = cls.types_factory.for_property(name, value_param) 

164 obj = klass(value) 

165 if parameters: 

166 if not hasattr(obj, "params"): 

167 obj.params = Parameters() 

168 for key, item in parameters.items(): 

169 if item is None: 

170 if key in obj.params: 

171 del obj.params[key] 

172 else: 

173 obj.params[key] = item 

174 return obj 

175 

176 def add( 

177 self, 

178 name: str, 

179 value, 

180 parameters: dict[str, str] | Parameters = None, 

181 encode: bool = True, # noqa: FBT001 

182 ): 

183 """Add a property. 

184 

185 :param name: Name of the property. 

186 :type name: string 

187 

188 :param value: Value of the property. Either of a basic Python type of 

189 any of the icalendar's own property types. 

190 :type value: Python native type or icalendar property type. 

191 

192 :param parameters: Property parameter dictionary for the value. Only 

193 available, if encode is set to True. 

194 :type parameters: Dictionary 

195 

196 :param encode: True, if the value should be encoded to one of 

197 icalendar's own property types (Fallback is "vText") 

198 or False, if not. 

199 :type encode: Boolean 

200 

201 :returns: None 

202 """ 

203 if isinstance(value, datetime) and name.lower() in ( 

204 "dtstamp", 

205 "created", 

206 "last-modified", 

207 ): 

208 # RFC expects UTC for those... force value conversion. 

209 value = tzp.localize_utc(value) 

210 

211 # encode value 

212 if ( 

213 encode 

214 and isinstance(value, list) 

215 and name.lower() not in ["rdate", "exdate", "categories"] 

216 ): 

217 # Individually convert each value to an ical type except rdate and 

218 # exdate, where lists of dates might be passed to vDDDLists. 

219 value = [self._encode(name, v, parameters, encode) for v in value] 

220 else: 

221 value = self._encode(name, value, parameters, encode) 

222 

223 # set value 

224 if name in self: 

225 # If property already exists, append it. 

226 oldval = self[name] 

227 if isinstance(oldval, list): 

228 if isinstance(value, list): 

229 value = oldval + value 

230 else: 

231 oldval.append(value) 

232 value = oldval 

233 else: 

234 value = [oldval, value] 

235 self[name] = value 

236 

237 def _decode(self, name, value): 

238 """Internal for decoding property values.""" 

239 

240 # TODO: Currently the decoded method calls the icalendar.prop instances 

241 # from_ical. We probably want to decode properties into Python native 

242 # types here. But when parsing from an ical string with from_ical, we 

243 # want to encode the string into a real icalendar.prop property. 

244 if isinstance(value, vDDDLists): 

245 # TODO: Workaround unfinished decoding 

246 return value 

247 decoded = self.types_factory.from_ical(name, value) 

248 # TODO: remove when proper decoded is implemented in every prop.* class 

249 # Workaround to decode vText properly 

250 if isinstance(decoded, vText): 

251 decoded = decoded.encode(DEFAULT_ENCODING) 

252 return decoded 

253 

254 def decoded(self, name, default=_marker): 

255 """Returns decoded value of property.""" 

256 # XXX: fail. what's this function supposed to do in the end? 

257 # -rnix 

258 

259 if name in self: 

260 value = self[name] 

261 if isinstance(value, list): 

262 return [self._decode(name, v) for v in value] 

263 return self._decode(name, value) 

264 if default is _marker: 

265 raise KeyError(name) 

266 return default 

267 

268 ######################################################################## 

269 # Inline values. A few properties have multiple values inlined in in one 

270 # property line. These methods are used for splitting and joining these. 

271 

272 def get_inline(self, name, decode=1): 

273 """Returns a list of values (split on comma).""" 

274 vals = [v.strip('" ') for v in q_split(self[name])] 

275 if decode: 

276 return [self._decode(name, val) for val in vals] 

277 return vals 

278 

279 def set_inline(self, name, values, encode=1): 

280 """Converts a list of values into comma separated string and sets value 

281 to that. 

282 """ 

283 if encode: 

284 values = [self._encode(name, value, encode=1) for value in values] 

285 self[name] = self.types_factory["inline"](q_join(values)) 

286 

287 ######################### 

288 # Handling of components 

289 

290 def add_component(self, component: Component): 

291 """Add a subcomponent to this component.""" 

292 self.subcomponents.append(component) 

293 

294 def _walk(self, name, select): 

295 """Walk to given component.""" 

296 result = [] 

297 if (name is None or self.name == name) and select(self): 

298 result.append(self) 

299 for subcomponent in self.subcomponents: 

300 result += subcomponent._walk(name, select) # noqa: SLF001 

301 return result 

302 

303 def walk(self, name=None, select=lambda _: True) -> list[Component]: 

304 """Recursively traverses component and subcomponents. Returns sequence 

305 of same. If name is passed, only components with name will be returned. 

306 

307 :param name: The name of the component or None such as ``VEVENT``. 

308 :param select: A function that takes the component as first argument 

309 and returns True/False. 

310 :returns: A list of components that match. 

311 :rtype: list[Component] 

312 """ 

313 if name is not None: 

314 name = name.upper() 

315 return self._walk(name, select) 

316 

317 ##################### 

318 # Generation 

319 

320 def property_items( 

321 self, 

322 recursive=True, 

323 sorted: bool = True, # noqa: A002, FBT001 

324 ) -> list[tuple[str, object]]: 

325 """Returns properties in this component and subcomponents as: 

326 [(name, value), ...] 

327 """ 

328 v_text = self.types_factory["text"] 

329 properties = [("BEGIN", v_text(self.name).to_ical())] 

330 property_names = self.sorted_keys() if sorted else self.keys() 

331 

332 for name in property_names: 

333 values = self[name] 

334 if isinstance(values, list): 

335 # normally one property is one line 

336 for value in values: 

337 properties.append((name, value)) 

338 else: 

339 properties.append((name, values)) 

340 if recursive: 

341 # recursion is fun! 

342 for subcomponent in self.subcomponents: 

343 properties += subcomponent.property_items(sorted=sorted) 

344 properties.append(("END", v_text(self.name).to_ical())) 

345 return properties 

346 

347 @classmethod 

348 def from_ical(cls, st, multiple: bool = False) -> Self | list[Self]: # noqa: FBT001 

349 """Populates the component recursively from a string.""" 

350 stack = [] # a stack of components 

351 comps = [] 

352 for line in Contentlines.from_ical(st): # raw parsing 

353 if not line: 

354 continue 

355 

356 try: 

357 name, params, vals = line.parts() 

358 except ValueError as e: 

359 # if unable to parse a line within a component 

360 # that ignores exceptions, mark the component 

361 # as broken and skip the line. otherwise raise. 

362 component = stack[-1] if stack else None 

363 if not component or not component.ignore_exceptions: 

364 raise 

365 component.errors.append((None, str(e))) 

366 continue 

367 

368 uname = name.upper() 

369 # check for start of component 

370 if uname == "BEGIN": 

371 # try and create one of the components defined in the spec, 

372 # otherwise get a general Components for robustness. 

373 c_name = vals.upper() 

374 c_class = cls.get_component_class(c_name) 

375 # If component factory cannot resolve ``c_name``, the generic 

376 # ``Component`` class is used which does not have the name set. 

377 # That's opposed to the usage of ``cls``, which represents a 

378 # more concrete subclass with a name set (e.g. VCALENDAR). 

379 component = c_class() 

380 if not getattr(component, "name", ""): # undefined components 

381 component.name = c_name 

382 stack.append(component) 

383 # check for end of event 

384 elif uname == "END": 

385 # we are done adding properties to this component 

386 # so pop it from the stack and add it to the new top. 

387 if not stack: 

388 # The stack is currently empty, the input must be invalid 

389 raise ValueError("END encountered without an accompanying BEGIN!") 

390 

391 component = stack.pop() 

392 if not stack: # we are at the end 

393 comps.append(component) 

394 else: 

395 stack[-1].add_component(component) 

396 if vals == "VTIMEZONE" and "TZID" in component: 

397 tzp.cache_timezone_component(component) 

398 # we are adding properties to the current top of the stack 

399 else: 

400 # Extract VALUE parameter if present 

401 value_param = params.get("VALUE") if params else None 

402 factory = cls.types_factory.for_property(name, value_param) 

403 component = stack[-1] if stack else None 

404 if not component: 

405 # only accept X-COMMENT at the end of the .ics file 

406 # ignore these components in parsing 

407 if uname == "X-COMMENT": 

408 break 

409 raise ValueError( 

410 f'Property "{name}" does not have a parent component.' 

411 ) 

412 datetime_names = ( 

413 "DTSTART", 

414 "DTEND", 

415 "RECURRENCE-ID", 

416 "DUE", 

417 "RDATE", 

418 "EXDATE", 

419 ) 

420 try: 

421 if name == "FREEBUSY": 

422 vals = vals.split(",") 

423 if "TZID" in params: 

424 parsed_components = [ 

425 factory(factory.from_ical(val, params["TZID"])) 

426 for val in vals 

427 ] 

428 else: 

429 parsed_components = [ 

430 factory(factory.from_ical(val)) for val in vals 

431 ] 

432 elif name in datetime_names and "TZID" in params: 

433 parsed_components = [ 

434 factory(factory.from_ical(vals, params["TZID"])) 

435 ] 

436 # Workaround broken ICS files with empty RDATE 

437 elif name == "RDATE" and vals == "": 

438 parsed_components = [] 

439 else: 

440 parsed_components = [factory(factory.from_ical(vals))] 

441 except ValueError as e: 

442 if not component.ignore_exceptions: 

443 raise 

444 component.errors.append((uname, str(e))) 

445 else: 

446 for parsed_component in parsed_components: 

447 parsed_component.params = params 

448 component.add(name, parsed_component, encode=0) 

449 

450 if multiple: 

451 return comps 

452 if len(comps) > 1: 

453 raise ValueError( 

454 cls._format_error( 

455 "Found multiple components where only one is allowed", st 

456 ) 

457 ) 

458 if len(comps) < 1: 

459 raise ValueError( 

460 cls._format_error( 

461 "Found no components where exactly one is required", st 

462 ) 

463 ) 

464 return comps[0] 

465 

466 @staticmethod 

467 def _format_error(error_description, bad_input, elipsis="[...]"): 

468 # there's three character more in the error, ie. ' ' x2 and a ':' 

469 max_error_length = 100 - 3 

470 if len(error_description) + len(bad_input) + len(elipsis) > max_error_length: 

471 truncate_to = max_error_length - len(error_description) - len(elipsis) 

472 return f"{error_description}: {bad_input[:truncate_to]} {elipsis}" 

473 return f"{error_description}: {bad_input}" 

474 

475 def content_line(self, name, value, sorted: bool = True): # noqa: A002, FBT001 

476 """Returns property as content line.""" 

477 params = getattr(value, "params", Parameters()) 

478 return Contentline.from_parts(name, params, value, sorted=sorted) 

479 

480 def content_lines(self, sorted: bool = True): # noqa: A002, FBT001 

481 """Converts the Component and subcomponents into content lines.""" 

482 contentlines = Contentlines() 

483 for name, value in self.property_items(sorted=sorted): 

484 cl = self.content_line(name, value, sorted=sorted) 

485 contentlines.append(cl) 

486 contentlines.append("") # remember the empty string in the end 

487 return contentlines 

488 

489 def to_ical(self, sorted: bool = True): # noqa: A002, FBT001 

490 """ 

491 :param sorted: Whether parameters and properties should be 

492 lexicographically sorted. 

493 """ 

494 

495 content_lines = self.content_lines(sorted=sorted) 

496 return content_lines.to_ical() 

497 

498 def __repr__(self): 

499 """String representation of class with all of it's subcomponents.""" 

500 subs = ", ".join(str(it) for it in self.subcomponents) 

501 return ( 

502 f"{self.name or type(self).__name__}" 

503 f"({dict(self)}{', ' + subs if subs else ''})" 

504 ) 

505 

506 def __eq__(self, other): 

507 if len(self.subcomponents) != len(other.subcomponents): 

508 return False 

509 

510 properties_equal = super().__eq__(other) 

511 if not properties_equal: 

512 return False 

513 

514 # The subcomponents might not be in the same order, 

515 # neither there's a natural key we can sort the subcomponents by nor 

516 # are the subcomponent types hashable, so we cant put them in a set to 

517 # check for set equivalence. We have to iterate over the subcomponents 

518 # and look for each of them in the list. 

519 for subcomponent in self.subcomponents: 

520 if subcomponent not in other.subcomponents: 

521 return False 

522 

523 return True 

524 

525 DTSTAMP = stamp = single_utc_property( 

526 "DTSTAMP", 

527 """RFC 5545: 

528 

529 Conformance: This property MUST be included in the "VEVENT", 

530 "VTODO", "VJOURNAL", or "VFREEBUSY" calendar components. 

531 

532 Description: In the case of an iCalendar object that specifies a 

533 "METHOD" property, this property specifies the date and time that 

534 the instance of the iCalendar object was created. In the case of 

535 an iCalendar object that doesn't specify a "METHOD" property, this 

536 property specifies the date and time that the information 

537 associated with the calendar component was last revised in the 

538 calendar store. 

539 

540 The value MUST be specified in the UTC time format. 

541 

542 In the case of an iCalendar object that doesn't specify a "METHOD" 

543 property, this property is equivalent to the "LAST-MODIFIED" 

544 property. 

545 """, 

546 ) 

547 LAST_MODIFIED = single_utc_property( 

548 "LAST-MODIFIED", 

549 """RFC 5545: 

550 

551 Purpose: This property specifies the date and time that the 

552 information associated with the calendar component was last 

553 revised in the calendar store. 

554 

555 Note: This is analogous to the modification date and time for a 

556 file in the file system. 

557 

558 Conformance: This property can be specified in the "VEVENT", 

559 "VTODO", "VJOURNAL", or "VTIMEZONE" calendar components. 

560 """, 

561 ) 

562 

563 @property 

564 def last_modified(self) -> datetime: 

565 """Datetime when the information associated with the component was last revised. 

566 

567 Since :attr:`LAST_MODIFIED` is an optional property, 

568 this returns :attr:`DTSTAMP` if :attr:`LAST_MODIFIED` is not set. 

569 """ 

570 return self.LAST_MODIFIED or self.DTSTAMP 

571 

572 @last_modified.setter 

573 def last_modified(self, value): 

574 self.LAST_MODIFIED = value 

575 

576 @last_modified.deleter 

577 def last_modified(self): 

578 del self.LAST_MODIFIED 

579 

580 @property 

581 def created(self) -> datetime: 

582 """Datetime when the information associated with the component was created. 

583 

584 Since :attr:`CREATED` is an optional property, 

585 this returns :attr:`DTSTAMP` if :attr:`CREATED` is not set. 

586 """ 

587 return self.CREATED or self.DTSTAMP 

588 

589 @created.setter 

590 def created(self, value): 

591 self.CREATED = value 

592 

593 @created.deleter 

594 def created(self): 

595 del self.CREATED 

596 

597 def is_thunderbird(self) -> bool: 

598 """Whether this component has attributes that indicate that Mozilla Thunderbird created it.""" # noqa: E501 

599 return any(attr.startswith("X-MOZ-") for attr in self.keys()) 

600 

601 @staticmethod 

602 def _utc_now(): 

603 """Return now as UTC value.""" 

604 return datetime.now(timezone.utc) 

605 

606 uid = uid_property 

607 comments = comments_property 

608 

609 CREATED = single_utc_property( 

610 "CREATED", 

611 """ 

612 CREATED specifies the date and time that the calendar 

613 information was created by the calendar user agent in the calendar 

614 store. 

615 

616 Conformance: 

617 The property can be specified once in "VEVENT", 

618 "VTODO", or "VJOURNAL" calendar components. The value MUST be 

619 specified as a date with UTC time. 

620 

621 """, 

622 ) 

623 

624 _validate_new = True 

625 

626 @staticmethod 

627 def _validate_start_and_end(start, end): 

628 """This validates start and end. 

629 

630 Raises: 

631 InvalidCalendar: If the information is not valid 

632 """ 

633 if start is None or end is None: 

634 return 

635 if start > end: 

636 raise InvalidCalendar("end must be after start") 

637 

638 @classmethod 

639 def new( 

640 cls, 

641 created: date | None = None, 

642 comments: list[str] | str | None = None, 

643 last_modified: date | None = None, 

644 stamp: date | None = None, 

645 ) -> Component: 

646 """Create a new component. 

647 

648 Arguments: 

649 comments: The :attr:`comments` of the component. 

650 created: The :attr:`created` of the component. 

651 last_modified: The :attr:`last_modified` of the component. 

652 stamp: The :attr:`DTSTAMP` of the component. 

653 

654 Raises: 

655 InvalidCalendar: If the content is not valid according to :rfc:`5545`. 

656 

657 .. warning:: As time progresses, we will be stricter with the validation. 

658 """ 

659 component = cls() 

660 component.DTSTAMP = stamp 

661 component.created = created 

662 component.last_modified = last_modified 

663 component.comments = comments 

664 return component 

665 

666 

667__all__ = ["Component"]