From 3e83fc58aad8c87c574b249c158552ed8b9cf94f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Tue, 21 Jul 2015 13:37:37 +0300 Subject: [PATCH 01/23] A little bit of python 3 support work --- .travis.yml | 4 +++ openerp_proxy/__init__.py | 8 +++--- openerp_proxy/connection/__init__.py | 6 ++-- openerp_proxy/connection/connection.py | 4 +-- openerp_proxy/connection/jsonrpc.py | 31 ++++++++++----------- openerp_proxy/connection/xmlrpc.py | 9 ++++-- openerp_proxy/core.py | 14 +++++----- openerp_proxy/ext/workflow.py | 4 +-- openerp_proxy/orm/cache.py | 7 +++-- openerp_proxy/orm/object.py | 4 +-- openerp_proxy/orm/record.py | 25 ++++++++--------- openerp_proxy/plugin.py | 11 ++++---- openerp_proxy/plugins/graph.py | 2 +- openerp_proxy/service/db.py | 4 +-- openerp_proxy/service/object.py | 2 +- openerp_proxy/service/report.py | 7 +++-- openerp_proxy/service/service.py | 14 ++++++---- openerp_proxy/session.py | 17 +++++++----- openerp_proxy/tests/test_client.py | 38 +++++++++++++++++++++++++- setup.py | 3 +- 20 files changed, 134 insertions(+), 80 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9c509cd..9875016 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ language: python python: - "2.7" + - "3.1" + - "3.2" + - "3.3" + - "3.4" env: - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='xml-rpc' diff --git a/openerp_proxy/__init__.py b/openerp_proxy/__init__.py index dda144b..830660d 100644 --- a/openerp_proxy/__init__.py +++ b/openerp_proxy/__init__.py @@ -36,17 +36,17 @@ class returned by connect() method of session object. def main(): """ Entry point for running as standalone APP """ - from session import Session - from core import Client + from .session import Session + from .core import Client session = Session() header_databases = "\n" - for index, url in session.index.iteritems(): + for index, url in session.index.items(): header_databases += " - [%3s] %s\n" % (index, url) header_aliases = "\n" - for aliase, url in session.aliases.iteritems(): + for aliase, url in session.aliases.items(): header_aliases += " - %7s: %s\n" % (aliase, url) header = HELP_HEADER % {'databases': header_databases, 'aliases': header_aliases} diff --git a/openerp_proxy/connection/__init__.py b/openerp_proxy/connection/__init__.py index 5e82a8d..590b655 100644 --- a/openerp_proxy/connection/__init__.py +++ b/openerp_proxy/connection/__init__.py @@ -1,3 +1,3 @@ -import xmlrpc -import jsonrpc -from connection import * +import openerp_proxy.connection.xmlrpc +import openerp_proxy.connection.jsonrpc +from .connection import * diff --git a/openerp_proxy/connection/connection.py b/openerp_proxy/connection/connection.py index b80215f..38d275f 100644 --- a/openerp_proxy/connection/connection.py +++ b/openerp_proxy/connection/connection.py @@ -1,3 +1,4 @@ +import six from extend_me import ExtensibleByHashType __all__ = ('get_connector', 'get_connector_names', 'ConnectorBase') @@ -17,10 +18,9 @@ def get_connector_names(): return ConnectorType.get_registered_names() -class ConnectorBase(object): +class ConnectorBase(six.with_metaclass(ConnectorType)): """ Base class for all connectors """ - __metaclass__ = ConnectorType def __init__(self, host, port, verbose=False): self.host = host diff --git a/openerp_proxy/connection/jsonrpc.py b/openerp_proxy/connection/jsonrpc.py index ec40829..99df487 100644 --- a/openerp_proxy/connection/jsonrpc.py +++ b/openerp_proxy/connection/jsonrpc.py @@ -1,11 +1,12 @@ # python imports import json -import urllib2 import random +#import urllib2 +import requests + # project imports -from openerp_proxy.connection.connection import ConnectorBase -from openerp_proxy.utils import ustr +from .connection import ConnectorBase import openerp_proxy.exceptions as exceptions @@ -28,9 +29,6 @@ def __unicode__(self): def __str__(self): return unicode(self).encode('utf-8') - def _repr_pretty_(self): - return "TEST" - class JSONRPCMethod(object): """ Class wrapper around XML-RPC method to wrap xmlrpclib.Fault @@ -54,20 +52,21 @@ def __call__(self, *args): }, "id": random.randint(0, 1000000000), } - req = urllib2.Request(url=self.__url, data=json.dumps(data), headers={ - "Content-Type": "application/json", - }) - result = urllib2.urlopen(req) - content = result.read() try: - result = json.loads(content) + res = requests.post(self.__url, data=json.dumps(data), headers={ + "Content-Type": "application/json", + }) + except requests.exceptions.RequestException: + raise JSONRPCError("Cannot connect to url %s" % self.__url) + + try: + result = json.loads(res.text) except ValueError: info = { "original_url": self.__url, - "url": result.geturl(), - "info": result.info(), - "code": result.getcode(), - "content": content, + "url": res.url, + "code": res.status_code, + "content": res.text, } raise JSONRPCError("Cannot decode JSON: %s" % info) diff --git a/openerp_proxy/connection/xmlrpc.py b/openerp_proxy/connection/xmlrpc.py index ef55f95..83407f7 100644 --- a/openerp_proxy/connection/xmlrpc.py +++ b/openerp_proxy/connection/xmlrpc.py @@ -1,8 +1,13 @@ # python imports -import xmlrpclib +import sys + +if sys.version_info < (3, 0, 0): + import xmlrpclib +else: + import xmlrpc.client as xmlrpclib # project imports -from openerp_proxy.connection.connection import ConnectorBase +from .connection import ConnectorBase from openerp_proxy.utils import ustr import openerp_proxy.exceptions as exceptions diff --git a/openerp_proxy/core.py b/openerp_proxy/core.py index 180a6bc..7e75184 100644 --- a/openerp_proxy/core.py +++ b/openerp_proxy/core.py @@ -48,12 +48,12 @@ # project imports -from openerp_proxy.connection import get_connector -from openerp_proxy.exceptions import (Error, - ClientException, - LoginException) -from openerp_proxy.service import ServiceManager -from openerp_proxy.plugin import PluginManager +from .connection import get_connector +from .exceptions import (Error, + ClientException, + LoginException) +from .service import ServiceManager +from .plugin import PluginManager # Activate orm internal logic @@ -61,7 +61,7 @@ # thay woudld like to use. Or simply create two entry points (one with all # enabled by default and another with only basic stuff which may be useful for # libraries that would like to get speed instead of better usability -import openerp_proxy.orm +from . import orm from extend_me import Extensible diff --git a/openerp_proxy/ext/workflow.py b/openerp_proxy/ext/workflow.py index 17bdf86..7739bc3 100644 --- a/openerp_proxy/ext/workflow.py +++ b/openerp_proxy/ext/workflow.py @@ -5,7 +5,7 @@ Also it provides simple methods to easily send workflow signals to records from Object and Record interfaces. """ - +import numbers from openerp_proxy.orm.record import Record from openerp_proxy.orm.record import ObjectRecords from openerp_proxy.exceptions import ObjectException @@ -42,7 +42,7 @@ def workflow(self): def workflow_signal(self, obj_id, signal): """ Triggers specified signal for object's workflow """ - assert isinstance(obj_id, (int, long)), "obj_id must be integer" + assert isinstance(obj_id, numbers.Integral), "obj_id must be integer" assert isinstance(signal, basestring), "signal must be string" return self.service.execute_wkf(self.name, signal, obj_id) diff --git a/openerp_proxy/orm/cache.py b/openerp_proxy/orm/cache.py index 911ae2c..2c3f555 100644 --- a/openerp_proxy/orm/cache.py +++ b/openerp_proxy/orm/cache.py @@ -1,4 +1,5 @@ #import openerp_proxy.orm.record +import six import collections __all__ = ('empty_cache') @@ -32,7 +33,7 @@ def update_keys(self, keys): # and difference calls) self.update({cid: {'id': cid} for cid in keys}) else: - self.update({cid: {'id': cid} for cid in set(keys).difference(self.viewkeys())}) + self.update({cid: {'id': cid} for cid in set(keys).difference(six.viewkeys(self))}) return self def update_context(self, new_context): @@ -51,7 +52,7 @@ def update_context(self, new_context): def get_ids_to_read(self, field): """ Return list of ids, that have no specified field in cache """ - return [key for key, val in self.viewitems() if field not in val] + return [key for key, val in six.viewitems(self) if field not in val] def cache_field(self, rid, ftype, field_name, value): """ This method impelment additional caching functionality, @@ -66,7 +67,7 @@ def cache_field(self, rid, ftype, field_name, value): if value and ftype == 'many2one': rcache = self._root_cache[self._object.columns_info[field_name]['relation']] - if isinstance(value, (int, long)): + if isinstance(value, numbers.Integral): rcache[value] # internal dict {'id': key} will be created by default (see ObjectCache) elif isinstance(value, (list, tuple)): rcache[value[0]]['__name_get_result'] = value[1] diff --git a/openerp_proxy/orm/object.py b/openerp_proxy/orm/object.py index 470adda..3b7cbfa 100644 --- a/openerp_proxy/orm/object.py +++ b/openerp_proxy/orm/object.py @@ -1,3 +1,4 @@ +import six from extend_me import ExtensibleByHashType from openerp_proxy.utils import AttrDict @@ -24,7 +25,7 @@ def get_object(proxy, name): # TODO: think about connecting it to service instead of Proxy -class Object(object): +class Object(six.with_metaclass(ObjectType)): """ Base class for all Objects Provides simple interface to remote osv.osv objects @@ -33,7 +34,6 @@ class Object(object): sale_obj = Object(erp, 'sale.order') sale_obj.search([('state','not in',['done','cancel'])]) """ - __metaclass__ = ObjectType def __init__(self, service, object_name): self._service = service diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index 7d788cf..416e13c 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -4,8 +4,10 @@ from openerp_proxy.orm.cache import empty_cache from extend_me import ExtensibleType +import six import collections import abc +import numbers __all__ = ( @@ -37,7 +39,7 @@ def get_record(obj, rid, cache=None, context=None): return RecordMeta.get_object(obj, rid, cache=cache, context=context) -class Record(object): +class Record(six.with_metaclass(RecordMeta, object)): """ Base class for all Records Constructor @@ -50,12 +52,11 @@ class Record(object): Note, to create instance of cache call *empty_cache* """ - __metaclass__ = RecordMeta __slots__ = ['__dict__', '_object', '_cache', '_lcache', '_id'] def __init__(self, obj, rid, cache=None, context=None): assert isinstance(obj, Object), "obj should be Object" - assert isinstance(rid, (int, long)), "rid must be int" + assert isinstance(rid, numbers.Integral), "rid must be int" self._id = rid self._object = obj @@ -144,7 +145,7 @@ def __eq__(self, other): if isinstance(other, Record): return other.id == self._id - if isinstance(other, (int, long)): + if isinstance(other, numbers.Integral): return self._id == other return False @@ -255,7 +256,7 @@ def get_record_list(obj, ids=None, fields=None, cache=None, context=None): return RecordListMeta.get_object(obj, ids, fields=fields, cache=cache, context=context) -class RecordList(collections.MutableSequence): +class RecordList(six.with_metaclass(RecordListMeta), collections.MutableSequence): """Class to hold list of records with some extra functionality :param obj: instance of Object to make this list related to @@ -270,8 +271,6 @@ class RecordList(collections.MutableSequence): :type context: dict """ - __metaclass__ = RecordListMeta - __slots__ = ('_object', '_cache', '_lcache', '_records') # TODO: expose object's methods via implementation of __dir__ @@ -364,7 +363,7 @@ def __len__(self): return self.length def __contains__(self, item): - if isinstance(item, (int, long)): + if isinstance(item, numbers.Integral): return item in self.ids if isinstance(item, Record): return item in self._records @@ -373,13 +372,13 @@ def __contains__(self, item): def insert(self, index, item): """ Insert record to list - :param item: Record instance to be inserted into list. if int or long passed, it considered to be ID of record - :type item: Record|int|long + :param item: Record instance to be inserted into list. if int passed, it considered to be ID of record + :type item: Record|int :param int index: position where to place new element :return: self :rtype: RecordList """ - assert isinstance(item, (Record, int, long)), "Only Record or int or long instances could be added to list" + assert isinstance(item, (Record, numbers.Integral)), "Only Record or int instances could be added to list" if isinstance(item, Record): self._records.insert(index, item) else: @@ -658,9 +657,9 @@ def read_records(self, ids, fields=None, context=None, cache=None): >>> for order in data: order.write({'note': 'order data is %s'%order.data}) """ - assert isinstance(ids, (int, long, list, tuple)), "ids must be instance of (int, long, list, tuple)" + assert isinstance(ids, (numbers.Integral, list, tuple)), "ids must be instance of (int, list, tuple)" - if isinstance(ids, (int, long)): + if isinstance(ids, numbers.Integral): record = get_record(self, ids, context=context) if fields is not None: record.read(fields) # read specified fields diff --git a/openerp_proxy/plugin.py b/openerp_proxy/plugin.py index 494523a..0500d4f 100644 --- a/openerp_proxy/plugin.py +++ b/openerp_proxy/plugin.py @@ -1,9 +1,11 @@ # Python imports - +import six import extend_me +PluginMeta = extend_me.ExtensibleByHashType._('Plugin', hashattr='name') + -class Plugin(object): +class Plugin(six.with_metaclass(PluginMeta)): """ Base class for all plugins, extensible by name (uses metaclass extend_me.ExtensibleByHashType) @@ -31,7 +33,6 @@ def get_sign_state(self): This plugin will automaticaly register itself in system, when module which contains it will be imported. """ - __metaclass__ = extend_me.ExtensibleByHashType._('Plugin', hashattr='name') def __init__(self, erp_proxy): self._erp_proxy = erp_proxy @@ -64,7 +65,7 @@ def test(self): return self.proxy.get_url() -class PluginManager(object): +class PluginManager(extend_me.Extensible): """ Class that holds information about all plugins :param erp_proxy: instance of Client to bind plugins to @@ -88,7 +89,7 @@ def __getitem__(self, name): try: pluginCls = type(Plugin).get_class(name) except ValueError as e: - raise KeyError(e.message) + raise KeyError(str(e)) plugin = pluginCls(self.__erp_proxy) self.__plugins[name] = plugin diff --git a/openerp_proxy/plugins/graph.py b/openerp_proxy/plugins/graph.py index 817be20..3f3cc21 100644 --- a/openerp_proxy/plugins/graph.py +++ b/openerp_proxy/plugins/graph.py @@ -7,7 +7,7 @@ try: import pydot except ImportError: - print "PyDot not installed!!!" + print("PyDot not installed!!!") class Model(Extensible): diff --git a/openerp_proxy/service/db.py b/openerp_proxy/service/db.py index 7e4e41b..0a0a06a 100644 --- a/openerp_proxy/service/db.py +++ b/openerp_proxy/service/db.py @@ -30,7 +30,7 @@ def create_db(self, password, dbname, demo=False, lang='en_US', admin_password=' from openerp_proxy.core import Client # requires server version >= 6.1 - if self.server_version >= parse_version('6.1'): + if self.server_version() >= parse_version('6.1'): self.create_database(password, dbname, demo, lang, admin_password) else: # for other server versions process_id = self.create(password, dbname, demo, lang, admin_password) @@ -67,4 +67,4 @@ def server_version(self): (Already parsed with pkg_resources.parse_version) """ - return parse_version(super(DBService, self).server_version()) + return parse_version(self._service.server_version()) diff --git a/openerp_proxy/service/object.py b/openerp_proxy/service/object.py index 80f90ed..fb45420 100644 --- a/openerp_proxy/service/object.py +++ b/openerp_proxy/service/object.py @@ -49,7 +49,7 @@ def execute_wkf(self, object_name, signal, object_id): :param str object_name: name of object/model to trigger workflow on :param str signal: name of signal to send to workflow - :param int|long object_id: ID of document (record) to send signal to + :param int object_id: ID of document (record) to send signal to """ result_wkf = self._service.exec_workflow(self.proxy.dbname, self.proxy.uid, self.proxy._pwd, object_name, signal, object_id) return result_wkf diff --git a/openerp_proxy/service/report.py b/openerp_proxy/service/report.py index bab18ec..1d2e98f 100644 --- a/openerp_proxy/service/report.py +++ b/openerp_proxy/service/report.py @@ -1,3 +1,4 @@ +import numbers from openerp_proxy.service.service import ServiceBase from extend_me import ExtensibleType @@ -104,7 +105,7 @@ def available_reports(self): def _prepare_report_data(self, model, ids, report_type): """ Performs preparation of data """ - ids = [ids] if isinstance(ids, (int, long)) else ids + ids = [ids] if isinstance(ids, numbers.Integral) else ids return { 'model': model, 'id': ids[0], @@ -125,7 +126,7 @@ def report(self, report_name, model, ids, report_type='pdf', context=None): :rtype: int """ context = {} if context is None else context - ids = [ids] if isinstance(ids, (int, long)) else ids + ids = [ids] if isinstance(ids, numbers.Integral) else ids data = self._prepare_report_data(model, ids, report_type) return self._service.report(self.proxy.dbname, self.proxy.uid, @@ -185,7 +186,7 @@ def render_report(self, report_name, model, ids, report_type='pdf', context=None :rtype: dict|ReportResult """ context = {} if context is None else context - ids = [ids] if isinstance(ids, (int, long)) else ids + ids = [ids] if isinstance(ids, numbers.Integral) else ids data = self._prepare_report_data(model, ids, report_type) if wrap_result: diff --git a/openerp_proxy/service/service.py b/openerp_proxy/service/service.py index 861043f..84d47a4 100644 --- a/openerp_proxy/service/service.py +++ b/openerp_proxy/service/service.py @@ -1,9 +1,11 @@ -from extend_me import ExtensibleByHashType +import six +from extend_me import (ExtensibleByHashType, + Extensible) __all__ = ('get_service_class', 'ServiceBase', 'ServiceManager') -class ServiceManager(object): +class ServiceManager(Extensible): """ Class to hold services related to specific proxy and to automaticaly clean service cached on update of service classes @@ -43,7 +45,7 @@ def __dir__(self): def list(self): """ Returns list of all registered services """ - return list(set(self.__services.keys() + ServiceType.get_registered_names())) + return list(set(list(self.__services.keys()) + ServiceType.get_registered_names())) def get_service(self, name): """ Returns instance of service with specified name @@ -67,6 +69,9 @@ def __getattr__(self, name): def __getitem__(self, name): return self.get_service(name) + def __contains__(self, name): + return name in self.list + ServiceType = ExtensibleByHashType._('Service', hashattr='name') @@ -77,10 +82,9 @@ def get_service_class(name): return ServiceType.get_class(name, default=True) -class ServiceBase(object): +class ServiceBase(six.with_metaclass(ServiceType)): """ Base class for all Services """ - __metaclass__ = ServiceType def __init__(self, service, erp_proxy): self._erp_proxy = erp_proxy diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index 23eba5c..59f9869 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -1,3 +1,5 @@ +import numbers +import six import json import os.path import sys @@ -5,7 +7,7 @@ from getpass import getpass # project imports -from core import Client +from .core import Client __all__ = ('ERP_Session', 'Session', 'IPYSession') @@ -219,7 +221,7 @@ def get_db(self, url_or_index, **kwargs): session.get_db('xml-rpc://katyukha@erp.jbm.int:8069/jbm0') # using url session.get_db('my_db') # using aliase """ - if isinstance(url_or_index, (int, long)): + if isinstance(url_or_index, numbers.Integral): url = self.index[url_or_index] else: url = self._db_aliases.get(url_or_index, url_or_index) @@ -238,7 +240,7 @@ def get_db(self, url_or_index, **kwargs): if self.option('store_passwords') and 'password' in ep_args: from simplecrypt import decrypt import base64 - crypter, password = base64.decodestring(ep_args.pop('password')).split(':') + crypter, password = base64.decodestring(ep_args.pop('password').encode('utf8')).split(b':') ep_args['pwd'] = decrypt(Client.to_url(ep_args), base64.decodestring(password)) else: ep_args['pwd'] = getpass('Password: ') @@ -298,7 +300,7 @@ def _get_db_init_args(self, database): if self.option('store_passwords') and database._pwd: from simplecrypt import encrypt import base64 - password = base64.encodestring('simplecrypt:' + base64.encodestring(encrypt(database.get_url(), database._pwd))) + password = base64.encodestring(b'simplecrypt:' + base64.encodestring(encrypt(database.get_url(), database._pwd))) res.update({'password': password}) return res elif isinstance(database, dict): @@ -310,7 +312,7 @@ def save(self): """ Saves session on disc """ databases = {} - for url, database in self._databases.iteritems(): + for url, database in self._databases.items(): if not getattr(database, '_no_save', False): init_args = self._get_db_init_args(database) databases[url] = init_args @@ -323,6 +325,7 @@ def save(self): 'options': self._options, } + return with open(self.data_file, 'wt') as json_data: json.dump(data, json_data, indent=4) @@ -332,7 +335,7 @@ def __getitem__(self, url_or_index): try: res = self.get_db(url_or_index) except ValueError as e: - raise KeyError(e.message) + raise KeyError(str(e)) return res # Overriden to be able to access database like @@ -341,7 +344,7 @@ def __getattr__(self, name): try: res = self.get_db(name) except ValueError as e: - raise AttributeError(e.message) + raise AttributeError(str(e)) return res def __str__(self): diff --git a/openerp_proxy/tests/test_client.py b/openerp_proxy/tests/test_client.py index aeaf1f1..74a0c23 100644 --- a/openerp_proxy/tests/test_client.py +++ b/openerp_proxy/tests/test_client.py @@ -1,9 +1,11 @@ +from pkg_resources import parse_version + from . import BaseTestCase from openerp_proxy.core import Client from openerp_proxy.orm.object import Object from openerp_proxy.orm.record import Record +from openerp_proxy.service.service import ServiceManager from openerp_proxy.plugin import Plugin -from openerp_proxy.exceptions import LoginException class Test_10_Client(BaseTestCase): @@ -22,6 +24,12 @@ def test_20_username(self): self.assertIsInstance(self.client.user, Record) self.assertEqual(self.client.user.login, self.env.user) + def test_25_server_version(self): + # Check that server version is wrapped in parse_version. thi allows to + # compare versions + self.assertIsInstance(self.client.server_version, type(parse_version('1.0.0'))) + + def test_30_get_obj(self): self.assertIn('res.partner', self.client.registered_objects) obj = self.client.get_obj('res.partner') @@ -46,6 +54,9 @@ def test_50_to_url(self): self.assertEqual(Client.to_url(None, **self.env), cl_url) self.assertEqual(self.client.get_url(), cl_url) + with self.assertRaises(ValueError): + Client.to_url('strange thing') + def test_60_plugins(self): self.assertIn('Test', self.client.plugins.registered_plugins) self.assertIn('Test', self.client.plugins) @@ -67,3 +78,28 @@ def test_62_plugins_wrong_name(self): with self.assertRaises(AttributeError): self.client.plugins.Test_Bad + + def test_70_client_services(self): + self.assertIsInstance(self.client.services, ServiceManager) + self.assertIn('db', self.client.services) + self.assertIn('object', self.client.services) + self.assertIn('report', self.client.services) + + self.assertIn('db', self.client.services.list) + self.assertIn('object', self.client.services.list) + self.assertIn('report', self.client.services.list) + + self.assertIn('db', dir(self.client.services)) + self.assertIn('object', dir(self.client.services)) + self.assertIn('report', dir(self.client.services)) + + def test_80_execute(self): + res = self.client.execute('res.partner', 'read', 1) + self.assertIsInstance(res, dict) + self.assertEqual(res['id'], 1) + + res = self.client.execute('res.partner', 'read', [1]) + self.assertIsInstance(res, list) + self.assertEqual(len(res), 1) + self.assertIsInstance(res[0], dict) + self.assertEqual(res[0]['id'], 1) diff --git a/setup.py b/setup.py index 6bd6549..d223611 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ }, install_requires=[ 'six', - 'extend_me>=1.1.2', + 'extend_me>=1.1.3', + 'requests', ], ) From 195993c38f439b6bbfdbe2a1e39c06e941f390e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Mon, 27 Jul 2015 13:51:56 +0300 Subject: [PATCH 02/23] Added little bit of tests --- .travis.yml | 5 +- openerp_proxy/orm/cache.py | 1 + openerp_proxy/orm/object.py | 2 +- openerp_proxy/orm/record.py | 2 +- openerp_proxy/orm/service.py | 2 +- openerp_proxy/tests/__init__.py | 6 +++ openerp_proxy/tests/all.py | 1 + openerp_proxy/tests/test_orm.py | 92 +++++++++++++++++++++++++++++++++ 8 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 openerp_proxy/tests/test_orm.py diff --git a/.travis.yml b/.travis.yml index 9875016..a587632 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: python python: - "2.7" - - "3.1" - - "3.2" - "3.3" - "3.4" @@ -10,6 +8,9 @@ env: - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='xml-rpc' - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='json-rpc' - ODOO_VERSION="7.0" ODOO_PACKAGE="openerp" ODOO_TEST_PROTOCOL='xml-rpc' + - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='xml-rpc' TEST_WITH_EXTENSIONS='openerp_proxy.ext.all' + - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='json-rpc' TEST_WITH_EXTENSIONS='openerp_proxy.ext.all' + - ODOO_VERSION="7.0" ODOO_PACKAGE="openerp" ODOO_TEST_PROTOCOL='xml-rpc' TEST_WITH_EXTENSIONS='openerp_proxy.ext.all' install: - "wget http://nightly.odoo.com/${ODOO_VERSION}/nightly/deb/${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb" diff --git a/openerp_proxy/orm/cache.py b/openerp_proxy/orm/cache.py index 2c3f555..b61482e 100644 --- a/openerp_proxy/orm/cache.py +++ b/openerp_proxy/orm/cache.py @@ -1,5 +1,6 @@ #import openerp_proxy.orm.record import six +import numbers import collections __all__ = ('empty_cache') diff --git a/openerp_proxy/orm/object.py b/openerp_proxy/orm/object.py index 3b7cbfa..36e3f89 100644 --- a/openerp_proxy/orm/object.py +++ b/openerp_proxy/orm/object.py @@ -62,7 +62,7 @@ def proxy(self): # Overriden to add some standard method to be available in introspection # Useful for IPython auto completition def __dir__(self): - res = dir(super(Object, self)) + res = dir(super(self.__class__, self)) res.extend(['read', 'search', 'write', 'unlink', 'create']) return res diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index 416e13c..103db55 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -67,7 +67,7 @@ def __init__(self, obj, rid, cache=None, context=None): def __dir__(self): # TODO: expose also object's methods - res = dir(super(Record, self)) + res = dir(super(self.__class__, self)) res.extend(self._columns_info.keys()) res.extend(['read', 'search', 'write', 'unlink']) return res diff --git a/openerp_proxy/orm/service.py b/openerp_proxy/orm/service.py index 5b37771..185bfa4 100644 --- a/openerp_proxy/orm/service.py +++ b/openerp_proxy/orm/service.py @@ -24,7 +24,7 @@ def get_obj(self, object_name): return self.__objects[object_name] if object_name not in self.get_registered_objects(): - raise ValueError("There is no object named '%s' in ERP" % object_name) + raise ValueError("There is no object named '%s'" % object_name) obj = get_object(self, object_name) self.__objects[object_name] = obj diff --git a/openerp_proxy/tests/__init__.py b/openerp_proxy/tests/__init__.py index 91a3313..720af23 100644 --- a/openerp_proxy/tests/__init__.py +++ b/openerp_proxy/tests/__init__.py @@ -20,3 +20,9 @@ def setUp(self): 'super_password': os.environ.get('ODOO_TEST_SUPER_PASSWORD', 'admin'), }) + # allow to specify extensions with environment var + with_extensions = os.environ.get('TEST_WITH_EXTENSIONS', False) + if with_extensions: + for ext in with_extensions.split(','): + __import__(ext) + diff --git a/openerp_proxy/tests/all.py b/openerp_proxy/tests/all.py index a564f1f..51ed461 100644 --- a/openerp_proxy/tests/all.py +++ b/openerp_proxy/tests/all.py @@ -1,2 +1,3 @@ from .test_connection import * from .test_client import * +from .test_orm import * diff --git a/openerp_proxy/tests/test_orm.py b/openerp_proxy/tests/test_orm.py new file mode 100644 index 0000000..62f2d41 --- /dev/null +++ b/openerp_proxy/tests/test_orm.py @@ -0,0 +1,92 @@ +from . import BaseTestCase +from openerp_proxy.core import Client +from openerp_proxy.orm.record import Record +from openerp_proxy.orm.record import RecordList + + +class Test_20_Object(BaseTestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + self.object = self.client.get_obj('res.partner') + + def test_dir(self): + self.assertIn('read', dir(self.object)) + self.assertIn('search', dir(self.object)) + self.assertIn('write', dir(self.object)) + self.assertIn('unlink', dir(self.object)) + self.assertIn('create', dir(self.object)) + + # test if normal mehtods avalilable in dir(object) + #self.assertIn('search_records', dir(self.object)) + #self.assertIn('browse', dir(self.object)) + + def test_getttr(self): + self.assertEqual(self.object.search.__name__, 'res.partner:search') + + # Test that attibute error is raised on access on private methods + with self.assertRaises(AttributeError): + self.object._do_smthing_private + + def test_model(self): + self.assertIsInstance(self.object.model, Record) + self.assertEqual(self.object.name, self.object.model.model) + self.assertEqual(self.object.model, self.object._model) + + # this will check that model_name is result of name_get on model record + self.assertEqual(self.object.model_name, self.object.model._name) + + +class Test_21_Record(BaseTestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + self.object = self.client.get_obj('res.partner') + self.record = self.object.browse(1) + + def test_dir(self): + self.assertIn('read', dir(self.record)) + self.assertIn('search', dir(self.record)) + self.assertIn('write', dir(self.record)) + self.assertIn('unlink', dir(self.record)) + + # test if normal mehtods avalilable in dir(object) + #self.assertIn('refresh', dir(self.record)) + + def test_name_get(self): + self.assertEqual(self.record._name, self.record.name_get()[0][1]) + + def test_record_equal(self): + rec1 = self.record + + rec_list = self.object.search_records([('id', '=', 1)]) + self.assertIsInstance(rec_list, RecordList) + self.assertEqual(rec_list.length, 1) + + rec2 = rec_list[0] + self.assertEqual(rec1, rec2) + self.assertEqual(rec1.id, rec2.id) + self.assertEqual(rec1._name, rec2._name) + + def test_getitem(self): + self.assertEqual(self.record['id'], self.record.id) + with self.assertRaises(KeyError): + self.record['some_unexistent_field'] + + def test_getattr(self): + # Check that, if we try to get unexistent field, result will be method + # wrapper for object method + f = self.record.some_unexistent_field + self.assertTrue(callable(f)) From 62393207e5d765dab7e63cb0aab16a068f63a507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Mon, 27 Jul 2015 13:56:22 +0300 Subject: [PATCH 03/23] Added ipython dependency to .travis.yml install section --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a587632..1db7a8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - "wget http://nightly.odoo.com/${ODOO_VERSION}/nightly/deb/${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb" - "sudo dpkg -i ${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb || true" - "sudo apt-get update && sudo apt-get install -f -y" - - "pip install coveralls" + - "pip install coveralls ipython[all]" - "python setup.py develop" before_script: From f33742711f5e19ece389787882d1f475c0a213f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Mon, 27 Jul 2015 14:34:01 +0300 Subject: [PATCH 04/23] Py3 related fix. (dict.keys() -> list(dict)) --- openerp_proxy/orm/cache.py | 2 +- openerp_proxy/orm/record.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openerp_proxy/orm/cache.py b/openerp_proxy/orm/cache.py index b61482e..9164f00 100644 --- a/openerp_proxy/orm/cache.py +++ b/openerp_proxy/orm/cache.py @@ -112,7 +112,7 @@ def prefetch_fields(self, fields): to_prefetch, related = self.parse_prefetch_fields(fields) col_info = self._object.columns_info - for data in self._object.read(self.keys(), to_prefetch): + for data in self._object.read(list(self), to_prefetch): for field, value in data.iteritems(): # Fill related cache diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index 103db55..57deb97 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -121,7 +121,7 @@ def _name(self): """ if self._data.get('__name_get_result', None) is None: lcache = self._lcache - data = self._object.name_get(lcache.keys(), context=self.context) + data = self._object.name_get(list(lcache), context=self.context) for _id, name in data: lcache[_id]['__name_get_result'] = name return self._data.get('__name_get_result', 'ERROR') @@ -218,7 +218,7 @@ def read(self, fields=None, context=None, multi=False): :rtype: dict """ ctx = {} if self.context is None else self.context.copy() - args = [self._lcache.keys()] if multi else [[self.id]] + args = [list(self._lcache)] if multi else [[self.id]] kwargs = {} From d69fa8aab0d36946e5a9cadbf513d486ee999d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Wed, 29 Jul 2015 13:48:23 +0300 Subject: [PATCH 05/23] Added more ORM tests --- .travis.yml | 2 +- openerp_proxy/connection/jsonrpc.py | 2 - openerp_proxy/connection/xmlrpc.py | 7 +-- openerp_proxy/orm/record.py | 4 +- openerp_proxy/session.py | 1 - openerp_proxy/tests/test_orm.py | 88 +++++++++++++++++++++++++++++ run_tests.bash | 16 ++++++ run_tests.sh | 7 --- 8 files changed, 107 insertions(+), 20 deletions(-) create mode 100755 run_tests.bash delete mode 100755 run_tests.sh diff --git a/.travis.yml b/.travis.yml index 1db7a8b..454e1cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - "wget http://nightly.odoo.com/${ODOO_VERSION}/nightly/deb/${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb" - "sudo dpkg -i ${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb || true" - "sudo apt-get update && sudo apt-get install -f -y" - - "pip install coveralls ipython[all]" + - "pip install coveralls mock ipython[all]" - "python setup.py develop" before_script: diff --git a/openerp_proxy/connection/jsonrpc.py b/openerp_proxy/connection/jsonrpc.py index 99df487..064e6d3 100644 --- a/openerp_proxy/connection/jsonrpc.py +++ b/openerp_proxy/connection/jsonrpc.py @@ -1,8 +1,6 @@ # python imports import json import random - -#import urllib2 import requests # project imports diff --git a/openerp_proxy/connection/xmlrpc.py b/openerp_proxy/connection/xmlrpc.py index 83407f7..3bc74b9 100644 --- a/openerp_proxy/connection/xmlrpc.py +++ b/openerp_proxy/connection/xmlrpc.py @@ -1,10 +1,5 @@ # python imports -import sys - -if sys.version_info < (3, 0, 0): - import xmlrpclib -else: - import xmlrpc.client as xmlrpclib +from six.moves import xmlrpc_client as xmlrpclib # project imports from .connection import ConnectorBase diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index 57deb97..04fc0d9 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -657,8 +657,6 @@ def read_records(self, ids, fields=None, context=None, cache=None): >>> for order in data: order.write({'note': 'order data is %s'%order.data}) """ - assert isinstance(ids, (numbers.Integral, list, tuple)), "ids must be instance of (int, list, tuple)" - if isinstance(ids, numbers.Integral): record = get_record(self, ids, context=context) if fields is not None: @@ -667,7 +665,7 @@ def read_records(self, ids, fields=None, context=None, cache=None): if isinstance(ids, (list, tuple)): return get_record_list(self, ids, fields=fields, context=context) - raise ValueError("Wrong type for ids args") + raise ValueError("Wrong type for ids argument: %s" % type(ids)) def browse(self, *args, **kwargs): """ Aliase to *read_records* method. In most cases same as serverside *browse* diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index 59f9869..73bfb38 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -325,7 +325,6 @@ def save(self): 'options': self._options, } - return with open(self.data_file, 'wt') as json_data: json.dump(data, json_data, indent=4) diff --git a/openerp_proxy/tests/test_orm.py b/openerp_proxy/tests/test_orm.py index 62f2d41..f3e0260 100644 --- a/openerp_proxy/tests/test_orm.py +++ b/openerp_proxy/tests/test_orm.py @@ -2,7 +2,15 @@ from openerp_proxy.core import Client from openerp_proxy.orm.record import Record from openerp_proxy.orm.record import RecordList +from openerp_proxy.exceptions import ConnectorError +try: + import unittest.mock as mock +except ImportError: + import mock + + +import numbers class Test_20_Object(BaseTestCase): @@ -34,6 +42,14 @@ def test_getttr(self): with self.assertRaises(AttributeError): self.object._do_smthing_private + def test_call_unexistent_method(self): + # method wrapper will be created + self.assertEqual(self.object.some_unexisting_mehtod.__name__, 'res.partner:some_unexisting_mehtod') + + # but exception should be raised + with self.assertRaises(ConnectorError): + self.object.some_unexisting_mehtod([1]) + def test_model(self): self.assertIsInstance(self.object.model, Record) self.assertEqual(self.object.name, self.object.model.model) @@ -42,6 +58,58 @@ def test_model(self): # this will check that model_name is result of name_get on model record self.assertEqual(self.object.model_name, self.object.model._name) + def test_search(self): + res = self.object.search([('id', '=', 1)]) + self.assertIsInstance(res, list) + self.assertEqual(res, [1]) + + res = self.object.search([('id', '=', 1)], count=1) + self.assertIsInstance(res, numbers.Integral) + self.assertEqual(res, 1) + + def test_search_records(self): + res = self.object.search_records([('id', '=', 1)]) + self.assertIsInstance(res, RecordList) + self.assertEqual(res.length, 1) + self.assertEqual(res[0].id, 1) + + res = self.object.search_records([('id', '=', 99999)]) + self.assertIsInstance(res, RecordList) + self.assertEqual(res.length, 0) + + res = self.object.search_records([('id', '=', 1)], count=1) + self.assertIsInstance(res, numbers.Integral) + self.assertEqual(res, 1) + + def test_read_records(self): + # read one record + res = self.object.read_records(1) + self.assertIsInstance(res, Record) + self.assertEqual(res.id, 1) + + # read set of records + res = self.object.read_records([1]) + self.assertIsInstance(res, RecordList) + self.assertEqual(res.length, 1) + self.assertEqual(res[0].id, 1) + + # try to call read_records with bad argument + with self.assertRaises(ValueError): + self.object.read_records(None) + + def test_browse(self): + with mock.patch.object(self.object, 'read_records') as fake_read_records: + self.object.browse(1) + fake_read_records.assert_called_with(1) + + with mock.patch.object(self.object, 'read_records') as fake_read_records: + self.object.browse([1]) + fake_read_records.assert_called_with([1]) + + with mock.patch.object(self.object, 'read_records') as fake_read_records: + self.object.browse(None) + fake_read_records.assert_called_with(None) + class Test_21_Record(BaseTestCase): @@ -80,6 +148,10 @@ def test_record_equal(self): self.assertEqual(rec1.id, rec2.id) self.assertEqual(rec1._name, rec2._name) + # Test that equality with simple integers works + self.assertEqual(rec1, rec2.id) + self.assertEqual(rec1.id, rec2) + def test_getitem(self): self.assertEqual(self.record['id'], self.record.id) with self.assertRaises(KeyError): @@ -90,3 +162,19 @@ def test_getattr(self): # wrapper for object method f = self.record.some_unexistent_field self.assertTrue(callable(f)) + + def test_record_to_int(self): + self.assertIs(int(self.record), 1) + + def test_record_relational_fields(self): + res = self.record.child_ids # read data from res.partner:child_ids field + + self.assertIsInstance(res, RecordList) + self.assertTrue(res.length >= 1) + self.assertIsInstance(res[0], Record) + self.assertEqual(res[0]._object.name, 'res.partner') + + # test many2one + self.assertIsInstance(res[0].parent_id, Record) + self.assertIsNot(res[0].parent_id, self.record) + self.assertEqual(res[0].parent_id, self.record) diff --git a/run_tests.bash b/run_tests.bash new file mode 100755 index 0000000..6511d3c --- /dev/null +++ b/run_tests.bash @@ -0,0 +1,16 @@ +#!/bin/bash + +SCRIPT=`readlink -f "$0"` +# Absolute path this script is in, thus /home/user/bin +SCRIPTPATH=`dirname "$SCRIPT"` + +(cd $SCRIPTPATH && \ + virtualenv venv_test && \ + source ./venv_test/bin/activate && \ + pip install --upgrade pip setuptools coverage mock && \ + python setup.py develop && \ + rm -f .coverage && \ + coverage run --source openerp_proxy -m unittest -v openerp_proxy.tests.all && \ + coverage html -d coverage && \ + deactivate && \ + rm -rf venv_test) diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index 1ddc260..0000000 --- a/run_tests.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -SCRIPT=`readlink -f "$0"` -# Absolute path this script is in, thus /home/user/bin -SCRIPTPATH=`dirname "$SCRIPT"` - -(cd $SCRIPTPATH && rm .coverage && coverage run --source openerp_proxy -m unittest -v openerp_proxy.tests.all && coverage html -d coverage) From b3b81c85db160c3a9125176d5fb1612cd1b41047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Wed, 29 Jul 2015 18:28:03 +0300 Subject: [PATCH 06/23] Addel little bit of tests on RecordList --- openerp_proxy/orm/record.py | 7 +++-- openerp_proxy/tests/test_orm.py | 55 +++++++++++++++++++++++++++++++++ openerp_proxy/utils.py | 14 ++++----- run_tests.bash | 2 +- 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index 04fc0d9..c6f1a1f 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -346,12 +346,15 @@ def __getitem__(self, index): if isinstance(index, slice): # Note no context passed, because it is stored in cache return get_record_list(self.object, - ids=(r.id for r in self._records[index]), + ids=[r.id for r in self._records[index]], cache=self._cache) return self._records[index] def __setitem__(self, index, value): - self._records[index] = value + if isinstance(value, Record): + self._records[index] = value + else: + raise ValueError("In 'RecordList[index] = value' operation, value must be instance of Record") def __delitem__(self, index): del self._records[index] diff --git a/openerp_proxy/tests/test_orm.py b/openerp_proxy/tests/test_orm.py index f3e0260..7fd50b7 100644 --- a/openerp_proxy/tests/test_orm.py +++ b/openerp_proxy/tests/test_orm.py @@ -152,6 +152,9 @@ def test_record_equal(self): self.assertEqual(rec1, rec2.id) self.assertEqual(rec1.id, rec2) + self.assertNotEqual(rec1, None) + self.assertNotEqual(rec1, 2) + def test_getitem(self): self.assertEqual(self.record['id'], self.record.id) with self.assertRaises(KeyError): @@ -166,6 +169,9 @@ def test_getattr(self): def test_record_to_int(self): self.assertIs(int(self.record), 1) + def test_record_hash(self): + self.assertEqual(hash(self.record), hash((self.record._object.name, self.record.id))) + def test_record_relational_fields(self): res = self.record.child_ids # read data from res.partner:child_ids field @@ -178,3 +184,52 @@ def test_record_relational_fields(self): self.assertIsInstance(res[0].parent_id, Record) self.assertIsNot(res[0].parent_id, self.record) self.assertEqual(res[0].parent_id, self.record) + + # test that empty many2one field is avaluated as False + self.assertIs(self.record.user_id, False) + + # test that empty x2many field is evaluated as empty RecordList + self.assertIsInstance(self.record.user_ids, RecordList) + self.assertEqual(self.record.user_ids.length, 0) + + +class Test_22_RecordList(BaseTestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + self.object = self.client.get_obj('res.partner') + self.obj_ids = self.object.search([], limit=10) + self.recordlist = self.object.read_records(self.obj_ids) + + def test_ids(self): + self.assertSequenceEqual(self.recordlist.ids, self.obj_ids) + + def test_length(self): + self.assertEqual(self.recordlist.length, len(self.obj_ids)) + self.assertEqual(len(self.recordlist), len(self.obj_ids)) + + def test_getitem(self): + id1 = self.obj_ids[0] + id2 = self.obj_ids[-1] + + id_slice = self.obj_ids[2:15:2] + + self.assertIsInstance(self.recordlist[0], Record) + self.assertEqual(self.recordlist[0].id, id1) + + self.assertIsInstance(self.recordlist[-1], Record) + self.assertEqual(self.recordlist[-1].id, id2) + + res = self.recordlist[2:15:2] + self.assertIsInstance(res, RecordList) + self.assertEqual(res.length, len(id_slice)) + self.assertSequenceEqual(res.ids, id_slice) + + with self.assertRaises(IndexError): + self.recordlist[100] diff --git a/openerp_proxy/utils.py b/openerp_proxy/utils.py index 578837b..8eb8a10 100644 --- a/openerp_proxy/utils.py +++ b/openerp_proxy/utils.py @@ -1,3 +1,4 @@ +import six import sys import functools @@ -57,12 +58,11 @@ def get_encodings(hint_encoding='utf-8'): def exception_to_unicode(e): - if (sys.version_info[:2] < (2, 6)) and hasattr(e, 'message'): - return ustr(e.message) if hasattr(e, 'args'): return "\n".join((ustr(a) for a in e.args)) + try: - return unicode(e) + return six.text_type(e) except Exception: return u"Unknown message" @@ -88,18 +88,18 @@ def ustr(value, hint_encoding='utf-8', errors='strict'): if isinstance(value, Exception): return exception_to_unicode(value) - if isinstance(value, unicode): + if isinstance(value, six.text_type): return value - if not isinstance(value, basestring): + if not isinstance(value, six.string_types): try: - return unicode(value) + return six.text_type(value) except Exception: raise UnicodeError('unable to convert %r' % (value,)) for ln in get_encodings(hint_encoding): try: - return unicode(value, ln, errors=errors) + return six.text_type(value, ln, errors=errors) except Exception: pass raise UnicodeError('unable to convert %r' % (value,)) diff --git a/run_tests.bash b/run_tests.bash index 6511d3c..3be0f63 100755 --- a/run_tests.bash +++ b/run_tests.bash @@ -7,7 +7,7 @@ SCRIPTPATH=`dirname "$SCRIPT"` (cd $SCRIPTPATH && \ virtualenv venv_test && \ source ./venv_test/bin/activate && \ - pip install --upgrade pip setuptools coverage mock && \ + pip install --upgrade pip setuptools coverage mock pudb && \ python setup.py develop && \ rm -f .coverage && \ coverage run --source openerp_proxy -m unittest -v openerp_proxy.tests.all && \ From e217baaf37142b4fb49ecb2adc33e50f02543614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Thu, 30 Jul 2015 14:57:13 +0300 Subject: [PATCH 07/23] Few more tests for recordlist --- openerp_proxy/orm/record.py | 5 ++++- openerp_proxy/tests/test_orm.py | 32 ++++++++++++++++++++++++++++++++ run_tests.bash | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index c6f1a1f..39dee3e 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -413,8 +413,11 @@ def refresh(self): def sort(self, cmp=None, key=None, reverse=False): """ sort(cmp=None, key=None, reverse=False) -- inplace sort cmp(x, y) -> -1, 0, 1 + + :return: self """ - return self._records.sort(cmp=cmp, key=key, reverse=reverse) + self._records.sort(cmp=cmp, key=key, reverse=reverse) + return self def copy(self, context=None, new_cache=False): """ Returns copy of this list, possibly with modified context diff --git a/openerp_proxy/tests/test_orm.py b/openerp_proxy/tests/test_orm.py index 7fd50b7..8d4ecfb 100644 --- a/openerp_proxy/tests/test_orm.py +++ b/openerp_proxy/tests/test_orm.py @@ -233,3 +233,35 @@ def test_getitem(self): with self.assertRaises(IndexError): self.recordlist[100] + + def test_contains(self): + rid = self.obj_ids[0] + rec = self.object.read_records(rid) + + brid = self.object.search([('id', 'not in', self.obj_ids)], limit=1)[0] + brec = self.object.read_records(brid) + + self.assertIn(rid, self.recordlist) + self.assertIn(rec, self.recordlist) + + self.assertNotIn(brid, self.recordlist) + self.assertNotIn(brec, self.recordlist) + + self.assertNotIn(None, self.recordlist) + + def test_prefetch(self): + # check that cache is only filled with ids + self.assertEqual(len(self.recordlist._lcache), self.recordlist.length) + for record in self.recordlist: + self.assertEqual(len(record._data), 1) + self.assertEqual(record._data.keys(), ['id']) + + # prefetch + self.recordlist.prefetch('name') + + self.assertEqual(len(self.recordlist._lcache), self.recordlist.length) + for record in self.recordlist: + self.assertEqual(len(record._data), 2) + self.assertEqual(record._data.keys(), ['id', 'name']) + + # TODO: test prefetch related diff --git a/run_tests.bash b/run_tests.bash index 3be0f63..8600487 100755 --- a/run_tests.bash +++ b/run_tests.bash @@ -7,7 +7,7 @@ SCRIPTPATH=`dirname "$SCRIPT"` (cd $SCRIPTPATH && \ virtualenv venv_test && \ source ./venv_test/bin/activate && \ - pip install --upgrade pip setuptools coverage mock pudb && \ + pip install --upgrade pip setuptools coverage mock pudb ipython && \ python setup.py develop && \ rm -f .coverage && \ coverage run --source openerp_proxy -m unittest -v openerp_proxy.tests.all && \ From 5b89e188ae9511defe7cbf22fd20a9641d69b4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Fri, 31 Jul 2015 14:26:32 +0300 Subject: [PATCH 08/23] More test for RecordList and little bit of bugfixes --- openerp_proxy/ext/data.py | 2 +- openerp_proxy/ext/sugar.py | 2 +- openerp_proxy/orm/cache.py | 2 +- openerp_proxy/orm/record.py | 6 +- openerp_proxy/service/report.py | 4 +- openerp_proxy/service/service.py | 2 +- openerp_proxy/tests/test_orm.py | 157 +++++++++++++++++++++++++++++-- 7 files changed, 160 insertions(+), 15 deletions(-) diff --git a/openerp_proxy/ext/data.py b/openerp_proxy/ext/data.py index e42283e..6520265 100644 --- a/openerp_proxy/ext/data.py +++ b/openerp_proxy/ext/data.py @@ -105,7 +105,7 @@ def data__get_grouped(self, group_rules, count=False): result = {} sub_domain = group_rules.pop('__sub_domain', []) for key, value in group_rules.iteritems(): - if isinstance(value, (list, tuple)): # If value is domain + if isinstance(value, collections.Iterable): # If value is domain domain = sub_domain + value result[key] = self.search_records(domain, count=count) elif isinstance(value, dict): # if value is subgroup of domains diff --git a/openerp_proxy/ext/sugar.py b/openerp_proxy/ext/sugar.py index 9cd740f..99e88f5 100644 --- a/openerp_proxy/ext/sugar.py +++ b/openerp_proxy/ext/sugar.py @@ -99,7 +99,7 @@ def __call__(self, *args, **kwargs): # normal domain passed, then just forward all arguments and # keyword arguments to *search_records* method - if isinstance(name, (list, tuple)): + if isinstance(name, collections.Iterable): return self.search_records(name, *args, **kwargs) # implement name_search capability diff --git a/openerp_proxy/orm/cache.py b/openerp_proxy/orm/cache.py index 9164f00..20f4669 100644 --- a/openerp_proxy/orm/cache.py +++ b/openerp_proxy/orm/cache.py @@ -70,7 +70,7 @@ def cache_field(self, rid, ftype, field_name, value): if isinstance(value, numbers.Integral): rcache[value] # internal dict {'id': key} will be created by default (see ObjectCache) - elif isinstance(value, (list, tuple)): + elif isinstance(value, collections.Iterable): rcache[value[0]]['__name_get_result'] = value[1] elif value and ftype in ('many2many', 'one2many'): rcache = self._root_cache[self._object.columns_info[field_name]['relation']] diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index 39dee3e..d0561b8 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -256,7 +256,7 @@ def get_record_list(obj, ids=None, fields=None, cache=None, context=None): return RecordListMeta.get_object(obj, ids, fields=fields, cache=cache, context=context) -class RecordList(six.with_metaclass(RecordListMeta), collections.MutableSequence): +class RecordList(six.with_metaclass(RecordListMeta, collections.MutableSequence)): """Class to hold list of records with some extra functionality :param obj: instance of Object to make this list related to @@ -526,7 +526,7 @@ def _get_many2one_rel_obj(self, name, rel_data, cached=True): if name not in self._related_objects or not cached: if rel_data: # Do not forged about relations in form [id, name] - rel_id = rel_data[0] if isinstance(rel_data, (list, tuple)) else rel_data + rel_id = rel_data[0] if isinstance(rel_data, collections.Iterable) else rel_data rel_obj = self._service.get_obj(self._columns_info[name]['relation']) self._related_objects[name] = get_record(rel_obj, rel_id, cache=self._cache, context=self.context) @@ -668,7 +668,7 @@ def read_records(self, ids, fields=None, context=None, cache=None): if fields is not None: record.read(fields) # read specified fields return record - if isinstance(ids, (list, tuple)): + if isinstance(ids, collections.Iterable): return get_record_list(self, ids, fields=fields, context=context) raise ValueError("Wrong type for ids argument: %s" % type(ids)) diff --git a/openerp_proxy/service/report.py b/openerp_proxy/service/report.py index 1d2e98f..86226f6 100644 --- a/openerp_proxy/service/report.py +++ b/openerp_proxy/service/report.py @@ -1,3 +1,4 @@ +import six import numbers from openerp_proxy.service.service import ServiceBase from extend_me import ExtensibleType @@ -9,7 +10,7 @@ class ReportError(Error): pass -class ReportResult(object): +class ReportResult(six.with_metaclass(ExtensibleType._('ReportResult'), object)): """ Just a simple and extensible wrapper on report result As variant of usage - wrap result returned by server methods @@ -18,7 +19,6 @@ class ReportResult(object): ReportResult(report_get(report_id)) """ - __metaclass__ = ExtensibleType._('ReportResult') def __init__(self, result, path=None): self._orig_result = result diff --git a/openerp_proxy/service/service.py b/openerp_proxy/service/service.py index 84d47a4..f9e878e 100644 --- a/openerp_proxy/service/service.py +++ b/openerp_proxy/service/service.py @@ -82,7 +82,7 @@ def get_service_class(name): return ServiceType.get_class(name, default=True) -class ServiceBase(six.with_metaclass(ServiceType)): +class ServiceBase(six.with_metaclass(ServiceType, object)): """ Base class for all Services """ diff --git a/openerp_proxy/tests/test_orm.py b/openerp_proxy/tests/test_orm.py index 8d4ecfb..1d79ff4 100644 --- a/openerp_proxy/tests/test_orm.py +++ b/openerp_proxy/tests/test_orm.py @@ -9,8 +9,9 @@ except ImportError: import mock - import numbers +import collections + class Test_20_Object(BaseTestCase): @@ -214,6 +215,10 @@ def test_length(self): self.assertEqual(self.recordlist.length, len(self.obj_ids)) self.assertEqual(len(self.recordlist), len(self.obj_ids)) + def test_recods(self): + self.assertIsInstance(self.recordlist.records, list) + self.assertIsInstance(self.recordlist.records[0], Record) + def test_getitem(self): id1 = self.obj_ids[0] id2 = self.obj_ids[-1] @@ -234,6 +239,33 @@ def test_getitem(self): with self.assertRaises(IndexError): self.recordlist[100] + def test_delitem(self): + r = self.recordlist[5] + self.assertEqual(len(self.recordlist), 10) + + del self.recordlist[5] + + self.assertEqual(len(self.recordlist), 9) + self.assertNotIn(r, self.recordlist) + + def test_setitem(self): + rec = self.object.search_records([('id', 'not in', self.recordlist.ids)], limit=1)[0] + + old_rec = self.recordlist[8] + + self.assertEqual(len(self.recordlist), 10) + self.assertNotIn(rec, self.recordlist) + self.assertIn(old_rec, self.recordlist) + + self.recordlist[8] = rec + + self.assertEqual(len(self.recordlist), 10) + self.assertIn(rec, self.recordlist) + self.assertNotIn(old_rec, self.recordlist) + + with self.assertRaises(ValueError): + self.recordlist[5] = 25 + def test_contains(self): rid = self.obj_ids[0] rec = self.object.read_records(rid) @@ -249,19 +281,132 @@ def test_contains(self): self.assertNotIn(None, self.recordlist) + def test_insert_record(self): + rec = self.object.search_records([('id', 'not in', self.recordlist.ids)], limit=1)[0] + + self.assertEqual(len(self.recordlist), 10) + self.assertNotIn(rec, self.recordlist) + + self.recordlist.insert(1, rec) + + self.assertEqual(len(self.recordlist), 11) + self.assertIn(rec, self.recordlist) + self.assertEqual(self.recordlist[1], rec) + + def test_insert_by_id(self): + rec = self.object.search_records([('id', 'not in', self.recordlist.ids)], limit=1)[0] + + self.assertEqual(len(self.recordlist), 10) + self.assertNotIn(rec, self.recordlist) + + self.recordlist.insert(1, rec.id) + + self.assertEqual(len(self.recordlist), 11) + self.assertIn(rec, self.recordlist) + self.assertEqual(self.recordlist[1], rec) + + def test_insert_bad_value(self): + rec = self.object.search_records([('id', 'not in', self.recordlist.ids)], limit=1)[0] + + self.assertEqual(len(self.recordlist), 10) + self.assertNotIn(rec, self.recordlist) + + with self.assertRaises(AssertionError): + self.recordlist.insert(1, "some strange type") + def test_prefetch(self): + cache = self.recordlist._cache + lcache = self.recordlist._lcache + # check that cache is only filled with ids - self.assertEqual(len(self.recordlist._lcache), self.recordlist.length) + self.assertEqual(len(lcache), self.recordlist.length) for record in self.recordlist: + # Note that record._data is a property, which means + # record._lcache[record.id]. _data property is dictionary. self.assertEqual(len(record._data), 1) - self.assertEqual(record._data.keys(), ['id']) + self.assertItemsEqual(list(record._data), ['id']) - # prefetch + # prefetch normal field self.recordlist.prefetch('name') self.assertEqual(len(self.recordlist._lcache), self.recordlist.length) for record in self.recordlist: self.assertEqual(len(record._data), 2) - self.assertEqual(record._data.keys(), ['id', 'name']) + self.assertItemsEqual(list(record._data), ['id', 'name']) + + # check that cache contains only res.partner object cache + self.assertEqual(len(cache), 1) + self.assertIn('res.partner', cache) + self.assertNotIn('res.country', cache) + + # prefetch related field name of caountry and country code + self.recordlist.prefetch('country_id.name', 'country_id.code') + + # test that cache now contains two objects ('res.partner', + # 'res.country') + self.assertEqual(len(cache), 2) + self.assertIn('res.partner', cache) + self.assertIn('res.country', cache) + + c_cache = cache['res.country'] + country_checked = False # if in some cases selected partners have no related countries, raise error + for record in self.recordlist: + # test that country_id field was added to partner's cache + self.assertEqual(len(record._data), 3) + self.assertItemsEqual(list(record._data), ['id', 'name', 'country_id']) + + # if current partner have related country_id + # + # Note, here check 'country_id' via '_data' property to avoid lazy + # loading of data. + country_id = record._data['country_id'] + + # if data is in form [id, ] + if isinstance(country_id, collections.Iterable): + country_id = country_id[0] + country_is_list = True + + if country_id: + country_checked = True + + # test, that there are some data for this country_id in country + # cache + self.assertIn(country_id, c_cache) + + # Note that, in case, of related many2one fields, Odoo may + # return list, with Id and resutlt of name_get method. + # thus, we program will imediatly cache this value + if country_is_list: + self.assertEqual(len(c_cache[country_id]), 4) + self.assertItemsEqual(list(c_cache[country_id]), ['id', 'name', 'code', '__name_get_result']) + else: + self.assertEqual(len(c_cache[country_id]), 4) + self.assertItemsEqual(list(c_cache[country_id]), ['id', 'name', 'code', '__name_get_result']) + + self.assertTrue(country_checked, "Country must be checked. may be there are wrong data in test database") + + def test_sorting(self): + def to_names(rlist): + return [r.name for r in rlist] + + names = to_names(self.recordlist) + + self.assertSequenceEqual(sorted(names), to_names(sorted(self.recordlist, key=lambda x: x.name))) + self.assertSequenceEqual(sorted(names, reverse=True), to_names(sorted(self.recordlist, key=lambda x: x.name, reverse=True))) + self.assertSequenceEqual(list(reversed(names)), to_names(reversed(self.recordlist))) + + # test recordlist sort methods + rlist = self.recordlist.copy() + rnames = names[:] # copy list + rlist.sort(key=lambda x: x.name) # inplace sort + rnames.sort() # inplace sort + self.assertSequenceEqual(rnames, to_names(rlist)) + + # test recordlist reverse method + rlist = self.recordlist.copy() + rnames = names[:] # copy list + rlist.reverse() # inplace reverse + rnames.reverse() # inplace reverse + self.assertSequenceEqual(rnames, to_names(rlist)) + - # TODO: test prefetch related From cf18127eb8d7ec2d6813214725969f18e0612ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Fri, 31 Jul 2015 15:22:28 +0300 Subject: [PATCH 09/23] Python 3 related fixes. --- openerp_proxy/orm/cache.py | 4 ++-- openerp_proxy/orm/object.py | 7 +++++-- openerp_proxy/orm/record.py | 21 ++++++++++++--------- openerp_proxy/tests/__init__.py | 5 +++++ run_tests.bash | 29 +++++++++++++++++++---------- 5 files changed, 43 insertions(+), 23 deletions(-) diff --git a/openerp_proxy/orm/cache.py b/openerp_proxy/orm/cache.py index 20f4669..cfaa864 100644 --- a/openerp_proxy/orm/cache.py +++ b/openerp_proxy/orm/cache.py @@ -113,7 +113,7 @@ def prefetch_fields(self, fields): col_info = self._object.columns_info for data in self._object.read(list(self), to_prefetch): - for field, value in data.iteritems(): + for field, value in data.items(): # Fill related cache ftype = col_info.get(field, {}).get('type', None) @@ -121,7 +121,7 @@ def prefetch_fields(self, fields): if related: # TODO: think how to avoid infinite recursion and double reads - for obj_name, rfields in related.viewitems(): + for obj_name, rfields in related.items(): self._root_cache[obj_name].prefetch_fields(rfields) diff --git a/openerp_proxy/orm/object.py b/openerp_proxy/orm/object.py index 36e3f89..19d61d9 100644 --- a/openerp_proxy/orm/object.py +++ b/openerp_proxy/orm/object.py @@ -25,6 +25,7 @@ def get_object(proxy, name): # TODO: think about connecting it to service instead of Proxy +@six.python_2_unicode_compatible class Object(six.with_metaclass(ObjectType)): """ Base class for all Objects @@ -87,8 +88,10 @@ def wrapper(*args, **kwargs): return getattr(self, name) def __str__(self): - return "Object ('%s')" % self.name - __repr__ = __str__ + return u"Object ('%s')" % self.name + + def __repr__(self): + return str(self) def __eq__(self, other): return self.name == other.name and self.proxy == other.proxy diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index d0561b8..9b2566f 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -39,6 +39,7 @@ def get_record(obj, rid, cache=None, context=None): return RecordMeta.get_object(obj, rid, cache=cache, context=context) +@six.python_2_unicode_compatible class Record(six.with_metaclass(RecordMeta, object)): """ Base class for all Records @@ -124,13 +125,10 @@ def _name(self): data = self._object.name_get(list(lcache), context=self.context) for _id, name in data: lcache[_id]['__name_get_result'] = name - return self._data.get('__name_get_result', 'ERROR') - - def __unicode__(self): - return u"R(%s, %s)[%s]" % (self._object.name, self.id, ustr(self._name)) + return self._data.get('__name_get_result', u'ERROR') def __str__(self): - return unicode(self).encode('utf-8') + return u"R(%s, %s)[%s]" % (self._object.name, self.id, ustr(self._name)) def __repr__(self): return str(self) @@ -256,6 +254,7 @@ def get_record_list(obj, ids=None, fields=None, cache=None, context=None): return RecordListMeta.get_object(obj, ids, fields=fields, cache=cache, context=context) +@six.python_2_unicode_compatible class RecordList(six.with_metaclass(RecordListMeta, collections.MutableSequence)): """Class to hold list of records with some extra functionality @@ -397,8 +396,10 @@ def __getattr__(self, name): return res def __str__(self): - return "RecordList(%s): length=%s" % (self.object.name, self.length) - __repr__ = __str__ + return u"RecordList(%s): length=%s" % (self.object.name, self.length) + + def __repr__(self): + return str(self) def refresh(self): """ Cleanup data caches. next try to get data will cause rereading of it @@ -410,13 +411,15 @@ def refresh(self): record.refresh() return self - def sort(self, cmp=None, key=None, reverse=False): + def sort(self, *args, **kwargs): """ sort(cmp=None, key=None, reverse=False) -- inplace sort cmp(x, y) -> -1, 0, 1 + Note, that 'cmp' argument, not available for python 3 + :return: self """ - self._records.sort(cmp=cmp, key=key, reverse=reverse) + self._records.sort(*args, **kwargs) return self def copy(self, context=None, new_cache=False): diff --git a/openerp_proxy/tests/__init__.py b/openerp_proxy/tests/__init__.py index 720af23..0235263 100644 --- a/openerp_proxy/tests/__init__.py +++ b/openerp_proxy/tests/__init__.py @@ -1,3 +1,4 @@ +import six import unittest from openerp_proxy.utils import AttrDict @@ -26,3 +27,7 @@ def setUp(self): for ext in with_extensions.split(','): __import__(ext) + if six.PY3: + def assertItemsEqual(self, *args, **kwargs): + return self.assertCountEqual(*args, **kwargs) + diff --git a/run_tests.bash b/run_tests.bash index 8600487..f7568d4 100755 --- a/run_tests.bash +++ b/run_tests.bash @@ -4,13 +4,22 @@ SCRIPT=`readlink -f "$0"` # Absolute path this script is in, thus /home/user/bin SCRIPTPATH=`dirname "$SCRIPT"` -(cd $SCRIPTPATH && \ - virtualenv venv_test && \ - source ./venv_test/bin/activate && \ - pip install --upgrade pip setuptools coverage mock pudb ipython && \ - python setup.py develop && \ - rm -f .coverage && \ - coverage run --source openerp_proxy -m unittest -v openerp_proxy.tests.all && \ - coverage html -d coverage && \ - deactivate && \ - rm -rf venv_test) +function test_it { + local py_version=$1; + (cd $SCRIPTPATH && \ + virtualenv venv_test -p python${py_version} && \ + source ./venv_test/bin/activate && \ + pip install --upgrade pip setuptools coverage mock pudb ipython && \ + python setup.py develop && \ + rm -f .coverage && \ + coverage run --source openerp_proxy -m unittest -v openerp_proxy.tests.all && \ + coverage html -d coverage && \ + deactivate && \ + rm -rf venv_test) +} + +PY_VERSIONS=${PY_VERSIONS:-"2.7 3.4"}; + +for version in $PY_VERSIONS; do + test_it $version; +done From 383ae332ea1626db66e0a489b6d6579f019e739d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Wed, 5 Aug 2015 15:20:55 +0300 Subject: [PATCH 10/23] Little bit more ORM tests + bugfixes --- openerp_proxy/core.py | 9 ++-- openerp_proxy/ext/sugar.py | 1 + openerp_proxy/orm/record.py | 21 ++++++-- openerp_proxy/session.py | 22 +++++--- openerp_proxy/tests/test_client.py | 6 +++ openerp_proxy/tests/test_orm.py | 82 ++++++++++++++++++++++++++++++ openerp_proxy/utils.py | 5 +- 7 files changed, 127 insertions(+), 19 deletions(-) diff --git a/openerp_proxy/core.py b/openerp_proxy/core.py index 7e75184..e990f40 100644 --- a/openerp_proxy/core.py +++ b/openerp_proxy/core.py @@ -46,6 +46,7 @@ ... 'assigned' """ +import six # project imports from .connection import get_connector @@ -73,7 +74,7 @@ ERPProxyException = ClientException - +six.python_2_unicode_compatible class Client(Extensible): """ A simple class to connect ot ERP via RPC (XML-RPC, JSON-RPC) @@ -353,7 +354,9 @@ def clean_caches(self): self.services.object.clean_caches() def __str__(self): - return "Client: %s" % self.get_url() - __repr__ = __str__ + return u"Client: %s" % self.get_url() + + def __repr__(self): + return str(self) ERP_Proxy = Client diff --git a/openerp_proxy/ext/sugar.py b/openerp_proxy/ext/sugar.py index 99e88f5..5d811ea 100644 --- a/openerp_proxy/ext/sugar.py +++ b/openerp_proxy/ext/sugar.py @@ -4,6 +4,7 @@ """ import numbers +import collections from openerp_proxy.orm.record import ObjectRecords from openerp_proxy.orm.record import get_record_list diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index 9b2566f..b240b0d 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -391,8 +391,8 @@ def insert(self, index, item): # present in this RecordList def __getattr__(self, name): method = getattr(self.object, name) - res = wpartial(method, self.ids, context=self.context) - #setattr(self, name, res) # commented because of __slots__ + kwargs = {} if self.context is None else {'context': self.context} + res = wpartial(method, self.ids, **kwargs) return res def __str__(self): @@ -482,7 +482,11 @@ def search(self, domain, *args, **kwargs): :returns: list of IDs found :rtype: list of integers """ - kwargs['context'] = self._new_context(kwargs.get('context', None)) + ctx = self._new_context(kwargs.get('context', None)) + + if ctx is not None: + kwargs['context'] = ctx + return self.object.search([('id', 'in', self.ids)] + domain, *args, **kwargs) def search_records(self, domain, *args, **kwargs): @@ -491,7 +495,11 @@ def search_records(self, domain, *args, **kwargs): :returns: RecordList of records found :rtype: RecordList instance """ - kwargs['context'] = self._new_context(kwargs.get('context', None)) + ctx = self._new_context(kwargs.get('context', None)) + + if ctx is not None: + kwargs['context'] = ctx + return self.object.search_records([('id', 'in', self.ids)] + domain, *args, **kwargs) def read(self, fields=None, context=None): @@ -501,7 +509,10 @@ def read(self, fields=None, context=None): kwargs = {} args = [] - kwargs['context'] = self._new_context(kwargs.get('context', None)) + ctx = self._new_context(kwargs.get('context', None)) + + if ctx is not None: + kwargs['context'] = ctx if fields is not None: args.append(fields) diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index 73bfb38..409e1c1 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -1,5 +1,4 @@ import numbers -import six import json import os.path import sys @@ -13,6 +12,13 @@ __all__ = ('ERP_Session', 'Session', 'IPYSession') +# Python 2/3 workaround in raw_input +try: + xinput = raw_input +except NameError: + xinput = input + + # TODO: completly refactor class Session(object): @@ -273,9 +279,9 @@ def connect(self, host=None, dbname=None, user=None, pwd=None, port=8069, protoc if interactive: # ask user for connection data if not provided, if interactive set # to True - host = host or raw_input('Server Host: ') - dbname = dbname or raw_input('Database name: ') - user = user or raw_input('ERP Login: ') + host = host or xinput('Server Host: ') + dbname = dbname or xinput('Database name: ') + user = user or xinput('ERP Login: ') pwd = pwd or getpass("Password: ") url = Client.to_url(inst=None, @@ -300,7 +306,7 @@ def _get_db_init_args(self, database): if self.option('store_passwords') and database._pwd: from simplecrypt import encrypt import base64 - password = base64.encodestring(b'simplecrypt:' + base64.encodestring(encrypt(database.get_url(), database._pwd))) + password = base64.encodestring(b'simplecrypt:' + base64.encodestring(encrypt(database.get_url(), database._pwd))).decode('utf-8') res.update({'password': password}) return res elif isinstance(database, dict): @@ -325,8 +331,10 @@ def save(self): 'options': self._options, } - with open(self.data_file, 'wt') as json_data: - json.dump(data, json_data, indent=4) + json_data = json.dumps(data, indent=4) + + with open(self.data_file, 'wt') as json_file: + json_file.write(json_data) # Overridden to be able to access database like # session[url_or_index] diff --git a/openerp_proxy/tests/test_client.py b/openerp_proxy/tests/test_client.py index 74a0c23..0aac6d2 100644 --- a/openerp_proxy/tests/test_client.py +++ b/openerp_proxy/tests/test_client.py @@ -57,6 +57,12 @@ def test_50_to_url(self): with self.assertRaises(ValueError): Client.to_url('strange thing') + def test_55_str(self): + self.assertEqual(str(self.client), u"Client: %s" % self.client.get_url()) + + def test_55_repr(self): + self.assertEqual(repr(self.client), str(self.client)) + def test_60_plugins(self): self.assertIn('Test', self.client.plugins.registered_plugins) self.assertIn('Test', self.client.plugins) diff --git a/openerp_proxy/tests/test_orm.py b/openerp_proxy/tests/test_orm.py index 1d79ff4..7050c43 100644 --- a/openerp_proxy/tests/test_orm.py +++ b/openerp_proxy/tests/test_orm.py @@ -82,6 +82,15 @@ def test_search_records(self): self.assertIsInstance(res, numbers.Integral) self.assertEqual(res, 1) + # test search_records with read_fields argument + res = self.object.search_records([], read_fields=['name', 'country_id'], limit=10) + self.assertIsInstance(res, RecordList) + self.assertEqual(res.length, 10) + self.assertEqual(len(res._lcache), res.length) + for record in res: + self.assertEqual(len(record._data), 3) + self.assertItemsEqual(list(record._data), ['id', 'name', 'country_id']) + def test_read_records(self): # read one record res = self.object.read_records(1) @@ -98,6 +107,11 @@ def test_read_records(self): with self.assertRaises(ValueError): self.object.read_records(None) + # Test read with specified fields + record = self.object.read_records(1, ['name', 'country_id']) + self.assertEqual(len(record._data), 3) + self.assertItemsEqual(list(record._data), ['id', 'name', 'country_id']) + def test_browse(self): with mock.patch.object(self.object, 'read_records') as fake_read_records: self.object.browse(1) @@ -111,6 +125,18 @@ def test_browse(self): self.object.browse(None) fake_read_records.assert_called_with(None) + def test_object_equal(self): + self.assertEqual(self.object, self.client['res.partner']) + self.assertIs(self.object, self.client['res.partner']) + self.assertNotEqual(self.object, self.client['res.users']) + self.assertIsNot(self.object, self.client['res.users']) + + def test_str(self): + self.assertEqual(str(self.object), u"Object ('res.partner')") + + def test_repr(self): + self.assertEqual(repr(self.object), str(self.object)) + class Test_21_Record(BaseTestCase): @@ -134,6 +160,30 @@ def test_dir(self): # test if normal mehtods avalilable in dir(object) #self.assertIn('refresh', dir(self.record)) + def test_proxy_property(self): + self.assertIs(self.record._proxy, self.client) + self.assertIs(self.record._object.proxy, self.client) + self.assertIs(self.object.proxy, self.client) + + def test_as_dict(self): + rdict = self.record.as_dict + + self.assertIsInstance(rdict, dict) + self.assertIsNot(rdict, self.record._data) + self.assertItemsEqual(rdict, self.record._data) + + # test that changes to rdict will not calue changes to record's data + rdict['new_key'] = 'new value' + + self.assertIn('new_key', rdict) + self.assertNotIn('new_key', self.record._data) + + def test_str(self): + self.assertEqual(str(self.record), u"R(res.partner, 1)[%s]" % (self.record.name_get()[0][1])) + + def test_repr(self): + self.assertEqual(str(self.record), repr(self.record)) + def test_name_get(self): self.assertEqual(self.record._name, self.record.name_get()[0][1]) @@ -219,6 +269,12 @@ def test_recods(self): self.assertIsInstance(self.recordlist.records, list) self.assertIsInstance(self.recordlist.records[0], Record) + def test_str(self): + self.assertEqual(str(self.recordlist), u"RecordList(res.partner): length=10") + + def test_repr(self): + self.assertEqual(repr(self.recordlist), str(self.recordlist)) + def test_getitem(self): id1 = self.obj_ids[0] id2 = self.obj_ids[-1] @@ -239,6 +295,14 @@ def test_getitem(self): with self.assertRaises(IndexError): self.recordlist[100] + def test_getattr(self): + with mock.patch.object(self.object, 'some_server_method') as fake_method: + # bug override in mock object (python 2.7) + if not getattr(fake_method, '__name__', False): + fake_method.__name__ = fake_method.name + self.recordlist.some_server_method('arg1', 'arg2') + fake_method.assert_called_with(self.recordlist.ids, 'arg1', 'arg2') + def test_delitem(self): r = self.recordlist[5] self.assertEqual(len(self.recordlist), 10) @@ -409,4 +473,22 @@ def to_names(rlist): rnames.reverse() # inplace reverse self.assertSequenceEqual(rnames, to_names(rlist)) + def test_search(self): + # TODO: test for context + with mock.patch.object(self.object, 'search') as fake_method: + self.recordlist.search([('id', '!=', 1)], limit=5) + fake_method.assert_called_with([('id', 'in', self.recordlist.ids), ('id', '!=', 1)], limit=5) + def test_search_records(self): + # TODO: test for context + with mock.patch.object(self.object, 'search_records') as fake_method: + self.recordlist.search_records([('id', '!=', 1)], limit=4) + fake_method.assert_called_with([('id', 'in', self.recordlist.ids), ('id', '!=', 1)], limit=4) + + def test_read(self): + # TODO: test for context + # or remove this test and method, because getattr pass context + # too + with mock.patch.object(self.object, 'read') as fake_method: + self.recordlist.read(['name']) + fake_method.assert_called_with(self.recordlist.ids, ['name']) diff --git a/openerp_proxy/utils.py b/openerp_proxy/utils.py index 8eb8a10..24abd0c 100644 --- a/openerp_proxy/utils.py +++ b/openerp_proxy/utils.py @@ -25,10 +25,7 @@ def wpartial(func, *args, **kwargs): """ partial = functools.partial(func, *args, **kwargs) - @functools.wraps(func) - def wrapper(*a, **kw): - return partial(*a, **kw) - return wrapper + return functools.wraps(func)(partial) # Copied from OpenERP source ustr function From bcd8a3fc0ef47bcc785f3653bc72b567fc4c76cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Thu, 20 Aug 2015 18:33:28 +0300 Subject: [PATCH 11/23] session.py: little bit refactored + tests added. --- .travis.yml | 2 +- bin/openerp_proxy | 2 +- openerp_proxy/__init__.py | 69 ---------- openerp_proxy/core.py | 8 +- openerp_proxy/main.py | 70 ++++++++++ openerp_proxy/session.py | 129 ++++++++---------- openerp_proxy/tests/all.py | 1 + openerp_proxy/tests/test_session.py | 201 ++++++++++++++++++++++++++++ openerp_proxy/utils.py | 28 ++++ run_tests.bash | 2 +- 10 files changed, 366 insertions(+), 146 deletions(-) create mode 100644 openerp_proxy/main.py create mode 100644 openerp_proxy/tests/test_session.py diff --git a/.travis.yml b/.travis.yml index 454e1cd..d3c603a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - "wget http://nightly.odoo.com/${ODOO_VERSION}/nightly/deb/${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb" - "sudo dpkg -i ${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb || true" - "sudo apt-get update && sudo apt-get install -f -y" - - "pip install coveralls mock ipython[all]" + - "pip install coveralls mock simple-crypt ipython[all]" - "python setup.py develop" before_script: diff --git a/bin/openerp_proxy b/bin/openerp_proxy index e8a8a5c..1573ca5 100755 --- a/bin/openerp_proxy +++ b/bin/openerp_proxy @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf8 -*- -from openerp_proxy import main +from openerp_proxy.main import main if __name__ == '__main__': main() diff --git a/openerp_proxy/__init__.py b/openerp_proxy/__init__.py index 830660d..e69de29 100644 --- a/openerp_proxy/__init__.py +++ b/openerp_proxy/__init__.py @@ -1,69 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf8 -*- - - -HELP_HEADER = """ - Usage: - >>> db = session.connect() - >>> so_obj = db['sale.orderl'] # get object - >>> dir(so_obj) # Thid will show all default methods of object - >>> so_id = 123 # ID of sale order - >>> so_obj.read(so_id) - >>> so_obj.write([so_id], {'note': 'Test'}) - >>> sm_obj = db['stock.move'] - >>> - >>> # check availability of stock move (call server-side method) - >>> sm_obj.check_assign([move_id1, move_id2,...]) - - Available objects in context: - ERP_Proxy - class that represents single OpenERP database and - provides methods to work with data. Instances of this - class returned by connect() method of session object. - session - represents session of client, stores in home directory list - of databases user works with, to simplify work. It is simpler - to get list of databases you have worked with previously on program - start, and to connect to them without remembrering hosts, users, ports - and other unneccesary information - - Databases You previously worked with: %(databases)s - - Aliases: %(aliases)s - - (Used index or url or aliase for session: session[1] or session[url] or session[aliase]) -""" - - -def main(): - """ Entry point for running as standalone APP - """ - from .session import Session - from .core import Client - - session = Session() - - header_databases = "\n" - for index, url in session.index.items(): - header_databases += " - [%3s] %s\n" % (index, url) - - header_aliases = "\n" - for aliase, url in session.aliases.items(): - header_aliases += " - %7s: %s\n" % (aliase, url) - - header = HELP_HEADER % {'databases': header_databases, 'aliases': header_aliases} - - _locals = { - 'ERP_Proxy': Client, - 'Client': Client, - 'session': session, - } - try: - from IPython import embed - embed(user_ns=_locals, header=header) - except ImportError: - from code import interact - interact(local=_locals, banner=header) - - session.save() - -if __name__ == '__main__': - main() diff --git a/openerp_proxy/core.py b/openerp_proxy/core.py index e990f40..93938a4 100644 --- a/openerp_proxy/core.py +++ b/openerp_proxy/core.py @@ -74,7 +74,7 @@ ERPProxyException = ClientException -six.python_2_unicode_compatible +@six.python_2_unicode_compatible class Client(Extensible): """ A simple class to connect ot ERP via RPC (XML-RPC, JSON-RPC) @@ -359,4 +359,10 @@ def __str__(self): def __repr__(self): return str(self) + def __eq__(self, other): + if isinstance(other, Client): + return self.get_url() == other.get_url() + else: + return False + ERP_Proxy = Client diff --git a/openerp_proxy/main.py b/openerp_proxy/main.py new file mode 100644 index 0000000..641e1ec --- /dev/null +++ b/openerp_proxy/main.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + + +HELP_HEADER = """ + Usage: + >>> db = session.connect() + >>> so_obj = db['sale.orderl'] # get object + >>> dir(so_obj) # Thid will show all default methods of object + >>> so_id = 123 # ID of sale order + >>> so_obj.read(so_id) + >>> so_obj.write([so_id], {'note': 'Test'}) + >>> sm_obj = db['stock.move'] + >>> + >>> # check availability of stock move (call server-side method) + >>> sm_obj.check_assign([move_id1, move_id2,...]) + + Available objects in context: + Client - class that represents single Odoo / OpenERP database and + provides methods to work with data. Session.connect method usualy + returns instances of this class. + session - represents session of client, stores in home directory list + of databases user works with, to simplify work. It is simpler + to get list of databases you have worked with previously on program + start, and to connect to them without remembrering hosts, users, ports + and other unneccesary information + + Databases You previously worked with: %(databases)s + + Aliases: %(aliases)s + + (Use index or url or aliase for session: session[1] or session[url] or session[aliase]) +""" + + +def main(): + """ Entry point for running as standalone APP + """ + from .session import Session + from .core import Client + + session = Session() + + header_databases = "\n" + for index, url in session.index.items(): + header_databases += " - [%3s] %s\n" % (index, url) + + header_aliases = "\n" + for aliase, url in session.aliases.items(): + header_aliases += " - %7s: %s\n" % (aliase, url) + + header = HELP_HEADER % {'databases': header_databases, 'aliases': header_aliases} + + _locals = { + 'ERP_Proxy': Client, + 'Client': Client, + 'session': session, + } + try: + from IPython import embed + embed(user_ns=_locals, header=header) + except ImportError: + from code import interact + interact(local=_locals, banner=header) + + session.save() + +if __name__ == '__main__': + main() + diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index 409e1c1..c7520cd 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -7,18 +7,14 @@ # project imports from .core import Client +from .utils import (json_read, + json_write, + xinput) __all__ = ('ERP_Session', 'Session', 'IPYSession') -# Python 2/3 workaround in raw_input -try: - xinput = raw_input -except NameError: - xinput = input - - # TODO: completly refactor class Session(object): @@ -43,6 +39,7 @@ def __init__(self, data_file='~/.openerp_proxy.json'): """ """ self.data_file = os.path.expanduser(data_file) + self._databases = {} # key: url; value: instance of DB or dict with init args self._db_aliases = {} # key: aliase name; value: url self._options = {} @@ -51,41 +48,39 @@ def __init__(self, data_file='~/.openerp_proxy.json'): self._db_index_rev = {} # key: url; value: index self._db_index_counter = 0 - self._start_up_imports = [] # list of modules/packages to be imported at startup - - self._extra_paths = set() - if os.path.exists(self.data_file): - with open(self.data_file, 'rt') as json_data: - data = json.load(json_data) + data = json_read(self.data_file) - self._databases = data.get('databases', {}) - self._db_aliases = data.get('aliases', {}) - self._options = data.get('options', {}) + self._databases = data.get('databases', {}) + self._db_aliases = data.get('aliases', {}) + self._options = data.get('options', {}) - self._init_paths(data) - self._init_start_up_imports(data) + for path in self.extra_paths: + self.add_path(path) - def _init_start_up_imports(self, data): - """ Loads list of modules/packages names to be imported at start-up, - saved in previous session + for module in self.start_up_imports: + try: + __import__(module) + except ImportError: + # TODO: implement some logging + pass - :param data: dictionary with data read from saved session file + @property + def extra_paths(self): + """ List of extra pyhton paths, used by this session """ - self._start_up_imports += data.get('start_up_imports', []) - self._start_up_imports = list(set(self._start_up_imports)) - for i in self._start_up_imports: - try: - __import__(i) - except ImportError: - # TODO: implement some logging - pass - - def _init_paths(self, data): - """ This method initializes aditional python paths saved in session + return self.option('extra_paths', default=[]) + + @property + def start_up_imports(self): + """ List of start-up imports + + If You want some module to be automaticaly imported on + when session starts, that just add it to this list:: + + session.start_up_imports.append('openerp_proxy.ext.sugar') """ - for path in data.get('extra_paths', []): - self.add_path(path) + return self.option('start_up_imports', default=[]) def add_path(self, path): """ Adds extra path to python import path. @@ -96,11 +91,14 @@ def add_path(self, path): Note: this way path will be saved in session """ + # TODO: rewrite extrapaths logic with custom importers. It will be more + # pythonic if path not in sys.path: sys.path.append(path) - self._extra_paths.add(path) + if path not in self.extra_paths: + self.extra_paths.append(path) - def option(self, opt, val=None): + def option(self, opt, val=None, default=None): """ Get or set option. if *val* is passed, *val* will be set as value for option, else just option value will be returned @@ -118,7 +116,9 @@ def option(self, opt, val=None): """ if val is not None: self._options[opt] = val - return self._options.get(opt, None) + elif opt not in self._options and default is not None: + self._options[opt] = default + return self._options.get(opt, default) @property def aliases(self): @@ -152,14 +152,17 @@ def aliase(self, name, val): :return: unchanged val """ - if val in self._databases: - self._db_aliases[name] = val - elif val in self.index: - self._db_aliases[name] = self.index[val] - elif isinstance(val, Client): - self._db_aliases[name] = val.get_url() + if isinstance(val, Client): + url = val.get_url() + elif isinstance(val, numbers.Integral) and val in self.index: + url = self.index[val] + else: + url = val + + if url in self._databases: + self._db_aliases[name] = url else: - raise ValueError("Bad value type") + raise ValueError("Bad value type: %s" % val) return val @@ -172,17 +175,6 @@ def index(self): self._index_url(url) return dict(self._db_index) - @property - def start_up_imports(self): - """ List of start-up imports - - If You want some module to be automaticaly imported on - when session starts, that just add it to this list:: - - session.start_up_imports.append('openerp_proxy.ext.sugar') - """ - return self._start_up_imports - def _index_url(self, url): """ Returns index of specified URL, or adds it to store assigning new index @@ -195,19 +187,15 @@ def _index_url(self, url): self._db_index_rev[url] = self._db_index_counter return self._db_index_counter - def _add_db(self, url, db): - """ Add database to history - """ - self._databases[url] = db - self._index_url(url) - def add_db(self, db): """ Add db to session. param db: database (client instance) to be added to session type db: Client instance """ - self._add_db(db.get_url(), db) + url = db.get_url() + self._databases[url] = db + self._index_url(url) def get_db(self, url_or_index, **kwargs): """ Returns instance of Client object, that represents single @@ -281,7 +269,7 @@ def connect(self, host=None, dbname=None, user=None, pwd=None, port=8069, protoc # to True host = host or xinput('Server Host: ') dbname = dbname or xinput('Database name: ') - user = user or xinput('ERP Login: ') + user = user or xinput('Login: ') pwd = pwd or getpass("Password: ") url = Client.to_url(inst=None, @@ -296,8 +284,8 @@ def connect(self, host=None, dbname=None, user=None, pwd=None, port=8069, protoc return db db = Client(host=host, dbname=dbname, user=user, pwd=pwd, port=port, protocol=protocol) - self._add_db(url, db) - db._no_save = no_save # disalows saving database connection in session + self.add_db(db) + db._no_save = no_save # if set to True, disalows saving database connection in session return db def _get_db_init_args(self, database): @@ -325,16 +313,11 @@ def save(self): data = { 'databases': databases, - 'extra_paths': list(self._extra_paths), 'aliases': self._db_aliases, - 'start_up_imports': self._start_up_imports, 'options': self._options, } - json_data = json.dumps(data, indent=4) - - with open(self.data_file, 'wt') as json_file: - json_file.write(json_data) + json_write(self.data_file, data, indent=4) # Overridden to be able to access database like # session[url_or_index] @@ -358,7 +341,7 @@ def __str__(self): return pprint.pformat(self.index) def __dir__(self): - res = dir(super(ERP_Session, self)) + res = dir(super(Session, self)) res += self.aliases.keys() return res diff --git a/openerp_proxy/tests/all.py b/openerp_proxy/tests/all.py index 51ed461..98341c9 100644 --- a/openerp_proxy/tests/all.py +++ b/openerp_proxy/tests/all.py @@ -1,3 +1,4 @@ from .test_connection import * from .test_client import * from .test_orm import * +from .test_session import * diff --git a/openerp_proxy/tests/test_session.py b/openerp_proxy/tests/test_session.py new file mode 100644 index 0000000..efae6f1 --- /dev/null +++ b/openerp_proxy/tests/test_session.py @@ -0,0 +1,201 @@ +from . import BaseTestCase +from openerp_proxy.core import Client +from openerp_proxy.session import Session +from openerp_proxy.orm.record import Record +from openerp_proxy.orm.record import RecordList +from openerp_proxy.exceptions import ConnectorError + +try: + import unittest.mock as mock +except ImportError: + import mock + +import sys +import os +import os.path +import numbers +import collections + + +class Test_30_Session(BaseTestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self._session_file_path = '/tmp/openerp_proxy.session.json' + + def tearDown(self): + if os.path.exists(self._session_file_path): + os.unlink(self._session_file_path) + + def test_01_init_save(self): + session = Session(self._session_file_path) + self.assertFalse(os.path.exists(self._session_file_path)) + session.save() + self.assertTrue(os.path.exists(self._session_file_path)) + + def test_05_add_path(self): + old_sys_path = sys.path[:] + self.assertNotIn('/new_path', sys.path) + session = Session(self._session_file_path) + session.add_path('/new_path') + self.assertIn('/new_path', sys.path) + session.save() + del session + sys.path = old_sys_path[:] + self.assertNotIn('/new_path', sys.path) + + # test that path is automaticaly added on new session init + session = Session(self._session_file_path) + self.assertIn('/new_path', sys.path) + del session + sys.path = old_sys_path[:] + self.assertNotIn('/new_path', sys.path) + + def test_10_option(self): + session = Session(self._session_file_path) + self.assertIs(session.option('store_passwords'), None) + session.option('store_passwords', True) + self.assertTrue(session.option('store_passwords')) + session.save() + del session + + session = Session(self._session_file_path) + self.assertTrue(session.option('store_passwords')) + + def test_15_connect_save_connect(self): + session = Session(self._session_file_path) + + # set store_passwords to true, to avoid password promt during tests + session.option('store_passwords', True) + + cl = session.connect(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port, + interactive=False) + + self.assertIsInstance(cl, Client) + self.assertIn(cl.get_url(), session._databases) + self.assertIn(cl.get_url(), session.db_list) + self.assertEqual(len(session.db_list), 1) + self.assertIs(session.get_db(cl.get_url()), cl) + self.assertEqual(session.index[1], cl.get_url()) # first db must be with index=1 + + # index and url may be used in this way too + self.assertIs(session[cl.get_url()], cl) + self.assertIs(session[1], cl) + + # save the session + session.save() + del session + + # recreate session + session = Session(self._session_file_path) + + # and test again + self.assertIn(cl.get_url(), session._databases) + self.assertIn(cl.get_url(), session.db_list) + self.assertEqual(len(session.db_list), 1) + self.assertIsNot(session.get_db(cl.get_url()), cl) + self.assertEqual(session.get_db(cl.get_url()), cl) + self.assertEqual(session.index[1], cl.get_url()) # first db must be with index=1 + self.assertIsNot(session[cl.get_url()], cl) + self.assertIsNot(session[1], cl) + self.assertEqual(session[cl.get_url()], cl) + self.assertEqual(session[1], cl) + + def test_20_connect_save_connect_no_save(self): + session = Session(self._session_file_path) + + # set store_passwords to true, to avoid password promt during tests + session.option('store_passwords', True) + + cl = session.connect(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port, + interactive=False, + no_save=True) # this arg is different from previous test + + self.assertIsInstance(cl, Client) + self.assertIn(cl.get_url(), session._databases) + self.assertIn(cl.get_url(), session.db_list) + self.assertEqual(len(session.db_list), 1) + self.assertIs(session.get_db(cl.get_url()), cl) + self.assertEqual(session.index[1], cl.get_url()) # first db must be with index=1 + + # index and url may be used in this way too + self.assertIs(session[cl.get_url()], cl) + self.assertIs(session[1], cl) + + # save the session + session.save() + del session + + # recreate session + session = Session(self._session_file_path) + + # and test again + self.assertNotIn(cl.get_url(), session._databases) + self.assertNotIn(cl.get_url(), session.db_list) + self.assertEqual(len(session.db_list), 0) + + with self.assertRaises(ValueError): + session.get_db(cl.get_url()) + + with self.assertRaises(KeyError): + session[cl.get_url()] + + def test_25_aliases(self): + session = Session(self._session_file_path) + + # set store_passwords to true, to avoid password promt during tests + session.option('store_passwords', True) + + cl = session.connect(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port, + interactive=False) + + self.assertEqual(len(session.aliases), 0) + + res = session.aliase('cl1', cl) + self.assertIs(res, cl) + + res = session.aliase('cl2', 1) # use index + self.assertIs(res, 1) + + res = session.aliase('cl3', cl.get_url()) # use url + self.assertEqual(res, cl.get_url()) + + self.assertIn('cl1', session.aliases) + self.assertIs(session.get_db('cl1'), cl) + self.assertIs(session['cl1'], cl) + self.assertIs(session.cl1, cl) + + self.assertIs(session.cl1, session.cl2) + self.assertIs(session.cl1, session.cl3) + + # save the session + session.save() + del session + + # recreate session + session = Session(self._session_file_path) + + # and test again + self.assertEqual(len(session.aliases), 3) + self.assertIn('cl1', session.aliases) + self.assertEqual(session.get_db('cl1'), cl) + self.assertEqual(session['cl1'], cl) + self.assertEqual(session.cl1, cl) + + self.assertIs(session.cl1, session.cl2) + self.assertIs(session.cl1, session.cl3) diff --git a/openerp_proxy/utils.py b/openerp_proxy/utils.py index 24abd0c..529a38b 100644 --- a/openerp_proxy/utils.py +++ b/openerp_proxy/utils.py @@ -1,9 +1,16 @@ import six import sys +import json import functools __all__ = ('ustr', 'AttrDict', 'wpartial') +# Python 2/3 workaround in raw_input +try: + xinput = raw_input +except NameError: + xinput = input + def r_eval(code): """ Helper function to be used in filters or so @@ -18,6 +25,27 @@ def r_eval_internal(record): return r_eval_internal +def json_read(file_path): + """ Read specified json file + """ + with open(file_path, 'rt') as json_data: + data = json.load(json_data) + return data + + +def json_write(file_path, *args, **kwargs): + """ Write data to specified json file + + Note, this function uses dumps function to convert data to json first, + and write only if conversion is successfule. This allows to avoid loss of data + when rewriting file. + """ + json_data = json.dumps(*args, **kwargs) + + with open(file_path, 'wt') as json_file: + json_file.write(json_data) + + def wpartial(func, *args, **kwargs): """Wrapped partial, same as functools.partial decorator, but also calls functools.wrap on its result thus shwing correct diff --git a/run_tests.bash b/run_tests.bash index f7568d4..4378fc2 100755 --- a/run_tests.bash +++ b/run_tests.bash @@ -9,7 +9,7 @@ function test_it { (cd $SCRIPTPATH && \ virtualenv venv_test -p python${py_version} && \ source ./venv_test/bin/activate && \ - pip install --upgrade pip setuptools coverage mock pudb ipython && \ + pip install --upgrade pip setuptools coverage mock pudb ipython simple-crypt && \ python setup.py develop && \ rm -f .coverage && \ coverage run --source openerp_proxy -m unittest -v openerp_proxy.tests.all && \ From 740dfa72cbfb39ec8f94474d5b828fe47707c504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Fri, 28 Aug 2015 15:47:00 +0300 Subject: [PATCH 12/23] Added tests sugar extension --- openerp_proxy/ext/sugar.py | 2 +- openerp_proxy/session.py | 3 +- openerp_proxy/tests/__init__.py | 9 ++- openerp_proxy/tests/all.py | 1 + openerp_proxy/tests/ext/__init__.py | 10 ++++ openerp_proxy/tests/ext/test_sugar.py | 79 +++++++++++++++++++++++++++ openerp_proxy/tests/test_client.py | 1 + openerp_proxy/tests/test_session.py | 20 +++++-- openerp_proxy/version.py | 2 +- 9 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 openerp_proxy/tests/ext/__init__.py create mode 100644 openerp_proxy/tests/ext/test_sugar.py diff --git a/openerp_proxy/ext/sugar.py b/openerp_proxy/ext/sugar.py index 5d811ea..f8c61d8 100644 --- a/openerp_proxy/ext/sugar.py +++ b/openerp_proxy/ext/sugar.py @@ -100,7 +100,7 @@ def __call__(self, *args, **kwargs): # normal domain passed, then just forward all arguments and # keyword arguments to *search_records* method - if isinstance(name, collections.Iterable): + if isinstance(name, (list, tuple)): return self.search_records(name, *args, **kwargs) # implement name_search capability diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index c7520cd..6bb0e59 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -1,5 +1,4 @@ import numbers -import json import os.path import sys import pprint @@ -264,7 +263,7 @@ def connect(self, host=None, dbname=None, user=None, pwd=None, port=8069, protoc :param bool no_save: if set to True database will not be saved to session :return: Client object """ - if interactive: + if interactive: # pragma: no cover # ask user for connection data if not provided, if interactive set # to True host = host or xinput('Server Host: ') diff --git a/openerp_proxy/tests/__init__.py b/openerp_proxy/tests/__init__.py index 0235263..7be27ad 100644 --- a/openerp_proxy/tests/__init__.py +++ b/openerp_proxy/tests/__init__.py @@ -21,11 +21,10 @@ def setUp(self): 'super_password': os.environ.get('ODOO_TEST_SUPER_PASSWORD', 'admin'), }) - # allow to specify extensions with environment var - with_extensions = os.environ.get('TEST_WITH_EXTENSIONS', False) - if with_extensions: - for ext in with_extensions.split(','): - __import__(ext) + # allow to specify if extensions should be enabled for testing + self.with_extensions = os.environ.get('TEST_WITH_EXTENSIONS', False) + if self.with_extensions: + import openerp_proxy.ext.all if six.PY3: def assertItemsEqual(self, *args, **kwargs): diff --git a/openerp_proxy/tests/all.py b/openerp_proxy/tests/all.py index 98341c9..df5f276 100644 --- a/openerp_proxy/tests/all.py +++ b/openerp_proxy/tests/all.py @@ -2,3 +2,4 @@ from .test_client import * from .test_orm import * from .test_session import * +from .ext.test_sugar import * diff --git a/openerp_proxy/tests/ext/__init__.py b/openerp_proxy/tests/ext/__init__.py new file mode 100644 index 0000000..cc17931 --- /dev/null +++ b/openerp_proxy/tests/ext/__init__.py @@ -0,0 +1,10 @@ +##import six +#import unittest +#import os +#from openerp_proxy.tests.test_sugar import * +##from openerp_proxy.tests import BaseTestCase + + +##@unittest.skipUnless(os.environ.get('TEST_WITH_EXTENSIONS', False), 'requires tests enabled') +##class BaseExtTestCase(BaseTestCase): + ##pass diff --git a/openerp_proxy/tests/ext/test_sugar.py b/openerp_proxy/tests/ext/test_sugar.py new file mode 100644 index 0000000..3ae9e14 --- /dev/null +++ b/openerp_proxy/tests/ext/test_sugar.py @@ -0,0 +1,79 @@ +import unittest +import os + +try: + import unittest.mock as mock +except ImportError: + import mock + +from openerp_proxy.tests import BaseTestCase +from openerp_proxy.core import Client +from openerp_proxy.orm.record import (Record, + RecordList) +from openerp_proxy.orm.object import Object + + +@unittest.skipUnless(os.environ.get('TEST_WITH_EXTENSIONS', False), 'requires tests enabled') +class Test_31_ExtSugar(BaseTestCase): + def setUp(self): + super(Test_31_ExtSugar, self).setUp() + + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + self.object = self.client.get_obj('res.partner') + self.record = self.object.browse(1) + self.obj_ids = self.object.search([], limit=10) + self.recordlist = self.object.read_records(self.obj_ids) + + def test_obj_search_record(self): + res = self.object.search_record([('name', 'ilike', 'admin')]) + self.assertIsInstance(res, Record) + self.assertEqual(res.name, 'Administrator') + + def test_obj_getitem(self): + res = self.object[self.record.id] + self.assertIsInstance(res, Record) + self.assertEqual(res, self.record) + + with self.assertRaises(KeyError): + self.object['bad key'] + + def test_obj_len(self): + self.assertEqual(len(self.object), self.object.search([], count=True)) + + def test_obj_call_name_search(self): + res = self.object('admin') # name_search by name. only one record with this name + self.assertIsInstance(res, Record) + self.assertEqual(res._name, 'Administrator') + + res = self.object('Bank') + self.assertIsInstance(res, RecordList) + bank_ids = [i for i, _ in self.object.name_search('Bank')] + self.assertItemsEqual(res.ids, bank_ids) + + def test_obj_call_search_records(self): + with mock.patch.object(self.object, 'search_records') as fake_search_records: + self.object([('name', 'ilike', 'admin')]) + fake_search_records.assert_called_with([('name', 'ilike', 'admin')]) + + self.object([('name', 'ilike', 'admin')], count=True) + fake_search_records.assert_called_with([('name', 'ilike', 'admin')], count=True) + + self.object(name='admin') + fake_search_records.assert_called_with([('name', '=', 'admin')]) + + def test_client_dir(self): + self.assertIn('_res_partner', dir(self.client)) + + def test_client_getattr(self): + res = self.client._res_partner + self.assertIsInstance(res, Object) + self.assertEqual(res, self.object) + + with self.assertRaises(AttributeError): + self.client._some_bad_model + diff --git a/openerp_proxy/tests/test_client.py b/openerp_proxy/tests/test_client.py index 0aac6d2..3fab1b4 100644 --- a/openerp_proxy/tests/test_client.py +++ b/openerp_proxy/tests/test_client.py @@ -73,6 +73,7 @@ def test_60_plugins(self): # check plugin's method result self.assertEqual(self.client.get_url(), self.client.plugins.Test.test()) + self.assertEqual(repr(self.client.plugins.Test), 'openerp_proxy.plugin.Plugin:Test') def test_62_plugins_wrong_name(self): self.assertNotIn('Test_Bad', self.client.plugins.registered_plugins) diff --git a/openerp_proxy/tests/test_session.py b/openerp_proxy/tests/test_session.py index efae6f1..2b1983a 100644 --- a/openerp_proxy/tests/test_session.py +++ b/openerp_proxy/tests/test_session.py @@ -1,9 +1,6 @@ from . import BaseTestCase from openerp_proxy.core import Client from openerp_proxy.session import Session -from openerp_proxy.orm.record import Record -from openerp_proxy.orm.record import RecordList -from openerp_proxy.exceptions import ConnectorError try: import unittest.mock as mock @@ -13,11 +10,9 @@ import sys import os import os.path -import numbers -import collections -class Test_30_Session(BaseTestCase): +class Test_90_Session(BaseTestCase): def setUp(self): super(self.__class__, self).setUp() @@ -105,6 +100,13 @@ def test_15_connect_save_connect(self): self.assertIsNot(session[1], cl) self.assertEqual(session[cl.get_url()], cl) self.assertEqual(session[1], cl) + del session + + # test situation when session just started and saved, without changes + # this code is aimed mostly to increase test coverage. In this case in + # ._databases all values will be dict when saveing + session = Session(self._session_file_path) + session.save() def test_20_connect_save_connect_no_save(self): session = Session(self._session_file_path) @@ -182,6 +184,7 @@ def test_25_aliases(self): self.assertIs(session.cl1, session.cl2) self.assertIs(session.cl1, session.cl3) + self.assertIn('cl1', dir(session)) # save the session session.save() @@ -199,3 +202,8 @@ def test_25_aliases(self): self.assertIs(session.cl1, session.cl2) self.assertIs(session.cl1, session.cl3) + + with self.assertRaises(AttributeError): + session.unexistent_aliase + + self.assertIn('cl1', dir(session)) diff --git a/openerp_proxy/version.py b/openerp_proxy/version.py index c0ca287..95787d5 100644 --- a/openerp_proxy/version.py +++ b/openerp_proxy/version.py @@ -1 +1 @@ -version = "0.5" +version = "0.5" # pragma: no cover From 087c4d44ac5d80227282e72d0ae3362ae65fcb93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Fri, 28 Aug 2015 16:52:47 +0300 Subject: [PATCH 13/23] Added tests for workflow and module_utils plugin --- openerp_proxy/tests/all.py | 2 + openerp_proxy/tests/ext/test_workflow.py | 70 ++++++++++++++++++++++ openerp_proxy/tests/test_plugins.py | 76 ++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 openerp_proxy/tests/ext/test_workflow.py create mode 100644 openerp_proxy/tests/test_plugins.py diff --git a/openerp_proxy/tests/all.py b/openerp_proxy/tests/all.py index df5f276..55688d5 100644 --- a/openerp_proxy/tests/all.py +++ b/openerp_proxy/tests/all.py @@ -1,5 +1,7 @@ from .test_connection import * from .test_client import * from .test_orm import * +from .test_plugins import * from .test_session import * from .ext.test_sugar import * +from .ext.test_workflow import * diff --git a/openerp_proxy/tests/ext/test_workflow.py b/openerp_proxy/tests/ext/test_workflow.py new file mode 100644 index 0000000..8f2b287 --- /dev/null +++ b/openerp_proxy/tests/ext/test_workflow.py @@ -0,0 +1,70 @@ +import unittest +import os + +try: + import unittest.mock as mock +except ImportError: + import mock + +from openerp_proxy.tests import BaseTestCase +from openerp_proxy.core import Client +from openerp_proxy.orm.record import (Record, + RecordList) +from openerp_proxy.orm.object import Object + + +@unittest.skipUnless(os.environ.get('TEST_WITH_EXTENSIONS', False), 'requires tests enabled') +class Test_32_ExtWorkFlow(BaseTestCase): + def setUp(self): + super(Test_32_ExtWorkFlow, self).setUp() + + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + self.object = self.client.get_obj('sale.order') + self.record = self.object.browse(1) + self.obj_ids = self.object.search([], limit=10) + self.recordlist = self.object.read_records(self.obj_ids) + + def test_obj_workflow(self): + res = self.object.workflow + self.assertIsInstance(res, Record) + self.assertEqual(res._object.name, 'workflow') + self.assertEqual(res.osv, 'sale.order') + + def test_record_wkf_instance(self): + res = self.record.workflow_instance + self.assertIsInstance(res, Record) + self.assertEqual(res.wkf_id.id, self.object.workflow.id) + self.assertEqual(res.res_id, self.record.id) + + def test_record_wkf_items(self): + res = self.record.workflow_items + self.assertIsInstance(res, RecordList) + self.assertEqual(len(res), 1) + self.assertEqual(res[0]._object.name, 'workflow.workitem') + + @unittest.skipIf(os.environ.get('TEST_WITHOUT_DB_CHANGES', False), 'db changes not allowed. skipped') + def test_record_signal_send(self): + # first sale order seems to be in draft state on just created DB + so = self.record + + # get current SO activity + act = so.workflow_items[0].act_id + act_id = act.id + + # get first avalable transition with signal + trans = [t for t in act.out_transitions if t.signal] + if not trans: + raise unittest.SkipError("There is no avalable transitions in first sale order to test workflow") + trans = trans[0] + + # send signal + so.workflow_signal(trans.signal) + so.refresh() # refresh record to reflect database changes + + # test it + self.assertNotEqual(so.workflow_items[0].act_id.id, act_id) diff --git a/openerp_proxy/tests/test_plugins.py b/openerp_proxy/tests/test_plugins.py new file mode 100644 index 0000000..6866caa --- /dev/null +++ b/openerp_proxy/tests/test_plugins.py @@ -0,0 +1,76 @@ +from . import BaseTestCase +from openerp_proxy.core import Client +from openerp_proxy.orm.record import Record +from openerp_proxy.orm.record import RecordList +from openerp_proxy.exceptions import ConnectorError + +import unittest + +try: + import unittest.mock as mock +except ImportError: + import mock + +import numbers +import collections + + +class Test_25_Plugin_ModuleUtils(BaseTestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + + def test_10_init_module_utils(self): + self.assertNotIn('module_utils', self.client.plugins) + + import openerp_proxy.plugins.module_utils + + self.assertIn('module_utils', self.client.plugins) + + def test_15_modules(self): + self.assertIn('sale', self.client.plugins.module_utils.modules) + + def test_20_modules_dir(self): + self.assertIn('m_sale', dir(self.client.plugins.module_utils)) + + def test_25_module_getitem(self): + res = self.client.plugins.module_utils['sale'] + self.assertIsInstance(res, Record) + self.assertEqual(res._object.name, 'ir.module.module') + + from openerp_proxy.plugins.module_utils import ModuleObject + + self.assertIn(ModuleObject, res._object.__class__.__bases__) + + def test_30_module_getattr(self): + res = self.client.plugins.module_utils.m_sale + + self.assertIsInstance(res, Record) + self.assertEqual(res._object.name, 'ir.module.module') + + from openerp_proxy.plugins.module_utils import ModuleObject + + self.assertIn(ModuleObject, res._object.__class__.__bases__) + + def test_35_module_install(self): + smod = self.client.plugins.module_utils.m_sale + + if smod.state == 'installed': + raise unittest.SkipTest('Module already installed') + + self.assertNotEqual(smod.state, 'installed') + smod.install() + smod.refresh() # reread data from database + self.assertEqual(smod.state, 'installed') + + def test_40_module_upgrade(self): + smod = self.client.plugins.module_utils.m_sale + + self.assertEqual(smod.state, 'installed') + smod.upgrade() # just call it From e838736ee164c9019c5729039bf36e5f52b26bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Fri, 28 Aug 2015 17:54:01 +0300 Subject: [PATCH 14/23] Python 3 fixes for previous commit. --- openerp_proxy/ext/repr.py | 4 ++-- openerp_proxy/ext/sugar.py | 2 +- openerp_proxy/orm/record.py | 2 +- openerp_proxy/session.py | 2 +- openerp_proxy/tests/ext/test_workflow.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openerp_proxy/ext/repr.py b/openerp_proxy/ext/repr.py index 49a1943..0b28bcd 100644 --- a/openerp_proxy/ext/repr.py +++ b/openerp_proxy/ext/repr.py @@ -429,7 +429,7 @@ def as_html(self, *fields): if not fields: fields = sorted((HField(col_name, name=col_data['string']) - for col_name, col_data in self._columns_info.iteritems() + for col_name, col_data in self._columns_info.items() if col_name in self._object.simple_fields), key=lambda x: _(x)) self.read() @@ -528,7 +528,7 @@ def as_html_table(self, fields=None): """ fields = self.default_fields if fields is None else fields info_struct = [{'name': key, - 'info': val} for key, val in self.iteritems()] + 'info': val} for key, val in self.items()] info_struct.sort(key=lambda x: x['name']) return HTMLTable(info_struct, fields, caption=u'Fields for %s' % _(self._object.name)) diff --git a/openerp_proxy/ext/sugar.py b/openerp_proxy/ext/sugar.py index f8c61d8..6d339ee 100644 --- a/openerp_proxy/ext/sugar.py +++ b/openerp_proxy/ext/sugar.py @@ -95,7 +95,7 @@ def __call__(self, *args, **kwargs): # no arguments, only keyword arguments passsed, # so build domain based on keyword arguments if name is None: - domain = [(k, '=', v) for k, v in kwargs.iteritems()] + domain = [(k, '=', v) for k, v in kwargs.items()] return self.search_records(domain, *args) # normal domain passed, then just forward all arguments and diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index b240b0d..1f5d5bc 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -578,7 +578,7 @@ def refresh(self): rel_objects = self._related_objects self._related_objects = {} - for rel in rel_objects.itervalues(): + for rel in rel_objects.values(): if isinstance(rel, (Record, RecordList)): rel.refresh() # both, Record and RecordList objects have 'refresh* method return self diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index 6bb0e59..98177cf 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -236,7 +236,7 @@ def get_db(self, url_or_index, **kwargs): crypter, password = base64.decodestring(ep_args.pop('password').encode('utf8')).split(b':') ep_args['pwd'] = decrypt(Client.to_url(ep_args), base64.decodestring(password)) else: - ep_args['pwd'] = getpass('Password: ') + ep_args['pwd'] = getpass('Password: ') # pragma: no cover db = Client(**ep_args) self.add_db(db) diff --git a/openerp_proxy/tests/ext/test_workflow.py b/openerp_proxy/tests/ext/test_workflow.py index 8f2b287..c956b66 100644 --- a/openerp_proxy/tests/ext/test_workflow.py +++ b/openerp_proxy/tests/ext/test_workflow.py @@ -59,7 +59,7 @@ def test_record_signal_send(self): # get first avalable transition with signal trans = [t for t in act.out_transitions if t.signal] if not trans: - raise unittest.SkipError("There is no avalable transitions in first sale order to test workflow") + raise unittest.SkipTest("There is no avalable transitions in first sale order to test workflow") trans = trans[0] # send signal From 500ff8f0e588afc8f7028c562fa5e90ed2cc7af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Fri, 28 Aug 2015 18:20:43 +0300 Subject: [PATCH 15/23] Little bit more python 3 related fixes --- CHANGELOG.rst | 1 + README.rst | 30 +++++++++++++++--------------- openerp_proxy/core.py | 19 +++---------------- openerp_proxy/ext/data.py | 3 ++- openerp_proxy/ext/repr.py | 7 ++++--- openerp_proxy/ext/sugar.py | 1 - openerp_proxy/ext/workflow.py | 3 ++- openerp_proxy/main.py | 1 - setup.py | 2 +- 9 files changed, 28 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bc80aba..4355e6e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,4 +1,5 @@ master: + - Experimental Python 3 support - Added ``HField.with_args`` method. - Added basic implementation of graph plugin. - Improved ``openerp_proxy.ext.log_execute_console`` extension. Added timing. diff --git a/README.rst b/README.rst index 85e36b2..9f9b35c 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,7 @@ OpenERP internal code to perform operations on **OpenERP** / **Odoo** objects hi Features ~~~~~~~~ +- Experimental *Python 3* support - supports call to all public methods on any OpenERP/Odoo object including: *read*, *search*, *write*, *unlink* and others - Have *a lot of speed optimizations* (especialy for situation, where required processing of @@ -59,10 +60,10 @@ Features - Missed feature? ask in `Project Issues `_ -Constraints -~~~~~~~~~~~ +Requirements +~~~~~~~~~~~~ -For High level functionality Odoo server must be of version 6.1 or higher +Odoo version >= 6.1 for high level functionality Examples @@ -76,8 +77,8 @@ What You can do with this - Quickly read and analyze some data that is not visible in interface without access to DB -- Use this project as library for code that need to access OpenERP data -- Use in scripts that migrates OpenERP data (after, for example, adding +- Use this project as library for code that need to access Odoo data +- Use in scripts that migrates Odoo data (after, for example, adding new functionality or changing old). (Migration using only SQL is bad idea because of functional fields with *store=True* which must be recalculated). @@ -152,11 +153,11 @@ So here is a way to create connection :: - import openerp_proxy.core as oe_core - db = oe_core.Client(dbname='my_db', - host='my_host.int', - user='my_db_user', - pwd='my_password here') + from openerp_proxy.core import Client + db = Client(host='my_host.int', + dbname='my_db', + user='my_db_user', + pwd='my_password here') And next all there same, no more differences betwen shell and lib usage. @@ -169,8 +170,6 @@ object and *openerp_proxy.ext.repr* extension. So in first cell of notebook import session and extensions/plugins You want:: from openerp_proxy.session import IPYSession as Session # Use IPython-itegrated session class - import openerp_proxy.ext.repr # Enable representation extension. This provides HTML representation of objects - from openerp_proxy.ext.repr import HField # Used in .as_html_table method of RecordList # also You may import all standard extensions in one line: from openerp_proxy.ext.all import * @@ -186,9 +185,10 @@ To solve this, it is recommended to uses *store_passwords* option:: session.option('store_passwords', True) session.save() -In this way, only when You connect first time, You need to explicitly pass password to *connect* of *get_db* methods. +Next use it likt shell (or like lib), but *do not forget to save session, after new connection* -(*do not forget to save session, after new connection*) +*Note*: in old version of IPython getpass was not work correctly, +so maybe You will need to pass password directly to *session.connect* method. General usage @@ -348,7 +348,7 @@ So let's start ``vim attendance.py`` -3. write folowing code there (note that this example works and tested for Odoo version 6.0) +3. write folowing code there (note that this example works and tested for Odoo version 6.0 only) :: diff --git a/openerp_proxy/core.py b/openerp_proxy/core.py index 93938a4..2ec2375 100644 --- a/openerp_proxy/core.py +++ b/openerp_proxy/core.py @@ -50,28 +50,17 @@ # project imports from .connection import get_connector -from .exceptions import (Error, - ClientException, - LoginException) +from .exceptions import LoginException from .service import ServiceManager from .plugin import PluginManager - -# Activate orm internal logic -# TODO: think about not enabling it by default, allowing users to choose what -# thay woudld like to use. Or simply create two entry points (one with all -# enabled by default and another with only basic stuff which may be useful for -# libraries that would like to get speed instead of better usability +# Enable ORM features from . import orm from extend_me import Extensible -__all__ = ('ERPProxyException', 'Client', 'ERP_Proxy') - - -# Backward compatability -ERPProxyException = ClientException +__all__ = ('Client') @six.python_2_unicode_compatible @@ -364,5 +353,3 @@ def __eq__(self, other): return self.get_url() == other.get_url() else: return False - -ERP_Proxy = Client diff --git a/openerp_proxy/ext/data.py b/openerp_proxy/ext/data.py index 6520265..a5b14c7 100644 --- a/openerp_proxy/ext/data.py +++ b/openerp_proxy/ext/data.py @@ -5,6 +5,7 @@ from openerp_proxy.orm.record import RecordList, get_record_list import collections import functools +import six __all__ = ('ObjectData', 'RecordListData') @@ -46,7 +47,7 @@ def group_by(self, grouper): cache=self._cache) res = collections.defaultdict(cls_init) for record in self.records: - if isinstance(grouper, basestring): + if isinstance(grouper, six.string_types): key = record[grouper] elif callable(grouper): key = grouper(record) diff --git a/openerp_proxy/ext/repr.py b/openerp_proxy/ext/repr.py index 0b28bcd..6da3efe 100644 --- a/openerp_proxy/ext/repr.py +++ b/openerp_proxy/ext/repr.py @@ -7,6 +7,7 @@ # TODO: rename to IPython or something like that +import six import csv import tempfile @@ -211,7 +212,7 @@ def update(self, fields=None, caption=None, highlighters=None, **kwargs): for field in fields: if isinstance(field, HField): self._fields.append(field) - elif isinstance(field, basestring): + elif isinstance(field, six.string_types): self._fields.append(HField(field)) elif callable(field): self._fields.append(HField(field)) @@ -435,7 +436,7 @@ def as_html(self, *fields): self.read() else: # TODO: implement in better way this prefetching - read_fields = (f.split('.')[0] for f in fields if isinstance(f, basestring) and f.split('.')[0] in self._columns_info) + read_fields = (f.split('.')[0] for f in fields if isinstance(f, six.string_types) and f.split('.')[0] in self._columns_info) prefetch_fields = [f for f in read_fields if f not in self._data] self.read(prefetch_fields) @@ -443,7 +444,7 @@ def as_html(self, *fields): for field in fields: if isinstance(field, HField): parsed_fields.append(field) - elif isinstance(field, basestring): + elif isinstance(field, six.string_types): parsed_fields.append(HField(field)) else: raise TypeError("Bad type of field %s" % repr(field)) diff --git a/openerp_proxy/ext/sugar.py b/openerp_proxy/ext/sugar.py index 6d339ee..b4d95b4 100644 --- a/openerp_proxy/ext/sugar.py +++ b/openerp_proxy/ext/sugar.py @@ -4,7 +4,6 @@ """ import numbers -import collections from openerp_proxy.orm.record import ObjectRecords from openerp_proxy.orm.record import get_record_list diff --git a/openerp_proxy/ext/workflow.py b/openerp_proxy/ext/workflow.py index 7739bc3..aa7255e 100644 --- a/openerp_proxy/ext/workflow.py +++ b/openerp_proxy/ext/workflow.py @@ -6,6 +6,7 @@ to records from Object and Record interfaces. """ import numbers +import six from openerp_proxy.orm.record import Record from openerp_proxy.orm.record import ObjectRecords from openerp_proxy.exceptions import ObjectException @@ -43,7 +44,7 @@ def workflow_signal(self, obj_id, signal): """ Triggers specified signal for object's workflow """ assert isinstance(obj_id, numbers.Integral), "obj_id must be integer" - assert isinstance(signal, basestring), "signal must be string" + assert isinstance(signal, six.string_types), "signal must be string" return self.service.execute_wkf(self.name, signal, obj_id) diff --git a/openerp_proxy/main.py b/openerp_proxy/main.py index 641e1ec..57f1bb1 100644 --- a/openerp_proxy/main.py +++ b/openerp_proxy/main.py @@ -52,7 +52,6 @@ def main(): header = HELP_HEADER % {'databases': header_databases, 'aliases': header_aliases} _locals = { - 'ERP_Proxy': Client, 'Client': Client, 'session': session, } diff --git a/setup.py b/setup.py index d223611..02f228c 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ ], keywords=['openerp', 'odoo', 'odoo-rpc', 'rpc', 'xmlrpc', 'xml-rpc', 'json-rpc', 'jsonrpc', 'odoo-client', 'ipython'], extras_require={ - 'ipython_shell': ['ipython'], + 'all': ['ipython[all]'], }, install_requires=[ 'six', From a802dff1ad2e4cc3c956f0feef6865a32b8745fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Fri, 28 Aug 2015 19:31:56 +0300 Subject: [PATCH 16/23] Enabled testing on python 3.5 --- .travis.yml | 1 + CHANGELOG.rst | 11 +++++++---- README.rst | 2 +- openerp_proxy/plugins/module_utils.py | 8 ++++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index d3c603a..f45d443 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - "2.7" - "3.3" - "3.4" + - "3.5" env: - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='xml-rpc' diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4355e6e..63dcdd1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,11 @@ master: - - Experimental Python 3 support + - *Backward incompatible*: Changed session file format. + *Start up imports* and *extra_paths* moved to *options* section of file. + - *Backward incompatible*: Changed signature of ``Session.connect()`` method. + - *Backward incompatible*: Renamed ``ERP_Proxy`` to ``Client`` and inherited objects renamed in such way + (for example sugar extension module) + + - Experimental *Python 3.3+* support - Added ``HField.with_args`` method. - Added basic implementation of graph plugin. - Improved ``openerp_proxy.ext.log_execute_console`` extension. Added timing. @@ -7,12 +13,9 @@ master: - RecordList prefetching logic moved to cache module and highly refactored (Added support of prefetching of related fields) - Added ``Client.login(dbname, user, password)`` method. - - Changed signature of ``Session.connect()`` method. - Added ``HTMLTable.update`` method. - Added ``RecordList.copy()`` and ``RecordList.existing()`` methods. - Added ``HTMLTable.to_csv()`` method. - - Renamed ``ERP_Proxy`` to ``Client`` and inherited objects renamed in such way - (for example sugar extension module) - Added ``Client.server_version`` property - Client parametrs (dbname, user, pwd) now are not required. This is useful when working with ``db`` service (``client.services.db``) diff --git a/README.rst b/README.rst index 9f9b35c..deefa08 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ OpenERP internal code to perform operations on **OpenERP** / **Odoo** objects hi Features ~~~~~~~~ -- Experimental *Python 3* support +- Experimental *Python 3.3+* support - supports call to all public methods on any OpenERP/Odoo object including: *read*, *search*, *write*, *unlink* and others - Have *a lot of speed optimizations* (especialy for situation, where required processing of diff --git a/openerp_proxy/plugins/module_utils.py b/openerp_proxy/plugins/module_utils.py index 280feef..578ebe9 100644 --- a/openerp_proxy/plugins/module_utils.py +++ b/openerp_proxy/plugins/module_utils.py @@ -13,12 +13,16 @@ class Meta: def upgrade(self, ids): """ Immediatly upgrades module """ - return self.button_immediate_upgrade(ids) + res = self.button_immediate_upgrade(ids) + self.proxy.clean_caches() # because new models may appear in DB, so registered_objects shoud be refreshed + return res def install(self, ids): """ Immediatly install module """ - return self.button_immediate_install(ids) + res = self.button_immediate_install(ids) + self.proxy.clean_caches() # because new models may appear in DB, so registered_objects shoud be refreshed + return res class ModuleUtils(Plugin): From b1b192d8310bc053e54d6cf04d36cacd6f92f7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Tue, 1 Sep 2015 15:17:40 +0300 Subject: [PATCH 17/23] coverage: omite test modules, little refactoring openerp_proxy.ext.data extension removed. useful logic moved to core (orm.record module) --- .coveragerc | 4 + .travis.yml | 4 +- examples/Examples & HTML tests.ipynb | 146 +++++++++++--------------- openerp_proxy/ext/all.py | 1 - openerp_proxy/ext/data.py | 120 --------------------- openerp_proxy/orm/record.py | 56 +++++++++- openerp_proxy/session.py | 2 +- openerp_proxy/tests/ext/test_sugar.py | 2 +- openerp_proxy/tests/test_orm.py | 43 ++++++++ openerp_proxy/tests/test_session.py | 4 + openerp_proxy/utils.py | 15 --- run_tests.bash | 17 +-- 12 files changed, 183 insertions(+), 231 deletions(-) create mode 100644 .coveragerc delete mode 100644 openerp_proxy/ext/data.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..a3d5032 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = + openerp_proxy/tests/*.* + openerp_proxy/main.py diff --git a/.travis.yml b/.travis.yml index f45d443..afad36a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ python: - "2.7" - "3.3" - "3.4" - - "3.5" +# - "3.5" env: - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='xml-rpc' @@ -17,7 +17,7 @@ install: - "wget http://nightly.odoo.com/${ODOO_VERSION}/nightly/deb/${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb" - "sudo dpkg -i ${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb || true" - "sudo apt-get update && sudo apt-get install -f -y" - - "pip install coveralls mock simple-crypt ipython[all]" + - "pip install coveralls mock simple-crypt ipython[notebook]" - "python setup.py develop" before_script: diff --git a/examples/Examples & HTML tests.ipynb b/examples/Examples & HTML tests.ipynb index 903b5a9..9ff1349 100644 --- a/examples/Examples & HTML tests.ipynb +++ b/examples/Examples & HTML tests.ipynb @@ -24,10 +24,10 @@ { "data": { "text/html": [ - "
xml-rpc://admin@localhost:8069/demo_db_1
Hostlocalhost
Port8069
Protocolxml-rpc
Databasedemo_db_1
loginadmin
To get list of registered objects for thist database
access registered_objects property:
 .registered_objectsTo get Object instance just call get_obj method
 .get_obj(name)
