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:
  • 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.

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:
  • 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.

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:
  • entries – A list of directives.

  • unused_options_map – An options map.

Returns:
  • A list of errors.

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:
  • 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.

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:
  • 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.

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:
  • 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.

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:
  • posting – An instance of Posting.

Returns:
  • An instance of Posting with a possibly replaced 'cost' attribute.

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:
  • refer – An instance of Refer.

Returns:
  • A currency string.

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:
  • 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.

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:
  • 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.)

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:
  • 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.

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:
  • 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.

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:
  • 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.

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 0x7d36c7f723e0>, allow_incomplete=False)

Compare two lists of entries exactly and print missing entries verbosely if they occur.

Parameters:
  • 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.

Exceptions:
  • AssertionError – If the exception fails.

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 0x7d36c7f723e0>, allow_incomplete=False)

Check that subset_entries is not included in entries and print extra entries.

Parameters:
  • 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.

Exceptions:
  • AssertionError – If the exception fails.

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 0x7d36c7f723e0>, allow_incomplete=False)

Check that subset_entries is included in entries and print missing entries.

Parameters:
  • 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.

Exceptions:
  • AssertionError – If the exception fails.

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:
  • 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.

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:
  • number – a Decimal instance, the number of the amount.

  • currency – a currency object (a str, really, see CURRENCY above)

Returns:
  • An instance of Amount.

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:
  • 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.

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:
  • 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.

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:
  • 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.

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:
  • 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.

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:
  • 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.

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:
  • 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".

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:
  • 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.

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:
  • value – One of the accepted custom values.

Returns:
  • A pair of (value, dtype) where 'dtype' is the datatype is that of the value.

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:
  • 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.

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:
  • 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.

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:
  • 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.

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())

Finally amend tags and links and return final objects to be inserted.

Parameters:
  • 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).

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:
  • A list of sorted directives.

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:
  • A dict of option names to options.

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:
  • object_list – the current list of objects.

  • new_object – the new object to be added.

Returns:
  • The new, updated list of objects.

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:
  • filename – current filename.

  • lineno – current line number.

  • include_name – A string, the name of the file to include.

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:
  • 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.

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:
  • 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.

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:
  • 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.

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:
  • filename – current filename.

  • lineno – current line number.

  • key – option's key (str)

  • value – option's value

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:
  • 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.

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:
  • filename – The current filename

  • lineno – The current line number

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:
  • 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.

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:
  • key – A string, a key to be removed from the meta dict.

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:
  • tag – A string, a tag to be removed from the current set of tags.

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:
  • 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.

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:
  • 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.

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:
  • key_value – A KeyValue instance, to be added to the dict of metadata.

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:
  • tag – A string, a tag to be added.

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:
  • 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.

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:
  • entries – A list of entries to store.

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

Add a link to the TagsLinks accumulator.

Parameters:
  • tags_links – The current TagsLinks accumulator.

  • link – A string, the new link to insert.

Returns:
  • An updated TagsLinks instance.

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

Add a string to the TagsLinks accumulator.

Parameters:
  • tags_links – The current TagsLinks accumulator.

  • string – A string, the new string to insert in the list.

Returns:
  • An updated TagsLinks instance.

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

Add a tag to the TagsLinks accumulator.

Parameters:
  • tags_links – The current TagsLinks accumulator.

  • tag – A string, the new tag to insert.

Returns:
  • An updated TagsLinks instance.

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

Create a new TagsLinks instance.

Returns:
  • An instance of TagsLinks, initialized with expected attributes.

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:
  • 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.

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:
  • 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.

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

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:
  • options – A dict of options, as per beancount.parser.options.

Returns:
  • A string, a regular expression that will match all account names.

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:
  • A string, the hexadecimal unique hash of relevant source code that should trigger a recompilation.

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:
  • account_name – a str, the valid name of an account.

Returns:
  • A string, the name of the account.

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:
  • currency_name – the name of the currency.

Returns:
  • A new currency object; for now, these are simply represented as the currency name.

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:
  • year – integer year.

  • month – integer month.

  • day – integer day

Returns:
  • A new datetime object.

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:
  • 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.

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

Process a LINK token.

Parameters:
  • 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.

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:
  • number – a str, the number to be converted.

Returns:
  • A Decimal instance built of the number string.

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:
  • string – the string to process.

Returns:
  • The string. Nothing to be done or cleaned up. Eventually we might do some decoding here.

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:
  • 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.

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:
  • message – The message of the error.

  • exc_type – An exception type, if an exception occurred.

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:
  • A string, the name of the root/type for invalid account names.

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:
  • 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).

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:
  • 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.

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 0x7d36c8f85870>, converter=None, deprecated=False, alias=None)

Alternative constructor for OptDesc, with default values.

Parameters:
  • 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.

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:
  • options – a dict of ledger options.

Returns:
  • An instance of AccountTypes, that contains all the prefixes.

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:
  • options – a dict of ledger options.

Returns:
  • A tuple of 2 account objects, one for booking current earnings, and one for current conversions.

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:
  • options – a dict of ledger options.

Returns:
  • A tuple of 3 account objects, for booking previous earnings, previous balances, and previous conversions.

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:
  • A string, formatted nicely to be printed in 80 columns.

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:
  • value – A string, the value provided as option.

Returns:
  • The new value, converted, if the conversion is successful.

Exceptions:
  • ValueError – If the value is invalid.

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:
  • value – A string, the value provided as option.

Returns:
  • The new value, converted, if the conversion is successful.

Exceptions:
  • ValueError – If the value is invalid.

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:
  • value – A string, the value provided as option.

Returns:
  • The new value, converted, if the conversion is successful.

Exceptions:
  • ValueError – If the value is invalid.

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:
  • value – A string, the value provided as option.

Returns:
  • The new value, converted, if the conversion is successful.

Exceptions:
  • ValueError – If the value is invalid.

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:
  • value – A string, the value provided as option.

Returns:
  • The new value, converted, if the conversion is successful.

Exceptions:
  • ValueError – If the value is invalid.

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:
  • value – A string, the value provided as option.

Returns:
  • The new value, converted, if the conversion is successful.

Exceptions:
  • ValueError – If the value is invalid.

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:

  1. They find matching lots for reducing inventory positions, and
  2. 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:
  • entries – A directive.

Returns:
  • A boolean, true if there are some missing portions of any postings found.

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:
  • entries – A directive.

Returns:
  • A boolean, true if there are some missing portions of any postings found.

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:
  • 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.

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:
  • 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.)

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:
  • string – A string with some Beancount input.

  • level – The number of extra stacks to ignore.

Returns:
  • A list of entries.

Exceptions:
  • AssertionError – If there are any errors.

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:
  • string – A string with some Beancount input.

  • level – The number of extra stacks to ignore.

Returns:
  • A list of entries.

Exceptions:
  • AssertionError – If there are any errors.

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:
  • 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).

Returns:
  • Same as the output of parse_file().

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:
  • obj – The directive to be rendered.

Returns:
  • A string, the rendered directive.

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:
  • 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.

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:
  • meta – A dict that contains the metadata for this directive.

  • oss – A file object to write to.

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:
  • 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.

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:
  • 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.

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:
  • 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.

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:
  • 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.

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:
  • 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.

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:
  • error – An error object.

  • file – An optional file object to write the errors to.

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:
  • errors – A list of errors.

  • file – An optional file object to write the errors to.

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:
  • meta – A dict with the metadata.

Returns:
  • A string, rendered to be interpretable as a message location for Emacs or other editors.

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']))