Ejemplo n.º 1
0
def update(
        table,
        dict_of_values,  # ------------- [ update ... [
        query_data=None,
        **kwargs):
    """
    Return a simple "UPDATE ..." SQL statement with placeholders
    and a values dict.

    Arguments:

      table -- name of the table (may contain the schema)
      dict_of_values -- a Python dict containing the new values
      query_data -- another dict with a disjunct set of keys
                    to restrict the rows changed.

    Keyword-only options:

      where -- a "WHERE ..." string (which may contain placeholders);
               currently necessary if some fields are both changed
               and used in the filtering `query_data`
      returning -- like supported by PostgreSQL 9.1+
      strict -- if False, unknown keyword arguments will be silently ignored;
                if True (default), a TypeError will be raised.
      fork -- Rarely specified: Whether to create a copy of the `query_data`
              before merging in the `dict_of_values`.
              Defaults to True.

      fork -- wenn <query_data> nach dem Methodenaufruf noch verwendet
              werden soll, muß intern eine Kopie angelegt werden

    >>> tup = update('the_table', {'status': 'done'},
    ...              {'id': [47, 11]})
    >>> tup[0]
    'UPDATE the_table SET status=:status WHERE id = ANY(:id);'
    >>> tup[1]
    {'status': 'done', 'id': [47, 11]}

    Die Funktion akzeptiert zwei Dictionary-Optionen, die oft zusammen verwendet werden;
    was passiert mit etwaigen Variablen?

    >>> values = {'status': 'success'}
    >>> query_data = {'id': [1, 2, 5]}
    >>> tup2 = update('the.table', values, query_data)
    >>> tup2[0]
    'UPDATE the.table SET status=:status WHERE id = ANY(:id);'
    >>> tup2[1]
    {'status': 'success', 'id': [1, 2, 5]}

    Beide Eingabe-Dictionarys sind unverändert:

    >>> values
    {'status': 'success'}
    >>> query_data
    {'id': [1, 2, 5]}

    Was ist, wenn ein Feldname sowohl im Filter als auch in den zugewiesenen Werten auftaucht?

    >>> update('the_table', {'status': 'done'}, {'status': None})
    Traceback (most recent call last):
    ...
    ValueError: key 'status' is both in query data (None) and update data ('done')!

    In solchen Fällen kann man sich helfen, indem man selbst einen where-String angibt:

    >>> tup3 = update('the_table', {'status': 'done'},
    ...               where='WHERE status is NULL')
    >>> tup3[0]
    'UPDATE the_table SET status=:status WHERE status is NULL;'
    >>> tup3[1]
    {'status': 'done'}

    >>> tup4 = update('the_table', {'status': 'done'}, {'status_old': 'in_progress'},
    ...               where='WHERE status = :status_old')
    >>> tup4[0]
    'UPDATE the_table SET status=:status WHERE status = :status_old;'
    >>> tup4[1]
    {'status': 'done', 'status_old': 'in_progress'}

    Table names are checked for invalid values:
    >>> update('evil.table;truncate table users', {'status': 'done'})[0]
    Traceback (most recent call last):
    ...
    ValueError: Invalid chars in 'evil.table;truncate table users': (' ', ';')

    >>> update('another.table', {'evil_field;drop database': 42})
    Traceback (most recent call last):
    ...
    ValueError: Invalid chars in 'evil_field;drop database': (' ', ';')

    The 2nd part of the returned 2-tuple will always be a dictionary,
    even if no query_data had been given:
    >>> update('the_table', {'status': 'done'})
    ('UPDATE the_table SET status=:status;', {'status': 'done'})

    (Well, as for the update function, this is not very surprising,
    since a call to it *without* any dictionary data doesn't make any sense.)

    For unknown keyword arguments, we get a TypeError:
    >>> update('the_table', {'status': 'done'}, unknown=42)
    Traceback (most recent call last):
    ...
    TypeError: Unknown option 'unknown' found!
    """
    keys = list(check_name(key) for key in dict_of_values.keys())
    qset = ', '.join([key + '=' + placeholder(key) for key in keys])
    query_l = [
        replace_names('UPDATE %(table)s SET', table=table),
        qset,
    ]
    if query_data is not None:
        if not isinstance(query_data, dict):
            raise ValueError(
                'query_data must be a dict; found %(query_data)r' % locals())
        query_keys = set(query_data.keys())
        value_keys = set(keys)
        keys_of_both = value_keys.intersection(query_keys)
        if keys_of_both:
            # Löschen aus Set während Iteration nicht erlaubt;
            # also iteration über "Kopie":
            for key in sorted(keys_of_both):
                u_val = dict_of_values[key]
                q_val = query_data[key]
                if u_val == q_val:
                    del dict_of_values[key]
                    keys_of_both.remove(key)
                else:
                    raise ValueError('key %(key)r is both'
                                     ' in query data (%(q_val)r)'
                                     ' and update data (%(u_val)r)!' %
                                     locals())
        if not dict_of_values:
            raise ValueError('Empty update data!')
        if keys_of_both:
            raise ValueError('intersection of value keys and '
                             'query keys (%(keys_of_both)s: '
                             'currently unsupported!' % locals())
        fork = kwargs.get('fork', True)
        if fork:
            query_data = dict(query_data)  # wg. Wiederverwendung!
    else:
        query_data = {}
        fork = kwargs.get('fork', False)
    check_kwargs(kwargs, allowed=set(['returning', 'where', 'fork', 'strict']))
    where = kwargs.get('where')
    if where and not isinstance(where, six_string_types):
        raise ValueError('where must be a string; found %(where)r' % locals())
    if query_data and not where:
        where = make_where_mask(query_data)

    if where:
        query_l.append(where)

    returning = kwargs.get('returning')
    if returning:
        query_l.append(make_returning_clause(returning))
    queries = [' '.join(query_l) + ';']
    query = ''.join(queries)
    # nicht alle "Query-Daten" dienen der Filterung (siehe oben, keys_of_both)
    query_data.update(dict_of_values)
    return query, query_data