where name is name of Object You want to get
or use get item syntax instead:
 [name]
" + "
xml-rpc://admin@localhost:8069/openerp_proxy_test_db
Hostlocalhost
Port8069
Protocolxml-rpc
Databaseopenerp_proxy_test_db
loginadmin
To get list of registered objects for thist database
access registered_objects property:
 .registered_objectsTo get Object instance just call get_obj method
 .get_obj(name)
where name is name of Object You want to get
or use get item syntax instead:
 [name]
" ], "text/plain": [ - "Client: xml-rpc://admin@localhost:8069/demo_db_1" + "Client: xml-rpc://admin@localhost:8069/openerp_proxy_test_db" ] }, "execution_count": 1, @@ -44,11 +44,13 @@ "# connect to local instance of server\n", "cl = Client('localhost')\n", "\n", - "# create demo database\n", - "cl.services.db.create_db('admin', 'demo_db_1', demo=True, lang='en_US')\n", + "# check if our demo database exists\n", + "if 'openerp_proxy_test_db' not in cl.services.db.list_db():\n", + " # create demo database\n", + " cl.services.db.create_db('admin', 'openerp_proxy_test_db', demo=True, lang='en_US')\n", "\n", "# login to created database\n", - "ldb = cl.login('demo_db_1', 'admin', 'admin') # all this arguments could be passed directly to Client constructor.\n", + "ldb = cl.login('openerp_proxy_test_db', 'admin', 'admin') # all this arguments could be passed directly to Client constructor.\n", "\n", "# Note that both 'cl' and 'ldb' are instances of same class\n", "# the difference is in presense of database connection args.\n", @@ -76,7 +78,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 2, "metadata": { "collapsed": false }, @@ -95,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 3, "metadata": { "collapsed": false }, @@ -106,7 +108,7 @@ "True" ] }, - "execution_count": 12, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -124,7 +126,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 4, "metadata": { "collapsed": false }, @@ -132,13 +134,13 @@ { "data": { "text/html": [ - "
Previous connections
DB URLDB IndexDB Aliases
xml-rpc://admin@localhost:8069/demo_db_11
To get connection just call
  • session.aliase
  • session[index]
  • session[aliase]
  • session[url]
  • session.get_db(url|index|aliase)
