1 #!/usr/bin/env python 2 3 """ 4 User interface data abstractions. 5 6 Copyright (C) 2014, 2015, 2017 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from datetime import datetime, timedelta 23 from imiptools.dates import end_date_from_calendar, end_date_to_calendar, \ 24 format_datetime, get_datetime, \ 25 get_datetime_attributes, get_end_of_day, \ 26 to_date, to_utc_datetime, to_timezone 27 from imiptools.period import RecurringPeriod 28 29 class PeriodError(Exception): 30 pass 31 32 class EventPeriod(RecurringPeriod): 33 34 """ 35 A simple period plus attribute details, compatible with RecurringPeriod, and 36 intended to represent information obtained from an iCalendar resource. 37 """ 38 39 def __init__(self, start, end, tzid=None, origin=None, start_attr=None, 40 end_attr=None, form_start=None, form_end=None, 41 replacement=False, cancelled=False, recurrenceid=None): 42 43 """ 44 Initialise a period with the given 'start' and 'end' datetimes. 45 46 The optional 'tzid' provides time zone information, and the optional 47 'origin' indicates the kind of period this object describes. 48 49 The optional 'start_attr' and 'end_attr' provide metadata for the start 50 and end datetimes respectively, and 'form_start' and 'form_end' are 51 values provided as textual input. 52 53 The 'replacement' flag indicates whether the period is provided by a 54 separate recurrence instance. 55 56 The 'cancelled' flag indicates whether a separate recurrence is 57 cancelled. 58 59 The 'recurrenceid' describes the original identity of the period, 60 regardless of whether it is separate or not. 61 """ 62 63 RecurringPeriod.__init__(self, start, end, tzid, origin, start_attr, end_attr) 64 self.form_start = form_start 65 self.form_end = form_end 66 67 # Information about whether a separate recurrence provides this period 68 # and the original period identity. 69 70 self.replacement = replacement 71 self.cancelled = cancelled 72 self.recurrenceid = recurrenceid 73 74 # Additional editing state. 75 76 self.new_replacement = False 77 78 def as_tuple(self): 79 return self.start, self.end, self.tzid, self.origin, self.start_attr, \ 80 self.end_attr, self.form_start, self.form_end, self.replacement, \ 81 self.cancelled, self.recurrenceid 82 83 def __repr__(self): 84 return "EventPeriod%r" % (self.as_tuple(),) 85 86 def copy(self): 87 return EventPeriod(*self.as_tuple()) 88 89 def _get_recurrenceid_item(self): 90 91 # Convert any stored identifier to the current time zone. 92 # NOTE: This should not be necessary, but is done for consistency with 93 # NOTE: the datetime properties. 94 95 dt = get_datetime(self.recurrenceid) 96 dt = to_timezone(dt, self.tzid) 97 return dt, get_datetime_attributes(dt) 98 99 def get_recurrenceid(self): 100 if not self.recurrenceid: 101 return RecurringPeriod.get_recurrenceid(self) 102 return self.recurrenceid 103 104 def get_recurrenceid_item(self): 105 if not self.recurrenceid: 106 return RecurringPeriod.get_recurrenceid_item(self) 107 return self._get_recurrenceid_item() 108 109 def as_event_period(self): 110 return self 111 112 def get_start_item(self): 113 return self.get_start(), self.get_start_attr() 114 115 def get_end_item(self): 116 return self.get_end(), self.get_end_attr() 117 118 # Form data compatibility methods. 119 120 def get_form_start(self): 121 if not self.form_start: 122 self.form_start = self.get_form_date(self.get_start(), self.start_attr) 123 return self.form_start 124 125 def get_form_end(self): 126 if not self.form_end: 127 self.form_end = self.get_form_date(end_date_from_calendar(self.get_end()), self.end_attr) 128 return self.form_end 129 130 def as_form_period(self): 131 return FormPeriod( 132 self.get_form_start(), 133 self.get_form_end(), 134 isinstance(self.end, datetime) or self.get_start() != self.get_end() - timedelta(1), 135 isinstance(self.start, datetime) or isinstance(self.end, datetime), 136 self.tzid, 137 self.origin, 138 self.replacement, 139 self.cancelled, 140 self.recurrenceid 141 ) 142 143 def get_form_date(self, dt, attr=None): 144 return FormDate( 145 format_datetime(to_date(dt)), 146 isinstance(dt, datetime) and str(dt.hour) or None, 147 isinstance(dt, datetime) and str(dt.minute) or None, 148 isinstance(dt, datetime) and str(dt.second) or None, 149 attr and attr.get("TZID") or None, 150 dt, attr 151 ) 152 153 class FormPeriod(RecurringPeriod): 154 155 "A period whose information originates from a form." 156 157 def __init__(self, start, end, end_enabled=True, times_enabled=True, 158 tzid=None, origin=None, replacement=False, cancelled=False, 159 recurrenceid=None): 160 self.start = start 161 self.end = end 162 self.end_enabled = end_enabled 163 self.times_enabled = times_enabled 164 self.tzid = tzid 165 self.origin = origin 166 self.replacement = replacement 167 self.cancelled = cancelled 168 self.recurrenceid = recurrenceid 169 self.new_replacement = False 170 171 def as_tuple(self): 172 return self.start, self.end, self.end_enabled, self.times_enabled, \ 173 self.tzid, self.origin, self.replacement, self.cancelled, \ 174 self.recurrenceid 175 176 def __repr__(self): 177 return "FormPeriod%r" % (self.as_tuple(),) 178 179 def copy(self): 180 return FormPeriod(*self.as_tuple()) 181 182 def as_event_period(self, index=None): 183 184 """ 185 Return a converted version of this object as an event period suitable 186 for iCalendar usage. If 'index' is indicated, include it in any error 187 raised in the conversion process. 188 """ 189 190 dtstart, dtstart_attr = self.get_start_item() 191 if not dtstart: 192 if index is not None: 193 raise PeriodError(("dtstart", index)) 194 else: 195 raise PeriodError("dtstart") 196 197 dtend, dtend_attr = self.get_end_item() 198 if not dtend: 199 if index is not None: 200 raise PeriodError(("dtend", index)) 201 else: 202 raise PeriodError("dtend") 203 204 if dtstart > dtend: 205 if index is not None: 206 raise PeriodError(("dtstart", index), ("dtend", index)) 207 else: 208 raise PeriodError("dtstart", "dtend") 209 210 return EventPeriod(dtstart, end_date_to_calendar(dtend), self.tzid, 211 self.origin, dtstart_attr, dtend_attr, 212 self.start, self.end, self.replacement, 213 self.cancelled, self.recurrenceid) 214 215 # Period data methods. 216 217 def get_start(self): 218 return self.start and self.start.as_datetime(self.times_enabled) or None 219 220 def get_end(self): 221 222 # Handle specified end datetimes. 223 224 if self.end_enabled: 225 dtend = self.end.as_datetime(self.times_enabled) 226 if not dtend: 227 return None 228 229 # Handle same day times. 230 231 elif self.times_enabled: 232 formdate = FormDate(self.start.date, self.end.hour, self.end.minute, self.end.second, self.end.tzid) 233 dtend = formdate.as_datetime(self.times_enabled) 234 if not dtend: 235 return None 236 237 # Otherwise, treat the end date as the start date. Datetimes are 238 # handled by making the event occupy the rest of the day. 239 240 else: 241 dtstart, dtstart_attr = self.get_start_item() 242 if dtstart: 243 if isinstance(dtstart, datetime): 244 dtend = get_end_of_day(dtstart, dtstart_attr["TZID"]) 245 else: 246 dtend = dtstart 247 else: 248 return None 249 250 return dtend 251 252 def get_start_attr(self): 253 return self.start and self.start.get_attributes(self.times_enabled) or {} 254 255 def get_end_attr(self): 256 return self.end and self.end.get_attributes(self.times_enabled) or {} 257 258 # Form data methods. 259 260 def get_form_start(self): 261 return self.start 262 263 def get_form_end(self): 264 return self.end 265 266 def as_form_period(self): 267 return self 268 269 class FormDate: 270 271 "Date information originating from form information." 272 273 def __init__(self, date=None, hour=None, minute=None, second=None, tzid=None, dt=None, attr=None): 274 self.date = date 275 self.hour = hour 276 self.minute = minute 277 self.second = second 278 self.tzid = tzid 279 self.dt = dt 280 self.attr = attr 281 282 def as_tuple(self): 283 return self.date, self.hour, self.minute, self.second, self.tzid, self.dt, self.attr 284 285 def reset(self): 286 self.dt = None 287 288 def __repr__(self): 289 return "FormDate%r" % (self.as_tuple(),) 290 291 def get_component(self, value): 292 return (value or "").rjust(2, "0")[:2] 293 294 def get_hour(self): 295 return self.get_component(self.hour) 296 297 def get_minute(self): 298 return self.get_component(self.minute) 299 300 def get_second(self): 301 return self.get_component(self.second) 302 303 def get_date_string(self): 304 return self.date or "" 305 306 def get_datetime_string(self): 307 if not self.date: 308 return "" 309 310 hour = self.hour; minute = self.minute; second = self.second 311 312 if hour or minute or second: 313 time = "T%s%s%s" % tuple(map(self.get_component, (hour, minute, second))) 314 else: 315 time = "" 316 317 return "%s%s" % (self.date, time) 318 319 def get_tzid(self): 320 return self.tzid 321 322 def as_datetime(self, with_time=True): 323 324 "Return a datetime for this object." 325 326 # Return any original datetime details. 327 328 if self.dt: 329 return self.dt 330 331 # Otherwise, construct a datetime. 332 333 s, attr = self.as_datetime_item(with_time) 334 if s: 335 return get_datetime(s, attr) 336 else: 337 return None 338 339 def as_datetime_item(self, with_time=True): 340 341 """ 342 Return a (datetime string, attr) tuple for the datetime information 343 provided by this object, where both tuple elements will be None if no 344 suitable date or datetime information exists. 345 """ 346 347 s = None 348 if with_time: 349 s = self.get_datetime_string() 350 attr = self.get_attributes(True) 351 if not s: 352 s = self.get_date_string() 353 attr = self.get_attributes(False) 354 if not s: 355 return None, None 356 return s, attr 357 358 def get_attributes(self, with_time=True): 359 360 "Return attributes for the date or datetime represented by this object." 361 362 if with_time: 363 return {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"} 364 else: 365 return {"VALUE" : "DATE"} 366 367 def event_period_from_period(period): 368 369 """ 370 Convert a 'period' to one suitable for use in an iCalendar representation. 371 In an "event period" representation, the end day of any date-level event is 372 encoded as the "day after" the last day actually involved in the event. 373 """ 374 375 if isinstance(period, EventPeriod): 376 return period 377 elif isinstance(period, FormPeriod): 378 return period.as_event_period() 379 else: 380 dtstart, dtstart_attr = period.get_start_item() 381 dtend, dtend_attr = period.get_end_item() 382 383 if not isinstance(period, RecurringPeriod): 384 dtend = end_date_to_calendar(dtend) 385 386 return EventPeriod(dtstart, dtend, period.tzid, period.origin, 387 dtstart_attr, dtend_attr, 388 recurrenceid=format_datetime(to_utc_datetime(dtstart))) 389 390 def event_periods_from_periods(periods): 391 return map(event_period_from_period, periods) 392 393 def form_period_from_period(period): 394 395 """ 396 Convert a 'period' into a representation usable in a user-editable form. 397 In a "form period" representation, the end day of any date-level event is 398 presented in a "natural" form, not the iCalendar "day after" form. 399 """ 400 401 if isinstance(period, EventPeriod): 402 return period.as_form_period() 403 elif isinstance(period, FormPeriod): 404 return period 405 else: 406 return event_period_from_period(period).as_form_period() 407 408 def form_periods_from_periods(periods): 409 return map(form_period_from_period, periods) 410 411 412 413 # Event period processing. 414 415 def periods_from_updated_periods(updated_periods, fn): 416 417 """ 418 Return periods from the given 'updated_periods' created using 'fn, setting 419 replacement, cancelled and recurrence identifier details. 420 """ 421 422 periods = [] 423 424 for sp, p in updated_periods: 425 if p: 426 period = fn(p) 427 if sp != p: 428 period.replacement = True 429 else: 430 period = fn(sp) 431 period.replacement = True 432 period.cancelled = True 433 434 # Replace the recurrence identifier with that of the original period. 435 436 period.recurrenceid = sp.get_recurrenceid() 437 periods.append(period) 438 439 return periods 440 441 def event_periods_from_updated_periods(updated_periods): 442 return periods_from_updated_periods(updated_periods, event_period_from_period) 443 444 def form_periods_from_updated_periods(updated_periods): 445 return periods_from_updated_periods(updated_periods, form_period_from_period) 446 447 def periods_by_recurrence(periods): 448 449 """ 450 Return a mapping from recurrence identifier to period for 'periods' along 451 with a collection of unmapped periods. 452 """ 453 454 d = {} 455 new = [] 456 457 for p in periods: 458 if not p.recurrenceid: 459 new.append(p) 460 else: 461 d[p.recurrenceid] = p 462 463 return d, new 464 465 def combine_periods(old, new): 466 467 "Combine 'old' and 'new' periods for comparison." 468 469 old_by_recurrenceid, _new_periods = periods_by_recurrence(old) 470 new_by_recurrenceid, new_periods = periods_by_recurrence(new) 471 472 combined = [] 473 474 for recurrenceid, op in old_by_recurrenceid.items(): 475 np = new_by_recurrenceid.get(recurrenceid) 476 if np and not np.cancelled: 477 combined.append((op, np)) 478 else: 479 combined.append((op, None)) 480 481 for np in new_periods: 482 combined.append((None, np)) 483 484 return combined 485 486 def classify_periods(updated_periods): 487 488 """ 489 Using the 'updated_periods', being a list of (stored, current) periods, 490 return a tuple containing collections of new, changed, unchanged and removed 491 periods. 492 493 Note that changed and unchanged indicate the presence or absence of 494 differences between the original event periods and the current periods, not 495 whether any editing operations have changed the periods. 496 """ 497 498 new = [] 499 changed = [] 500 unchanged = [] 501 removed = [] 502 503 for sp, p in updated_periods: 504 if sp: 505 if not p or p.cancelled: 506 removed.append(sp) 507 elif p != sp or p.replacement: 508 changed.append(p) 509 if not p.replacement: 510 p.new_replacement = True 511 else: 512 unchanged.append(p) 513 if p.new_replacement: 514 p.new_replacement = False 515 elif p: 516 new.append(p) 517 518 return new, changed, unchanged, removed 519 520 def classify_operations(new, changed, unchanged, removed, is_organiser, is_shared): 521 522 """ 523 Classify the operations for the update of an event. Return the unscheduled 524 periods, rescheduled periods, excluded periods, and the periods to be set in 525 the object to replace the existing stored periods. 526 """ 527 528 active_periods = new + unchanged + changed 529 530 # As organiser... 531 532 if is_organiser: 533 to_exclude = [] 534 535 # For unshared events... 536 # All modifications redefine the event. 537 538 # For shared events... 539 # New periods should cause the event to be redefined. 540 541 if not is_shared or new: 542 to_unschedule = [] 543 to_reschedule = [] 544 to_set = active_periods 545 546 # Changed periods should be rescheduled separately. 547 # Removed periods should be cancelled separately. 548 549 else: 550 to_unschedule = removed 551 to_reschedule = changed 552 to_set = [] 553 554 # As attendee... 555 556 else: 557 to_unschedule = [] 558 559 # Changed periods without new or removed periods are proposed as 560 # separate changes. 561 562 if not new and not removed: 563 to_exclude = [] 564 to_reschedule = changed 565 to_set = [] 566 567 # Otherwise, the event is defined in terms of new periods and 568 # exceptions for removed periods. 569 570 else: 571 to_exclude = removed 572 to_reschedule = [] 573 to_set = active_periods 574 575 return to_unschedule, to_reschedule, to_exclude, to_set 576 577 578 579 # Form field extraction and serialisation. 580 581 def get_date_control_inputs(args, name, tzid_name=None): 582 583 """ 584 Return a tuple of date control inputs taken from 'args' for field names 585 starting with 'name'. 586 587 If 'tzid_name' is specified, the time zone information will be acquired 588 from fields starting with 'tzid_name' instead of 'name'. 589 """ 590 591 return args.get("%s-date" % name, []), \ 592 args.get("%s-hour" % name, []), \ 593 args.get("%s-minute" % name, []), \ 594 args.get("%s-second" % name, []), \ 595 args.get("%s-tzid" % (tzid_name or name), []) 596 597 def get_date_control_values(args, name, multiple=False, tzid_name=None, tzid=None): 598 599 """ 600 Return a form date object representing fields taken from 'args' starting 601 with 'name'. 602 603 If 'multiple' is set to a true value, many date objects will be returned 604 corresponding to a collection of datetimes. 605 606 If 'tzid_name' is specified, the time zone information will be acquired 607 from fields starting with 'tzid_name' instead of 'name'. 608 609 If 'tzid' is specified, it will provide the time zone where no explicit 610 time zone information is indicated in the field data. 611 """ 612 613 dates, hours, minutes, seconds, tzids = get_date_control_inputs(args, name, tzid_name) 614 615 # Handle absent values by employing None values. 616 617 field_values = map(None, dates, hours, minutes, seconds, tzids) 618 619 if not field_values and not multiple: 620 all_values = FormDate() 621 else: 622 all_values = [] 623 for date, hour, minute, second, tzid_field in field_values: 624 value = FormDate(date, hour, minute, second, tzid_field or tzid) 625 626 # Return a single value or append to a collection of all values. 627 628 if not multiple: 629 return value 630 else: 631 all_values.append(value) 632 633 return all_values 634 635 def set_date_control_values(formdates, args, name, tzid_name=None): 636 637 """ 638 Using the values of the given 'formdates', replace form fields in 'args' 639 starting with 'name'. 640 641 If 'tzid_name' is specified, the time zone information will be stored in 642 fields starting with 'tzid_name' instead of 'name'. 643 """ 644 645 args["%s-date" % name] = [] 646 args["%s-hour" % name] = [] 647 args["%s-minute" % name] = [] 648 args["%s-second" % name] = [] 649 args["%s-tzid" % (tzid_name or name)] = [] 650 651 for d in formdates: 652 args["%s-date" % name].append(d and d.date or "") 653 args["%s-hour" % name].append(d and d.hour or "") 654 args["%s-minute" % name].append(d and d.minute or "") 655 args["%s-second" % name].append(d and d.second or "") 656 args["%s-tzid" % (tzid_name or name)].append(d and d.tzid or "") 657 658 def get_period_control_values(args, start_name, end_name, 659 end_enabled_name, times_enabled_name, 660 origin=None, origin_name=None, 661 replacement_name=None, cancelled_name=None, 662 recurrenceid_name=None, tzid=None): 663 664 """ 665 Return period values from fields found in 'args' prefixed with the given 666 'start_name' (for start dates), 'end_name' (for end dates), 667 'end_enabled_name' (to enable end dates for periods), 'times_enabled_name' 668 (to enable times for periods). 669 670 If 'origin' is specified, a single period with the given origin is 671 returned. If 'origin_name' is specified, fields containing the name will 672 provide origin information. 673 674 If specified, fields containing 'replacement_name' will indicate periods 675 provided by separate recurrences, fields containing 'cancelled_name' 676 will indicate periods that are replacements and cancelled, and fields 677 containing 'recurrenceid_name' will indicate periods that have existing 678 recurrence details from an event. 679 680 If 'tzid' is specified, it will provide the time zone where no explicit 681 time zone information is indicated in the field data. 682 """ 683 684 # Get the end datetime and time presence settings. 685 686 all_end_enabled = args.get(end_enabled_name, []) 687 all_times_enabled = args.get(times_enabled_name, []) 688 689 # Get the origins of period data and whether the periods are replacements. 690 691 if origin: 692 all_origins = [origin] 693 else: 694 all_origins = origin_name and args.get(origin_name, []) or [] 695 696 all_replacements = replacement_name and args.get(replacement_name, []) or [] 697 all_cancelled = cancelled_name and args.get(cancelled_name, []) or [] 698 all_recurrenceids = recurrenceid_name and args.get(recurrenceid_name, []) or [] 699 700 # Get the start and end datetimes. 701 702 all_starts = get_date_control_values(args, start_name, True, tzid=tzid) 703 all_ends = get_date_control_values(args, end_name, True, start_name, tzid=tzid) 704 705 # Construct period objects for each start, end, origin combination. 706 707 periods = [] 708 709 for index, (start, end, found_origin, recurrenceid) in \ 710 enumerate(map(None, all_starts, all_ends, all_origins, all_recurrenceids)): 711 712 # Obtain period settings from separate controls. 713 714 end_enabled = str(index) in all_end_enabled 715 times_enabled = str(index) in all_times_enabled 716 replacement = str(index) in all_replacements 717 cancelled = str(index) in all_cancelled 718 719 period = FormPeriod(start, end, end_enabled, times_enabled, tzid, 720 found_origin or origin, replacement, cancelled, 721 recurrenceid) 722 periods.append(period) 723 724 # Return a single period if a single origin was specified. 725 726 if origin: 727 return periods[0] 728 else: 729 return periods 730 731 def set_period_control_values(periods, args, start_name, end_name, 732 end_enabled_name, times_enabled_name, 733 origin_name=None, replacement_name=None, 734 cancelled_name=None, recurrenceid_name=None): 735 736 """ 737 Using the given 'periods', replace form fields in 'args' prefixed with the 738 given 'start_name' (for start dates), 'end_name' (for end dates), 739 'end_enabled_name' (to enable end dates for periods), 'times_enabled_name' 740 (to enable times for periods). 741 742 If 'origin_name' is specified, fields containing the name will provide 743 origin information, fields containing 'replacement_name' will indicate 744 periods provided by separate recurrences, fields containing 'cancelled_name' 745 will indicate periods that are replacements and cancelled, and fields 746 containing 'recurrenceid_name' will indicate periods that have existing 747 recurrence details from an event. 748 """ 749 750 # Record period settings separately. 751 752 args[end_enabled_name] = [] 753 args[times_enabled_name] = [] 754 755 # Record origin and replacement information if naming is defined. 756 757 if origin_name: 758 args[origin_name] = [] 759 760 if replacement_name: 761 args[replacement_name] = [] 762 763 if cancelled_name: 764 args[cancelled_name] = [] 765 766 if recurrenceid_name: 767 args[recurrenceid_name] = [] 768 769 all_starts = [] 770 all_ends = [] 771 772 for index, period in enumerate(periods): 773 774 # Encode period settings in controls. 775 776 if period.end_enabled: 777 args[end_enabled_name].append(str(index)) 778 if period.times_enabled: 779 args[times_enabled_name].append(str(index)) 780 781 # Add origin information where controls are present to record it. 782 783 if origin_name: 784 args[origin_name].append(period.origin or "") 785 786 # Add replacement information where controls are present to record it. 787 788 if replacement_name and period.replacement: 789 args[replacement_name].append(str(index)) 790 791 # Add cancelled recurrence information where controls are present to 792 # record it. 793 794 if cancelled_name and period.cancelled: 795 args[cancelled_name].append(str(index)) 796 797 # Add recurrence identifiers where controls are present to record it. 798 799 if recurrenceid_name: 800 args[recurrenceid_name].append(period.recurrenceid or "") 801 802 # Collect form date information for addition below. 803 804 all_starts.append(period.get_form_start()) 805 all_ends.append(period.get_form_end()) 806 807 # Set the controls for the dates. 808 809 set_date_control_values(all_starts, args, start_name) 810 set_date_control_values(all_ends, args, end_name, tzid_name=start_name) 811 812 813 814 # Utilities. 815 816 def filter_duplicates(l): 817 818 """ 819 Return collection 'l' filtered for duplicate values, retaining the given 820 element ordering. 821 """ 822 823 s = set() 824 f = [] 825 826 for value in l: 827 if value not in s: 828 s.add(value) 829 f.append(value) 830 831 return f 832 833 def remove_from_collection(l, indexes, fn): 834 835 """ 836 Remove from collection 'l' all values present at the given 'indexes' where 837 'fn' applied to each referenced value returns a true value. Values where 838 'fn' returns a false value are added to a list of deferred removals which is 839 returned. 840 """ 841 842 still_to_remove = [] 843 correction = 0 844 845 for i in indexes: 846 try: 847 i = int(i) - correction 848 value = l[i] 849 except (IndexError, ValueError): 850 continue 851 852 if fn(value): 853 del l[i] 854 correction += 1 855 else: 856 still_to_remove.append(value) 857 858 return still_to_remove 859 860 # vim: tabstop=4 expandtab shiftwidth=4