merge
This commit is contained in:
commit
0c39c8b95d
20 files changed
+657
-308
No files matched your search
@@ -37,6 +37,8 @@ def parse_commandline():
|
||||
help="file where the server pid will be stored")
|
||||
parser.add_option("--logfile", dest="logfile",
|
||||
help="file where the server log will be stored")
|
||||
parser.add_option("--disable-cron", dest="cron",
|
||||
action="store_false", help="disable cron")
|
||||
|
||||
(opt, _) = parser.parse_args()
|
||||
|
||||
@@ -51,6 +53,7 @@ def parse_commandline():
|
||||
'debug_mode',
|
||||
'pidfile',
|
||||
'logfile',
|
||||
'cron',
|
||||
):
|
||||
if getattr(opt, arg) is not None:
|
||||
options[arg] = getattr(opt, arg)
|
||||
|
||||
@@ -90,3 +90,6 @@
|
||||
|
||||
# Prevent database listing
|
||||
#prevent_dblist = False
|
||||
|
||||
# Enable cron
|
||||
# cron = True
|
||||
@@ -1,5 +1,6 @@
|
||||
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
from trytond.const import CONTEXT_CACHE_SIZE, MODEL_CACHE_SIZE
|
||||
|
||||
DatabaseIntegrityError = None
|
||||
DatabaseOperationalError = None
|
||||
@@ -103,7 +104,8 @@ class CursorInterface(object):
|
||||
IN_MAX = 1000
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {}
|
||||
from trytond.cache import LRUDict
|
||||
self.cache = LRUDict(CONTEXT_CACHE_SIZE)
|
||||
|
||||
def get_cache(self, context=None):
|
||||
'''
|
||||
@@ -112,6 +114,7 @@ class CursorInterface(object):
|
||||
:param context: the context
|
||||
:return: the cache dictionary
|
||||
'''
|
||||
from trytond.cache import LRUDict
|
||||
if context is None:
|
||||
context = {}
|
||||
cache_ctx = context.copy()
|
||||
@@ -119,7 +122,8 @@ class CursorInterface(object):
|
||||
'_delete_records'):
|
||||
if i in cache_ctx:
|
||||
del cache_ctx[i]
|
||||
return self.cache.setdefault(repr(cache_ctx), {})
|
||||
return self.cache.setdefault(repr(cache_ctx),
|
||||
LRUDict(MODEL_CACHE_SIZE))
|
||||
|
||||
def execute(self, sql, params=None):
|
||||
'''
|
||||
|
||||
@@ -121,7 +121,7 @@ CREATE TABLE wkf_transition (
|
||||
act_to BIGINT,
|
||||
`condition` VARCHAR(255),
|
||||
trigger_expr_id VARCHAR(255),
|
||||
signal VARCHAR(255),
|
||||
`signal` VARCHAR(255),
|
||||
`group` BIGINT,
|
||||
PRIMARY KEY(id),
|
||||
CONSTRAINT wkf_transition_act_from_fkey FOREIGN KEY (act_from) REFERENCES wkf_activity (id) ON DELETE CASCADE,
|
||||
|
||||
@@ -7,6 +7,7 @@ from threading import Lock
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.config import CONFIG
|
||||
from trytond.backend import Database
|
||||
from trytond.tools import OrderedDict
|
||||
|
||||
|
||||
class Cache(object):
|
||||
@@ -167,3 +168,32 @@ class Cache(object):
|
||||
cursor.commit()
|
||||
cursor.close()
|
||||
Cache._resets_lock.release()
|
||||
|
||||
|
||||
class LRUDict(OrderedDict):
|
||||
"""
|
||||
Dictionary with a size limit.
|
||||
If size limit is reached, it will remove the first added items.
|
||||
"""
|
||||
def __init__(self, size_limit, *args, **kwargs):
|
||||
assert size_limit > 0
|
||||
self.size_limit = size_limit
|
||||
super(LRUDict, self).__init__(*args, **kwargs)
|
||||
self._check_size_limit()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super(LRUDict, self).__setitem__(key, value)
|
||||
self._check_size_limit()
|
||||
|
||||
def update(*args, **kwargs):
|
||||
super(LRUDict, self).update(*args, **kwargs)
|
||||
self._check_size_limit()
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
default = super(LRUDict, self).setdefault(key, default=default)
|
||||
self._check_size_limit()
|
||||
return default
|
||||
|
||||
def _check_size_limit(self):
|
||||
while len(self) > self.size_limit:
|
||||
self.popitem(last=False)
|
||||
@@ -52,6 +52,7 @@ class ConfigManager(object):
|
||||
'prevent_dblist': False,
|
||||
'init': {},
|
||||
'update': {},
|
||||
'cron': True,
|
||||
}
|
||||
self.configfile = None
|
||||
|
||||
|
||||
@@ -18,4 +18,7 @@ OPERATORS = (
|
||||
'>',
|
||||
)
|
||||
|
||||
CONTEXT_CACHE_SIZE = 10
|
||||
MODEL_CACHE_SIZE = 200
|
||||
RECORD_CACHE_SIZE = 2000
|
||||
BROWSE_FIELD_TRESHOLD = 100
|
||||
+106
-112
@@ -6,13 +6,14 @@ import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import traceback
|
||||
import sys
|
||||
import logging
|
||||
from trytond.backend import Database
|
||||
from trytond.model import ModelView, ModelSQL, fields
|
||||
from trytond.tools import safe_eval
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.backend import TableHandler
|
||||
|
||||
_INTERVALTYPES = {
|
||||
'work_days': lambda interval: relativedelta(days=interval),
|
||||
'days': lambda interval: relativedelta(days=interval),
|
||||
'hours': lambda interval: relativedelta(hours=interval),
|
||||
'weeks': lambda interval: relativedelta(weeks=interval),
|
||||
@@ -26,7 +27,8 @@ class Cron(ModelSQL, ModelView):
|
||||
_description = __doc__
|
||||
name = fields.Char('Name', required=True, translate=True)
|
||||
user = fields.Many2One('res.user', 'Execution User', required=True,
|
||||
help="The user used to execute this action")
|
||||
domain=[('active', '=', False)],
|
||||
help="The user used to execute this action")
|
||||
request_user = fields.Many2One(
|
||||
'res.user', 'Request User', required=True,
|
||||
help="The user who will receive requests in case of failure")
|
||||
@@ -35,24 +37,20 @@ class Cron(ModelSQL, ModelView):
|
||||
interval_type = fields.Selection( [
|
||||
('minutes', 'Minutes'),
|
||||
('hours', 'Hours'),
|
||||
('work_days', 'Work Days'),
|
||||
('days', 'Days'),
|
||||
('weeks', 'Weeks'),
|
||||
('months', 'Months'),
|
||||
], 'Interval Unit')
|
||||
numbercall = fields.Integer('Number of calls', select=1,
|
||||
help='Number of times the function is called,\n' \
|
||||
'a negative number indicates that the function ' \
|
||||
'will always be called')
|
||||
doall = fields.Boolean('Repeat missed')
|
||||
nextcall = fields.DateTime('Next call date', required=True,
|
||||
number_calls = fields.Integer('Number of Calls', select=1,
|
||||
help=('Number of times the function is called, a negative '
|
||||
'number indicates that the function will always be '
|
||||
'called'))
|
||||
repeat_missed = fields.Boolean('Repeat Missed')
|
||||
next_call = fields.DateTime('Next Call', required=True,
|
||||
select=1)
|
||||
model = fields.Char('Model')
|
||||
function = fields.Char('Function')
|
||||
args = fields.Text('Arguments')
|
||||
priority = fields.Integer('Priority',
|
||||
help='0=Very Urgent\n10=Not urgent')
|
||||
running = fields.Boolean('Running', readonly=True, select=1)
|
||||
|
||||
def __init__(self):
|
||||
super(Cron, self).__init__()
|
||||
@@ -62,135 +60,131 @@ class Cron(ModelSQL, ModelView):
|
||||
"properly: \"%s\"\n Traceback: \n\n%s\n"
|
||||
})
|
||||
|
||||
def default_nextcall(self):
|
||||
def init(self, module_name):
|
||||
cursor = Transaction().cursor
|
||||
|
||||
# Migration from 2.0: rename numbercall, doall and nextcall
|
||||
table = TableHandler(cursor, self, module_name)
|
||||
table.column_rename('numbercall', 'number_calls')
|
||||
table.column_rename('doall', 'repeat_missed')
|
||||
table.column_rename('nextcall', 'next_call')
|
||||
|
||||
super(Cron, self).init(module_name)
|
||||
|
||||
# Migration from 2.0: work_days removed
|
||||
cursor.execute('UPDATE "%s" '
|
||||
'SET interval_type = %%s '
|
||||
'WHERE interval_type = %%s' % self._table,
|
||||
('days', 'work_days'))
|
||||
|
||||
def default_next_call(self):
|
||||
return datetime.datetime.now()
|
||||
|
||||
def default_priority(self):
|
||||
return 5
|
||||
|
||||
def default_user(self):
|
||||
return int(Transaction().user)
|
||||
|
||||
def default_interval_number(self):
|
||||
return 1
|
||||
|
||||
def default_interval_type(self):
|
||||
return 'months'
|
||||
|
||||
def default_numbercall(self):
|
||||
return 1
|
||||
def default_number_calls(self):
|
||||
return -1
|
||||
|
||||
def default_active(self):
|
||||
return True
|
||||
|
||||
def default_doall(self):
|
||||
def default_repeat_missed(self):
|
||||
return True
|
||||
|
||||
def default_running(self):
|
||||
return False
|
||||
|
||||
def check_xml_record(self, ids, values):
|
||||
return True
|
||||
|
||||
def get_delta(self, cron):
|
||||
'''
|
||||
Return the relativedelta for the next call
|
||||
'''
|
||||
return _INTERVALTYPES[cron.interval_type](cron.interval_number)
|
||||
|
||||
def _get_request_values(self, cron):
|
||||
tb_s = reduce(lambda x, y: x + y,
|
||||
traceback.format_exception(*sys.exc_info()))
|
||||
tb_s = tb_s.decode('utf-8', 'ignore')
|
||||
name = self.raise_user_error('request_title',
|
||||
raise_exception=False)
|
||||
body = self.raise_user_error('request_body', (cron.name, tb_s),
|
||||
raise_exception=False)
|
||||
values = {
|
||||
'name': name,
|
||||
'priority': '2',
|
||||
'act_from': cron.user.id,
|
||||
'act_to': cron.request_user.id,
|
||||
'body': body,
|
||||
'date_sent': datetime.datetime.now(),
|
||||
'references': [
|
||||
('create', {
|
||||
'reference': '%s,%s' % (self._name, cron.id),
|
||||
}),
|
||||
],
|
||||
'state': 'waiting',
|
||||
'trigger_date': datetime.datetime.now(),
|
||||
}
|
||||
return values
|
||||
|
||||
def _callback(self, cron):
|
||||
try:
|
||||
args = (cron['args'] or []) and safe_eval(cron['args'])
|
||||
obj = self.pool.get(cron['model'])
|
||||
if not obj and hasattr(obj, cron['function']):
|
||||
return False
|
||||
fct = getattr(obj, cron['function'])
|
||||
with Transaction().set_user(cron['user']):
|
||||
fct(*args)
|
||||
args = (cron.args or []) and safe_eval(cron.args)
|
||||
model_obj = self.pool.get(cron.model)
|
||||
with Transaction().set_user(cron.user.id):
|
||||
getattr(model_obj, cron.function)(*args)
|
||||
except Exception, error:
|
||||
Transaction().cursor.rollback()
|
||||
|
||||
tb_s = ''
|
||||
for line in traceback.format_exception(*sys.exc_info()):
|
||||
try:
|
||||
line = line.encode('utf-8', 'ignore')
|
||||
except Exception:
|
||||
continue
|
||||
tb_s += line
|
||||
try:
|
||||
tb_s += str(error).decode('utf-8', 'ignore')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
request_obj = self.pool.get('res.request')
|
||||
try:
|
||||
user_obj = self.pool.get('res.user')
|
||||
req_user = user_obj.browse(cron['request_user'])
|
||||
language = (req_user.language.code if req_user.language
|
||||
else 'en_US')
|
||||
with contextlib.nested(Transaction().set_user(cron['user']),
|
||||
Transaction().set_context(language=language)):
|
||||
rid = request_obj.create({
|
||||
'name': self.raise_user_error('request_title',
|
||||
raise_exception=False),
|
||||
'priority': '2',
|
||||
'act_from': cron['user'],
|
||||
'act_to': cron['request_user'],
|
||||
'body': self.raise_user_error('request_body',
|
||||
(cron['name'], tb_s), raise_exception=False),
|
||||
'date_sent': datetime.datetime.now(),
|
||||
'references': [
|
||||
('create', {
|
||||
'reference': "ir.cron,%s" % cron['id'],
|
||||
}),
|
||||
],
|
||||
'state': 'waiting',
|
||||
'trigger_date': datetime.datetime.now(),
|
||||
})
|
||||
Transaction().cursor.commit()
|
||||
except Exception:
|
||||
Transaction().cursor.rollback()
|
||||
req_user = cron.request_user
|
||||
language = (req_user.language.code if req_user.language
|
||||
else 'en_US')
|
||||
with contextlib.nested(Transaction().set_user(cron.user.id),
|
||||
Transaction().set_context(language=language)):
|
||||
values = self._get_request_values(cron)
|
||||
rid = request_obj.create(values)
|
||||
Transaction().cursor.commit()
|
||||
|
||||
def pool_jobs(self, db_name):
|
||||
def run(self, db_name):
|
||||
now = datetime.datetime.now()
|
||||
with Transaction().start(db_name, 0) as transaction:
|
||||
try:
|
||||
transaction.cursor.lock(self._table)
|
||||
transaction.cursor.execute('SELECT * FROM ir_cron '
|
||||
'WHERE numbercall <> 0 '
|
||||
'AND active '
|
||||
'AND nextcall <= %s '
|
||||
'AND NOT running '
|
||||
'ORDER BY priority', (datetime.datetime.now(),))
|
||||
crons = transaction.cursor.dictfetchall()
|
||||
transaction.cursor.lock(self._table)
|
||||
cron_ids = self.search([
|
||||
('number_calls', '!=', 0),
|
||||
('next_call', '<=', datetime.datetime.now()),
|
||||
])
|
||||
crons = self.browse(cron_ids)
|
||||
|
||||
for cron in crons:
|
||||
transaction.cursor.execute('UPDATE ir_cron '
|
||||
'SET running = %s '
|
||||
'WHERE id = %s', (True, cron['id']))
|
||||
nextcall = cron['nextcall']
|
||||
numbercall = cron['numbercall']
|
||||
done = False
|
||||
|
||||
while nextcall < now and numbercall:
|
||||
if numbercall > 0:
|
||||
numbercall -= 1
|
||||
if not done or cron['doall']:
|
||||
for cron in crons:
|
||||
try:
|
||||
next_call = cron.next_call
|
||||
number_calls = cron.number_calls
|
||||
first = True
|
||||
while next_call < now and number_calls != 0:
|
||||
if first or cron.repeat_missed:
|
||||
self._callback(cron)
|
||||
if numbercall:
|
||||
nextcall += _INTERVALTYPES[cron['interval_type']](
|
||||
cron['interval_number'])
|
||||
done = True
|
||||
next_call += self.get_delta(cron)
|
||||
if number_calls > 0:
|
||||
number_calls -= 1
|
||||
first = False
|
||||
|
||||
addsql = ''
|
||||
addsql_param = []
|
||||
if not numbercall:
|
||||
addsql = ', active = %s'
|
||||
addsql_param = [False]
|
||||
transaction.cursor.execute("UPDATE ir_cron "
|
||||
"SET nextcall = %s, "
|
||||
"running = %s, "
|
||||
"numbercall = %s" + addsql + " "
|
||||
"WHERE id = %s",
|
||||
[nextcall, False, numbercall] + addsql_param +
|
||||
[cron['id']])
|
||||
values = {
|
||||
'next_call': next_call,
|
||||
'number_calls': number_calls,
|
||||
}
|
||||
if not number_calls:
|
||||
values['active'] = False
|
||||
self.write(cron.id, values)
|
||||
transaction.cursor.commit()
|
||||
except Exception:
|
||||
transaction.cursor.rollback()
|
||||
raise
|
||||
except Exception:
|
||||
transaction.cursor.rollback()
|
||||
tb_s = reduce(lambda x, y: x + y,
|
||||
traceback.format_exception(*sys.exc_info()))
|
||||
tb_s = tb_s.decode('utf-8', 'ignore')
|
||||
logger = logging.getLogger('cron')
|
||||
logger.error('Exception:\n%s' % tb_s)
|
||||
|
||||
Cron()
|
||||
+17
-23
@@ -12,16 +12,14 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="arch" type="xml">
|
||||
<![CDATA[
|
||||
<tree string="Scheduled Actions">
|
||||
<field name="nextcall"/>
|
||||
<field name="next_call"/>
|
||||
<field name="name" select="1"/>
|
||||
<field name="user" select="1"/>
|
||||
<field name="request_user" select="2"/>
|
||||
<field name="priority" select="2"/>
|
||||
<field name="interval_number"/>
|
||||
<field name="interval_type"/>
|
||||
<field name="numbercall" select="2"/>
|
||||
<field name="number_calls" select="2"/>
|
||||
<field name="active" select="2"/>
|
||||
<field name="running" select="2"/>
|
||||
</tree>
|
||||
]]>
|
||||
</field>
|
||||
@@ -31,37 +29,33 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<![CDATA[
|
||||
<form string="Scheduled Action" col="6">
|
||||
<form string="Scheduled Action">
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
<label name="active"/>
|
||||
<field name="active"/>
|
||||
<label name="user"/>
|
||||
<field name="user"/>
|
||||
<label name="request_user"/>
|
||||
<field name="request_user"/>
|
||||
<label name="numbercall"/>
|
||||
<field name="numbercall"/>
|
||||
<label name="priority"/>
|
||||
<field name="priority"/>
|
||||
<label name="active"/>
|
||||
<field name="active"/>
|
||||
<label name="interval_number"/>
|
||||
<field name="interval_number"/>
|
||||
<label name="interval_type"/>
|
||||
<field name="interval_type"/>
|
||||
<label name="nextcall"/>
|
||||
<field name="nextcall"/>
|
||||
<label name="doall"/>
|
||||
<field name="doall"/>
|
||||
<separator string="Action to trigger" colspan="6"
|
||||
<label name="number_calls"/>
|
||||
<field name="number_calls"/>
|
||||
<label name="next_call"/>
|
||||
<field name="next_call"/>
|
||||
<label name="repeat_missed"/>
|
||||
<field name="repeat_missed"/>
|
||||
<separator string="Action to trigger" colspan="4"
|
||||
id="action_trigger"/>
|
||||
<label name="model"/>
|
||||
<field name="model" readonly="1"/>
|
||||
<field name="model"/>
|
||||
<label name="function"/>
|
||||
<field name="function" readonly="1"/>
|
||||
<label name="running"/>
|
||||
<field name="running"/>
|
||||
<separator name="args" colspan="6"/>
|
||||
<field name="args" colspan="6" readonly="1"/>
|
||||
<field name="function"/>
|
||||
<label name="args"/>
|
||||
<field name="args" colspan="3" widget="char"/>
|
||||
</form>
|
||||
]]>
|
||||
</field>
|
||||
@@ -69,7 +63,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<record model="ir.action.act_window" id="act_cron_form">
|
||||
<field name="name">Scheduled Actions</field>
|
||||
<field name="res_model">ir.cron</field>
|
||||
<field name="context">{'active_test': False}</field>
|
||||
<field name="context"></field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_cron_form_view1">
|
||||
|
||||
+7
-3
@@ -163,6 +163,7 @@ class Rule(ModelSQL, ModelView):
|
||||
rule_group_user_obj = self.pool.get('ir.rule.group-res.user')
|
||||
rule_group_group_obj = self.pool.get('ir.rule.group-res.group')
|
||||
user_group_obj = self.pool.get('res.user-res.group')
|
||||
user_obj = self.pool.get('res.user')
|
||||
|
||||
cursor = Transaction().cursor
|
||||
cursor.execute('SELECT r.id FROM "' + self._table + '" r ' \
|
||||
@@ -191,15 +192,17 @@ class Rule(ModelSQL, ModelView):
|
||||
clause_global = {}
|
||||
operand2query = self._operand_get('res.user', level=1,
|
||||
recur=['many2one'], root_tech='user', root='User')
|
||||
user_id = Transaction().user
|
||||
with Transaction().set_user(0, set_context=True):
|
||||
user = user_obj.browse(user_id)
|
||||
# Use root user without context to prevent recursion
|
||||
user = Transaction().user
|
||||
with contextlib.nested(Transaction().set_user(0),
|
||||
Transaction().set_context(user=0)):
|
||||
for rule in self.browse(ids):
|
||||
dom = safe_eval("[('%s', '%s', %s)]" % \
|
||||
(rule.field.name, rule.operator,
|
||||
operand2query[rule.operand]), {
|
||||
'user': self.pool.get('res.user').browse(user),
|
||||
'user': user,
|
||||
'time': time,
|
||||
})
|
||||
|
||||
@@ -226,7 +229,8 @@ class Rule(ModelSQL, ModelView):
|
||||
'FROM "' + rule_group_group_obj._table + '" g_rel ' \
|
||||
'JOIN "' + user_group_obj._table + '" u_rel ' \
|
||||
'ON g_rel.group_id = u_rel.gid ' \
|
||||
'WHERE u_rel.uid = %s))', (model_name, user, user))
|
||||
'WHERE u_rel.uid = %s))',
|
||||
(model_name, user_id, user_id))
|
||||
fetchone = cursor.fetchone()
|
||||
if fetchone:
|
||||
group_id = fetchone[0]
|
||||
|
||||
@@ -49,7 +49,7 @@ class Sequence(ModelSQL, ModelView):
|
||||
states={
|
||||
'invisible': Not(In(Eval('type'), ['incremental'])),
|
||||
}, depends=['type'])
|
||||
timestamp_rounding = fields.Integer('Timestamp Rounding', required=True,
|
||||
timestamp_rounding = fields.Float('Timestamp Rounding', required=True,
|
||||
states={
|
||||
'invisible': Not(In(Eval('type'),
|
||||
['decimal timestamp', 'hexadecimal timestamp'])),
|
||||
|
||||
@@ -144,5 +144,6 @@ attlist.xpath &=
|
||||
attribute position { "inside" | "replace" | "replace_attributes" | "after" | "before" }?
|
||||
action = element action { attlist.action, empty }
|
||||
attlist.action &= attribute name { text }
|
||||
attlist.action &= [ a:defaultValue = "1" ] attribute colspan { text }?
|
||||
label |= notAllowed
|
||||
start = data | board | label
|
||||
@@ -447,6 +447,11 @@
|
||||
<define name="attlist.action" combine="interleave">
|
||||
<attribute name="name"/>
|
||||
</define>
|
||||
<define name="attlist.action" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="colspan" a:defaultValue="1"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="label" combine="choice">
|
||||
<notAllowed/>
|
||||
</define>
|
||||
|
||||
+161
-145
@@ -2,10 +2,13 @@
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
from __future__ import with_statement
|
||||
import contextlib
|
||||
from itertools import islice, ifilter, ifilterfalse
|
||||
from itertools import islice, ifilter, ifilterfalse, chain
|
||||
from trytond.model import fields
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.const import BROWSE_FIELD_TRESHOLD
|
||||
from trytond.cache import LRUDict
|
||||
from trytond.const import MODEL_CACHE_SIZE, RECORD_CACHE_SIZE, \
|
||||
BROWSE_FIELD_TRESHOLD
|
||||
|
||||
|
||||
class BrowseRecordList(list):
|
||||
'''
|
||||
@@ -46,164 +49,176 @@ class BrowseRecord(object):
|
||||
An object that represents record defined by a ORM object.
|
||||
'''
|
||||
|
||||
def __init__(self, record_id, model, local_cache=None):
|
||||
def __init__(self, record_id, model, ids, local_cache):
|
||||
assert isinstance(ids, list)
|
||||
if ids:
|
||||
assert record_id in ids
|
||||
self._cursor = Transaction().cursor
|
||||
self._user = Transaction().user
|
||||
self._id = record_id
|
||||
self.id = record_id
|
||||
self._model = model
|
||||
self._model_name = self._model._name
|
||||
self._context = Transaction().context
|
||||
self._ids = ids
|
||||
|
||||
cache = self._cursor.get_cache(self._context)
|
||||
cache.setdefault(model._name, {})
|
||||
cache.setdefault('_language_cache', {})
|
||||
if model._name not in cache:
|
||||
cache[model._name] = LRUDict(RECORD_CACHE_SIZE)
|
||||
self._data = cache[model._name]
|
||||
if not record_id in self._data:
|
||||
self._data[record_id] = {'id': record_id}
|
||||
self._cache = cache
|
||||
if local_cache is None:
|
||||
local_cache = {}
|
||||
self._local_cache = local_cache
|
||||
self._local_cache.setdefault(model._name, {})
|
||||
self._local_cache.setdefault('_language_cache', {})
|
||||
self._local_data = self._local_cache[model._name]
|
||||
if not record_id in self._local_data:
|
||||
self._local_data[record_id] = {}
|
||||
assert isinstance(local_cache, LRUDict)
|
||||
self._local_data = local_cache
|
||||
|
||||
def __getitem__(self, name):
|
||||
if name == 'id':
|
||||
return self._id
|
||||
if name == 'setLang':
|
||||
return self.setLang
|
||||
if not self._data[self._id].has_key(name) \
|
||||
and not self._local_data[self._id].has_key(name):
|
||||
# build the list of fields we will fetch
|
||||
# Access to LRUDict must be atomic
|
||||
result = self._local_data.get(self.id, {}).get(name)
|
||||
if (self.id in self._local_data
|
||||
and name in self._local_data[self.id]):
|
||||
return result
|
||||
result = self._data.get(self.id, {}).get(name)
|
||||
if self.id in self._data and name in self._data[self.id]:
|
||||
return result
|
||||
|
||||
# fetch the definition of the field which was asked for
|
||||
if name in self._model._columns:
|
||||
col = self._model._columns[name]
|
||||
elif name in self._model._inherit_fields:
|
||||
col = self._model._inherit_fields[name][2]
|
||||
elif hasattr(self._model, name):
|
||||
return getattr(self._model, name)
|
||||
else:
|
||||
raise Exception('Error', 'Programming error: field "%s" ' \
|
||||
'does not exist in model "%s"!' \
|
||||
% (name, self._model._name))
|
||||
# build the list of fields we will fetch
|
||||
# fetch the definition of the field which was asked for
|
||||
if name in self._model._columns:
|
||||
col = self._model._columns[name]
|
||||
elif name in self._model._inherit_fields:
|
||||
col = self._model._inherit_fields[name][2]
|
||||
elif hasattr(self._model, name):
|
||||
return getattr(self._model, name)
|
||||
else:
|
||||
raise Exception('Error', 'Programming error: field "%s" ' \
|
||||
'does not exist in model "%s"!' \
|
||||
% (name, self._model._name))
|
||||
|
||||
ffields = {}
|
||||
if col.loading == 'eager':
|
||||
field_access_obj = self._model.pool.get('ir.model.field.access')
|
||||
fread_accesses = {}
|
||||
for inherit_name in self._model._inherits:
|
||||
inherit_obj = self._model.pool.get(inherit_name)
|
||||
fread_accesses.update(field_access_obj.check(inherit_name,
|
||||
inherit_obj._columns.keys(), 'read', access=True))
|
||||
fread_accesses.update(field_access_obj.check(self._model._name,
|
||||
self._model._columns.keys(), 'read', access=True))
|
||||
to_remove = set(x for x, y in fread_accesses.iteritems()
|
||||
if not y and x != name)
|
||||
ffields = {}
|
||||
if col.loading == 'eager':
|
||||
field_access_obj = self._model.pool.get('ir.model.field.access')
|
||||
fread_accesses = {}
|
||||
for inherit_name in self._model._inherits:
|
||||
inherit_obj = self._model.pool.get(inherit_name)
|
||||
fread_accesses.update(field_access_obj.check(inherit_name,
|
||||
inherit_obj._columns.keys(), 'read', access=True))
|
||||
fread_accesses.update(field_access_obj.check(self._model._name,
|
||||
self._model._columns.keys(), 'read', access=True))
|
||||
to_remove = set(x for x, y in fread_accesses.iteritems()
|
||||
if not y and x != name)
|
||||
|
||||
threshold = BROWSE_FIELD_TRESHOLD
|
||||
inherit_threshold = threshold - len(self._model._columns)
|
||||
threshold = BROWSE_FIELD_TRESHOLD
|
||||
inherit_threshold = threshold - len(self._model._columns)
|
||||
|
||||
def not_cached(item):
|
||||
fname, field = item
|
||||
return (fname not in self._data[self._id]
|
||||
and fname not in self._local_data[self._id])
|
||||
def to_load(item):
|
||||
fname, field = item
|
||||
return (field.loading == 'eager'
|
||||
and fname not in to_remove)
|
||||
def overrided(item):
|
||||
fname, field = item
|
||||
return fname in self._model._columns
|
||||
def not_cached(item):
|
||||
fname, field = item
|
||||
return (fname not in self._data.get(self.id, {})
|
||||
and fname not in self._local_data.get(self.id, {}))
|
||||
def to_load(item):
|
||||
fname, field = item
|
||||
return (field.loading == 'eager'
|
||||
and fname not in to_remove)
|
||||
def overrided(item):
|
||||
fname, field = item
|
||||
return fname in self._model._columns
|
||||
|
||||
if inherit_threshold > 0:
|
||||
ifields = ((fname, field)
|
||||
for fname, (_, _, field) in
|
||||
self._model._inherit_fields.iteritems())
|
||||
ifields = ifilterfalse(overrided,
|
||||
ifilter(to_load,
|
||||
ifilter(not_cached, ifields)))
|
||||
ifields = islice(ifields, 0, inherit_threshold)
|
||||
ffields.update(ifields)
|
||||
threshold -= inherit_threshold
|
||||
|
||||
ifields = ifilter(to_load,
|
||||
ifilter(not_cached,
|
||||
self._model._columns.iteritems()))
|
||||
ifields = islice(ifields, 0, threshold)
|
||||
if inherit_threshold > 0:
|
||||
ifields = ((fname, field)
|
||||
for fname, (_, _, field) in
|
||||
self._model._inherit_fields.iteritems())
|
||||
ifields = ifilterfalse(overrided,
|
||||
ifilter(to_load,
|
||||
ifilter(not_cached, ifields)))
|
||||
ifields = islice(ifields, 0, inherit_threshold)
|
||||
ffields.update(ifields)
|
||||
threshold -= inherit_threshold
|
||||
|
||||
ffields[name] = col
|
||||
ifields = ifilter(to_load,
|
||||
ifilter(not_cached,
|
||||
self._model._columns.iteritems()))
|
||||
ifields = islice(ifields, 0, threshold)
|
||||
ffields.update(ifields)
|
||||
|
||||
# add datetime_field
|
||||
for field in ffields:
|
||||
if hasattr(field, 'datetime_field') and field.datetime_field:
|
||||
if field.datetime_field in self._model._columns:
|
||||
date_field = self._model._columns[field.datetime_field]
|
||||
else:
|
||||
date_field = self._model._inherit_fields[
|
||||
field.datetime_field][2]
|
||||
ffields[field.datetime_field] = datetime_field
|
||||
ffields[name] = col
|
||||
|
||||
if len(self._data) <= self._cursor.IN_MAX:
|
||||
iterids = self._data.iterkeys()
|
||||
else:
|
||||
iterids = self._local_data.iterkeys()
|
||||
ids = [x for x in iterids \
|
||||
if not self._data.setdefault(x, {}).has_key(name) \
|
||||
and not self._local_data.setdefault(x, {}).has_key(name)]
|
||||
# read the data
|
||||
with contextlib.nested(Transaction().set_cursor(self._cursor),
|
||||
Transaction().set_user(self._user),
|
||||
Transaction().set_context(self._context)):
|
||||
datas = self._model.read(ids, ffields.keys())
|
||||
# add datetime_field
|
||||
for field in ffields:
|
||||
if hasattr(field, 'datetime_field') and field.datetime_field:
|
||||
if field.datetime_field in self._model._columns:
|
||||
date_field = self._model._columns[field.datetime_field]
|
||||
else:
|
||||
date_field = self._model._inherit_fields[
|
||||
field.datetime_field][2]
|
||||
ffields[field.datetime_field] = datetime_field
|
||||
|
||||
# create browse records for 'remote' models
|
||||
for data in datas:
|
||||
for i, j in ffields.iteritems():
|
||||
model = None
|
||||
if (hasattr(j, 'model_name') and
|
||||
j.model_name in
|
||||
self._model.pool.object_name_list()):
|
||||
model = self._model.pool.get(j.model_name)
|
||||
elif hasattr(j, 'get_target'):
|
||||
model = j.get_target(self._model.pool)
|
||||
if model and j._type in ('many2one', 'one2one'):
|
||||
if (not data[i]
|
||||
and not (isinstance(data[i], (int, long))
|
||||
and not isinstance(data[i],
|
||||
type(False)))):
|
||||
data[i] = BrowseRecordNull()
|
||||
else:
|
||||
_datetime = None
|
||||
if (hasattr(j, 'datetime_field')
|
||||
and j.datetime_field):
|
||||
_datetime = data[j.datetime_field]
|
||||
with Transaction().set_context(
|
||||
_datetime=_datetime):
|
||||
data[i] = BrowseRecord(data[i], model,
|
||||
local_cache=self._local_cache)
|
||||
elif (model
|
||||
and j._type in ('one2many', 'many2many')
|
||||
and len(data[i])):
|
||||
def filter_(id_):
|
||||
return True
|
||||
if (id_ in self._local_data
|
||||
and name in self._local_data[id_]):
|
||||
return False
|
||||
if id_ in self._data and name in self._data[id_]:
|
||||
return False
|
||||
return True
|
||||
index = self._ids.index(self.id)
|
||||
ids = list(islice(ifilter(filter_, chain(self._ids, self._ids)),
|
||||
index, self._cursor.IN_MAX + index))
|
||||
model2ids = {}
|
||||
model2cache = {}
|
||||
# read the data
|
||||
with contextlib.nested(Transaction().set_cursor(self._cursor),
|
||||
Transaction().set_user(self._user),
|
||||
Transaction().set_context(self._context)):
|
||||
# create browse records for 'remote' models
|
||||
for data in self._model.read(ids, ffields.keys()):
|
||||
for i, j in ffields.iteritems():
|
||||
model = None
|
||||
if (hasattr(j, 'model_name') and
|
||||
j.model_name in
|
||||
self._model.pool.object_name_list()):
|
||||
model = self._model.pool.get(j.model_name)
|
||||
elif hasattr(j, 'get_target'):
|
||||
model = j.get_target(self._model.pool)
|
||||
if model and j._type in ('many2one', 'one2one'):
|
||||
if (not data[i]
|
||||
and not (isinstance(data[i], (int, long))
|
||||
and not isinstance(data[i],
|
||||
type(False)))):
|
||||
data[i] = BrowseRecordNull()
|
||||
else:
|
||||
_datetime = None
|
||||
if hasattr(j, 'datetime_field') and j.datetime_field:
|
||||
if (hasattr(j, 'datetime_field')
|
||||
and j.datetime_field):
|
||||
_datetime = data[j.datetime_field]
|
||||
with Transaction().set_context(
|
||||
_datetime=_datetime):
|
||||
data[i] = BrowseRecordList(BrowseRecord(
|
||||
x, model, local_cache=self._local_cache)
|
||||
for x in data[i])
|
||||
if isinstance(j, fields.Function):
|
||||
self._local_data.setdefault(data['id'], {})[i] = data[i]
|
||||
del data[i]
|
||||
self._data[data['id']].update(data)
|
||||
if name in self._local_data[self._id]:
|
||||
return self._local_data[self._id][name]
|
||||
return self._data[self._id][name]
|
||||
ids = model2ids.setdefault(model, [])
|
||||
ids.append(data[i])
|
||||
local_cache = model2cache.setdefault(model,
|
||||
LRUDict(RECORD_CACHE_SIZE))
|
||||
data[i] = BrowseRecord(data[i], model,
|
||||
ids, local_cache)
|
||||
elif (model
|
||||
and j._type in ('one2many', 'many2many')
|
||||
and len(data[i])):
|
||||
_datetime = None
|
||||
if hasattr(j, 'datetime_field') and j.datetime_field:
|
||||
_datetime = data[j.datetime_field]
|
||||
with Transaction().set_context(
|
||||
_datetime=_datetime):
|
||||
ids = model2ids.setdefault(model, [])
|
||||
ids.extend(data[i])
|
||||
local_cache = model2cache.setdefault(model,
|
||||
LRUDict(RECORD_CACHE_SIZE))
|
||||
data[i] = BrowseRecordList(
|
||||
BrowseRecord(x, model, ids, local_cache)
|
||||
for x in data[i])
|
||||
if isinstance(j, fields.Function):
|
||||
if data['id'] == self.id and i == name:
|
||||
result = data[i]
|
||||
self._local_data.setdefault(data['id'], {})[i] = data[i]
|
||||
del data[i]
|
||||
self._data.setdefault(data['id'], {}).update(data)
|
||||
if data['id'] == self.id and name in data:
|
||||
result = data[name]
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
# TODO raise an AttributeError exception
|
||||
@@ -218,16 +233,16 @@ class BrowseRecord(object):
|
||||
return name in self
|
||||
|
||||
def __int__(self):
|
||||
return self._id
|
||||
return self.id
|
||||
|
||||
def __str__(self):
|
||||
return "BrowseRecord(%s, %d)" % (self._model_name, self._id)
|
||||
return "BrowseRecord(%s, %d)" % (self._model_name, self.id)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self._model_name, self._id) == (other._model_name, other._id)
|
||||
return (self._model_name, self.id) == (other._model_name, other.id)
|
||||
|
||||
def __ne__(self, other):
|
||||
return (self._model_name, self._id) != (other._model_name, other._id)
|
||||
return (self._model_name, self.id) != (other._model_name, other.id)
|
||||
|
||||
# we need to define __unicode__ even though we've already defined __str__
|
||||
# because we have overridden __getattr__
|
||||
@@ -235,7 +250,7 @@ class BrowseRecord(object):
|
||||
return unicode(str(self))
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self._model_name, self._id))
|
||||
return hash((self._model_name, self.id))
|
||||
|
||||
def __nonzero__(self):
|
||||
return True
|
||||
@@ -246,22 +261,23 @@ class BrowseRecord(object):
|
||||
self._context = self._context.copy()
|
||||
prev_lang = self._context.get('language') or 'en_US'
|
||||
self._context['language'] = lang
|
||||
for cache in (self._cache, self._local_cache):
|
||||
language_cache = cache['_language_cache']
|
||||
for cache in (self._cache, self._local_data):
|
||||
language_cache = cache.setdefault('_language_cache', {})
|
||||
for model in cache:
|
||||
if model == '_language_cache':
|
||||
continue
|
||||
for record_id in cache[model]:
|
||||
language_cache.setdefault(prev_lang,
|
||||
{}).setdefault(model, {})[record_id] = \
|
||||
cache[model][record_id]
|
||||
LRUDict(MODEL_CACHE_SIZE)).setdefault(model,
|
||||
LRUDict(RECORD_CACHE_SIZE))[record_id] = \
|
||||
cache[model][record_id]
|
||||
if lang in language_cache \
|
||||
and model in language_cache[lang] \
|
||||
and record_id in language_cache[lang][model]:
|
||||
cache[model][record_id] = \
|
||||
language_cache[lang][model][record_id]
|
||||
else:
|
||||
cache[model][record_id] = {'id': record_id}
|
||||
cache[model][record_id] = {}
|
||||
|
||||
def get_eval(self, name):
|
||||
res = self[name]
|
||||
|
||||
@@ -5,13 +5,15 @@ import contextlib
|
||||
import datetime
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from itertools import islice
|
||||
from trytond.model import ModelStorage
|
||||
from trytond.model import fields
|
||||
from trytond.backend import FIELDS, TableHandler
|
||||
from trytond.backend import DatabaseIntegrityError, Database
|
||||
from trytond.tools import reduce_ids
|
||||
from trytond.const import OPERATORS
|
||||
from trytond.const import OPERATORS, RECORD_CACHE_SIZE
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.cache import LRUDict
|
||||
_RE_UNIQUE = re.compile('UNIQUE\s*\((.*)\)', re.I)
|
||||
_RE_CHECK = re.compile('CHECK\s*\((.*)\)', re.I)
|
||||
|
||||
@@ -1196,11 +1198,11 @@ class ModelSQL(ModelStorage):
|
||||
|
||||
datas = cursor.dictfetchmany(cursor.IN_MAX)
|
||||
cache = cursor.get_cache()
|
||||
cache.setdefault(self._name, {})
|
||||
cache.setdefault(self._name, LRUDict(RECORD_CACHE_SIZE))
|
||||
delete_records = Transaction().delete_records.setdefault(self._name,
|
||||
set())
|
||||
keys = None
|
||||
for data in datas:
|
||||
for data in islice(datas, 0, cache.size_limit):
|
||||
if data['id'] in delete_records:
|
||||
continue
|
||||
if not keys:
|
||||
@@ -1215,8 +1217,7 @@ class ModelSQL(ModelStorage):
|
||||
continue
|
||||
for k in keys:
|
||||
del data[k]
|
||||
cache[self._name].setdefault(data['id'], {})
|
||||
cache[self._name][data['id']].update(data)
|
||||
cache[self._name].setdefault(data['id'], {}).update(data)
|
||||
|
||||
if len(datas) >= cursor.IN_MAX:
|
||||
select_fields2 = [select_fields[0]]
|
||||
|
||||
@@ -19,8 +19,9 @@ from trytond.model.browse import BrowseRecordList, BrowseRecord, \
|
||||
from trytond.model.browse import EvalEnvironment
|
||||
from trytond.tools import safe_eval, reduce_domain
|
||||
from trytond.pyson import PYSONEncoder, PYSONDecoder, PYSON
|
||||
from trytond.const import OPERATORS
|
||||
from trytond.const import OPERATORS, RECORD_CACHE_SIZE
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.cache import LRUDict
|
||||
|
||||
|
||||
class ModelStorage(Model):
|
||||
@@ -189,7 +190,7 @@ class ModelStorage(Model):
|
||||
trigger_ids = eligibles.keys()
|
||||
if not trigger_ids:
|
||||
return
|
||||
records = self.browse(chain(*eligibles.values()))
|
||||
records = self.browse(list(chain(*eligibles.values())))
|
||||
id2record = dict((x.id, x) for x in records)
|
||||
triggers = trigger_obj.browse(trigger_ids)
|
||||
for trigger in triggers:
|
||||
@@ -511,10 +512,10 @@ class ModelStorage(Model):
|
||||
:param ids: a list of ids or an id
|
||||
:return: a BrowseRecordList or a BrowseRecord
|
||||
'''
|
||||
local_cache = LRUDict(RECORD_CACHE_SIZE)
|
||||
if isinstance(ids, (int, long)):
|
||||
return BrowseRecord(ids, self)
|
||||
local_cache = {}
|
||||
return BrowseRecordList((BrowseRecord(x, self, local_cache=local_cache)
|
||||
return BrowseRecord(ids, self, [ids], local_cache)
|
||||
return BrowseRecordList((BrowseRecord(x, self, ids, local_cache)
|
||||
for x in ids))
|
||||
|
||||
def __export_row(self, record, fields_names):
|
||||
|
||||
+3
-2
@@ -663,6 +663,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="login">user_cron_trigger</field>
|
||||
<field name="name">Cron Trigger</field>
|
||||
<field name="groups" eval="[('add', ref('group_admin'))]"/>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.cron" id="cron_trigger_time">
|
||||
@@ -672,8 +673,8 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="active" eval="True"/>
|
||||
<field name="interval_number">5</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="number_calls">-1</field>
|
||||
<field name="repeat_missed" eval="False"/>
|
||||
<field name="model">ir.trigger</field>
|
||||
<field name="function">trigger_time</field>
|
||||
</record>
|
||||
|
||||
+15
-9
@@ -235,19 +235,25 @@ class TrytonServer(object):
|
||||
import psyco
|
||||
psyco.full()
|
||||
|
||||
threads = {}
|
||||
while True:
|
||||
if CONFIG['auto_reload'] and monitor():
|
||||
try:
|
||||
Pool.start()
|
||||
except Exception:
|
||||
pass
|
||||
for dbname in Pool.database_list():
|
||||
pool = Pool(dbname)
|
||||
if 'ir.cron' not in pool.object_name_list():
|
||||
continue
|
||||
cron_obj = pool.get('ir.cron')
|
||||
thread = threading.Thread(
|
||||
target=cron_obj.pool_jobs,
|
||||
args=(dbname,), kwargs={})
|
||||
thread.start()
|
||||
if CONFIG['cron']:
|
||||
for dbname in Pool.database_list():
|
||||
thread = threads.get(dbname)
|
||||
if thread and thread.is_alive():
|
||||
continue
|
||||
pool = Pool(dbname)
|
||||
if 'ir.cron' not in pool.object_name_list():
|
||||
continue
|
||||
cron_obj = pool.get('ir.cron')
|
||||
thread = threading.Thread(
|
||||
target=cron_obj.run,
|
||||
args=(dbname,), kwargs={})
|
||||
thread.start()
|
||||
threads[dbname] = thread
|
||||
time.sleep(60)
|
||||
@@ -2,3 +2,7 @@
|
||||
#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
|
||||
@@ -0,0 +1,278 @@
|
||||
# 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
|
||||
|
||||
__update = update # let subclasses override update without breaking __init__
|
||||
|
||||
__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)
|
||||
Reference in new issue
Block a user