" + "
Previous connections
DB URLDB IndexDB Aliases
xml-rpc://admin@localhost:8069/demo_db_12
xml-rpc://admin@localhost:8069/openerp_proxy_test_db1ldb
To get connection just call
  • session.aliase
  • session[index]
  • session[aliase]
  • session[url]
  • session.get_db(url|index|aliase)
" ], "text/plain": [ - "" + "" ] }, - "execution_count": 13, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -157,7 +159,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 5, "metadata": { "collapsed": false }, @@ -165,13 +167,13 @@ { "data": { "text/html": [ - "
Previous connections
DB URLDB IndexDB Aliases
xml-rpc://admin@localhost:8069/demo_db_11ldb
To get connection just call
  • session.aliase
  • session[index]
  • session[aliase]
  • session[url]
  • session.get_db(url|index|aliase)
" + "
Previous connections
DB URLDB IndexDB Aliases
xml-rpc://admin@localhost:8069/demo_db_12
xml-rpc://admin@localhost:8069/openerp_proxy_test_db1ldb
To get connection just call
  • session.aliase
  • session[index]
  • session[aliase]
  • session[url]
  • session.get_db(url|index|aliase)
" ], "text/plain": [ - "" + "" ] }, - "execution_count": 14, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -192,7 +194,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 6, "metadata": { "collapsed": true }, @@ -217,7 +219,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 7, "metadata": { "collapsed": false }, @@ -225,13 +227,13 @@ { "data": { "text/html": [ - "
xml-rpc://admin@localhost:8069/demo_db_1
Hostlocalhost
Port8069
Protocolxml-rpc
Databasedemo_db_1
loginadmin
To get list of registered objects for thist database
access registered_objects property:
 .registered_objectsTo get Object instance just call get_obj method
 .get_obj(name)