Ejemplo n.º 2
0
def make_defaulttime_calculator(year=0, month=0, day=0,
                                nextmonth=False,
                                dateonly=True,
                                default_date_format='%Y-%m-%d',
                                **kwargs):
    """
    Gib eine Funktion zurück, die ein Standarddatum erzeugt, z. B. das
    Standard-Ablaufdatum einer TAN (--> @@tan.utils.default_expiration_date).

    By default the time.localtime function is used; you can force e.g.
    time.gmtime to be used by the keyword-only option "utc".
    We need this for testability:

    >>> kw = dict(utc=True)

    Preparation of test data:

    >>> heute=gmtime(1406808000.0)
    >>> heute
    time.struct_time(tm_year=2014, tm_mon=7, tm_mday=31, tm_hour=12, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=212, tm_isdst=0)
    >>> timegm(heute)
    1406808000

    Create a function f which adds at least one year and increases single
    days until the 1st day of a month is reached:

    >>> f = make_defaulttime_calculator(year=1, nextmonth=True, **kw)

    By default, the result is formatted according to the default_date_format:

    >>> f(today=heute)
    '2015-08-01'

    By specifying mask=None, you can get a number:

    >>> num = f(today=heute, mask=None)
    >>> num
    1438387200

    By default, the time part is set to 0:00:

    >>> gmtime(num)
    time.struct_time(tm_year=2015, tm_mon=8, tm_mday=1, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=5, tm_yday=213, tm_isdst=0)

    However, you can suppress this behaviour:

    >>> gmtime(f(today=heute, mask=None, dateonly=False))
    time.struct_time(tm_year=2015, tm_mon=8, tm_mday=1, tm_hour=12, tm_min=0, tm_sec=0, tm_wday=5, tm_yday=213, tm_isdst=0)

    Create a function f2 which adds 90 days:

    >>> f2 = make_defaulttime_calculator(day=90, nextmonth=False, **kw)
    >>> f2(today=heute)
    '2014-10-29'
    >>> f2(today=heute, mask=None)
    1414540800
    """
    pop = kwargs.pop
    utc = pop('utc', False)

    check_kwargs(kwargs)  # raises TypeError if necessary

    if utc:
        time_factory = gmtime
        time_to_secs = timegm
    else:
        time_factory = localtime
        time_to_secs = mktime

    def calc_date(mask='', today=None, dateonly=dateonly):
        if today is None:
            today = time_factory()
        elif isinstance(today, float):
            today = time_factory(today)
        timelist = list(today)
        if year or month or day or dateonly:
            if year:
                timelist[0] += year
            if month:
                timelist[1] += month
            if day:
                timelist[2] += day
            if dateonly:
                timelist[3:6] = [0, 0, 0]
        if nextmonth:
            timelist = list(time_factory(timegm(timelist)))
            if timelist[2] != 1:
                timelist[1] += 1
                timelist[2] = 1
        val = time_to_secs(timelist)
        if mask is None:
            return val
        else:
            return strftime(mask or default_date_format, time_factory(val))

    return calc_date
