gnucash master: Multiple changes pushed

John Ralls jralls at code.gnucash.org
Mon Jul 6 15:59:08 EDT 2020


Updated	 via  https://github.com/Gnucash/gnucash/commit/b0b23895 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/22f91c40 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/40cfb70f (commit)
	 via  https://github.com/Gnucash/gnucash/commit/e23bf0bc (commit)
	 via  https://github.com/Gnucash/gnucash/commit/7c8e0a28 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/3e842a7b (commit)
	 via  https://github.com/Gnucash/gnucash/commit/b9c6fc28 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/0434acbe (commit)
	 via  https://github.com/Gnucash/gnucash/commit/485d8a65 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/44e61f4d (commit)
	 via  https://github.com/Gnucash/gnucash/commit/5833c5af (commit)
	 via  https://github.com/Gnucash/gnucash/commit/17d606e1 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/c222503f (commit)
	 via  https://github.com/Gnucash/gnucash/commit/ee77b713 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/b073dbc5 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/4e280b95 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/48072f5a (commit)
	 via  https://github.com/Gnucash/gnucash/commit/ee3342d2 (commit)
	from  https://github.com/Gnucash/gnucash/commit/4ee573e2 (commit)



commit b0b238958e2b8c36426688762341f3288d3919ff
Merge: 4ee573e23 22f91c407
Author: John Ralls <jralls at ceridwen.us>
Date:   Mon Jul 6 12:45:07 2020 -0700

    Merge Christoph Holtermann's 'python-sessionOpenMode' into master.


commit 22f91c407ee52fcba649d2f608f900a7be6f99fc
Author: c-holtermann <mail at c-holtermann.net>
Date:   Sat Jul 4 22:26:35 2020 +0200

    use same order in comment as in definition of SessionOpenMode enum

diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py
index 72837ba14..50eb6c41a 100644
--- a/bindings/python/gnucash_core.py
+++ b/bindings/python/gnucash_core.py
@@ -191,14 +191,14 @@ class Session(GnuCashCoreClass):
         uri and if none is found, create it. If the file or database exists post a
         QOF_BACKED_STORE_EXISTS and return.
         @par
+        `SESSION_NEW_OVERWRITE`: Create a new file or database at the provided uri,
+        deleting any existing file or database.
+        @par
         `SESSION_READ_ONLY`: Find an existing file or database and open it without
         disturbing the lock if it exists or setting one if not. This will also set a
         flag on the book that will prevent many elements from being edited and will
         prevent the backend from saving any edits.
         @par
-        `SESSION_NEW_OVERWRITE`: Create a new file or database at the provided uri,
-        deleting any existing file or database.
-        @par
         `SESSION_BREAK_LOCK`: Find an existing file or database, lock it, and open
         it. If there is already a lock replace it with a new one for this session.
 
diff --git a/libgnucash/engine/qofsession.h b/libgnucash/engine/qofsession.h
index 4bab7f0fd..e01d95a65 100644
--- a/libgnucash/engine/qofsession.h
+++ b/libgnucash/engine/qofsession.h
@@ -164,14 +164,14 @@ void qof_session_swap_data (QofSession *session_1, QofSession *session_2);
  * uri and if none is found, create it. If the file or database exists post a
  * QOF_BACKED_STORE_EXISTS and return.
  * @par
+ * `SESSION_NEW_OVERWRITE`: Create a new file or database at the provided uri,
+ * deleting any existing file or database.
+ * @par
  * `SESSION_READ_ONLY`: Find an existing file or database and open it without
  * disturbing the lock if it exists or setting one if not. This will also set a
  * flag on the book that will prevent many elements from being edited and will
  * prevent the backend from saving any edits.
  * @par
- * `SESSION_NEW_OVERWRITE`: Create a new file or database at the provided uri,
- * deleting any existing file or database.
- * @par
  * `SESSION_BREAK_LOCK`: Find an existing file or database, lock it, and open
  * it. If there is already a lock replace it with a new one for this session.
  *

commit 40cfb70fb722501278d87bc283588d70a578b583
Author: c-holtermann <mail at c-holtermann.net>
Date:   Sat Jul 4 22:22:16 2020 +0200

    fix SessionOpenMode explanation for SESSION_NORMAL_OPEN

diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py
index 2ef37911d..72837ba14 100644
--- a/bindings/python/gnucash_core.py
+++ b/bindings/python/gnucash_core.py
@@ -184,7 +184,7 @@ class Session(GnuCashCoreClass):
         @note SessionOpenMode replaces deprecated ignore_lock, is_new and force_new.
 
         @par SessionOpenMode
-        `SESSION_NORMAL`: Find an existing file or database at the provided uri and
+        `SESSION_NORMAL_OPEN`: Find an existing file or database at the provided uri and
         open it if it is unlocked. If it is locked post a QOF_BACKEND_LOCKED error.
         @par
         `SESSION_NEW_STORE`: Check for an existing file or database at the provided
@@ -196,7 +196,7 @@ class Session(GnuCashCoreClass):
         flag on the book that will prevent many elements from being edited and will
         prevent the backend from saving any edits.
         @par
-        `SESSION_OVERWRITE`: Create a new file or database at the provided uri,
+        `SESSION_NEW_OVERWRITE`: Create a new file or database at the provided uri,
         deleting any existing file or database.
         @par
         `SESSION_BREAK_LOCK`: Find an existing file or database, lock it, and open

commit e23bf0bc1c7e35c66c7be2d8250e7f6073eb3b8b
Author: c-holtermann <mail at c-holtermann.net>
Date:   Sat Jul 4 22:16:13 2020 +0200

    fix SessionOpenMode explanation for SESSION_NEW_OVERWRITE

diff --git a/libgnucash/engine/qofsession.h b/libgnucash/engine/qofsession.h
index 781622952..4bab7f0fd 100644
--- a/libgnucash/engine/qofsession.h
+++ b/libgnucash/engine/qofsession.h
@@ -169,7 +169,7 @@ void qof_session_swap_data (QofSession *session_1, QofSession *session_2);
  * flag on the book that will prevent many elements from being edited and will
  * prevent the backend from saving any edits.
  * @par
- * `SESSION_OVERWRITE`: Create a new file or database at the provided uri,
+ * `SESSION_NEW_OVERWRITE`: Create a new file or database at the provided uri,
  * deleting any existing file or database.
  * @par
  * `SESSION_BREAK_LOCK`: Find an existing file or database, lock it, and open

commit 7c8e0a28fc7ef0be313a6e92910fbd9e691fa204
Author: c-holtermann <mail at c-holtermann.net>
Date:   Sat Jun 20 13:21:41 2020 +0200

    better display for doxygen, typo and consistent naming

diff --git a/libgnucash/engine/qofsession.h b/libgnucash/engine/qofsession.h
index 5836d879c..781622952 100644
--- a/libgnucash/engine/qofsession.h
+++ b/libgnucash/engine/qofsession.h
@@ -154,24 +154,28 @@ void qof_session_swap_data (QofSession *session_1, QofSession *session_2);
  * assumed. Customized backends can choose to search other
  * application-specific directories or URI schemes as well.
  *
- * @param mode The SessionMode.
+ * @param mode The SessionOpenMode.
  *
- * ==== SessionMode ====
- * `SESSION_NORMAL`: Find an existing file or database at the provided uri and
+ * @par ==== SessionOpenMode ====
+ * `SESSION_NORMAL_OPEN`: Find an existing file or database at the provided uri and
  * open it if it is unlocked. If it is locked post a QOF_BACKEND_LOCKED error.
+ * @par
  * `SESSION_NEW_STORE`: Check for an existing file or database at the provided
  * uri and if none is found, create it. If the file or database exists post a
  * QOF_BACKED_STORE_EXISTS and return.
+ * @par
  * `SESSION_READ_ONLY`: Find an existing file or database and open it without
  * disturbing the lock if it exists or setting one if not. This will also set a
  * flag on the book that will prevent many elements from being edited and will
  * prevent the backend from saving any edits.
+ * @par
  * `SESSION_OVERWRITE`: Create a new file or database at the provided uri,
  * deleting any existing file or database.
