beancount.parser
Parser module for beancount input files.
beancount.parser.booking
Algorithms for 'booking' inventory, that is, the process of finding a matching lot when reducing the content of an inventory.
beancount.parser.booking.BookingError (tuple)
BookingError(source, message, entry)
beancount.parser.booking.BookingError.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/booking.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.booking.BookingError.__new__(_cls, source, message, entry)
special
staticmethod
Create new instance of BookingError(source, message, entry)
beancount.parser.booking.BookingError.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/booking.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.booking.book(incomplete_entries, options_map)
Book inventory lots and complete all positions with incomplete numbers.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking.py
def book(incomplete_entries, options_map):
"""Book inventory lots and complete all positions with incomplete numbers.
Args:
incomplete_entries: A list of directives, with some postings possibly left
with incomplete amounts as produced by the parser.
options_map: An options dict as produced by the parser.
Returns:
A pair of
entries: A list of completed entries with all their postings completed.
errors: New errors produced during interpolation.
"""
# Get the list of booking methods for each account.
booking_methods = collections.defaultdict(lambda: options_map["booking_method"])
for entry in incomplete_entries:
if isinstance(entry, data.Open) and entry.booking:
booking_methods[entry.account] = entry.booking
# Do the booking here!
entries, booking_errors = booking_full.book(incomplete_entries, options_map,
booking_methods)
# Check for MISSING elements remaining.
missing_errors = validate_missing_eliminated(entries, options_map)
return entries, (booking_errors + missing_errors)
beancount.parser.booking.validate_inventory_booking(entries, unused_options_map, booking_methods)
Validate that no position at cost is allowed to go negative.
This routine checks that when a posting reduces a position, existing or not, that the subsequent inventory does not result in a position with a negative number of units. A negative number of units would only be required for short trades of trading spreads on futures, and right now this is not supported. It would not be difficult to support this, however, but we want to be strict about it, because being pedantic about this is otherwise a great way to detect user data entry mistakes.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking.py
def validate_inventory_booking(entries, unused_options_map, booking_methods):
"""Validate that no position at cost is allowed to go negative.
This routine checks that when a posting reduces a position, existing or not,
that the subsequent inventory does not result in a position with a negative
number of units. A negative number of units would only be required for short
trades of trading spreads on futures, and right now this is not supported.
It would not be difficult to support this, however, but we want to be strict
about it, because being pedantic about this is otherwise a great way to
detect user data entry mistakes.
Args:
entries: A list of directives.
unused_options_map: An options map.
booking_methods: A mapping of account name to booking method, accumulated
in the main loop.
Returns:
A list of errors.
"""
errors = []
balances = collections.defaultdict(inventory.Inventory)
for entry in entries:
if isinstance(entry, data.Transaction):
for posting in entry.postings:
# Update the balance of each posting on its respective account
# without allowing booking to a negative position, and if an error
# is encountered, catch it and return it.
running_balance = balances[posting.account]
position_, _ = running_balance.add_position(posting)
# Skip this check if the booking method is set to ignore it.
if booking_methods.get(posting.account, None) == data.Booking.NONE:
continue
# Check if the resulting inventory is mixed, which is not
# allowed under the STRICT method.
if running_balance.is_mixed():
errors.append(
BookingError(
entry.meta,
("Reducing position results in inventory with positive "
"and negative lots: {}").format(position_),
entry))
return errors
beancount.parser.booking.validate_missing_eliminated(entries, unused_options_map)
Validate that all the missing bits of postings have been eliminated.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking.py
def validate_missing_eliminated(entries, unused_options_map):
"""Validate that all the missing bits of postings have been eliminated.
Args:
entries: A list of directives.
unused_options_map: An options map.
Returns:
A list of errors.
"""
errors = []
for entry in entries:
if isinstance(entry, data.Transaction):
for posting in entry.postings:
units = posting.units
cost = posting.cost
if (MISSING in (units.number, units.currency) or
cost is not None and MISSING in (cost.number, cost.currency,
cost.date, cost.label)):
errors.append(
BookingError(entry.meta,
"Transaction has incomplete elements",
entry))
break
return errors
beancount.parser.booking_full
Full (new) booking implementation.
Problem description:
Interpolation and booking feed on each other, that is, numbers filled in from interpolation might affect the booking process, and numbers derived from the booking process may help carry out interpolation that would otherwise be under-defined. Here's an example of interpolation helping the booking process:
Assume the ante-inventory of Assets:Investments contains two lots of shares of HOOL, one at 100.00 USD and the other at 101.00 USD and apply this transaction:
2015-09-30 *
Assets:Investments -10 HOOL {USD}
Assets:Cash 1000 USD
Income:Gains -200 USD
Interpolation is unambiguously able to back out a cost of 100 USD / HOOL, which would then result in an unambiguous booking result.
On the other hand, consider this transaction:
2015-09-30 *
Assets:Investments -10 HOOL {USD}
Assets:Cash 1000 USD
Income:Gains
Now the interpolation cannot succeed. If the Assets:Investments account is configured to use the FIFO method, the 10 oldest shares would be selected for the cost, and we could then interpolate the capital gains correctly.
First observation: The second case is much more frequent than the first, and the first is easily resolved manually by requiring a particular cost be specified. Moreover, in many cases there isn't just a single lot of shares to be reduced from and figuring out the correct set of shares given a target cost is an underspecified problem.
Second observation: Booking can only be achieved for inventory reductions, not for augmentations. Therefore, we should carry out booking on inventory reductions and fail early if reduction is undefined there, and leave inventory augmentations with missing numbers undefined, so that interpolation can fill them in at a later stage.
Note that one case we'd like to but may not be able to handle is of a reduction with interpolated price, like this:
2015-09-30 *
Assets:Investments -10 HOOL {100.00 # USD}
Expenses:Commission 9.95 USD
Assets:Cash 990.05 USD
Therefore we choose to
1) Carry out booking first, on inventory reductions only, and leave inventory augmentations as they are, possibly undefined. The 'cost' attributed of booked postings are converted from CostSpec to Cost. Augmented postings with missing amounts are left as CostSpec instances in order to allow for interpolation of total vs. per-unit amount.
2) Compute interpolations on the resulting postings. Undefined costs for inventory augmentations may be filled in by interpolations at this stage (if possible).
3) Finally, convert the interpolated CostSpec instances to Cost instances.
Improving on this algorithm would require running a loop over the booking and interpolation steps until all numbers are resolved or no more inference can occur. We may consider that for later, as an experimental feature. My hunch is that there are so few cases for which this would be useful that we won't bother improving on the algorithm above.
beancount.parser.booking_full.CategorizationError (tuple)
CategorizationError(source, message, entry)
beancount.parser.booking_full.CategorizationError.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/booking_full.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.booking_full.CategorizationError.__new__(_cls, source, message, entry)
special
staticmethod
Create new instance of CategorizationError(source, message, entry)
beancount.parser.booking_full.CategorizationError.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/booking_full.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.booking_full.InterpolationError (tuple)
InterpolationError(source, message, entry)
beancount.parser.booking_full.InterpolationError.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/booking_full.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.booking_full.InterpolationError.__new__(_cls, source, message, entry)
special
staticmethod
Create new instance of InterpolationError(source, message, entry)
beancount.parser.booking_full.InterpolationError.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/booking_full.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.booking_full.MissingType (Enum)
The type of missing number.
beancount.parser.booking_full.ReductionError (tuple)
ReductionError(source, message, entry)
beancount.parser.booking_full.ReductionError.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/booking_full.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.booking_full.ReductionError.__new__(_cls, source, message, entry)
special
staticmethod
Create new instance of ReductionError(source, message, entry)
beancount.parser.booking_full.ReductionError.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/booking_full.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.booking_full.Refer (tuple)
Refer(index, units_currency, cost_currency, price_currency)
beancount.parser.booking_full.Refer.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/booking_full.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.booking_full.Refer.__new__(_cls, index, units_currency, cost_currency, price_currency)
special
staticmethod
Create new instance of Refer(index, units_currency, cost_currency, price_currency)
beancount.parser.booking_full.Refer.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/booking_full.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.booking_full.SelfReduxError (tuple)
SelfReduxError(source, message, entry)
beancount.parser.booking_full.SelfReduxError.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/booking_full.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.booking_full.SelfReduxError.__new__(_cls, source, message, entry)
special
staticmethod
Create new instance of SelfReduxError(source, message, entry)
beancount.parser.booking_full.SelfReduxError.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/booking_full.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.booking_full.book(entries, options_map, methods)
Interpolate missing data from the entries using the full historical algorithm. See the internal implementation _book() for details. This method only stripes some of the return values.
See _book() for arguments and return values.
Source code in beancount/parser/booking_full.py
def book(entries, options_map, methods):
"""Interpolate missing data from the entries using the full historical algorithm.
See the internal implementation _book() for details.
This method only stripes some of the return values.
See _book() for arguments and return values.
"""
entries, errors, _ = _book(entries, options_map, methods)
return entries, errors
beancount.parser.booking_full.book_reductions(entry, group_postings, balances, methods)
Book inventory reductions against the ante-balances.
This function accepts a dict of (account, Inventory balance) and for each posting that is a reduction against its inventory, attempts to find a corresponding lot or list of lots to reduce the balance with.
-
For reducing lots, the CostSpec instance of the posting is replaced by a Cost instance.
-
For augmenting lots, the CostSpec instance of the posting is left alone, except for its date, which is inherited from the parent Transaction.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking_full.py
def book_reductions(entry, group_postings, balances,
methods):
"""Book inventory reductions against the ante-balances.
This function accepts a dict of (account, Inventory balance) and for each
posting that is a reduction against its inventory, attempts to find a
corresponding lot or list of lots to reduce the balance with.
* For reducing lots, the CostSpec instance of the posting is replaced by a
Cost instance.
* For augmenting lots, the CostSpec instance of the posting is left alone,
except for its date, which is inherited from the parent Transaction.
Args:
entry: An instance of Transaction. This is only used to refer to when
logging errors.
group_postings: A list of Posting instances for the group.
balances: A dict of account name to inventory contents.
methods: A mapping of account name to their corresponding booking
method enum.
Returns:
A pair of
booked_postings: A list of booked postings, with reducing lots resolved
against specific position in the corresponding accounts'
ante-inventory balances. Note single reducing posting in the input may
result in multiple postings in the output. Also note that augmenting
postings held-at-cost will still refer to 'cost' instances of
CostSpec, left to be interpolated later.
errors: A list of errors, if there were any.
"""
errors = []
# A local copy of the balances dictionary which is updated just for the
# duration of this function's updates, in order to take into account the
# cumulative effect of all the postings inferred here
local_balances = {}
empty = inventory.Inventory()
booked_postings = []
for posting in group_postings:
# Process a single posting.
units = posting.units
costspec = posting.cost
account = posting.account
# Note: We ensure there is no mutation on 'balances' to keep this
# function without side-effects. Note that we may be able to optimize
# performance later on by giving up this property.
#
# Also note that if there is no existing balance, then won't be any lot
# reduction because none of the postings will be able to match against
# any currencies of the balance.
previous_balance = balances.get(account, empty)
balance = local_balances.setdefault(account, copy.copy(previous_balance))
# Check if this is a lot held at cost.
if costspec is None or units.number is MISSING:
# This posting is not held at cost; we do nothing.
booked_postings.append(posting)
else:
# This posting is held at cost; figure out if it's a reduction or an
# augmentation.
method = methods[account]
if (method is not Booking.NONE and
balance is not None and
balance.is_reduced_by(units)):
# This posting is a reduction.
# Match the positions.
cost_number = compute_cost_number(costspec, units)
matches = []
for position in balance:
# Skip inventory contents of a different currency.
if (units.currency and
position.units.currency != units.currency):
continue
# Skip balance positions not held at cost.
if position.cost is None:
continue
if (cost_number is not None and
position.cost.number != cost_number):
continue
if (isinstance(costspec.currency, str) and
position.cost.currency != costspec.currency):
continue
if (costspec.date and
position.cost.date != costspec.date):
continue
if (costspec.label and
position.cost.label != costspec.label):
continue
matches.append(position)
# Check for ambiguous matches.
if len(matches) == 0:
errors.append(
ReductionError(entry.meta,
'No position matches "{}" against balance {}'.format(
posting, balance),
entry))
return [], errors # This is irreconcilable, remove these postings.
reduction_postings, matched_postings, ambi_errors = (
booking_method.handle_ambiguous_matches(entry, posting, matches,
method))
if ambi_errors:
errors.extend(ambi_errors)
return [], errors
# Add the reductions to the resulting list of booked postings.
booked_postings.extend(reduction_postings)
# Update the local balance in order to avoid matching against
# the same postings twice when processing multiple postings in
# the same transaction. Note that we only do this for postings
# held at cost because the other postings may need interpolation
# in order to be resolved properly.
for posting in reduction_postings:
balance.add_position(posting)
else:
# This posting is an augmentation.
#
# Note that we do not convert the CostSpec instances to Cost
# instances, because we want to let the subsequent interpolation
# process able to interpolate either the cost per-unit or the
# total cost, separately.
# Put in the date of the parent Transaction if there is no
# explicit date specified on the spec.
if costspec.date is None:
dated_costspec = costspec._replace(date=entry.date)
posting = posting._replace(cost=dated_costspec)
# FIXME: Insert unique ids for trade tracking; right now this
# creates ambiguous matches errors (and it shouldn't).
# # Insert a unique label if there isn't one.
# if posting.cost is not None and posting.cost.label is None:
# posting = posting._replace(
# cost=posting.cost._replace(label=unique_label()))
booked_postings.append(posting)
return booked_postings, errors
beancount.parser.booking_full.categorize_by_currency(entry, balances)
Group the postings by the currency they declare.
This is used to prepare the postings for the next stages: Interpolation and booking will then be carried out separately on each currency group. At the outset of this routine, we should have distinct groups of currencies without any ambiguities regarding which currency they need to balance against.
Here's how this works.
-
First we apply the constraint that cost-currency and price-currency must match, if there is both a cost and a price. This reduces the space of possibilities somewhat.
-
If the currency is explicitly specified, we put the posting in that currency's bucket.
-
If not, we have a few methods left to disambiguate the currency:
-
We look at the remaining postings... if they are all of a single currency, the posting must be in that currency too.
-
If we cannot do that, we inspect the contents of the inventory of the account for the posting. If all the contents are of a single currency, we use that one.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking_full.py
def categorize_by_currency(entry, balances):
"""Group the postings by the currency they declare.
This is used to prepare the postings for the next stages: Interpolation and
booking will then be carried out separately on each currency group. At the
outset of this routine, we should have distinct groups of currencies without
any ambiguities regarding which currency they need to balance against.
Here's how this works.
- First we apply the constraint that cost-currency and price-currency must
match, if there is both a cost and a price. This reduces the space of
possibilities somewhat.
- If the currency is explicitly specified, we put the posting in that
currency's bucket.
- If not, we have a few methods left to disambiguate the currency:
1. We look at the remaining postings... if they are all of a single
currency, the posting must be in that currency too.
2. If we cannot do that, we inspect the contents of the inventory of the
account for the posting. If all the contents are of a single currency,
we use that one.
Args:
postings: A list of incomplete postings to categorize.
balances: A dict of currency to inventory contents before the transaction is
applied.
Returns:
A list of (currency string, list of tuples) items describing each postings
and its interpolated currencies, and a list of generated errors for
currency interpolation. The entry's original postings are left unmodified.
Each tuple in the value-list contains:
index: The posting index in the original entry.
units_currency: The interpolated currency for units.
cost_currency: The interpolated currency for cost.
price_currency: The interpolated currency for price.
"""
errors = []
groups = collections.defaultdict(list)
sortdict = {}
auto_postings = []
unknown = []
for index, posting in enumerate(entry.postings):
units = posting.units
cost = posting.cost
price = posting.price
# Extract and override the currencies locally.
units_currency = (units.currency
if units is not MISSING and units is not None
else None)
cost_currency = (cost.currency
if cost is not MISSING and cost is not None
else None)
price_currency = (price.currency
if price is not MISSING and price is not None
else None)
# First we apply the constraint that cost-currency and price-currency
# must match, if there is both a cost and a price. This reduces the
# space of possibilities somewhat.
if cost_currency is MISSING and isinstance(price_currency, str):
cost_currency = price_currency
if price_currency is MISSING and isinstance(cost_currency, str):
price_currency = cost_currency
refer = Refer(index, units_currency, cost_currency, price_currency)
if units is MISSING and price_currency is None:
# Bucket auto-postings separately from unknown.
auto_postings.append(refer)
else:
# Bucket with what we know so far.
currency = get_bucket_currency(refer)
if currency is not None:
sortdict.setdefault(currency, index)
groups[currency].append(refer)
else:
# If we need to infer the currency, store in unknown.
unknown.append(refer)
# We look at the remaining postings... if they are all of a single currency,
# the posting must be in that currency too.
if unknown and len(unknown) == 1 and len(groups) == 1:
(index, units_currency, cost_currency, price_currency) = unknown.pop()
other_currency = next(iter(groups.keys()))
if price_currency is None and cost_currency is None:
# Infer to the units currency.
units_currency = other_currency
else:
# Infer to the cost and price currencies.
if price_currency is MISSING:
price_currency = other_currency
if cost_currency is MISSING:
cost_currency = other_currency
refer = Refer(index, units_currency, cost_currency, price_currency)
currency = get_bucket_currency(refer)
assert currency is not None
sortdict.setdefault(currency, index)
groups[currency].append(refer)
# Finally, try to resolve all the unknown legs using the inventory contents
# of each account.
for refer in unknown:
(index, units_currency, cost_currency, price_currency) = refer
posting = entry.postings[index]
balance = balances.get(posting.account, None)
if balance is None:
balance = inventory.Inventory()
if units_currency is MISSING:
balance_currencies = balance.currencies()
if len(balance_currencies) == 1:
units_currency = balance_currencies.pop()
if cost_currency is MISSING or price_currency is MISSING:
balance_cost_currencies = balance.cost_currencies()
if len(balance_cost_currencies) == 1:
balance_cost_currency = balance_cost_currencies.pop()
if price_currency is MISSING:
price_currency = balance_cost_currency
if cost_currency is MISSING:
cost_currency = balance_cost_currency
refer = Refer(index, units_currency, cost_currency, price_currency)
currency = get_bucket_currency(refer)
if currency is not None:
sortdict.setdefault(currency, index)
groups[currency].append(refer)
else:
errors.append(
CategorizationError(posting.meta,
"Failed to categorize posting {}".format(index + 1),
entry))
# Fill in missing units currencies if some remain as missing. This may occur
# if we used the cost or price to bucket the currency but the units currency
# was missing.
for currency, refers in groups.items():
for rindex, refer in enumerate(refers):
if refer.units_currency is MISSING:
posting = entry.postings[refer.index]
balance = balances.get(posting.account, None)
if balance is None:
continue
balance_currencies = balance.currencies()
if len(balance_currencies) == 1:
refers[rindex] = refer._replace(units_currency=balance_currencies.pop())
# Deal with auto-postings.
if len(auto_postings) > 1:
refer = auto_postings[-1]
posting = entry.postings[refer.index]
errors.append(
CategorizationError(posting.meta,
"You may not have more than one auto-posting per currency",
entry))
auto_postings = auto_postings[0:1]
for refer in auto_postings:
for currency in groups.keys():
sortdict.setdefault(currency, refer.index)
groups[currency].append(Refer(refer.index, currency, None, None))
# Issue error for all currencies which we could not resolve.
for currency, refers in groups.items():
for refer in refers:
posting = entry.postings[refer.index]
for currency, name in [(refer.units_currency, 'units'),
(refer.cost_currency, 'cost'),
(refer.price_currency, 'price')]:
if currency is MISSING:
errors.append(CategorizationError(
posting.meta,
"Could not resolve {} currency".format(name),
entry))
sorted_groups = sorted(groups.items(), key=lambda item: sortdict[item[0]])
return sorted_groups, errors
beancount.parser.booking_full.compute_cost_number(costspec, units)
Given a CostSpec, return the cost number, if possible to compute.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking_full.py
def compute_cost_number(costspec, units):
"""Given a CostSpec, return the cost number, if possible to compute.
Args:
costspec: A parsed instance of CostSpec.
units: An instance of Amount for the units of the position.
Returns:
If it is not possible to calculate the cost, return None.
Otherwise, returns a Decimal instance, the per-unit cost.
"""
number_per = costspec.number_per
number_total = costspec.number_total
if MISSING in (number_per, number_total):
return None
if number_total is not None:
# Compute the per-unit cost if there is some total cost
# component involved.
cost_total = number_total
units_number = units.number
if number_per is not None:
cost_total += number_per * units_number
unit_cost = cost_total / abs(units_number)
elif number_per is None:
return None
else:
unit_cost = number_per
return unit_cost
beancount.parser.booking_full.convert_costspec_to_cost(posting)
Convert an instance of CostSpec to Cost, if present on the posting.
If the posting has no cost, it itself is just returned.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking_full.py
def convert_costspec_to_cost(posting):
"""Convert an instance of CostSpec to Cost, if present on the posting.
If the posting has no cost, it itself is just returned.
Args:
posting: An instance of Posting.
Returns:
An instance of Posting with a possibly replaced 'cost' attribute.
"""
cost = posting.cost
if isinstance(cost, position.CostSpec):
if cost is not None:
units_number = posting.units.number
number_per = cost.number_per
number_total = cost.number_total
if number_total is not None:
# Compute the per-unit cost if there is some total cost
# component involved.
cost_total = number_total
if number_per is not MISSING:
cost_total += number_per * units_number
unit_cost = cost_total / abs(units_number)
else:
unit_cost = number_per
new_cost = Cost(unit_cost, cost.currency, cost.date, cost.label)
posting = posting._replace(units=posting.units, cost=new_cost)
return posting
beancount.parser.booking_full.get_bucket_currency(refer)
Given currency references for a posting, return the bucket currency.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking_full.py
def get_bucket_currency(refer):
"""Given currency references for a posting, return the bucket currency.
Args:
refer: An instance of Refer.
Returns:
A currency string.
"""
currency = None
if isinstance(refer.cost_currency, str):
currency = refer.cost_currency
elif isinstance(refer.price_currency, str):
currency = refer.price_currency
elif (refer.cost_currency is None and
refer.price_currency is None and
isinstance(refer.units_currency, str)):
currency = refer.units_currency
return currency
beancount.parser.booking_full.has_self_reduction(postings, methods)
Return true if the postings potentially reduce each other at cost.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking_full.py
def has_self_reduction(postings, methods):
"""Return true if the postings potentially reduce each other at cost.
Args:
postings: A list of postings with uninterpolated CostSpec cost instances.
methods: A mapping of account name to their corresponding booking
method.
Returns:
A boolean, true if there's a potential for self-reduction.
"""
# A mapping of (currency, cost-currency) and sign.
cost_changes = {}
for posting in postings:
cost = posting.cost
if cost is None:
continue
if methods[posting.account] is Booking.NONE:
continue
key = (posting.account, posting.units.currency)
sign = 1 if posting.units.number > ZERO else -1
if cost_changes.setdefault(key, sign) != sign:
return True
return False
beancount.parser.booking_full.interpolate_group(postings, balances, currency, tolerances)
Interpolate missing numbers in the set of postings.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking_full.py
def interpolate_group(postings, balances, currency, tolerances):
"""Interpolate missing numbers in the set of postings.
Args:
postings: A list of Posting instances.
balances: A dict of account to its ante-inventory.
currency: The weight currency of this group, used for reporting errors.
tolerances: A dict of currency to tolerance values.
Returns:
A tuple of
postings: A lit of new posting instances.
errors: A list of errors generated during interpolation.
interpolated: A boolean, true if we did have to interpolate.
In the case of an error, this returns the original list of postings, which
is still incomplete. If an error is returned, you should probably skip the
transaction altogether, or just not include the postings in it. (An
alternative behaviour would be to return only the list of valid postings,
but that would likely result in an unbalanced transaction. We do it this
way by choice.)
"""
errors = []
# Figure out which type of amount is missing, by creating a list of
# incomplete postings and which type of units is missing.
incomplete = []
for index, posting in enumerate(postings):
units = posting.units
cost = posting.cost
price = posting.price
# Identify incomplete parts of the Posting components.
if units.number is MISSING:
incomplete.append((MissingType.UNITS, index))
if isinstance(cost, CostSpec):
if cost and cost.number_per is MISSING:
incomplete.append((MissingType.COST_PER, index))
if cost and cost.number_total is MISSING:
incomplete.append((MissingType.COST_TOTAL, index))
else:
# Check that a resolved instance of Cost never needs interpolation.
#
# Note that in theory we could support the interpolation of regular
# per-unit costs in these if we wanted to; but because they're all
# reducing postings that have been booked earlier, those never need
# to be interpolated.
if cost is not None:
assert isinstance(cost.number, Decimal), (
"Internal error: cost has no number: {}".format(cost))
if price and price.number is MISSING:
incomplete.append((MissingType.PRICE, index))
# The replacement posting for the incomplete posting of this group.
new_posting = None
if len(incomplete) == 0:
# If there are no missing numbers, just convert the CostSpec to Cost and
# return that.
out_postings = [convert_costspec_to_cost(posting)
for posting in postings]
elif len(incomplete) > 1:
# If there is more than a single value to be interpolated, generate an
# error and return no postings.
_, posting_index = incomplete[0]
errors.append(InterpolationError(
postings[posting_index].meta,
"Too many missing numbers for currency group '{}'".format(currency),
None))
out_postings = []
else:
# If there is a single missing number, calculate it and fill it in here.
missing, index = incomplete[0]
incomplete_posting = postings[index]
# Convert augmenting postings' costs from CostSpec to corresponding Cost
# instances, except for the incomplete posting.
new_postings = [(posting
if posting is incomplete_posting
else convert_costspec_to_cost(posting))
for posting in postings]
# Compute the balance of the other postings.
residual = interpolate.compute_residual(posting
for posting in new_postings
if posting is not incomplete_posting)
assert len(residual) < 2, "Internal error in grouping postings by currencies."
if not residual.is_empty():
respos = next(iter(residual))
assert respos.cost is None, (
"Internal error; cost appears in weight calculation.")
assert respos.units.currency == currency, (
"Internal error; residual different than currency group.")
weight = -respos.units.number
weight_currency = respos.units.currency
else:
weight = ZERO
weight_currency = currency
if missing == MissingType.UNITS:
units = incomplete_posting.units
cost = incomplete_posting.cost
if cost:
# Handle the special case where we only have total cost.
if cost.number_per == ZERO:
errors.append(InterpolationError(
incomplete_posting.meta,
"Cannot infer per-unit cost only from total", None))
return postings, errors, True
assert cost.currency == weight_currency, (
"Internal error; residual currency different than missing currency.")
cost_total = cost.number_total or ZERO
units_number = (weight - cost_total) / cost.number_per
elif incomplete_posting.price:
assert incomplete_posting.price.currency == weight_currency, (
"Internal error; residual currency different than missing currency.")
units_number = weight / incomplete_posting.price.number
else:
assert units.currency == weight_currency, (
"Internal error; residual currency different than missing currency.")
units_number = weight
# Quantize the interpolated units if necessary.
units_number = interpolate.quantize_with_tolerance(tolerances,
units.currency,
units_number)
if weight != ZERO:
new_pos = Position(Amount(units_number, units.currency), cost)
new_posting = incomplete_posting._replace(units=new_pos.units,
cost=new_pos.cost)
else:
new_posting = None
elif missing == MissingType.COST_PER:
units = incomplete_posting.units
cost = incomplete_posting.cost
assert cost.currency == weight_currency, (
"Internal error; residual currency different than missing currency.")
if units.number != ZERO:
number_per = (weight - (cost.number_total or ZERO)) / units.number
new_cost = cost._replace(number_per=number_per)
new_pos = Position(units, new_cost)
new_posting = incomplete_posting._replace(units=new_pos.units,
cost=new_pos.cost)
else:
new_posting = None
elif missing == MissingType.COST_TOTAL:
units = incomplete_posting.units
cost = incomplete_posting.cost
assert cost.currency == weight_currency, (
"Internal error; residual currency different than missing currency.")
number_total = (weight - cost.number_per * units.number)
new_cost = cost._replace(number_total=number_total)
new_pos = Position(units, new_cost)
new_posting = incomplete_posting._replace(units=new_pos.units,
cost=new_pos.cost)
elif missing == MissingType.PRICE:
units = incomplete_posting.units
cost = incomplete_posting.cost
if cost is not None:
errors.append(InterpolationError(
incomplete_posting.meta,
"Cannot infer price for postings with units held at cost", None))
return postings, errors, True
else:
price = incomplete_posting.price
assert price.currency == weight_currency, (
"Internal error; residual currency different than missing currency.")
new_price_number = abs(weight / units.number)
new_posting = incomplete_posting._replace(price=Amount(new_price_number,
price.currency))
else:
assert False, "Internal error; Invalid missing type."
# 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
# Cost instance.
new_postings[index] = convert_costspec_to_cost(new_posting)
else:
del new_postings[index]
out_postings = new_postings
assert all(not isinstance(posting.cost, CostSpec)
for posting in out_postings)
# Check that units are non-zero and that no cost remains negative; issue an
# error if this is the case.
for posting in out_postings:
if posting.cost is None:
continue
# If there is a cost, we don't allow either a cost value of zero,
# nor a zero number of units. Note that we allow a price of zero as
# the only special case allowed (for conversion entries), but never
# for costs.
if posting.units.number == ZERO:
errors.append(InterpolationError(
posting.meta,
'Amount is zero: "{}"'.format(posting.units), None))
if posting.cost.number < ZERO:
errors.append(InterpolationError(
posting.meta,
'Cost is negative: "{}"'.format(posting.cost), None))
return out_postings, errors, (new_posting is not None)
beancount.parser.booking_full.replace_currencies(postings, refer_groups)
Replace resolved currencies in the entry's Postings.
This essentially applies the findings of categorize_by_currency() to produce new postings with all currencies resolved.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking_full.py
def replace_currencies(postings, refer_groups):
"""Replace resolved currencies in the entry's Postings.
This essentially applies the findings of categorize_by_currency() to produce
new postings with all currencies resolved.
Args:
postings: A list of Posting instances to replace.
refer_groups: A list of (currency, list of posting references) items as
returned by categorize_by_currency().
Returns:
A new list of items of (currency, list of Postings), postings for which the
currencies have been replaced by their interpolated currency values.
"""
new_groups = []
for currency, refers in refer_groups:
new_postings = []
for refer in sorted(refers, key=lambda r: r.index):
posting = postings[refer.index]
units = posting.units
if units is MISSING or units is None:
posting = posting._replace(units=Amount(MISSING, refer.units_currency))
else:
replace = False
cost = posting.cost
price = posting.price
if units.currency is MISSING:
units = Amount(units.number, refer.units_currency)
replace = True
if cost and cost.currency is MISSING:
cost = cost._replace(currency=refer.cost_currency)
replace = True
if price and price.currency is MISSING:
price = Amount(price.number, refer.price_currency)
replace = True
if replace:
posting = posting._replace(units=units, cost=cost, price=price)
new_postings.append(posting)
new_groups.append((currency, new_postings))
return new_groups
beancount.parser.booking_full.unique_label()
Return a globally unique label for cost entries.
Source code in beancount/parser/booking_full.py
def unique_label() -> Text:
"Return a globally unique label for cost entries."
return str(uuid.uuid4())
beancount.parser.booking_method
Implementations of all the particular booking methods. This code is used by the full booking algorithm.
beancount.parser.booking_method.AmbiguousMatchError (tuple)
AmbiguousMatchError(source, message, entry)
beancount.parser.booking_method.AmbiguousMatchError.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/booking_method.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.booking_method.AmbiguousMatchError.__new__(_cls, source, message, entry)
special
staticmethod
Create new instance of AmbiguousMatchError(source, message, entry)
beancount.parser.booking_method.AmbiguousMatchError.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/booking_method.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.booking_method.booking_method_AVERAGE(entry, posting, matches)
AVERAGE booking method implementation.
Source code in beancount/parser/booking_method.py
def booking_method_AVERAGE(entry, posting, matches):
"""AVERAGE booking method implementation."""
booked_reductions = []
booked_matches = []
errors = [AmbiguousMatchError(entry.meta, "AVERAGE method is not supported", entry)]
return booked_reductions, booked_matches, errors, False
# FIXME: Future implementation here.
# pylint: disable=unreachable
if False: # pylint: disable=using-constant-test
# DISABLED - This is the code for AVERAGE, which is currently disabled.
# If there is more than a single match we need to ultimately merge the
# postings. Also, if the reducing posting provides a specific cost, we
# need to update the cost basis as well. Both of these cases are carried
# out by removing all the matches and readding them later on.
if len(matches) == 1 and (
not isinstance(posting.cost.number_per, Decimal) and
not isinstance(posting.cost.number_total, Decimal)):
# There is no cost. Just reduce the one leg. This should be the
# normal case if we always merge augmentations and the user lets
# Beancount deal with the cost.
match = matches[0]
sign = -1 if posting.units.number < ZERO else 1
number = min(abs(match.units.number), abs(posting.units.number))
match_units = Amount(number * sign, match.units.currency)
booked_reductions.append(posting._replace(units=match_units, cost=match.cost))
insufficient = (match_units.number != posting.units.number)
else:
# Merge the matching postings to a single one.
merged_units = inventory.Inventory()
merged_cost = inventory.Inventory()
for match in matches:
merged_units.add_amount(match.units)
merged_cost.add_amount(convert.get_weight(match))
if len(merged_units) != 1 or len(merged_cost) != 1:
errors.append(
AmbiguousMatchError(
entry.meta,
'Cannot merge positions in multiple currencies: {}'.format(
', '.join(position.to_string(match_posting)
for match_posting in matches)), entry))
else:
if (isinstance(posting.cost.number_per, Decimal) or
isinstance(posting.cost.number_total, Decimal)):
errors.append(
AmbiguousMatchError(
entry.meta,
"Explicit cost reductions aren't supported yet: {}".format(
position.to_string(posting)), entry))
else:
# Insert postings to remove all the matches.
booked_reductions.extend(
posting._replace(units=-match.units, cost=match.cost,
flag=flags.FLAG_MERGING)
for match in matches)
units = merged_units[0].units
date = matches[0].cost.date ## FIXME: Select which one,
## oldest or latest.
cost_units = merged_cost[0].units
cost = Cost(cost_units.number/units.number, cost_units.currency,
date, None)
# Insert a posting to refill those with a replacement match.
booked_reductions.append(
posting._replace(units=units, cost=cost, flag=flags.FLAG_MERGING))
# Now, match the reducing request against this lot.
booked_reductions.append(
posting._replace(units=posting.units, cost=cost))
insufficient = abs(posting.units.number) > abs(units.number)
beancount.parser.booking_method.booking_method_FIFO(entry, posting, matches)
FIFO booking method implementation.
Source code in beancount/parser/booking_method.py
def booking_method_FIFO(entry, posting, matches):
"""FIFO booking method implementation."""
return _booking_method_xifo(entry, posting, matches, False)
beancount.parser.booking_method.booking_method_LIFO(entry, posting, matches)
LIFO booking method implementation.
Source code in beancount/parser/booking_method.py
def booking_method_LIFO(entry, posting, matches):
"""LIFO booking method implementation."""
return _booking_method_xifo(entry, posting, matches, True)
beancount.parser.booking_method.booking_method_NONE(entry, posting, matches)
NONE booking method implementation.
Source code in beancount/parser/booking_method.py
def booking_method_NONE(entry, posting, matches):
"""NONE booking method implementation."""
# This never needs to match against any existing positions... we
# disregard the matches, there's never any error. Note that this never
# gets called in practice, we want to treat NONE postings as
# augmentations. Default behaviour is to return them with their original
# CostSpec, and the augmentation code will handle signaling an error if
# there is insufficient detail to carry out the conversion to an
# instance of Cost.
# Note that it's an interesting question whether a reduction on an
# account with NONE method which happens to match a single position
# ought to be matched against it. We don't allow it for now.
return [posting], [], False
beancount.parser.booking_method.booking_method_STRICT(entry, posting, matches)
Strict booking method.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking_method.py
def booking_method_STRICT(entry, posting, matches):
"""Strict booking method.
Args:
entry: The parent Transaction instance.
posting: An instance of Posting, the reducing posting which we're
attempting to match.
matches: A list of matching Position instances from the ante-inventory.
Those positions are known to already match the 'posting' spec.
Returns:
A triple of
booked_reductions: A list of matched Posting instances, whose 'cost'
attributes are ensured to be of type Cost.
errors: A list of errors to be generated.
insufficient: A boolean, true if we could not find enough matches
to fulfill the reduction.
"""
booked_reductions = []
booked_matches = []
errors = []
insufficient = False
# In strict mode, we require at most a single matching posting.
if len(matches) > 1:
# If the total requested to reduce matches the sum of all the
# ambiguous postings, match against all of them.
sum_matches = sum(p.units.number for p in matches)
if sum_matches == -posting.units.number:
booked_reductions.extend(
posting._replace(units=-match.units, cost=match.cost)
for match in matches)
else:
errors.append(
AmbiguousMatchError(entry.meta,
'Ambiguous matches for "{}": {}'.format(
position.to_string(posting),
', '.join(position.to_string(match_posting)
for match_posting in matches)),
entry))
else:
# Replace the posting's units and cost values.
match = matches[0]
sign = -1 if posting.units.number < ZERO else 1
number = min(abs(match.units.number), abs(posting.units.number))
match_units = Amount(number * sign, match.units.currency)
booked_reductions.append(posting._replace(units=match_units, cost=match.cost))
booked_matches.append(match)
insufficient = (match_units.number != posting.units.number)
return booked_reductions, booked_matches, errors, insufficient
beancount.parser.booking_method.handle_ambiguous_matches(entry, posting, matches, method)
Handle ambiguous matches by dispatching to a particular method.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/booking_method.py
def handle_ambiguous_matches(entry, posting, matches, method):
"""Handle ambiguous matches by dispatching to a particular method.
Args:
entry: The parent Transaction instance.
posting: An instance of Posting, the reducing posting which we're
attempting to match.
matches: A list of matching Position instances from the ante-inventory.
Those positions are known to already match the 'posting' spec.
methods: A mapping of account name to their corresponding booking
method.
Returns:
A pair of
booked_reductions: A list of matched Posting instances, whose 'cost'
attributes are ensured to be of type Cost.
errors: A list of errors to be generated.
"""
assert isinstance(method, Booking), (
"Invalid type: {}".format(method))
assert matches, "Internal error: Invalid call with no matches"
#method = globals()['booking_method_{}'.format(method.name)]
method = _BOOKING_METHODS[method]
(booked_reductions,
booked_matches, errors, insufficient) = method(entry, posting, matches)
if insufficient:
errors.append(
AmbiguousMatchError(entry.meta,
'Not enough lots to reduce "{}": {}'.format(
position.to_string(posting),
', '.join(position.to_string(match_posting)
for match_posting in matches)),
entry))
return booked_reductions, booked_matches, errors
beancount.parser.cmptest
Support utilities for testing scripts.
beancount.parser.cmptest.TestError (Exception)
Errors within the test implementation itself. These should never occur.
beancount.parser.cmptest.assertEqualEntries(expected_entries, actual_entries, failfunc=<function fail at 0x755e1567c180>, allow_incomplete=False)
Compare two lists of entries exactly and print missing entries verbosely if they occur.
Parameters: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/cmptest.py
def assertEqualEntries(expected_entries, actual_entries,
failfunc=DEFAULT_FAILFUNC, allow_incomplete=False):
"""Compare two lists of entries exactly and print missing entries verbosely if
they occur.
Args:
expected_entries: Either a list of directives or a string, in which case the
string is run through beancount.parser.parse_string() and the resulting
list is used.
actual_entries: Same treatment as expected_entries, the other list of
directives to compare to.
failfunc: A function to call on failure.
allow_incomplete: A boolean, true if we allow incomplete inputs and perform
light-weight booking.
Raises:
AssertionError: If the exception fails.
"""
expected_entries = read_string_or_entries(expected_entries, allow_incomplete)
actual_entries = read_string_or_entries(actual_entries, allow_incomplete)
same, expected_missing, actual_missing = compare.compare_entries(expected_entries,
actual_entries)
if not same:
assert expected_missing or actual_missing, "Missing is missing: {}, {}".format(
expected_missing, actual_missing)
oss = io.StringIO()
if expected_missing:
oss.write("Present in expected set and not in actual set:\n\n")
for entry in expected_missing:
oss.write(printer.format_entry(entry))
oss.write('\n')
if actual_missing:
oss.write("Present in actual set and not in expected set:\n\n")
for entry in actual_missing:
oss.write(printer.format_entry(entry))
oss.write('\n')
failfunc(oss.getvalue())
beancount.parser.cmptest.assertExcludesEntries(subset_entries, entries, failfunc=<function fail at 0x755e1567c180>, allow_incomplete=False)
Check that subset_entries is not included in entries and print extra entries.
Parameters: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/cmptest.py
def assertExcludesEntries(subset_entries, entries,
failfunc=DEFAULT_FAILFUNC, allow_incomplete=False):
"""Check that subset_entries is not included in entries and print extra entries.
Args:
subset_entries: Either a list of directives or a string, in which case the
string is run through beancount.parser.parse_string() and the resulting
list is used.
entries: Same treatment as subset_entries, the other list of
directives to compare to.
failfunc: A function to call on failure.
allow_incomplete: A boolean, true if we allow incomplete inputs and perform
light-weight booking.
Raises:
AssertionError: If the exception fails.
"""
subset_entries = read_string_or_entries(subset_entries, allow_incomplete)
entries = read_string_or_entries(entries)
excludes, extra = compare.excludes_entries(subset_entries, entries)
if not excludes:
assert extra, "Extra is empty: {}".format(extra)
oss = io.StringIO()
if extra:
oss.write("Extra from from first/excluded set:\n\n")
for entry in extra:
oss.write(printer.format_entry(entry))
oss.write('\n')
failfunc(oss.getvalue())
beancount.parser.cmptest.assertIncludesEntries(subset_entries, entries, failfunc=<function fail at 0x755e1567c180>, allow_incomplete=False)
Check that subset_entries is included in entries and print missing entries.
Parameters: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/cmptest.py
def assertIncludesEntries(subset_entries, entries,
failfunc=DEFAULT_FAILFUNC, allow_incomplete=False):
"""Check that subset_entries is included in entries and print missing entries.
Args:
subset_entries: Either a list of directives or a string, in which case the
string is run through beancount.parser.parse_string() and the resulting
list is used.
entries: Same treatment as subset_entries, the other list of
directives to compare to.
failfunc: A function to call on failure.
allow_incomplete: A boolean, true if we allow incomplete inputs and perform
light-weight booking.
Raises:
AssertionError: If the exception fails.
"""
subset_entries = read_string_or_entries(subset_entries, allow_incomplete)
entries = read_string_or_entries(entries)
includes, missing = compare.includes_entries(subset_entries, entries)
if not includes:
assert missing, "Missing is empty: {}".format(missing)
oss = io.StringIO()
if missing:
oss.write("Missing from from expected set:\n\n")
for entry in missing:
oss.write(printer.format_entry(entry))
oss.write('\n')
failfunc(oss.getvalue())
beancount.parser.cmptest.read_string_or_entries(entries_or_str, allow_incomplete=False)
Read a string of entries or just entries.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/cmptest.py
def read_string_or_entries(entries_or_str, allow_incomplete=False):
"""Read a string of entries or just entries.
Args:
entries_or_str: Either a list of directives, or a string containing directives.
allow_incomplete: A boolean, true if we allow incomplete inputs and perform
light-weight booking.
Returns:
A list of directives.
"""
if isinstance(entries_or_str, str):
entries, errors, options_map = parser.parse_string(
textwrap.dedent(entries_or_str))
if allow_incomplete:
# Do a simplistic local conversion in order to call the comparison.
entries = [_local_booking(entry) for entry in entries]
else:
# Don't accept incomplete entries either.
if any(parser.is_entry_incomplete(entry) for entry in entries):
raise TestError("Entries in assertions may not use interpolation.")
entries, booking_errors = booking.book(entries, options_map)
errors = errors + booking_errors
# Don't tolerate errors.
if errors:
oss = io.StringIO()
printer.print_errors(errors, file=oss)
raise TestError("Unexpected errors in expected: {}".format(oss.getvalue()))
else:
assert isinstance(entries_or_str, list), "Expecting list: {}".format(entries_or_str)
entries = entries_or_str
return entries
beancount.parser.grammar
Builder for Beancount grammar.
beancount.parser.grammar.Builder (LexBuilder)
A builder used by the lexer and grammar parser as callbacks to create the data objects corresponding to rules parsed from the input file.
beancount.parser.grammar.Builder.amount(self, number, currency)
Process an amount grammar rule.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def amount(self, number, currency):
"""Process an amount grammar rule.
Args:
number: a Decimal instance, the number of the amount.
currency: a currency object (a str, really, see CURRENCY above)
Returns:
An instance of Amount.
"""
# Update the mapping that stores the parsed precisions.
# Note: This is relatively slow, adds about 70ms because of number.as_tuple().
self.dcupdate(number, currency)
return Amount(number, currency)
beancount.parser.grammar.Builder.balance(self, filename, lineno, date, account, amount, tolerance, kvlist)
Process an assertion directive.
We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def balance(self, filename, lineno, date, account, amount, tolerance, kvlist):
"""Process an assertion directive.
We produce no errors here by default. We replace the failing ones in the
routine that does the verification later one, that these have succeeded
or failed.
Args:
filename: The current filename.
lineno: The current line number.
date: A datetime object.
account: A string, the account to balance.
amount: The expected amount, to be checked.
tolerance: The tolerance number.
kvlist: a list of KeyValue instances.
Returns:
A new Balance object.
"""
diff_amount = None
meta = new_metadata(filename, lineno, kvlist)
return Balance(meta, date, account, amount, tolerance, diff_amount)
beancount.parser.grammar.Builder.build_grammar_error(self, filename, lineno, exc_value, exc_type=None, exc_traceback=None)
Build a grammar error and appends it to the list of pending errors.
Parameters: |
|
---|
Source code in beancount/parser/grammar.py
def build_grammar_error(self, filename, lineno, exc_value,
exc_type=None, exc_traceback=None):
"""Build a grammar error and appends it to the list of pending errors.
Args:
filename: The current filename
lineno: The current line number
excvalue: The exception value, or a str, the message of the error.
exc_type: An exception type, if an exception occurred.
exc_traceback: A traceback object.
"""
if exc_type is not None:
assert not isinstance(exc_value, str)
strings = traceback.format_exception_only(exc_type, exc_value)
tblist = traceback.extract_tb(exc_traceback)
filename, lineno, _, __ = tblist[0]
message = '{} ({}:{})'.format(strings[0], filename, lineno)
else:
message = str(exc_value)
meta = new_metadata(filename, lineno)
self.errors.append(
ParserSyntaxError(meta, message, None))
beancount.parser.grammar.Builder.close(self, filename, lineno, date, account, kvlist)
Process a close directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def close(self, filename, lineno, date, account, kvlist):
"""Process a close directive.
Args:
filename: The current filename.
lineno: The current line number.
date: A datetime object.
account: A string, the name of the account.
kvlist: a list of KeyValue instances.
Returns:
A new Close object.
"""
meta = new_metadata(filename, lineno, kvlist)
return Close(meta, date, account)
beancount.parser.grammar.Builder.commodity(self, filename, lineno, date, currency, kvlist)
Process a close directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def commodity(self, filename, lineno, date, currency, kvlist):
"""Process a close directive.
Args:
filename: The current filename.
lineno: The current line number.
date: A datetime object.
currency: A string, the commodity being declared.
kvlist: a list of KeyValue instances.
Returns:
A new Close object.
"""
meta = new_metadata(filename, lineno, kvlist)
return Commodity(meta, date, currency)
beancount.parser.grammar.Builder.compound_amount(self, number_per, number_total, currency)
Process an amount grammar rule.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def compound_amount(self, number_per, number_total, currency):
"""Process an amount grammar rule.
Args:
number_per: a Decimal instance, the number of the cost per share.
number_total: a Decimal instance, the number of the cost over all shares.
currency: a currency object (a str, really, see CURRENCY above)
Returns:
A triple of (Decimal, Decimal, currency string) to be processed further when
creating the final per-unit cost number.
"""
# Update the mapping that stores the parsed precisions.
# Note: This is relatively slow, adds about 70ms because of number.as_tuple().
self.dcupdate(number_per, currency)
self.dcupdate(number_total, currency)
# Note that we are not able to reduce the value to a number per-share
# here because we only get the number of units in the full lot spec.
return CompoundAmount(number_per, number_total, currency)
beancount.parser.grammar.Builder.cost_merge(self, _)
Create a 'merge cost' token.
Source code in beancount/parser/grammar.py
def cost_merge(self, _):
"""Create a 'merge cost' token."""
return MERGE_COST
beancount.parser.grammar.Builder.cost_spec(self, cost_comp_list, is_total)
Process a cost_spec grammar rule.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def cost_spec(self, cost_comp_list, is_total):
"""Process a cost_spec grammar rule.
Args:
cost_comp_list: A list of CompoundAmount, a datetime.date, or
label ID strings.
is_total: Assume only the total cost is specified; reject the <number> # <number>
syntax, that is, no compound amounts may be specified. This is used to support
the {{...}} syntax.
Returns:
A cost-info tuple of CompoundAmount, lot date and label string. Any of these
may be set to a sentinel indicating "unset".
"""
if not cost_comp_list:
return CostSpec(MISSING, None, MISSING, None, None, False)
assert isinstance(cost_comp_list, list), (
"Internal error in parser: {}".format(cost_comp_list))
compound_cost = None
date_ = None
label = None
merge = None
for comp in cost_comp_list:
if isinstance(comp, CompoundAmount):
if compound_cost is None:
compound_cost = comp
else:
self.errors.append(
ParserError(self.get_lexer_location(),
"Duplicate cost: '{}'.".format(comp), None))
elif isinstance(comp, date):
if date_ is None:
date_ = comp
else:
self.errors.append(
ParserError(self.get_lexer_location(),
"Duplicate date: '{}'.".format(comp), None))
elif comp is MERGE_COST:
if merge is None:
merge = True
self.errors.append(
ParserError(self.get_lexer_location(),
"Cost merging is not supported yet", None))
else:
self.errors.append(
ParserError(self.get_lexer_location(),
"Duplicate merge-cost spec", None))
else:
assert isinstance(comp, str), (
"Currency component is not string: '{}'".format(comp))
if label is None:
label = comp
else:
self.errors.append(
ParserError(self.get_lexer_location(),
"Duplicate label: '{}'.".format(comp), None))
# If there was a cost_comp_list, thus a "{...}" cost basis spec, you must
# indicate that by creating a CompoundAmount(), always.
if compound_cost is None:
number_per, number_total, currency = MISSING, None, MISSING
else:
number_per, number_total, currency = compound_cost
if is_total:
if number_total is not None:
self.errors.append(
ParserError(
self.get_lexer_location(),
("Per-unit cost may not be specified using total cost "
"syntax: '{}'; ignoring per-unit cost").format(compound_cost),
None))
# Ignore per-unit number.
number_per = ZERO
else:
# There's a single number specified; interpret it as a total cost.
number_total = number_per
number_per = ZERO
if merge is None:
merge = False
return CostSpec(number_per, number_total, currency, date_, label, merge)
beancount.parser.grammar.Builder.custom(self, filename, lineno, date, dir_type, custom_values, kvlist)
Process a custom directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def custom(self, filename, lineno, date, dir_type, custom_values, kvlist):
"""Process a custom directive.
Args:
filename: the current filename.
lineno: the current line number.
date: a datetime object.
dir_type: A string, a type for the custom directive being parsed.
custom_values: A list of the various tokens seen on the same line.
kvlist: a list of KeyValue instances.
Returns:
A new Custom object.
"""
meta = new_metadata(filename, lineno, kvlist)
return Custom(meta, date, dir_type, custom_values)
beancount.parser.grammar.Builder.custom_value(self, value, dtype=None)
Create a custom value object, along with its type.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def custom_value(self, value, dtype=None):
"""Create a custom value object, along with its type.
Args:
value: One of the accepted custom values.
Returns:
A pair of (value, dtype) where 'dtype' is the datatype is that of the
value.
"""
if dtype is None:
dtype = type(value)
return ValueType(value, dtype)
beancount.parser.grammar.Builder.dcupdate(self, number, currency)
Update the display context.
Source code in beancount/parser/grammar.py
def dcupdate(self, number, currency):
"""Update the display context."""
if isinstance(number, Decimal) and currency and currency is not MISSING:
self._dcupdate(number, currency)
beancount.parser.grammar.Builder.document(self, filename, lineno, date, account, document_filename, tags_links, kvlist)
Process a document directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def document(self, filename, lineno, date, account, document_filename, tags_links,
kvlist):
"""Process a document directive.
Args:
filename: the current filename.
lineno: the current line number.
date: a datetime object.
account: an Account instance.
document_filename: a str, the name of the document file.
tags_links: The current TagsLinks accumulator.
kvlist: a list of KeyValue instances.
Returns:
A new Document object.
"""
meta = new_metadata(filename, lineno, kvlist)
if not path.isabs(document_filename):
document_filename = path.abspath(path.join(path.dirname(filename),
document_filename))
tags, links = self.finalize_tags_links(tags_links.tags, tags_links.links)
return Document(meta, date, account, document_filename, tags, links)
beancount.parser.grammar.Builder.event(self, filename, lineno, date, event_type, description, kvlist)
Process an event directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def event(self, filename, lineno, date, event_type, description, kvlist):
"""Process an event directive.
Args:
filename: the current filename.
lineno: the current line number.
date: a datetime object.
event_type: a str, the name of the event type.
description: a str, the event value, the contents.
kvlist: a list of KeyValue instances.
Returns:
A new Event object.
"""
meta = new_metadata(filename, lineno, kvlist)
return Event(meta, date, event_type, description)
beancount.parser.grammar.Builder.finalize(self)
Finalize the parser, check for final errors and return the triple.
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def finalize(self):
"""Finalize the parser, check for final errors and return the triple.
Returns:
A triple of
entries: A list of parsed directives, which may need completion.
errors: A list of errors, hopefully empty.
options_map: A dict of options.
"""
# If the user left some tags unbalanced, issue an error.
for tag in self.tags:
meta = new_metadata(self.options['filename'], 0)
self.errors.append(
ParserError(meta, "Unbalanced pushed tag: '{}'".format(tag), None))
# If the user left some metadata unpopped, issue an error.
for key, value_list in self.meta.items():
meta = new_metadata(self.options['filename'], 0)
self.errors.append(
ParserError(meta, (
"Unbalanced metadata key '{}'; leftover metadata '{}'").format(
key, ', '.join(value_list)), None))
# Weave the commas option in the DisplayContext itself, so it propagates
# everywhere it is used automatically.
self.dcontext.set_commas(self.options['render_commas'])
return (self.get_entries(), self.errors, self.get_options())
beancount.parser.grammar.Builder.finalize_tags_links(self, tags, links)
Finally amend tags and links and return final objects to be inserted.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def finalize_tags_links(self, tags, links):
"""Finally amend tags and links and return final objects to be inserted.
Args:
tags: A set of tag strings (warning: this gets mutated in-place).
links: A set of link strings.
Returns:
A sanitized pair of (tags, links).
"""
if self.tags:
tags.update(self.tags)
return (frozenset(tags) if tags else EMPTY_SET,
frozenset(links) if links else EMPTY_SET)
beancount.parser.grammar.Builder.get_entries(self)
Return the accumulated entries.
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def get_entries(self):
"""Return the accumulated entries.
Returns:
A list of sorted directives.
"""
return sorted(self.entries, key=data.entry_sortkey)
beancount.parser.grammar.Builder.get_invalid_account(self)
See base class.
Source code in beancount/parser/grammar.py
def get_invalid_account(self):
"""See base class."""
return account.join(self.options['name_equity'], 'InvalidAccountName')
beancount.parser.grammar.Builder.get_long_string_maxlines(self)
See base class.
Source code in beancount/parser/grammar.py
def get_long_string_maxlines(self):
"""See base class."""
return self.options['long_string_maxlines']
beancount.parser.grammar.Builder.get_options(self)
Return the final options map.
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def get_options(self):
"""Return the final options map.
Returns:
A dict of option names to options.
"""
# Build and store the inferred DisplayContext instance.
self.options['dcontext'] = self.dcontext
# Add the full list of seen commodities.
#
# IMPORTANT: This is currently where the list of all commodities seen
# from the parser lives. The
# beancount.core.getters.get_commodities_map() routine uses this to
# automatically generate a full list of directives. An alternative would
# be to implement a plugin that enforces the generate of these
# post-parsing so that they are always guaranteed to live within the
# flow of entries. This would allow us to keep all the data in that list
# of entries and to avoid depending on the options to store that output.
self.options['commodities'] = self.commodities
return self.options
beancount.parser.grammar.Builder.handle_list(self, object_list, new_object)
Handle a recursive list grammar rule, generically.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def handle_list(self, object_list, new_object):
"""Handle a recursive list grammar rule, generically.
Args:
object_list: the current list of objects.
new_object: the new object to be added.
Returns:
The new, updated list of objects.
"""
if object_list is None:
object_list = []
if new_object is not None:
object_list.append(new_object)
return object_list
beancount.parser.grammar.Builder.include(self, filename, lineno, include_filename)
Process an include directive.
Parameters: |
|
---|
Source code in beancount/parser/grammar.py
def include(self, filename, lineno, include_filename):
"""Process an include directive.
Args:
filename: current filename.
lineno: current line number.
include_name: A string, the name of the file to include.
"""
self.options['include'].append(include_filename)
beancount.parser.grammar.Builder.key_value(self, key, value)
Process a document directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def key_value(self, key, value):
"""Process a document directive.
Args:
filename: The current filename.
lineno: The current line number.
date: A datetime object.
account: A string, the account the document relates to.
document_filename: A str, the name of the document file.
Returns:
A new KeyValue object.
"""
return KeyValue(key, value)
beancount.parser.grammar.Builder.note(self, filename, lineno, date, account, comment, kvlist)
Process a note directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def note(self, filename, lineno, date, account, comment, kvlist):
"""Process a note directive.
Args:
filename: The current filename.
lineno: The current line number.
date: A datetime object.
account: A string, the account to attach the note to.
comment: A str, the note's comments contents.
kvlist: a list of KeyValue instances.
Returns:
A new Note object.
"""
meta = new_metadata(filename, lineno, kvlist)
return Note(meta, date, account, comment)
beancount.parser.grammar.Builder.open(self, filename, lineno, date, account, currencies, booking_str, kvlist)
Process an open directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def open(self, filename, lineno, date, account, currencies, booking_str, kvlist):
"""Process an open directive.
Args:
filename: The current filename.
lineno: The current line number.
date: A datetime object.
account: A string, the name of the account.
currencies: A list of constraint currencies.
booking_str: A string, the booking method, or None if none was specified.
kvlist: a list of KeyValue instances.
Returns:
A new Open object.
"""
meta = new_metadata(filename, lineno, kvlist)
error = False
if booking_str:
try:
# Note: Somehow the 'in' membership operator is not defined on Enum.
booking = Booking[booking_str]
except KeyError:
# If the per-account method is invalid, set it to the global
# default method and continue.
booking = self.options['booking_method']
error = True
else:
booking = None
entry = Open(meta, date, account, currencies, booking)
if error:
self.errors.append(ParserError(meta,
"Invalid booking method: {}".format(booking_str),
entry))
return entry
beancount.parser.grammar.Builder.option(self, filename, lineno, key, value)
Process an option directive.
Parameters: |
|
---|
Source code in beancount/parser/grammar.py
def option(self, filename, lineno, key, value):
"""Process an option directive.
Args:
filename: current filename.
lineno: current line number.
key: option's key (str)
value: option's value
"""
if key not in self.options:
meta = new_metadata(filename, lineno)
self.errors.append(
ParserError(meta, "Invalid option: '{}'".format(key), None))
elif key in options.READ_ONLY_OPTIONS:
meta = new_metadata(filename, lineno)
self.errors.append(
ParserError(meta, "Option '{}' may not be set".format(key), None))
else:
option_descriptor = options.OPTIONS[key]
# Issue a warning if the option is deprecated.
if option_descriptor.deprecated:
assert isinstance(option_descriptor.deprecated, str), "Internal error."
meta = new_metadata(filename, lineno)
self.errors.append(
DeprecatedError(meta, option_descriptor.deprecated, None))
# Rename the option if it has an alias.
if option_descriptor.alias:
key = option_descriptor.alias
option_descriptor = options.OPTIONS[key]
# Convert the value, if necessary.
if option_descriptor.converter:
try:
value = option_descriptor.converter(value)
except ValueError as exc:
meta = new_metadata(filename, lineno)
self.errors.append(
ParserError(meta,
"Error for option '{}': {}".format(key, exc),
None))
return
option = self.options[key]
if isinstance(option, list):
# Append to a list of values.
option.append(value)
elif isinstance(option, dict):
# Set to a dict of values.
if not (isinstance(value, tuple) and len(value) == 2):
self.errors.append(
ParserError(
meta, "Error for option '{}': {}".format(key, value), None))
return
dict_key, dict_value = value
option[dict_key] = dict_value
elif isinstance(option, bool):
# Convert to a boolean.
if not isinstance(value, bool):
value = (value.lower() in {'true', 'on'}) or (value == '1')
self.options[key] = value
else:
# Set the value.
self.options[key] = value
# Refresh the list of valid account regexps as we go along.
if key.startswith('name_'):
# Update the set of valid account types.
self.account_regexp = valid_account_regexp(self.options)
elif key == 'insert_pythonpath':
# Insert the PYTHONPATH to this file when and only if you
# encounter this option.
sys.path.insert(0, path.dirname(filename))
beancount.parser.grammar.Builder.pad(self, filename, lineno, date, account, source_account, kvlist)
Process a pad directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def pad(self, filename, lineno, date, account, source_account, kvlist):
"""Process a pad directive.
Args:
filename: The current filename.
lineno: The current line number.
date: A datetime object.
account: A string, the account to be padded.
source_account: A string, the account to pad from.
kvlist: a list of KeyValue instances.
Returns:
A new Pad object.
"""
meta = new_metadata(filename, lineno, kvlist)
return Pad(meta, date, account, source_account)
beancount.parser.grammar.Builder.pipe_deprecated_error(self, filename, lineno)
Issue a 'Pipe deprecated' error.
Parameters: |
|
---|
Source code in beancount/parser/grammar.py
def pipe_deprecated_error(self, filename, lineno):
"""Issue a 'Pipe deprecated' error.
Args:
filename: The current filename
lineno: The current line number
"""
if self.options['allow_pipe_separator']:
return
meta = new_metadata(filename, lineno)
self.errors.append(
ParserSyntaxError(meta, "Pipe symbol is deprecated.", None))
beancount.parser.grammar.Builder.plugin(self, filename, lineno, plugin_name, plugin_config)
Process a plugin directive.
Parameters: |
|
---|
Source code in beancount/parser/grammar.py
def plugin(self, filename, lineno, plugin_name, plugin_config):
"""Process a plugin directive.
Args:
filename: current filename.
lineno: current line number.
plugin_name: A string, the name of the plugin module to import.
plugin_config: A string or None, an optional configuration string to
pass in to the plugin module.
"""
self.options['plugin'].append((plugin_name, plugin_config))
beancount.parser.grammar.Builder.popmeta(self, key)
Removed a key off the current set of stacks.
Parameters: |
|
---|
Source code in beancount/parser/grammar.py
def popmeta(self, key):
"""Removed a key off the current set of stacks.
Args:
key: A string, a key to be removed from the meta dict.
"""
try:
if key not in self.meta:
raise IndexError
value_list = self.meta[key]
value_list.pop(-1)
if not value_list:
self.meta.pop(key)
except IndexError:
meta = new_metadata(self.options['filename'], 0)
self.errors.append(
ParserError(meta,
"Attempting to pop absent metadata key: '{}'".format(key),
None))
beancount.parser.grammar.Builder.poptag(self, tag)
Pop a tag off the current set of stacks.
Parameters: |
|
---|
Source code in beancount/parser/grammar.py
def poptag(self, tag):
"""Pop a tag off the current set of stacks.
Args:
tag: A string, a tag to be removed from the current set of tags.
"""
try:
self.tags.remove(tag)
except ValueError:
meta = new_metadata(self.options['filename'], 0)
self.errors.append(
ParserError(meta, "Attempting to pop absent tag: '{}'".format(tag), None))
beancount.parser.grammar.Builder.posting(self, filename, lineno, account, units, cost, price, istotal, flag)
Process a posting grammar rule.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def posting(self, filename, lineno, account, units, cost, price, istotal, flag):
"""Process a posting grammar rule.
Args:
filename: the current filename.
lineno: the current line number.
account: A string, the account of the posting.
units: An instance of Amount for the units.
cost: An instance of CostSpec for the cost.
price: Either None, or an instance of Amount that is the cost of the position.
istotal: A bool, True if the price is for the total amount being parsed, or
False if the price is for each lot of the position.
flag: A string, one-character, the flag associated with this posting.
Returns:
A new Posting object, with no parent entry.
"""
meta = new_metadata(filename, lineno)
# Prices may not be negative.
if price and isinstance(price.number, Decimal) and price.number < ZERO:
self.errors.append(
ParserError(meta, (
"Negative prices are not allowed: {} "
"(see http://furius.ca/beancount/doc/bug-negative-prices "
"for workaround)"
).format(price), None))
# Fix it and continue.
price = Amount(abs(price.number), price.currency)
# If the price is specified for the entire amount, compute the effective
# price here and forget about that detail of the input syntax.
if istotal:
if units.number == ZERO:
number = ZERO
else:
number = price.number
if number is not MISSING:
number = number/abs(units.number)
price = Amount(number, price.currency)
# Note: Allow zero prices because we need them for round-trips for
# conversion entries.
#
# if price is not None and price.number == ZERO:
# self.errors.append(
# ParserError(meta, "Price is zero: {}".format(price), None))
# If both cost and price are specified, the currencies must match, or
# that is an error.
if (cost is not None and
price is not None and
isinstance(cost.currency, str) and
isinstance(price.currency, str) and
cost.currency != price.currency):
self.errors.append(
ParserError(meta,
"Cost and price currencies must match: {} != {}".format(
cost.currency, price.currency), None))
return Posting(account, units, cost, price, chr(flag) if flag else None, meta)
beancount.parser.grammar.Builder.price(self, filename, lineno, date, currency, amount, kvlist)
Process a price directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def price(self, filename, lineno, date, currency, amount, kvlist):
"""Process a price directive.
Args:
filename: the current filename.
lineno: the current line number.
date: a datetime object.
currency: the currency to be priced.
amount: an instance of Amount, that is the price of the currency.
kvlist: a list of KeyValue instances.
Returns:
A new Price object.
"""
meta = new_metadata(filename, lineno, kvlist)
return Price(meta, date, currency, amount)
beancount.parser.grammar.Builder.pushmeta(self, key, value)
Set a metadata field on the current key-value pairs to be added to transactions.
Parameters: |
|
---|
Source code in beancount/parser/grammar.py
def pushmeta(self, key, value):
"""Set a metadata field on the current key-value pairs to be added to transactions.
Args:
key_value: A KeyValue instance, to be added to the dict of metadata.
"""
self.meta[key].append(value)
beancount.parser.grammar.Builder.pushtag(self, tag)
Push a tag on the current set of tags.
Note that this does not need to be stack ordered.
Parameters: |
|
---|
Source code in beancount/parser/grammar.py
def pushtag(self, tag):
"""Push a tag on the current set of tags.
Note that this does not need to be stack ordered.
Args:
tag: A string, a tag to be added.
"""
self.tags.append(tag)
beancount.parser.grammar.Builder.query(self, filename, lineno, date, query_name, query_string, kvlist)
Process a document directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def query(self, filename, lineno, date, query_name, query_string, kvlist):
"""Process a document directive.
Args:
filename: the current filename.
lineno: the current line number.
date: a datetime object.
query_name: a str, the name of the query.
query_string: a str, the SQL query itself.
kvlist: a list of KeyValue instances.
Returns:
A new Query object.
"""
meta = new_metadata(filename, lineno, kvlist)
return Query(meta, date, query_name, query_string)
beancount.parser.grammar.Builder.store_result(self, entries)
Start rule stores the final result here.
Parameters: |
|
---|
Source code in beancount/parser/grammar.py
def store_result(self, entries):
"""Start rule stores the final result here.
Args:
entries: A list of entries to store.
"""
if entries:
self.entries = entries
beancount.parser.grammar.Builder.tag_link_LINK(self, tags_links, link)
Add a link to the TagsLinks accumulator.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def tag_link_LINK(self, tags_links, link):
"""Add a link to the TagsLinks accumulator.
Args:
tags_links: The current TagsLinks accumulator.
link: A string, the new link to insert.
Returns:
An updated TagsLinks instance.
"""
tags_links.links.add(link)
return tags_links
beancount.parser.grammar.Builder.tag_link_STRING(self, tags_links, string)
Add a string to the TagsLinks accumulator.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def tag_link_STRING(self, tags_links, string):
"""Add a string to the TagsLinks accumulator.
Args:
tags_links: The current TagsLinks accumulator.
string: A string, the new string to insert in the list.
Returns:
An updated TagsLinks instance.
"""
tags_links.strings.append(string)
return tags_links
beancount.parser.grammar.Builder.tag_link_TAG(self, tags_links, tag)
Add a tag to the TagsLinks accumulator.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def tag_link_TAG(self, tags_links, tag):
"""Add a tag to the TagsLinks accumulator.
Args:
tags_links: The current TagsLinks accumulator.
tag: A string, the new tag to insert.
Returns:
An updated TagsLinks instance.
"""
tags_links.tags.add(tag)
return tags_links
beancount.parser.grammar.Builder.tag_link_new(self, _)
Create a new TagsLinks instance.
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def tag_link_new(self, _):
"""Create a new TagsLinks instance.
Returns:
An instance of TagsLinks, initialized with expected attributes.
"""
return TagsLinks(set(), set())
beancount.parser.grammar.Builder.transaction(self, filename, lineno, date, flag, txn_strings, tags_links, posting_or_kv_list)
Process a transaction directive.
All the postings of the transaction are available at this point, and so the the transaction is balanced here, incomplete postings are completed with the appropriate position, and errors are being accumulated on the builder to be reported later on.
This is the main routine that takes up most of the parsing time; be very careful with modifications here, they have an impact on performance.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def transaction(self, filename, lineno, date, flag, txn_strings, tags_links,
posting_or_kv_list):
"""Process a transaction directive.
All the postings of the transaction are available at this point, and so the
the transaction is balanced here, incomplete postings are completed with the
appropriate position, and errors are being accumulated on the builder to be
reported later on.
This is the main routine that takes up most of the parsing time; be very
careful with modifications here, they have an impact on performance.
Args:
filename: the current filename.
lineno: the current line number.
date: a datetime object.
flag: a str, one-character, the flag associated with this transaction.
txn_strings: A list of strings, possibly empty, possibly longer.
tags_links: A TagsLinks namedtuple of tags, and/or links.
posting_or_kv_list: a list of Posting or KeyValue instances, to be inserted in
this transaction, or None, if no postings have been declared.
Returns:
A new Transaction object.
"""
meta = new_metadata(filename, lineno)
# Separate postings and key-values.
explicit_meta = {}
postings = []
tags, links = tags_links.tags, tags_links.links
if posting_or_kv_list:
last_posting = None
for posting_or_kv in posting_or_kv_list:
if isinstance(posting_or_kv, Posting):
postings.append(posting_or_kv)
last_posting = posting_or_kv
elif isinstance(posting_or_kv, TagsLinks):
if postings:
self.errors.append(ParserError(
meta,
"Tags or links not allowed after first " +
"Posting: {}".format(posting_or_kv), None))
else:
tags.update(posting_or_kv.tags)
links.update(posting_or_kv.links)
else:
if last_posting is None:
value = explicit_meta.setdefault(posting_or_kv.key,
posting_or_kv.value)
if value is not posting_or_kv.value:
self.errors.append(ParserError(
meta, "Duplicate metadata field on entry: {}".format(
posting_or_kv), None))
else:
if last_posting.meta is None:
last_posting = last_posting._replace(meta={})
postings.pop(-1)
postings.append(last_posting)
value = last_posting.meta.setdefault(posting_or_kv.key,
posting_or_kv.value)
if value is not posting_or_kv.value:
self.errors.append(ParserError(
meta, "Duplicate posting metadata field: {}".format(
posting_or_kv), None))
# Freeze the tags & links or set to default empty values.
tags, links = self.finalize_tags_links(tags, links)
# Initialize the metadata fields from the set of active values.
if self.meta:
for key, value_list in self.meta.items():
meta[key] = value_list[-1]
# Add on explicitly defined values.
if explicit_meta:
meta.update(explicit_meta)
# Unpack the transaction fields.
payee_narration = self.unpack_txn_strings(txn_strings, meta)
if payee_narration is None:
return None
payee, narration = payee_narration
# We now allow a single posting when its balance is zero, so we
# commented out the check below. If a transaction has a single posting
# with a non-zero balance, it'll get caught below in the booking code.
#
# # Detect when a transaction does not have at least two legs.
# if postings is None or len(postings) < 2:
# self.errors.append(
# ParserError(meta,
# "Transaction with only one posting: {}".format(postings),
# None))
# return None
# If there are no postings, make sure we insert a list object.
if postings is None:
postings = []
# Create the transaction.
return Transaction(meta, date, chr(flag),
payee, narration, tags, links, postings)
beancount.parser.grammar.Builder.unpack_txn_strings(self, txn_strings, meta)
Unpack a tags_links accumulator to its payee and narration fields.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def unpack_txn_strings(self, txn_strings, meta):
"""Unpack a tags_links accumulator to its payee and narration fields.
Args:
txn_strings: A list of strings.
meta: A metadata dict for errors generated in this routine.
Returns:
A pair of (payee, narration) strings or None objects, or None, if
there was an error.
"""
num_strings = 0 if txn_strings is None else len(txn_strings)
if num_strings == 1:
payee, narration = None, txn_strings[0]
elif num_strings == 2:
payee, narration = txn_strings
elif num_strings == 0:
payee, narration = None, ""
else:
self.errors.append(
ParserError(meta,
"Too many strings on transaction description: {}".format(
txn_strings), None))
return None
return payee, narration
beancount.parser.grammar.CompoundAmount (tuple)
CompoundAmount(number_per, number_total, currency)
beancount.parser.grammar.CompoundAmount.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/grammar.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.grammar.CompoundAmount.__new__(_cls, number_per, number_total, currency)
special
staticmethod
Create new instance of CompoundAmount(number_per, number_total, currency)
beancount.parser.grammar.CompoundAmount.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/grammar.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.grammar.DeprecatedError (tuple)
DeprecatedError(source, message, entry)
beancount.parser.grammar.DeprecatedError.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/grammar.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.grammar.DeprecatedError.__new__(_cls, source, message, entry)
special
staticmethod
Create new instance of DeprecatedError(source, message, entry)
beancount.parser.grammar.DeprecatedError.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/grammar.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.grammar.KeyValue (tuple)
KeyValue(key, value)
beancount.parser.grammar.KeyValue.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/grammar.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.grammar.KeyValue.__new__(_cls, key, value)
special
staticmethod
Create new instance of KeyValue(key, value)
beancount.parser.grammar.KeyValue.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/grammar.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.grammar.ParserError (tuple)
ParserError(source, message, entry)
beancount.parser.grammar.ParserError.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/grammar.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.grammar.ParserError.__new__(_cls, source, message, entry)
special
staticmethod
Create new instance of ParserError(source, message, entry)
beancount.parser.grammar.ParserError.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/grammar.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.grammar.ParserSyntaxError (tuple)
ParserSyntaxError(source, message, entry)
beancount.parser.grammar.ParserSyntaxError.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/grammar.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.grammar.ParserSyntaxError.__new__(_cls, source, message, entry)
special
staticmethod
Create new instance of ParserSyntaxError(source, message, entry)
beancount.parser.grammar.ParserSyntaxError.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/grammar.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.grammar.TagsLinks (tuple)
TagsLinks(tags, links)
beancount.parser.grammar.TagsLinks.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/grammar.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.grammar.TagsLinks.__new__(_cls, tags, links)
special
staticmethod
Create new instance of TagsLinks(tags, links)
beancount.parser.grammar.TagsLinks.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/grammar.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.grammar.ValueType (tuple)
ValueType(value, dtype)
beancount.parser.grammar.ValueType.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/grammar.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.grammar.ValueType.__new__(_cls, value, dtype)
special
staticmethod
Create new instance of ValueType(value, dtype)
beancount.parser.grammar.ValueType.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/grammar.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.grammar.valid_account_regexp(options)
Build a regexp to validate account names from the options.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/grammar.py
def valid_account_regexp(options):
"""Build a regexp to validate account names from the options.
Args:
options: A dict of options, as per beancount.parser.options.
Returns:
A string, a regular expression that will match all account names.
"""
names = map(options.__getitem__, ('name_assets',
'name_liabilities',
'name_equity',
'name_income',
'name_expenses'))
# Replace the first term of the account regular expression with the specific
# names allowed under the options configuration. This code is kept in sync
# with {5672c7270e1e}.
return re.compile("(?:{})(?:{}{})+".format('|'.join(names),
account.sep,
account.ACC_COMP_NAME_RE))
beancount.parser.hashsrc
Compute a hash of the source files in order to warn when the source goes out of date.
beancount.parser.hashsrc.check_parser_source_files()
Check the extension module's source hash and issue a warning if the current source differs from that of the module.
If the source files aren't located in the Python source directory, ignore the warning, we're probably running this from an installed based, in which case we don't need to check anything (this check is useful only for people running directly from source).
Source code in beancount/parser/hashsrc.py
def check_parser_source_files():
"""Check the extension module's source hash and issue a warning if the
current source differs from that of the module.
If the source files aren't located in the Python source directory, ignore
the warning, we're probably running this from an installed based, in which
case we don't need to check anything (this check is useful only for people
running directly from source).
"""
parser_source_hash = hash_parser_source_files()
if parser_source_hash is None:
return
# pylint: disable=import-outside-toplevel
from . import _parser
if _parser.SOURCE_HASH and _parser.SOURCE_HASH != parser_source_hash:
warnings.warn(
("The Beancount parser C extension module is out-of-date ('{}' != '{}'). "
"You need to rebuild.").format(_parser.SOURCE_HASH, parser_source_hash))
beancount.parser.hashsrc.hash_parser_source_files()
Compute a unique hash of the parser's Python code in order to bake that into the extension module. This is used at load-time to verify that the extension module and the corresponding Python codes match each other. If not, it issues a warning that you should rebuild your extension module.
Returns: |
|
---|
Source code in beancount/parser/hashsrc.py
def hash_parser_source_files():
"""Compute a unique hash of the parser's Python code in order to bake that into
the extension module. This is used at load-time to verify that the extension
module and the corresponding Python codes match each other. If not, it
issues a warning that you should rebuild your extension module.
Returns:
A string, the hexadecimal unique hash of relevant source code that should
trigger a recompilation.
"""
md5 = hashlib.md5()
for filename in PARSER_SOURCE_FILES:
fullname = path.join(path.dirname(__file__), filename)
if not path.exists(fullname):
return None
with open(fullname, 'rb') as file:
md5.update(file.read())
# Note: Prepend a character in front of the hash because under Windows MSDEV
# removes escapes, and if the hash starts with a number it fails to
# recognize this is a string. A small compromise for portability.
return md5.hexdigest()
beancount.parser.lexer
Beancount syntax lexer.
beancount.parser.lexer.LexBuilder
A builder used only for building lexer objects.
Attributes:
Name | Type | Description |
---|---|---|
long_string_maxlines_default |
Number of lines for a string to trigger a warning. This is meant to help users detecting dangling quotes in their source. |
beancount.parser.lexer.LexBuilder.ACCOUNT(self, account_name)
Process an ACCOUNT token.
This function attempts to reuse an existing account if one exists, otherwise creates one on-demand.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/lexer.py
def ACCOUNT(self, account_name):
"""Process an ACCOUNT token.
This function attempts to reuse an existing account if one exists,
otherwise creates one on-demand.
Args:
account_name: a str, the valid name of an account.
Returns:
A string, the name of the account.
"""
# Check account name validity.
if not self.account_regexp.match(account_name):
raise ValueError("Invalid account name: {}".format(account_name))
# Reuse (intern) account strings as much as possible. This potentially
# reduces memory usage a fair bit, because these strings are repeated
# liberally.
return self.accounts.setdefault(account_name, account_name)
beancount.parser.lexer.LexBuilder.CURRENCY(self, currency_name)
Process a CURRENCY token.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/lexer.py
def CURRENCY(self, currency_name):
"""Process a CURRENCY token.
Args:
currency_name: the name of the currency.
Returns:
A new currency object; for now, these are simply represented
as the currency name.
"""
self.commodities.add(currency_name)
return currency_name
beancount.parser.lexer.LexBuilder.DATE(self, year, month, day)
Process a DATE token.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/lexer.py
def DATE(self, year, month, day):
"""Process a DATE token.
Args:
year: integer year.
month: integer month.
day: integer day
Returns:
A new datetime object.
"""
return datetime.date(year, month, day)
beancount.parser.lexer.LexBuilder.KEY(self, ident)
Process an identifier token.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/lexer.py
def KEY(self, ident):
"""Process an identifier token.
Args:
ident: a str, the name of the key string.
Returns:
The link string itself. For now we don't need to represent this by
an object.
"""
return ident
beancount.parser.lexer.LexBuilder.LINK(self, link)
Process a LINK token.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/lexer.py
def LINK(self, link):
"""Process a LINK token.
Args:
link: a str, the name of the string.
Returns:
The link string itself. For now we don't need to represent this by
an object.
"""
return link
beancount.parser.lexer.LexBuilder.NUMBER(self, number)
Process a NUMBER token. Convert into Decimal.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/lexer.py
def NUMBER(self, number):
"""Process a NUMBER token. Convert into Decimal.
Args:
number: a str, the number to be converted.
Returns:
A Decimal instance built of the number string.
"""
# Note: We don't use D() for efficiency here.
# The lexer will only yield valid number strings.
if ',' in number:
# Extract the integer part and check the commas match the
# locale-aware formatted version. This
match = re.match(r"([\d,]*)(\.\d*)?$", number)
if not match:
# This path is never taken because the lexer will parse a comma
# in the fractional part as two NUMBERs with a COMMA token in
# between.
self.errors.append(
LexerError(self.get_lexer_location(),
"Invalid number format: '{}'".format(number), None))
else:
int_string, float_string = match.groups()
reformatted_number = r"{:,.0f}".format(int(int_string.replace(",", "")))
if int_string != reformatted_number:
self.errors.append(
LexerError(self.get_lexer_location(),
"Invalid commas: '{}'".format(number), None))
number = number.replace(',', '')
return Decimal(number)
beancount.parser.lexer.LexBuilder.STRING(self, string)
Process a STRING token.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/lexer.py
def STRING(self, string):
"""Process a STRING token.
Args:
string: the string to process.
Returns:
The string. Nothing to be done or cleaned up. Eventually we might
do some decoding here.
"""
# If a multiline string, warm over a certain number of lines.
if '\n' in string:
num_lines = string.count('\n') + 1
if num_lines > self.long_string_maxlines_default:
# This is just a warning; accept the string anyhow.
self.errors.append(
LexerError(
self.get_lexer_location(),
"String too long ({} lines); possible error".format(num_lines),
None))
return string
beancount.parser.lexer.LexBuilder.TAG(self, tag)
Process a TAG token.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/lexer.py
def TAG(self, tag):
"""Process a TAG token.
Args:
tag: a str, the tag to be processed.
Returns:
The tag string itself. For now we don't need an object to represent
those; keeping it simple.
"""
return tag
beancount.parser.lexer.LexBuilder.build_lexer_error(self, message, exc_type=None)
Build a lexer error and appends it to the list of pending errors.
Parameters: |
|
---|
Source code in beancount/parser/lexer.py
def build_lexer_error(self, message, exc_type=None): # {0e31aeca3363}
"""Build a lexer error and appends it to the list of pending errors.
Args:
message: The message of the error.
exc_type: An exception type, if an exception occurred.
"""
if not isinstance(message, str):
message = str(message)
if exc_type is not None:
message = '{}: {}'.format(exc_type.__name__, message)
self.errors.append(
LexerError(self.get_lexer_location(), message, None))
beancount.parser.lexer.LexBuilder.get_invalid_account(self)
Return the name of an invalid account placeholder.
When an account name is not deemed a valid one, replace it by this account name. This can be overridden by the parser to take into account the options.
Returns: |
|
---|
Source code in beancount/parser/lexer.py
def get_invalid_account(self):
"""Return the name of an invalid account placeholder.
When an account name is not deemed a valid one, replace it by
this account name. This can be overridden by the parser to
take into account the options.
Returns:
A string, the name of the root/type for invalid account names.
"""
return 'Equity:InvalidAccountName'
beancount.parser.lexer.LexerError (tuple)
LexerError(source, message, entry)
beancount.parser.lexer.LexerError.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/lexer.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.lexer.LexerError.__new__(_cls, source, message, entry)
special
staticmethod
Create new instance of LexerError(source, message, entry)
beancount.parser.lexer.LexerError.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/lexer.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.lexer.lex_iter(file, builder=None, encoding=None)
An iterator that yields all the tokens in the given file.
Parameters: |
|
---|
Yields: Tuples of the token (a string), the matched text (a string), and the line no (an integer).
Source code in beancount/parser/lexer.py
def lex_iter(file, builder=None, encoding=None):
"""An iterator that yields all the tokens in the given file.
Args:
file: A string, the filename to run the lexer on, or a file object.
builder: A builder of your choice. If not specified, a LexBuilder is
used and discarded (along with its errors).
encoding: A string (or None), the default encoding to use for strings.
Yields:
Tuples of the token (a string), the matched text (a string), and the line
no (an integer).
"""
if isinstance(file, str):
filename = file
else:
filename = file.name
if builder is None:
builder = LexBuilder()
_parser.lexer_initialize(filename, builder, encoding)
try:
while 1:
token_tuple = _parser.lexer_next()
if token_tuple is None:
break
yield token_tuple
finally:
_parser.lexer_finalize()
beancount.parser.lexer.lex_iter_string(string, builder=None, encoding=None)
Parse an input string and print the tokens to an output file.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/lexer.py
def lex_iter_string(string, builder=None, encoding=None):
"""Parse an input string and print the tokens to an output file.
Args:
input_string: a str or bytes, the contents of the ledger to be parsed.
builder: A builder of your choice. If not specified, a LexBuilder is
used and discarded (along with its errors).
encoding: A string (or None), the default encoding to use for strings.
Returns:
A iterator on the string. See lex_iter() for details.
"""
tmp_file = tempfile.NamedTemporaryFile('w' if isinstance(string, str) else 'wb')
tmp_file.write(string)
tmp_file.flush()
# Note: We pass in the file object in order to keep it alive during parsing.
return lex_iter(tmp_file, builder, encoding)
beancount.parser.options
Declaration of options and their default values.
beancount.parser.options.OptDesc (tuple)
OptDesc(name, default_value, example_value, converter, deprecated, alias)
beancount.parser.options.OptDesc.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/options.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.options.OptDesc.__new__(_cls, name, default_value, example_value, converter, deprecated, alias)
special
staticmethod
Create new instance of OptDesc(name, default_value, example_value, converter, deprecated, alias)
beancount.parser.options.OptDesc.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/options.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.options.OptGroup (tuple)
OptGroup(description, options)
beancount.parser.options.OptGroup.__getnewargs__(self)
special
Return self as a plain tuple. Used by copy and pickle.
Source code in beancount/parser/options.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
beancount.parser.options.OptGroup.__new__(_cls, description, options)
special
staticmethod
Create new instance of OptGroup(description, options)
beancount.parser.options.OptGroup.__repr__(self)
special
Return a nicely formatted representation string
Source code in beancount/parser/options.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
beancount.parser.options.Opt(name, default_value, example_value=<object object at 0x755e169893e0>, converter=None, deprecated=False, alias=None)
Alternative constructor for OptDesc, with default values.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/options.py
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)
beancount.parser.options.get_account_types(options)
Extract the account type names from the parser's options.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/options.py
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")])
beancount.parser.options.get_current_accounts(options)
Return account names for the current earnings and conversion accounts.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/options.py
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)
beancount.parser.options.get_previous_accounts(options)
Return account names for the previous earnings, balances and conversion accounts.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/options.py
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)
beancount.parser.options.list_options()
Produce a formatted text of the available options and their description.
Returns: |
|
---|
Source code in beancount/parser/options.py
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()
beancount.parser.options.options_validate_booking_method(value)
Validate a booking method name.
Parameters: |
|
---|
Returns: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/options.py
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))
beancount.parser.options.options_validate_boolean(value)
Validate a boolean option.
Parameters: |
|
---|
Returns: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/options.py
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')
beancount.parser.options.options_validate_plugin(value)
Validate the plugin option.
Parameters: |
|
---|
Returns: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/options.py
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)
beancount.parser.options.options_validate_processing_mode(value)
Validate the options processing mode.
Parameters: |
|
---|
Returns: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/options.py
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
beancount.parser.options.options_validate_tolerance(value)
Validate the tolerance option.
Parameters: |
|
---|
Returns: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/options.py
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)
beancount.parser.options.options_validate_tolerance_map(value)
Validate an option with a map of currency/tolerance pairs in a string.
Parameters: |
|
---|
Returns: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/options.py
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))
beancount.parser.parser
Beancount syntax parser.
IMPORTANT: The parser (and its grammar builder) produces "incomplete" Transaction objects. This means that some of the data can be found missing from the output of the parser and some of the data types vary slightly. Missing components are replaced not by None, but by a special constant 'NA' which helps diagnose problems if a user inadvertently attempts to work on an incomplete posting instead of a complete one. Those incomplete entries are then run through the "booking" routines which do two things simultaneously:
- They find matching lots for reducing inventory positions, and
- They interpolate missing numbers.
In doing so they normalize the entries to "complete" entries by converting a position/lot's "cost" attribute from a CostSpec to a Cost. A Cost is similar to an Amount in that it shares "number" and "currency" attributes, but also has a label and a lot date. A CostSpec is similar to a Cost, but has all optional data; it consists in a specification for matching against a particular inventory lot.
Other parts of a posting may also be missing, not just parts of the cost. Leaving out some parts of the input is used to invoke interpolation, to tell Beancount to automatically compute the missing numbers (if possible).
Missing components will be set to the special value "beancount.core.number.MISSING" until inventory booking and number interpolation has been completed. The "MISSING" value should never appear in completed, loaded transaction postings.
For instance, all of the units may be missing:
INPUT: Assets:Account posting.units = MISSING
Or just the number of the units:
INPUT: Assets:Account USD posting.units = Amount(MISSING, "USD")
You must always specify the currency.
If a price annotation is simply absent, it appears as None:
INPUT: Assets:Account 2 MXN posting.price = None
However, you may indicate that there is a price but have Beancount compute it automatically:
INPUT: Assets:Account 2 MXN @ posting.price = Amount(MISSING, MISSING)
Indicating the conversion currency is also possible (and recommended):
INPUT: Assets:Account 2 MXN @ USD posting.price = Amount(MISSING, "USD")
If a cost specification is provided, a "cost" attribute it set but it does not refer to a Cost instance (as in complete entries) but rather to a CostSpec instance. Some of the fields of a CostSpec may be MISSING if they were not specified in the input. For example:
INPUT: Assets:Account 1 HOOL {100 # 5 USD} posting.cost = CostSpec(Decimal("100"), Decimal("5"), "USD", None, None, False))
Note how we never consider the label of date override to be MISSING; this is because those inputs are optional: A missing label is simply left unset in the computed Cost, and a missing date override uses the date of the transaction that contains the posting.
You can indicate that there is a total number to be filled in like this:
INPUT: Assets:Account 1 HOOL {100 # USD} posting.cost = CostSpec(Decimal("100"), MISSING, "USD", None, None, False))
This is in contrast to the total value simple not being used:
INPUT: Assets:Account 1 HOOL {100 USD} posting.cost = CostSpec(Decimal("100"), None, "USD", None, None, False))
Both per-unit and total numbers may be omitted as well, in which case, only the number-per-unit portion of the CostSpec will appear as MISSING:
INPUT: Assets:Account 1 HOOL {USD} posting.cost = CostSpec(MISSING, None, "USD", None, None, False))
And furthermore, all the cost basis may be missing:
INPUT: Assets:Account 1 HOOL {} posting.cost = CostSpec(MISSING, None, MISSING, None, None, False))
If you ask for the lots to be merged, you get this:
INPUT: Assets:Account 1 HOOL {*} posting.cost = CostSpec(MISSING, None, MISSING, None, None, True))
The numbers have to be computed by Beancount, so we output this with MISSING values.
Of course, you can provide only the non-basis information, like just the date or label:
INPUT: Assets:Account 1 HOOL {2015-09-21} posting.cost = CostSpec(MISSING, None, MISSING, date(2015, 9, 21), None, True)
See the test beancount.parser.grammar_test.TestIncompleteInputs for examples and corresponding expected values.
beancount.parser.parser.is_entry_incomplete(entry)
Detect the presence of elided amounts in Transactions.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/parser.py
def is_entry_incomplete(entry):
"""Detect the presence of elided amounts in Transactions.
Args:
entries: A directive.
Returns:
A boolean, true if there are some missing portions of any postings found.
"""
if isinstance(entry, data.Transaction):
if any(is_posting_incomplete(posting) for posting in entry.postings):
return True
return False
beancount.parser.parser.is_posting_incomplete(posting)
Detect the presence of any elided amounts in a Posting.
If any of the possible amounts are missing, this returns True.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/parser.py
def is_posting_incomplete(posting):
"""Detect the presence of any elided amounts in a Posting.
If any of the possible amounts are missing, this returns True.
Args:
entries: A directive.
Returns:
A boolean, true if there are some missing portions of any postings found.
"""
units = posting.units
if (units is MISSING or
units.number is MISSING or
units.currency is MISSING):
return True
price = posting.price
if (price is MISSING or
price is not None and (price.number is MISSING or
price.currency is MISSING)):
return True
cost = posting.cost
if cost is not None and (cost.number_per is MISSING or
cost.number_total is MISSING or
cost.currency is MISSING):
return True
return False
beancount.parser.parser.parse_doc(expect_errors=False, allow_incomplete=False)
Factory of decorators that parse the function's docstring as an argument.
Note that the decorators thus generated only run the parser on the tests, not the loader, so is no validation, balance checks, nor plugins applied to the parsed text.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/parser.py
def parse_doc(expect_errors=False, allow_incomplete=False):
"""Factory of decorators that parse the function's docstring as an argument.
Note that the decorators thus generated only run the parser on the tests,
not the loader, so is no validation, balance checks, nor plugins applied to
the parsed text.
Args:
expect_errors: A boolean or None, with the following semantics,
True: Expect errors and fail if there are none.
False: Expect no errors and fail if there are some.
None: Do nothing, no check.
allow_incomplete: A boolean, if true, allow incomplete input. Otherwise
barf if the input would require interpolation. The default value is set
not to allow it because we want to minimize the features tests depend on.
Returns:
A decorator for test functions.
"""
def decorator(fun):
"""A decorator that parses the function's docstring as an argument.
Args:
fun: the function object to be decorated.
Returns:
A decorated test function.
"""
filename = inspect.getfile(fun)
lines, lineno = inspect.getsourcelines(fun)
# decorator line + function definition line (I realize this is largely
# imperfect, but it's only for reporting in our tests) - empty first line
# stripped away.
lineno += 1
@functools.wraps(fun)
def wrapper(self):
assert fun.__doc__ is not None, (
"You need to insert a docstring on {}".format(fun.__name__))
entries, errors, options_map = parse_string(fun.__doc__,
report_filename=filename,
report_firstline=lineno,
dedent=True)
if not allow_incomplete and any(is_entry_incomplete(entry)
for entry in entries):
self.fail("parse_doc() may not use interpolation.")
if expect_errors is not None:
if expect_errors is False and errors:
oss = io.StringIO()
printer.print_errors(errors, file=oss)
self.fail("Unexpected errors found:\n{}".format(oss.getvalue()))
elif expect_errors is True and not errors:
self.fail("Expected errors, none found:")
return fun(self, entries, errors, options_map)
wrapper.__input__ = wrapper.__doc__
wrapper.__doc__ = None
return wrapper
return decorator
beancount.parser.parser.parse_file(filename, **kw)
Parse a beancount input file and return Ledger with the list of transactions and tree of accounts.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/parser.py
def parse_file(filename, **kw):
"""Parse a beancount input file and return Ledger with the list of
transactions and tree of accounts.
Args:
filename: the name of the file to be parsed.
kw: a dict of keywords to be applied to the C parser.
Returns:
A tuple of (
list of entries parsed in the file,
list of errors that were encountered during parsing, and
a dict of the option values that were parsed from the file.)
"""
abs_filename = path.abspath(filename) if filename else None
builder = grammar.Builder(abs_filename)
_parser.parse_file(filename, builder, **kw)
return builder.finalize()
beancount.parser.parser.parse_many(string, level=0)
Parse a string with a snippet of Beancount input and replace vars from caller.
Parameters: |
|
---|
Returns: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/parser.py
def parse_many(string, level=0):
"""Parse a string with a snippet of Beancount input and replace vars from caller.
Args:
string: A string with some Beancount input.
level: The number of extra stacks to ignore.
Returns:
A list of entries.
Raises:
AssertionError: If there are any errors.
"""
# Get the locals in the stack for the callers and produce the final text.
frame = inspect.stack()[level+1]
varkwds = frame[0].f_locals
input_string = textwrap.dedent(string.format(**varkwds))
# Parse entries and check there are no errors.
entries, errors, __ = parse_string(input_string)
assert not errors
return entries
beancount.parser.parser.parse_one(string)
Parse a string with single Beancount directive and replace vars from caller.
Parameters: |
|
---|
Returns: |
|
---|
Exceptions: |
|
---|
Source code in beancount/parser/parser.py
def parse_one(string):
"""Parse a string with single Beancount directive and replace vars from caller.
Args:
string: A string with some Beancount input.
level: The number of extra stacks to ignore.
Returns:
A list of entries.
Raises:
AssertionError: If there are any errors.
"""
entries = parse_many(string, level=1)
assert len(entries) == 1
return entries[0]
beancount.parser.parser.parse_string(string, report_filename=None, **kw)
Parse a beancount input file and return Ledger with the list of transactions and tree of accounts.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/parser.py
def parse_string(string, report_filename=None, **kw):
"""Parse a beancount input file and return Ledger with the list of
transactions and tree of accounts.
Args:
string: A string, the contents to be parsed instead of a file's.
report_filename: A string, the source filename from which this string
has been extracted, if any. This is stored in the metadata of the
parsed entries.
**kw: See parse.c. This function parses out 'dedent' which removes
whitespace from the front of the text (default is False).
Return:
Same as the output of parse_file().
"""
if kw.pop('dedent', None):
string = textwrap.dedent(string)
builder = grammar.Builder(report_filename or '<string>')
_parser.parse_string(string, builder, report_filename=report_filename, **kw)
return builder.finalize()
beancount.parser.printer
Conversion from internal data structures to text.
beancount.parser.printer.EntryPrinter
A multi-method interface for printing all directive types.
Attributes:
Name | Type | Description |
---|---|---|
dcontext |
An instance of DisplayContext with which to render all the numbers. |
|
render_weight |
A boolean, true if we should render the weight of the postings as a comment, for debugging. |
|
min_width_account |
An integer, the minimum width to leave for the account name. |
beancount.parser.printer.EntryPrinter.__call__(self, obj)
special
Render a directive.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/printer.py
def __call__(self, obj):
"""Render a directive.
Args:
obj: The directive to be rendered.
Returns:
A string, the rendered directive.
"""
oss = io.StringIO()
method = getattr(self, obj.__class__.__name__)
method(obj, oss)
return oss.getvalue()
beancount.parser.printer.EntryPrinter.render_posting_strings(self, posting)
This renders the three components of a posting: the account and its optional posting flag, the position, and finally, the weight of the position. The purpose is to align these in the caller.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/printer.py
def render_posting_strings(self, posting):
"""This renders the three components of a posting: the account and its optional
posting flag, the position, and finally, the weight of the position. The
purpose is to align these in the caller.
Args:
posting: An instance of Posting, the posting to render.
Returns:
A tuple of
flag_account: A string, the account name including the flag.
position_str: A string, the rendered position string.
weight_str: A string, the rendered weight of the posting.
"""
# Render a string of the flag and the account.
flag = '{} '.format(posting.flag) if posting.flag else ''
flag_account = flag + posting.account
# Render a string with the amount and cost and optional price, if
# present. Also render a string with the weight.
weight_str = ''
if isinstance(posting.units, amount.Amount):
position_str = position.to_string(posting, self.dformat)
# Note: we render weights at maximum precision, for debugging.
if posting.cost is None or (isinstance(posting.cost, position.Cost) and
isinstance(posting.cost.number, Decimal)):
weight_str = str(convert.get_weight(posting))
else:
position_str = ''
if posting.price is not None:
position_str += ' @ {}'.format(posting.price.to_string(self.dformat_max))
return flag_account, position_str, weight_str
beancount.parser.printer.EntryPrinter.write_metadata(self, meta, oss, prefix=None)
Write metadata to the file object, excluding filename and line number.
Parameters: |
|
---|
Source code in beancount/parser/printer.py
def write_metadata(self, meta, oss, prefix=None):
"""Write metadata to the file object, excluding filename and line number.
Args:
meta: A dict that contains the metadata for this directive.
oss: A file object to write to.
"""
if meta is None:
return
if prefix is None:
prefix = self.prefix
for key, value in sorted(meta.items()):
if key not in self.META_IGNORE:
value_str = None
if isinstance(value, str):
value_str = '"{}"'.format(misc_utils.escape_string(value))
elif isinstance(value, (Decimal, datetime.date, amount.Amount)):
value_str = str(value)
elif isinstance(value, bool):
value_str = 'TRUE' if value else 'FALSE'
elif isinstance(value, (dict, inventory.Inventory)):
pass # Ignore dicts, don't print them out.
elif value is None:
value_str = '' # Render null metadata as empty, on purpose.
else:
raise ValueError("Unexpected value: '{!r}'".format(value))
if value_str is not None:
oss.write("{}{}: {}\n".format(prefix, key, value_str))
beancount.parser.printer.align_position_strings(strings)
A helper used to align rendered amounts positions to their first currency character (an uppercase letter). This class accepts a list of rendered positions and calculates the necessary width to render them stacked in a column so that the first currency word aligns. It does not go beyond that (further currencies, e.g. for the price or cost, are not aligned).
This is perhaps best explained with an example. The following positions will be aligned around the column marked with '^':
45 HOOL {504.30 USD}
4 HOOL {504.30 USD, 2014-11-11}
9.95 USD
-22473.32 CAD @ 1.10 USD ^
Strings without a currency character will be rendered flush left.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/printer.py
def align_position_strings(strings):
"""A helper used to align rendered amounts positions to their first currency
character (an uppercase letter). This class accepts a list of rendered
positions and calculates the necessary width to render them stacked in a
column so that the first currency word aligns. It does not go beyond that
(further currencies, e.g. for the price or cost, are not aligned).
This is perhaps best explained with an example. The following positions will
be aligned around the column marked with '^':
45 HOOL {504.30 USD}
4 HOOL {504.30 USD, 2014-11-11}
9.95 USD
-22473.32 CAD @ 1.10 USD
^
Strings without a currency character will be rendered flush left.
Args:
strings: A list of rendered position or amount strings.
Returns:
A pair of a list of aligned strings and the width of the aligned strings.
"""
# Maximum length before the alignment character.
max_before = 0
# Maximum length after the alignment character.
max_after = 0
# Maximum length of unknown strings.
max_unknown = 0
string_items = []
search = re.compile('[A-Z]').search
for string in strings:
match = search(string)
if match:
index = match.start()
if index != 0:
max_before = max(index, max_before)
max_after = max(len(string) - index, max_after)
string_items.append((index, string))
continue
# else
max_unknown = max(len(string), max_unknown)
string_items.append((None, string))
# Compute formatting string.
max_total = max(max_before + max_after, max_unknown)
max_after_prime = max_total - max_before
fmt = "{{:>{0}}}{{:{1}}}".format(max_before, max_after_prime).format
fmt_unknown = "{{:<{0}}}".format(max_total).format
# Align the strings and return them.
aligned_strings = []
for index, string in string_items:
if index is not None:
string = fmt(string[:index], string[index:])
else:
string = fmt_unknown(string)
aligned_strings.append(string)
return aligned_strings, max_total
beancount.parser.printer.format_entry(entry, dcontext=None, render_weights=False, prefix=None)
Format an entry into a string in the same input syntax the parser accepts.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/printer.py
def format_entry(entry, dcontext=None, render_weights=False, prefix=None):
"""Format an entry into a string in the same input syntax the parser accepts.
Args:
entry: An entry instance.
dcontext: An instance of DisplayContext used to format the numbers.
render_weights: A boolean, true to render the weights for debugging.
Returns:
A string, the formatted entry.
"""
return EntryPrinter(dcontext, render_weights, prefix=prefix)(entry)
beancount.parser.printer.format_error(error)
Given an error objects, return a formatted string for it.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/printer.py
def format_error(error):
"""Given an error objects, return a formatted string for it.
Args:
error: a namedtuple objects representing an error. It has to have an
'entry' attribute that may be either a single directive object or a
list of directive objects.
Returns:
A string, the errors rendered.
"""
oss = io.StringIO()
oss.write('{} {}\n'.format(render_source(error.source), error.message))
if error.entry is not None:
entries = error.entry if isinstance(error.entry, list) else [error.entry]
error_string = '\n'.join(format_entry(entry) for entry in entries)
oss.write('\n')
oss.write(textwrap.indent(error_string, ' '))
oss.write('\n')
return oss.getvalue()
beancount.parser.printer.print_entries(entries, dcontext=None, render_weights=False, file=None, prefix=None)
A convenience function that prints a list of entries to a file.
Parameters: |
|
---|
Source code in beancount/parser/printer.py
def print_entries(entries, dcontext=None, render_weights=False, file=None, prefix=None):
"""A convenience function that prints a list of entries to a file.
Args:
entries: A list of directives.
dcontext: An instance of DisplayContext used to format the numbers.
render_weights: A boolean, true to render the weights for debugging.
file: An optional file object to write the entries to.
"""
assert isinstance(entries, list), "Entries is not a list: {}".format(entries)
output = file or (codecs.getwriter("utf-8")(sys.stdout.buffer)
if hasattr(sys.stdout, 'buffer') else
sys.stdout)
if prefix:
output.write(prefix)
previous_type = type(entries[0]) if entries else None
eprinter = EntryPrinter(dcontext, render_weights)
for entry in entries:
# Insert a newline between transactions and between blocks of directives
# of the same type.
entry_type = type(entry)
if (entry_type in (data.Transaction, data.Commodity) or
entry_type is not previous_type):
output.write('\n')
previous_type = entry_type
string = eprinter(entry)
output.write(string)
beancount.parser.printer.print_entry(entry, dcontext=None, render_weights=False, file=None)
A convenience function that prints a single entry to a file.
Parameters: |
|
---|
Source code in beancount/parser/printer.py
def print_entry(entry, dcontext=None, render_weights=False, file=None):
"""A convenience function that prints a single entry to a file.
Args:
entry: A directive entry.
dcontext: An instance of DisplayContext used to format the numbers.
render_weights: A boolean, true to render the weights for debugging.
file: An optional file object to write the entries to.
"""
output = file or (codecs.getwriter("utf-8")(sys.stdout.buffer)
if hasattr(sys.stdout, 'buffer') else
sys.stdout)
output.write(format_entry(entry, dcontext, render_weights))
output.write('\n')
beancount.parser.printer.print_error(error, file=None)
A convenience function that prints a single error to a file.
Parameters: |
|
---|
Source code in beancount/parser/printer.py
def print_error(error, file=None):
"""A convenience function that prints a single error to a file.
Args:
error: An error object.
file: An optional file object to write the errors to.
"""
output = file or sys.stdout
output.write(format_error(error))
output.write('\n')
beancount.parser.printer.print_errors(errors, file=None, prefix=None)
A convenience function that prints a list of errors to a file.
Parameters: |
|
---|
Source code in beancount/parser/printer.py
def print_errors(errors, file=None, prefix=None):
"""A convenience function that prints a list of errors to a file.
Args:
errors: A list of errors.
file: An optional file object to write the errors to.
"""
output = file or sys.stdout
if prefix:
output.write(prefix)
for error in errors:
output.write(format_error(error))
output.write('\n')
beancount.parser.printer.render_source(meta)
Render the source for errors in a way that it will be both detected by Emacs and align and rendered nicely.
Parameters: |
|
---|
Returns: |
|
---|
Source code in beancount/parser/printer.py
def render_source(meta):
"""Render the source for errors in a way that it will be both detected by
Emacs and align and rendered nicely.
Args:
meta: A dict with the metadata.
Returns:
A string, rendered to be interpretable as a message location for Emacs or
other editors.
"""
return '{}:{:8}'.format(meta['filename'], '{}:'.format(meta['lineno']))