From 3b2fd0039888133682fab1beb3e7445bc558ebd9 Mon Sep 17 00:00:00 2001 From: Mark Vytlacil Date: Tue, 6 Oct 2020 16:41:01 -0500 Subject: [PATCH 1/7] Python bindings classmethod function capability is enhanced. Gnucash functions can be bound to python class methods. The changes allow these methods to return instances of python classes. This allows e.g. binding of gnucash object creation functions to a python classmethod. This means that a gnucash python instance can be created without an existing instance. Minor clarification of the existing class method code is done. --- .../python/example_scripts/str_methods.py | 2 +- bindings/python/function_class.py | 33 +++++++++++++++---- bindings/python/tests/test_function_class.py | 15 +++++++++ 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/bindings/python/example_scripts/str_methods.py b/bindings/python/example_scripts/str_methods.py index dfbec36c66..26bf5bc573 100644 --- a/bindings/python/example_scripts/str_methods.py +++ b/bindings/python/example_scripts/str_methods.py @@ -65,7 +65,7 @@ def ya_add_method(_class, function, method_name=None, clsmethod=False, noinstanc setattr(gnucash.gnucash_core_c,function.__name__,function) if clsmethod: - mf=_class.ya_add_classmethod(function.__name__,method_name) + mf=_class.add_classmethod(function.__name__,method_name) elif noinstance: mf=_class.add_method(function.__name__,method_name) else: diff --git a/bindings/python/function_class.py b/bindings/python/function_class.py index 5ed9f82f8c..54ba840487 100644 --- a/bindings/python/function_class.py +++ b/bindings/python/function_class.py @@ -87,7 +87,7 @@ class ClassFromFunctions(object): @classmethod def add_method(cls, function_name, method_name): - """! Add the function, method_name to this class as a method named name + """! Add the function, function_name to this class as a method named method_name arguments: @param cls Class: class to add methods to @@ -116,8 +116,8 @@ class ClassFromFunctions(object): return method_function @classmethod - def ya_add_classmethod(cls, function_name, method_name): - """! Add the function, method_name to this class as a classmethod named name + def add_classmethod(cls, function_name, method_name): + """! Add the function, function_name to this class as a classmethod named method_name Taken from function_class and modified from add_method() to add classmethod instead of method and not to turn self argument to self.instance. @@ -129,17 +129,16 @@ class ClassFromFunctions(object): function will be wrapped by method_function""" - def method_function(self, *meth_func_args, **meth_func_kargs): + def method_function(cls, *meth_func_args, **meth_func_kargs): """! wrapper method for function arguments: - @param self: FunctionClass instance. + @param cls: FunctionClass. @param *meth_func_args: arguments to be passed to function. All FunctionClass objects will be turned to their respective instances. @param **meth_func_kargs: keyword arguments to be passed to function. All FunctionClass objects will be turned to their respective instances.""" - return getattr(self._module, function_name)( - self, + return getattr(cls._module, function_name)( *process_list_convert_to_instance(meth_func_args), **process_dict_convert_to_instance(meth_func_kargs) ) @@ -240,6 +239,26 @@ def method_function_returns_instance(method_function, cls): return new_function +def classmethod_function_returns_instance(method_function, cls): + """A function decorator that is used to decorate classmethod functions that + return instance data, to return instances instead. + + You can't use this decorator with @, because this function has a second + argument. + """ + assert( 'instance' == INSTANCE_ARGUMENT ) + # Will get a class in arguement list, + # use static so we don't add another here. + @staticmethod + def new_function(*args, **kargs): + kargs_cls = { INSTANCE_ARGUMENT : method_function(*args, **kargs) } + if kargs_cls['instance'] == None: + return None + else: + return cls( **kargs_cls ) + + return new_function + def method_function_returns_instance_list(method_function, cls): def new_function(*args, **kargs): return [ cls( **{INSTANCE_ARGUMENT: item} ) diff --git a/bindings/python/tests/test_function_class.py b/bindings/python/tests/test_function_class.py index df099e086b..dea8f16599 100644 --- a/bindings/python/tests/test_function_class.py +++ b/bindings/python/tests/test_function_class.py @@ -41,6 +41,10 @@ def other_function(self, arg=None): return self, arg +def class_other_function(arg=None): + return arg + + class TestClass(ClassFromFunctions): _module = sys.modules[__name__] @@ -80,6 +84,17 @@ class TestFunctionClass(TestCase): obj, arg = self.t.other_method(arg=self.t) self.assertIsInstance(arg, Instance) + def test_add_classmethod(self): + """test if add_classmethod adds method and if in case of FunctionClass + Instance instances get returned instead of FunctionClass instances""" + TestClass.add_constructor_and_methods_with_prefix("prefix_", "new_function") + TestClass.add_classmethod("class_other_function", "other_method") + self.t = TestClass() + arg = TestClass.other_method(self.t) + self.assertIsInstance(arg, Instance) + arg = TestClass.other_method(arg=self.t) + self.assertIsInstance(arg, Instance) + def test_default_arguments_decorator(self): """test default_arguments_decorator()""" TestClass.backup_test_function_return_args = TestClass.test_function_return_args From 8c8a2a34c6196e7e9626c017ddc129399d1e21c9 Mon Sep 17 00:00:00 2001 From: Mark Vytlacil Date: Tue, 6 Oct 2020 20:58:12 -0500 Subject: [PATCH 2/7] Python bindings GncLot class method make_default added. This method creates a new GncLot python instance that is bound to a new gnucash gnclot object. It does not require an existing GncLot instance. --- bindings/python/gnucash_core.py | 9 ++++++--- bindings/python/tests/runTests.py.in | 1 + bindings/python/tests/test_lot.py | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 bindings/python/tests/test_lot.py diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py index 50eb6c41a0..1ffc8d0653 100644 --- a/bindings/python/gnucash_core.py +++ b/bindings/python/gnucash_core.py @@ -38,7 +38,8 @@ from gnucash.function_class import \ ClassFromFunctions, extract_attributes_with_prefix, \ default_arguments_decorator, method_function_returns_instance, \ methods_return_instance, process_list_convert_to_instance, \ - method_function_returns_instance_list, methods_return_instance_lists + method_function_returns_instance_list, methods_return_instance_lists, \ + classmethod_function_returns_instance from gnucash.gnucash_core_c import gncInvoiceLookup, gncInvoiceGetInvoiceFromTxn, \ gncInvoiceGetInvoiceFromLot, gncEntryLookup, gncInvoiceLookup, \ @@ -744,6 +745,8 @@ GncCommodityNamespace.get_commodity_list = \ # GncLot GncLot.add_constructor_and_methods_with_prefix('gnc_lot_', 'new') +# replace method for gnc_lot_make_default() to be a classmethod +GncLot.add_classmethod('gnc_lot_make_default', 'make_default') gnclot_dict = { 'get_account' : Account, @@ -751,10 +754,10 @@ gnclot_dict = { 'get_earliest_split' : Split, 'get_latest_split' : Split, 'get_balance' : GncNumeric, - 'lookup' : GncLot, - 'make_default' : GncLot + 'lookup' : GncLot } methods_return_instance(GncLot, gnclot_dict) +GncLot.make_default = classmethod_function_returns_instance(GncLot.make_default, GncLot) # Transaction Transaction.add_methods_with_prefix('xaccTrans') diff --git a/bindings/python/tests/runTests.py.in b/bindings/python/tests/runTests.py.in index 5b9142592d..d351ed443c 100755 --- a/bindings/python/tests/runTests.py.in +++ b/bindings/python/tests/runTests.py.in @@ -16,6 +16,7 @@ from test_business import TestBusiness from test_commodity import TestCommodity, TestCommodityNamespace from test_numeric import TestGncNumeric from test_query import TestQuery +from test_lot import TestLot if __name__ == '__main__': unittest.main() diff --git a/bindings/python/tests/test_lot.py b/bindings/python/tests/test_lot.py new file mode 100644 index 0000000000..4ade8b6f7c --- /dev/null +++ b/bindings/python/tests/test_lot.py @@ -0,0 +1,16 @@ +from unittest import main +from gnucash import Book, Account, GncLot + +from test_account import AccountSession + +class LotSession(AccountSession): + def setUp(self): + AccountSession.setUp(self) + +class TestLot(LotSession): + def test_make_default(self): + lot = GncLot.make_default(self.account) + self.assertIsInstance(lot, GncLot) + +if __name__ == '__main__': + unittest.main() From 89915925e3bb4468c1ceed00bc74ccad726770f5 Mon Sep 17 00:00:00 2001 From: Mark Vytlacil Date: Tue, 6 Oct 2020 21:56:05 -0500 Subject: [PATCH 3/7] Adds AssignToLot method to python bindings Split class. This lets python scripts assign a gnucash split to a gnucash gnclot. --- bindings/python/gnucash_core.py | 3 ++- bindings/python/tests/test_lot.py | 34 +++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py index 1ffc8d0653..9b04aa679b 100644 --- a/bindings/python/gnucash_core.py +++ b/bindings/python/gnucash_core.py @@ -809,7 +809,8 @@ split_dict = { 'GetReconciledBalance': GncNumeric, 'VoidFormerAmount': GncNumeric, 'VoidFormerValue': GncNumeric, - 'GetGUID': GUID + 'GetGUID': GUID, + 'AssignToLot': Split } methods_return_instance(Split, split_dict) diff --git a/bindings/python/tests/test_lot.py b/bindings/python/tests/test_lot.py index 4ade8b6f7c..d89c7ce304 100644 --- a/bindings/python/tests/test_lot.py +++ b/bindings/python/tests/test_lot.py @@ -1,16 +1,42 @@ from unittest import main -from gnucash import Book, Account, GncLot +from gnucash import Book, Account, GncLot, Split, GncNumeric from test_account import AccountSession +from test_split import SplitSession -class LotSession(AccountSession): +class LotSession(AccountSession, SplitSession): def setUp(self): AccountSession.setUp(self) + self.NUM = 10000 + self.amount = GncNumeric(self.NUM, 100) + + def setup_buysplit(self): + self.buysplit = Split(self.book) + self.buysplit.SetAccount(self.account) + self.buysplit.SetAmount(self.amount) + + def setup_sellsplit(self): + self.sellsplit = Split(self.book) + self.sellsplit.SetAccount(self.account) + self.sellsplit.SetAmount(self.amount.neg()) class TestLot(LotSession): def test_make_default(self): - lot = GncLot.make_default(self.account) - self.assertIsInstance(lot, GncLot) + self.lot = GncLot.make_default(self.account) + self.assertIsInstance(self.lot, GncLot) + + def test_AssignToLot(self): + self.lot = GncLot.make_default(self.account) + + self.setup_buysplit() + self.buysplit.AssignToLot(self.lot) + self.assertEqual(self.NUM, self.lot.get_balance().num()) + self.assertTrue(not self.lot.is_closed()) + + self.setup_sellsplit() + self.sellsplit.AssignToLot(self.lot) + self.assertEqual(0, self.lot.get_balance().num()) + self.assertTrue(self.lot.is_closed()) if __name__ == '__main__': unittest.main() From e9c22ecd13b1244e75c292839a054315a3481da5 Mon Sep 17 00:00:00 2001 From: Mark Vytlacil Date: Tue, 6 Oct 2020 22:08:15 -0500 Subject: [PATCH 4/7] Adds GetLot method to python bindings Split class. This lets a python script find out which, if any, gnclot a gnucash split may be assigned to. --- bindings/python/gnucash_core.py | 1 + bindings/python/tests/test_lot.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py index 9b04aa679b..1c338ad9bc 100644 --- a/bindings/python/gnucash_core.py +++ b/bindings/python/gnucash_core.py @@ -797,6 +797,7 @@ split_dict = { 'GetBook': Book, 'GetAccount': Account, 'GetParent': Transaction, + 'GetLot': GncLot, 'Lookup': Split, 'GetOtherSplit': Split, 'GetAmount': GncNumeric, diff --git a/bindings/python/tests/test_lot.py b/bindings/python/tests/test_lot.py index d89c7ce304..c951961f78 100644 --- a/bindings/python/tests/test_lot.py +++ b/bindings/python/tests/test_lot.py @@ -38,5 +38,12 @@ class TestLot(LotSession): self.assertEqual(0, self.lot.get_balance().num()) self.assertTrue(self.lot.is_closed()) + def test_Split_GetLot(self): + self.lot = GncLot.make_default(self.account) + self.setup_buysplit() + self.buysplit.AssignToLot(self.lot) + rtn_lot = self.buysplit.GetLot() + self.assertEqual(rtn_lot.get_title(), self.lot.get_title()) + if __name__ == '__main__': unittest.main() From ff51f6c085f9c513ec9c4a32fc2c674e86cfa6c3 Mon Sep 17 00:00:00 2001 From: Mark Vytlacil Date: Tue, 6 Oct 2020 22:19:47 -0500 Subject: [PATCH 5/7] Adds get_split_list method to python bindings GncLot class. This lets python scripts get a list of gnucash splits that are assigned to a gnclot. --- bindings/python/gnucash_core.py | 3 +++ bindings/python/tests/test_lot.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py index 1c338ad9bc..c1db87cadc 100644 --- a/bindings/python/gnucash_core.py +++ b/bindings/python/gnucash_core.py @@ -758,6 +758,9 @@ gnclot_dict = { } methods_return_instance(GncLot, gnclot_dict) GncLot.make_default = classmethod_function_returns_instance(GncLot.make_default, GncLot) +methods_return_instance_lists( + GncLot, { 'get_split_list': Split + }) # Transaction Transaction.add_methods_with_prefix('xaccTrans') diff --git a/bindings/python/tests/test_lot.py b/bindings/python/tests/test_lot.py index c951961f78..4172fd183f 100644 --- a/bindings/python/tests/test_lot.py +++ b/bindings/python/tests/test_lot.py @@ -44,6 +44,14 @@ class TestLot(LotSession): self.buysplit.AssignToLot(self.lot) rtn_lot = self.buysplit.GetLot() self.assertEqual(rtn_lot.get_title(), self.lot.get_title()) + + def test_get_split_list(self): + self.lot = GncLot.make_default(self.account) + self.setup_buysplit() + self.buysplit.AssignToLot(self.lot) + splits = self.lot.get_split_list() + self.assertEqual(self.NUM, splits[0].GetAmount().num()) + self.assertEqual(self.account.name, splits[0].GetAccount().name) if __name__ == '__main__': unittest.main() From 2c697a77388b902188298d03b8fa2ff2e829ed5c Mon Sep 17 00:00:00 2001 From: Mark Vytlacil Date: Tue, 6 Oct 2020 22:26:20 -0500 Subject: [PATCH 6/7] Adds GetLotList method to python bindings Account class. This lets python scripts get a list of gnucash gnclots that have assigned splits from the account in question. --- bindings/python/gnucash_core.py | 1 + bindings/python/tests/test_lot.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py index c1db87cadc..551280ad39 100644 --- a/bindings/python/gnucash_core.py +++ b/bindings/python/gnucash_core.py @@ -859,6 +859,7 @@ account_dict = { methods_return_instance(Account, account_dict) methods_return_instance_lists( Account, { 'GetSplitList': Split, + 'GetLotList': GncLot, 'get_children': Account, 'get_children_sorted': Account, 'get_descendants': Account, diff --git a/bindings/python/tests/test_lot.py b/bindings/python/tests/test_lot.py index 4172fd183f..961f6dca2b 100644 --- a/bindings/python/tests/test_lot.py +++ b/bindings/python/tests/test_lot.py @@ -2,9 +2,9 @@ from unittest import main from gnucash import Book, Account, GncLot, Split, GncNumeric from test_account import AccountSession -from test_split import SplitSession +# from test_split import SplitSession -class LotSession(AccountSession, SplitSession): +class LotSession(AccountSession): def setUp(self): AccountSession.setUp(self) self.NUM = 10000 @@ -53,5 +53,12 @@ class TestLot(LotSession): self.assertEqual(self.NUM, splits[0].GetAmount().num()) self.assertEqual(self.account.name, splits[0].GetAccount().name) + def test_Account_GetLotList(self): + self.lot = GncLot.make_default(self.account) + self.setup_buysplit() + self.buysplit.AssignToLot(self.lot) + lots = self.account.GetLotList() + self.assertEqual(self.account.name, lots[0].get_account().name) + if __name__ == '__main__': unittest.main() From 2106d462ded362898624eda30a1f234e1e442fb3 Mon Sep 17 00:00:00 2001 From: Mark Vytlacil Date: Tue, 6 Oct 2020 22:32:00 -0500 Subject: [PATCH 7/7] Fixes a memory leak in the python bindings. Most lists of C structures returned by engine functions are the internal lists; but account and lot lists are copies and need to be freed by the caller. The bindings need to free these lists after lists of python classes are created from them. --- common/base-typemaps.i | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/base-typemaps.i b/common/base-typemaps.i index 98fbe30279..fa243fed6d 100644 --- a/common/base-typemaps.i +++ b/common/base-typemaps.i @@ -308,5 +308,8 @@ typedef char gchar; PyList_Append(list, SWIG_NewPointerObj(data, SWIGTYPE_p_void, 0)); } $result = list; + if ($1_descriptor == $descriptor(AccountList *) || + $1_descriptor == $descriptor(LotList *)) + g_list_free($1); } #endif