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