[tryton-debian-vcs] tryton-proteus branch upstream updated. upstream/3.4.3-1-gfefed7b
Mathias Behrle
tryton-debian-vcs at alioth.debian.org
Thu Apr 23 16:08:05 UTC 2015
The following commit has been merged in the upstream branch:
https://alioth.debian.org/plugins/scmgit/cgi-bin/gitweb.cgi/?p=tryton/tryton-proteus.git;a=commitdiff;h=upstream/3.4.3-1-gfefed7b
commit fefed7b8d776a096ba3d9a6f267916184f88a7e7
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Thu Apr 23 17:00:10 2015 +0200
Adding upstream version 3.6.0.
Signed-off-by: Mathias Behrle <mathiasb at m9s.biz>
diff --git a/CHANGELOG b/CHANGELOG
index 712f9a8..1a3002d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,11 +1,10 @@
-Version 3.4.3 - 2015-03-30
-* Bug fixes (see mercurial logs for details)
-
-Version 3.4.2 - 2015-02-16
-* Bug fixes (see mercurial logs for details)
-
-Version 3.4.1 - 2014-12-03
+Version 3.6.0 - 2015-04-20
* Bug fixes (see mercurial logs for details)
+* Add support for PyPy
+* Allow to provide extra keyword arguments to set_xmlrpc()
+* Add support of fields.TimeDelta
+* Remove password on set_trytond
+* Manage reload, save, delete, duplicate and click as class and instance method
Version 3.4.0 - 2014-10-20
* Bug fixes (see mercurial logs for details)
diff --git a/PKG-INFO b/PKG-INFO
index 1e5d728..f6488f0 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,12 +1,12 @@
Metadata-Version: 1.1
Name: proteus
-Version: 3.4.3
+Version: 3.6.0
Summary: Library to access Tryton server as a client
Home-page: http://www.tryton.org/
Author: Tryton
Author-email: issue_tracker at tryton.org
License: LGPL-3
-Download-URL: http://downloads.tryton.org/3.4/
+Download-URL: http://downloads.tryton.org/3.6/
Description: proteus
=======
@@ -152,4 +152,6 @@ Classifier: Intended Audience :: Legal Industry
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Office/Business
diff --git a/proteus.egg-info/PKG-INFO b/proteus.egg-info/PKG-INFO
index 1e5d728..f6488f0 100644
--- a/proteus.egg-info/PKG-INFO
+++ b/proteus.egg-info/PKG-INFO
@@ -1,12 +1,12 @@
Metadata-Version: 1.1
Name: proteus
-Version: 3.4.3
+Version: 3.6.0
Summary: Library to access Tryton server as a client
Home-page: http://www.tryton.org/
Author: Tryton
Author-email: issue_tracker at tryton.org
License: LGPL-3
-Download-URL: http://downloads.tryton.org/3.4/
+Download-URL: http://downloads.tryton.org/3.6/
Description: proteus
=======
@@ -152,4 +152,6 @@ Classifier: Intended Audience :: Legal Industry
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Office/Business
diff --git a/proteus.egg-info/requires.txt b/proteus.egg-info/requires.txt
index 232d2df..0a70047 100644
--- a/proteus.egg-info/requires.txt
+++ b/proteus.egg-info/requires.txt
@@ -4,7 +4,7 @@ python-dateutil
cdecimal
[trytond]
-trytond >= 3.4, < 3.5
+trytond >= 3.6, < 3.7
[simplejson]
simplejson
\ No newline at end of file
diff --git a/proteus/__init__.py b/proteus/__init__.py
index 3c95c68..297d160 100644
--- a/proteus/__init__.py
+++ b/proteus/__init__.py
@@ -1,9 +1,9 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
'''
A library to access Tryton's models like a client.
'''
-__version__ = "3.4.3"
+__version__ = "3.6.0"
__all__ = ['Model', 'Wizard', 'Report']
import sys
try:
@@ -16,6 +16,7 @@ except ImportError:
sys.modules['cdecimal'] = decimal
import threading
import datetime
+import functools
from decimal import Decimal
from types import NoneType
@@ -73,6 +74,37 @@ class _EvalEnvironment(dict):
return item in self.parent._fields
+class dualmethod(object):
+ """Descriptor implementing combination of class and instance method
+
+ When called on an instance, the class is passed as the first argument and a
+ list with the instance as the second.
+ When called on a class, the class itself is passed as the first argument.
+
+ >>> class Example(object):
+ ... @dualmethod
+ ... def method(cls, instances):
+ ... print len(instances)
+ ...
+ >>> Example.method([Example()])
+ 1
+ >>> Example().method()
+ 1
+ """
+ def __init__(self, func):
+ self.func = func
+
+ def __get__(self, instance, owner):
+
+ @functools.wraps(self.func)
+ def newfunc(*args, **kwargs):
+ if instance:
+ return self.func(owner, [instance], *args, **kwargs)
+ else:
+ return self.func(owner, *args, **kwargs)
+ return newfunc
+
+
class FieldDescriptor(object):
default = None
@@ -94,10 +126,10 @@ class FieldDescriptor(object):
instance._values[self.name] = value
if previous != getattr(instance, self.name):
instance._changed.add(self.name)
- instance._on_change(self.name)
+ instance._on_change([self.name])
if instance._parent:
instance._parent._changed.add(instance._parent_field_name)
- instance._parent._on_change(instance._parent_field_name)
+ instance._parent._on_change([instance._parent_field_name])
class BooleanDescriptor(FieldDescriptor):
@@ -120,7 +152,7 @@ class BinaryDescriptor(FieldDescriptor):
default = None
def __set__(self, instance, value):
- assert isinstance(value, (basestring, buffer)) or value is None
+ assert isinstance(value, (bytes, bytearray)) or value is None
super(BinaryDescriptor, self).__set__(instance, value)
@@ -193,7 +225,19 @@ class TimeDescriptor(FieldDescriptor):
super(TimeDescriptor, self).__set__(instance, value)
+class TimeDeltaDescriptor(FieldDescriptor):
+ def __set__(self, instance, value):
+ assert isinstance(value, datetime.timedelta) or value is None
+ super(TimeDeltaDescriptor, self).__set__(instance, value)
+
+
class DictDescriptor(FieldDescriptor):
+ def __get__(self, instance, owner):
+ value = super(DictDescriptor, self).__get__(instance, owner)
+ if value:
+ value = value.copy()
+ return value
+
def __set__(self, instance, value):
assert isinstance(value, dict) or value is None
super(DictDescriptor, self).__set__(instance, value)
@@ -366,13 +410,13 @@ class MetaModelFactory(object):
'integer': IntegerDescriptor,
'biginteger': IntegerDescriptor,
'float': FloatDescriptor,
- 'float_time': FloatDescriptor,
'numeric': NumericDescriptor,
'reference': ReferenceDescriptor,
'date': DateDescriptor,
'datetime': DateTimeDescriptor,
'timestamp': DateTimeDescriptor,
'time': TimeDescriptor,
+ 'timedelta': TimeDeltaDescriptor,
'dict': DictDescriptor,
'many2one': Many2OneDescriptor,
'one2many': One2ManyDescriptor,
@@ -468,7 +512,7 @@ class ModelList(list):
'Signal change to parent'
if self.parent:
self.parent._changed.add(self.parent_field_name)
- self.parent._on_change(self.parent_field_name)
+ self.parent._on_change([self.parent_field_name])
def _get_context(self):
from .pyson import PYSONDecoder
@@ -532,13 +576,15 @@ class ModelList(list):
return res
pop.__doc__ = list.pop.__doc__
- def remove(self, record):
- self.record_deleted.add(record)
+ def remove(self, record, _changed=True):
+ if record.id >= 0:
+ self.record_deleted.add(record)
record._parent = None
record._parent_field_name = ''
record._parent_name = ''
res = super(ModelList, self).remove(record)
- self._changed()
+ if _changed:
+ self._changed()
return res
remove.__doc__ = list.remove.__doc__
@@ -660,6 +706,11 @@ class Model(object):
'The unique ID'
return self.__id
+ @id.setter
+ def id(self, value):
+ assert self.__id < 0
+ self.__id = int(value)
+
@classmethod
def find(cls, condition=None, offset=0, limit=None, order=None):
'Return records matching condition'
@@ -669,46 +720,91 @@ class Model(object):
cls._config.context)
return [cls(id) for id in ids]
- def reload(self):
+ @dualmethod
+ def reload(cls, records):
'Reload record'
- self._values = {}
- self._changed = set()
-
- def save(self):
- 'Save the record'
- context = self._config.context
- if self.id < 0:
- values = self._get_values()
- self.__id, = self._proxy.create([values], context)
- else:
- if not self._changed:
- return
- values = self._get_values(fields=self._changed)
- context['_timestamp'] = self._get_timestamp()
- self._proxy.write([self.id], values, context)
- self.reload()
-
- def delete(self):
- 'Delete the record'
- if self.id > 0:
- context = self._config.context
- context['_timestamp'] = self._get_timestamp()
- return self._proxy.delete([self.id], context)
- self.reload()
- return True
+ for record in records:
+ record._values = {}
+ record._changed = set()
- @classmethod
+ @dualmethod
+ def save(cls, records):
+ 'Save records'
+ if not records:
+ return
+ proxy = records[0]._proxy
+ config = records[0]._config
+ context = config.context
+ create, write = [], []
+ for record in records:
+ assert proxy == record._proxy
+ assert config == record._config
+ if record.id < 0:
+ create.append(record)
+ elif record._changed:
+ write.append(record)
+
+ if create:
+ values = [r._get_values() for r in create]
+ ids = proxy.create(values, context)
+ for record, id_ in zip(create, ids):
+ record.id = id_
+ if write:
+ values = []
+ context['_timestamp'] = {}
+ for record in write:
+ values.append([record.id])
+ values.append(record._get_values(fields=record._changed))
+ context['_timestamp'].update(record._get_timestamp())
+ values.append(context)
+ proxy.write(*values)
+ for record in records:
+ record.reload()
+
+ @dualmethod
+ def delete(cls, records):
+ 'Delete records'
+ if not records:
+ return
+ proxy = records[0]._proxy
+ config = records[0]._config
+ context = config.context
+ context['_timestamp'] = {}
+ delete = []
+ for record in records:
+ assert proxy == record._proxy
+ assert config == record._config
+ if record.id > 0:
+ context['_timestamp'].update(record._get_timestamp())
+ delete.append(record.id)
+ if delete:
+ proxy.delete(delete, context)
+ cls.reload(records)
+
+ @dualmethod
def duplicate(cls, records, default=None):
'Duplicate the record'
ids = cls._proxy.copy([r.id for r in records], default,
cls._config.context)
return [cls(id) for id in ids]
- def click(self, button):
+ @dualmethod
+ def click(cls, records, button, change=None):
'Click on button'
- self.save()
- self.reload() # Force reload because save doesn't always
- return getattr(self._proxy, button)([self.id], self._config.context)
+ if not records:
+ return
+
+ proxy = records[0]._proxy
+ context = records[0]._config.context
+ if change is None:
+ cls.save(records)
+ cls.reload(records) # Force reload because save doesn't always
+ return getattr(proxy, button)([r.id for r in records], context)
+ else:
+ record, = records
+ values = record._on_change_args(change)
+ changes = getattr(proxy, button)(values, context)
+ record._set_on_change(changes)
def _get_values(self, fields=None):
'Return dictionary values'
@@ -781,8 +877,7 @@ class Model(object):
else:
self._values[field] = value
fieldnames.append(field)
- for field in sorted(fieldnames):
- self._on_change(field)
+ self._on_change(sorted(fieldnames))
def _get_eval(self):
values = dict((x, getattr(self, '__%s_eval' % x))
@@ -845,7 +940,7 @@ class Model(object):
to_remove.append(record)
for record in to_remove:
# remove without signal
- list.remove(getattr(self, field), record)
+ getattr(self, field).remove(record, _changed=False)
if value and (value.get('add') or value.get('update')):
for index, vals in value.get('add', []):
relation = Model.get(self._fields[field]['relation'],
@@ -875,58 +970,70 @@ class Model(object):
self._values[field] = value
self._changed.add(field)
- def _on_change(self, name):
+ def _set_on_change(self, values):
+ later = {}
+ for field, value in values.iteritems():
+ if field not in self._fields:
+ continue
+ if self._fields[field]['type'] in ('one2many', 'many2many'):
+ later[field] = value
+ continue
+ self._on_change_set(field, value)
+ for field, value in later.iteritems():
+ self._on_change_set(field, value)
+
+ def _on_change(self, names):
'Call on_change for field'
# Import locally to not break installation
from proteus.pyson import PYSONDecoder
- definition = self._fields[name]
- if definition.get('on_change'):
- if isinstance(definition['on_change'], basestring):
- definition['on_change'] = PYSONDecoder().decode(
- definition['on_change'])
- args = self._on_change_args(definition['on_change'])
+
+ values = {}
+ for name in names:
+ definition = self._fields[name]
+ on_change = definition.get('on_change')
+ if not on_change:
+ continue
+ if isinstance(on_change, basestring):
+ definition['on_change'] = on_change = PYSONDecoder().decode(
+ on_change)
+ values.update(self._on_change_args(on_change))
+ if values:
context = self._config.context
- res = getattr(self._proxy, 'on_change_%s' % name)(args, context)
- later = {}
- for field, value in res.iteritems():
- if field not in self._fields:
- continue
- if self._fields[field]['type'] in ('one2many', 'many2many'):
- later[field] = value
- continue
- self._on_change_set(field, value)
- for field, value in later.iteritems():
- self._on_change_set(field, value)
+ changes = getattr(self._proxy, 'on_change')(values, names, context)
+ for change in changes:
+ self._set_on_change(change)
+
values = {}
+ fieldnames = set(names)
to_change = set()
later = set()
for field, definition in self._fields.iteritems():
- if not definition.get('on_change_with'):
+ on_change_with = definition.get('on_change_with')
+ if not on_change_with:
continue
- if name not in definition['on_change_with']:
+ if not fieldnames & set(on_change_with):
continue
- if field == name:
- continue
- if to_change & set(definition['on_change_with']):
+ if to_change & set(on_change_with):
later.add(field)
+ continue
to_change.add(field)
- values.update(self._on_change_args(definition['on_change_with']))
+ values.update(self._on_change_args(on_change_with))
if to_change:
context = self._config.context
- result = getattr(self._proxy, 'on_change_with')(values,
+ changes = getattr(self._proxy, 'on_change_with')(values,
list(to_change), context)
- for field, value in result.items():
- self._on_change_set(field, value)
+ self._set_on_change(changes)
for field in later:
- definition = self._fields[field]
- values = self._on_change_args(definition['on_change_with'])
+ on_change_with = self._fields[field]['on_change_with']
+ values = self._on_change_args(on_change_with)
context = self._config.context
result = getattr(self._proxy, 'on_change_with_%s' % field)(values,
context)
self._on_change_set(field, result)
+
if self._parent:
self._parent._changed.add(self._parent_field_name)
- self._parent._on_change(self._parent_field_name)
+ self._parent._on_change([self._parent_field_name])
class Wizard(object):
@@ -997,6 +1104,9 @@ class Wizard(object):
if self.state == self.end_state:
self._proxy.delete(self.session_id, self._config.context)
+ if self.models:
+ for record in self.models:
+ record.reload()
class Report(object):
diff --git a/proteus/config.py b/proteus/config.py
index 3aeeb35..c80dc49 100644
--- a/proteus/config.py
+++ b/proteus/config.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
"""
Configuration functions for the proteus package for Tryton.
"""
@@ -20,7 +20,7 @@ def dump_decimal(self, value, write):
self.dump_struct(value, write)
-def dump_buffer(self, value, write):
+def dump_bytes(self, value, write):
self.write = write
value = xmlrpclib.Binary(value)
value.encode(self)
@@ -45,10 +45,19 @@ def dump_time(self, value, write):
}
self.dump_struct(value, write)
+
+def dump_timedelta(self, value, write):
+ value = {'__class__': 'timedelta',
+ 'seconds': value.total_seconds(),
+ }
+ self.dump_struct(value, write)
+
xmlrpclib.Marshaller.dispatch[Decimal] = dump_decimal
xmlrpclib.Marshaller.dispatch[datetime.date] = dump_date
xmlrpclib.Marshaller.dispatch[datetime.time] = dump_time
-xmlrpclib.Marshaller.dispatch[buffer] = dump_buffer
+xmlrpclib.Marshaller.dispatch[datetime.timedelta] = dump_timedelta
+xmlrpclib.Marshaller.dispatch[bytes] = dump_bytes
+xmlrpclib.Marshaller.dispatch[bytearray] = dump_bytes
class XMLRPCDecoder(object):
@@ -70,6 +79,8 @@ XMLRPCDecoder.register('date',
XMLRPCDecoder.register('time',
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
dct['microsecond']))
+XMLRPCDecoder.register('timedelta',
+ lambda dct: datetime.timedelta(seconds=dct['seconds']))
XMLRPCDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
@@ -191,13 +202,14 @@ class TrytondProxy(object):
class TrytondConfig(Config):
'Configuration for trytond'
- def __init__(self, database=None, user='admin', language='en_US',
- password='', config_file=os.environ.get('TRYTOND_CONFIG')):
+ def __init__(self, database=None, user='admin', config_file=None):
super(TrytondConfig, self).__init__()
if not database:
database = os.environ.get('TRYTOND_DATABASE_URI')
else:
os.environ['TRYTOND_DATABASE_URI'] = database
+ if not config_file:
+ config_file = os.environ.get('TRYTOND_CONFIG')
from trytond.config import config
config.update_etc(config_file)
from trytond.pool import Pool
@@ -261,11 +273,10 @@ class TrytondConfig(Config):
return methods
-def set_trytond(database=None, user='admin', language='en_US', password='',
+def set_trytond(database=None, user='admin',
config_file=None):
'Set trytond package as backend'
- _CONFIG.current = TrytondConfig(database, user, language=language,
- password=password, config_file=config_file)
+ _CONFIG.current = TrytondConfig(database, user, config_file=config_file)
return _CONFIG.current
@@ -286,10 +297,11 @@ class XmlrpcProxy(object):
class XmlrpcConfig(Config):
'Configuration for XML-RPC'
- def __init__(self, url):
+ def __init__(self, url, **kwargs):
super(XmlrpcConfig, self).__init__()
self.url = url
- self.server = xmlrpclib.ServerProxy(url, allow_none=1, use_datetime=1)
+ self.server = xmlrpclib.ServerProxy(
+ url, allow_none=1, use_datetime=1, **kwargs)
# TODO add user
self.user = None
self._context = self.server.model.res.user.get_preferences(True, {})
@@ -320,9 +332,12 @@ class XmlrpcConfig(Config):
and '.' not in x[len(object_) + 1:]]
-def set_xmlrpc(url):
- 'Set XML-RPC as backend'
- _CONFIG.current = XmlrpcConfig(url)
+def set_xmlrpc(url, **kwargs):
+ '''
+ Set XML-RPC as backend.
+ It pass the keyword arguments received to xmlrpclib.ServerProxy()
+ '''
+ _CONFIG.current = XmlrpcConfig(url, **kwargs)
return _CONFIG.current
diff --git a/proteus/pyson.py b/proteus/pyson.py
index 9e354d9..d90cd8a 100644
--- a/proteus/pyson.py
+++ b/proteus/pyson.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
__all__ = ['PYSONEncoder', 'PYSONDecoder', 'Eval', 'Not', 'Bool', 'And', 'Or',
'Equal', 'Greater', 'Less', 'If', 'Get', 'In', 'Date', 'DateTime', 'Len']
try:
@@ -8,7 +8,23 @@ except ImportError:
import json
import datetime
from dateutil.relativedelta import relativedelta
-from functools import reduce
+from functools import reduce, wraps
+
+
+def reduced_type(types):
+ types = types.copy()
+ for k, r in [(long, int), (str, basestring), (unicode, basestring)]:
+ if k in types:
+ types.remove(k)
+ types.add(r)
+ return types
+
+
+def reduce_type(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ return reduced_type(func(*args, **kwargs))
+ return wrapper
class PYSON(object):
@@ -30,25 +46,25 @@ class PYSON(object):
return Not(self)
def __and__(self, other):
+ if (isinstance(other, PYSON)
+ and other.types() != set([bool])):
+ other = Bool(other)
if (isinstance(self, And)
and not isinstance(self, Or)):
self._statements.append(other)
return self
- if (isinstance(other, PYSON)
- and other.types() != set([bool])):
- other = Bool(other)
if self.types() != set([bool]):
return And(Bool(self), other)
else:
return And(self, other)
def __or__(self, other):
- if isinstance(self, Or):
- self._statements.append(other)
- return self
if (isinstance(other, PYSON)
and other.types() != set([bool])):
other = Bool(other)
+ if isinstance(self, Or):
+ self._statements.append(other)
+ return self
if self.types() != set([bool]):
return Or(Bool(self), other)
else:
@@ -81,6 +97,14 @@ class PYSON(object):
def contains(self, k):
return In(k, self)
+ def __repr__(self):
+ klass = self.__class__.__name__
+ return '%s(%s)' % (klass, ', '.join(map(repr, self.__repr_params__)))
+
+ @property
+ def __repr_params__(self):
+ return NotImplementedError
+
class PYSONEncoder(json.JSONEncoder):
@@ -99,24 +123,34 @@ class PYSONEncoder(json.JSONEncoder):
class PYSONDecoder(json.JSONDecoder):
- def __init__(self, context=None):
+ def __init__(self, context=None, noeval=False):
self.__context = context or {}
+ self.noeval = noeval
super(PYSONDecoder, self).__init__(object_hook=self._object_hook)
def _object_hook(self, dct):
if '__class__' in dct:
- klass = globals().get(dct['__class__'])
- if klass and hasattr(klass, 'eval'):
- return klass.eval(dct, self.__context)
+ klass = CONTEXT.get(dct['__class__'])
+ if klass:
+ if not self.noeval:
+ return klass.eval(dct, self.__context)
+ else:
+ dct = dct.copy()
+ del dct['__class__']
+ return klass(**dct)
return dct
class Eval(PYSON):
- def __init__(self, value, default=''):
+ def __init__(self, v, d=''):
super(Eval, self).__init__()
- self._value = value
- self._default = default
+ self._value = v
+ self._default = d
+
+ @property
+ def __repr_params__(self):
+ return self._value, self._default
def pyson(self):
return {
@@ -125,6 +159,7 @@ class Eval(PYSON):
'd': self._default,
}
+ @reduce_type
def types(self):
if isinstance(self._default, PYSON):
return self._default.types()
@@ -138,13 +173,17 @@ class Eval(PYSON):
class Not(PYSON):
- def __init__(self, value):
+ def __init__(self, v):
super(Not, self).__init__()
- if isinstance(value, PYSON):
- assert value.types() == set([bool]), 'value must be boolean'
+ if isinstance(v, PYSON):
+ assert v.types() == set([bool]), 'value must be boolean'
else:
- assert isinstance(value, bool), 'value must be boolean'
- self._value = value
+ assert isinstance(v, bool), 'value must be boolean'
+ self._value = v
+
+ @property
+ def __repr_params__(self):
+ return (self._value,)
def pyson(self):
return {
@@ -162,9 +201,13 @@ class Not(PYSON):
class Bool(PYSON):
- def __init__(self, value):
+ def __init__(self, v):
super(Bool, self).__init__()
- self._value = value
+ self._value = v
+
+ @property
+ def __repr_params__(self):
+ return (self._value,)
def pyson(self):
return {
@@ -182,8 +225,9 @@ class Bool(PYSON):
class And(PYSON):
- def __init__(self, *statements):
+ def __init__(self, *statements, **kwargs):
super(And, self).__init__()
+ statements = list(statements) + kwargs.get('s', [])
for statement in statements:
if isinstance(statement, PYSON):
assert statement.types() == set([bool]), \
@@ -192,7 +236,11 @@ class And(PYSON):
assert isinstance(statement, bool), \
'statement must be boolean'
assert len(statements) >= 2, 'must have at least 2 statements'
- self._statements = list(statements)
+ self._statements = statements
+
+ @property
+ def __repr_params__(self):
+ return tuple(self._statements)
def pyson(self):
return {
@@ -222,20 +270,25 @@ class Or(And):
class Equal(PYSON):
- def __init__(self, statement1, statement2):
+ def __init__(self, s1, s2):
+ statement1, statement2 = s1, s2
super(Equal, self).__init__()
if isinstance(statement1, PYSON):
types1 = statement1.types()
else:
- types1 = set([type(statement1)])
+ types1 = reduced_type(set([type(s1)]))
if isinstance(statement2, PYSON):
types2 = statement2.types()
else:
- types2 = set([type(statement2)])
+ types2 = reduced_type(set([type(s2)]))
assert types1 == types2, 'statements must have the same type'
self._statement1 = statement1
self._statement2 = statement2
+ @property
+ def __repr_params__(self):
+ return (self._statement1, self._statement2)
+
def pyson(self):
return {
'__class__': 'Equal',
@@ -253,7 +306,8 @@ class Equal(PYSON):
class Greater(PYSON):
- def __init__(self, statement1, statement2, equal=False):
+ def __init__(self, s1, s2, e=False):
+ statement1, statement2, equal = s1, s2, e
super(Greater, self).__init__()
for i in (statement1, statement2):
if isinstance(i, PYSON):
@@ -270,6 +324,10 @@ class Greater(PYSON):
self._statement2 = statement2
self._equal = equal
+ @property
+ def __repr_params__(self):
+ return (self._statement1, self._statement2, self._equal)
+
def pyson(self):
return {
'__class__': 'Greater',
@@ -282,7 +340,16 @@ class Greater(PYSON):
return set([bool])
@staticmethod
+ def _convert(dct):
+ for i in ('s1', 's2'):
+ if not isinstance(dct[i], (int, long, float)):
+ dct = dct.copy()
+ dct[i] = float(dct[i])
+ return dct
+
+ @staticmethod
def eval(dct, context):
+ dct = Greater._convert(dct)
if dct['e']:
return dct['s1'] >= dct['s2']
else:
@@ -298,6 +365,7 @@ class Less(Greater):
@staticmethod
def eval(dct, context):
+ dct = Less._convert(dct)
if dct['e']:
return dct['s1'] <= dct['s2']
else:
@@ -306,7 +374,8 @@ class Less(Greater):
class If(PYSON):
- def __init__(self, condition, then_statement, else_statement=None):
+ def __init__(self, c, t, e=None):
+ condition, then_statement, else_statement = c, t, e
super(If, self).__init__()
if isinstance(condition, PYSON):
assert condition.types() == set([bool]), \
@@ -316,17 +385,21 @@ class If(PYSON):
if isinstance(then_statement, PYSON):
then_types = then_statement.types()
else:
- then_types = set([type(then_statement)])
+ then_types = reduced_type(set([type(then_statement)]))
if isinstance(else_statement, PYSON):
- assert then_types == else_statement.types(), \
- 'then and else statements must be the same type'
+ else_types = else_statement.types()
else:
- assert then_types == set([type(else_statement)]), \
- 'then and else statements must be the same type'
+ else_types = reduced_type(set([type(else_statement)]))
+ assert then_types == else_types, \
+ 'then and else statements must be the same type'
self._condition = condition
self._then_statement = then_statement
self._else_statement = else_statement
+ @property
+ def __repr_params__(self):
+ return (self._condition, self._then_statement, self._else_statement)
+
def pyson(self):
return {
'__class__': 'If',
@@ -335,6 +408,7 @@ class If(PYSON):
'e': self._else_statement,
}
+ @reduce_type
def types(self):
if isinstance(self._then_statement, PYSON):
return self._then_statement.types()
@@ -351,7 +425,8 @@ class If(PYSON):
class Get(PYSON):
- def __init__(self, obj, key, default=''):
+ def __init__(self, v, k, d=''):
+ obj, key, default = v, k, d
super(Get, self).__init__()
if isinstance(obj, PYSON):
assert obj.types() == set([dict]), 'obj must be a dict'
@@ -359,12 +434,16 @@ class Get(PYSON):
assert isinstance(obj, dict), 'obj must be a dict'
self._obj = obj
if isinstance(key, PYSON):
- assert key.types() == set([str]), 'key must be a string'
+ assert key.types() == set([basestring]), 'key must be a string'
else:
- assert type(key) == str, 'key must be a string'
+ assert isinstance(key, basestring), 'key must be a string'
self._key = key
self._default = default
+ @property
+ def __repr_params__(self):
+ return (self._obj, self._key, self._default)
+
def pyson(self):
return {
'__class__': 'Get',
@@ -373,6 +452,7 @@ class Get(PYSON):
'd': self._default,
}
+ @reduce_type
def types(self):
if isinstance(self._default, PYSON):
return self._default.types()
@@ -386,26 +466,31 @@ class Get(PYSON):
class In(PYSON):
- def __init__(self, key, obj):
+ def __init__(self, k, v):
+ key, obj = k, v
super(In, self).__init__()
if isinstance(key, PYSON):
- assert key.types().issubset(set([str, int, long])), \
+ assert key.types().issubset(set([basestring, int])), \
'key must be a string or an integer or a long'
else:
- assert type(key) in [str, int, long], \
+ assert isinstance(key, (basestring, int, long)), \
'key must be a string or an integer or a long'
if isinstance(obj, PYSON):
assert obj.types().issubset(set([dict, list])), \
'obj must be a dict or a list'
if obj.types() == set([dict]):
- assert type(key) == str, 'key must be a string'
+ assert isinstance(key, basestring), 'key must be a string'
else:
- assert type(obj) in [dict, list]
- if type(obj) == dict:
- assert type(key) == str, 'key must be a string'
+ assert isinstance(obj, (dict, list))
+ if isinstance(obj, dict):
+ assert isinstance(key, basestring), 'key must be a string'
self._key = key
self._obj = obj
+ @property
+ def __repr_params__(self):
+ return (self._key, self._obj)
+
def pyson(self):
return {
'__class__': 'In',
@@ -424,7 +509,13 @@ class In(PYSON):
class Date(PYSON):
def __init__(self, year=None, month=None, day=None,
- delta_years=0, delta_months=0, delta_days=0):
+ delta_years=0, delta_months=0, delta_days=0, **kwargs):
+ year = kwargs.get('y', year)
+ month = kwargs.get('M', month)
+ day = kwargs.get('d', day)
+ delta_years = kwargs.get('dy', delta_years)
+ delta_months = kwargs.get('dM', delta_months)
+ delta_days = kwargs.get('dd', delta_days)
super(Date, self).__init__()
for i in (year, month, day, delta_years, delta_months, delta_days):
if isinstance(i, PYSON):
@@ -440,6 +531,11 @@ class Date(PYSON):
self._delta_months = delta_months
self._delta_days = delta_days
+ @property
+ def __repr_params__(self):
+ return (self._year, self._month, self._day,
+ self._delta_years, self._delta_months, self._delta_days)
+
def pyson(self):
return {
'__class__': 'Date',
@@ -472,14 +568,22 @@ class DateTime(Date):
hour=None, minute=None, second=None, microsecond=None,
delta_years=0, delta_months=0, delta_days=0,
delta_hours=0, delta_minutes=0, delta_seconds=0,
- delta_microseconds=0):
+ delta_microseconds=0, **kwargs):
+ hour = kwargs.get('h', hour)
+ minute = kwargs.get('m', minute)
+ second = kwargs.get('s', second)
+ microsecond = kwargs.get('ms', microsecond)
+ delta_hours = kwargs.get('dh', delta_hours)
+ delta_minutes = kwargs.get('dm', delta_minutes)
+ delta_seconds = kwargs.get('ds', delta_seconds)
+ delta_microseconds = kwargs.get('dms', delta_microseconds)
super(DateTime, self).__init__(year=year, month=month, day=day,
delta_years=delta_years, delta_months=delta_months,
- delta_days=delta_days)
+ delta_days=delta_days, **kwargs)
for i in (hour, minute, second, microsecond,
delta_hours, delta_minutes, delta_seconds, delta_microseconds):
if isinstance(i, PYSON):
- assert i.types() == set([int, long, type(None)]), \
+ assert i.types() == set([int, type(None)]), \
'%s must be an integer or None' % (i,)
else:
assert isinstance(i, (int, long, type(None))), \
@@ -493,6 +597,15 @@ class DateTime(Date):
self._delta_seconds = delta_seconds
self._delta_microseconds = delta_microseconds
+ @property
+ def __repr_params__(self):
+ date_params = super(DateTime, self).__repr_params__
+ return (date_params[:3]
+ + (self._hour, self._minute, self._second, self._microsecond)
+ + date_params[3:]
+ + (self._delta_hours, self._delta_minutes, self._delta_seconds,
+ self._delta_microseconds))
+
def pyson(self):
res = super(DateTime, self).pyson()
res['__class__'] = 'DateTime'
@@ -531,15 +644,19 @@ class DateTime(Date):
class Len(PYSON):
- def __init__(self, value):
+ def __init__(self, v):
super(Len, self).__init__()
- if isinstance(value, PYSON):
- assert value.types().issubset(set([dict, list, str])), \
+ if isinstance(v, PYSON):
+ assert v.types().issubset(set([dict, list, basestring])), \
'value must be a dict or a list or a string'
else:
- assert type(value) in [dict, list, str], \
+ assert isinstance(v, (dict, list, basestring)), \
'value must be a dict or list or a string'
- self._value = value
+ self._value = v
+
+ @property
+ def __repr_params__(self):
+ return (self._value,)
def pyson(self):
return {
@@ -548,13 +665,12 @@ class Len(PYSON):
}
def types(self):
- return set([int, long])
+ return set([int])
@staticmethod
def eval(dct, context):
return len(dct['v'])
-
CONTEXT = {
'Eval': Eval,
'Not': Not,
@@ -570,4 +686,4 @@ CONTEXT = {
'Date': Date,
'DateTime': DateTime,
'Len': Len,
-}
+ }
diff --git a/proteus/tests/__init__.py b/proteus/tests/__init__.py
index baddecf..a2eb873 100644
--- a/proteus/tests/__init__.py
+++ b/proteus/tests/__init__.py
@@ -1,5 +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.
+# 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
import sys
import unittest
diff --git a/proteus/tests/common.py b/proteus/tests/common.py
index 15e1622..81afcf7 100644
--- a/proteus/tests/common.py
+++ b/proteus/tests/common.py
@@ -1,5 +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.
+# 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
from unittest import TestCase
diff --git a/proteus/tests/test_config.py b/proteus/tests/test_config.py
index 2ad83f2..acec337 100644
--- a/proteus/tests/test_config.py
+++ b/proteus/tests/test_config.py
@@ -1,5 +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.
+# 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 proteus.config
from .common import ProteusTestCase
@@ -24,3 +24,9 @@ class TestConfig(ProteusTestCase):
self.assertEqual(config1, config2)
self.assertRaises(NotImplementedError, config1.__eq__, None)
+
+ def test_repr(self):
+ config = proteus.config.TrytondConfig()
+ self.assertEqual(repr(config),
+ "proteus.config.TrytondConfig("
+ "'sqlite://', 'admin', config_file=None)")
diff --git a/proteus/tests/test_model.py b/proteus/tests/test_model.py
index f6aa197..f0dd983 100644
--- a/proteus/tests/test_model.py
+++ b/proteus/tests/test_model.py
@@ -1,5 +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.
+# 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 proteus import Model
from .common import ProteusTestCase
@@ -126,6 +126,15 @@ class TestModel(ProteusTestCase):
self.assertEqual(test2.name, 'Test 2')
self.assertEqual(test2.login, 'test2')
+ test.signature = 'classmethod save'
+ test2.name = 'Test 2 classmethod'
+ test3 = User(name='Test 3', login='test3')
+ User.save([test, test2, test3])
+ self.assertEqual(test.signature, 'classmethod save')
+ self.assertEqual(test2.name, 'Test 2 classmethod')
+ self.assert_(test3.id > 0)
+ self.assertEqual(test3.name, 'Test 3')
+
def test_save_many2one(self):
User = Model.get('res.user')
test = User()
diff --git a/proteus/tests/test_report.py b/proteus/tests/test_report.py
index a04f643..3df9fc5 100644
--- a/proteus/tests/test_report.py
+++ b/proteus/tests/test_report.py
@@ -1,5 +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.
+# 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 unittest
try:
import pydot
diff --git a/proteus/tests/test_wizard.py b/proteus/tests/test_wizard.py
index 68b5226..0a812c2 100644
--- a/proteus/tests/test_wizard.py
+++ b/proteus/tests/test_wizard.py
@@ -1,5 +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.
+# 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 proteus import Wizard, Model
from .common import ProteusTestCase
diff --git a/setup.py b/setup.py
index e551c36..b7e9014 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 setuptools import setup, find_packages
import os
import proteus
@@ -58,6 +58,8 @@ setup(name=name,
'GNU Library or Lesser General Public License (LGPL)',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Office/Business',
],
platforms='any',
--
tryton-proteus
More information about the tryton-debian-vcs
mailing list