Ejemplo n.º 3
0
def query(clause, *args, **kwargs):  # [ ---------- [ query ... [
    r"""
    Prepare an arbitrary SQL statement

    Arguments:

      clause -- a string, containing the SQL code to transform
                (may contain any number of statements).
                May contain %(name)s placeholders.
                NOTE: This code is not parsed or checked in any way;
                      don't use code from untrusted sources,
                      most notably: user input!
      names -- a dictionary of *names* which are
               replaced by this function
      query_data -- a dictionary which is used by the database adapter
                    to replace values.

    >>> txt = 'SELECT t1.user_id, t1.name, t2.address ' \
    ...         'FROM %(table1)s t1 ' \
    ...         'JOIN %(table2)s t2 ' \
    ...           'ON t1.user_id = t2.user_id ' \
    ...        "WHERE t1.name ILIKE %(filter)s;"
    >>> query(txt,
    ...       {'table1': 'users', 'table2': 'addresses'},
    ...       {'filter': '%beeblebrox%'})
    ('SELECT t1.user_id, t1.name, t2.address FROM users t1 JOIN addresses t2 ON t1.user_id = t2.user_id WHERE t1.name ILIKE :filter;', {'filter': '%beeblebrox%'})

    The placeholders for the values are not directly replaced by the values
    (which is the task of the database adapter)
    but transformed to the requested convention
    (which is :name in the case of SQLAlchemy).

    The difference is important: names and values are quoted differently
    in SQL; for values, single quotes are used ('), and double quotes (")
    for names.  Thus, if you don't treat the names specially, you'll get
    something which the database server will very likely reject:

    >>> query(txt,
    ...       query_data={  # BAD EXAMPLE!
    ...        'table1': 'users', 'table2': 'addresses',
    ...        'filter': '%beeblebrox%'})
    ('SELECT t1.user_id, t1.name, t2.address FROM :table1 t1 JOIN :table2 t2 ON t1.user_id = t2.user_id WHERE t1.name ILIKE :filter;', {'table2': 'addresses', 'filter': '%beeblebrox%', 'table1': 'users'})

    The database adapter will probably replace the :table1 string by
    'users', but here a name is expected,
    and this must be double quoted ("users"); thus,
    a standards-complying SQL server will reject this.

    If you have your names replaced already, you can easily skip the names
    argument, of course:

    >>> txt = 'SELECT t1.user_id, t1.name, t2.address ' \
    ...         'FROM users t1 ' \
    ...         'JOIN addresses t2 ' \
    ...           'ON t1.user_id = t2.user_id ' \
    ...        "WHERE t1.name ILIKE %(filter)s;"
    >>> query(txt,
    ...       None,
    ...       {'filter': '%beeblebrox%'})
    ('SELECT t1.user_id, t1.name, t2.address FROM users t1 JOIN addresses t2 ON t1.user_id = t2.user_id WHERE t1.name ILIKE :filter;', {'filter': '%beeblebrox%'})

    In some situations the aliases (t1, t2 in the above example) won't work,
    e.g. when using GROUP BY and HAVING;
    you can avoid them by using the names dictionary:

    >>> txt = 'SELECT %(table1)s.user_id, %(table1)s.name, %(table2)s.address' \
    ...        ' FROM %(table1)s ' \
    ...         'JOIN %(table2)s ' \
    ...           'ON %(table1)s.user_id = %(table2)s.user_id ' \
    ...        "WHERE %(table1)s.name ILIKE %(filter)s;"
    >>> query(txt,
    ...       {'table1': 'users', 'table2': 'addresses'},
    ...       {'filter': '%beeblebrox%'})[0]
    'SELECT users.user_id, users.name, addresses.address FROM users JOIN addresses ON users.user_id = addresses.user_id WHERE users.name ILIKE :filter;'

    Some keyword arguments which are useful for other function are
    explicitly rejected:

    >>> query(txt,
    ...       {'table1': 'users', 'table2': 'addresses'},
    ...       {'filter': '%beeblebrox%'},
    ...       fields=['user_id', 'address'])
    Traceback (most recent call last):
      ...
    TypeError: 'fields' argument not supported; we don't build SQL statements here.

    This is done as a hint for this function being a quite simple transformer;
    building arbitrary statements is beyond it's scope.
    """
    for misplaced in ['distinct', 'fields', 'where']:
        if misplaced in kwargs:
            raise TypeError('%(misplaced)r argument not supported; '
                            'we don\'t build SQL statements here.' % locals())
    names, query_data = _some_dicts(args, kwargs, 'names query_data')
    if query_data is None:
        query_data = {}
    if names is None:
        query = replace_names(clause, **kwargs)
    else:
        check_kwargs(kwargs)
        query = replace_names(clause, **names)
    return query, query_data
