Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Refactor most of the files for tests to pass on sqlalchemy2. #232

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat!: Refactor most of the files for tests to pass on sqlalchemy2.
BREAKINGCHANGE: Some older versions of sqlalchemy might not be supported
anymore.
bvanelli committed Jun 28, 2024
commit 7975029281ccab8018908e56a3339ea4ece12483
5 changes: 3 additions & 2 deletions examples/simple_move_split.py
Original file line number Diff line number Diff line change
@@ -24,8 +24,9 @@
eur,
"transaction {}".format(i),
)
Split(accounts[random.randrange(N)], value=v, transaction=tx)
Split(accounts[random.randrange(N)], value=-v, transaction=tx)
book.add(tx)
book.add(Split(accounts[random.randrange(N)], value=v, transaction=tx))
book.add(Split(accounts[random.randrange(N)], value=-v, transaction=tx))
book.save()

# select two accounts
4 changes: 2 additions & 2 deletions piecash/_declbase.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

from sqlalchemy import Column, VARCHAR, event
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relation, foreign, object_session
from sqlalchemy.orm import relationship, foreign, object_session

from ._common import CallableList
from .kvp import DictWrapper, Slot
@@ -23,7 +23,7 @@ class DeclarativeBaseGuid(DictWrapper, DeclarativeBase):