where name is name of Object You want to get
or use get item syntax instead:
 [name]
" + "
xml-rpc://admin@localhost:8069/openerp_proxy_test_db
Hostlocalhost
Port8069
Protocolxml-rpc
Databaseopenerp_proxy_test_db
loginadmin
To get list of registered objects for thist database
access registered_objects property:
 .registered_objectsTo get Object instance just call get_obj method
 .get_obj(name)
where name is name of Object You want to get
or use get item syntax instead:
 [name]
" ], "text/plain": [ - "Client: xml-rpc://admin@localhost:8069/demo_db_1" + "Client: xml-rpc://admin@localhost:8069/openerp_proxy_test_db" ] }, - "execution_count": 2, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -261,7 +263,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 8, "metadata": { "collapsed": false }, @@ -269,34 +271,10 @@ { "data": { "text/plain": [ - "{'auto_refresh': 0,\n", - " 'auto_search': True,\n", - " 'context': {'disable_log': True},\n", - " 'domain': False,\n", - " 'filter': False,\n", - " 'groups_id': [],\n", - " 'help': False,\n", - " 'id': 291,\n", - " 'limit': 80,\n", - " 'multi': False,\n", - " 'name': 'Configure Accounting Data',\n", - " 'nodestroy': False,\n", - " 'res_id': 0,\n", - " 'res_model': 'account.installer',\n", - " 'search_view': '{\\'name\\': \\'default\\', \\'fields\\': {\\'date_stop\\': {\\'selectable\\': True, \\'required\\': True, \\'type\\': \\'date\\', \\'string\\': \\'End Date\\', \\'views\\': {}}}, \\'arch\\': \\'\\', \\'model\\': \\'account.installer\\', \\'type\\': \\'search\\', \\'view_id\\': 0, \\'field_parent\\': False}',\n", - " 'search_view_id': False,\n", - " 'src_model': False,\n", - " 'target': 'new',\n", - " 'type': 'ir.actions.act_window',\n", - " 'usage': False,\n", - " 'view_id': [474, 'account.installer.form'],\n", - " 'view_ids': [],\n", - " 'view_mode': 'form',\n", - " 'view_type': 'form',\n", - " 'views': [[474, 'form']]}" + "{'tag': 'reload', 'type': 'ir.actions.client'}" ] }, - "execution_count": 3, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -314,7 +292,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 9, "metadata": { "collapsed": true }, @@ -341,7 +319,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 10, "metadata": { "collapsed": false }, @@ -349,13 +327,13 @@ { "data": { "text/html": [ - "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/demo_db_1
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" + "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" ], "text/plain": [ "Object ('sale.order')" ] }, - "execution_count": 4, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -374,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 11, "metadata": { "collapsed": false }, @@ -382,13 +360,13 @@ { "data": { "text/html": [ - "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/demo_db_1
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" + "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" ], "text/plain": [ "Object ('sale.order')" ] }, - "execution_count": 5, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -409,7 +387,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 12, "metadata": { "collapsed": false }, @@ -417,13 +395,13 @@ { "data": { "text/html": [ - "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/demo_db_1
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" + "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" ], "text/plain": [ "Object ('sale.order')" ] }, - "execution_count": 6, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -442,7 +420,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 13, "metadata": { "collapsed": false, "scrolled": false @@ -471,7 +449,7 @@ " 'help': 'The tax amount.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x73761b8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x6fce1b8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Taxes',\n", " 'type': 'float'},\n", " 'amount_total': {'digits': [16, 2],\n", @@ -482,7 +460,7 @@ " 'help': 'The total amount.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x73762a8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x6fce2a8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Total',\n", " 'type': 'float'},\n", " 'amount_untaxed': {'digits': [16, 2],\n", @@ -493,7 +471,7 @@ " 'help': 'The amount without tax.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x73760c8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x6fce0c8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Untaxed Amount',\n", " 'type': 'float'},\n", " 'client_order_ref': {'selectable': True,\n", @@ -791,7 +769,7 @@ " 'type': 'many2one'}}" ] }, - "execution_count": 7, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -809,7 +787,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 14, "metadata": { "collapsed": false, "scrolled": true @@ -818,13 +796,13 @@ { "data": { "text/html": [ - "
RecordList(sale.order): length=8
ObjectObject ('sale.order')
Proxyxml-rpc://admin@localhost:8069/demo_db_1
Record count8
To get table representation of data call method
 .as_html_table
