/
options.py
586 lines (486 loc) · 23.7 KB
/
options.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
"""
Declaration of options and their default values.
"""
__copyright__ = "Copyright (C) 2013-2016 Martin Blais"
__license__ = "GNU GPLv2"
import collections
import io
import re
import textwrap
from beancount.core.number import D
from beancount.core import data
from beancount.core import account_types
from beancount.core import account
from beancount.core import display_context
def options_validate_processing_mode(value):
"""Validate the options processing mode.
Args:
value: A string, the value provided as option.
Returns:
The new value, converted, if the conversion is successful.
Raises:
ValueError: If the value is invalid.
"""
if value not in ('raw', 'default'):
raise ValueError("Invalid value '{}'".format(value))
return value
def options_validate_plugin(value):
"""Validate the plugin option.
Args:
value: A string, the value provided as option.
Returns:
The new value, converted, if the conversion is successful.
Raises:
ValueError: If the value is invalid.
"""
# Process the 'plugin' option specially: accept an optional
# argument from it. NOTE: We will eventually phase this out and
# replace it by a dedicated 'plugin' directive.
match = re.match('(.*):(.*)', value)
if match:
plugin_name, plugin_config = match.groups()
else:
plugin_name, plugin_config = value, None
return (plugin_name, plugin_config)
def options_validate_tolerance(value):
"""Validate the tolerance option.
Args:
value: A string, the value provided as option.
Returns:
The new value, converted, if the conversion is successful.
Raises:
ValueError: If the value is invalid.
"""
return D(value)
def options_validate_tolerance_map(value):
"""Validate an option with a map of currency/tolerance pairs in a string.
Args:
value: A string, the value provided as option.
Returns:
The new value, converted, if the conversion is successful.
Raises:
ValueError: If the value is invalid.
"""
# Process the setting of a key-value, whereby the value is a Decimal
# representation.
match = re.match('(.*):(.*)', value)
if not match:
raise ValueError("Invalid value '{}'".format(value))
currency, tolerance_str = match.groups()
return (currency, D(tolerance_str))
def options_validate_boolean(value):
"""Validate a boolean option.
Args:
value: A string, the value provided as option.
Returns:
The new value, converted, if the conversion is successful.
Raises:
ValueError: If the value is invalid.
"""
return value.lower() in ('1', 'true', 'yes')
def options_validate_booking_method(value):
"""Validate a booking method name.
Args:
value: A string, the value provided as option.
Returns:
The new value, converted, if the conversion is successful.
Raises:
ValueError: If the value is invalid.
"""
try:
return data.Booking[value]
except KeyError as exc:
raise ValueError(str(exc)) from exc
# List of option groups, with their description, option names and default
# values.
OptGroup = collections.namedtuple('OptGroup',
'description options')
# An option description.
#
# Attributes:
# name: A string, the short name of the option, as used in the syntax.
# default_value: The default value for the option. If an option may
# show up multiple times, should be a list or a dict.
# example_value: The value to be rendered in the documentation. Even if
# the value may be specified multiple times, this should just be an
# example string for the user to model itself on.
# converter: A function object to be called to convert or validate the
# option during parsing, or None, if no conversion is necessary. The
# callable must either successfully return with the parsed value, or
# raise a ValueError for the handler to report an error to the parser.
# deprecated: A string, a message set if the option is deprecated. This is
# used to issue suitable warnings when options aren't honored or about
# not to be anymore.
# alias: A string or None; if set, this option automatically gets
# translated to this alias. This is present to support renaming of
# option names.
OptDesc = collections.namedtuple(
'OptDesc',
'name default_value example_value converter deprecated alias')
UNSET = object()
# pylint: disable=invalid-name
def Opt(name, default_value,
example_value=UNSET,
converter=None,
deprecated=False,
alias=None):
"""Alternative constructor for OptDesc, with default values.
Args:
name: See OptDesc.
default_value: See OptDesc.
example_value: See OptDesc.
converter: See OptDesc.
deprecated: See OptDesc.
alias: See OptDesc.
Returns:
An instance of OptDesc.
"""
if example_value is UNSET:
example_value = default_value
return OptDesc(name, default_value, example_value, converter, deprecated, alias)
_TYPES = account_types.DEFAULT_ACCOUNT_TYPES
# Options that consist of data produced as a by-product of the parsing process.
# These options cannot be input by the user. This is essentially read-only state
# that is conceptually separate from the input options.
OUTPUT_OPTION_GROUPS = [
OptGroup("""
The name of the top-level Beancount input file parsed from which the
contents of the ledger have been extracted. This may be None, if no file
was used.
""", [Opt("filename", None)]),
OptGroup("""
A list of other filenames to include. This is output from the parser and
processed by the loader but the list should otherwise have been cleared by the
time it gets to the top-level loader.load_*() function that invoked it.
The filenames are absolute. Relative include filenames are resolved against
the file that contains the include directives.
This is used in the parser, but also, the loader sets this list to the
full list of parsed absolute filenames in the options map. This is how you
can find out the entire list of files involved in a Beancount load
procedure.
""", [Opt("include", [], "some-other-file.beancount")]),
OptGroup("""
A hash of some of the input data. This is used to supplement the
timestamps of the input files for the purpose of load caching. We
typically hash the sizes of the files or perhaps even some of the
contents, or determine any of the inputs have changed beyond the
timestamps of the input files. (Internal use only; do not rely on this.)
""", [Opt("input_hash", "", "841ee3be9acef165feba2342")]),
OptGroup("""
An instance of DisplayContext, which is used to format numbers for output
with precision inferred from that in the input file. This is created
automatically by the parser.
""", [Opt("dcontext", display_context.DisplayContext())]),
OptGroup("""
A set of all the commodities that we have seen in the file.
This is mainly used for efficiency, best computed once at parse time.
""", [Opt("commodities", set())]),
OptGroup("""
A list of Python modules containing transformation functions to run the
entries through after parsing. The parser reads the entries as they are,
transforms them through a list of standard functions, such as balance
checks and inserting padding entries, and then hands the entries over to
those plugins to add more auto-generated goodies. The list is a list of
pairs/tuples, in the format (plugin-name, plugin-configuration). The
plugin-name should be the name of a Python module to import, and within
the module we expect a special '__plugins__' attribute that should list
the name of transform functions to run the entries through. The
plugin-configuration argument is an optional string to be provided by the
user. Each function accepts a pair of (entries, options_map) and should
return a pair of (new entries, error instances). If a plugin configuration
is provided, it is provided as an extra argument to the plugin function.
Errors should not be printed out the output, they will be converted to
strings by the loader and displayed as dictated by the output medium.
""", [Opt("plugin", [], "beancount.plugins.module_name",
converter=options_validate_plugin)]),
]
# Options that are visible to the user and that can be set.
PUBLIC_OPTION_GROUPS = [
OptGroup("""
The title of this ledger / input file. This shows up at the top of every
page.
""", [Opt("title", "Beancount", "Joe Smith's Personal Ledger")]),
OptGroup("""
Root names of every account. This can be used to customize your category
names, so that if you prefer "Revenue" over "Income" or "Capital" over
"Equity", you can set them here. The account names in your input files
must match, and the parser will validate these. You should place these
options at the beginning of your file, because they affect how the parser
recognizes account names.
""", [
Opt("name_assets", _TYPES.assets),
Opt("name_liabilities", _TYPES.liabilities),
Opt("name_equity", _TYPES.equity),
Opt("name_income", _TYPES.income),
Opt("name_expenses", _TYPES.expenses),
]),
OptGroup("""
Leaf name of the equity account used for summarizing previous transactions
into opening balances.
""", [Opt("account_previous_balances", "Opening-Balances")]),
OptGroup("""
Leaf name of the equity account used for transferring previous retained
earnings from income and expenses accrued before the beginning of the
exercise into the balance sheet.
""", [Opt("account_previous_earnings", "Earnings:Previous")]),
OptGroup("""
Leaf name of the equity account used for inserting conversions that will
zero out remaining amounts due to transfers before the opening date. This
will essentially "fixup" the basic accounting equation due to the errors
that priced conversions introduce.
""", [Opt("account_previous_conversions", "Conversions:Previous")]),
OptGroup("""
Leaf name of the equity account used for transferring current retained
earnings from income and expenses accrued during the current exercise into
the balance sheet. This is most often called "Net Income".
""", [Opt("account_current_earnings", "Earnings:Current")]),
# TODO(blais): Remove this option, it's not used anymore.
OptGroup("""
Leaf name of the equity account used for inserting conversions that will
zero out remaining amounts due to transfers during the exercise period.
""", [Opt("account_current_conversions", "Conversions:Current",
deprecated=('This was used by reports, not used anymore.'))]),
# TODO(blais): Remove this option, it's not used anymore.
OptGroup("""
The name of an account to be used to post unrealized gains to. This is used
when making any kind of conversion from cost to price on a balance sheet
(or any realization). The amount inserted - the difference between book
value and market value - has to be posted to a gains account to keep the
balance on the sheet. This has no effect on behavior, other than providing
a configurable account name for such postings to occur.
""", [Opt("account_unrealized_gains",
"Earnings:Unrealized", "Earnings:Unrealized",
deprecated=('This was used by reports, not used anymore.'))]),
OptGroup("""
The name of an account to be used to post to and accumulate rounding error.
This is unset and this feature is disabled by default; setting this value to
an account name will automatically enable the addition of postings on all
transactions that have a residual amount.
""", [Opt("account_rounding", None, "Rounding")]),
OptGroup("""
The imaginary currency used to convert all units for conversions at a
degenerate rate of zero. This can be any currency name that isn't used in
the rest of the ledger. Choose something unique that makes sense in your
language.
""", [Opt("conversion_currency", "NOTHING")]),
OptGroup("""
Mappings of currency to the tolerance used when it cannot be inferred
automatically. The tolerance at hand is the one used for verifying (1)
that transactions balance, (2) explicit balance checks from 'balance'
directives balance, and (3) in the tolerance used for padding (from the
'pad' directive).
The values must be strings in the following format:
<currency>:<tolerance>
for example, 'USD:0.005'.
By default, the tolerance allowed for currencies without an inferred value
is zero. As a special case, this value, that is, the fallback value used
for all currencies without an explicit default can be overridden using the
'*' currency, like this: '*:0.5'. Used by itself, this last example sets
the fallabck tolerance as '0.5' for all currencies.
For detailed documentation about how tolerances are handled, see this doc:
http://furius.ca/beancount/doc/tolerances
""", [Opt("inferred_tolerance_default", {}, "CHF:0.01",
converter=options_validate_tolerance_map)]),
OptGroup("""
A multiplier for inferred tolerance values.
When the tolerance values aren't specified explicitly via the
'inferred_tolerance_default' option, the tolerance is inferred from the
numbers in the input file. For example, if a transaction has posting with
a value like '32.424 CAD', the tolerance for CAD will be inferred to be
0.001 times some multiplier. This is the muliplier value.
We normally assume that the institution we're reproducing this posting
from applies rounding, and so the default value for the multiplier is
0.5, that is, half of the smallest digit encountered.
You can customize this multiplier by changing this option, typically
expanding it to account for amounts slightly beyond the usual tolerance,
for example, if you deal with institutions with bad of unexpected rounding
behaviour.
For detailed documentation about how tolerances are handled, see this doc:
http://furius.ca/beancount/doc/tolerances
""", [Opt("inferred_tolerance_multiplier", D("0.5"), "1.1",
converter=D)]),
OptGroup("""
Enable a feature that expands the maximum tolerance inferred on
transactions to include values on cost currencies inferred by postings
held at-cost or converted at price. Those postings can imply a tolerance
value by multiplying the smallest digit of the unit by the cost or price
value and taking half of that value.
For example, if a posting has an amount of "2.345 RGAGX {45.00 USD}"
attached to it, it implies a tolerance of 0.001 x 45.00 * M = 0.045 USD
(where M is the inferred_tolerance_multiplier) and this is added to the
mix to enlarge the tolerance allowed for units of USD on that transaction.
All the normally inferred tolerances (see
http://furius.ca/beancount/doc/tolerances) are still taken into account.
Enabling this flag only makes the tolerances potentially wider.
""", [Opt("infer_tolerance_from_cost", False, True)]),
OptGroup("""
A list of directory roots, relative to the CWD, which should be searched
for document files. For the document files to be automatically found they
must have the following filename format: YYYY-MM-DD.(.*)
""", [Opt("documents", [], "/path/to/your/documents/archive")]),
OptGroup("""
A list of currencies that we single out during reporting and create
dedicated columns for. This is used to indicate the main currencies that
you work with in real life. (Refrain from listing all the possible
currencies here, this is not what it is made for; just list the very
principal currencies you use daily only.)
Because our system is agnostic to any unit definition that occurs in the
input file, we use this to display these values in table cells without
their associated unit strings. This allows you to import the numbers in a
spreadsheet (e.g, "101.00 USD" does not get parsed by a spreadsheet
import, but "101.00" does).
If you need to enter a list of operating currencies, you may input this
option multiple times, that is, you repeat the entire directive once for
each desired operating currency.
""", [Opt("operating_currency", [], "USD")]),
OptGroup("""
A boolean, true if the number formatting routines should output commas
as thousand separators in numbers.
""", [Opt("render_commas", False, "TRUE",
converter=options_validate_boolean)]),
OptGroup("""
A string that defines which set of plugins is to be run by the loader: if
the mode is "default", a preset list of plugins are automatically run
before any user plugin. If the mode is "raw", no preset plugins are run at
all, only user plugins are run (the user should explicitly load the
desired list of plugins by using the 'plugin' option. This is useful in case the
user wants full control over the ordering in which the plugins are run).
""", [Opt("plugin_processing_mode", "default", "raw",
converter=options_validate_processing_mode)]),
OptGroup("""
The number of lines beyond which a multi-line string will trigger an
overly long line warning. This warning is meant to help detect a dangling
quote by warning users of unexpectedly long strings.
""", [Opt("long_string_maxlines", 64)]),
OptGroup("""
The booking method to apply to ambiguous reductions of inventory lots.
When a posting is matched against the contents of an account's inventory
to reduce its contents and multiple lots match, the method dictates how
this ambiguity is resolved. Methods include "STRICT" which raises an
error, "FIFO" which selects the oldest lot, and "NONE" which allows any
reduction to be added to the inventory despite the absence of a match
(resulting in mixed inventories).
See the following documents for details:
http://furius.ca/beancount/doc/inventories
http://furius.ca/beancount/doc/proposal-booking
""", [Opt("booking_method", data.Booking.STRICT, "STRICT",
converter=options_validate_booking_method)]),
OptGroup("""
Support the pipe (|) symbol to for transaction separator.
This is only provided as a temporary stopgap to ease transition, and will
be removed eventually. This is why this option is marked as deprecated.
""", [Opt("allow_pipe_separator", False, "TRUE",
converter=options_validate_boolean,
deprecated=('Allowing pipe separator temporary; '
'this will go away eventually.'))]),
OptGroup("""
Allow plugins to produce a None object for the 'tags' and 'links'
attributes of a Transaction instance. By default, without this, those
attributes are now ensured to be a Set type, and an empty frozenset()
instance if there are no values
This is only provided as a temporary mechanism to allow you some time to
port your plugins code.
""", [Opt("allow_deprecated_none_for_tags_and_links", False, "TRUE",
converter=options_validate_boolean,
deprecated=('Allowing None for tags and link '
'will go away eventually.'))]),
OptGroup("""
A boolean, if true, prepend the directory name of the top-level file to
the PYTHONPATH.
""", [Opt("insert_pythonpath", False, "TRUE",
converter=options_validate_boolean)]),
]
OPTION_GROUPS = OUTPUT_OPTION_GROUPS + PUBLIC_OPTION_GROUPS
# A dict of the option names to their descriptors.
OPTIONS = {desc.name: desc
for group in OPTION_GROUPS
for desc in group.options}
# A dict of the option names to their default value.
OPTIONS_DEFAULTS = {desc.name: desc.default_value
for group in OPTION_GROUPS
for desc in group.options}
# A list of options that cannot be modified.
READ_ONLY_OPTIONS = {"filename", "plugin"}
def get_account_types(options):
"""Extract the account type names from the parser's options.
Args:
options: a dict of ledger options.
Returns:
An instance of AccountTypes, that contains all the prefixes.
"""
return account_types.AccountTypes(
*[options[key]
for key in ("name_assets",
"name_liabilities",
"name_equity",
"name_income",
"name_expenses")])
def get_previous_accounts(options):
"""Return account names for the previous earnings, balances and conversion accounts.
Args:
options: a dict of ledger options.
Returns:
A tuple of 3 account objects, for booking previous earnings,
previous balances, and previous conversions.
"""
equity = options['name_equity']
account_previous_earnings = account.join(equity,
options['account_previous_earnings'])
account_previous_balances = account.join(equity,
options['account_previous_balances'])
account_previous_conversions = account.join(equity,
options['account_previous_conversions'])
return (account_previous_earnings,
account_previous_balances,
account_previous_conversions)
def get_current_accounts(options):
"""Return account names for the current earnings and conversion accounts.
Args:
options: a dict of ledger options.
Returns:
A tuple of 2 account objects, one for booking current earnings, and one
for current conversions.
"""
equity = options['name_equity']
account_current_earnings = account.join(equity,
options['account_current_earnings'])
account_current_conversions = account.join(equity,
options['account_current_conversions'])
return (account_current_earnings,
account_current_conversions)
def get_unrealized_account(options):
"""Return the full account name for the unrealized account.
Args:
options: a dict of ledger options.
Returns:
A tuple of 2 account objects, one for booking current earnings, and one
for current conversions.
"""
income = options['name_income']
return account.join(income, options['account_unrealized_gains'])
def list_options():
"""Produce a formatted text of the available options and their description.
Returns:
A string, formatted nicely to be printed in 80 columns.
"""
oss = io.StringIO()
for group in PUBLIC_OPTION_GROUPS:
for desc in group.options:
oss.write('option "{}" "{}"\n'.format(desc.name, desc.example_value))
if desc.deprecated:
oss.write(textwrap.fill(
"THIS OPTION IS DEPRECATED: {}".format(desc.deprecated),
initial_indent=" ",
subsequent_indent=" "))
oss.write('\n\n')
description = ' '.join(line.strip()
for line in group.description.strip().splitlines())
oss.write(textwrap.fill(description,
initial_indent=' ',
subsequent_indent=' '))
oss.write('\n')
if isinstance(desc.default_value, (list, dict, set)):
oss.write('\n')
oss.write(' (This option may be supplied multiple times.)\n')
oss.write('\n\n')
return oss.getvalue()