[tryton-debian-vcs] tryton-proteus branch debian updated. debian/3.4.3-1-2-ge22dc8c

Mathias Behrle tryton-debian-vcs at alioth.debian.org
Thu Apr 23 16:08:03 UTC 2015


The following commit has been merged in the debian branch:
https://alioth.debian.org/plugins/scmgit/cgi-bin/gitweb.cgi/?p=tryton/tryton-proteus.git;a=commitdiff;h=debian/3.4.3-1-2-ge22dc8c

commit e22dc8cab68252fa025e2a334dc8b0445a1e5a93
Author: Mathias Behrle <mathiasb at m9s.biz>
Date:   Thu Apr 23 17:00:11 2015 +0200

    Merging upstream version 3.6.0.

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