...
 
Commits (3)
repo: 26cf45c5599ff39ae649d05d52e820039256157e
node: 72fe17518253760048e781ea7c870b48267ec0de
branch: default
tag: 2.2.0
......@@ -4,6 +4,25 @@
Note: This file contains a list changes in the 'default' branch.
2019-01-05
- Merged pull request #85
(https://bitbucket.org/blais/beancount/pull-requests/85) from hardikar,
adding function grepn() to the shell.
- Merged pull request #86
(https://bitbucket.org/blais/beancount/pull-requests/86) from hardikar,
adding function subst() to the shell.
- Merged pull request #89
(https://bitbucket.org/blais/beancount/pull-requests/89) from hardikar,
adding functions date_diff(), date_add() to the shell.
- Implemented best compression for zipfile in bean-bake as per Justus
Pendleton's suggestion. For context, see this:
https://groups.google.com/d/msgid/beancount/de75671d-532a-4deb-bd0f-fd9377e63753%40googlegroups.com?utm_medium=email&utm_source=footer
2018-12-18
- Fixed #214: Add directory of current filename to sys.path with new option
......
......@@ -15,6 +15,7 @@ Contributors (in alphabetical order):
- Jason Chu (bug fixes, plugins, lots of comments)
- Jeff Brantley (windows installation improvements)
- Johannes Harms (bug fixes, forecast plugin improvement)
- Justus Pendleton (bug fixes, patches)
- Mark Hansen (docs)
- Markus Teufelberger (bug fix / testing)
- Martin Michlmayr (numerous tickets, CI setup, bug fixes and improvements)
......
......@@ -13,7 +13,7 @@ if (sys.version_info.major, sys.version_info.minor) < (3, 3):
raise ImportError("Python 3.3 or above is required")
__version__ = '2.2.0-dev'
__version__ = '2.2.0'
# Remove annoying warnings in third-party modules.
......
......@@ -442,7 +442,7 @@ def replace_currencies(postings, refer_groups):
for refer in sorted(refers, key=lambda r: r.index):
posting = postings[refer.index]
units = posting.units
if units is MISSING:
if units is MISSING or units is None:
posting = posting._replace(units=Amount(MISSING, refer.units_currency))
else:
replace = False
......@@ -891,6 +891,8 @@ def interpolate_group(postings, balances, currency, tolerances):
# Replace the number in the posting.
if new_posting is not None:
# Set meta-data on the new posting to indicate it was interpolated.
if new_posting.meta is None:
new_posting = new_posting._replace(meta={})
new_posting.meta[interpolate.AUTOMATIC_META] = True
# Convert augmenting posting costs from CostSpec to a corresponding
......
......@@ -30,6 +30,7 @@ from beancount.parser import booking_full as bf
from beancount.parser import booking_method as bm
from beancount.parser import booking_test
from beancount.parser import cmptest
from beancount.parser import options
from beancount import loader
......@@ -2706,6 +2707,31 @@ class TestBasicBooking(_BookingTestBase):
Assets:Account 2 HOOL {101.00 USD, 2015-10-01}
"""
class TestBookingApi(unittest.TestCase):
def test_book_single(self):
txn = data.Transaction(data.new_metadata(__file__, 0),
datetime.date(2018, 12, 31),
'*',
'Payee',
'Narration',
None,
None,
[])
data.create_simple_posting(txn, 'Assets:Cash', 100.00, 'USD')
data.create_simple_posting(txn, 'Expenses:Stuff', None, None)
for method in Booking:
methods = collections.defaultdict(lambda: method)
entries, errors = bf.book([txn], options.OPTIONS_DEFAULTS.copy(), methods)
self.assertEqual(len(entries), 1)
self.assertEqual(len(errors), 0)
entry = entries[0]
self.assertEqual(len(entry.postings), 2)
self.assertEqual(entry.postings[0], txn.postings[0])
self.assertNotEqual(entry.postings[1], txn.postings[1])
self.assertEqual(entry.postings[1].account, 'Expenses:Stuff')
self.assertEqual(entry.postings[1].units, A('-100.00 USD'))
# FIXME: TODO - Rewrite these tests. See average_test.py.
class TestBook(unittest.TestCase):
......
......@@ -27,6 +27,7 @@ from beancount.core import getters
from beancount.core import convert
from beancount.core import prices
from beancount.query import query_compile
from beancount.utils.date_utils import parse_date_liberally
# Non-agreggating functions. These functionals maintain no state.
......@@ -269,6 +270,32 @@ class Grep(query_compile.EvalFunction):
if match:
return match.group(0)
class GrepN(query_compile.EvalFunction):
"Match a pattern with subgroups against a string and return the subgroup at the index"
__intypes__ = [str, str, int]
def __init__(self, operands):
super().__init__(operands, str)
def __call__(self, context):
args = self.eval_args(context)
match = re.search(args[0], args[1])
if match:
return match.group(args[2])
class Subst(query_compile.EvalFunction):
"Substitute leftmost non-overlapping occurrences of pattern by replacement."
__intypes__ = [str, str, str]
def __init__(self, operands):
super().__init__(operands, str)
def __call__(self, context):
args = self.eval_args(context)
if any([arg is None for arg in args]):
return None
return re.sub(args[0], args[1], args[2])
class OpenDate(query_compile.EvalFunction):
"Get the date of the open directive of the account."
__intypes__ = [str]
......@@ -753,6 +780,70 @@ class PosSignInventory(query_compile.EvalFunction):
sign = account_types.get_account_sign(account, context.account_types)
return inv if sign >= 0 else -inv
class Coalesce(query_compile.EvalFunction):
"Return the first non-null argument"
__intypes__ = [object, object]
def __init__(self, operands):
super().__init__(operands, object)
def __call__(self, context):
args = self.eval_args(context)
for arg in args:
if arg is not None:
return arg
return None
class Date(query_compile.EvalFunction):
"Construct a date with year, month, day arguments"
__intypes__ = [int, int, int]
def __init__(self, operands):
super().__init__(operands, inventory.Inventory)
def __call__(self, context):
args = self.eval_args(context)
year, month, day = args
return datetime.date(year, month, day)
class ParseDate(query_compile.EvalFunction):
"Construct a date with year, month, day arguments"
__intypes__ = [str]
def __init__(self, operands):
super().__init__(operands, inventory.Inventory)
def __call__(self, context):
args = self.eval_args(context)
return parse_date_liberally(args[0])
class DateDiff(query_compile.EvalFunction):
"Calculates the difference (in days) between two dates"
__intypes__ = [datetime.date, datetime.date]
def __init__(self, operands):
super().__init__(operands, int)
def __call__(self, context):
args = self.eval_args(context)
if args[0] is None or args[1] is None:
return None
return (args[0] - args[1]).days
class DateAdd(query_compile.EvalFunction):
"Adds/subtracts number of days from the given date"
__intypes__ = [datetime.date, int]
def __init__(self, operands):
super().__init__(operands, datetime.date)
def __call__(self, context):
args = self.eval_args(context)
if args[0] is None or args[1] is None:
return None
return args[0] + datetime.timedelta(days=args[1])
# FIXME: Why do I need to specify the arguments here? They are already derived
......@@ -774,6 +865,8 @@ SIMPLE_FUNCTIONS = {
'parent' : Parent,
'leaf' : Leaf,
'grep' : Grep,
'grepn' : GrepN,
'subst' : Subst,
'open_date' : OpenDate,
'close_date' : CloseDate,
'meta' : Meta,
......@@ -794,6 +887,10 @@ SIMPLE_FUNCTIONS = {
'day' : Day,
'weekday' : Weekday,
'today' : Today,
('date', int, int, int) : Date,
('date', str) : ParseDate,
'date_diff' : DateDiff,
'date_add' : DateAdd,
('convert', amount.Amount, str) : ConvertAmount,
('convert', amount.Amount, str, datetime.date) : ConvertAmountWithDate,
('convert', position.Position, str) : ConvertPosition,
......@@ -816,6 +913,7 @@ SIMPLE_FUNCTIONS = {
('possign', amount.Amount, str) : PosSignAmount,
('possign', position.Position, str) : PosSignPosition,
('possign', inventory.Inventory, str) : PosSignInventory,
'coalesce' : Coalesce,
# FIXME: 'only' should be removed.
'only' : OnlyInventory,
......
......@@ -141,3 +141,134 @@ class TestEnv(unittest.TestCase):
rtypes, rrows = query.run_query(entries, options_map,
'SELECT ANY_META("empty") as m')
self.assertEqual([(None,)], rrows)
@parser.parse_doc()
def test_GrepN(self, entries, _, options_map):
"""
2016-11-20 * "prev match in context next"
Assets:Banking 1 USD
"""
rtypes, rrows = query.run_query(entries, options_map, '''
SELECT GREPN("in", narration, 0) as m
''')
self.assertEqual([('in',)], rrows)
rtypes, rrows = query.run_query(entries, options_map, '''
SELECT GREPN("match (.*) context", narration, 1) as m
''')
self.assertEqual([('in',)], rrows)
rtypes, rrows = query.run_query(entries, options_map, '''
SELECT GREPN("(.*) in (.*)", narration, 2) as m
''')
self.assertEqual([('context next',)], rrows)
rtypes, rrows = query.run_query(entries, options_map, '''
SELECT GREPN("ab(at)hing", "abathing", 1) as m
''')
self.assertEqual([('at',)], rrows)
@parser.parse_doc()
def test_Subst(self, entries, _, options_map):
"""
2016-11-20 * "I love candy"
Assets:Banking -1 USD
2016-11-21 * "Buy thing thing"
Assets:Cash -1 USD
"""
rtypes, rrows = query.run_query(entries, options_map, '''
SELECT SUBST("[Cc]andy", "carrots", narration) as m where date = 2016-11-20
''')
self.assertEqual([('I love carrots',)], rrows)
rtypes, rrows = query.run_query(entries, options_map, '''
SELECT SUBST("thing", "t", narration) as m where date = 2016-11-21
''')
self.assertEqual([('Buy t t',)], rrows)
rtypes, rrows = query.run_query(entries, options_map, '''
SELECT SUBST("random", "t", narration) as m where date = 2016-11-21
''')
self.assertEqual([('Buy thing thing',)], rrows)
rtypes, rrows = query.run_query(entries, options_map, '''
SELECT SUBST("(love)", "\\1 \\1", narration) as m where date = 2016-11-20
''')
self.assertEqual([('I love love candy',)], rrows)
rtypes, rrows = query.run_query(entries, options_map, '''
SELECT SUBST("Assets:.*", "Savings", account) as a, str(sum(position)) as p
''')
self.assertEqual([('Savings', '(-2 USD)')], rrows)
@parser.parse_doc()
def test_Coalesce(self, entries, _, options_map):
"""
2016-11-20 *
Assets:Banking 1 USD
"""
rtypes, rrows = query.run_query(entries, options_map,
'SELECT COALESCE(account, price) as m')
self.assertEqual([('Assets:Banking',)], rrows)
rtypes, rrows = query.run_query(entries, options_map,
'SELECT COALESCE(price, account) as m')
self.assertEqual([('Assets:Banking',)], rrows)
rtypes, rrows = query.run_query(entries, options_map,
'SELECT COALESCE(price, cost_number) as m')
self.assertEqual([(None,)], rrows)
rtypes, rrows = query.run_query(entries, options_map,
'SELECT COALESCE(narration, account) as m')
self.assertEqual([('',)], rrows)
@parser.parse_doc()
def test_Date(self, entries, _, options_map):
"""
2016-11-20 * "ok"
Assets:Banking 1 USD
"""
rtypes, rrows = query.run_query(entries, options_map,
'SELECT date(2020, 1, 2) as m')
self.assertEqual([(datetime.date(2020, 1, 2),)], rrows)
rtypes, rrows = query.run_query(entries, options_map,
'SELECT date(year, month, 1) as m')
self.assertEqual([(datetime.date(2016, 11, 1),)], rrows)
with self.assertRaisesRegex(ValueError, "day is out of range for month"):
rtypes, rrows = query.run_query(entries, options_map,
'SELECT date(2020, 2, 32) as m')
rtypes, rrows = query.run_query(entries, options_map,
'SELECT date("2020-01-02") as m')
self.assertEqual([(datetime.date(2020, 1, 2),)], rrows)
rtypes, rrows = query.run_query(entries, options_map,
'SELECT date("2016/11/1") as m')
self.assertEqual([(datetime.date(2016, 11, 1),)], rrows)
@parser.parse_doc()
def test_DateDiffAdjust(self, entries, _, options_map):
"""
2016-11-20 * "ok"
Assets:Banking -1 STOCK { 5 USD, 2016-10-30 }
"""
rtypes, rrows = query.run_query(entries, options_map,
'SELECT date_diff(date, cost_date) as m')
self.assertEqual([(21,)], rrows)
rtypes, rrows = query.run_query(entries, options_map,
'SELECT date_diff(cost_date, date) as m')
self.assertEqual([(-21,)], rrows)
rtypes, rrows = query.run_query(entries, options_map,
'SELECT date_add(date, 1) as m')
self.assertEqual([(datetime.date(2016, 11, 21),)], rrows)
rtypes, rrows = query.run_query(entries, options_map,
'SELECT date_add(date, -1) as m')
self.assertEqual([(datetime.date(2016, 11, 19),)], rrows)
......@@ -8,14 +8,15 @@ fetched directory contents to the archive and delete them.
__copyright__ = "Copyright (C) 2014-2016 Martin Blais"
__license__ = "GNU GPLv2"
from os import path
import functools
import importlib
import logging
import os
import subprocess
import shutil
import shlex
import re
from os import path
import shlex
import shutil
import subprocess
import zipfile
import lxml.html
......@@ -200,7 +201,21 @@ def archive_zip(directory, archive):
directory: A string, the name of the directory to archive.
archive: A string, the name of the file to output.
"""
with file_utils.chdir(directory), zipfile.ZipFile(archive, 'w') as archfile:
# Figure out optimal level of compression among the supported ones in this
# installation.
for spec, compression in [
('lzma', zipfile.ZIP_LZMA),
('bz2', zipfile.ZIP_BZIP2),
('zlib', zipfile.ZIP_DEFLATED)]:
if importlib.util.find_spec(spec):
zip_compression = compression
break
else:
# Default is no compression.
zip_compression = zipfile.ZIP_STORED
with file_utils.chdir(directory), zipfile.ZipFile(
archive, 'w', compression=zip_compression) as archfile:
for root, dirs, files in os.walk(directory):
for filename in files:
relpath = path.relpath(path.join(root, filename), directory)
......
beancount (2.2.0-1) unstable; urgency=medium
* New upstream release.
-- Stefano Zacchiroli <zack@debian.org> Tue, 08 Jan 2019 21:31:17 +0100
beancount (2.1.3+hg20181225-3) unstable; urgency=medium
* rules: do not ship __pycache__ dirs with Python examples
......