beancount-parser icon indicating copy to clipboard operation
beancount-parser copied to clipboard

Hiccup with pushtag

Open aisamu opened this issue 8 months ago • 3 comments

Hi!

I'm running into a hiccup trying to import transactions into a project that contains an unrelated file that uses push/poptags directives.

If we define a Beancount file with `pushtag` and `poptag` directives:

pushtag #test

2000-01-01 open Assets:Test
2000-01-01 open Expenses:Test

2001-01-01 * "Test"
  Expenses:Test  1 USD
  Assets:Test

poptag #test

And provide a placeholder configuration:

inputs: []
imports: []

Running bh import fails with a parsing error:

bh import -c empty-config.yaml -b document-with-pushtag.beancount 2>&1
[22:22:59] INFO     Loaded import doc from config.yaml          import_cli.py:72
           INFO     Generated 0 transactions                   import_cli.py:109
           INFO     Deleted 0 transactions                     import_cli.py:110
           INFO     Skipped 0 transactions                     import_cli.py:111
           INFO     Collecting existing imported transactions  import_cli.py:122
                    from Beancount books ...
Traceback (most recent call last):
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/lexer.py", line 665, in lex
    yield lexer.next_token(lexer_state, parser_state)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/lexer.py", line 598, in next_token
    raise UnexpectedCharacters(lex_state.text, line_ctr.char_pos, line_ctr.line, line_ctr.column,
lark.exceptions.UnexpectedCharacters: No terminal matches '#' in the current parser context, at line 1 col 9

pushtag #test
        ^
Expected one of:
	* COLON

Previous tokens: Token('METADATA_KEY', 'pushtag')


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/nix/store/49ks2fr4p22hddm76cjfky1hgy8qfb7c-python3.12-beanhub-cli-2.1.1/bin/.bh-wrapped", line 9, in <module>
    sys.exit(cli())
             ^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 1161, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 1082, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 1697, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 1443, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 788, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/decorators.py", line 92, in new_func
  return ctx.invoke(f, obj, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 788, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/sq347ia5a61cpc6jildf6w93f853ikad-devenv-profile/lib/python3.12/site-packages/beanhub_cli/import_cli.py", line 126, in main
    existing_txns = list(
                    ^^^^^
  File "/nix/store/psjfv2mma7my7lfb0bhchh0pcljwgvbw-python3.12-beanhub-import-1.2.0/lib/python3.12/site-packages/beanhub_import/post_processor.py", line 52, in extract_existing_transactions
    for bean_path, tree in traverse(
                           ^^^^^^^^^
  File "/nix/store/4afpjpmfa6bjvihyhbbjlnl0r7b6j6a0-python3.12-beancount-parser-1.2.3/lib/python3.12/site-packages/beancount_parser/parser.py", line 54, in traverse
    tree = parser.parse(current_file.read_text())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/lark.py", line 655, in parse
    return self.parser.parse(text, start=start, on_error=on_error)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/parser_frontends.py", line 104, in parse
    return self.parser.parse(stream, chosen_start, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/parsers/lalr_parser.py", line 42, in parse
    return self.parser.parse(lexer, start)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/parsers/lalr_parser.py", line 88, in parse
    return self.parse_from_state(parser_state)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/parsers/lalr_parser.py", line 111, in parse_from_state
    raise e
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/parsers/lalr_parser.py", line 100, in parse_from_state
    for token in state.lexer.lex(state):
                 ^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/lexer.py", line 674, in lex
    raise UnexpectedToken(token, e.allowed, state=parser_state, token_history=[last_token], terminals_by_name=self.root_lexer.terminals_by_name)
lark.exceptions.UnexpectedToken: Unexpected token Token('TAGS', '#test') at line 1, column 9.
Expected one of:
	* COLON
Previous tokens: [Token('METADATA_KEY', 'pushtag')]

Using a hash-less tag fails in a similar way:

pushtag test

2000-01-01 open Assets:Test
2000-01-01 open Expenses:Test

2001-01-01 * "Test"
  Expenses:Test  1 USD
  Assets:Test

poptag test
bh import -c empty-config.yaml -b document-with-hashless-pushtag.beancount 2>&1
[22:35:55] INFO     Loaded import doc from empty-config.yaml    import_cli.py:72
           INFO     Generated 0 transactions                   import_cli.py:109
           INFO     Deleted 0 transactions                     import_cli.py:110
           INFO     Skipped 0 transactions                     import_cli.py:111
           INFO     Collecting existing imported transactions  import_cli.py:122
                    from Beancount books ...
Traceback (most recent call last):
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/lexer.py", line 665, in lex
    yield lexer.next_token(lexer_state, parser_state)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/lexer.py", line 598, in next_token
    raise UnexpectedCharacters(lex_state.text, line_ctr.char_pos, line_ctr.line, line_ctr.column,
lark.exceptions.UnexpectedCharacters: No terminal matches 't' in the current parser context, at line 1 col 9

pushtag test
        ^
Expected one of:
	* COLON

Previous tokens: Token('METADATA_KEY', 'pushtag')


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/nix/store/49ks2fr4p22hddm76cjfky1hgy8qfb7c-python3.12-beanhub-cli-2.1.1/bin/.bh-wrapped", line 9, in <module>
    sys.exit(cli())
             ^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 1161, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 1082, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 1697, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 1443, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 788, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/decorators.py", line 92, in new_func
    return ctx.invoke(f, obj, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/q43f205zpdgrkl60ihwzinvzccbyzyig-python3.12-click-8.1.8/lib/python3.12/site-packages/click/core.py", line 788, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/sq347ia5a61cpc6jildf6w93f853ikad-devenv-profile/lib/python3.12/site-packages/beanhub_cli/import_cli.py", line 126, in main
    existing_txns = list(
                    ^^^^^
  File "/nix/store/psjfv2mma7my7lfb0bhchh0pcljwgvbw-python3.12-beanhub-import-1.2.0/lib/python3.12/site-packages/beanhub_import/post_processor.py", line 52, in extract_existing_transactions
    for bean_path, tree in traverse(
                           ^^^^^^^^^
  File "/nix/store/4afpjpmfa6bjvihyhbbjlnl0r7b6j6a0-python3.12-beancount-parser-1.2.3/lib/python3.12/site-packages/beancount_parser/parser.py", line 54, in traverse
    tree = parser.parse(current_file.read_text())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/lark.py", line 655, in parse
    return self.parser.parse(text, start=start, on_error=on_error)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/parser_frontends.py", line 104, in parse
    return self.parser.parse(stream, chosen_start, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/parsers/lalr_parser.py", line 42, in parse
    return self.parser.parse(lexer, start)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/parsers/lalr_parser.py", line 88, in parse
    return self.parse_from_state(parser_state)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/parsers/lalr_parser.py", line 111, in parse_from_state
    raise e
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/parsers/lalr_parser.py", line 100, in parse_from_state
    for token in state.lexer.lex(state):
                 ^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/qldafmcpnlj5f6z6cmlf5bc2g0hc0kix-python3.12-lark-1.2.2/lib/python3.12/site-packages/lark/lexer.py", line 674, in lex
    raise UnexpectedToken(token, e.allowed, state=parser_state, token_history=[last_token], terminals_by_name=self.root_lexer.terminals_by_name)
lark.exceptions.UnexpectedToken: Unexpected token Token('METADATA_KEY', 'test') at line 1, column 9.
Expected one of:
	* COLON
Previous tokens: [Token('METADATA_KEY', 'pushtag')]

Is this expected?

Side-note: it'd be very useful to have a file-name on the error output!

Thanks for this project!

aisamu avatar May 23 '25 01:05 aisamu

Hi @aisamu, thanks for reporting the issue. Unfortunately we don't support push tag and have no plan to support in the short term. Because push tag makes sorting and formatting way more complex. We think it's a bit unnecessary given that if our users adopt tools like our beanhub-import, they can easily manage tags for the transactions they want.

fangpenlin avatar May 23 '25 05:05 fangpenlin

Noted!

Just to reinforce, I ran into this while trying to migrate my existing ledgers to beanhub-import! I'm more than happy to have something handle tags other than myself.

Do you have a suggestion on how to handle existing/old files? Manual conversion would be a bit tricky given the volume, and excluding them (e.g. via a separate safe-main.beancount) would potentially make things like duplicate detection fail for those accounts/files, right?

aisamu avatar May 23 '25 13:05 aisamu

Currently, there's no easy way provided to convert your legacy Beancount file with pushtag syntax to not using pushtax instead. But the beancount-parser for parsing the syntax into syntax tree + beancount-black for formatting syntax tree back into beancount file can help.

In beanhub-cli, we provided account renaming feature along with the format subcommand:

https://beanhub-cli-docs.beanhub.io/commands/format/

We actually implemented a transformer to transform tne syntax tree for renaming account or currency:

https://github.com/LaunchPlatform/beanhub-cli/blob/f9a97e92bcbdb7e3ff7fc51bbcee9657503edf34/beanhub_cli/format.py#L75-L86

I think it's possible to create a transformer to inject tags into your new beancount file. I would envision write a simple script to record the line numbers of push tag and the tags to push. Then, write it to a JSON file or something like that one the side. Then, with a custom transformer, read your beancount file after commenting all the push tag syntax and see if the entry are between the line numbers you have recorded. If so, inject tag into the syntax tree and let beancount-black serialize them back into a new beancount file.

Sorry I know this is less than idea. But I am thinking a better way to do it. Another approach might be asking a LLM model to do the changes for you.

fangpenlin avatar May 24 '25 05:05 fangpenlin