Ejemplo n.º 4
0
def insert(
        table,
        dict_of_values,  # ------------- [ insert ... [
        **kwargs):
    """
    Return a simple "INSERT INTO ..." SQL statement with placeholders
    and a values dict.

    Mandatory arguments:

      table -- name of the table (may contain the schema)
      dict_of_values -- a Python dict containing the values

    Keyword-only options:

      returning -- like supported by PostgreSQL 9.1+
      strict -- if False, unknown keyword arguments will be silently ignored;
                if True (default), a TypeError will be raised.

    >>> insert('die_tabelle', {'name': 'Zaphod', 'heads': 2})[0]
    'INSERT INTO die_tabelle (heads, name) VALUES (:heads, :name);'
    >>> insert('die_tabelle', {'name': 'Zaphod', 'heads': 2},
    ...        returning='id')[0]
    'INSERT INTO die_tabelle (heads, name) VALUES (:heads, :name) RETURNING id;'

    (As for the INSERT statement, the 2nd returned value is not very interesting -
    it is the the `dict_of_values` which was given as an argument.)

    Table names are checked for invalid values:
    >>> insert('evil.table;truncate table users', {'status': 'done'})[0]
    Traceback (most recent call last):
    ...
    ValueError: Invalid chars in 'evil.table;truncate table users': (' ', ';')

    >>> insert('another.table', {'evil_field;drop database': 42})
    Traceback (most recent call last):
    ...
    ValueError: Invalid chars in 'evil_field;drop database': (' ', ';')

    For unknown keyword arguments, we get a TypeError:
    >>> insert('the_guide', {'name': 'Earth'}, mostly='harmless')
    Traceback (most recent call last):
      ...
    TypeError: Unknown option 'mostly' found!

    """
    keys = list(check_name(key) for key in dict_of_values.keys())
    rows = ', '.join(keys)
    values = ', '.join([placeholder(key) for key in keys])
    query_l = [
        replace_names('INSERT INTO %(table)s', table=table),
        '(%s)' % rows,
        'VALUES (%s)' % values,
    ]
    check_kwargs(kwargs, allowed=set(['returning', 'strict']))
    returning = kwargs.get('returning')
    if returning:
        query_l.append(make_returning_clause(returning))
    statements = [' '.join(query_l)]
    statements.append('')
    query = ';'.join(statements)
    return query, dict_of_values
