imip-agent

imipweb/data.py

1272:65e999dd88f0
2017-09-18 Paul Boddie Added a convenience method for loading objects. Added docstrings. client-editing-simplification
     1 #!/usr/bin/env python     2      3 """     4 Web 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     def as_tuple(self):    75         return self.start, self.end, self.tzid, self.origin, self.start_attr, \    76                self.end_attr, self.form_start, self.form_end, self.replacement, \    77                self.cancelled, self.recurrenceid    78     79     def __repr__(self):    80         return "EventPeriod%r" % (self.as_tuple(),)    81     82     def copy(self):    83         return EventPeriod(*self.as_tuple())    84     85     def _get_recurrenceid_item(self):    86     87         # Convert any stored identifier to the current time zone.    88         # NOTE: This should not be necessary, but is done for consistency with    89         # NOTE: the datetime properties.    90     91         dt = get_datetime(self.recurrenceid)    92         dt = to_timezone(dt, self.tzid)    93         return dt, get_datetime_attributes(dt)    94     95     def get_recurrenceid(self):    96         if not self.recurrenceid:    97             return RecurringPeriod.get_recurrenceid(self)    98         return self.recurrenceid    99    100     def get_recurrenceid_item(self):   101         if not self.recurrenceid:   102             return RecurringPeriod.get_recurrenceid_item(self)   103         return self._get_recurrenceid_item()   104    105     def as_event_period(self):   106         return self   107    108     def get_start_item(self):   109         return self.get_start(), self.get_start_attr()   110    111     def get_end_item(self):   112         return self.get_end(), self.get_end_attr()   113    114     # Form data compatibility methods.   115    116     def get_form_start(self):   117         if not self.form_start:   118             self.form_start = self.get_form_date(self.get_start(), self.start_attr)   119         return self.form_start   120    121     def get_form_end(self):   122         if not self.form_end:   123             self.form_end = self.get_form_date(end_date_from_calendar(self.get_end()), self.end_attr)   124         return self.form_end   125    126     def as_form_period(self):   127         return FormPeriod(   128             self.get_form_start(),   129             self.get_form_end(),   130             isinstance(self.end, datetime) or self.get_start() != self.get_end() - timedelta(1),   131             isinstance(self.start, datetime) or isinstance(self.end, datetime),   132             self.tzid,   133             self.origin,   134             self.replacement,   135             self.cancelled,   136             self.recurrenceid   137             )   138    139     def get_form_date(self, dt, attr=None):   140         return FormDate(   141             format_datetime(to_date(dt)),   142             isinstance(dt, datetime) and str(dt.hour) or None,   143             isinstance(dt, datetime) and str(dt.minute) or None,   144             isinstance(dt, datetime) and str(dt.second) or None,   145             attr and attr.get("TZID") or None,   146             dt, attr   147             )   148    149 class FormPeriod(RecurringPeriod):   150    151     "A period whose information originates from a form."   152    153     def __init__(self, start, end, end_enabled=True, times_enabled=True,   154                  tzid=None, origin=None, replacement=False, cancelled=False,   155                  recurrenceid=None):   156         self.start = start   157         self.end = end   158         self.end_enabled = end_enabled   159         self.times_enabled = times_enabled   160         self.tzid = tzid   161         self.origin = origin   162         self.replacement = replacement   163         self.cancelled = cancelled   164         self.recurrenceid = recurrenceid   165    166     def as_tuple(self):   167         return self.start, self.end, self.end_enabled, self.times_enabled, \   168                self.tzid, self.origin, self.replacement, self.cancelled, \   169                self.recurrenceid   170    171     def __repr__(self):   172         return "FormPeriod%r" % (self.as_tuple(),)   173    174     def copy(self):   175         return FormPeriod(*self.as_tuple())   176    177     def as_event_period(self, index=None):   178    179         """   180         Return a converted version of this object as an event period suitable   181         for iCalendar usage. If 'index' is indicated, include it in any error   182         raised in the conversion process.   183         """   184    185         dtstart, dtstart_attr = self.get_start_item()   186         if not dtstart:   187             if index is not None:   188                 raise PeriodError(("dtstart", index))   189             else:   190                 raise PeriodError("dtstart")   191    192         dtend, dtend_attr = self.get_end_item()   193         if not dtend:   194             if index is not None:   195                 raise PeriodError(("dtend", index))   196             else:   197                 raise PeriodError("dtend")   198    199         if dtstart > dtend:   200             if index is not None:   201                 raise PeriodError(("dtstart", index), ("dtend", index))   202             else:   203                 raise PeriodError("dtstart", "dtend")   204    205         return EventPeriod(dtstart, end_date_to_calendar(dtend), self.tzid,   206                            self.origin, dtstart_attr, dtend_attr,   207                            self.start, self.end, self.replacement,   208                            self.cancelled, self.recurrenceid)   209    210     # Period data methods.   211    212     def get_start(self):   213         return self.start and self.start.as_datetime(self.times_enabled) or None   214    215     def get_end(self):   216    217         # Handle specified end datetimes.   218    219         if self.end_enabled:   220             dtend = self.end.as_datetime(self.times_enabled)   221             if not dtend:   222                 return None   223    224         # Handle same day times.   225    226         elif self.times_enabled:   227             formdate = FormDate(self.start.date, self.end.hour, self.end.minute, self.end.second, self.end.tzid)   228             dtend = formdate.as_datetime(self.times_enabled)   229             if not dtend:   230                 return None   231    232         # Otherwise, treat the end date as the start date. Datetimes are   233         # handled by making the event occupy the rest of the day.   234    235         else:   236             dtstart, dtstart_attr = self.get_start_item()   237             if dtstart:   238                 if isinstance(dtstart, datetime):   239                     dtend = get_end_of_day(dtstart, dtstart_attr["TZID"])   240                 else:   241                     dtend = dtstart   242             else:   243                 return None   244    245         return dtend   246    247     def get_start_attr(self):   248         return self.start and self.start.get_attributes(self.times_enabled) or {}   249    250     def get_end_attr(self):   251         return self.end and self.end.get_attributes(self.times_enabled) or {}   252    253     # Form data methods.   254    255     def get_form_start(self):   256         return self.start   257    258     def get_form_end(self):   259         return self.end   260    261     def as_form_period(self):   262         return self   263    264 class FormDate:   265    266     "Date information originating from form information."   267    268     def __init__(self, date=None, hour=None, minute=None, second=None, tzid=None, dt=None, attr=None):   269         self.date = date   270         self.hour = hour   271         self.minute = minute   272         self.second = second   273         self.tzid = tzid   274         self.dt = dt   275         self.attr = attr   276    277     def as_tuple(self):   278         return self.date, self.hour, self.minute, self.second, self.tzid, self.dt, self.attr   279    280     def reset(self):   281         self.dt = None   282    283     def __repr__(self):   284         return "FormDate%r" % (self.as_tuple(),)   285    286     def get_component(self, value):   287         return (value or "").rjust(2, "0")[:2]   288    289     def get_hour(self):   290         return self.get_component(self.hour)   291    292     def get_minute(self):   293         return self.get_component(self.minute)   294    295     def get_second(self):   296         return self.get_component(self.second)   297    298     def get_date_string(self):   299         return self.date or ""   300    301     def get_datetime_string(self):   302         if not self.date:   303             return ""   304    305         hour = self.hour; minute = self.minute; second = self.second   306    307         if hour or minute or second:   308             time = "T%s%s%s" % tuple(map(self.get_component, (hour, minute, second)))   309         else:   310             time = ""   311                312         return "%s%s" % (self.date, time)   313    314     def get_tzid(self):   315         return self.tzid   316    317     def as_datetime(self, with_time=True):   318    319         "Return a datetime for this object."   320    321         # Return any original datetime details.   322    323         if self.dt:   324             return self.dt   325    326         # Otherwise, construct a datetime.   327    328         s, attr = self.as_datetime_item(with_time)   329         if s:   330             return get_datetime(s, attr)   331         else:   332             return None   333    334     def as_datetime_item(self, with_time=True):   335    336         """   337         Return a (datetime string, attr) tuple for the datetime information   338         provided by this object, where both tuple elements will be None if no   339         suitable date or datetime information exists.   340         """   341    342         s = None   343         if with_time:   344             s = self.get_datetime_string()   345             attr = self.get_attributes(True)   346         if not s:   347             s = self.get_date_string()   348             attr = self.get_attributes(False)   349         if not s:   350             return None, None   351         return s, attr   352    353     def get_attributes(self, with_time=True):   354    355         "Return attributes for the date or datetime represented by this object."   356    357         if with_time:   358             return {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"}   359         else:   360             return {"VALUE" : "DATE"}   361    362 def event_period_from_period(period):   363    364     """   365     Convert a 'period' to one suitable for use in an iCalendar representation.   366     In an "event period" representation, the end day of any date-level event is   367     encoded as the "day after" the last day actually involved in the event.   368     """   369    370     if isinstance(period, EventPeriod):   371         return period   372     elif isinstance(period, FormPeriod):   373         return period.as_event_period()   374     else:   375         dtstart, dtstart_attr = period.get_start_item()   376         dtend, dtend_attr = period.get_end_item()   377    378         if not isinstance(period, RecurringPeriod):   379             dtend = end_date_to_calendar(dtend)   380    381         return EventPeriod(dtstart, dtend, period.tzid, period.origin,   382                            dtstart_attr, dtend_attr,   383                            recurrenceid=format_datetime(to_utc_datetime(dtstart)))   384    385 def event_periods_from_periods(periods):   386     return map(event_period_from_period, periods)   387    388 def form_period_from_period(period):   389    390     """   391     Convert a 'period' into a representation usable in a user-editable form.   392     In a "form period" representation, the end day of any date-level event is   393     presented in a "natural" form, not the iCalendar "day after" form.   394     """   395    396     if isinstance(period, EventPeriod):   397         return period.as_form_period()   398     elif isinstance(period, FormPeriod):   399         return period   400     else:   401         return event_period_from_period(period).as_form_period()   402    403 def form_periods_from_periods(periods):   404     return map(form_period_from_period, periods)   405    406    407    408 # Event period processing.   409    410 def periods_from_updated_periods(updated_periods, fn):   411    412     """   413     Return periods from the given 'updated_periods' created using 'fn, setting   414     replacement, cancelled and recurrence identifier details.   415     """   416    417     periods = []   418    419     for sp, p in updated_periods:   420         if p:   421             period = fn(p)   422             if sp != p:   423                 period.replacement = True   424         else:   425             period = fn(sp)   426             period.replacement = True   427             period.cancelled = True   428    429         # Replace the recurrence identifier with that of the original period.   430    431         period.recurrenceid = sp.get_recurrenceid()   432         periods.append(period)   433    434     return periods   435    436 def event_periods_from_updated_periods(updated_periods):   437     return periods_from_updated_periods(updated_periods, event_period_from_period)   438    439 def form_periods_from_updated_periods(updated_periods):   440     return periods_from_updated_periods(updated_periods, form_period_from_period)   441    442 def get_main_period(periods):   443     for p in periods:   444         if p.origin == "DTSTART":   445             return p   446     return None   447    448 def get_recurrence_periods(periods):   449     l = []   450     for p in periods:   451         if p.origin != "DTSTART":   452             l.append(p)   453     return l   454    455 def periods_by_recurrence(periods):   456    457     """   458     Return a mapping from recurrence identifier to period for 'periods' along   459     with a collection of unmapped periods.   460     """   461    462     d = {}   463     new = []   464    465     for p in periods:   466         if not p.recurrenceid:   467             new.append(p)   468         else:   469             d[p.recurrenceid] = p   470    471     return d, new   472    473 def combine_periods(old, new):   474    475     "Combine 'old' and 'new' periods for comparison."   476    477     old_by_recurrenceid, _new_periods = periods_by_recurrence(old)   478     new_by_recurrenceid, new_periods = periods_by_recurrence(new)   479    480     combined = []   481    482     for recurrenceid, op in old_by_recurrenceid.items():   483         np = new_by_recurrenceid.get(recurrenceid)   484         if np and not np.cancelled:   485             combined.append((op, np))   486         else:   487             combined.append((op, None))   488    489     for np in new_periods:   490         combined.append((None, np))   491    492     return combined   493    494 def classify_periods(updated_periods):   495    496     """   497     Using the 'updated_periods', being a list of (stored, current) periods,   498     return a tuple containing collections of new, changed, unchanged and removed   499     periods.   500    501     Note that changed and unchanged indicate the presence or absence of   502     differences between the original event periods and the current periods, not   503     whether any editing operations have changed the periods.   504     """   505    506     new = []   507     changed = []   508     unchanged = []   509     removed = []   510    511     for sp, p in updated_periods:   512         if sp:   513             if not p or p.cancelled:   514                 removed.append(sp)   515             elif p != sp or p.replacement:   516                 changed.append(p)   517             else:   518                 unchanged.append(p)   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