GnuCash c935c2f+
Loading...
Searching...
No Matches
test_price_and_wrapping.py
1"""Tests for GncPrice / GncPriceDB / GncLot return-type wrapping.
2
3All tests create data programmatically in in-memory sessions so they run
4without any external data files or XML backend.
5"""
6
7import warnings
8from datetime import datetime
9from unittest import TestCase, main
10
11from gnucash import (
12 Account,
13 Book,
14 GncCommodity,
15 GncNumeric,
16 GncPrice,
17 Session,
18 Split,
19 Transaction,
20)
21from gnucash.gnucash_core import GncCommodityNamespace, GncLot, GncPriceDB
22
23
24# ---------------------------------------------------------------------------
25# Helper: set up an in-memory book with a commodity and prices
26# ---------------------------------------------------------------------------
27class PriceSession(TestCase):
28 """Base class that creates a session with a custom commodity and prices."""
29
30 def setUp(self):
31 self.ses = Session()
32 self.book = self.ses.get_book()
33 self.table = self.book.get_table()
34 self.usd = self.table.lookup("CURRENCY", "USD")
35
36 # Create a custom commodity
37 self.test_comm = GncCommodity(
38 self.book, "Test Stock", "NASDAQ", "TSTK", "TSTK", 10000
39 )
40 self.table.insert(self.test_comm)
41
42 # Add prices to the price DB
43 self.pricedb = self.book.get_price_db()
44
45 self.price1 = GncPrice(self.book)
46 self.price1.set_commodity(self.test_comm)
47 self.price1.set_currency(self.usd)
48 self.price1.set_time64(datetime(2025, 1, 15))
49 self.price1.set_value(GncNumeric(4200, 100)) # 42.00
50 self.price1.set_typestr("last")
51 self.pricedb.add_price(self.price1)
52
53 self.price2 = GncPrice(self.book)
54 self.price2.set_commodity(self.test_comm)
55 self.price2.set_currency(self.usd)
56 self.price2.set_time64(datetime(2025, 6, 15))
57 self.price2.set_value(GncNumeric(4500, 100)) # 45.00
58 self.price2.set_typestr("last")
59 self.pricedb.add_price(self.price2)
60
61 def tearDown(self):
62 self.ses.end()
63
64
65# ---------------------------------------------------------------------------
66# Test: GncPrice and GncPriceDB wrapping
67# ---------------------------------------------------------------------------
69 """Verify that GncPrice / GncPriceDB methods return properly wrapped
70 Python objects instead of raw SwigPyObjects."""
71
72 # -- GncPriceDB single-price lookups --
73
74 def test_lookup_latest_returns_gnc_price(self):
75 price = self.pricedb.lookup_latest(self.test_commtest_comm, self.usd)
76 self.assertIsNotNone(price, "No price found for TSTK/USD")
77 self.assertIsInstance(price, GncPrice)
78
79 def test_nth_price_returns_gnc_price(self):
80 price = self.pricedb.nth_price(self.test_commtest_comm, 0)
81 self.assertIsNotNone(price, "nth_price(TSTK, 0) returned None")
82 self.assertIsInstance(price, GncPrice)
83
84 # -- GncPrice attribute methods --
85
86 def test_get_commodity_returns_gnc_commodity(self):
87 price = self.pricedb.lookup_latest(self.test_commtest_comm, self.usd)
88 self.assertIsNotNone(price)
89 comm = price.get_commodity()
90 self.assertIsInstance(comm, GncCommodity)
91
92 def test_get_currency_returns_gnc_commodity(self):
93 price = self.pricedb.lookup_latest(self.test_commtest_comm, self.usd)
94 self.assertIsNotNone(price)
95 curr = price.get_currency()
96 self.assertIsInstance(curr, GncCommodity)
97
98 def test_get_value_returns_gnc_numeric(self):
99 price = self.pricedb.lookup_latest(self.test_commtest_comm, self.usd)
100 self.assertIsNotNone(price)
101 val = price.get_value()
102 self.assertIsInstance(val, GncNumeric)
103
104 def test_clone_returns_gnc_price(self):
105 price = self.pricedb.lookup_latest(self.test_commtest_comm, self.usd)
106 self.assertIsNotNone(price)
107 cloned = price.clone(self.book)
108 self.assertIsInstance(cloned, GncPrice)
109
110 # -- GncPriceDB list methods --
111
112 def test_lookup_latest_any_currency_returns_list_of_gnc_price(self):
113 prices = self.pricedb.lookup_latest_any_currency(self.test_commtest_comm)
114 self.assertIsInstance(prices, list)
115 self.assertGreater(len(prices), 0,
116 "Expected at least one price for TSTK")
117 for p in prices:
118 self.assertIsInstance(p, GncPrice)
119
120 def test_get_prices_returns_list_of_gnc_price(self):
121 prices = self.pricedb.get_prices(self.test_commtest_comm, self.usd)
122 self.assertIsInstance(prices, list)
123 self.assertGreater(len(prices), 0)
124 for p in prices:
125 self.assertIsInstance(p, GncPrice)
126
127 def test_lookup_nearest_in_time_any_currency(self):
128 date = datetime(2025, 1, 20)
129 prices = self.pricedb.lookup_nearest_in_time_any_currency_t64(
130 self.test_commtest_comm, date)
131 self.assertIsInstance(prices, list)
132 for p in prices:
133 self.assertIsInstance(p, GncPrice)
134
135 def test_lookup_nearest_before_any_currency(self):
136 date = datetime(2025, 7, 1)
137 prices = self.pricedb.lookup_nearest_before_any_currency_t64(
138 self.test_commtest_comm, date)
139 self.assertIsInstance(prices, list)
140 for p in prices:
141 self.assertIsInstance(p, GncPrice)
142
143
144# ---------------------------------------------------------------------------
145# Test: GncLot.get_split_list
146# ---------------------------------------------------------------------------
147class TestGncLotSplitList(TestCase):
148 """Verify that GncLot.get_split_list() returns wrapped Split objects.
149
150 Creates a buy+sell pair on an account then scrubs lots, matching the
151 pattern in test_account.py's test_assignlots.
152 """
153
154 def setUp(self):
155 self.ses = Session()
156 self.book = self.ses.get_book()
157 table = self.book.get_table()
158 currency = table.lookup("CURRENCY", "USD")
159
160 stock = GncCommodity(self.book, "Lot Test", "COMMODITY", "LTX", "LTX", 100000)
161 table.insert(stock)
162
163 self.stock_acct = Account(self.book)
164 self.stock_acct.SetCommodity(stock)
165 root = self.book.get_root_account()
166 root.append_child(self.stock_acct)
167
168 cash_acct = Account(self.book)
169 cash_acct.SetCommodity(currency)
170 root.append_child(cash_acct)
171
172 tx = Transaction(self.book)
173 tx.BeginEdit()
174 tx.SetCurrency(currency)
175 tx.SetDateEnteredSecs(datetime.now())
176 tx.SetDatePostedSecs(datetime.now())
177
178 # Buy 1.3 shares
179 s1 = Split(self.book)
180 s1.SetParent(tx)
181 s1.SetAccount(self.stock_acct)
182 s1.SetAmount(GncNumeric(13, 10))
183 s1.SetValue(GncNumeric(100, 1))
184
185 s2 = Split(self.book)
186 s2.SetParent(tx)
187 s2.SetAccount(cash_acct)
188 s2.SetAmount(GncNumeric(-100, 1))
189 s2.SetValue(GncNumeric(-100, 1))
190
191 # Sell 1.3 shares
192 s3 = Split(self.book)
193 s3.SetParent(tx)
194 s3.SetAccount(self.stock_acct)
195 s3.SetAmount(GncNumeric(-13, 10))
196 s3.SetValue(GncNumeric(-100, 1))
197
198 s4 = Split(self.book)
199 s4.SetParent(tx)
200 s4.SetAccount(cash_acct)
201 s4.SetAmount(GncNumeric(100, 1))
202 s4.SetValue(GncNumeric(100, 1))
203
204 tx.CommitEdit()
205 self.stock_acct.ScrubLots()
206
207 def tearDown(self):
208 self.ses.end()
209
210 def test_lot_exists(self):
211 lots = self.stock_acct.GetLotList()
212 self.assertIsInstance(lots, list)
213 self.assertGreater(len(lots), 0, "ScrubLots should have created a lot")
214 for lot in lots:
215 self.assertIsInstance(lot, GncLot)
216
217 def test_get_split_list_returns_splits(self):
218 lots = self.stock_acct.GetLotList()
219 self.assertGreater(len(lots), 0)
220 splits = lots[0].get_split_list()
221 self.assertIsInstance(splits, list)
222 self.assertGreater(len(splits), 0, "Lot has no splits")
223 for s in splits:
224 self.assertIsInstance(s, Split)
225
226
227# ---------------------------------------------------------------------------
228# Test: Split.GetNoclosingBalance and Split.GetCapGains return GncNumeric
229# ---------------------------------------------------------------------------
231 """Verify that Split methods returning gnc_numeric by value are wrapped."""
232
233 def setUp(self):
234 self.ses = Session()
235 self.book = self.ses.get_book()
236 table = self.book.get_table()
237 currency = table.lookup("CURRENCY", "USD")
238
239 root = self.book.get_root_account()
240 acct = Account(self.book)
241 acct.SetCommodity(currency)
242 root.append_child(acct)
243
244 other = Account(self.book)
245 other.SetCommodity(currency)
246 root.append_child(other)
247
248 tx = Transaction(self.book)
249 tx.BeginEdit()
250 tx.SetCurrency(currency)
251 tx.SetDateEnteredSecs(datetime.now())
252 tx.SetDatePostedSecs(datetime.now())
253
254 self.split = Split(self.book)
255 self.split.SetParent(tx)
256 self.split.SetAccount(acct)
257 self.split.SetAmount(GncNumeric(100, 1))
258 self.split.SetValue(GncNumeric(100, 1))
259
260 s2 = Split(self.book)
261 s2.SetParent(tx)
262 s2.SetAccount(other)
263 s2.SetAmount(GncNumeric(-100, 1))
264 s2.SetValue(GncNumeric(-100, 1))
265
266 tx.CommitEdit()
267
268 def tearDown(self):
269 self.ses.end()
270
271 def test_get_noclosing_balance_returns_gnc_numeric(self):
272 val = self.split.GetNoclosingBalance()
273 self.assertIsInstance(val, GncNumeric)
274
275 def test_get_cap_gains_returns_gnc_numeric(self):
276 val = self.split.GetCapGains()
277 self.assertIsInstance(val, GncNumeric)
278
279
280# ---------------------------------------------------------------------------
281# Test: Account.get_currency_or_parent
282# ---------------------------------------------------------------------------
284 """Verify Account.get_currency_or_parent() returns GncCommodity."""
285
286 def setUp(self):
287 self.ses = Session()
288 self.book = self.ses.get_book()
289 table = self.book.get_table()
290 self.usd = table.lookup("CURRENCY", "USD")
291
292 root = self.book.get_root_account()
293 self.acct = Account(self.book)
294 self.acct.SetCommodity(self.usd)
295 root.append_child(self.acct)
296
297 def tearDown(self):
298 self.ses.end()
299
300 def test_get_currency_or_parent_returns_commodity(self):
301 result = self.acct.get_currency_or_parent()
302 self.assertIsNotNone(result)
303 self.assertIsInstance(result, GncCommodity)
304
305
306# ---------------------------------------------------------------------------
307# Test: GncCommodity.obtain_twin wrapping
308# ---------------------------------------------------------------------------
310 """Verify GncCommodity.obtain_twin(book) returns GncCommodity."""
311
312 def test_obtain_twin_same_book(self):
313 ses = Session()
314 book = ses.get_book()
315 table = book.get_table()
316 usd = table.lookup("CURRENCY", "USD")
317 self.assertIsNotNone(usd)
318 twin = usd.obtain_twin(book)
319 self.assertIsInstance(twin, GncCommodity)
320 ses.end()
321
322
323# ---------------------------------------------------------------------------
324# Test: GncCommodity.get_namespace_ds wrapping
325# ---------------------------------------------------------------------------
327 """Verify GncCommodity.get_namespace_ds() returns
328 GncCommodityNamespace."""
329
330 def test_get_namespace_ds(self):
331 ses = Session()
332 book = ses.get_book()
333 table = book.get_table()
334 usd = table.lookup("CURRENCY", "USD")
335 self.assertIsNotNone(usd)
336 ns = usd.get_namespace_ds()
337 self.assertIsInstance(ns, GncCommodityNamespace)
338 ses.end()
339
340
341# ---------------------------------------------------------------------------
342# Test: SWIG typemap compatibility (wrapper → instance unwrap)
343# ---------------------------------------------------------------------------
345 """Verify that passing a ClassFromFunctions wrapper to a C function
346 still works (via the SWIG typemap) and emits a DeprecationWarning."""
347
349 """Passing a GncPrice wrapper to gnucash_core_c should emit
350 DeprecationWarning and still return a valid result."""
351 from gnucash import gnucash_core_c as gc
352
353 price = self.pricedb.lookup_latest(self.test_comm, self.usd)
354 if price is None:
355 self.skipTest("No price data")
356
357 self.assertIsInstance(price, GncPrice)
358
359 with warnings.catch_warnings(record=True) as w:
360 warnings.simplefilter("always")
361 # Pass the wrapper object directly — typemap should unwrap it
362 comm_instance = gc.gnc_price_get_commodity(price)
363 dep_warnings = [x for x in w
364 if issubclass(x.category, DeprecationWarning)]
365 self.assertGreater(len(dep_warnings), 0,
366 "Expected DeprecationWarning from typemap")
367
369 """Passing price.instance directly should NOT emit a warning."""
370 from gnucash import gnucash_core_c as gc
371
372 price = self.pricedb.lookup_latest(self.test_comm, self.usd)
373 if price is None:
374 self.skipTest("No price data")
375
376 with warnings.catch_warnings(record=True) as w:
377 warnings.simplefilter("always")
378 comm_instance = gc.gnc_price_get_commodity(price.instance)
379 dep_warnings = [x for x in w
380 if issubclass(x.category, DeprecationWarning)]
381 self.assertEqual(len(dep_warnings), 0,
382 "Unexpected DeprecationWarning for .instance")
383
384
385# ---------------------------------------------------------------------------
386# Test: GncPriceDB.get_*_price returns GncNumeric
387# ---------------------------------------------------------------------------
389 """Verify that get_latest_price, get_nearest_price, and
390 get_nearest_before_price return GncNumeric instead of raw
391 _gnc_numeric."""
392
393 def test_get_latest_price_returns_gnc_numeric(self):
394 val = self.pricedb.get_latest_price(self.test_commtest_comm, self.usdusd)
395 self.assertIsInstance(val, GncNumeric)
396 self.assertNotEqual(float(val), 0.0,
397 "Expected a non-zero price for TSTK/USD")
398
399 def test_get_nearest_price_returns_gnc_numeric(self):
400 date = datetime(2025, 1, 20)
401 val = self.pricedb.get_nearest_price(self.test_commtest_comm, self.usdusd, date)
402 self.assertIsInstance(val, GncNumeric)
403
404 def test_get_nearest_before_price_returns_gnc_numeric(self):
405 date = datetime(2025, 7, 1)
406 val = self.pricedb.get_nearest_before_price(
407 self.test_commtest_comm, self.usdusd, date)
408 self.assertIsInstance(val, GncNumeric)
409
411 """Verify the returned GncNumeric supports arithmetic."""
412 val = self.pricedb.get_latest_price(self.test_commtest_comm, self.usdusd)
413 doubled = val + val
414 self.assertIsInstance(doubled, GncNumeric)
415 self.assertAlmostEqual(float(doubled), float(val) * 2, places=6)
416
417
418# ---------------------------------------------------------------------------
419# Test: ClassFromFunctions double-wrap protection
420# ---------------------------------------------------------------------------
422 """Verify that passing a wrapper object as instance= to a wrapper
423 class constructor unwraps it instead of creating a broken object."""
424
425 def test_gnc_numeric_double_wrap(self):
426 original = GncNumeric(7, 3)
427 double = GncNumeric(instance=original)
428 self.assertEqual(double.num(), 7)
429 self.assertEqual(double.denom(), 3)
430
431 def test_gnc_numeric_double_wrap_arithmetic(self):
432 original = GncNumeric(1, 4)
433 double = GncNumeric(instance=original)
434 result = double + GncNumeric(3, 4)
435 self.assertAlmostEqual(float(result), 1.0, places=6)
436
437 def test_gnc_commodity_double_wrap(self):
438 ses = Session()
439 book = ses.get_book()
440 table = book.get_table()
441 usd = table.lookup("CURRENCY", "USD")
442 double = GncCommodity(instance=usd)
443 self.assertIsInstance(double, GncCommodity)
444 self.assertEqual(double.get_mnemonic(), "USD")
445 ses.end()
446
448 """Passing a raw SWIG proxy as instance= must still work."""
449 from gnucash import gnucash_core_c as gc
450 raw = gc.gnc_numeric_create(5, 2)
451 val = GncNumeric(instance=raw)
452 self.assertEqual(val.num(), 5)
453 self.assertEqual(val.denom(), 2)
454
455
456if __name__ == '__main__':
457 main()