Ejemplo n.º 5
0
def select(
        table,  # ----------------------------- [ select ... [
        *args,
        **kwargs):
    """
    Return a simple "SELECT ..." SQL statement with placeholders
    and a values dict.

    Arguments:

      table -- name of the table (may contain the schema)
      fields -- '*' by default; other values must not be strings
               (but sequences of strings).
      query_data -- a dict to restrict the returned rows.

    For convenience, both fields and query_data can be specified named or unnamed.
    Keyword arguments are checked first; missing specifications are taken from the anonymous arguments.

    Keyword-only options:

      where -- a "WHERE ..." string (which may contain placeholders);
               currently necessary if some fields are both changed
               and used in the filtering `query_data`
      distinct -- if True, the DISTINCT keyword is inserted right after SELECT
                  (default: False)

    There is currently no JOIN, ORDER BY etc. support whatsoever;
    if you you need this, please use views.

    >>> tup1 = select('the.table', {'status': 'done', 'num': 42})
    >>> tup1[0]
    'SELECT * FROM the.table WHERE num = :num AND status = :status;'
    >>> tup1[1]
    {'status': 'done', 'num': 42}

    >>> select('another.table')[0]
    'SELECT * FROM another.table;'

    Table names are checked for invalid values:
    >>> select('another.table;truncate table users')[0]
    Traceback (most recent call last):
    ...
    ValueError: Invalid chars in 'another.table;truncate table users': (' ', ';')

    Field names are checked for invalid values:
    >>> select('another.table', {'evil_field;truncate table users': 42})[0]
    Traceback (most recent call last):
    ...
    ValueError: Invalid chars in 'evil_field;truncate table users': (' ', ';')

    Often we are interested in a particular subset of the fields:
    >>> select('another.table', {'status': 'done', 'num': 42}, fields=['id', 'changed'])[0]
    'SELECT id, changed FROM another.table WHERE num = :num AND status = :status;'

    The fields can as well be given as a single string (joined by whtiespace):
    >>> select('yet_another.table', 'id changed', {'status': 'done', 'num': 43})[0]
    'SELECT id, changed FROM yet_another.table WHERE num = :num AND status = :status;'

    If both fields and the query_data are given ancnymously, the fields are preferred,
    which is important when a None value is given:
    >>> select('yet_another.table', None, 'id changed')
    Traceback (most recent call last):
    ...
    TypeError: Duplicate fields spec. ('id changed'; we already have None)

    >>> select('yet_another.table', None, None, None)
    Traceback (most recent call last):
    ...
    TypeError: Surplus anonymous argument None

    >>> select('fancy.table', None, {})
    ('SELECT * FROM fancy.table;', {})

    >>> select('yet_another.table', None, None)
    ('SELECT * FROM yet_another.table;', {})

    >>> select('some_boring.table', 'id', distinct=True)
    ('SELECT DISTINCT id FROM some_boring.table;', {})

    For unknown keyword arguments, we get a TypeError:
    >>> select('fancy.table', unknown=42)
    Traceback (most recent call last):
    ...
    TypeError: Unknown option 'unknown' found!
    """
    fields, query_data = _fields_and_querydata(args, kwargs)

    pop = kwargs.pop
    where = pop('where', None)
    distinct = pop('distinct', False)
    check_kwargs(kwargs)

    query_l = [
        'SELECT',
        fields,
        replace_names('FROM %(table)s', table=table),
    ]
    if distinct:
        query_l.insert(1, 'DISTINCT')
    if where and not isinstance(where, six_string_types):
        raise ValueError('where must be a string; found %(where)r' % locals())
    if where is None and query_data:
        where = make_where_mask(query_data, fields)
    if where:
        query_l.append(where)
    query = ' '.join(query_l) + ';'
    if query_data is None:
        query_data = {}
    return query, query_data