@declared_attr
def slots(cls):
rel = relation(
rel = relationship(
"Slot",
primaryjoin=foreign(Slot.obj_guid) == cls.guid,
cascade="all, delete-orphan",
10 changes: 5 additions & 5 deletions piecash/budget.py
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
import uuid

from sqlalchemy import Column, VARCHAR, INTEGER, BIGINT, ForeignKey
from sqlalchemy.orm import relation, foreign
from sqlalchemy.orm import relationship, foreign

from ._common import hybrid_property_gncnumeric, Recurrence, CallableList
from ._declbase import DeclarativeBaseGuid
@@ -38,14 +38,14 @@ class Budget(DeclarativeBaseGuid):
num_periods = Column("num_periods", INTEGER(), nullable=False)

# # relation definitions
recurrence = relation(
recurrence = relationship(
Recurrence,
primaryjoin=foreign(Recurrence.obj_guid) == guid,
cascade="all, delete-orphan",
uselist=False,
)

amounts = relation(
amounts = relationship(
"BudgetAmount",
back_populates="budget",
cascade="all, delete-orphan",
@@ -86,8 +86,8 @@ class BudgetAmount(DeclarativeBase):
amount = hybrid_property_gncnumeric(_amount_num, _amount_denom)

# relation definitions
account = relation("Account", back_populates="budget_amounts")
budget = relation("Budget", back_populates="amounts")
account = relationship("Account", back_populates="budget_amounts")
budget = relationship("Budget", back_populates="amounts")

def __str__(self):
return "BudgetAmount<{}={}>".format(self.period_num, self.amount)
24 changes: 12 additions & 12 deletions piecash/business/invoice.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import uuid

from sqlalchemy import Column, INTEGER, BIGINT, VARCHAR, ForeignKey
from sqlalchemy.orm import composite, relation
from sqlalchemy.orm import composite, relationship

from .person import PersonType

@@ -41,13 +41,13 @@ class Billterm(DeclarativeBaseGuid):
cutoff = Column("cutoff", INTEGER())

# relation definitions
children = relation(
children = relationship(
"Billterm",
back_populates="parent",
cascade="all, delete-orphan",
collection_class=CallableList,
)
parent = relation(
parent = relationship(
"Billterm",
back_populates="children",
remote_side=guid,
@@ -104,8 +104,8 @@ class Entry(DeclarativeBaseGuid):
order_guid = Column("order_guid", VARCHAR(length=32), ForeignKey("orders.guid"))

# relation definitions
order = relation("Order", back_populates="entries")
invoice = relation("Invoice", back_populates="entries")
order = relationship("Order", back_populates="entries")
invoice = relationship("Invoice", back_populates="entries")

def __str__(self):
return "Entry<{}>".format(self.description)
@@ -147,13 +147,13 @@ class Invoice(DeclarativeBaseGuid):

# relation definitions
# todo: check all relations and understanding of types...
term = relation("Billterm")
currency = relation("Commodity")
post_account = relation("Account")
post_lot = relation("Lot")
post_txn = relation("Transaction")
term = relationship("Billterm")
currency = relationship("Commodity")
post_account = relationship("Account")
post_lot = relationship("Lot")
post_txn = relationship("Transaction")

entries = relation(
entries = relationship(
"Entry",
back_populates="invoice",
cascade="all, delete-orphan",
@@ -225,7 +225,7 @@ class Order(DeclarativeBaseGuid):

# relation definitions
# todo: owner_guid/type links to Vendor or Customer
entries = relation(
entries = relationship(
"Entry",
back_populates="order",
cascade="all, delete-orphan",
16 changes: 8 additions & 8 deletions piecash/business/person.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

from sqlalchemy import Column, VARCHAR, INTEGER, BIGINT, ForeignKey, and_, event
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import composite, relation, foreign
from sqlalchemy.orm import composite, relationship, foreign

from .._common import hybrid_property_gncnumeric, CallableList
from .._declbase import DeclarativeBaseGuid
@@ -113,7 +113,7 @@ def currency_guid(cls):

@declared_attr
def currency(cls):
return relation("Commodity")
return relationship("Commodity")

# hold the name of the counter to use for id
_counter_name = None
@@ -149,7 +149,7 @@ def __declare_last__(cls):

owner_type = PersonType.get(cls, None)
if owner_type and not hasattr(cls, "jobs"):
cls.jobs = relation('Job',
cls.jobs = relationship('Job',
primaryjoin=and_(
cls.guid == foreign(Job.owner_guid),
owner_type == Job.owner_type,
@@ -226,8 +226,8 @@ class Customer(Person, DeclarativeBaseGuid):
taxtable_guid = Column("taxtable", VARCHAR(length=32), ForeignKey("taxtables.guid"))

# relation definitions
taxtable = relation("Taxtable")
term = relation("Billterm")
taxtable = relationship("Taxtable")
term = relationship("Billterm")

def __init__(
self,
@@ -307,7 +307,7 @@ class Employee(Person, DeclarativeBaseGuid):
rate = hybrid_property_gncnumeric(_rate_num, _rate_denom)

# relation definitions
creditcard_account = relation("Account")
creditcard_account = relationship("Account")

def __init__(
self,
@@ -384,8 +384,8 @@ class Vendor(Person, DeclarativeBaseGuid):
)

# relation definitions
taxtable = relation("Taxtable")
term = relation("Billterm")
taxtable = relationship("Taxtable")
term = relationship("Billterm")

def __init__(
self,
12 changes: 6 additions & 6 deletions piecash/business/tax.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import uuid
from sqlalchemy import Column, VARCHAR, BIGINT, INTEGER, ForeignKey
from sqlalchemy.orm import relation
from sqlalchemy.orm import relationship

from .._common import hybrid_property_gncnumeric, CallableList
from .._declbase import DeclarativeBaseGuid, DeclarativeBase
@@ -26,19 +26,19 @@ class Taxtable(DeclarativeBaseGuid):
parent_guid = Column("parent", VARCHAR(length=32), ForeignKey("taxtables.guid"))

# relation definitions
entries = relation(
entries = relationship(
"TaxtableEntry",
back_populates="taxtable",
cascade="all, delete-orphan",
collection_class=CallableList,
)
children = relation(
children = relationship(
"Taxtable",
back_populates="parent",
cascade="all, delete-orphan",
collection_class=CallableList,
)
parent = relation(
parent = relationship(
"Taxtable",
back_populates="children",
remote_side=guid,
@@ -79,8 +79,8 @@ class TaxtableEntry(DeclarativeBase):
type = Column("type", ChoiceType({1: "value", 2: "percentage"}), nullable=False)

# relation definitions
taxtable = relation("Taxtable", back_populates="entries")
account = relation("Account")
taxtable = relationship("Taxtable", back_populates="entries")
account = relationship("Account")

def __init__(self, type, amount, account, taxtable=None):
self.type = type
16 changes: 8 additions & 8 deletions piecash/core/account.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
from enum import Enum

from sqlalchemy import Column, VARCHAR, ForeignKey, INTEGER
from sqlalchemy.orm import relation, validates
from sqlalchemy.orm import relationship, validates

from .._common import CallableList, GncConversionError
from .._declbase import DeclarativeBaseGuid
@@ -165,37 +165,37 @@ def commodity_scu(self, value):
)

# relation definitions
commodity = relation("Commodity", back_populates="accounts")
children = relation(
commodity = relationship("Commodity", back_populates="accounts")
children = relationship(
"Account",
back_populates="parent",
cascade="all, delete-orphan",
collection_class=CallableList,
)
parent = relation(
parent = relationship(
"Account",
back_populates="children",
remote_side=guid,
)
splits = relation(
splits = relationship(
"Split",
back_populates="account",
cascade="all, delete-orphan",
collection_class=CallableList,
)
lots = relation(
lots = relationship(
"Lot",
back_populates="account",
cascade="all, delete-orphan",
collection_class=CallableList,
)
budget_amounts = relation(
budget_amounts = relationship(
"BudgetAmount",
back_populates="account",
cascade="all, delete-orphan",
collection_class=CallableList,
)
scheduled_transaction = relation(
scheduled_transaction = relationship(
"ScheduledTransaction",
back_populates="template_account",
cascade="all, delete-orphan",
8 changes: 5 additions & 3 deletions piecash/core/book.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
from operator import attrgetter

from sqlalchemy import Column, VARCHAR, ForeignKey, inspect
from sqlalchemy.orm import relation, aliased, joinedload
from sqlalchemy.orm import relationship, aliased, joinedload
from sqlalchemy.orm.base import instance_state
from sqlalchemy.orm.exc import NoResultFound

@@ -99,12 +99,12 @@ class Book(DeclarativeBaseGuid):
)

# relation definitions
root_account = relation(
root_account = relationship(
"Account",
# back_populates='root_book',
foreign_keys=[root_account_guid],
)
root_template = relation("Account", foreign_keys=[root_template_guid])
root_template = relationship("Account", foreign_keys=[root_template_guid])

uri = None
session = None
@@ -333,6 +333,8 @@ def close(self):
# # remove the lock
# session.delete_lock()
session.close()
# close the engine
self.session.bind.engine.dispose()

# add general getters for gnucash classes
def get(self, cls, **kwargs):
17 changes: 9 additions & 8 deletions piecash/core/commodity.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
from decimal import Decimal

from sqlalchemy import Column, VARCHAR, INTEGER, ForeignKey, BIGINT, Index
from sqlalchemy.orm import relation
from sqlalchemy.orm import relationship
from sqlalchemy.orm.exc import MultipleResultsFound

from ._commodity_helper import quandl_fx
@@ -67,12 +67,12 @@ class Price(DeclarativeBaseGuid):
value = hybrid_property_gncnumeric(_value_num, _value_denom)

# relation definitions
commodity = relation(
commodity = relationship(
"Commodity",
back_populates="prices",
foreign_keys=[commodity_guid],
)
currency = relation(
currency = relationship(
"Commodity",
foreign_keys=[currency_guid],
)
@@ -174,19 +174,19 @@ def base_currency(self):
)

# relation definitions
accounts = relation(
accounts = relationship(
"Account",
back_populates="commodity",
cascade="all, delete-orphan",
collection_class=CallableList,
)
transactions = relation(
transactions = relationship(
"Transaction",
back_populates="currency",
cascade="all, delete-orphan",
collection_class=CallableList,
)
prices = relation(
prices = relationship(
"Price",
back_populates="commodity",
foreign_keys=[Price.commodity_guid],
@@ -304,6 +304,7 @@ def update_prices(self, start_date=None):
date=datetime.datetime.strptime(q.date, "%Y-%m-%d").date(),
value=str(q.rate),
)
self.book.add(p)

else:
symbol = self.mnemonic
@@ -313,13 +314,13 @@ def update_prices(self, start_date=None):

# get historical data
for q in download_quote(symbol, start_date, datetime.date.today(), tz):
Price(
self.book.add(Price(
commodity=self,
currency=currency,
date=q.date,
value=q.close,
type="last",
)
))

def object_to_validate(self, change):
if change[-1] != "deleted":
12 changes: 7 additions & 5 deletions piecash/core/session.py
Original file line number Diff line number Diff line change
@@ -417,6 +417,7 @@ def open_book(
# backup database if readonly=False and do_backup=True
if not readonly and do_backup:
if engine.name != "sqlite":
engine.dispose()
raise GnucashException(
"Cannot do a backup for engine '{}'. Do yourself a backup and then specify do_backup=False".format(
engine.name
@@ -428,7 +429,8 @@ def open_book(

shutil.copyfile(url, url_backup)

locks = list(engine.execute(gnclock.select()))
with engine.connect() as conn:
locks = list(conn.execute(gnclock.select()))

# ensure the file is not locked by GnuCash itself
if locks and not open_if_lock:
@@ -446,6 +448,7 @@ def open_book(
if version_book == {k: v for k, v in vt.items() if "Gnucash" not in k}:
break
else:
engine.dispose()
raise ValueError("Unsupported table versions")
assert version == "3.0" or version == "3.7" or version == "4.1", (
"This version of piecash only support books from gnucash (3.0|3.7|4.1) "
@@ -488,9 +491,8 @@ def readonly_commit(*args, **kwargs):
# add logic to create/delete GnuCash locks
def delete_lock():
session.execute(
gnclock.delete(
whereclause=(gnclock.c.hostname == socket.gethostname())
and (gnclock.c.pid == os.getpid())
gnclock.delete().where(
(gnclock.c.hostname == socket.gethostname()) and (gnclock.c.pid == os.getpid())
)
)
session.commit()
@@ -499,7 +501,7 @@ def delete_lock():

def create_lock():
session.execute(
gnclock.insert(values=dict(hostname=socket.gethostname(), pid=os.getpid()))
gnclock.insert().values(dict(hostname=socket.gethostname(), pid=os.getpid()))
)
session.commit()

21 changes: 11 additions & 10 deletions piecash/core/transaction.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
from decimal import Decimal

from sqlalchemy import Column, VARCHAR, ForeignKey, BIGINT, INTEGER, Index
from sqlalchemy.orm import relation, validates, foreign
from sqlalchemy.orm import relationship, validates, foreign
from sqlalchemy.orm.base import NEVER_SET

from .._common import CallableList, GncImbalanceError
@@ -74,9 +74,9 @@ class Split(DeclarativeBaseGuid):
lot_guid = Column("lot_guid", VARCHAR(length=32), ForeignKey("lots.guid"))

# relation definitions
account = relation("Account", back_populates="splits")
lot = relation("Lot", back_populates="splits")
transaction = relation(
account = relationship("Account", back_populates="splits")
lot = relationship("Lot", back_populates="splits")
transaction = relationship(
"Transaction", back_populates="splits", cascade="refresh-expire"
)

@@ -199,6 +199,7 @@ def validate(self):
type="transaction",
source="user:split-register",
)
self.book.add(pr)

# and an action if not yet defined
if self.action == "":
@@ -258,11 +259,11 @@ class Transaction(DeclarativeBaseGuid):
)

# relation definitions
currency = relation(
currency = relationship(
"Commodity",
back_populates="transactions",
)
splits = relation(
splits = relationship(
"Split",
back_populates="transaction",
cascade="all, delete-orphan",
@@ -464,8 +465,8 @@ class ScheduledTransaction(DeclarativeBaseGuid):
)

# relation definitions
template_account = relation("Account")
recurrence = relation(
template_account = relationship("Account")
recurrence = relationship(
"Recurrence",
primaryjoin=guid == foreign(Recurrence.obj_guid),
cascade="all, delete-orphan",
@@ -501,11 +502,11 @@ class Lot(DeclarativeBaseGuid):
notes = pure_slot_property("notes")

# relation definitions
account = relation(
account = relationship(
"Account",
back_populates="lots",
)
splits = relation(
splits = relationship(
"Split",
back_populates="lot",
collection_class=CallableList,
5 changes: 2 additions & 3 deletions piecash/kvp.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import datetime
import decimal
import sys
import uuid
from enum import Enum
from importlib import import_module

from sqlalchemy import Column, VARCHAR, INTEGER, REAL, BIGINT, types, event, Index
from sqlalchemy.orm import relation, foreign, object_session, backref
from sqlalchemy.orm import relationship, foreign, object_session, backref

from ._common import CallableList
from ._common import hybrid_property_gncnumeric
@@ -267,7 +266,7 @@ class SlotFrame(DictWrapper, Slot):

guid_val = Column("guid_val", VARCHAR(length=32))

slots = relation(
slots = relationship(
"Slot",
primaryjoin=foreign(Slot.obj_guid) == guid_val,
cascade="all, delete-orphan",
5 changes: 2 additions & 3 deletions piecash/sa_extra.py
Original file line number Diff line number Diff line change
@@ -40,14 +40,13 @@
from sqlalchemy.orm.decl_base import _declarative_constructor

def as_declarative(**kw):
bind, metadata, class_registry, constructor = (
kw.pop("bind", None),
metadata, class_registry, constructor = (
kw.pop("metadata", None),
kw.pop("class_registry", None),
kw.pop("constructor", _declarative_constructor),
)

return registry(_bind=bind, metadata=metadata, class_registry=class_registry, constructor=constructor).as_declarative_base(**kw)
return registry(metadata=metadata, class_registry=class_registry, constructor=constructor).as_declarative_base(**kw)

except ImportError:
# `as_declarative` was under `sqlalchemy.ext.declarative` prior to 1.4
18 changes: 9 additions & 9 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -22,24 +22,24 @@ ipython==7.16.1
ipython-genutils==0.2.0
jedi==0.17.2
Jinja2==3.1.4
MarkupSafe==1.1.1
numpy==1.22.0
MarkupSafe==2.1.5
numpy==1.26.4
packaging==20.4
pandas==1.1.3
pandas==2.2.2
parso==0.7.1
pickleshare==0.7.5
pipenv==2020.8.13
pluggy==0.13.1
pockets==0.9.1
prompt-toolkit==3.0.8
psycopg2==2.8.6
psycopg2-binary==2.9.9
py==1.9.0
Pygments==2.15.0
PyMySQL==1.1.1
pyparsing==2.4.7
pytest==6.1.1
pytest-cov==2.10.1
python-dateutil==2.8.1
pytest==8.2.2
pytest-cov==5.0.0
python-dateutil==2.9.0
pytz==2020.1
qifparse==0.5
requests==2.32.2
@@ -54,8 +54,8 @@ sphinxcontrib-jsmath==1.0.1
sphinxcontrib-programoutput==0.16
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.4
SQLAlchemy==1.3.20
SQLAlchemy-Utils==0.36.8
SQLAlchemy==2.0.31
SQLAlchemy-Utils==0.41.2
toml==0.10.1
tox==3.20.1
tox-pipenv==1.10.1
92 changes: 50 additions & 42 deletions tests/test_book.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@
from pathlib import Path

import pytest
from sqlalchemy import create_engine
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import Session
@@ -89,18 +88,19 @@ def test_create_USD_book(self, new_book_USD):
assert CUR.namespace == "CURRENCY"

def test_create_specific_currency(self):
b = create_book(currency="USD")
CUR = b.commodities[0]
assert CUR.mnemonic == "USD"
assert CUR.namespace == "CURRENCY"
with create_book(currency="USD") as b:
CUR = b.commodities[0]
assert CUR.mnemonic == "USD"
assert CUR.namespace == "CURRENCY"

b = create_book(currency="CHF")
CUR = b.commodities[0]
assert CUR.mnemonic == "CHF"
assert CUR.namespace == "CURRENCY"
with create_book(currency="CHF") as b:
CUR = b.commodities[0]
assert CUR.mnemonic == "CHF"
assert CUR.namespace == "CURRENCY"

with pytest.raises(ValueError):
b = create_book(currency="ZIE")
with create_book(currency="ZIE"):
pass

def test_create_named_sqlite_book(self):
# remove file if left from previous test
@@ -110,52 +110,54 @@ def test_create_named_sqlite_book(self):

# assert error if both sqlite_file and uri_conn are defined
with pytest.raises(ValueError):
b = create_book(db_sqlite, db_sqlite_uri)
with create_book(db_sqlite, db_sqlite_uri):
pass

# assert creation of file
b = create_book(db_sqlite)
assert db_sqlite.exists()
t = db_sqlite.stat().st_mtime
with create_book(db_sqlite) as b:
assert db_sqlite.exists()
t = db_sqlite.stat().st_mtime

# ensure error if no overwrite
with pytest.raises(GnucashException):
b = create_book(db_sqlite)
with create_book(db_sqlite):
pass
assert db_sqlite.stat().st_mtime == t
with pytest.raises(GnucashException):
b = create_book(uri_conn="sqlite:///{}".format(db_sqlite))
with create_book(uri_conn="sqlite:///{}".format(db_sqlite)):
pass
assert db_sqlite.stat().st_mtime == t
with pytest.raises(GnucashException):
b = create_book(db_sqlite, overwrite=False)
with (pytest.raises(GnucashException)):
with create_book(db_sqlite, overwrite=False):
pass
assert db_sqlite.stat().st_mtime == t

# if overwrite, DB is recreated
b = create_book(db_sqlite, overwrite=True)
assert db_sqlite.stat().st_mtime > t
with create_book(db_sqlite, overwrite=True):
assert db_sqlite.stat().st_mtime > t

# clean test
db_sqlite.unlink()

@pytest.mark.skip(reason="I cannot fix this test")
def test_create_with_FK(self):
# create and keep FK
b = create_book(uri_conn=db_sqlite_uri, keep_foreign_keys=True, overwrite=True)
b.session.close()

insp = Inspector.from_engine(create_engine(db_sqlite_uri))
fk_total = []
for tbl in insp.get_table_names():
fk_total.append(insp.get_foreign_keys(tbl))
assert len(fk_total) == 25
with create_book(uri_conn=db_sqlite_uri, keep_foreign_keys=True, overwrite=True) as b:
engine = b.session.bind.engine
insp = Inspector.from_engine(engine)
fk_total = []
for tbl in insp.get_table_names():
fk_total.extend(insp.get_foreign_keys(tbl))
assert len(fk_total) > 0

def test_create_without_FK(self):
# create without FK
b = create_book(uri_conn=db_sqlite_uri, keep_foreign_keys=False, overwrite=True)
b.session.close()

insp = Inspector.from_engine(create_engine(db_sqlite_uri))
for tbl in insp.get_table_names():
fk = insp.get_foreign_keys(tbl)
assert len(fk) == 0

with create_book(uri_conn=db_sqlite_uri, keep_foreign_keys=False, overwrite=True) as b:
engine = b.session.bind.engine
insp = Inspector.from_engine(engine)
for tbl in insp.get_table_names():
fk = insp.get_foreign_keys(tbl)
assert len(fk) == 0

class TestBook_open_book(object):
def test_open_noarg(self):
@@ -165,15 +167,17 @@ def test_open_noarg(self):
def test_open_default(self, book_uri):
# open book that does not exists
with pytest.raises(GnucashException):
b = open_book(uri_conn=book_uri)
with open_book(uri_conn=book_uri):
pass

# create book
with create_book(uri_conn=book_uri):
pass

# assert error if both sqlite_file and uri_conn are defined on open_book
with pytest.raises(ValueError):
b = open_book(db_sqlite, db_sqlite_uri)
with open_book(db_sqlite, db_sqlite_uri):
pass

# open book that exists
with open_book(uri_conn=book_uri) as b:
@@ -187,11 +191,13 @@ def test_open_default(self, book_uri):
# open book with checking existence
book_uri_fail = book_uri.replace("foo", "foofail")
with pytest.raises(GnucashException, match="Database .* does not exist"):
open_book(uri_conn=book_uri_fail)
with open_book(uri_conn=book_uri_fail):
pass

# open book without checking existence
with pytest.raises(OperationalError):
open_book(uri_conn=book_uri_fail, check_exists=False)
with open_book(uri_conn=book_uri_fail, check_exists=False):
pass

def test_open_RW_backup(self, book_uri):
# create book
@@ -202,7 +208,8 @@ def test_open_RW_backup(self, book_uri):
if engine_type != "sqlite":
# raise an exception as try to do a backup on postgres which is not supported yet
with pytest.raises(GnucashException):
b = open_book(uri_conn=book_uri, readonly=False)
with open_book(uri_conn=book_uri, readonly=False):
pass

elif engine_type == "sqlite":
# delete all potential existing backup files
@@ -232,7 +239,8 @@ def test_open_lock(self, book_uri):

# try to open locked book
with pytest.raises(GnucashException):
b = open_book(uri_conn=book_uri)
with open_book(uri_conn=book_uri):
pass

# open book specifying open_if_lock as True
with open_book(uri_conn=book_uri, open_if_lock=True) as b:
9 changes: 9 additions & 0 deletions tests/test_commodity.py
Original file line number Diff line number Diff line change
@@ -46,6 +46,8 @@ def test_create_commodity_uniqueness(self, book_basic):
cdty2 = Commodity(
namespace="AMEX", mnemonic="APPLE", fullname="Apple", book=book_basic
)
book_basic.add(cdty1)
book_basic.add(cdty2)

with pytest.raises(ValueError):
book_basic.save()
@@ -54,6 +56,7 @@ def test_base_currency_commodity(self, book_basic):
cdty = Commodity(
namespace="AMEX", mnemonic="APPLE", fullname="Apple", book=book_basic
)
book_basic.add(cdty)

with pytest.raises(GnucashException):
cdty.base_currency
@@ -89,6 +92,7 @@ def test_create_basicprice(self, book_basic):
date=date(2014, 2, 22),
value=Decimal("0.54321"),
)
book_basic.add(p)
book_basic.flush()

# check price exist
@@ -102,6 +106,7 @@ def test_create_basicprice(self, book_basic):
date=date(2014, 2, 21),
value=Decimal("0.12345"),
)
book_basic.add(p2)
book_basic.flush()
assert p.value + p2.value == Decimal("0.66666")
assert len(USD.prices.all()) == 2
@@ -121,6 +126,8 @@ def test_create_duplicateprice(self, book_basic):
date=date(2014, 2, 22),
value=Decimal("0.12345"),
)
book_basic.add(p)
book_basic.add(p1)

book_basic.flush()
assert USD.prices.filter_by(value=Decimal("0")).all() == []
@@ -146,6 +153,8 @@ def test_create_duplicateprice_different_source(self, book_basic):
value=Decimal("0.12345"),
source="other:price",
)
book_basic.add(p)
book_basic.add(p1)

book_basic.flush()
assert USD.prices.filter_by(value=Decimal("0")).all() == []
7 changes: 5 additions & 2 deletions tests/test_factories.py
Original file line number Diff line number Diff line change
@@ -109,14 +109,15 @@ class TestFactoriesTransactions(object):
def test_single_transaction(self, book_basic):
today = datetime.today()
print("today=", today)
factories.single_transaction(
tx = factories.single_transaction(
today.date(),
today,
"my test",
Decimal(100),
from_account=book_basic.accounts(name="inc"),
to_account=book_basic.accounts(name="asset"),
)
book_basic.add(tx)
book_basic.save()
tr = book_basic.transactions(description="my test")
assert len(tr.splits) == 2
@@ -143,21 +144,23 @@ def test_single_transaction_tz(self, book_basic):
from_account=book_basic.accounts(name="inc"),
to_account=book_basic.accounts(name="asset"),
)
book_basic.add(tr)
book_basic.save()
tr = book_basic.transactions(description="my test")
assert tr.post_date == today.date()
assert tr.enter_date == today.replace(microsecond=0)

def test_single_transaction_rollback(self, book_basic):
today = pytz.timezone(str(tzlocal.get_localzone())).localize(datetime.today())
factories.single_transaction(
tx = factories.single_transaction(
today.date(),
today,
"my test",
Decimal(100),
from_account=book_basic.accounts(name="inc"),
to_account=book_basic.accounts(name="asset"),
)
book_basic.add(tx)
book_basic.validate()
assert len(book_basic.transactions) == 1
book_basic.cancel()
21 changes: 16 additions & 5 deletions tests/test_helper.py
Original file line number Diff line number Diff line change
@@ -261,22 +261,28 @@ def book_transactions(request):
cdty = Commodity(
namespace=u"BEL20", mnemonic=u"GnuCash Inc.", fullname=u"GnuCash Inc. stock"
)
b.add(cdty)
asset = Account(
name="asset", type="ASSET", commodity=curr, parent=b.root_account
)
b.add(asset)
foreign_asset = Account(
name="foreign asset",
type="ASSET",
commodity=other_curr,
parent=b.root_account,
)
b.add(foreign_asset)
stock = Account(name="broker", type="STOCK", commodity=cdty, parent=asset)
b.add(stock)
expense = Account(
name="exp", type="EXPENSE", commodity=curr, parent=b.root_account
)
b.add(expense)
income = Account(
name="inc", type="INCOME", commodity=curr, parent=b.root_account
)
b.add(income)

tr1 = Transaction(
post_date=date(2015, 10, 21),
@@ -287,6 +293,7 @@ def book_transactions(request):
Split(account=income, value=(-1000, 1)),
],
)
b.add(tr1)
tr2 = Transaction(
post_date=date(2015, 10, 25),
description="my expense",
@@ -297,6 +304,7 @@ def book_transactions(request):
Split(account=expense, value=(80, 1), memo="cost of Y"),
],
)
b.add(tr2)
tr_stock = Transaction(
post_date=date(2015, 10, 29),
description="my purchase of stock",
@@ -312,6 +320,7 @@ def book_transactions(request):
),
],
)
b.add(tr_stock)
tr_to_foreign = Transaction(
post_date=date(2015, 10, 30),
description="transfer to foreign asset",
@@ -321,6 +330,7 @@ def book_transactions(request):
Split(account=foreign_asset, value=(200, 1), quantity=(135, 1)),
],
)
b.add(tr_to_foreign)
tr_from_foreign = Transaction(
post_date=date(2015, 10, 31),
description="transfer from foreign asset",
@@ -330,19 +340,20 @@ def book_transactions(request):
Split(account=foreign_asset, value=(-135, 1)),
],
)
Price(
b.add(tr_from_foreign)
b.add(Price(
commodity=cdty,
currency=other_curr,
date=date(2015, 11, 1),
value=(123, 100),
)
Price(
))
b.add(Price(
commodity=cdty,
currency=other_curr,
date=date(2015, 11, 4),
value=(127, 100),
)
Price(commodity=cdty, currency=curr, date=date(2015, 11, 2), value=(234, 100))
))
b.add(Price(commodity=cdty, currency=curr, date=date(2015, 11, 2), value=(234, 100)))

b.save()
yield b
10 changes: 10 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

import datetime
import os
import pathlib
import shutil
import threading
from decimal import Decimal
@@ -67,6 +68,15 @@ def finalizer():


class TestIntegration_ExampleScripts(object):

@pytest.fixture(autouse=True)
def examples_root_folder(self) -> None:
"""Tests from this class reference the examples folder, so we change to the root folder if not already."""
prev_path = Path()
os.chdir(Path(__file__).parent.parent)
yield None
os.chdir(prev_path)

def test_simple_move_split(self):
run_file("examples/simple_move_split.py")

15 changes: 8 additions & 7 deletions tests/test_model_common.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

import pytest
import pytz
import sqlalchemy
from sqlalchemy import create_engine, Column, TEXT
from sqlalchemy.orm import sessionmaker, composite

@@ -17,8 +18,7 @@ def session():
engine = create_engine("sqlite://")

metadata = mc.DeclarativeBase.metadata
metadata.bind = engine
metadata.create_all()
metadata.create_all(bind=engine)

s = sessionmaker(bind=engine)()

@@ -82,7 +82,9 @@ def __init__(self, day):
s.flush()
assert a.day

assert str(list(s.bind.execute("select day from c_table"))[0][0]) == "20100412"
with s.bind.connect() as conn:
day = list(conn.execute(sqlalchemy.text("select day from c_table")))[0][0]
assert str(day) == "20100412"

def test_datetime(self):
class D(DeclarativeBaseGuid):
@@ -98,10 +100,9 @@ def __init__(self, time):
s.flush()
assert a.time

assert (
str(list(s.bind.execute("select time from d_table"))[0][0])
== "2010-04-12 03:04:05"
)
with s.bind.connect() as conn:
time = list(conn.execute(sqlalchemy.text("select time from d_table")))[0][0]
assert str(time) == "2010-04-12 03:04:05"

def test_float_in_gncnumeric(self):
Mock = collections.namedtuple("Mock", "name")
13 changes: 11 additions & 2 deletions tests/test_transaction.py
Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@ def test_create_basictransaction_validation_date(self, book_basic):
enter_date=datetime(2014, 1, 1),
splits=splits,
)
book_basic.add(tr)

with pytest.raises(GncValidationError):
tr = Transaction(
@@ -86,6 +87,7 @@ def test_create_basictransaction_validation_date(self, book_basic):
enter_date=time(10, 59, 00),
splits=splits,
)
book_basic.add(tr)

with pytest.raises(GncValidationError):
tr = Transaction(
@@ -96,6 +98,7 @@ def test_create_basictransaction_validation_date(self, book_basic):
enter_date=date(2014, 1, 1),
splits=splits,
)
book_basic.add(tr)

tr = Transaction(
currency=EUR,
@@ -105,6 +108,7 @@ def test_create_basictransaction_validation_date(self, book_basic):
enter_date=None,
splits=splits,
)
book_basic.add(tr)

with pytest.raises(GncImbalanceError):
book_basic.flush()
@@ -127,13 +131,14 @@ def test_create_basictransaction(self, book_basic):
Split(account=e, value=-10, memo="mémo exp"),
],
)
book_basic.add(tr)
# check issue with balance
with pytest.raises(GncImbalanceError):
book_basic.flush()
book_basic.validate()

# adjust balance
Split(account=e, value=-90, memo="missing exp", transaction=tr)
book_basic.add(Split(account=e, value=-90, memo="missing exp", transaction=tr))
book_basic.flush()

# check no issue with str
@@ -168,6 +173,7 @@ def test_create_cdtytransaction(self, book_basic):
Split(account=s, value=-90, memo="mémo brok"),
],
)
book_basic.add(tr)

# check issue with quantity for broker split not defined
with pytest.raises(GncValidationError):
@@ -187,7 +193,7 @@ def test_create_cdtytransaction(self, book_basic):
book_basic.validate()

# adjust balance
Split(account=a, value=-10, memo="missing asset corr", transaction=tr)
book_basic.add(Split(account=a, value=-10, memo="missing asset corr", transaction=tr))
book_basic.save()
assert str(sb)
assert str(sb)
@@ -251,6 +257,7 @@ def test_create_cdtytransaction_cdtycurrency(self, book_basic):
Split(account=s, value=-100, quantity=-10, memo="mémo brok"),
],
)
book_basic.add(tr)
# raise error as Transaction has a non CURRENCY commodity
with pytest.raises(GncValidationError):
book_basic.validate()
@@ -273,6 +280,7 @@ def test_create_cdtytransaction_tradingaccount(self, book_basic):
Split(account=s, value=-100, quantity=-15, memo="mémo brok"),
],
)
book_basic.add(tr)
book_basic.validate()

assert "{}".format(tr) == "Transaction<[EUR] 'buy stock' on 2014-01-02>"
@@ -468,6 +476,7 @@ def test_create_simplelot_inconsistentaccounts(self, book_basic):
Split(account=s, value=-10, quantity=-2, memo="mémo brok", lot=l),
],
)
book_basic.add(tr)

with pytest.raises(ValueError):
book_basic.validate()