passing as arguments fields You want to see in resulting table
for better information get doc on as_html_table method:
 .as_html_table?
example of using this mehtod:
 .as_html_table('id','name','_name')
Here _name field is aliase for result of name_get methodcalled on record
" + "
RecordList(sale.order): length=8
ObjectObject ('sale.order')
Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
Record count8
To get table representation of data call method
 .as_html_table
passing as arguments fields You want to see in resulting table
for better information get doc on as_html_table method:
 .as_html_table?
example of using this mehtod:
 .as_html_table('id','name','_name')
Here _name field is aliase for result of name_get methodcalled on record
" ], "text/plain": [ "RecordList(sale.order): length=8" ] }, - "execution_count": 8, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -844,7 +822,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 15, "metadata": { "collapsed": false }, @@ -852,13 +830,13 @@ { "data": { "text/html": [ - "
RecordList(sale.order): length=8
ObjectObject ('sale.order')
Proxyxml-rpc://admin@localhost:8069/demo_db_1
Record count8
To get table representation of data call method
 .as_html_table
passing as arguments fields You want to see in resulting table
for better information get doc on as_html_table method:
 .as_html_table?
example of using this mehtod:
 .as_html_table('id','name','_name')
Here _name field is aliase for result of name_get methodcalled on record
" + "
RecordList(sale.order): length=8
ObjectObject ('sale.order')
Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
Record count8
To get table representation of data call method
 .as_html_table
