From bc6acd08ee4922bd06c1f3f11160353eb9fc5e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 31 Jan 2014 00:02:03 +0100 Subject: [PATCH] Drop support of Python 2.6 review2691002 --- CHANGELOG | 1 + bin/trytond | 36 ++-- doc/topics/install.rst | 2 +- setup.py | 1 - trytond/cache.py | 3 +- trytond/ir/attachment.py | 11 +- trytond/ir/cron.py | 5 +- trytond/ir/rule.py | 9 +- trytond/ir/translation.py | 14 +- trytond/model/fields/reference.py | 5 +- trytond/model/modelsql.py | 9 +- trytond/model/modelstorage.py | 13 +- trytond/modules/__init__.py | 20 ++- trytond/protocols/common.py | 94 ---------- trytond/protocols/jsonrpc.py | 9 +- trytond/protocols/xmlrpc.py | 6 +- trytond/tests/run-tests.py | 17 +- trytond/tests/test_trigger.py | 22 +-- trytond/tools/__init__.py | 4 - trytond/tools/ordereddict.py | 287 ------------------------------ 20 files changed, 76 insertions(+), 492 deletions(-) delete mode 100644 trytond/tools/ordereddict.py diff --git a/CHANGELOG b/CHANGELOG index 459c305ae9..02ca70701b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Drop support of Python 2.6 * Deprecate on_change, on_change_with, selection_change_with and autocomplete field arguments * Add fields.depends decorator diff --git a/bin/trytond b/bin/trytond index a55d02c253..d8b84ef4af 100755 --- a/bin/trytond +++ b/bin/trytond @@ -3,7 +3,7 @@ #this repository contains the full copyright notices and license terms. import sys import os -import optparse +import argparse DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..', 'trytond'))) @@ -17,37 +17,39 @@ from trytond.version import VERSION def parse_commandline(): options = {} - parser = optparse.OptionParser(version=VERSION) + parser = argparse.ArgumentParser(prog='trytond') - parser.add_option("-c", "--config", dest="config", + parser.add_argument('--version', action='version', + version='%(prog)s ' + VERSION) + parser.add_argument("-c", "--config", dest="config", help="specify config file") - parser.add_option('--debug', dest='debug_mode', action='store_true', + parser.add_argument('--debug', dest='debug_mode', action='store_true', help='enable debug mode (start post-mortem debugger if exceptions' ' occur)') - parser.add_option("-v", "--verbose", action="store_true", + parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", help="enable verbose mode") - parser.add_option("-d", "--database", dest="db_name", + parser.add_argument("-d", "--database", dest="db_name", help="specify the database name") - parser.add_option("-i", "--init", dest="init", + parser.add_argument("-i", "--init", dest="init", help="init a module (use \"all\" for all modules)") - parser.add_option("-u", "--update", dest="update", + parser.add_argument("-u", "--update", dest="update", help="update a module (use \"all\" for all modules)") - parser.add_option("--pidfile", dest="pidfile", + parser.add_argument("--pidfile", dest="pidfile", help="file where the server pid will be stored") - parser.add_option("--logfile", dest="logfile", + parser.add_argument("--logfile", dest="logfile", help="file where the server log will be stored") - parser.add_option("--disable-cron", dest="cron", + parser.add_argument("--disable-cron", dest="cron", action="store_false", help="disable cron") - parser.epilog = 'The first time a database is initialized with "-i" admin'\ - ' password is read from file defined by TRYTONPASSFILE '\ - 'environment variable or interactively ask user. '\ - 'The config file can be specified in the TRYTOND_CONFIG '\ - 'environment variable.' + parser.epilog = ('The first time a database is initialized with "-i" admin' + ' password is read from file defined by TRYTONPASSFILE ' + 'environment variable or interactively ask user. ' + 'The config file can be specified in the TRYTOND_CONFIG ' + 'environment variable.') - (opt, _) = parser.parse_args() + opt = parser.parse_args() if opt.config: options['configfile'] = opt.config diff --git a/doc/topics/install.rst b/doc/topics/install.rst index 8ea75e368e..9bc856d1c6 100644 --- a/doc/topics/install.rst +++ b/doc/topics/install.rst @@ -7,7 +7,7 @@ How to install Tryton Prerequisites ============= - * Python 2.6 or later (http://www.python.org/) + * Python 2.7 or later (http://www.python.org/) * lxml 2.0 or later (http://codespeak.net/lxml/) * relatorio 0.2.0 or later (http://relatorio.openhex.org/) * python-dateutil (http://labix.org/python-dateutil) diff --git a/setup.py b/setup.py index f3823d7832..70385bd2c4 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,6 @@ setup(name=PACKAGE, 'Natural Language :: Slovenian', 'Natural Language :: Spanish', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Software Development :: Libraries :: Application Frameworks', ], diff --git a/trytond/cache.py b/trytond/cache.py index e7ccbd2bb7..c6d7adb991 100644 --- a/trytond/cache.py +++ b/trytond/cache.py @@ -2,10 +2,11 @@ #this repository contains the full copyright notices and license terms. import datetime from threading import Lock +from collections import OrderedDict + from trytond.transaction import Transaction from trytond.config import CONFIG from trytond import backend -from trytond.tools import OrderedDict __all__ = ['Cache', 'LRUDict'] diff --git a/trytond/ir/attachment.py b/trytond/ir/attachment.py index b4b928d0ab..d261509e65 100644 --- a/trytond/ir/attachment.py +++ b/trytond/ir/attachment.py @@ -1,11 +1,7 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. import os -try: - import hashlib -except ImportError: - hashlib = None - import md5 +import hashlib from sql.operators import Concat from ..model import ModelView, ModelSQL, fields @@ -147,10 +143,7 @@ class Attachment(ModelSQL, ModelView): directory = os.path.join(CONFIG['data_path'], db_name) if not os.path.isdir(directory): os.makedirs(directory, 0770) - if hashlib: - digest = hashlib.md5(value).hexdigest() - else: - digest = md5.new(value).hexdigest() + digest = hashlib.md5(value).hexdigest() directory = os.path.join(directory, digest[0:2], digest[2:4]) if not os.path.isdir(directory): os.makedirs(directory, 0770) diff --git a/trytond/ir/cron.py b/trytond/ir/cron.py index 904a2fd78b..f255284645 100644 --- a/trytond/ir/cron.py +++ b/trytond/ir/cron.py @@ -1,6 +1,5 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -import contextlib import datetime from dateutil.relativedelta import relativedelta import traceback @@ -167,8 +166,8 @@ class Cron(ModelSQL, ModelView): req_user = cron.request_user language = (req_user.language.code if req_user.language else Config.get_language()) - with contextlib.nested(Transaction().set_user(cron.user.id), - Transaction().set_context(language=language)): + with Transaction().set_user(cron.user.id), \ + Transaction().set_context(language=language): cls.send_error_message(cron) @classmethod diff --git a/trytond/ir/rule.py b/trytond/ir/rule.py index 45138073ed..479f162234 100644 --- a/trytond/ir/rule.py +++ b/trytond/ir/rule.py @@ -1,6 +1,5 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -import contextlib import time import datetime @@ -215,8 +214,8 @@ class Rule(ModelSQL, ModelView): clause_global = {} ctx = cls._get_context() # Use root user without context to prevent recursion - with contextlib.nested(Transaction().set_user(0), - Transaction().set_context(user=0)): + with Transaction().set_user(0), \ + Transaction().set_context(user=0): for rule in cls.browse(ids): assert rule.domain, ('Rule domain empty,' 'check if migration was done') @@ -255,8 +254,8 @@ class Rule(ModelSQL, ModelView): clause = ['AND', clause_global, clause] # Use root to prevent infinite recursion - with contextlib.nested(Transaction().set_user(0), - Transaction().set_context(active_test=False, user=0)): + with Transaction().set_user(0), \ + Transaction().set_context(active_test=False, user=0): query = obj.search(clause, order=[], query=True) cls._domain_get_cache.set(key, query) diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py index 0f91fb1d2d..6db3acb8c2 100644 --- a/trytond/ir/translation.py +++ b/trytond/ir/translation.py @@ -1,6 +1,5 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -import contextlib try: import cStringIO as StringIO except ImportError: @@ -10,10 +9,7 @@ import polib import xml.dom.minidom from difflib import SequenceMatcher import os -try: - from hashlib import md5 -except ImportError: - from md5 import md5 +from hashlib import md5 from lxml import etree from itertools import izip from sql import Column @@ -805,8 +801,8 @@ class Translation(ModelSQL, ModelView): res_id = model_data.db_id else: res_id = -1 - with contextlib.nested(Transaction().set_user(0), - Transaction().set_context(module=res_id_module)): + with Transaction().set_user(0), \ + Transaction().set_context(module=res_id_module): translation, = cls.search([ ('name', '=', name), ('res_id', '=', res_id), @@ -868,8 +864,8 @@ class Translation(ModelSQL, ModelView): translation_ids.extend(ids) continue - with contextlib.nested(Transaction().set_user(0), - Transaction().set_context(module=module)): + with Transaction().set_user(0), \ + Transaction().set_context(module=module): if not ids: to_create.append({ 'name': name, diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py index 62c4badf32..cb016c8980 100644 --- a/trytond/model/fields/reference.py +++ b/trytond/model/fields/reference.py @@ -1,6 +1,5 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -import contextlib from types import NoneType from sql import Cast, Literal, Column, Query, Expression from sql.functions import Substring, Position @@ -68,8 +67,8 @@ class Reference(Field): ref_to_check[ref_model][1].append(i) # Check if reference ids still exist - with contextlib.nested(Transaction().set_context(active_test=False), - Transaction().set_user(0)): + with Transaction().set_context(active_test=False), \ + Transaction().set_user(0): for ref_model, (ref_ids, ids) in ref_to_check.iteritems(): try: pool.get(ref_model) diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index 9523049793..ec72f70dcc 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.py @@ -1,6 +1,5 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -import contextlib import re from functools import reduce from itertools import islice, izip @@ -359,8 +358,8 @@ class ModelSQL(ModelStorage): id_new = cursor.lastid() new_ids.append(id_new) except DatabaseIntegrityError, exception: - with contextlib.nested(Transaction().new_cursor(), - Transaction().set_user(0)): + with Transaction().new_cursor(), \ + Transaction().set_user(0): cls.__raise_integrity_error(exception, values) raise @@ -703,8 +702,8 @@ class ModelSQL(ModelStorage): cursor.execute(*table.update(columns, update_values, where=red_sql)) except DatabaseIntegrityError, exception: - with contextlib.nested(Transaction().new_cursor(), - Transaction().set_user(0)): + with Transaction().new_cursor(), \ + Transaction().set_user(0): cls.__raise_integrity_error(exception, values, values.keys()) raise diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index 9e8d8e2d7b..d3732faafd 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -4,7 +4,6 @@ import datetime import time import logging -import contextlib import traceback import sys import csv @@ -1310,9 +1309,9 @@ class ModelStorage(Model): model2ids = {} model2cache = {} # Read the data - with contextlib.nested(Transaction().set_cursor(self._cursor), - Transaction().set_user(self._user), - Transaction().set_context(self._context)): + with Transaction().set_cursor(self._cursor), \ + Transaction().set_user(self._user), \ + Transaction().set_context(self._context): if self.id in self._cache and name in self._cache[self.id]: # Use values from cache ids = islice(chain(islice(self._ids, index, None), @@ -1407,9 +1406,9 @@ class ModelStorage(Model): self._values = None if save_values or self.id < 0: try: - with contextlib.nested(Transaction().set_cursor(self._cursor), - Transaction().set_user(self._user), - Transaction().set_context(self._context)): + with Transaction().set_cursor(self._cursor), \ + Transaction().set_user(self._user), \ + Transaction().set_context(self._context): if self.id < 0: self._ids.remove(self.id) try: diff --git a/trytond/modules/__init__.py b/trytond/modules/__init__.py index 4a38266055..f954fb55d0 100644 --- a/trytond/modules/__init__.py +++ b/trytond/modules/__init__.py @@ -4,7 +4,6 @@ import os import sys import itertools import logging -import contextlib from functools import reduce import imp import operator @@ -365,13 +364,8 @@ def register_classes(): def load_modules(database_name, pool, update=False, lang=None): res = True - if not Transaction().cursor: - contextmanager = Transaction().start(database_name, 0) - else: - contextmanager = contextlib.nested(Transaction().new_cursor(), - Transaction().set_user(0), - Transaction().reset_context()) - with contextmanager: + + def _load_modules(): cursor = Transaction().cursor if update: # Migration from 2.2: workflow module removed @@ -428,5 +422,15 @@ def load_modules(database_name, pool, update=False, lang=None): Module = pool.get('ir.module.module') Module.update_list() cursor.commit() + + if not Transaction().cursor: + with Transaction().start(database_name, 0): + _load_modules() + else: + with Transaction().new_cursor(), \ + Transaction().set_user(0), \ + Transaction().reset_context(): + _load_modules() + Cache.resets(database_name) return res diff --git a/trytond/protocols/common.py b/trytond/protocols/common.py index 7c1dca215e..959b83aaf0 100644 --- a/trytond/protocols/common.py +++ b/trytond/protocols/common.py @@ -4,12 +4,6 @@ import errno import os import socket import threading -import sys -import gzip -try: - import cStringIO as StringIO -except ImportError: - import StringIO from SocketServer import StreamRequestHandler @@ -57,91 +51,3 @@ class RegisterHandlerMixin: self.server.handlers.remove(self) except KeyError: pass - - -class GZipRequestHandlerMixin: - - if sys.version_info[:2] <= (2, 6): - # Copy from SimpleXMLRPCServer.py with gzip encoding added - def do_POST(self): - """Handles the HTTP POST request. - - Attempts to interpret all HTTP POST requests as XML-RPC calls, - which are forwarded to the server's _dispatch method for handling. - """ - - # Check that the path is legal - if not self.is_rpc_path_valid(): - self.report_404() - return - - try: - # Get arguments by reading body of request. - # We read this in chunks to avoid straining - # socket.read(); around the 10 or 15Mb mark, some platforms - # begin to have problems (bug #792570). - max_chunk_size = 10 * 1024 * 1024 - size_remaining = int(self.headers["content-length"]) - L = [] - while size_remaining: - chunk_size = min(size_remaining, max_chunk_size) - L.append(self.rfile.read(chunk_size)) - size_remaining -= len(L[-1]) - data = ''.join(L) - - data = self.decode_request_content(data) - if data is None: - return # response has been sent - - # In previous versions of SimpleXMLRPCServer, _dispatch - # could be overridden in this class, instead of in - # SimpleXMLRPCDispatcher. To maintain backwards compatibility, - # check to see if a subclass implements _dispatch and dispatch - # using that method if present. - response = self.server._marshaled_dispatch( - data, getattr(self, '_dispatch', None) - ) - except Exception: # This should only happen if the module is buggy - # internal error, report as HTTP server error - self.send_response(500) - self.end_headers() - else: - # got a valid XML RPC response - self.send_response(200) - self.send_header("Content-type", "text/xml") - - # Handle gzip encoding - if ('gzip' in self.headers.get('Accept-Encoding', - '').split(',') - and len(response) > self.encode_threshold): - buffer = StringIO.StringIO() - output = gzip.GzipFile(mode='wb', fileobj=buffer) - output.write(response) - output.close() - buffer.seek(0) - response = buffer.getvalue() - self.send_header('Content-Encoding', 'gzip') - - self.send_header("Content-length", str(len(response))) - self.end_headers() - self.wfile.write(response) - - def decode_request_content(self, data): - #support gzip encoding of request - encoding = self.headers.get("content-encoding", "identity").lower() - if encoding == "identity": - return data - if encoding == "gzip": - f = StringIO.StringIO(data) - gzf = gzip.GzipFile(mode="rb", fileobj=f) - try: - decoded = gzf.read() - except IOError: - self.send_response(400, "error decoding gzip content") - f.close() - gzf.close() - return decoded - else: - self.send_response(501, "encoding %r not supported" % encoding) - self.send_header("Content-length", "0") - self.end_headers() diff --git a/trytond/protocols/jsonrpc.py b/trytond/protocols/jsonrpc.py index 201ba416b2..85dfe4cad6 100644 --- a/trytond/protocols/jsonrpc.py +++ b/trytond/protocols/jsonrpc.py @@ -3,8 +3,7 @@ from trytond.protocols.sslsocket import SSLSocket from trytond.protocols.dispatcher import dispatch from trytond.config import CONFIG -from trytond.protocols.common import daemon, GZipRequestHandlerMixin, \ - RegisterHandlerMixin +from trytond.protocols.common import daemon, RegisterHandlerMixin from trytond.exceptions import UserError, UserWarning, NotLogged, \ ConcurrencyException import SimpleXMLRPCServer @@ -157,8 +156,7 @@ class GenericJSONRPCRequestHandler: return res -class SimpleJSONRPCRequestHandler(GZipRequestHandlerMixin, - RegisterHandlerMixin, +class SimpleJSONRPCRequestHandler(RegisterHandlerMixin, GenericJSONRPCRequestHandler, SimpleXMLRPCServer.SimpleXMLRPCRequestHandler, SimpleHTTPServer.SimpleHTTPRequestHandler): @@ -325,14 +323,13 @@ class SimpleThreadedJSONRPCServer(SocketServer.ThreadingMixIn, SimpleJSONRPCServer): timeout = 1 daemon_threads = True + disable_nagle_algorithm = True def server_bind(self): self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - self.socket.setsockopt(socket.IPPROTO_TCP, - socket.TCP_NODELAY, 1) SimpleJSONRPCServer.server_bind(self) diff --git a/trytond/protocols/xmlrpc.py b/trytond/protocols/xmlrpc.py index 5e13d7d7b8..ec020235c5 100644 --- a/trytond/protocols/xmlrpc.py +++ b/trytond/protocols/xmlrpc.py @@ -3,8 +3,7 @@ from trytond.protocols.sslsocket import SSLSocket from trytond.protocols.dispatcher import dispatch from trytond.config import CONFIG -from trytond.protocols.common import daemon, GZipRequestHandlerMixin, \ - RegisterHandlerMixin +from trytond.protocols.common import daemon, RegisterHandlerMixin from trytond.exceptions import UserError, UserWarning, NotLogged, \ ConcurrencyException from trytond import security @@ -145,8 +144,7 @@ class GenericXMLRPCRequestHandler: security.logout(database_name, user, session) -class SimpleXMLRPCRequestHandler(GZipRequestHandlerMixin, - RegisterHandlerMixin, +class SimpleXMLRPCRequestHandler(RegisterHandlerMixin, GenericXMLRPCRequestHandler, SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): protocol_version = "HTTP/1.1" diff --git a/trytond/tests/run-tests.py b/trytond/tests/run-tests.py index 234b67ae4a..13bbd88935 100755 --- a/trytond/tests/run-tests.py +++ b/trytond/tests/run-tests.py @@ -2,7 +2,7 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. import logging -import optparse +import argparse import os import time import unittest @@ -13,14 +13,15 @@ if __name__ != '__main__': raise ImportError('%s can not be imported' % __name__) logging.basicConfig(level=logging.ERROR) -parser = optparse.OptionParser(usage="Usage: %prog [options] [test] [...]") -parser.add_option("-c", "--config", dest="config", +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--config", dest="config", help="specify config file") -parser.add_option("-m", "--modules", action="store_true", dest="modules", +parser.add_argument("-m", "--modules", action="store_true", dest="modules", default=False, help="Run also modules tests") -parser.add_option("-v", action="count", default=0, dest="verbosity", +parser.add_argument("-v", action="count", default=0, dest="verbosity", help="Increase verbosity") -opt, modules = parser.parse_args() +parser.add_argument('tests', metavar='test', nargs='*') +opt = parser.parse_args() CONFIG['db_type'] = 'sqlite' CONFIG.update_etc(opt.config) @@ -35,7 +36,7 @@ os.environ['DB_NAME'] = database_name from trytond.tests.test_tryton import all_suite, modules_suite if not opt.modules: - suite = all_suite(modules) + suite = all_suite(opt.tests) else: - suite = modules_suite(modules) + suite = modules_suite(opt.tests) unittest.TextTestRunner(verbosity=opt.verbosity).run(suite) diff --git a/trytond/tests/test_trigger.py b/trytond/tests/test_trigger.py index 02b8a2bc34..23bd8153cc 100644 --- a/trytond/tests/test_trigger.py +++ b/trytond/tests/test_trigger.py @@ -4,26 +4,8 @@ import unittest import time from xmlrpclib import MAXINT -try: - from itertools import combinations -except ImportError: - def combinations(iterable, r): - pool = tuple(iterable) - n = len(pool) - if r > n: - return - indices = range(r) - yield tuple(pool[i] for i in indices) - while True: - for i in reversed(range(r)): - if indices[i] != i + n - r: - break - else: - return - indices[i] += 1 - for j in range(i + 1, r): - indices[j] = indices[j - 1] + 1 - yield tuple(pool[i] for i in indices) +from itertools import combinations + from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \ install_module from trytond.tests.trigger import TRIGGER_LOGS diff --git a/trytond/tools/__init__.py b/trytond/tools/__init__.py index 90e391d7d5..e209cffb7c 100644 --- a/trytond/tools/__init__.py +++ b/trytond/tools/__init__.py @@ -2,10 +2,6 @@ #this repository contains the full copyright notices and license terms. from .misc import * from .datetime_strftime import * -try: - from collections import OrderedDict -except ImportError: - from .ordereddict import OrderedDict class ClassProperty(property): diff --git a/trytond/tools/ordereddict.py b/trytond/tools/ordereddict.py deleted file mode 100644 index 856bd1582d..0000000000 --- a/trytond/tools/ordereddict.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright (C) 2009 by Raymond Hettinger -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and -# pypy. -# Passes Python2.7's test suite and incorporates all the latest updates. - -try: - from thread import get_ident as _get_ident -except ImportError: - from dummy_thread import get_ident as _get_ident - -try: - from _abcoll import KeysView, ValuesView, ItemsView -except ImportError: - pass - - -class OrderedDict(dict): - 'Dictionary that remembers insertion order' - # An inherited dict maps keys to values. - # The inherited dict provides __getitem__, __len__, __contains__, and get. - # The remaining methods are order-aware. - # Big-O running times for all methods are the same as for regular - # dictionaries. - - # The internal self.__map dictionary maps keys to links in a doubly linked - # list. - # The circular doubly linked list starts and ends with a sentinel element. - # The sentinel element never gets deleted (this simplifies the algorithm). - # Each link is stored as a list of length three: [PREV, NEXT, KEY]. - - def __init__(self, *args, **kwds): - '''Initialize an ordered dictionary. Signature is the same as for - regular dictionaries, but keyword arguments are not recommended - because their insertion order is arbitrary. - - ''' - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__root - except AttributeError: - self.__root = root = [] # sentinel node - root[:] = [root, root, None] - self.__map = {} - self.__update(*args, **kwds) - - def __setitem__(self, key, value, dict_setitem=dict.__setitem__): - 'od.__setitem__(i, y) <==> od[i]=y' - # Setting a new item creates a new link which goes at the end of the - # linked list, and the inherited dictionary is updated with the new - # key/value pair. - if key not in self: - root = self.__root - last = root[0] - last[1] = root[0] = self.__map[key] = [last, root, key] - dict_setitem(self, key, value) - - def __delitem__(self, key, dict_delitem=dict.__delitem__): - 'od.__delitem__(y) <==> del od[y]' - # Deleting an existing item uses self.__map to find the link which is - # then removed by updating the links in the predecessor and successor - # nodes. - dict_delitem(self, key) - link_prev, link_next, key = self.__map.pop(key) - link_prev[1] = link_next - link_next[0] = link_prev - - def __iter__(self): - 'od.__iter__() <==> iter(od)' - root = self.__root - curr = root[1] - while curr is not root: - yield curr[2] - curr = curr[1] - - def __reversed__(self): - 'od.__reversed__() <==> reversed(od)' - root = self.__root - curr = root[0] - while curr is not root: - yield curr[2] - curr = curr[0] - - def clear(self): - 'od.clear() -> None. Remove all items from od.' - try: - for node in self.__map.itervalues(): - del node[:] - root = self.__root - root[:] = [root, root, None] - self.__map.clear() - except AttributeError: - pass - dict.clear(self) - - def popitem(self, last=True): - '''od.popitem() -> (k, v), return and remove a (key, value) pair. - Pairs are returned in LIFO order if last is true or FIFO order if - false. - - ''' - if not self: - raise KeyError('dictionary is empty') - root = self.__root - if last: - link = root[0] - link_prev = link[0] - link_prev[1] = root - root[0] = link_prev - else: - link = root[1] - link_next = link[1] - root[1] = link_next - link_next[0] = root - key = link[2] - del self.__map[key] - value = dict.pop(self, key) - return key, value - - # -- the following methods do not depend on the internal structure -- - - def keys(self): - 'od.keys() -> list of keys in od' - return list(self) - - def values(self): - 'od.values() -> list of values in od' - return [self[key] for key in self] - - def items(self): - 'od.items() -> list of (key, value) pairs in od' - return [(key, self[key]) for key in self] - - def iterkeys(self): - 'od.iterkeys() -> an iterator over the keys in od' - return iter(self) - - def itervalues(self): - 'od.itervalues -> an iterator over the values in od' - for k in self: - yield self[k] - - def iteritems(self): - 'od.iteritems -> an iterator over the (key, value) items in od' - for k in self: - yield (k, self[k]) - - def update(*args, **kwds): - '''od.update(E, **F) -> None. Update od from dict/iterable E and F. - - If E is a dict instance, does: for k in E: od[k] = E[k] - If E has a .keys() method, does: for k in E.keys(): od[k] = E[k] - Or if E is an iterable of items, does: for k, v in E: od[k] = v - In either case, this is followed by: for k, v in F.items(): od[k] = v - - ''' - if len(args) > 2: - raise TypeError('update() takes at most 2 positional ' - 'arguments (%d given)' % (len(args),)) - elif not args: - raise TypeError('update() takes at least 1 argument (0 given)') - self = args[0] - # Make progressively weaker assumptions about "other" - other = () - if len(args) == 2: - other = args[1] - if isinstance(other, dict): - for key in other: - self[key] = other[key] - elif hasattr(other, 'keys'): - for key in other.keys(): - self[key] = other[key] - else: - for key, value in other: - self[key] = value - for key, value in kwds.items(): - self[key] = value - - # let subclasses override update without breaking __init__ - __update = update - - __marker = object() - - def pop(self, key, default=__marker): - '''od.pop(k[,d]) -> v, remove specified key and return the - corresponding value. If key is not found, d is returned if given, - otherwise KeyError is raised. - - ''' - if key in self: - result = self[key] - del self[key] - return result - if default is self.__marker: - raise KeyError(key) - return default - - def setdefault(self, key, default=None): - 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' - if key in self: - return self[key] - self[key] = default - return default - - def __repr__(self, _repr_running={}): - 'od.__repr__() <==> repr(od)' - call_key = id(self), _get_ident() - if call_key in _repr_running: - return '...' - _repr_running[call_key] = 1 - try: - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - finally: - del _repr_running[call_key] - - def __reduce__(self): - 'Return state information for pickling' - items = [[k, self[k]] for k in self] - inst_dict = vars(self).copy() - for k in vars(OrderedDict()): - inst_dict.pop(k, None) - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def copy(self): - 'od.copy() -> a shallow copy of od' - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S - and values equal to v (which defaults to None). - - ''' - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - '''od.__eq__(y) <==> od==y. Comparison to another OD is - order-sensitive while comparison to a regular mapping is - order-insensitive. - - ''' - if isinstance(other, OrderedDict): - return len(self) == len(other) and self.items() == other.items() - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other - - # -- the following methods are only used in Python 2.7 -- - - def viewkeys(self): - "od.viewkeys() -> a set-like object providing a view on od's keys" - return KeysView(self) - - def viewvalues(self): - "od.viewvalues() -> an object providing a view on od's values" - return ValuesView(self) - - def viewitems(self): - "od.viewitems() -> a set-like object providing a view on od's items" - return ItemsView(self)