Ejemplo n.º 6
0
def delete(
        table,  # ----------------------------- [ delete ... [
        query_data=None,
        **kwargs):
    """
    Return a simple "DELETE ..." SQL statement with placeholders
    and a values dict.

    Arguments:

      table -- name of the table (may contain the schema)
      query_data -- a dict with a disjunct set of keys
                    to restrict the rows deleted.

    Keyword-only options:

      where -- a "WHERE ..." string (which may contain placeholders)
      returning -- like supported by PostgreSQL 9.1+
      strict -- if False, unknown keyword arguments will be silently ignored;
                if True (default), a TypeError will be raised.

    Caution: Without a WHERE criterion (whether specified as `where` or `query_data` option,
             or a combination of both), the whole table will be deleted (like TRUNCATE, but slower and
             without the need for the TRUNCATE permission granted);
             there is currently nothing to prevent his!

    >>> tup1 = delete('the_table', {'status': 'done'})
    >>> tup1[0]
    'DELETE FROM the_table WHERE status = :status;'
    >>> tup1[1]
    {'status': 'done'}

    SQL injection prevention:
    >>> delete('evil.table;drop database')
    Traceback (most recent call last):
    ...
    ValueError: Invalid chars in 'evil.table;drop database': (' ', ';')

    >>> delete('another.table', {'evil_field;drop database': 42})
    Traceback (most recent call last):
    ...
    ValueError: Invalid chars in 'evil_field;drop database': (' ', ';')

    Table names are checked for invalid values:
    >>> delete('evil.table;truncate table users', {'status': 'done'})[0]
    Traceback (most recent call last):
    ...
    ValueError: Invalid chars in 'evil.table;truncate table users': (' ', ';')

    The 2nd part of the returned 2-tuple will always be a dictionary,
    even if no query_data had been given:
    >>> delete('slowly_truncated.table')
    ('DELETE FROM slowly_truncated.table;', {})

    For unknown keyword arguments, we get a TypeError:
    >>> delete('fancy.table', unknown=42)
    Traceback (most recent call last):
    ...
    TypeError: Unknown option 'unknown' found!
    """
    query_l = [
        replace_names('DELETE FROM %(table)s', table=table),
    ]
    where = kwargs.get('where')
    if where and not isinstance(where, six_string_types):
        raise ValueError('where must be a string; found %(where)r' % locals())
    if query_data and not where:
        where = make_where_mask(query_data)
    if where:
        query_l.append(where)
    returning = kwargs.get('returning')
    check_kwargs(kwargs, allowed=set(['returning', 'where', 'strict']))
    if returning:
        query_l.append(make_returning_clause(returning))
    queries = [' '.join(query_l) + ';']
    query = ''.join(queries)
    if query_data is None:
        query_data = {}
    return query, query_data
Ejemplo n.º 7
0
def has_strings(haystack, *needles, **kwargs):
    r"""
    Search the given text <haystack> for the given "needles"; if found anything,
    print an information (unless some other function was specified)
    and return True.

    >>> txt = 'Vitaler Nebel mit Sinn ist im Leben relativ'
    >>> liz = []
    >>> has_strings(txt, 'Nebel', 'r', before=3, after=5, head=10,
    ...             func=liz.append)
    True
    >>> liz
    ["'Vitaler Ne'...", '        v', " 6: 'aler Nebe'", " 8: 'er Nebel mit '", "36: 'en relati'"]

    This function is designed to be used in the pdb b(reak) feature:
    the optional 2nd argument is an expression which -- if evaluating to
    True -- will trigger the break; therefore, the `has_strings`
    function will either print nothing and return False, or print the
    interesting information and return True.
    """
    pop = kwargs.pop
    before = max(pop('before', 20), 0)
    after = max(pop('after', 20), 0)
    head = max(pop('head', 76), 0)
    label = pop('label', None)
    func = pop('func', print)
    other = pop('other', None)
    if isinstance(other, six_string_types):
        other = [other]

    check_kwargs(kwargs)  # raises TypeError if necessary

    found = []
    # The "needles" are our criterion to tell whether the result is interesting:
    _needle_tuples(haystack, needles, found, before=before, after=after)
    if not found:
        return False

    # If it is interesting, we might want to know more:
    if other:
        _needle_tuples(haystack, other, found, before=before, after=after)

    found.sort()
    found_max = found[-1][1]
    max_len = len(str(found_max))
    if label:
        func(label)
    shortened = haystack[:head]
    if haystack[head:]:
        func(repr(shortened)+'...')
    else:
        func(repr(shortened))
    prev_offset = None
    if isinstance(haystack, six_text_type):
        zugabe = 5  # 'u' prefix
    else:
        zugabe = 4
    for tup in found:
        start, found_at, end, needle = tup
        offset = found_at - start
        if offset and offset != prev_offset:
            func('%+*s' % (offset + max_len + zugabe, 'v'))
            prev_offset = offset
        func('%*d: %r' % (max_len, found_at, haystack[start:end]))
    return True