- * `SESSION_BREAK_LOCK1: Find an existing file or database, lock it, and open
+ * @par
+ * `SESSION_BREAK_LOCK`: Find an existing file or database, lock it, and open
  * it. If there is already a lock replace it with a new one for this session.
  *
- * ==== Errors ====
+ * @par ==== Errors ====
  * This function signals failure by queuing errors. After it completes use
  * qof_session_get_error() and test that the value is `ERROR_BACKEND_NONE` to
  * determine that the session began successfully.

commit 3e842a7bf6e3b5479c9e110554c0399b461373a6
Author: c-holtermann <mail at c-holtermann.net>
Date:   Sat Jun 20 10:35:31 2020 +0200

    use urllib.parse.urlparse to check for xml on python Session init

diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py
index f5647c012..2ef37911d 100644
--- a/bindings/python/gnucash_core.py
+++ b/bindings/python/gnucash_core.py
@@ -29,6 +29,8 @@
 #  @ingroup python_bindings
 
 from enum import IntEnum
+from urllib.parse import urlparse
+
 from gnucash import gnucash_core_c
 from gnucash import _sw_core_utils
 
@@ -228,7 +230,8 @@ class Session(GnuCashCoreClass):
                 # Any existing store obviously has to be loaded
                 # More background: https://bugs.gnucash.org/show_bug.cgi?id=726891
                 is_new = mode in (SessionOpenMode.SESSION_NEW_STORE, SessionOpenMode.SESSION_NEW_OVERWRITE)
-                if book_uri[:3] != "xml" or not is_new:
+                scheme = urlparse(book_uri).scheme
+                if not (is_new and scheme == 'xml'):
                     self.load()
             except GnuCashBackendException as backend_exception:
                 self.end()

commit b9c6fc28767c130c7bc52f68c3dbaee88fe77f41
Author: c-holtermann <mail at c-holtermann.net>
Date:   Thu Jun 11 17:52:02 2020 +0200

    add some unittests for python Session
    
    test arguments, deprecated as well as new mode arguments
    test creating a session with a new xml file using __init__()
    and begin(). Test raising exception when opening nonexistent
    file without respective mode setting.

diff --git a/bindings/python/tests/test_session.py b/bindings/python/tests/test_session.py
index 7751e37e6..8127e2e6a 100644
--- a/bindings/python/tests/test_session.py
+++ b/bindings/python/tests/test_session.py
@@ -10,12 +10,57 @@
 
 from unittest import TestCase, main
 
-from gnucash import Session
+from gnucash import (
+        Session,
+        SessionOpenMode
+)
+
+from gnucash.gnucash_core import GnuCashBackendException
 
 class TestSession(TestCase):
     def test_create_empty_session(self):
         self.ses = Session()
 
+    def test_session_deprecated_arguments(self):
+        """use deprecated arguments ignore_lock, is_new, force_new"""
+        self.ses = Session(ignore_lock=False, is_new=True, force_new=False)
+
+    def test_session_mode(self):
+        """use mode argument"""
+        self.ses = Session(mode=SessionOpenMode.SESSION_NORMAL_OPEN)
+
+    def test_session_with_new_file(self):
+        """create Session with new xml file"""
+        from tempfile import TemporaryDirectory
+        from urllib.parse import urlunparse
+        with TemporaryDirectory() as tempdir:
+            uri = urlunparse(("xml", tempdir, "tempfile", "", "", ""))
+            with Session(uri, SessionOpenMode.SESSION_NEW_STORE) as ses:
+                pass
+
+            # try to open nonexistent file without NEW mode - should raise Exception
+            uri = urlunparse(("xml", tempdir, "tempfile2", "", "", ""))
+            with Session() as ses:
+                with self.assertRaises(GnuCashBackendException):
+                    ses.begin(uri, mode=SessionOpenMode.SESSION_NORMAL_OPEN)
+
+            # try to open nonexistent file without NEW mode - should raise Exception
+            # use deprecated arg is_new
+            uri = urlunparse(("xml", tempdir, "tempfile2", "", "", ""))
+            with Session() as ses:
+                with self.assertRaises(GnuCashBackendException):
+                    ses.begin(uri, is_new=False)
+
+            uri = urlunparse(("xml", tempdir, "tempfile3", "", "", ""))
+            with Session() as ses:
+                ses.begin(uri, mode=SessionOpenMode.SESSION_NEW_STORE)
+
+            # test using deprecated args
+            uri = urlunparse(("xml", tempdir, "tempfile4", "", "", ""))
+            with Session() as ses:
+                ses.begin(uri, is_new=True)
+
+
     def test_app_utils_get_current_session(self):
         from gnucash import _sw_app_utils
         self.ses_instance = _sw_app_utils.gnc_get_current_session()

commit 0434acbe1035ed679d23242a412c48a680ac5a07
Author: c-holtermann <mail at c-holtermann.net>
Date:   Fri Jun 19 20:45:21 2020 +0200

    reformat two python example scripts with black
    
    use black python code formatter on latex_invoices.py and gncinvoice_jinja.py

diff --git a/bindings/python/example_scripts/gncinvoice_jinja.py b/bindings/python/example_scripts/gncinvoice_jinja.py
index bfa7c09a5..f13675a69 100755
--- a/bindings/python/example_scripts/gncinvoice_jinja.py
+++ b/bindings/python/example_scripts/gncinvoice_jinja.py
@@ -45,10 +45,12 @@ except ImportError as import_error:
     print(import_error)
     sys.exit(2)
 
+
 class Usage(Exception):
     def __init__(self, msg):
         self.msg = msg
 
+
 def main(argv=None):
     if argv is None:
         argv = sys.argv
@@ -69,27 +71,27 @@ def main(argv=None):
         try:
             opts, args = getopt.getopt(argv[1:], "fhliI:t:o:OP:", ["help"])
         except getopt.error as msg:
-             raise Usage(msg)
+            raise Usage(msg)
 
         for opt in opts:
             if opt[0] in ["-f"]:
                 print("ignoring lock")
                 ignore_lock = True
-            if opt[0] in ["-h","--help"]:
+            if opt[0] in ["-h", "--help"]:
                 raise Usage("Help:")
             if opt[0] in ["-I"]:
                 invoice_id = opt[1]
-                print ("using invoice ID '" + str(invoice_id) + "'.")
+                print("using invoice ID '" + str(invoice_id) + "'.")
             if opt[0] in ["-i"]:
-                print ("Using ipshell")
+                print("Using ipshell")
                 with_ipshell = True
             if opt[0] in ["-o"]:
                 filename_output = opt[1]
                 print("using output file", filename_output)
             if opt[0] in ["-O"]:
                 if filename_output:
-                    print ("given output filename will be overwritten,")
-                print ("creating output filename from Invoice data.")
+                    print("given output filename will be overwritten,")
+                print("creating output filename from Invoice data.")
                 filename_from_invoice = True
             if opt[0] in ["-t"]:
                 filename_template = opt[1]
@@ -99,13 +101,13 @@ def main(argv=None):
                 print("listing invoices")
             if opt[0] in ["-P"]:
                 output_path = opt[1]
-                print ("output path is", output_path + ".")
+                print("output path is", output_path + ".")
 
         # Check for correct input
-        if len(args)>1:
-            print("opts:",opts,"args:",args)
+        if len(args) > 1:
+            print("opts:", opts, "args:", args)
             raise Usage("Only one input possible !")
-        if len(args)==0:
+        if len(args) == 0:
             raise Usage("No input given !")
         input_url = args[0]
 
@@ -123,16 +125,16 @@ def main(argv=None):
 
     except Usage as err:
         if err.msg == "Help:":
-            retcode=0
+            retcode = 0
         else:
             print("Error:", err.msg, file=sys.stderr)
             print("for help use --help", file=sys.stderr)
-            retcode=2
+            retcode = 2
 
         print()
         print("Usage:")
         print()
-        print("Invoke with",prog_name,"gnucash_url.")
+        print("Invoke with", prog_name, "gnucash_url.")
         print("where input is")
         print("   filename")
         print("or file://filename")
@@ -173,9 +175,9 @@ def main(argv=None):
     invoice_list = get_all_invoices(book)
 
     if list_invoices:
-       for number,invoice in enumerate(invoice_list):
-           print(str(number)+")")
-           print(invoice)
+        for number, invoice in enumerate(invoice_list):
+            print(str(number) + ")")
+            print(invoice)
 
     if not (no_output):
 
@@ -191,7 +193,6 @@ def main(argv=None):
         print("Using the following invoice:")
         print(invoice)
 
-
         path_template = os.path.dirname(filename_template)
         filename_template_basename = os.path.basename(filename_template)
 
@@ -199,25 +200,37 @@ def main(argv=None):
         env = jinja2.Environment(loader=loader)
         template = env.get_template(filename_template_basename)
 
-        #company = gnucash_business.Company(book.instance)
+        # company = gnucash_business.Company(book.instance)
 
-        output = template.render(invoice=invoice, locale=locale) #, company=company)
+        output = template.render(invoice=invoice, locale=locale)  # , company=company)
 
         if filename_from_invoice:
-            filename_date = invoice.GetDatePosted().strftime("%Y-%m-%d") # something like 2014-11-01
+            filename_date = invoice.GetDatePosted().strftime(
+                "%Y-%m-%d"
+            )  # something like 2014-11-01
             filename_owner_name = str(invoice.GetOwner().GetName())
             filename_invoice_id = str(invoice.GetID())
-            filename_output = filename_date + "_" + filename_owner_name + "_"  + filename_invoice_id + ".tex"
+            filename_output = (
+                filename_date
+                + "_"
+                + filename_owner_name
+                + "_"
+                + filename_invoice_id
+                + ".tex"
+            )
 
         if output_path:
-            filename_output = os.path.join(output_path, os.path.basename(filename_output))
+            filename_output = os.path.join(
+                output_path, os.path.basename(filename_output)
+            )
 
-        print ("Writing output", filename_output, ".")
-        with open(filename_output, 'w') as f:
+        print("Writing output", filename_output, ".")
+        with open(filename_output, "w") as f:
             f.write(output)
 
         if with_ipshell:
             import IPython
+
             IPython.embed()
 
 
diff --git a/bindings/python/example_scripts/latex_invoices.py b/bindings/python/example_scripts/latex_invoices.py
index c29922e05..829021adf 100644
--- a/bindings/python/example_scripts/latex_invoices.py
+++ b/bindings/python/example_scripts/latex_invoices.py
@@ -57,13 +57,24 @@ try:
     import str_methods
     from gncinvoicefkt import *
     from IPython import version_info as IPython_version_info
-    if IPython_version_info[0]>=1:
+
+    if IPython_version_info[0] >= 1:
         from IPython.terminal.ipapp import TerminalIPythonApp
     else:
         from IPython.frontend.terminal.ipapp import TerminalIPythonApp
-    from gnucash.gnucash_business import Customer, Employee, Vendor, Job, \
-        Address, Invoice, Entry, TaxTable, TaxTableEntry, GNC_AMT_TYPE_PERCENT, \
-            GNC_DISC_PRETAX
+    from gnucash.gnucash_business import (
+        Customer,
+        Employee,
+        Vendor,
+        Job,
+        Address,
+        Invoice,
+        Entry,
+        TaxTable,
+        TaxTableEntry,
+        GNC_AMT_TYPE_PERCENT,
+        GNC_DISC_PRETAX,
+    )
     from gnucash import SessionOpenMode
     import locale
 except ImportError as import_error:
@@ -71,99 +82,102 @@ except ImportError as import_error:
     print(import_error)
     sys.exit(2)
 
+
 class Usage(Exception):
     def __init__(self, msg):
         self.msg = msg
 
-def invoice_to_lco(invoice):
-  """returns a string which forms a lco-file for use with LaTeX"""
-
-  lco_out=u"\ProvidesFile{data.lco}[]\n"
-
-  def write_variable(ukey, uvalue, replace_linebreak=True):
-
-    outstr = u""
-    if uvalue.endswith("\n"):
-        uvalue=uvalue[0:len(uvalue)-1]
-
-    if not ukey in [u"fromaddress",u"toaddress",u"date"]:
-        outstr += u'\\newkomavar{'
-        outstr += ukey
-        outstr += u"}\n"
-
-    outstr += u"\\setkomavar{"
-    outstr += ukey
-    outstr += u"}{"
-    if replace_linebreak:
-        outstr += uvalue.replace(u"\n",u"\\\\")+"}"
-    return outstr
-
-  # Write owners address
-  add_str=u""
-  owner = invoice.GetOwner()
-  if owner.GetName() != "":
-    add_str += owner.GetName().decode("UTF-8")+"\n"
-
-  addr  = owner.GetAddr()
-  if addr.GetName() != "":
-    add_str += addr.GetName().decode("UTF-8")+"\n"
-  if addr.GetAddr1() != "":
-    add_str += addr.GetAddr1().decode("UTF-8")+"\n"
-  if addr.GetAddr2() != "":
-    add_str += addr.GetAddr2().decode("UTF-8")+"\n"
-  if addr.GetAddr3() != "":
-    add_str += addr.GetAddr3().decode("UTF-8")+"\n"
-  if addr.GetAddr4() != "":
-    add_str += addr.GetAddr4().decode("UTF-8")+"\n"
-
-  lco_out += write_variable("toaddress2",add_str)
 
-  # Invoice number
-  inr_str = invoice.GetID()
-  lco_out += write_variable("rechnungsnummer",inr_str)
-
-  # date
-  date      = invoice.GetDatePosted()
-  udate     = date.strftime("%d.%m.%Y")
-  lco_out  += write_variable("date",udate)+"\n"
-
-  # date due
-  date_due  = invoice.GetDateDue()
-  udate_due = date_due.strftime("%d.%m.%Y")
-  lco_out  += write_variable("date_due",udate_due)+"\n"
-
-
-  # Write the entries
-  ent_str = u""
-  locale.setlocale(locale.LC_ALL,"de_DE")
-  for n,ent in enumerate(invoice.GetEntries()):
-
-      line_str = u""
-
-      if type(ent) != Entry:
-        ent=Entry(instance=ent)                                 # Add to method_returns_list
-
-      descr = ent.GetDescription()
-      price = ent.GetInvPrice().to_double()
-      n     = ent.GetQuantity()
+def invoice_to_lco(invoice):
+    """returns a string which forms a lco-file for use with LaTeX"""
 
-      uprice = locale.currency(price).rstrip(" EUR")
-      un = unicode(int(float(n.num())/n.denom()))               # choose best way to format numbers according to locale
+    lco_out = u"\ProvidesFile{data.lco}[]\n"
 
-      line_str =  u"\Artikel{"
-      line_str += un
-      line_str += u"}{"
-      line_str += descr.decode("UTF-8")
-      line_str += u"}{"
-      line_str += uprice
-      line_str += u"}"
+    def write_variable(ukey, uvalue, replace_linebreak=True):
 
-      #print(line_str)
-      ent_str += line_str
+        outstr = u""
+        if uvalue.endswith("\n"):
+            uvalue = uvalue[0 : len(uvalue) - 1]
 
-  lco_out += write_variable("entries",ent_str)
+        if not ukey in [u"fromaddress", u"toaddress", u"date"]:
+            outstr += u"\\newkomavar{"
+            outstr += ukey
+            outstr += u"}\n"
 
-  return lco_out
+        outstr += u"\\setkomavar{"
+        outstr += ukey
+        outstr += u"}{"
+        if replace_linebreak:
+            outstr += uvalue.replace(u"\n", u"\\\\") + "}"
+        return outstr
+
+    # Write owners address
+    add_str = u""
+    owner = invoice.GetOwner()
+    if owner.GetName() != "":
+        add_str += owner.GetName().decode("UTF-8") + "\n"
+
+    addr = owner.GetAddr()
+    if addr.GetName() != "":
+        add_str += addr.GetName().decode("UTF-8") + "\n"
+    if addr.GetAddr1() != "":
+        add_str += addr.GetAddr1().decode("UTF-8") + "\n"
+    if addr.GetAddr2() != "":
+        add_str += addr.GetAddr2().decode("UTF-8") + "\n"
+    if addr.GetAddr3() != "":
+        add_str += addr.GetAddr3().decode("UTF-8") + "\n"
+    if addr.GetAddr4() != "":
+        add_str += addr.GetAddr4().decode("UTF-8") + "\n"
+
+    lco_out += write_variable("toaddress2", add_str)
+
+    # Invoice number
+    inr_str = invoice.GetID()
+    lco_out += write_variable("rechnungsnummer", inr_str)
+
+    # date
+    date = invoice.GetDatePosted()
+    udate = date.strftime("%d.%m.%Y")
+    lco_out += write_variable("date", udate) + "\n"
+
+    # date due
+    date_due = invoice.GetDateDue()
+    udate_due = date_due.strftime("%d.%m.%Y")
+    lco_out += write_variable("date_due", udate_due) + "\n"
+
+    # Write the entries
+    ent_str = u""
+    locale.setlocale(locale.LC_ALL, "de_DE")
+    for n, ent in enumerate(invoice.GetEntries()):
+
+        line_str = u""
+
+        if type(ent) != Entry:
+            ent = Entry(instance=ent)  # Add to method_returns_list
+
+        descr = ent.GetDescription()
+        price = ent.GetInvPrice().to_double()
+        n = ent.GetQuantity()
+
+        uprice = locale.currency(price).rstrip(" EUR")
+        un = unicode(
+            int(float(n.num()) / n.denom())
+        )  # choose best way to format numbers according to locale
+
+        line_str = u"\Artikel{"
+        line_str += un
+        line_str += u"}{"
+        line_str += descr.decode("UTF-8")
+        line_str += u"}{"
+        line_str += uprice
+        line_str += u"}"
+
+        # print(line_str)
+        ent_str += line_str
+
+    lco_out += write_variable("entries", ent_str)
+
+    return lco_out
 
 
 def main(argv=None):
@@ -181,20 +195,20 @@ def main(argv=None):
         try:
             opts, args = getopt.getopt(argv[1:], "fhiln:po:", ["help"])
         except getopt.error as msg:
-             raise Usage(msg)
+            raise Usage(msg)
 
         for opt in opts:
             if opt[0] in ["-f"]:
                 print("ignoring lock")
                 ignore_lock = True
-            if opt[0] in ["-h","--help"]:
+            if opt[0] in ["-h", "--help"]:
                 raise Usage("Help:")
             if opt[0] in ["-i"]:
                 print("Using ipshell")
                 with_ipshell = True
             if opt[0] in ["-l"]:
                 print("listing all invoices")
-                list_invoices=True
+                list_invoices = True
             if opt[0] in ["-n"]:
                 invoice_number = int(opt[1])
                 print("using invoice number", invoice_number)
@@ -202,25 +216,25 @@ def main(argv=None):
             if opt[0] in ["-o"]:
                 output_file_name = opt[1]
                 print("using output file", output_file_name)
-        if len(args)>1:
-            print("opts:",opts,"args:",args)
+        if len(args) > 1:
+            print("opts:", opts, "args:", args)
             raise Usage("Only one input can be accepted !")
-        if len(args)==0:
+        if len(args) == 0:
             raise Usage("No input given !")
         input_url = args[0]
     except Usage as err:
         if err.msg == "Help:":
-            retcode=0
+            retcode = 0
         else:
             print("Error:", err.msg, file=sys.stderr)
             print("for help use --help", file=sys.stderr)
-            retcode=2
+            retcode = 2
 
         print("Generate a LaTeX invoice or print out all invoices.")
         print()
         print("Usage:")
         print()
-        print("Invoke with",prog_name,"input.")
+        print("Invoke with", prog_name, "input.")
         print("where input is")
         print("   filename")
         print("or file://filename")
@@ -253,39 +267,39 @@ def main(argv=None):
     comm_table = book.get_table()
     EUR = comm_table.lookup("CURRENCY", "EUR")
 
-    invoice_list=get_all_invoices(book)
+    invoice_list = get_all_invoices(book)
 
     if list_invoices:
-        for number,invoice in enumerate(invoice_list):
-            print(str(number)+")")
+        for number, invoice in enumerate(invoice_list):
+            print(str(number) + ")")
             print(invoice)
 
     if not (no_latex_output):
 
         if invoice_number == None:
             print("Using the first invoice:")
-            invoice_number=0
+            invoice_number = 0
 
-        invoice=invoice_list[invoice_number]
+        invoice = invoice_list[invoice_number]
         print("Using the following invoice:")
         print(invoice)
 
-        lco_str=invoice_to_lco(invoice)
+        lco_str = invoice_to_lco(invoice)
 
         # Opening output file
-        f=open(output_file_name,"w")
-        lco_str=lco_str.encode("latin1")
+        f = open(output_file_name, "w")
+        lco_str = lco_str.encode("latin1")
         f.write(lco_str)
         f.close()
 
     if with_ipshell:
         app = TerminalIPythonApp.instance()
-        app.initialize(argv=[]) # argv=[] instructs IPython to ignore sys.argv
+        app.initialize(argv=[])  # argv=[] instructs IPython to ignore sys.argv
         app.start()
 
-    #session.save()
+    # session.save()
     session.end()
 
+
 if __name__ == "__main__":
     sys.exit(main())
-

commit 485d8a65b0ad4d6e7e3e52de991fdf99135ff088
Author: c-holtermann <mail at c-holtermann.net>
Date:   Fri Jun 19 19:17:09 2020 +0200

    decorate Session.begin with default mode argument

diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py
index 360c71ced..f5647c012 100644
--- a/bindings/python/gnucash_core.py
+++ b/bindings/python/gnucash_core.py
@@ -627,7 +627,9 @@ Session.decorate_functions(one_arg_default_none, "load", "save")
 
 Session.decorate_functions( Session.raise_backend_errors_after_call,
                             "begin", "load", "save", "end")
+Session.decorate_method(default_arguments_decorator, "begin", None, mode=SessionOpenMode.SESSION_NORMAL_OPEN)
 Session.decorate_functions(deprecated_args_session_begin, "begin")
+
 Session.get_book = method_function_returns_instance(
     Session.get_book, Book )
 

commit 44e61f4df27c972c62218dea2069a43e92819ea5
Author: c-holtermann <mail at c-holtermann.net>
Date:   Fri Jun 19 19:15:51 2020 +0200

    enable Session.__init__() to be provided with existing instance or book

diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py
index 32c33bca3..360c71ced 100644
--- a/bindings/python/gnucash_core.py
+++ b/bindings/python/gnucash_core.py
@@ -210,7 +210,12 @@ class Session(GnuCashCoreClass):
         you don't need to cleanup and call end() and destroy(), that is handled
         for you, and the exception is raised.
         """
-        GnuCashCoreClass.__init__(self, Book())
+        if instance is not None:
+            GnuCashCoreClass.__init__(self, instance=instance)
+        else:
+            if book is None:
+                book = Book()
+            GnuCashCoreClass.__init__(self, book)
 
         if book_uri is not None:
             try:

commit 5833c5afcbce5a60bf65291bc52407be1508913a
Author: c-holtermann <mail at c-holtermann.net>
Date:   Fri Jun 19 18:24:33 2020 +0200

    add unittests for function_class
    
    add tests for some existing function_class functionality.
    Add tests for the keyword argument changes.

diff --git a/bindings/python/tests/CMakeLists.txt b/bindings/python/tests/CMakeLists.txt
index e118e5a6c..f583e1ce8 100644
--- a/bindings/python/tests/CMakeLists.txt
+++ b/bindings/python/tests/CMakeLists.txt
@@ -25,6 +25,7 @@ set(test_python_bindings_DATA
         test_session.py
         test_split.py
         test_transaction.py
-        test_query.py)
+        test_query.py
+        test_function_class.py)
 
 set_dist_list(test_python_bindings_DIST CMakeLists.txt ${test_python_bindings_DATA})
diff --git a/bindings/python/tests/runTests.py.in b/bindings/python/tests/runTests.py.in
index 8f88d8eb6..5b9142592 100755
--- a/bindings/python/tests/runTests.py.in
+++ b/bindings/python/tests/runTests.py.in
@@ -5,6 +5,7 @@ import os
 
 os.environ["GNC_UNINSTALLED"] = "1"
 
+from test_function_class import TestFunctionClass
 from test_gettext import TestGettext
 from test_session import TestSession
 from test_book import TestBook
diff --git a/bindings/python/tests/test_function_class.py b/bindings/python/tests/test_function_class.py
new file mode 100644
index 000000000..df099e086
--- /dev/null
+++ b/bindings/python/tests/test_function_class.py
@@ -0,0 +1,177 @@
+# test cases for function_class.py
+#
+# @date 2020-06-18
+# @author Christoph Holtermann <mail at c-holtermann.net>
+
+import sys
+from unittest import TestCase, main
+from gnucash.function_class import ClassFromFunctions, default_arguments_decorator
+
+
+class Instance:
+    """instance class for ClassFromFunction tests"""
+
+    pass
+
+
+def prefix_new_function():
+    """new function for ClassFromFunction tests
+    
+    returns instance of Instance class"""
+    return Instance()
+
+
+def prefix_test_function(self):
+    """test function for ClassFromFunction tests"""
+    return True
+
+
+def prefix_test_function_return_args(self, *args, **kargs):
+    return self, args, kargs
+
+
+b_default = "b default value"
+
+
+def prefix_test_function_return_arg_karg(self, a, b=b_default):
+    return {"self": self, "a": a, "b": b}
+
+
+def other_function(self, arg=None):
+    return self, arg
+
+
+class TestClass(ClassFromFunctions):
+    _module = sys.modules[__name__]
+
+    pass
+
+
+class TestFunctionClass(TestCase):
+    def test_add_constructor_and_methods_with_prefix(self):
+        TestClass.add_constructor_and_methods_with_prefix("prefix_", "new_function")
+        self.TestClass = TestClass
+        self.testClass = TestClass()
+        self.assertIsInstance(self.testClass.instance, Instance)
+        self.assertTrue(self.testClass.test_function())
+
+    def test_add_method(self):
+        """test if add_method adds method and if in case of FunctionClass
+        Instance instances get returned instead of FunctionClass instances"""
+        TestClass.add_method("other_function", "other_method")
+        self.t = TestClass()
+        obj, arg = self.t.other_method()
+        self.assertIsInstance(obj, Instance)
+        obj, arg = self.t.other_method(self.t)
+        self.assertIsInstance(arg, Instance)
+        obj, arg = self.t.other_method(arg=self.t)
+        self.assertIsInstance(arg, Instance)
+
+    def test_ya_add_method(self):
+        """test if ya_add_method adds method and if in case of FunctionClass
+        Instance instances get returned instead of FunctionClass instances
+        with the exception of self (first) argument"""
+        TestClass.ya_add_method("other_function", "other_method")
+        self.t = TestClass()
+        obj, arg = self.t.other_method()
+        self.assertIsInstance(obj, TestClass)
+        obj, arg = self.t.other_method(self.t)
+        self.assertIsInstance(arg, Instance)
+        obj, arg = self.t.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
+        TestClass.backup_test_function_return_arg_karg = (
+            TestClass.test_function_return_arg_karg
+        )
+        self.t = TestClass()
+
+        arg1 = "arg1"
+        arg2 = "arg2"
+        arg3 = {"arg3": arg2}
+        arg4 = 4
+        TestClass.decorate_method(
+            default_arguments_decorator, "test_function_return_args", arg1, arg2
+        )
+        self.assertEqual(
+            self.t.test_function_return_args(), (self.t.instance, (arg2,), {})
+        )  # default arg1 gets overwritten by class instances instance attribute
+        self.assertEqual(
+            self.t.test_function_return_args(arg3), (self.t.instance, (arg3,), {})
+        )
+        self.assertEqual(
+            self.t.test_function_return_args(arg1, arg3),
+            (self.t.instance, (arg1, arg3), {}),
+        )
+        self.assertEqual(
+            self.t.test_function_return_args(arg1, arg3, arg4=arg4),
+            (self.t.instance, (arg1, arg3), {"arg4": arg4}),
+        )
+
+        TestClass.test_function_return_args = TestClass.backup_test_function_return_args
+        TestClass.decorate_method(
+            default_arguments_decorator,
+            "test_function_return_args",
+            arg1,
+            arg2,
+            arg4=arg4,
+        )
+        self.assertEqual(
+            self.t.test_function_return_args(),
+            (self.t.instance, (arg2,), {"arg4": arg4}),
+        )
+        self.assertEqual(
+            self.t.test_function_return_args(arg1, arg3, arg4=arg2),
+            (self.t.instance, (arg1, arg3), {"arg4": arg2}),
+        )
+
+        with self.assertRaises(TypeError):
+            # should fail because a is set both as a positional and as a keyword argument
+            TestClass.decorate_method(
+                default_arguments_decorator,
+                "test_function_return_arg_karg",
+                None,
+                arg1,
+                a=arg2,
+                kargs_pos={"a": 1, "b": 2},
+            )
+        TestClass.decorate_method(
+            default_arguments_decorator,
+            "test_function_return_arg_karg",
+            None,
+            a=arg1,
+            kargs_pos={"a": 1, "b": 2},
+        )
+        self.assertEqual(
+            self.t.test_function_return_arg_karg(),
+            {"self": self.t.instance, "a": arg1, "b": b_default},
+        )
+
+        TestClass.test_function_return_arg_karg = (
+            TestClass.backup_test_function_return_arg_karg
+        )
+        TestClass.decorate_method(
+            default_arguments_decorator,
+            "test_function_return_arg_karg",
+            None,
+            arg1,
+            kargs_pos={"a": 1, "b": 2},
+        )
+        self.assertEqual(
+            self.t.test_function_return_arg_karg(),
+            {"self": self.t.instance, "a": arg1, "b": b_default},
+        )
+        self.assertEqual(
+            self.t.test_function_return_arg_karg(arg2),
+            {"self": self.t.instance, "a": arg2, "b": b_default},
+        )
+        self.assertEqual(
+            self.t.test_function_return_arg_karg(arg2, arg3),
+            {"self": self.t.instance, "a": arg2, "b": arg3},
+        )
+
+
+if __name__ == "__main__":
+    main()

commit 17d606e1f80915fd201606eac9d1f67c5ad0d536
Author: c-holtermann <mail at c-holtermann.net>
Date:   Fri Jun 19 19:14:20 2020 +0200

    enable keyword arguments for default_arguments_decorator
    
    default_arguments_decorator until now only allows positional
    argument defaults. This adds keyword defaults. The keywords
    can be mapped to the positional arguments by optional argument
    kargs_pos so interactions between keyword and positional arg
    defaults can raise a TypeError. Some more information in
    the docstring is included. In addition the docstring of
    the wrapped function will be modified to contain information
    about the defaults.

diff --git a/bindings/python/function_class.py b/bindings/python/function_class.py
index 31559f6c1..5ed9f82f8 100644
--- a/bindings/python/function_class.py
+++ b/bindings/python/function_class.py
@@ -252,18 +252,93 @@ def methods_return_instance_lists(cls, function_dict):
                 method_function_returns_instance_list(
                 getattr(cls, func_name), instance_name))
 
-def default_arguments_decorator(function, *args):
-    """Decorates a function to give it default, positional arguments
+def default_arguments_decorator(function, *args, **kargs):
+    """! Decorates a function to give it default, positional and keyword arguments
+
+    mimics python behavior when setting defaults in function/method arguments.
+    arguments can be set for positional or keyword arguments.
+
+    kargs_pos contains positions of the keyword arguments.
+    @exception A TypeError will be raised if an argument is set as a positional and keyword argument
+    at the same time.
+    @note It might be possible to get keyword argument positional information using
+    introspection to avoid having to specify them manually
+
+    a keyword argument default will be overwritten by a positional argument at the
+    actual function call
+
+    this function modifies the docstring of the wrapped funtion to reflect
+    the defaults.
 
     You can't use this decorator with @, because this function has more
     than one argument.
+
+    arguments:
+    @param *args: optional positional defaults
+    @param kargs_pos: dict with keyword arguments as key and their position in the argument list as value
+    @param **kargs: optional keyword defaults
+
+    @return new_function wrapping original function
     """
-    def new_function(*function_args):
+
+    def new_function(*function_args, **function_kargs):
+        kargs_pos = {}
+        if "kargs_pos" in kargs:
+            kargs_pos = kargs.pop("kargs_pos")
         new_argset = list(function_args)
-        new_argset.extend( args[ len(function_args): ] )
-        return function( *new_argset )
+        new_argset.extend(args[len(function_args) :])
+        new_kargset = {**kargs, **function_kargs}
+        for karg_pos in kargs_pos:
+            if karg_pos in new_kargset:
+                pos_karg = kargs_pos[karg_pos]
+                if pos_karg < len(new_argset):
+                    new_kargset.pop(karg_pos)
+
+        return function(*new_argset, **new_kargset)
+
+    kargs_pos = {} if "kargs_pos" not in kargs else kargs["kargs_pos"]
+    for karg_pos in kargs_pos:
+        if karg_pos in kargs:
+            pos_karg = kargs_pos[karg_pos]
+            if pos_karg < len(args):
+                raise TypeError(
+                    "default_arguments_decorator() got multiple values for argument '%s'"
+                    % karg_pos
+                )
+
+    if new_function.__doc__ is None:
+        new_function.__doc__ = ""
+    if len(args):
+        firstarg = True
+        new_function.__doc__ += "positional argument defaults:\n"
+        for arg in args:
+            if not firstarg:
+                new_function.__doc__ += ", "
+            else:
+                new_function.__doc__ += "  "
+                firstarg = False
+            new_function.__doc__ += str(arg)
+        new_function.__doc__ += "\n"
+    if len(kargs):
+        new_function.__doc__ += "keyword argument defaults:\n"
+        for karg in kargs:
+            if karg != "kargs_pos":
+                new_function.__doc__ += (
+                    "  " + str(karg) + " = " + str(kargs[karg]) + "\n"
+                )
+        if kargs_pos:
+            new_function.__doc__ += "keyword argument positions:\n"
+            for karg in kargs_pos:
+                new_function.__doc__ += (
+                    "  " + str(karg) + " is at pos " + str(kargs_pos[karg]) + "\n"
+                )
+    if len(args) or len(kargs):
+        new_function.__doc__ += (
+            "(defaults have been set by default_arguments_decorator method)"
+        )
     return new_function
 
+
 def return_instance_if_value_has_it(value):
     """Return value.instance if value is an instance of ClassFromFunctions,
     else return value

commit c222503f42fd47ef973b0cfd49457de96f12a694
Author: c-holtermann <mail at c-holtermann.net>
Date:   Fri Jun 19 19:08:00 2020 +0200

    add method decorate_method to function_class.py
    
    ClassFromFunctions.decorate_method() allows to provide positional
    and keyword arguments for the decorator call besides the wrapped
    method.

diff --git a/bindings/python/function_class.py b/bindings/python/function_class.py
index fc99cc80f..31559f6c1 100644
--- a/bindings/python/function_class.py
+++ b/bindings/python/function_class.py
@@ -207,6 +207,22 @@ class ClassFromFunctions(object):
             setattr( cls, function_name,
                      decorator( getattr(cls, function_name) ) )
 
+    @classmethod
+    def decorate_method(cls, decorator, method_name, *args, **kargs):
+        """! decorate method method_name of class cls with decorator decorator
+
+        in difference to decorate_functions() this allows to provide additional
+        arguments for the decorator function.
+
+        arguments:
+            @param cls: class
+            @param decorator: function to decorate method
+            @param method_name: name of method to decorate (string)
+            @param *args: positional arguments for decorator
+            @param **kargs: keyword arguments for decorator"""
+        setattr(cls, method_name,
+                    decorator(getattr(cls, method_name), *args, **kargs))
+
 def method_function_returns_instance(method_function, cls):
     """A function decorator that is used to decorate method functions that
     return instance data, to return instances instead.

commit ee77b713c235e8eb0ee73710bdb35f4918e363a6
Author: c-holtermann <mail at c-holtermann.net>
Date:   Fri Jun 12 12:24:05 2020 +0200

    update example scripts to SessionOpenMode

diff --git a/bindings/python/example_scripts/account_analysis.py b/bindings/python/example_scripts/account_analysis.py
index 135d2e60e..fe24a2e21 100644
--- a/bindings/python/example_scripts/account_analysis.py
+++ b/bindings/python/example_scripts/account_analysis.py
@@ -35,7 +35,7 @@ from math import log10
 import csv
 
 # gnucash imports
-from gnucash import Session, GncNumeric, Split
+from gnucash import Session, GncNumeric, Split, SessionOpenMode
 
 # Invoke this script like the following example
 # $ python3 account_analysis.py gnucash_file.gnucash \
@@ -173,7 +173,7 @@ def main():
 
         account_path = argv[8:]
 
-        gnucash_session = Session(gnucash_file, is_new=False)
+        gnucash_session = Session(gnucash_file, SessionOpenMode.SESSION_NORMAL_OPEN)
         root_account = gnucash_session.book.get_root_account()
         account_of_interest = account_from_path(root_account, account_path)
 
diff --git a/bindings/python/example_scripts/gncinvoice_jinja.py b/bindings/python/example_scripts/gncinvoice_jinja.py
index f44f308ab..bfa7c09a5 100755
--- a/bindings/python/example_scripts/gncinvoice_jinja.py
+++ b/bindings/python/example_scripts/gncinvoice_jinja.py
@@ -39,6 +39,7 @@ try:
     import str_methods
     import jinja2
     from gncinvoicefkt import *
+    from gnucash import SessionOpenMode
 except ImportError as import_error:
     print("Problem importing modules.")
     print(import_error)
@@ -137,7 +138,7 @@ def main(argv=None):
         print("or file://filename")
         print("or mysql://user:password@host/databasename")
         print()
-        print("-f             force open = ignore lock")
+        print("-f             force open = ignore lock (read only)")
         print("-l             list all invoices")
         print("-h or --help   for this help")
         print("-I ID          use invoice ID")
@@ -150,8 +151,15 @@ def main(argv=None):
 
     # Try to open the given input
     try:
-        print("Opening", input_url, ".")
-        session = gnucash.Session(input_url, ignore_lock=ignore_lock)
+        print(
+            "Opening", input_url, " (ignore-lock = read-only)." if ignore_lock else "."
+        )
+        session = gnucash.Session(
+            input_url,
+            SessionOpenMode.SESSION_READ_ONLY
+            if ignore_lock
+            else SessionOpenMode.SESSION_NORMAL_OPEN,
+        )
     except Exception as exception:
         print("Problem opening input.")
         print(exception)
diff --git a/bindings/python/example_scripts/latex_invoices.py b/bindings/python/example_scripts/latex_invoices.py
index 2937c980a..c29922e05 100644
--- a/bindings/python/example_scripts/latex_invoices.py
+++ b/bindings/python/example_scripts/latex_invoices.py
@@ -64,6 +64,7 @@ try:
     from gnucash.gnucash_business import Customer, Employee, Vendor, Job, \
         Address, Invoice, Entry, TaxTable, TaxTableEntry, GNC_AMT_TYPE_PERCENT, \
             GNC_DISC_PRETAX
+    from gnucash import SessionOpenMode
     import locale
 except ImportError as import_error:
     print("Problem importing modules.")
@@ -236,7 +237,12 @@ def main(argv=None):
 
     # Try to open the given input
     try:
-        session = gnucash.Session(input_url,ignore_lock=ignore_lock)
+        session = gnucash.Session(
+            input_url,
+            SessionOpenMode.SESSION_READ_ONLY
+            if ignore_lock
+            else SessionOpenMode.SESSION_NORMAL_OPEN,
+        )
     except Exception as exception:
         print("Problem opening input.")
         print(exception)
diff --git a/bindings/python/example_scripts/new_book_with_opening_balances.py b/bindings/python/example_scripts/new_book_with_opening_balances.py
index df2d29ae7..3a2d04e7b 100644
--- a/bindings/python/example_scripts/new_book_with_opening_balances.py
+++ b/bindings/python/example_scripts/new_book_with_opening_balances.py
@@ -28,7 +28,8 @@
 #   @author Mark Jenkins, ParIT Worker Co-operative <mark at parit.ca>
 #   @ingroup python_bindings_examples
 
-from gnucash import Session, Account, Transaction, Split, GncNumeric
+from gnucash import (
+        Session, Account, Transaction, Split, GncNumeric, SessionOpenMode)
 from gnucash.gnucash_core_c import \
     GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT, \
     ACCT_TYPE_ASSET, ACCT_TYPE_BANK, ACCT_TYPE_CASH, ACCT_TYPE_CHECKING, \
@@ -299,8 +300,8 @@ def main():
 
     #have everything in a try block to unable us to release our hold on stuff to the extent possible
     try:
-        original_book_session = Session(argv[1], is_new=False)
-        new_book_session = Session(argv[2], is_new=True)
+        original_book_session = Session(argv[1], SessionOpenMode.SESSION_NORMAL_OPEN)
+        new_book_session = Session(argv[2], SessionOpenMode.SESSION_NEW_STORE)
         new_book = new_book_session.get_book()
         new_book_root = new_book.get_root_account()
 
diff --git a/bindings/python/example_scripts/rest-api/gnucash_rest.py b/bindings/python/example_scripts/rest-api/gnucash_rest.py
index 51c75eb09..533447139 100644
--- a/bindings/python/example_scripts/rest-api/gnucash_rest.py
+++ b/bindings/python/example_scripts/rest-api/gnucash_rest.py
@@ -68,6 +68,8 @@ from gnucash import \
 from gnucash import \
     INVOICE_IS_PAID
 
+from gnucash import SessionOpenMode
+
 app = Flask(__name__)
 app.debug = True
 
@@ -1884,7 +1886,7 @@ for option, value in options:
 
 #start gnucash session base on connection string argument
 if is_new:
-    session = gnucash.Session(arguments[0], is_new=True)
+    session = gnucash.Session(arguments[0], SessionOpenMode.SESSION_NEW_STORE)
 
     # seem to get errors if we use the session directly, so save it and
     #destroy it so it's no longer new
@@ -1893,7 +1895,8 @@ if is_new:
     session.end()
     session.destroy()
 
-session = gnucash.Session(arguments[0], ignore_lock=True)
+# unsure about SESSION_BREAK_LOCK - it used to be ignore_lock=True 
+session = gnucash.Session(arguments[0], SessionOpenMode.SESSION_BREAK_LOCK)
 
 # register method to close gnucash connection gracefully
 atexit.register(shutdown)
diff --git a/bindings/python/example_scripts/simple_book.py b/bindings/python/example_scripts/simple_book.py
index 19ebb0186..0059ff64e 100644
--- a/bindings/python/example_scripts/simple_book.py
+++ b/bindings/python/example_scripts/simple_book.py
@@ -5,13 +5,13 @@
 #   @ingroup python_bindings_examples
 
 import sys
-from gnucash import Session
+from gnucash import Session, SessionOpenMode
 
 # We need to tell GnuCash the data format to create the new file as (xml://)
 uri = "xml:///tmp/simple_book.gnucash"
 
 print("uri:", uri)
-with Session(uri, is_new=True) as ses:
+with Session(uri, SessionOpenMode.SESSION_NEW_STORE) as ses:
     book = ses.get_book()
 
     #Call some methods that produce output to show that Book works
diff --git a/bindings/python/example_scripts/simple_business_create.py b/bindings/python/example_scripts/simple_business_create.py
index bb00846df..e0df30bb8 100644
--- a/bindings/python/example_scripts/simple_business_create.py
+++ b/bindings/python/example_scripts/simple_business_create.py
@@ -53,7 +53,7 @@ from os.path import abspath
 from sys import argv, exit
 import datetime
 from datetime import timedelta
-from gnucash import Session, Account, GncNumeric
+from gnucash import Session, Account, GncNumeric, SessionOpenMode
 from gnucash.gnucash_business import Customer, Employee, Vendor, Job, \
     Address, Invoice, Entry, TaxTable, TaxTableEntry, GNC_AMT_TYPE_PERCENT, \
     GNC_DISC_PRETAX    
@@ -70,7 +70,7 @@ if len(argv) < 2:
     
 
 try:
-    s = Session(argv[1], is_new=True)
+    s = Session(argv[1], SessionOpenMode.SESSION_NEW_STORE)
 
     book = s.book
     root = book.get_root_account()
diff --git a/bindings/python/example_scripts/simple_invoice_insert.py b/bindings/python/example_scripts/simple_invoice_insert.py
index eef4c03ba..ee1bbfb06 100644
--- a/bindings/python/example_scripts/simple_invoice_insert.py
+++ b/bindings/python/example_scripts/simple_invoice_insert.py
@@ -46,7 +46,7 @@
 #   @author Mark Jenkins, ParIT Worker Co-operative <mark at parit.ca>
 #   @ingroup python_bindings_examples
 
-from gnucash import Session, GUID, GncNumeric
+from gnucash import Session, GUID, GncNumeric, SessionOpenMode
 from gnucash.gnucash_business import Customer, Invoice, Entry
 from gnucash.gnucash_core_c import string_to_guid
 from os.path import abspath
@@ -86,7 +86,7 @@ def gnc_numeric_from_decimal(decimal_value):
     return GncNumeric(numerator, denominator)
 
 
-s = Session(argv[1], is_new=False)
+s = Session(argv[1], SessionOpenMode.SESSION_NORMAL_OPEN)
 
 book = s.book
 root = book.get_root_account()
diff --git a/bindings/python/example_scripts/simple_session.py b/bindings/python/example_scripts/simple_session.py
index 05da9487b..daebfdf64 100644
--- a/bindings/python/example_scripts/simple_session.py
+++ b/bindings/python/example_scripts/simple_session.py
@@ -3,9 +3,11 @@
 #   @brief Example Script simple session
 #   @ingroup python_bindings_examples
 
-from gnucash import \
-     Session, GnuCashBackendException, \
+from gnucash import (
+     Session, GnuCashBackendException,
+     SessionOpenMode,
      ERR_BACKEND_LOCKED, ERR_FILEIO_FILE_NOT_FOUND
+)
 
 FILE_1 = "/tmp/not_there.xac"
 FILE_2 = "/tmp/example_file.xac"
@@ -19,7 +21,7 @@ except GnuCashBackendException as backend_exception:
 
 
 # create a new file, this requires a file type specification
-with Session("xml://%s" % FILE_2, is_new=True) as session:
+with Session("xml://%s" % FILE_2, SessionOpenMode.SESSION_NEW_STORE) as session:
     book = session.book
     root = book.get_root_account()
 
diff --git a/bindings/python/example_scripts/simple_sqlite_create.py b/bindings/python/example_scripts/simple_sqlite_create.py
index 7900c9534..61675be08 100644
--- a/bindings/python/example_scripts/simple_sqlite_create.py
+++ b/bindings/python/example_scripts/simple_sqlite_create.py
@@ -3,11 +3,11 @@
 #   @brief Example Script simple sqlite create 
 #   @ingroup python_bindings_examples
 
-from gnucash import Session, Account
+from gnucash import Session, Account, SessionOpenMode
 from os.path import abspath
 from gnucash.gnucash_core_c import ACCT_TYPE_ASSET
 
-s = Session('sqlite3://%s' % abspath('test.blob'), is_new=True)
+s = Session('sqlite3://%s' % abspath('test.blob'), SessionOpenMode.SESSION_NEW_STORE)
 # this seems to make a difference in more complex cases
 s.save()
 
diff --git a/bindings/python/example_scripts/simple_test.py b/bindings/python/example_scripts/simple_test.py
index b8a1bed02..7101823df 100644
--- a/bindings/python/example_scripts/simple_test.py
+++ b/bindings/python/example_scripts/simple_test.py
@@ -3,11 +3,12 @@
 # @brief Creates a basic set of accounts and a couple of transactions
 # @ingroup python_bindings_examples
 
-from gnucash import Session, Account, Transaction, Split, GncNumeric
+from gnucash import (
+        Session, Account, Transaction, Split, GncNumeric, SessionOpenMode)
 
 FILE_1 = "/tmp/example.gnucash"
 
-with Session("xml://%s" % FILE_1, is_new=True) as session:
+with Session("xml://%s" % FILE_1, SessionOpenMode.SESSION_NEW_STORE) as session:
 
     book = session.book
     root_acct = Account(book)
@@ -80,4 +81,4 @@ with Session("xml://%s" % FILE_1, is_new=True) as session:
 
 
     trans1.CommitEdit()
-    trans2.CommitEdit()
\ No newline at end of file
+    trans2.CommitEdit()

commit b073dbc5c323f88363e97231afc37fab017dfa8c
Author: c-holtermann <mail at c-holtermann.net>
Date:   Thu Jun 11 21:11:06 2020 +0200

    allow keyword arguments for function_class.py
    
    allow keyword arguments for function_class methods
    and functions. process_dict_convert_to_instance() is added to
    mimic the behavior of the process_list_convert_to_instance()
    Derived methods in gnucash_core.py like raise_backend_errors_after_call
    get modified to accept being called with keyword args.
    Also adds some docstrings.

diff --git a/bindings/python/function_class.py b/bindings/python/function_class.py
index 81bebb049..fc99cc80f 100644
--- a/bindings/python/function_class.py
+++ b/bindings/python/function_class.py
@@ -71,7 +71,8 @@ class ClassFromFunctions(object):
             self.__instance = kargs[INSTANCE_ARGUMENT]
         else:
             self.__instance = getattr(self._module, self._new_instance)(
-                *process_list_convert_to_instance(args) )
+                *process_list_convert_to_instance(args),
+                **process_dict_convert_to_instance(kargs))
 
     def get_instance(self):
         """Get the instance data.
@@ -86,12 +87,29 @@ 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
-        """
-        def method_function(self, *meth_func_args):
+        """! Add the function, method_name to this class as a method named name
+
+        arguments:
+        @param cls Class: class to add methods to
+        @param function_name string: name of the function to add
+        @param method_name string: name of the method that function will be called
+
+        function will be wrapped by method_function"""
+
+        def method_function(self, *meth_func_args, **meth_func_kargs):
+            """! wrapper method for function
+
+            arguments:
+            @param self: FunctionClass instance. Will be turned to its instance property.
+            @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.instance,
-                *process_list_convert_to_instance(meth_func_args) )
+                *process_list_convert_to_instance(meth_func_args),
+                **process_dict_convert_to_instance(meth_func_kargs)
+            )
 
         setattr(cls, method_name, method_function)
         setattr(method_function, "__name__", method_name)
@@ -99,14 +117,32 @@ class ClassFromFunctions(object):
 
     @classmethod
     def ya_add_classmethod(cls, function_name, method_name):
-        """Add the function, method_name to this class as a classmethod named name
+        """! Add the function, method_name to this class as a classmethod named name
 
-        Taken from function_class and slightly modified.
-        """
-        def method_function(self, *meth_func_args):
+        Taken from function_class and modified from add_method() to add classmethod
+        instead of method and not to turn self argument to self.instance.
+
+        arguments:
+        @param cls Class: class to add methods to
+        @param function_name string: name of the function to add
+        @param method_name string: name of the classmethod that function will be called
+
+        function will be wrapped by method_function"""
+
+        def method_function(self, *meth_func_args, **meth_func_kargs):
+            """! wrapper method for function
+
+            arguments:
+            @param self: FunctionClass instance.
+            @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,
-                *process_list_convert_to_instance(meth_func_args) )
+                *process_list_convert_to_instance(meth_func_args),
+                **process_dict_convert_to_instance(meth_func_kargs)
+            )
 
         setattr(cls, method_name, classmethod(method_function))
         setattr(method_function, "__name__", method_name)
@@ -114,14 +150,32 @@ class ClassFromFunctions(object):
 
     @classmethod
     def ya_add_method(cls, function_name, method_name):
-        """Add the function, method_name to this class as a method named name
+        """! Add the function, method_name to this class as a method named name
 
-        Taken from function_class and slightly modified.
-        """
-        def method_function(self, *meth_func_args):
+        Taken from function_class. Modified to not turn self to self.instance
+        as add_method() does.
+
+        arguments:
+        @param cls Class: class to add methods to
+        @param function_name string: name of the function to add
+        @param method_name string: name of the method that function will be called
+
+        function will be wrapped by method_function"""
+
+        def method_function(self, *meth_func_args, **meth_func_kargs):
+            """! wrapper method for function
+
+            arguments:
+            @param self: FunctionClass instance.
+            @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,
-                *process_list_convert_to_instance(meth_func_args) )
+                *process_list_convert_to_instance(meth_func_args),
+                **process_dict_convert_to_instance(meth_func_kargs)
+            )
 
         setattr(cls, method_name, method_function)
         setattr(method_function, "__name__", method_name)
@@ -161,19 +215,19 @@ def method_function_returns_instance(method_function, cls):
     argument.
     """
     assert( 'instance' == INSTANCE_ARGUMENT )
-    def new_function(*args):
-        kargs = { INSTANCE_ARGUMENT : method_function(*args) }
-        if kargs['instance'] == None:
+    def new_function(*args, **kargs):
+        kargs_cls = { INSTANCE_ARGUMENT : method_function(*args, **kargs) }
+        if kargs_cls['instance'] == None:
             return None
         else:
-            return cls( **kargs )
+            return cls( **kargs_cls )
 
     return new_function
 
 def method_function_returns_instance_list(method_function, cls):
-    def new_function(*args):
+    def new_function(*args, **kargs):
         return [ cls( **{INSTANCE_ARGUMENT: item} )
-                 for item in method_function(*args) ]
+                 for item in method_function(*args, **kargs) ]
     return new_function
 
 def methods_return_instance_lists(cls, function_dict):
@@ -213,6 +267,18 @@ def process_list_convert_to_instance( value_list ):
     return [ return_instance_if_value_has_it(value)
              for value in value_list ]
 
+def process_dict_convert_to_instance(value_dict):
+    """Return a dict built from value_dict, where if a value is in an instance
+    of ClassFromFunctions, we put value.instance in the dict instead.
+
+    Things that are not instances of ClassFromFunctions are returned to
+    the new dict unchanged.
+    """
+    return {
+        key: return_instance_if_value_has_it(value) for key, value in value_dict.items()
+    }
+
+
 def extract_attributes_with_prefix(obj, prefix):
     """Generator that iterates through the attributes of an object and
     for any attribute that matches a prefix, this yields
diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py
index fa116b1d7..32c33bca3 100644
--- a/bindings/python/gnucash_core.py
+++ b/bindings/python/gnucash_core.py
@@ -266,12 +266,12 @@ class Session(GnuCashCoreClass):
 
     # STATIC METHODS
     @staticmethod
-    def raise_backend_errors_after_call(function):
+    def raise_backend_errors_after_call(function, *args, **kwargs):
         """A function decorator that results in a call to
         raise_backend_errors after execution.
         """
-        def new_function(self, *args):
-            return_value = function(self, *args)
+        def new_function(self, *args, **kwargs):
+            return_value = function(self, *args, **kwargs)
             self.raise_backend_errors(function.__name__)
             return return_value
         return new_function

commit 4e280b959349e420e34ca93938982fb4e39481bd
Author: c-holtermann <mail at c-holtermann.net>
Date:   Tue Jun 9 22:41:20 2020 +0200

    adapt to use of sessionOpenMode in qof_session_begin

diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py
index 5dbf68383..fa116b1d7 100644
--- a/bindings/python/gnucash_core.py
+++ b/bindings/python/gnucash_core.py
@@ -48,6 +48,12 @@ from gnucash.gnucash_core_c import gncInvoiceLookup, gncInvoiceGetInvoiceFromTxn
     gnc_numeric_create, double_to_gnc_numeric, string_to_gnc_numeric, \
     gnc_numeric_to_string
 
+from gnucash.deprecation import (
+    deprecated_args_session,
+    deprecated_args_session_init,
+    deprecated_args_session_begin
+)
+
 try:
     import gettext
 
@@ -148,44 +154,75 @@ class Session(GnuCashCoreClass):
     Invoice..) is associated with a particular book where it is stored.
     """
 
-    def __init__(self, book_uri=None, ignore_lock=False, is_new=False,
-                 force_new=False, instance=None):
-        """A convenient constructor that allows you to specify a book URI,
+    @deprecated_args_session_init
+    def __init__(self, book_uri=None, mode=None, instance=None, book=None):
+        """!
+        A convenient constructor that allows you to specify a book URI,
         begin the session, and load the book.
 
         This can give you the power of calling
         qof_session_new, qof_session_begin, and qof_session_load all in one!
 
-        book_uri can be None to skip the calls to qof_session_begin and
-        qof_session_load, or it can be a string like "file:/test.xac"
-
-        qof_session_load is only called if is_new is set to False
-
-        is_new is passed to qof_session_begin as the argument create,
-        and force_new as the argument force. Is_new will create a new
-        database or file; force will force creation even if it will
-        destroy an existing dataset.
+        qof_session_load is only called if url scheme is "xml" and
+        mode is SESSION_NEW_STORE or SESSION_NEW_OVERWRITE
 
-        ignore_lock is passed to qof_session_begin's argument of the
-        same name and is used to break an existing lock on a dataset.
+        @param book_uri must be a string in the form of a URI/URL. The access
+        method specified depends on the loaded backends. Paths may be relative
+        or absolute.  If the path is relative, that is if the argument is
+        "file://somefile.xml", then the current working directory is
+        assumed. Customized backends can choose to search other
+        application-specific directories or URI schemes as well.
+        It be None to skip the calls to qof_session_begin and
+        qof_session_load.
 
-        instance argument can be passed if new Session is used as a
+        @param instance argument can be passed if new Session is used as a
         wrapper for an existing session instance
 
-
-        This function can raise a GnuCashBackendException. If it does,
+        @param mode The SessionOpenMode.
+        @note SessionOpenMode replaces deprecated ignore_lock, is_new and force_new.
+
+        @par SessionOpenMode
+        `SESSION_NORMAL`: Find an existing file or database at the provided uri and
+        open it if it is unlocked. If it is locked post a QOF_BACKEND_LOCKED error.
+        @par
+        `SESSION_NEW_STORE`: Check for an existing file or database at the provided
+        uri and if none is found, create it. If the file or database exists post a
+        QOF_BACKED_STORE_EXISTS and return.
+        @par
+        `SESSION_READ_ONLY`: Find an existing file or database and open it without
+        disturbing the lock if it exists or setting one if not. This will also set a
+        flag on the book that will prevent many elements from being edited and will
+        prevent the backend from saving any edits.
+        @par
+        `SESSION_OVERWRITE`: Create a new file or database at the provided uri,
+        deleting any existing file or database.
+        @par
+        `SESSION_BREAK_LOCK`: Find an existing file or database, lock it, and open
+        it. If there is already a lock replace it with a new one for this session.
+
+        @par Errors
+        qof_session_begin() signals failure by queuing errors. After it completes use
+        qof_session_get_error() and test that the value is `ERROR_BACKEND_NONE` to
+        determine that the session began successfully.
+
+        @exception as begin() and load() are wrapped with raise_backend_errors_after_call()
+        this function can raise a GnuCashBackendException. If it does,
         you don't need to cleanup and call end() and destroy(), that is handled
         for you, and the exception is raised.
         """
         GnuCashCoreClass.__init__(self, Book())
+
         if book_uri is not None:
             try:
-                self.begin(book_uri, ignore_lock, is_new, force_new)
+                if mode is None:
+                    mode = SessionOpenMode.SESSION_NORMAL_OPEN
+                self.begin(book_uri, mode)
                 # Take care of backend inconsistency
                 # New xml file can't be loaded, new sql store
                 # has to be loaded before it can be altered
                 # Any existing store obviously has to be loaded
                 # More background: https://bugs.gnucash.org/show_bug.cgi?id=726891
+                is_new = mode in (SessionOpenMode.SESSION_NEW_STORE, SessionOpenMode.SESSION_NEW_OVERWRITE)
                 if book_uri[:3] != "xml" or not is_new:
                     self.load()
             except GnuCashBackendException as backend_exception:
@@ -585,6 +622,7 @@ Session.decorate_functions(one_arg_default_none, "load", "save")
 
 Session.decorate_functions( Session.raise_backend_errors_after_call,
                             "begin", "load", "save", "end")
+Session.decorate_functions(deprecated_args_session_begin, "begin")
 Session.get_book = method_function_returns_instance(
     Session.get_book, Book )
 

commit 48072f5a4c957e5ef4a168f8774006872cd4acf7
Author: c-holtermann <mail at c-holtermann.net>
Date:   Thu Jun 11 17:50:49 2020 +0200

    make SessionOpenMode enum available for python

diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py
index b82c5a53b..5dbf68383 100644
--- a/bindings/python/gnucash_core.py
+++ b/bindings/python/gnucash_core.py
@@ -28,6 +28,7 @@
 #  @author Jeff Green,   ParIT Worker Co-operative <jeff at parit.ca>
 #  @ingroup python_bindings
 
+from enum import IntEnum
 from gnucash import gnucash_core_c
 from gnucash import _sw_core_utils
 
@@ -83,6 +84,57 @@ class GnuCashBackendException(Exception):
         Exception.__init__(self, msg)
         self.errors = errors
 
+
+class SessionOpenMode(IntEnum):
+    """Mode for opening sessions.
+
+    This replaces three booleans that were passed in order: ignore_lock, create,
+    and force. It's structured so that one can use it as a bit field with the
+    values in the same order, i.e. ignore_lock = 1 << 2, create_new = 1 << 1, and
+    force_new = 1.
+
+    enumeration members
+    -------------------
+
+    SESSION_NORMAL_OPEN = 0     (All False)
+    Open will fail if the URI doesn't exist or is locked.
+
+    SESSION_NEW_STORE = 2       (False, True, False (create))
+    Create a new store at the URI. It will fail if the store already exists and is found to contain data that would be overwritten.
+
+    SESSION_NEW_OVERWRITE = 3   (False, True, True (create | force))
+    Create a new store at the URI even if a store already exists there.
+
+    SESSION_READ_ONLY = 4,      (True, False, False (ignore_lock))
+    Open the session read-only, ignoring any existing lock and not creating one if the URI isn't locked.
+
+    SESSION_BREAK_LOCK = 5     (True, False, True (ignore_lock | force))
+    Open the session, taking over any existing lock.
+
+    source: lignucash/engine/qofsession.h
+    """
+
+    SESSION_NORMAL_OPEN = gnucash_core_c.SESSION_NORMAL_OPEN
+    """All False
+    Open will fail if the URI doesn't exist or is locked."""
+
+    SESSION_NEW_STORE = gnucash_core_c.SESSION_NEW_STORE
+    """False, True, False (create)
+    Create a new store at the URI. It will fail if the store already exists and is found to contain data that would be overwritten."""
+
+    SESSION_NEW_OVERWRITE = gnucash_core_c.SESSION_NEW_OVERWRITE
+    """False, True, True (create | force)
+    Create a new store at the URI even if a store already exists there."""
+
+    SESSION_READ_ONLY = gnucash_core_c.SESSION_READ_ONLY
+    """True, False, False (ignore_lock)
+    Open the session read-only, ignoring any existing lock and not creating one if the URI isn't locked."""
+
+    SESSION_BREAK_LOCK = gnucash_core_c.SESSION_BREAK_LOCK
+    """True, False, True (ignore_lock | force)
+    Open the session, taking over any existing lock."""
+
+
 class Session(GnuCashCoreClass):
     """A GnuCash book editing session
 

commit ee3342d2b474fc94be206e1a768dc8de07b0ea32
Author: c-holtermann <mail at c-holtermann.net>
Date:   Fri Jun 19 22:43:08 2020 +0200

    introduce python submodule deprecation
    
    the deprecation submodule will house content related to deprecation.
    That is general convenience function and functions related to specific
    deprecation issues. The latter starts with decorator functions to bridge
    the change in qof_session_begin argument change to SessionOpenMode.

diff --git a/bindings/python/CMakeLists.txt b/bindings/python/CMakeLists.txt
index 8c5884e3a..4e7480495 100644
--- a/bindings/python/CMakeLists.txt
+++ b/bindings/python/CMakeLists.txt
@@ -1,7 +1,7 @@
 add_subdirectory(example_scripts)
 add_subdirectory(tests)
 
-set(PYEXEC_FILES  __init__.py function_class.py gnucash_business.py gnucash_core.py app_utils.py)
+set(PYEXEC_FILES  __init__.py function_class.py gnucash_business.py gnucash_core.py app_utils.py deprecation.py)
 
 set(SWIG_FILES ${CMAKE_CURRENT_SOURCE_DIR}/gnucash_core.i ${CMAKE_CURRENT_SOURCE_DIR}/time64.i)
 set(GNUCASH_CORE_C_INCLUDES
diff --git a/bindings/python/__init__.py b/bindings/python/__init__.py
index 16b72fba9..8b3e0b687 100644
--- a/bindings/python/__init__.py
+++ b/bindings/python/__init__.py
@@ -5,6 +5,7 @@
 # >>> from gnucash.gnucash_core import thingy
 from gnucash.gnucash_core import *
 from . import app_utils
+from . import deprecation
 ##  @file
 #   @brief helper file for the importing of gnucash
 #   @author Mark Jenkins, ParIT Worker Co-operative <mark at parit.ca>
diff --git a/bindings/python/deprecation.py b/bindings/python/deprecation.py
new file mode 100644
index 000000000..81d4cdc36
--- /dev/null
+++ b/bindings/python/deprecation.py
@@ -0,0 +1,68 @@
+# deprecation.py - gnucash submodule with deprecation related content
+#
+# contains decorator methods dealing with deprecated methods and
+# deprecation related convenience methods
+#
+# @brief gnucash submodule with deprecation related content
+# @author Christoph Holtermann <mail at c-holtermann.net>
+# @ingroup python_bindings
+
+from functools import wraps
+
+# use of is_new, force_new and ignore_lock is deprecated, use mode instead
+# the following decorators enable backward compatibility for the deprecation period
+def deprecated_args_session(ignore_lock_or_mode=None, is_new=None,
+        force_new=None, mode=None, ignore_lock=None):
+
+    # check for usage of deprecated arguments (ignore_lock, is_new, force_new)
+    deprecated_args = (ignore_lock, is_new, force_new)
+    deprecated_keyword_use = deprecated_args.count(None) != len(deprecated_args)
+    if deprecated_keyword_use:
+        # deprecated arguments have been used by keyword or more than three args have been used which is only possible with the deprecated args
+        deprecation = True
+    else:
+        deprecation = False
+        # __init__ could have been called without keywords like __init__(book_uri, True) where True aims at ignore_lock
+        # which ist not distinguishable from __init__(book, SessionOpenMode.SESSION_NORMAL_OPEN)
+        # so if mode has not been set by keyword use the 3rd argument
+        if mode is None:
+            mode = ignore_lock_or_mode
+
+    if deprecation:
+        # if any(item in ("is_new", "ignore_lock", "force_new") for item in kwargs):
+        import warnings
+        warnings.warn(
+            "Use of ignore_lock, is_new or force_new arguments is deprecated. Use mode argument instead. Have a look at gnucash.SessionOpenMode.",
+            category=DeprecationWarning,
+            stacklevel=3
+        )
+
+        # if not provided calculate mode from deprecated args
+        if mode is None:
+            from gnucash.gnucash_core import SessionOpenMode
+            ignore_lock = False if ignore_lock is None else ignore_lock
+            is_new = False if is_new is None else is_new
+            force_new = False if force_new is None else force_new
+            mode = SessionOpenMode((ignore_lock << 2) + (is_new << 1) + force_new)
+
+    return mode
+
+def deprecated_args_session_init(original_function):
+    """decorator for Session.__init__() to provide backward compatibility for deprecated use of ignore_lock, is_new and force_new"""
+    @wraps(original_function)
+    def new_function(self, book_uri=None, ignore_lock_or_mode=None, is_new=None,
+                 force_new=None, instance=None, mode=None, ignore_lock=None):
+
+        mode = deprecated_args_session(ignore_lock_or_mode, is_new, force_new, mode, ignore_lock)
+        return(original_function(self, book_uri=book_uri, mode=mode, instance=instance))
+    return new_function
+
+def deprecated_args_session_begin(original_function):
+    """decorator for Session.begin() to provide backward compatibility for deprecated use of ignore_lock, is_new and force_new"""
+    @wraps(original_function)
+    def new_function(self, new_uri=None, ignore_lock_or_mode=None, is_new=None,
+                 force_new=None, mode=None, ignore_lock=None):
+        mode = deprecated_args_session(ignore_lock_or_mode, is_new, force_new, mode, ignore_lock)
+        return(original_function(self, new_uri=new_uri, mode=mode))
+    return new_function
+



Summary of changes:
 bindings/python/CMakeLists.txt                     |   2 +-
 bindings/python/__init__.py                        |   1 +
 bindings/python/deprecation.py                     |  68 ++++++
 .../python/example_scripts/account_analysis.py     |   4 +-
 .../python/example_scripts/gncinvoice_jinja.py     |  75 ++++---
 bindings/python/example_scripts/latex_invoices.py  | 236 +++++++++++----------
 .../new_book_with_opening_balances.py              |   7 +-
 .../example_scripts/rest-api/gnucash_rest.py       |   7 +-
 bindings/python/example_scripts/simple_book.py     |   4 +-
 .../example_scripts/simple_business_create.py      |   4 +-
 .../example_scripts/simple_invoice_insert.py       |   4 +-
 bindings/python/example_scripts/simple_session.py  |   8 +-
 .../python/example_scripts/simple_sqlite_create.py |   4 +-
 bindings/python/example_scripts/simple_test.py     |   7 +-
 bindings/python/function_class.py                  | 209 +++++++++++++++---
 bindings/python/gnucash_core.py                    | 146 +++++++++++--
 bindings/python/tests/CMakeLists.txt               |   3 +-
 bindings/python/tests/runTests.py.in               |   1 +
 bindings/python/tests/test_function_class.py       | 177 ++++++++++++++++
 bindings/python/tests/test_session.py              |  47 +++-
 libgnucash/engine/qofsession.h                     |  18 +-
 21 files changed, 817 insertions(+), 215 deletions(-)
 create mode 100644 bindings/python/deprecation.py
 create mode 100644 bindings/python/tests/test_function_class.py



More information about the gnucash-changes mailing list