def do_linked(filename, args): """Print out a list of transactions linked to the one at the given line. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. """ from beancount.parser import options from beancount.parser import printer from beancount.core import account_types from beancount.core import inventory from beancount.core import data from beancount.core import realization from beancount import loader # Parse the arguments, get the line number. if len(args) != 1: raise SystemExit("Missing line number argument.") lineno = int(args[0]) # Load the input file. entries, errors, options_map = loader.load_file(filename) # Find the closest entry. closest_entry = data.find_closest(entries, options_map['filename'], lineno) # Find its links. links = closest_entry.links if closest_entry is None: raise SystemExit("No entry could be found before {}:{}".format( filename, lineno)) if not links: return # Find all linked entries. linked_entries = [ entry for entry in entries if (isinstance(entry, data.Transaction) and entry.links and entry.links & links) ] # Render linked entries (in date order) as errors (for Emacs). errors = [RenderError(entry.meta, '', entry) for entry in linked_entries] printer.print_errors(errors) # Print out balances. real_root = realization.realize(linked_entries) realization.dump_balances(real_root, file=sys.stdout) # Print out net income change. acctypes = options.get_account_types(options_map) net_income = inventory.Inventory() for real_node in realization.iter_children(real_root): if account_types.is_income_statement_account(real_node.account, acctypes): net_income.add_inventory(real_node.balance) print() print('Net Income: {}'.format(-net_income))
def render_file_context(entries, options_map, filename, lineno): """Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. filename: A string, the name of the file from which the transaction was parsed. lineno: An integer, the line number in the file the transaction was parsed from. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. """ # Find the closest entry. closest_entry = data.find_closest(entries, filename, lineno) if closest_entry is None: raise SystemExit("No entry could be found before {}:{}".format( filename, lineno)) # Run just the parser stage (no booking nor interpolation, which would # remove the postings) on the input file to produced the corresponding # unbooked transaction, so that we can get the list of accounts. if path.exists(filename): parsed_entries, _, __ = parser.parse_file(filename) # Note: We cannot bisect as we cannot rely on sorting behavior from the parser. lineno = closest_entry.meta['lineno'] closest_parsed_entries = [ parsed_entry for parsed_entry in parsed_entries if parsed_entry.meta['lineno'] == lineno ] if len(closest_parsed_entries) != 1: # This is an internal error, this should never occur. raise RuntimeError( "Parsed entry corresponding to real entry not found in original filename." ) closest_parsed_entry = next(iter(closest_parsed_entries)) else: closest_parsed_entry = None return render_entry_context(entries, options_map, closest_entry, closest_parsed_entry)
def render_file_context(entries, options_map, filename, lineno): """Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. filename: A string, the name of the file from which the transaction was parsed. lineno: An integer, the line number in the file the transaction was parsed from. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. """ # Find the closest entry. closest_entry = data.find_closest(entries, filename, lineno) if closest_entry is None: raise SystemExit("No entry could be found before {}:{}".format( filename, lineno)) return render_entry_context(entries, options_map, closest_entry)
def test_find_closest(self): entry1 = data.Transaction( data.new_metadata("/tmp/apples.beancount", 200), datetime.date(2014, 9, 14), '*', None, "", None, None, []) # Insert a decoy from another file (should fail). entry2 = data.Transaction( data.new_metadata("/tmp/bananas.beancount", 100), datetime.date(2014, 9, 20), '*', None, "", None, None, []) entry3 = data.Transaction( data.new_metadata("/tmp/apples.beancount", 105), datetime.date(2014, 10, 1), '*', None, "", None, None, []) entries = [entry1, entry2, entry3] # Try an exact match. self.assertTrue( data.find_closest(entries, "/tmp/apples.beancount", 105) is entry3) # Try a bit after. self.assertTrue( data.find_closest(entries, "/tmp/apples.beancount", 107) is entry3) # Try way closer to the next one. self.assertTrue( data.find_closest(entries, "/tmp/apples.beancount", 199) is entry3) # Get the next one. self.assertTrue( data.find_closest(entries, "/tmp/apples.beancount", 201) is entry1) # Get from the other file. self.assertTrue( data.find_closest(entries, "/tmp/bananas.beancount", 300) is entry2 ) # Get none. self.assertTrue( data.find_closest(entries, "/tmp/apples.beancount", 99) is None)
def do_linked(filename, args): """Print out a list of transactions linked to the one at the given line. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. """ from beancount.parser import options from beancount.parser import printer from beancount.core import account_types from beancount.core import inventory from beancount.core import data from beancount.core import realization from beancount import loader # Parse the arguments, get the line number. if len(args) != 1: raise SystemExit("Missing line number argument.") lineno = int(args[0]) # Load the input file. entries, errors, options_map = loader.load_file(filename) # Find the closest entry. closest_entry = data.find_closest(entries, options_map['filename'], lineno) # Find its links. if closest_entry is None: raise SystemExit("No entry could be found before {}:{}".format( filename, lineno)) links = (closest_entry.links if isinstance(closest_entry, data.Transaction) else data.EMPTY_SET) if not links: linked_entries = [closest_entry] else: # Find all linked entries. # # Note that there is an option here: You can either just look at the links # on the closest entry, or you can include the links of the linked # transactions as well. Whichever one you want depends on how you use your # links. Best would be to query the user (in Emacs) when there are many # links present. follow_links = True if not follow_links: linked_entries = [ entry for entry in entries if (isinstance(entry, data.Transaction) and entry.links and entry.links & links) ] else: links = set(links) linked_entries = [] while True: num_linked = len(linked_entries) linked_entries = [ entry for entry in entries if (isinstance(entry, data.Transaction) and entry.links and entry.links & links) ] if len(linked_entries) == num_linked: break for entry in linked_entries: if entry.links: links.update(entry.links) # Render linked entries (in date order) as errors (for Emacs). errors = [RenderError(entry.meta, '', entry) for entry in linked_entries] printer.print_errors(errors) # Print out balances. real_root = realization.realize(linked_entries) dformat = options_map['dcontext'].build( alignment=display_context.Align.DOT, reserved=2) realization.dump_balances(real_root, dformat, file=sys.stdout) # Print out net income change. acctypes = options.get_account_types(options_map) net_income = inventory.Inventory() for real_node in realization.iter_children(real_root): if account_types.is_income_statement_account(real_node.account, acctypes): net_income.add_inventory(real_node.balance) print() print('Net Income: {}'.format(-net_income))
def linked(filename, location_spec): """List related transactions. List all transaction in ledger FILENAME linked to LINK or tagged with TAG, or linked to the one at LOCATION, or linked to any transaction in REGION. The LINK and TAG arguments must include the leading ^ or # charaters. The LOCATION argument is either a line number or a filename:lineno tuple to indicate a location in a ledger file included from the main input file. The REGION argument is either a stard:end line numbers tuple or a filename:start:end triplet to indicate a region in a ledger file included from the main input file. """ # Load the input file. entries, errors, options_map = loader.load_file(filename) # Link name. if re.match(r"\^(.*)$", location_spec): search_filename = options_map['filename'] links = {location_spec[1:]} linked_entries = find_linked_entries(entries, links, False) # Tag name. elif re.match(r"#(.*)$", location_spec): search_filename = options_map['filename'] tag = location_spec[1:] linked_entries = find_tagged_entries(entries, tag) else: # Parse the argument as a line number or a # "<filename>:<lineno>:<lineno>" spec to pull context from, with # optional filename and optional last line number. # # If a filename is not provided, the ledger's top-level filename is used # (this is the common case). An explicit filename is used to get context # in included files. # # If a single line number is provided the closest transaction is # selected. If an internal of line numbers is provided, the list of all # transactions whose first line is inside the interval are selected. match = re.match(r"(\d+)(?::(\d+))?$", location_spec) if match: included_filename = None first_line, last_line = match.groups() else: match = re.match(r"(.+?):(\d+)(?::(\d+))?$", location_spec) if match: included_filename, first_line, last_line = match.groups() else: raise SystemExit("Invalid line number or link format for location.") search_filename = (os.path.abspath(included_filename) if included_filename else options_map['filename']) lineno = int(first_line) if last_line is None: # Find the closest entry. closest_entry = data.find_closest(entries, search_filename, lineno) selected_entries = [closest_entry] # Find its links. if closest_entry is None: raise SystemExit("No entry could be found before {}:{}".format( search_filename, lineno)) links = (closest_entry.links if isinstance(closest_entry, data.Transaction) else data.EMPTY_SET) else: # Find all the entries in the interval, following all links. last_lineno = int(last_line) links = set() selected_entries = [] for entry in data.filter_txns(entries): if (entry.meta['filename'] == search_filename and lineno <= entry.meta['lineno'] <= last_lineno): links.update(entry.links) selected_entries.append(entry) # Get the linked entries, or just the closest one, if no links. linked_entries = (find_linked_entries(entries, links, True) if links else selected_entries) render_mini_balances(linked_entries, options_map, None)
def do_linked(filename, args): """Print out a list of transactions linked to the one at the given line. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be a string which contains either a lineno integer or a filename:lineno combination (which can be used if the location is not in the top-level file). """ from beancount.parser import options from beancount.parser import printer from beancount.core import account_types from beancount.core import inventory from beancount.core import data from beancount.core import realization from beancount import loader # Parse the arguments, get the line number. if len(args) != 1: raise SystemExit("Missing line number or link argument.") location_spec = args[0] # Load the input file. entries, errors, options_map = loader.load_file(filename) # Accept an explicit link name as the location. Must include the '^' # character. if re.match(r"\^(.*)$", location_spec): search_filename = options_map['filename'] links = {location_spec[1:]} linked_entries = find_linked_entries(entries, links, False) else: # Parse the argument as a line number or a "<filename>:<lineno>" spec to # pull context from. match = re.match(r"(.+):(\d+)$", location_spec) if match: search_filename = path.abspath(match.group(1)) lineno = int(match.group(2)) elif re.match(r"(\d+)$", location_spec): # Parse the argument as just a line number to pull context from on # the main filename. search_filename = options_map['filename'] lineno = int(location_spec) else: raise SystemExit( "Invalid line number or link format for location.") # Find the closest entry. closest_entry = data.find_closest(entries, search_filename, lineno) # Find its links. if closest_entry is None: raise SystemExit("No entry could be found before {}:{}".format( search_filename, lineno)) links = (closest_entry.links if isinstance( closest_entry, data.Transaction) else data.EMPTY_SET) # Get the linked entries, or just the closest one, if no links. linked_entries = (find_linked_entries(entries, links, True) if links else [closest_entry]) # Render linked entries (in date order) as errors (for Emacs). errors = [RenderError(entry.meta, '', entry) for entry in linked_entries] printer.print_errors(errors) # Print out balances. real_root = realization.realize(linked_entries) dformat = options_map['dcontext'].build( alignment=display_context.Align.DOT, reserved=2) realization.dump_balances(real_root, dformat, file=sys.stdout) # Print out net income change. acctypes = options.get_account_types(options_map) net_income = inventory.Inventory() for real_node in realization.iter_children(real_root): if account_types.is_income_statement_account(real_node.account, acctypes): net_income.add_inventory(real_node.balance) print() print('Net Income: {}'.format(-net_income))
def do_linked(filename, args): """Print out a list of transactions linked to the one at the given line. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be a string which contains either a lineno integer or a (filename:)?lineno(:lineno)? combination (which can be used if the location is not in the top-level file). """ # Parse the arguments, get the line number. if len(args) != 1: raise SystemExit("Missing line number or link argument.") location_spec = args[0] # Load the input file. entries, errors, options_map = loader.load_file(filename) # Accept an explicit link name as the location. Must include the '^' # character. if re.match(r"\^(.*)$", location_spec): search_filename = options_map['filename'] links = {location_spec[1:]} linked_entries = find_linked_entries(entries, links, False) elif re.match(r"#(.*)$", location_spec): search_filename = options_map['filename'] tag = location_spec[1:] linked_entries = find_tagged_entries(entries, tag) else: # Parse the argument as a line number or a # "<filename>:<lineno>:<lineno>" spec to pull context from, with # optional filename and optional last line number. # # If a filename is not provided, the ledger's top-level filename is used # (this is the common case). An explicit filename is used to get context # in included files. # # If a single line number is provided the closest transaction is # selected. If an internal of line numbers is provided, the list of all # transactions whose first line is inside the interval are selected. match = re.match(r"(\d+)(?::(\d+))?$", location_spec) if match: included_filename = None first_line, last_line = match.groups() else: match = re.match(r"(.+?):(\d+)(?::(\d+))?$", location_spec) if match: included_filename, first_line, last_line = match.groups() else: raise SystemExit( "Invalid line number or link format for location.") search_filename = (path.abspath(included_filename) if included_filename else options_map['filename']) lineno = int(first_line) if last_line is None: # Find the closest entry. closest_entry = data.find_closest(entries, search_filename, lineno) # Find its links. if closest_entry is None: raise SystemExit("No entry could be found before {}:{}".format( search_filename, lineno)) links = (closest_entry.links if isinstance( closest_entry, data.Transaction) else data.EMPTY_SET) else: # Find all the entries in the interval, following all links. last_lineno = int(last_line) links = set() for entry in data.filter_txns(entries): if (entry.meta['filename'] == search_filename and lineno <= entry.meta['lineno'] <= last_lineno): links.update(entry.links) # Get the linked entries, or just the closest one, if no links. linked_entries = (find_linked_entries(entries, links, True) if links else [closest_entry]) render_mini_balances(linked_entries, options_map, None)