Ejemplo n.º 8
0
    def __init__(self, *args, **kwargs):
        """
        Named options:

        `factory` - the factory function (or class) the unnamed options are fed into;
                    should produce a valid dictionary or some subclass.
                    Defaults to the `WriteProtected` class.

        `checked` - if True, the later-pushed dictionaries are checked
                    to not contain any keys which have not been found during
                    initialization.
                    Defaults to True, if non-empty dictionaries have been
                    provided during initialization, else to False

        `strict` - True by default. Will raise a TypeError if unknown keyword arguments are found.

        The constructor is limited regarding the initialization of the first dictionary;
        you'll use something like kwargs in most cases anyway.
        """
        pop = kwargs.pop
        self.factory = pop('factory', WriteProtected)
        factory = self.factory
        if issubclass(factory, WriteProtected):
            freeze_method = pop('freeze_method', WriteProtected.freeze)
        else:
            freeze_method = pop('freeze_method', None)
        auto_freeze = pop('auto_freeze', freeze_method is not None)
        self.auto_freeze = auto_freeze
        self.freeze_method = freeze_method
        checked = pop('checked', None)
        no_check = checked is not None and not checked
        maybe_check = not no_check
        found_keys = set()

        check_kwargs(kwargs)  # raises TypeError if necessary

        self._stack = []
        self._key_lists = []
        if args:
            for arg in args:
                if arg is None:
                    if factory is None:
                        dic = {}
                    else:
                        dic = factory()
                else:
                    if maybe_check:
                        if isinstance(arg, dict):
                            found_keys.update(arg.keys())  # noqa
                        else:
                            found_keys.update([t[0] for t in arg])
                    if factory is None:
                        dic = arg
                    else:
                        dic = factory(arg)
                if auto_freeze:
                    freeze_method(dic)
                self._stack.append(dic)
                self._key_lists.append(list(dic.keys()))
        if maybe_check and found_keys:
            self._checked = True
        else:
            self._checked = False
        if self._checked:
            self._allowed_keys = found_keys
Ejemplo n.º 9
0
def Proxy(func, *args, **kwargs):
    """
    Factory, die einen Proxy für die Funktion <func> erzeugt,
    der also die Ergebnisse ihrer Funktionsaufrufe zwischenspeichert.

    >>> func = lambda a: a*3
    >>> proxy = Proxy(func)
    >>> proxy[2]
    6

    Optionale Argumente, stets benannt zu übergeben:
    aggressive - wenn True, wird mit Exceptions gearbeitet;
                 sinnvoll, wenn die Wahrscheinlichkeit für "Cache-Hits" sehr groß ist,
                 oder notfalls, wenn None ein sinnvoller Wert ist
                 (Auswirkung auf das Laufzeitverhalten, aber nicht auf das Ergebnis)

    normalize - eine Funktion zur Normalisierung des Schlüssels:

    >>> def n(liz):
    ...     return tuple(sorted(set(liz)))
    >>> p = Proxy(list, normalize=n)
    >>> p['einszwei']
    ['e', 'i', 'n', 's', 'w', 'z']
    >>> p['zweieins']
    ['e', 'i', 'n', 's', 'w', 'z']
    >>> len(p)
    1
    >>> p.keys()
    [('e', 'i', 'n', 's', 'w', 'z')]
    """
    aggressive = kwargs.pop('aggressive', False)
    normalize = kwargs.pop('normalize', None)

    check_kwargs(kwargs)  # raises TypeError if necessary

    getitem = dict.__getitem__
    setitem = dict.__setitem__

    class PoliteProxy(dict):
        def __getitem__(self, key):
            if key in self:
                val = getitem(self, key)
            else:
                val = func(key)
                setitem(self, key, val)
            return val

    class AggressiveProxy(dict):
        def __getitem__(self, key):
            try:
                return getitem(self, key)
            except KeyError:
                val = func(key)
                setitem(self, key, val)
                return val

    if aggressive:
        proxycls = AggressiveProxy
    else:
        proxycls = PoliteProxy

    if normalize is not None:

        class NormalizingProxy(proxycls):
            def __getitem__(self, key):
                key = normalize(key)
                return proxycls.__getitem__(self, key)

            def __setitem(self, key, val):
                key = normalize(key)
                return dict.__setitem__(self, key, val)

        return NormalizingProxy(*args)

    return proxycls(*args)