passing as arguments fields You want to see in resulting table
for better information get doc on as_html_table method:
 .as_html_table?
example of using this mehtod:
 .as_html_table('id','name','_name')
Here _name field is aliase for result of name_get methodcalled on record
" ], "text/plain": [ "RecordList(sale.order): length=8" ] }, - "execution_count": 9, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -885,7 +863,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 16, "metadata": { "collapsed": false }, @@ -893,13 +871,13 @@ { "data": { "text/html": [ - "
Note, that You may use .to_csv() method of this table to export it to CSV format
RecordList(sale.order): length=8
idnamePartner namePartner emailorder_line.as_html_listRelated Invoicesstate
8SO008Millennium IndustriesFalse
  • 20: Laptop Customized
  • 21: Mouse, Wireless
    draft
    7SO007Luminous TechnologiesFalse
    • 16: Laptop E5023
    • 17: GrapWorks Software
    • 18: Datacard
    • 19: USB Adapter
      manual
      6SO006Think Big Systemsinfo@thinkbig.com
      • 15: PC Assamble + 2GB RAM
        draft
        5SO005Agrolaitinfo@agrolait.com
        • 12: External Hard disk
        • 13: Blank DVD-RW
        • 14: Printer, All-in-one
          draft
          4SO004Millennium IndustriesFalse
          • 8: Service on demand
          • 9: Webcam
          • 10: Multimedia Speakers
          • 11: Switch, 24 ports
            draft
            3SO003Chamber Worksinfo@chamberworks.com
            • 6: On Site Monitoring
            • 7: Toner Cartridge
              draft
              2SO002Bank Wealthy and sonsemail@wealthyandsons.com
              • 4: Service on demand
              • 5: On Site Assistance
                draft
                1SO001Agrolaitinfo@agrolait.com
                • 1: Laptop E5023
                • 2: Pen drive, 16GB
                • 3: Headset USB
                  draft
                  " + "
                  Note, that You may use .to_csv() method of this table to export it to CSV format
                  RecordList(sale.order): length=8
                  idnamePartner namePartner emailorder_line.as_html_listRelated Invoicesstate
                  8SO008Millennium IndustriesFalse
                  • 20: Laptop Customized
                  • 21: Mouse, Wireless
                    draft
                    7SO007Luminous TechnologiesFalse
                    • 16: Laptop E5023
                    • 17: GrapWorks Software
                    • 18: Datacard
                    • 19: USB Adapter
                      manual
                      6SO006Think Big Systemsinfo@thinkbig.com
                      • 15: PC Assamble + 2GB RAM
                        draft
                        5SO005Agrolaitinfo@agrolait.com
                        • 12: External Hard disk
                        • 13: Blank DVD-RW
                        • 14: Printer, All-in-one
                          draft
                          4SO004Millennium IndustriesFalse
                          • 8: Service on demand
                          • 9: Webcam
                          • 10: Multimedia Speakers
                          • 11: Switch, 24 ports
                            draft
                            3SO003Chamber Worksinfo@chamberworks.com
                            • 6: On Site Monitoring
                            • 7: Toner Cartridge
                              draft
                              2SO002Bank Wealthy and sonsemail@wealthyandsons.com
                              • 4: Service on demand
                              • 5: On Site Assistance
                                draft
                                1SO001Agrolaitinfo@agrolait.com
                                • 1: Laptop E5023
                                • 2: Pen drive, 16GB
                                • 3: Headset USB
                                  done
                                  " ], "text/plain": [ "" ] }, - "execution_count": 10, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -944,7 +922,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 17, "metadata": { "collapsed": false }, @@ -952,13 +930,13 @@ { "data": { "text/html": [ - "./tmp/csv/tmpFI0iVI.csv
                                  " + "./tmp/csv/tmpl_ErrO.csv
                                  " ], "text/plain": [ - "/home/katyukha/projects/erp-proxy/examples/tmp/csv/tmpFI0iVI.csv" + "/home/katyukha/projects/erp-proxy/examples/tmp/csv/tmpl_ErrO.csv" ] }, - "execution_count": 11, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -976,7 +954,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 18, "metadata": { "collapsed": false }, @@ -984,13 +962,13 @@ { "data": { "text/html": [ - "
                                  R(sale.order, 8)[SO008]
                                  ObjectObject ('sale.order')
                                  Proxyxml-rpc://admin@localhost:8069/demo_db_1
                                  NameSO008
                                  To get HTML Table representation of this record call method:
                                   .as_html()
                                  Optionaly You can pass list of fields You want to see:
                                   .as_html('name', 'origin')
                                  for better information get doc on as_html method:
                                   .as_html?
                                  " + "
                                  R(sale.order, 8)[SO008]
                                  ObjectObject ('sale.order')
                                  Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
                                  NameSO008
                                  To get HTML Table representation of this record call method:
                                   .as_html()
                                  Optionaly You can pass list of fields You want to see:
                                   .as_html('name', 'origin')
                                  for better information get doc on as_html method:
                                   .as_html?
                                  " ], "text/plain": [ "R(sale.order, 8)[SO008]" ] }, - "execution_count": 12, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -1008,7 +986,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 19, "metadata": { "collapsed": false }, @@ -1022,7 +1000,7 @@ "" ] }, - "execution_count": 13, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -1037,7 +1015,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 20, "metadata": { "collapsed": false }, @@ -1045,13 +1023,13 @@ { "data": { "text/html": [ - "
                                  Record SO008
                                  ColumnValue
                                  Confirmation DateFalse
                                  Contract / AnalyticFalse
                                  Create Invoicemanual
                                  Creation Date2015-07-14 12:40:59
                                  CustomerR(res.partner, 19)[Millennium Industries]
                                  Customer ReferenceFalse
                                  Date2015-07-14
                                  Delivery AddressR(res.partner, 52)[Millennium Industries, Jacob Taylor]
                                  Fiscal PositionFalse
                                  Invoice AddressR(res.partner, 52)[Millennium Industries, Jacob Taylor]
                                  Invoice onorder
                                  InvoicesRecordList(account.invoice): length=0
                                  MessagesRecordList(mail.message): length=2
                                  Order LinesRecordList(sale.order.line): length=2
                                  Order ReferenceSO008
                                  Payment TermFalse
                                  PricelistR(product.pricelist, 1)[Public Pricelist (EUR)]
                                  SalespersonR(res.users, 3)[Demo User]
                                  ShopR(sale.shop, 1)[Your Company]
                                  Source DocumentFalse
                                  Statusdraft
                                  Terms and conditionsFalse
                                  " + "
                                  Record SO008
                                  ColumnValue
                                  Confirmation DateFalse
                                  Contract / AnalyticFalse
                                  Create Invoicemanual
                                  Creation Date2015-08-28 13:14:32
                                  CustomerR(res.partner, 19)[Millennium Industries]
                                  Customer ReferenceFalse
                                  Date2015-08-28
                                  Delivery AddressR(res.partner, 52)[Millennium Industries, Jacob Taylor]
                                  Fiscal PositionFalse
                                  Invoice AddressR(res.partner, 52)[Millennium Industries, Jacob Taylor]
                                  Invoice onorder
                                  InvoicesRecordList(account.invoice): length=0
                                  MessagesRecordList(mail.message): length=2
                                  Order LinesRecordList(sale.order.line): length=2
                                  Order ReferenceSO008
                                  Payment TermFalse
                                  PricelistR(product.pricelist, 1)[Public Pricelist (EUR)]
                                  SalespersonR(res.users, 3)[Demo User]
                                  ShopR(sale.shop, 1)[Your Company]
                                  Source DocumentFalse
                                  Statusdraft
                                  Terms and conditionsFalse
                                  " ], "text/plain": [ "" ] }, - "execution_count": 14, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } diff --git a/openerp_proxy/ext/all.py b/openerp_proxy/ext/all.py index e69d3ea..6285adc 100644 --- a/openerp_proxy/ext/all.py +++ b/openerp_proxy/ext/all.py @@ -1,7 +1,6 @@ """ Just imports of all extensions """ -import openerp_proxy.ext.data import openerp_proxy.ext.field_datetime import openerp_proxy.ext.sugar import openerp_proxy.ext.workflow diff --git a/openerp_proxy/ext/data.py b/openerp_proxy/ext/data.py deleted file mode 100644 index a5b14c7..0000000 --- a/openerp_proxy/ext/data.py +++ /dev/null @@ -1,120 +0,0 @@ -""" This module provides extension which allows aditional -data manipulations, especialy filtering and grouping capabilities. -""" -from openerp_proxy.orm.record import ObjectRecords -from openerp_proxy.orm.record import RecordList, get_record_list -import collections -import functools -import six - - -__all__ = ('ObjectData', 'RecordListData') - - -class RecordListData(RecordList): - """ Extend record list to add aditional method to work with lists of records - """ - - def group_by(self, grouper): - """ Groups all records in list by specifed grouper. - - :param grouper: field name or callable to group results by. - if function is passed, it should receive only - one argument - record instance, and result of - calling grouper will be used to group records. - :type grouper: string|callable(record) - - for example we have list of sale orders and want to group it by state:: - - # so_list - variable that contains list of sale orders selected - # by some criterias. so to group it by state we will do: - group = so_list.group_by('state') - for state, rlist in group.iteritems(): # Iterate over resulting dictionary - print state, rlist.length # Print state and amount of items with such state - - or imagine that we would like to groupe records by last letter of sale order number:: - - # so_list - variable that contains list of sale orders selected - # by some criterias. so to group it by last letter of sale - # order name we will do: - group = so_list.group_by(lambda so: so.name[-1]) - for letter, rlist in group.iteritems(): # Iterate over resulting dictionary - print letter, rlist.length # Print state and amount of items with such state - """ - cls_init = functools.partial(get_record_list, - self.object, - ids=[], - cache=self._cache) - res = collections.defaultdict(cls_init) - for record in self.records: - if isinstance(grouper, six.string_types): - key = record[grouper] - elif callable(grouper): - key = grouper(record) - - res[key].append(record) - return res - - def filter(self, func): - """ Filters items using *func*. - - :param func: callable to check if record should be included in result. - also *openerp_proxy.utils.r_eval* may be used - :type func: callable(record)->bool - :return: RecordList which contains records that matches results - :rtype: RecordList - """ - result_ids = [record.id for record in self.records if func(record)] - return get_record_list(self.object, ids=result_ids, cache=self._cache) - - -# TODO: implement some class wrapper to by default load only count of domains, -# and by some method load ids, or records if required. this will allow to -# work better with data when accessing root object showing all groups and -# amounts of objects within, but when accessing some object we could get -# records related to that group to analyse them. -class ObjectData(ObjectRecords): - """ Provides aditional methods to work with data - """ - - def data__get_grouped(self, group_rules, count=False): - """ Returns dictionary with grouped data. if count=True returns only amount of items found for rule - otherwise returns list of records found for each rule - - :param group_rules: dictionary with keys=group_names and values are domains or other dictionary - with domains. - For example - - :: - - group_rules = {'g1': [('state','=','done')], - 'g2': { - '__sub_domain': [('partner_id','=',5)], - 'total': [], - 'done': [('state', '=', 'done')], - 'cancel': [('state', '=', 'cancel')] - }} - - Each group may contain '__sub_domain' field with domain applied to all - items of group - :type group_rules: dict - :param count: if True then result dictinary will contain only counts - otherwise each group in result dictionary will contain RecordList of records found - :type count: boolean (default: False) - :return: dictionary like 'group_rules' but with domains replaced by search result (RecordList instance). - """ - result = {} - sub_domain = group_rules.pop('__sub_domain', []) - for key, value in group_rules.iteritems(): - if isinstance(value, collections.Iterable): # If value is domain - domain = sub_domain + value - result[key] = self.search_records(domain, count=count) - elif isinstance(value, dict): # if value is subgroup of domains - _sub_domain = sub_domain + value.get('__sub_domain', []) - if _sub_domain: - value['__sub_domain'] = _sub_domain - result[key] = self.data__get_grouped(value, count=count) - else: - raise TypeError("Unsupported type for 'group_rules' value for key %s: %s" % (key, type(value))) - return result - diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index 1f5d5bc..0b444d2 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -5,9 +5,10 @@ from extend_me import ExtensibleType import six -import collections import abc import numbers +import functools +import collections __all__ = ( @@ -422,6 +423,59 @@ def sort(self, *args, **kwargs): self._records.sort(*args, **kwargs) return self + def group_by(self, grouper): + """ Groups all records in list by specifed grouper. + + :param grouper: field name or callable to group results by. + if callable is passed, it should receive only + one argument - record instance, and result of + calling grouper will be used as key to group records by. + :type grouper: string|callable(record) + + for example we have list of sale orders and want to group it by state:: + + # so_list - variable that contains list of sale orders selected + # by some criterias. so to group it by state we will do: + group = so_list.group_by('state') + for state, rlist in group.iteritems(): # Iterate over resulting dictionary + print state, rlist.length # Print state and amount of items with such state + + or imagine that we would like to groupe records by last letter of sale order number:: + + # so_list - variable that contains list of sale orders selected + # by some criterias. so to group it by last letter of sale + # order name we will do: + group = so_list.group_by(lambda so: so.name[-1]) + for letter, rlist in group.iteritems(): # Iterate over resulting dictionary + print letter, rlist.length # Print state and amount of items with such state + """ + cls_init = functools.partial(get_record_list, + self.object, + ids=[], + cache=self._cache) + res = collections.defaultdict(cls_init) + for record in self.records: + if isinstance(grouper, six.string_types): + key = record[grouper] + elif callable(grouper): + key = grouper(record) + + res[key].append(record) + return res + + def filter(self, func): + """ Filters items using *func*. + + :param func: callable to check if record should be included in result. + also *openerp_proxy.utils.r_eval* may be used + :type func: callable(record)->bool + :return: RecordList which contains records that matches results + :rtype: RecordList + """ + return get_record_list(self.object, + ids=[r.id for r in self.records if func(r)], + cache=self._cache) + def copy(self, context=None, new_cache=False): """ Returns copy of this list, possibly with modified context and new empty cache. diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index 98177cf..bf10272 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -57,7 +57,7 @@ def __init__(self, data_file='~/.openerp_proxy.json'): for path in self.extra_paths: self.add_path(path) - for module in self.start_up_imports: + for module in self.start_up_imports: # pragma: no cover try: __import__(module) except ImportError: diff --git a/openerp_proxy/tests/ext/test_sugar.py b/openerp_proxy/tests/ext/test_sugar.py index 3ae9e14..ba6f988 100644 --- a/openerp_proxy/tests/ext/test_sugar.py +++ b/openerp_proxy/tests/ext/test_sugar.py @@ -13,7 +13,7 @@ from openerp_proxy.orm.object import Object -@unittest.skipUnless(os.environ.get('TEST_WITH_EXTENSIONS', False), 'requires tests enabled') +@unittest.skipUnless(os.environ.get('TEST_WITH_EXTENSIONS', False), 'requires extensions enabled') class Test_31_ExtSugar(BaseTestCase): def setUp(self): super(Test_31_ExtSugar, self).setUp() diff --git a/openerp_proxy/tests/test_orm.py b/openerp_proxy/tests/test_orm.py index 7050c43..3b90dd0 100644 --- a/openerp_proxy/tests/test_orm.py +++ b/openerp_proxy/tests/test_orm.py @@ -243,6 +243,35 @@ def test_record_relational_fields(self): self.assertIsInstance(self.record.user_ids, RecordList) self.assertEqual(self.record.user_ids.length, 0) + def test_record_refresh(self): + # read all data for record + self.record.read() + + # read company_id field + self.record.company_id.name + + # check that data had been loaded + self.assertTrue(len(self.record._data.keys()) > 5) + + # test before refresh + self.assertEqual(len(self.record._cache.keys()), 2) + self.assertIn('res.partner', self.record._cache) + self.assertIn('res.company', self.record._cache) + self.assertIn(len(list(self.record._cache['res.company'].values())[0]), [2, 3]) + self.assertIn('name', list(self.record._cache['res.company'].values())[0]) + + # refresh record + self.record.refresh() + + # test after refresh + self.assertEqual(len(self.record._data.keys()), 1) + self.assertItemsEqual(list(self.record._data), ['id']) + self.assertEqual(len(self.record._cache.keys()), 2) + self.assertIn('res.partner', self.record._cache) + self.assertIn('res.company', self.record._cache) + self.assertEqual(len(list(self.record._cache['res.company'].values())[0]), 1) + self.assertNotIn('name', list(self.record._cache['res.company'].values())[0]) + class Test_22_RecordList(BaseTestCase): @@ -492,3 +521,17 @@ def test_read(self): with mock.patch.object(self.object, 'read') as fake_method: self.recordlist.read(['name']) fake_method.assert_called_with(self.recordlist.ids, ['name']) + + def test_filter(self): + res = self.recordlist.filter(lambda x: x.id % 2 == 0) + expected_ids = [r.id for r in self.recordlist if r.id % 2 == 0] + self.assertIsInstance(res, RecordList) + self.assertEqual(res.ids, expected_ids) + + def test_group_by(self): + res = self.recordlist.group_by(lambda x: x.id % 2 == 0) + self.assertIsInstance(res, collections.defaultdict) + self.assertItemsEqual(res.keys(), [True, False]) + # TODO: write better test + + res = self.recordlist.group_by('country_id') diff --git a/openerp_proxy/tests/test_session.py b/openerp_proxy/tests/test_session.py index 2b1983a..5b26dcf 100644 --- a/openerp_proxy/tests/test_session.py +++ b/openerp_proxy/tests/test_session.py @@ -177,6 +177,9 @@ def test_25_aliases(self): res = session.aliase('cl3', cl.get_url()) # use url self.assertEqual(res, cl.get_url()) + with self.assertRaises(ValueError): + session.aliase('cl4', 'bad url') + self.assertIn('cl1', session.aliases) self.assertIs(session.get_db('cl1'), cl) self.assertIs(session['cl1'], cl) @@ -194,6 +197,7 @@ def test_25_aliases(self): session = Session(self._session_file_path) # and test again + self.assertTrue(bool(session.index)) self.assertEqual(len(session.aliases), 3) self.assertIn('cl1', session.aliases) self.assertEqual(session.get_db('cl1'), cl) diff --git a/openerp_proxy/utils.py b/openerp_proxy/utils.py index 529a38b..41e15c3 100644 --- a/openerp_proxy/utils.py +++ b/openerp_proxy/utils.py @@ -12,19 +12,6 @@ xinput = input -def r_eval(code): - """ Helper function to be used in filters or so - At this moment this function mostly suitable for extensions like - 'openerp_proxy.ext.data' or 'openerp_proxy.ext.repr' - """ - def r_eval_internal(record): - return eval(code, { - 'r': record, - 'rec': record, - 'record': record}) - return r_eval_internal - - def json_read(file_path): """ Read specified json file """ @@ -131,8 +118,6 @@ def ustr(value, hint_encoding='utf-8', errors='strict'): class AttrDict(dict): - # TODO: think about reimplementing it via self.__dict__ = self - # (http://stackoverflow.com/questions/4984647/accessing-dict-keys-like-an-attribute-in-python) """ Simple class to make dictionary able to use attribute get operation to get elements it contains using syntax like: diff --git a/run_tests.bash b/run_tests.bash index 4378fc2..2d60a3b 100755 --- a/run_tests.bash +++ b/run_tests.bash @@ -4,22 +4,27 @@ SCRIPT=`readlink -f "$0"` # Absolute path this script is in, thus /home/user/bin SCRIPTPATH=`dirname "$SCRIPT"` +TEST_MODULE=${TEST_MODULE:-'openerp_proxy.tests.all'}; +PY_VERSIONS=${PY_VERSIONS:-"2.7 3.4"}; + function test_it { local py_version=$1; (cd $SCRIPTPATH && \ virtualenv venv_test -p python${py_version} && \ source ./venv_test/bin/activate && \ - pip install --upgrade pip setuptools coverage mock pudb ipython simple-crypt && \ + pip install --upgrade pip setuptools coverage mock pudb ipython[notebook] simple-crypt && \ python setup.py develop && \ rm -f .coverage && \ - coverage run --source openerp_proxy -m unittest -v openerp_proxy.tests.all && \ + coverage run --source openerp_proxy -m unittest -v $TEST_MODULE && \ coverage html -d coverage && \ deactivate && \ rm -rf venv_test) } -PY_VERSIONS=${PY_VERSIONS:-"2.7 3.4"}; +function main { + for version in $PY_VERSIONS; do + test_it $version; + done +} -for version in $PY_VERSIONS; do - test_it $version; -done +main; From 265e35e6fdba344cdf15469be5a0a91f23aa1f7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Tue, 1 Sep 2015 18:11:08 +0300 Subject: [PATCH 18/23] IPYSession moved to extension + Readme fixes --- CHANGELOG.rst | 4 +++ README.rst | 22 +++++++------- examples/Examples & HTML tests.ipynb | 26 +++++++--------- openerp_proxy/core.py | 10 +++---- openerp_proxy/ext/repr.py | 35 ++++++++++++++++++++++ openerp_proxy/orm/object.py | 2 +- openerp_proxy/orm/service.py | 2 +- openerp_proxy/session.py | 44 ++-------------------------- openerp_proxy/utils.py | 2 +- 9 files changed, 72 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 63dcdd1..de32d66 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,9 +1,13 @@ master: - *Backward incompatible*: Changed session file format. *Start up imports* and *extra_paths* moved to *options* section of file. + - *Backward incompatible*: ``IPYSession`` moved to ``openerp_proxy.ext.repr`` extensions. + Now when using IPython notebook, this extension have to be imported first, + to enable HTML representation of session object - *Backward incompatible*: Changed signature of ``Session.connect()`` method. - *Backward incompatible*: Renamed ``ERP_Proxy`` to ``Client`` and inherited objects renamed in such way (for example sugar extension module) + - *Backward incompatible*: removed ``ERP_Proxy` and ``ERP_Session`` compatability aliases - Experimental *Python 3.3+* support - Added ``HField.with_args`` method. diff --git a/README.rst b/README.rst index deefa08..5f0e612 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Overview This project is just **RPC client** for Odoo. It aims to ease access to openerp data via shell and used mostly for data debuging purposes. This project provides interface similar to -OpenERP internal code to perform operations on **OpenERP** / **Odoo** objects hiding +Odoo internal code to perform operations on **OpenERP** / **Odoo** objects hiding **XML-RPC** or **JSON-RPC** behind. @@ -29,7 +29,7 @@ Features ~~~~~~~~ - Experimental *Python 3.3+* support -- supports call to all public methods on any OpenERP/Odoo object including: +- supports call to all public methods on any OpenERP / Odoo object including: *read*, *search*, *write*, *unlink* and others - Have *a lot of speed optimizations* (especialy for situation, where required processing of large datasets) @@ -53,7 +53,7 @@ Features - *Plugin Support*. Plugins here meant utils, which could store some aditional logic, to simplify routine operations. Accessible from ``db.plugins.`` attribute. -- Support of **JSON-RPC** for *version 8* of OpenERP/Odoo (***experimental***) +- Support of **JSON-RPC** for *version 8* of Odoo (***experimental***) - Support of using **named parametrs** in RPC method calls (server version 6.1 and higher). - *Sugar extension* which simplifys code a lot. @@ -108,7 +108,7 @@ If You want to install development version of *OpenERP Proxy* you can do it via: Also if You plan to use this project as shell client, it is **recommended to install IPython** -and If You would like to have ability to play with Odoo / OpenERP data in IPython notebook, +and If You would like to have ability to play with Odoo data in IPython notebook, it is recommended to also install IPython's Notebook support. To install IPython and IPython Notebook just type:: @@ -128,14 +128,14 @@ And You will get the openerp_proxy shell. If *IPython* is installed then IPython will be used, else usual python shell will be used. There is in context exists *session* variable that represents current session to work with -Next You have to get connection to some OpenERP/Odoo database. +Next You have to get connection to some Odoo database. :: >>> db = session.connect() This will ask You for host, port, database, etc to connect to. Now You -have connection to OpenERP database which allows You to use database +have connection to Odoo database which allows You to use database objects. @@ -169,11 +169,13 @@ To better suit for HTML capable notebook You would like to use IPython's version object and *openerp_proxy.ext.repr* extension. So in first cell of notebook import session and extensions/plugins You want:: - from openerp_proxy.session import IPYSession as Session # Use IPython-itegrated session class - # also You may import all standard extensions in one line: from openerp_proxy.ext.all import * + # note that extensions were imported before session, + # because some of them modify Session class + from openerp_proxy.session import Session + session = Session() Now most things same as for shell usage, but... @@ -207,9 +209,9 @@ database: So we have 5 orders in done state. So let's read them. -Default way to read data from OpenERP is to search for required records +Default way to read data from Odoo is to search for required records with *search* method which return's list of IDs of records, then read -data using *read* method. Both methods mostly same as OpenERP internal +data using *read* method. Both methods mostly same as Odoo internal ones: :: diff --git a/examples/Examples & HTML tests.ipynb b/examples/Examples & HTML tests.ipynb index 9ff1349..5fb5349 100644 --- a/examples/Examples & HTML tests.ipynb +++ b/examples/Examples & HTML tests.ipynb @@ -36,11 +36,13 @@ } ], "source": [ - "from openerp_proxy.session import IPYSession as Session\n", + "from openerp_proxy.ext.all import HField # import extensions first (they modify Session and Client classes)\n", + "\n", "from openerp_proxy.core import Client\n", - "from openerp_proxy.ext.all import HField\n", "import openerp_proxy.plugins.module_utils # Enable module_utils plugin\n", "\n", + "from openerp_proxy.session import Session\n", + "\n", "# connect to local instance of server\n", "cl = Client('localhost')\n", "\n", @@ -133,11 +135,8 @@ "outputs": [ { "data": { - "text/html": [ - "
                                  Previous connections
                                  DB URLDB IndexDB Aliases
                                  xml-rpc://admin@localhost:8069/demo_db_12
                                  xml-rpc://admin@localhost:8069/openerp_proxy_test_db1ldb
                                  To get connection just call
                                  • session.aliase
                                  • session[index]
                                  • session[aliase]
                                  • session[url]
                                  • session.get_db(url|index|aliase)
                                  " - ], "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -166,11 +165,8 @@ "outputs": [ { "data": { - "text/html": [ - "
                                  Previous connections
                                  DB URLDB IndexDB Aliases
                                  xml-rpc://admin@localhost:8069/demo_db_12
                                  xml-rpc://admin@localhost:8069/openerp_proxy_test_db1ldb
                                  To get connection just call
                                  • session.aliase
                                  • session[index]
                                  • session[aliase]
                                  • session[url]
                                  • session.get_db(url|index|aliase)
                                  " - ], "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -449,7 +445,7 @@ " 'help': 'The tax amount.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x6fce1b8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x5a3a0c8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Taxes',\n", " 'type': 'float'},\n", " 'amount_total': {'digits': [16, 2],\n", @@ -460,7 +456,7 @@ " 'help': 'The total amount.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x6fce2a8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x5a3a1b8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Total',\n", " 'type': 'float'},\n", " 'amount_untaxed': {'digits': [16, 2],\n", @@ -471,7 +467,7 @@ " 'help': 'The amount without tax.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x6fce0c8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x5a29f50>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Untaxed Amount',\n", " 'type': 'float'},\n", " 'client_order_ref': {'selectable': True,\n", @@ -930,10 +926,10 @@ { "data": { "text/html": [ - "./tmp/csv/tmpl_ErrO.csv
                                  " + "./tmp/csv/tmpil4cjF.csv
                                  " ], "text/plain": [ - "/home/katyukha/projects/erp-proxy/examples/tmp/csv/tmpl_ErrO.csv" + "/home/katyukha/projects/erp-proxy/examples/tmp/csv/tmpil4cjF.csv" ] }, "execution_count": 17, diff --git a/openerp_proxy/core.py b/openerp_proxy/core.py index 2ec2375..34a8507 100644 --- a/openerp_proxy/core.py +++ b/openerp_proxy/core.py @@ -1,5 +1,5 @@ # -*- coding: utf8 -*- -""" This module provides some classes to simplify acces to OpenERP server via xmlrpc. +""" This module provides some classes to simplify acces to Odoo server via xmlrpc. Some of these classes are may be not safe enough and should be used with carefully Example ussage of this module: @@ -66,7 +66,7 @@ @six.python_2_unicode_compatible class Client(Extensible): """ - A simple class to connect ot ERP via RPC (XML-RPC, JSON-RPC) + A simple class to connect to Odoo instance via RPC (XML-RPC, JSON-RPC) Should be initialized with following arguments: :param str host: server host name to connect to @@ -84,7 +84,7 @@ class Client(Extensible): >>> cl = Client('host') >>> db2 = cl.login('dbname', 'user', 'password') - Allows access to ERP objects via dictionary syntax:: + Allows access to Odoo objects / models via dictionary syntax:: >>> db['sale.order'] Object ('sale.order') @@ -189,7 +189,7 @@ def server_version(self): @property def registered_objects(self): - """ Stores list of registered in ERP database objects + """ Stores list of registered in Odoo database objects """ return self.services['object'].get_registered_objects() @@ -280,7 +280,7 @@ def execute_wkf(self, object_name, signal, object_id): return result_wkf def get_obj(self, object_name): - """ Returns wraper around openERP object 'object_name' which is instance of Object + """ Returns wraper around Odoo object 'object_name' which is instance of Object :param object_name: name of an object to get wraper for :return: instance of Object which wraps choosen object diff --git a/openerp_proxy/ext/repr.py b/openerp_proxy/ext/repr.py index 6da3efe..1aaa631 100644 --- a/openerp_proxy/ext/repr.py +++ b/openerp_proxy/ext/repr.py @@ -16,6 +16,7 @@ from openerp_proxy.orm.object import Object from openerp_proxy.core import Client from openerp_proxy.utils import AttrDict +from openerp_proxy.session import Session from IPython.display import HTML, FileLink @@ -621,3 +622,37 @@ def to_row(header, val): table = ttable % (caption + data) return html % (table + help_text) + + +class IPYSession(Session): + def _repr_html_(self): + """ Provides HTML representation of session (Used for IPython) + """ + from openerp_proxy.utils import ustr as _ + + def _get_data(): + for url in self._databases.keys(): + index = self._index_url(url) + aliases = (_(al) for al, aurl in self.aliases.items() if aurl == url) + yield (url, index, u", ".join(aliases)) + ttable = u"%s
                                  " + trow = u"%s" + tdata = u"%s" + caption = u"Previous connections" + hrow = u"DB URLDB IndexDB Aliases" + help_text = (u"
                                  " + u"To get connection just call
                                    " + u"
                                  • session.aliase
                                  • " + u"
                                  • session[index]
                                  • " + u"
                                  • session[aliase]
                                  • " + u"
                                  • session[url]
                                  • " + u"
                                  • session.get_db(url|index|aliase)
                                  • " + u"
                                  ") + + data = u"" + for row in _get_data(): + data += trow % (u''.join((tdata % i for i in row))) + + table = ttable % (caption + hrow + data) + + return u"
                                  %s %s
                                  " % (table, help_text) diff --git a/openerp_proxy/orm/object.py b/openerp_proxy/orm/object.py index 19d61d9..2056b28 100644 --- a/openerp_proxy/orm/object.py +++ b/openerp_proxy/orm/object.py @@ -69,7 +69,7 @@ def __dir__(self): def __getattr__(self, name): def method_wrapper(object_name, method_name): - """ Wraper around ERP objects's methods. + """ Wraper around Odoo objects's methods. for internal use. It is used in Object class. diff --git a/openerp_proxy/orm/service.py b/openerp_proxy/orm/service.py index 185bfa4..b6b1e7d 100644 --- a/openerp_proxy/orm/service.py +++ b/openerp_proxy/orm/service.py @@ -13,7 +13,7 @@ def __init__(self, *args, **kwargs): self.__objects = {} # cached objects def get_obj(self, object_name): - """ Returns wraper around OpenERP object 'object_name' which is instance of Object + """ Returns wraper around Odoo object 'object_name' which is instance of Object :param object_name: name of an object to get wraper for :type object_name: string diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index bf10272..364c053 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -11,10 +11,9 @@ xinput) -__all__ = ('ERP_Session', 'Session', 'IPYSession') +__all__ = ('Session',) -# TODO: completly refactor class Session(object): """ Simple session manager which allows to manage databases easier @@ -198,7 +197,7 @@ def add_db(self, db): def get_db(self, url_or_index, **kwargs): """ Returns instance of Client object, that represents single - OpenERP database it connected to, specified by passed index (integer) or + Odoo database it connected to, specified by passed index (integer) or url (string) of database, previously saved in session. :param url_or_index: must be integer (if index) or string (if url). this parametr @@ -343,42 +342,3 @@ def __dir__(self): res = dir(super(Session, self)) res += self.aliases.keys() return res - - -# For Backward compatability -ERP_Session = Session - - -# TODO: move to repr / ipython extension -class IPYSession(Session): - def _repr_html_(self): - """ Provides HTML representation of session (Used for IPython) - """ - from openerp_proxy.utils import ustr as _ - - def _get_data(): - for url in self._databases.keys(): - index = self._index_url(url) - aliases = (_(al) for al, aurl in self.aliases.items() if aurl == url) - yield (url, index, u", ".join(aliases)) - ttable = u"%s
                                  " - trow = u"%s" - tdata = u"%s" - caption = u"Previous connections" - hrow = u"DB URLDB IndexDB Aliases" - help_text = (u"
                                  " - u"To get connection just call
                                    " - u"
                                  • session.aliase
                                  • " - u"
                                  • session[index]
                                  • " - u"
                                  • session[aliase]
                                  • " - u"
                                  • session[url]
                                  • " - u"
                                  • session.get_db(url|index|aliase)
                                  • " - u"
                                  ") - - data = u"" - for row in _get_data(): - data += trow % (u''.join((tdata % i for i in row))) - - table = ttable % (caption + hrow + data) - - return u"
                                  %s %s
                                  " % (table, help_text) diff --git a/openerp_proxy/utils.py b/openerp_proxy/utils.py index 41e15c3..6e4b033 100644 --- a/openerp_proxy/utils.py +++ b/openerp_proxy/utils.py @@ -43,7 +43,7 @@ def wpartial(func, *args, **kwargs): return functools.wraps(func)(partial) -# Copied from OpenERP source ustr function +# Copied from Odoo source ustr function def get_encodings(hint_encoding='utf-8'): fallbacks = { 'latin1': 'latin9', From 474aeeaf859a4e16b02ba4a497841e2b4cd6e66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Mon, 7 Sep 2015 10:13:18 +0300 Subject: [PATCH 19/23] The way password is saved in session changed. --- CHANGELOG.rst | 3 +++ openerp_proxy/session.py | 12 +++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index de32d66..e146ff0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,9 @@ master: (for example sugar extension module) - *Backward incompatible*: removed ``ERP_Proxy` and ``ERP_Session`` compatability aliases + - Changed ``store_passwords`` option meaning. now if set it will store passwords bese64 encoded, + instead of using simple-crypt module. This change makes it faster to decode password, + because last-versions of simple-crypt become too slow. and usualy no encryption needed here. - Experimental *Python 3.3+* support - Added ``HField.with_args`` method. - Added basic implementation of graph plugin. diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index 364c053..b8327e0 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -230,10 +230,13 @@ def get_db(self, url_or_index, **kwargs): if 'pwd' not in ep_args: if self.option('store_passwords') and 'password' in ep_args: - from simplecrypt import decrypt import base64 - crypter, password = base64.decodestring(ep_args.pop('password').encode('utf8')).split(b':') - ep_args['pwd'] = decrypt(Client.to_url(ep_args), base64.decodestring(password)) + crypter, password = base64.b64decode(ep_args.pop('password').encode('utf8')).split(b':') + if crypter == 'simplecrypt': + import simplecrypt + ep_args['pwd'] = simplecrypt.decrypt(Client.to_url(ep_args), base64.b64decode(password)) + elif crypter == 'plain': + ep_args['pwd'] = password.decode('utf-8') else: ep_args['pwd'] = getpass('Password: ') # pragma: no cover @@ -290,9 +293,8 @@ def _get_db_init_args(self, database): if isinstance(database, Client): res = database.get_init_args() if self.option('store_passwords') and database._pwd: - from simplecrypt import encrypt import base64 - password = base64.encodestring(b'simplecrypt:' + base64.encodestring(encrypt(database.get_url(), database._pwd))).decode('utf-8') + password = base64.b64encode(b'plain:' + database._pwd.encode('utf-8')).decode('utf-8') res.update({'password': password}) return res elif isinstance(database, dict): From cae58f62f997b6ac69692f18b5ccda623787fcad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Mon, 7 Sep 2015 11:14:50 +0300 Subject: [PATCH 20/23] Added more tests for recordlist. (for methods: ```existing``` and ```refresh```) --- openerp_proxy/tests/test_orm.py | 74 ++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/openerp_proxy/tests/test_orm.py b/openerp_proxy/tests/test_orm.py index 3b90dd0..cd932cc 100644 --- a/openerp_proxy/tests/test_orm.py +++ b/openerp_proxy/tests/test_orm.py @@ -1,7 +1,8 @@ from . import BaseTestCase from openerp_proxy.core import Client -from openerp_proxy.orm.record import Record -from openerp_proxy.orm.record import RecordList +from openerp_proxy.orm.record import (Record, + RecordList, + get_record_list) from openerp_proxy.exceptions import ConnectorError try: @@ -535,3 +536,72 @@ def test_group_by(self): # TODO: write better test res = self.recordlist.group_by('country_id') + + def test_existing(self): + # all existing object ids + all_obj_ids = self.object.search([], limit=False) + + # generate 10 unexisting ids + unexistent_ids = range(max(all_obj_ids) + 1, max(all_obj_ids) + 40, 4) + self.assertEqual(len(unexistent_ids), 10) + + # test simple existense + rlist = get_record_list(self.object, all_obj_ids[:10] + unexistent_ids) + self.assertEqual(len(rlist), 20) + elist = rlist.existing() + self.assertEqual(len(elist), 10) + self.assertItemsEqual(elist.ids, all_obj_ids[:10]) + + # test existense with repeated items + rlist = get_record_list(self.object, all_obj_ids[:10] + unexistent_ids + all_obj_ids[:5]) + self.assertEqual(len(rlist), 25) + + # with uniqify=True (defualt) + elist = rlist.existing() + self.assertEqual(len(elist), 10) + self.assertItemsEqual(elist.ids, all_obj_ids[:10]) + + # with uniqify=False + elist = rlist.existing(uniqify=False) + self.assertEqual(len(elist), 15) + self.assertItemsEqual(elist.ids, all_obj_ids[:10] + all_obj_ids[:5]) + + def test_refresh(self): + # save cache pointers to local namespase to simplify access to it + cache = self.recordlist._cache + pcache = cache['res.partner'] # partner cache + ccache = cache['res.country'] # country cache + + # load data to record list + self.recordlist.prefetch('name', 'country_id.name', 'country_id.code') + + # create related records. This is still required, becuase, prefetch + # just fills cache without creating record instances + for rec in self.recordlist: + rec.country_id + + self.assertTrue(len(pcache) == len(self.recordlist)) + self.assertTrue(len(ccache) > 2) + + clen = len(ccache) + + for data in pcache.values(): + self.assertItemsEqual(list(data), ['id', 'name', 'country_id']) + + for data in ccache.values(): + if '__name_get_result' in data: + self.assertItemsEqual(list(data), ['id', 'name', 'code', '__name_get_result']) + else: + self.assertItemsEqual(list(data), ['id', 'name', 'code']) + + # refresh recordlist + self.recordlist.refresh() + + self.assertTrue(len(pcache) == len(self.recordlist)) + self.assertTrue(len(ccache) == clen) + + for data in pcache.values(): + self.assertItemsEqual(list(data), ['id']) + + for data in ccache.values(): + self.assertItemsEqual(list(data), ['id']) From 026abd1cfd5f7dcc267413e4562eef28a512f803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Mon, 7 Sep 2015 11:59:10 +0300 Subject: [PATCH 21/23] Readme and doc improvements --- README.rst | 51 ++----- docs/source/conf.py | 4 +- docs/source/intro.rst | 129 +++++++++--------- docs/source/module_ref/openerp_proxy.ext.rst | 8 -- docs/source/module_ref/openerp_proxy.orm.rst | 8 ++ .../module_ref/openerp_proxy.service.rst | 8 ++ examples/Examples & HTML tests.ipynb | 20 ++- openerp_proxy/core.py | 2 +- openerp_proxy/orm/cache.py | 3 +- openerp_proxy/orm/service.py | 2 + openerp_proxy/session.py | 3 +- 11 files changed, 118 insertions(+), 120 deletions(-) diff --git a/README.rst b/README.rst index 5f0e612..1b2f7a3 100644 --- a/README.rst +++ b/README.rst @@ -28,32 +28,27 @@ Odoo internal code to perform operations on **OpenERP** / **Odoo** objects hidin Features ~~~~~~~~ -- Experimental *Python 3.3+* support -- supports call to all public methods on any OpenERP / Odoo object including: +- *Python 3.3+* support +- You can call any public method on any OpenERP / Odoo object including: *read*, *search*, *write*, *unlink* and others -- Have *a lot of speed optimizations* (especialy for situation, where required processing of - large datasets) +- Have *a lot of speed optimizations* (caching, read only fields accessed, + read data for all records in current set, by one RPC call, etc) - Desinged to take as more benefits of **IPython autocomplete** as posible - Works nice in **IPython Notebook** providing **HTML representation** for a most of objects. -- Ability to display set of records as **HTML Table** - including conditional **row highlighting**. - (Useful in IPython Notebook for *data-analysis*) -- Ability to represent HTML table also as *CSV file* -- Provides session/history functionality, so if You used it to connect to - some database before, new connection will be simpler (just enter password). - Version 0.5 and higher have ability to store passwords. just use - ``session.option('store_passwords', True); session.save()`` +- Ability to export HTML table recordlist representation to *CSV file* +- Ability to save connections to different databases in session. + (By default password is not saved, and will be asked, but if You need to save it, just do this: + ``session.option('store_passwords', True); session.save()``) - Provides *browse\_record* like interface, allowing to browse related - models too. Supports *browse* method. Adds method *search\_records* to simplify + models too. Supports *browse* method. Also adds method *search\_records* to simplify search-and-read operations. - *Extension support*. You can easily modify most of components of this app/lib - creating Your own extensions. It is realy simple. See for examples in + creating Your own extensions and plugins. It is realy simple. See for examples in openerp_proxy/ext/ directory. -- *Plugin Support*. Plugins here meant utils, which could store some aditional - logic, to simplify routine operations. - Accessible from ``db.plugins.`` attribute. -- Support of **JSON-RPC** for *version 8* of Odoo (***experimental***) +- *Plugin Support*. Plugins are same as extensions, but aimed to implement additional logic. + For example look at *openerp_proxy/plugins* and *openerp_proxy/plugin.py* +- Support of **JSON-RPC** for *version 8+* of Odoo - Support of using **named parametrs** in RPC method calls (server version 6.1 and higher). - *Sugar extension* which simplifys code a lot. @@ -72,26 +67,6 @@ Examples - `Examples & HTML tests `_ -What You can do with this -~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Quickly read and analyze some data that is not visible in interface - without access to DB -- Use this project as library for code that need to access Odoo data -- Use in scripts that migrates Odoo data (after, for example, adding - new functionality or changing old). (Migration using only SQL is bad - idea because of functional fields with *store=True* which must be - recalculated). - -Near future plans -~~~~~~~~~~~~~~~~~ - -- Django-like search API implemented as extension - - Something like ``F`` or ``Q`` expressions from Django - - to make working constructions like: - ``object.filter((F('price') > 100.0) & (F('price') != F('Price2')))`` - - Install ------- diff --git a/docs/source/conf.py b/docs/source/conf.py index 54d2d1f..634881b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -51,7 +51,7 @@ # General information about the project. project = u'openerp_proxy' -copyright = u'2014, Dmytro Katyukha' +copyright = u'2015, Dmytro Katyukha' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -105,7 +105,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/source/intro.rst b/docs/source/intro.rst index 1b9496e..1b2f7a3 100644 --- a/docs/source/intro.rst +++ b/docs/source/intro.rst @@ -1,70 +1,70 @@ OpenERP / Odoo proxy ==================== -This project aims to ease access to openerp data via shell and used -mostly for debug purposes. This project provides interface similar to -OpenERP internal code to perform operations on **OpenERP** / **Odoo** object hiding -XML-RPC behind +Build Status +------------ + +.. image:: https://travis-ci.org/katyukha/openerp-proxy.svg?branch=master + :target: https://travis-ci.org/katyukha/openerp-proxy + +.. image:: https://coveralls.io/repos/katyukha/openerp-proxy/badge.svg?branch=master&service=github + :target: https://coveralls.io/github/katyukha/openerp-proxy?branch=master + Overview -------- +This project is just **RPC client** for Odoo. +It aims to ease access to openerp data via shell and used +mostly for data debuging purposes. This project provides interface similar to +Odoo internal code to perform operations on **OpenERP** / **Odoo** objects hiding +**XML-RPC** or **JSON-RPC** behind. + + + - Are You still using pgAdmin for quering Odoo database? + - Try this package (expecialy via IPython Notebook), and You will forget about pgAdmin! + + Features ~~~~~~~~ -- supports call to all public methods on any OpenERP/Odoo object including: +- *Python 3.3+* support +- You can call any public method on any OpenERP / Odoo object including: *read*, *search*, *write*, *unlink* and others -- Designed not for speed but to be useful like cli client to OpenERP/Odoo - (*Versiion 0.5 introduces orm optimizations*) +- Have *a lot of speed optimizations* (caching, read only fields accessed, + read data for all records in current set, by one RPC call, etc) - Desinged to take as more benefits of **IPython autocomplete** as posible -- Also it works good enough in **IPython Notebook** providing **HTML - representation** for a lot of objects. -- Ability to display set of records as **HTML Table** - including **row highlighting** -- Provides session/history functionality, so if You used it to connect to - some database before, new connection will be simpler (just enter password). - Version 0.5 and higher have ability to store passwords. just use - ``session.option('store_passwords', True); session.save()`` +- Works nice in **IPython Notebook** providing **HTML + representation** for a most of objects. +- Ability to export HTML table recordlist representation to *CSV file* +- Ability to save connections to different databases in session. + (By default password is not saved, and will be asked, but if You need to save it, just do this: + ``session.option('store_passwords', True); session.save()``) - Provides *browse\_record* like interface, allowing to browse related - models too. But use's methods *search\_records* and *browse\_records* - instead of *browse*. (From version 0.4 *browse* works too) + models too. Supports *browse* method. Also adds method *search\_records* to simplify + search-and-read operations. - *Extension support*. You can easily modify most of components of this app/lib - creating Your own extensions. It is realy simple. See for examples in + creating Your own extensions and plugins. It is realy simple. See for examples in openerp_proxy/ext/ directory. -- *Plugin Support*. Plugins here meant utils, which could store some aditional - logic, to simplify routine operations. - Accessible from ``db.plugins.`` attribute. -- Support of **JSON-RPC** for *version 8* of OpenERP/Odoo (*experimental*) +- *Plugin Support*. Plugins are same as extensions, but aimed to implement additional logic. + For example look at *openerp_proxy/plugins* and *openerp_proxy/plugin.py* +- Support of **JSON-RPC** for *version 8+* of Odoo - Support of using **named parametrs** in RPC method calls (server version 6.1 and higher). +- *Sugar extension* which simplifys code a lot. - Missed feature? ask in `Project Issues `_ -Examples -~~~~~~~~ -- `Examples & HTML tests `_ - +Requirements +~~~~~~~~~~~~ -What You can do with this -~~~~~~~~~~~~~~~~~~~~~~~~~ +Odoo version >= 6.1 for high level functionality -- Quickly read and analyze some data that is not visible in interface - without access to DB -- Use this project as library for code that need to access OpenERP data -- Use in scripts that migrates OpenERP data (after, for example, adding - new functionality or changing old). (Migration using only SQL is bad - idea because of functional fields with *store=True* which must be - recalculated). -Near future plans -~~~~~~~~~~~~~~~~~ +Examples +~~~~~~~~ -- Better plugin system which will allow to extend API on database, - object, and record levels. **DONE** -- Django-like search API implemented as extension - - Something like ``F`` or ``Q`` expressions from Django - - to make working constructions like: - ``object.filter((F('price') > 100.0) & (F('price') != F('Price2')))`` +- `Examples & HTML tests `_ Install @@ -82,8 +82,8 @@ If You want to install development version of *OpenERP Proxy* you can do it via: pip install -e git+https://github.com/katyukha/openerp-proxy.git#egg=openerp_proxy -Also if You plan to use this project as shell client, it is recommended to install IPython -and If You would like to have ability to play with Odoo / OpenERP data in IPython notebook, +Also if You plan to use this project as shell client, it is **recommended to install IPython** +and If You would like to have ability to play with Odoo data in IPython notebook, it is recommended to also install IPython's Notebook support. To install IPython and IPython Notebook just type:: @@ -103,14 +103,14 @@ And You will get the openerp_proxy shell. If *IPython* is installed then IPython will be used, else usual python shell will be used. There is in context exists *session* variable that represents current session to work with -Next You have to get connection to some OpenERP/Odoo database. +Next You have to get connection to some Odoo database. :: >>> db = session.connect() This will ask You for host, port, database, etc to connect to. Now You -have connection to OpenERP database which allows You to use database +have connection to Odoo database which allows You to use database objects. @@ -128,11 +128,11 @@ So here is a way to create connection :: - import openerp_proxy.core as oe_core - db = oe_core.ERP_Proxy(dbname='my_db', - host='my_host.int', - user='my_db_user', - pwd='my_password here') + from openerp_proxy.core import Client + db = Client(host='my_host.int', + dbname='my_db', + user='my_db_user', + pwd='my_password here') And next all there same, no more differences betwen shell and lib usage. @@ -144,9 +144,12 @@ To better suit for HTML capable notebook You would like to use IPython's version object and *openerp_proxy.ext.repr* extension. So in first cell of notebook import session and extensions/plugins You want:: - from openerp_proxy.session import IPYSession as Session # Use IPython-itegrated session class - import openerp_proxy.ext.repr # Enable representation extension. This provides HTML representation of objects - from openerp_proxy.ext.repr import HField # Used in .as_html_table method of RecordList + # also You may import all standard extensions in one line: + from openerp_proxy.ext.all import * + + # note that extensions were imported before session, + # because some of them modify Session class + from openerp_proxy.session import Session session = Session() @@ -159,9 +162,10 @@ To solve this, it is recommended to uses *store_passwords* option:: session.option('store_passwords', True) session.save() -In this way, only when You connect first time, You need to explicitly pass password to *connect* of *get_db* methods. +Next use it likt shell (or like lib), but *do not forget to save session, after new connection* -(*do not forget to save session, after new connection*) +*Note*: in old version of IPython getpass was not work correctly, +so maybe You will need to pass password directly to *session.connect* method. General usage @@ -180,9 +184,9 @@ database: So we have 5 orders in done state. So let's read them. -Default way to read data from OpenERP is to search for required records +Default way to read data from Odoo is to search for required records with *search* method which return's list of IDs of records, then read -data using *read* method. Both methods mostly same as OpenERP internal +data using *read* method. Both methods mostly same as Odoo internal ones: :: @@ -302,12 +306,13 @@ Plugins ------- In version 0.4 plugin system was completly refactored. At this version -we start using *extend_me* library to build extensions and plugins. +we start using [*extend_me*](https://pypi.python.org/pypi/extend_me) +library to build extensions and plugins easily. Plugins are usual classes that provides functionality that should be available at ``db.plugins.*`` point, implementing logic not related to core system. -To ilustrate what is plugins and what they can do we will create one. +To ilustrate what is plugins and what they can do we will create a simplest one. So let's start 1. create some directory to place plugins in: @@ -320,7 +325,7 @@ So let's start ``vim attendance.py`` -3. write folowing code there +3. write folowing code there (note that this example works and tested for Odoo version 6.0 only) :: diff --git a/docs/source/module_ref/openerp_proxy.ext.rst b/docs/source/module_ref/openerp_proxy.ext.rst index 8fbb61e..99643ec 100644 --- a/docs/source/module_ref/openerp_proxy.ext.rst +++ b/docs/source/module_ref/openerp_proxy.ext.rst @@ -8,14 +8,6 @@ :undoc-members: :show-inheritance: -:mod:`data` Module ------------------- - -.. automodule:: openerp_proxy.ext.data - :members: - :undoc-members: - :show-inheritance: - :mod:`sugar` Module ------------------- diff --git a/docs/source/module_ref/openerp_proxy.orm.rst b/docs/source/module_ref/openerp_proxy.orm.rst index 3411025..bd5db93 100644 --- a/docs/source/module_ref/openerp_proxy.orm.rst +++ b/docs/source/module_ref/openerp_proxy.orm.rst @@ -16,6 +16,14 @@ :undoc-members: :show-inheritance: +:mod:`cache` Module +-------------------- + +.. automodule:: openerp_proxy.orm.cache + :members: + :undoc-members: + :show-inheritance: + :mod:`record` Module -------------------- diff --git a/docs/source/module_ref/openerp_proxy.service.rst b/docs/source/module_ref/openerp_proxy.service.rst index 2df79a4..e5da3a1 100644 --- a/docs/source/module_ref/openerp_proxy.service.rst +++ b/docs/source/module_ref/openerp_proxy.service.rst @@ -8,6 +8,14 @@ :undoc-members: :show-inheritance: +:mod:`db` Module +-------------------- + +.. automodule:: openerp_proxy.service.db + :members: + :undoc-members: + :show-inheritance: + :mod:`object` Module -------------------- diff --git a/examples/Examples & HTML tests.ipynb b/examples/Examples & HTML tests.ipynb index 5fb5349..6b5a4e6 100644 --- a/examples/Examples & HTML tests.ipynb +++ b/examples/Examples & HTML tests.ipynb @@ -135,8 +135,11 @@ "outputs": [ { "data": { + "text/html": [ + "
                                  Previous connections
                                  DB URLDB IndexDB Aliases
                                  xml-rpc://admin@localhost:8069/demo_db_12
                                  xml-rpc://admin@localhost:8069/openerp_proxy_test_db1ldb
                                  To get connection just call
                                  • session.aliase
                                  • session[index]
                                  • session[aliase]
                                  • session[url]
                                  • session.get_db(url|index|aliase)
                                  " + ], "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -165,8 +168,11 @@ "outputs": [ { "data": { + "text/html": [ + "
                                  Previous connections
                                  DB URLDB IndexDB Aliases
                                  xml-rpc://admin@localhost:8069/demo_db_12
                                  xml-rpc://admin@localhost:8069/openerp_proxy_test_db1ldb
                                  To get connection just call
                                  • session.aliase
                                  • session[index]
                                  • session[aliase]
                                  • session[url]
                                  • session.get_db(url|index|aliase)
                                  " + ], "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -445,7 +451,7 @@ " 'help': 'The tax amount.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x5a3a0c8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x78381b8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Taxes',\n", " 'type': 'float'},\n", " 'amount_total': {'digits': [16, 2],\n", @@ -456,7 +462,7 @@ " 'help': 'The total amount.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x5a3a1b8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x78382a8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Total',\n", " 'type': 'float'},\n", " 'amount_untaxed': {'digits': [16, 2],\n", @@ -467,7 +473,7 @@ " 'help': 'The amount without tax.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x5a29f50>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x78380c8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Untaxed Amount',\n", " 'type': 'float'},\n", " 'client_order_ref': {'selectable': True,\n", @@ -926,10 +932,10 @@ { "data": { "text/html": [ - "./tmp/csv/tmpil4cjF.csv
                                  " + "./tmp/csv/tmpCnqAk0.csv
                                  " ], "text/plain": [ - "/home/katyukha/projects/erp-proxy/examples/tmp/csv/tmpil4cjF.csv" + "/home/katyukha/projects/erp-proxy/examples/tmp/csv/tmpCnqAk0.csv" ] }, "execution_count": 17, diff --git a/openerp_proxy/core.py b/openerp_proxy/core.py index 34a8507..d704431 100644 --- a/openerp_proxy/core.py +++ b/openerp_proxy/core.py @@ -60,7 +60,7 @@ from extend_me import Extensible -__all__ = ('Client') +__all__ = ('Client',) @six.python_2_unicode_compatible diff --git a/openerp_proxy/orm/cache.py b/openerp_proxy/orm/cache.py index cfaa864..acdad1e 100644 --- a/openerp_proxy/orm/cache.py +++ b/openerp_proxy/orm/cache.py @@ -2,7 +2,8 @@ import six import numbers import collections -__all__ = ('empty_cache') + +__all__ = ('empty_cache',) class ObjectCache(dict): diff --git a/openerp_proxy/orm/service.py b/openerp_proxy/orm/service.py index b6b1e7d..e437411 100644 --- a/openerp_proxy/orm/service.py +++ b/openerp_proxy/orm/service.py @@ -1,6 +1,8 @@ from openerp_proxy.service.object import ObjectService from openerp_proxy.orm.object import get_object +__all__ = ('Service',) + class Service(ObjectService): """ Service class to simplify interaction with 'object' service. diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index b8327e0..5a437b8 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -3,6 +3,7 @@ import sys import pprint from getpass import getpass +from extend_me import Extensible # project imports from .core import Client @@ -14,7 +15,7 @@ __all__ = ('Session',) -class Session(object): +class Session(Extensible): """ Simple session manager which allows to manage databases easier This class stores information about databases You used in home From a8b169e79a76c4054ee3c74b5b7b9f34b25c80ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Mon, 7 Sep 2015 12:24:48 +0300 Subject: [PATCH 22/23] Added Python and Odoo versions to readme and classefiers to setup.py --- README.rst | 17 ++++++++++++++--- docs/source/intro.rst | 17 ++++++++++++++--- setup.py | 7 +++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 1b2f7a3..23d626d 100644 --- a/README.rst +++ b/README.rst @@ -55,10 +55,21 @@ Features - Missed feature? ask in `Project Issues `_ -Requirements -~~~~~~~~~~~~ +Supported Python versions +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Support Python 2.7, 3.3, 3.4 + + +Supported Odoo server versions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tested with Odoo 7.0 and 8.0 + +Also shoud work with Odoo 6.1 and 9.0 -Odoo version >= 6.1 for high level functionality +Also it should work with Odoo version 6.0, except the things related to passing named parametrs +to server methods, such as using context in ``openerp_proxy.orm`` package Examples diff --git a/docs/source/intro.rst b/docs/source/intro.rst index 1b2f7a3..23d626d 100644 --- a/docs/source/intro.rst +++ b/docs/source/intro.rst @@ -55,10 +55,21 @@ Features - Missed feature? ask in `Project Issues `_ -Requirements -~~~~~~~~~~~~ +Supported Python versions +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Support Python 2.7, 3.3, 3.4 + + +Supported Odoo server versions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tested with Odoo 7.0 and 8.0 + +Also shoud work with Odoo 6.1 and 9.0 -Odoo version >= 6.1 for high level functionality +Also it should work with Odoo version 6.0, except the things related to passing named parametrs +to server methods, such as using context in ``openerp_proxy.orm`` package Examples diff --git a/setup.py b/setup.py index 02f228c..a86b511 100644 --- a/setup.py +++ b/setup.py @@ -28,8 +28,15 @@ 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Utilities', 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', ], keywords=['openerp', 'odoo', 'odoo-rpc', 'rpc', 'xmlrpc', 'xml-rpc', 'json-rpc', 'jsonrpc', 'odoo-client', 'ipython'], extras_require={ From bee1df58b54395e48ec64ee186f7149946fe5edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?= =?UTF-8?q?=D1=8E=D1=85=D0=B0?= Date: Mon, 7 Sep 2015 15:45:19 +0300 Subject: [PATCH 23/23] Python 3 test fix --- openerp_proxy/tests/test_orm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp_proxy/tests/test_orm.py b/openerp_proxy/tests/test_orm.py index cd932cc..cd9b7a8 100644 --- a/openerp_proxy/tests/test_orm.py +++ b/openerp_proxy/tests/test_orm.py @@ -542,7 +542,7 @@ def test_existing(self): all_obj_ids = self.object.search([], limit=False) # generate 10 unexisting ids - unexistent_ids = range(max(all_obj_ids) + 1, max(all_obj_ids) + 40, 4) + unexistent_ids = list(range(max(all_obj_ids) + 1, max(all_obj_ids) + 40, 4)) self.assertEqual(len(unexistent_ids), 10) # test simple existense