Opinionated code formatter, just like Python's black code formatter but for Beancount

Overview

beancount-black CircleCI

Opinionated code formatter, just like Python's black code formatter but for Beancount

Try it out online here

Features

  • MIT licensed - based on beancount-parser, a Lark based LALR(1) Beancount syntax parser
  • Extremely fast - 5K+ lines file generated by bean-example can be formatted in around 1 second
  • Section awareness - entries separated by Emac org symbol mark * will be formatted in groups without changing the overall structure
  • Comment preserving - comments are preserved and will be formatted as well
  • Auto column width - calculate maximum column width and adjust accordingly
  • Valid beancount file assumed - please notice that the formatter assumes the given beacnount file is valid, it doesn't not perform any kind of validation

Sponsor

The original project beancount-black was meant to be an internal tool built by Launch Platform LLC for

BeanHub logo

A modern accounting book service based on the most popular open source version control system Git and text-based double entry accounting book software Beancount. We realized adding new entries with BeanHub automatically over time makes beancount file a mess. So obviously, a strong code formatter is needed. While SaaS businesses won't be required to open source an internal tool like this, we still love that the service is only possible because of the open-source tool we are using. We think it would be greatly beneficial for the community to access a tool like this, so we've decided to open source it under MIT license, hope you find this tool useful 😄

Install

To install the formatter, simply run

pip install beancount-black

Usage

Run

bean-black /path/to/file.bean

Then the file will be formatted. Since this tool is still in its early stage, a backup file at <filepath>.backup will be created automatically by default just in case. The creation of backup files can be disabled by passing -n or --no-backup like this

bean-black -n /path/to/file.bean

It's highly recommended to use BeanHub, Git or other version control system to track your Beancount book files before running the formatter against them without a backup.

If you want to run the formatter programmatically, you can do this

import io

from beancount_parser.parser import make_parser
from beancount_black.formatter import Formatter

parser = make_parser()
formatter = Formatter()

tree = parser.parse(beancount_content)
output_file = io.StringIO()
formatter.format(tree, output_file)

Future features

  • Add argument for renaming account and commodity
  • Add argument for following other files from include statements and also format those files

Feedbacks, bugs reporting or feature requests are welcome 🙌 , just please open an issue. No guarantee we have time to deal with them, but will see what we can do.

Comments
  • space arounds { }

    space arounds { }

    bean-black made this change:

    -  Assets:Cash                    1 FOO {10.00 EUR, 2011-12-18}
    +  Assets:Cash                                                1 FOO { 10.00 EUR , 2011-12-18 }
    

    Can we not have spaces around {...}. IMHO {10.00 EUR , 2011-12-18} is better.

    But maybe I just don't agree with bean-black's opiniated opinion and we have to agree to disagree.

    Test case:

    2000-01-01 open Assets:Cash
    2000-01-01 open Equity:Opening-Balance
    
    2022-01-01 * "Opening balance: cash"
      Assets:Cash                    1 FOO {10.00 EUR, 2011-12-18}
      Equity:Opening-Balance
    
    opened by tbm 2
  • Don't add space before comma

    Don't add space before comma

    bean-black made this change:

    -  Assets:Cash                    1 FOO {10.00 EUR, 2011-12-18}
    +  Assets:Cash                                                1 FOO { 10.00 EUR , 2011-12-18 }
    

    Note the space before the comma ("EUR , 2011").

    bean-black may be opinionated but this is plain wrong.

    opened by tbm 2
  • Difficulty with debugging

    Difficulty with debugging

    I have the following error.

    I am not able to determine what the source of error is

    INFO:beancount_black.main:Processing file ledger.bean
    Traceback (most recent call last):
      File "/home/user/.local/bin/bean-black", line 8, in <module>
        sys.exit(main())
      File "/home/user/.local/lib/python3.9/site-packages/click/core.py", line 1130, in __call__
        return self.main(*args, **kwargs)
      File "/home/user/.local/lib/python3.9/site-packages/click/core.py", line 1055, in main
        rv = self.invoke(ctx)
      File "/home/user/.local/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
        return ctx.invoke(self.callback, **ctx.params)
      File "/home/user/.local/lib/python3.9/site-packages/click/core.py", line 760, in invoke
        return __callback(*args, **kwargs)
      File "/home/user/.local/lib/python3.9/site-packages/beancount_black/main.py", line 49, in main
        formatter.format(tree, output_file)
      File "/home/user/.local/lib/python3.9/site-packages/beancount_black/formatter.py", line 558, in format
        self.calculate_column_widths(tree)
      File "/home/user/.local/lib/python3.9/site-packages/beancount_black/formatter.py", line 550, in calculate_column_widths
        width = len(self.format_number(amount.children[0]))
      File "/home/user/.local/lib/python3.9/site-packages/beancount_black/formatter.py", line 210, in format_number
        value = token.value.replace(",", "")
    AttributeError: 'Tree' object has no attribute 'value'
    
    opened by alexhallam 0
  • Bump beancount-parser version

    Bump beancount-parser version

    beancount-parser had been bumped to version 0.1.23 (https://github.com/LaunchPlatform/beancount-parser/commit/c9ec3ba02c00266d549fa830b9ca1bd4bc901b0d), resolving some parsing failures. It would be nice to bump the beancount-black version as well.

    opened by Soptq 0
  • Failure when encountering links on a new line

    Failure when encountering links on a new line

    When trying to format a transaction containing a link on the line after the narration, beancount-black throws an exception.

    Example file:

    2022-08-01 "Payee" "Narration"
      ^reference-XXX
      Assets:Checking     -12345.67 EUR
      Expenses:Other
    

    Exception:

    $ bean-black --no-backup example.bean
    INFO:beancount_black.main:Processing file example.bean
    Traceback (most recent call last):
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\lark\lexer.py", line 528, in lex
        yield lexer.next_token(lexer_state, parser_state)
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\lark\lexer.py", line 466, 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 2 col 3
    
      ^reference-XXX
      ^
    Expected one of:
            * ACCOUNT
            * _NL
            * METADATA_KEY
            * COMMENT
            * SECTION_HEADER
            * DATE
            * FLAG
    
    Previous tokens: Token('_NL', '\n')
    
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "c:\program files\python39\lib\runpy.py", line 197, in _run_module_as_main
        return _run_code(code, main_globals, None,
      File "c:\program files\python39\lib\runpy.py", line 87, in _run_code
        exec(code, run_globals)
      File "c:\users\markus\.local\bin\bean-black.exe\__main__.py", line 7, in <module>
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\click\core.py", line 1130, in __call__
        return self.main(*args, **kwargs)
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\click\core.py", line 1055, in main
        rv = self.invoke(ctx)
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\click\core.py", line 1404, in invoke
        return ctx.invoke(self.callback, **ctx.params)
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\click\core.py", line 760, in invoke
        return __callback(*args, **kwargs)
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\beancount_black\main.py", line 47, in main
        tree = parser.parse(input_content)
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\lark\lark.py", line 625, in parse
        return self.parser.parse(text, start=start, on_error=on_error)
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\lark\parser_frontends.py", line 96, in parse
        return self.parser.parse(stream, chosen_start, **kw)
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\lark\parsers\lalr_parser.py", line 41, in parse
        return self.parser.parse(lexer, start)
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\lark\parsers\lalr_parser.py", line 171, in parse
        return self.parse_from_state(parser_state)
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\lark\parsers\lalr_parser.py", line 188, in parse_from_state
        raise e
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\lark\parsers\lalr_parser.py", line 178, in parse_from_state
        for token in state.lexer.lex(state):
      File "C:\Users\Markus\.local\pipx\venvs\beancount\lib\site-packages\lark\lexer.py", line 537, 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('LINK', '^reference-XXX') at line 2, column 3.
    Expected one of:
            * ACCOUNT
            * $END
            * INCLUDE
            * PLUGIN
            * _NL
            * METADATA_KEY
            * COMMENT
            * OPTION
            * SECTION_HEADER
            * DATE
            * FLAG
    Previous tokens: [Token('_NL', '\n')]
    
    
    
    opened by korrat 1
  • bean-black fails to parse expressions in a transaction amount

    bean-black fails to parse expressions in a transaction amount

    Program errors when a transaction contains an expression for the amount.

    Example transaction:

    2022-07-25 * "Food Place" "Meal for Two"
      Expenses:Food:Restaurants              (38.00 / 2) EUR
      Assets:Receivable                      (38.00 / 2) EUR
      Assets:Current:Cash
    

    Output:

    ❯ bean-check start.beancount
    ❯ bean-black start.beancount
    INFO:beancount_black.main:Processing file start.beancount
    Traceback (most recent call last):
      File "/home/user/.local/lib/python3.10/site-packages/lark/lexer.py", line 536, in lex
        token = self.root_lexer.next_token(lexer_state, parser_state)
      File "/home/user/.local/lib/python3.10/site-packages/lark/lexer.py", line 466, 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 236 col 42
    
     Expenses:Food:Restaurants              (38.00 / 2) EUR
                                            ^
    Expected one of: 
    	* FLAG
    	* COMMA
    	* METADATA_KEY
    	* RBRACE
    	* COMMENT
    	* BOOLEAN
    	* "@@"
    	* ACCOUNT
    	* DATE
    	* _NL
    	* SECTION_HEADER
    	* "{{"
    	* LBRACE
    	* ESCAPED_STRING
    	* SIGNED_NUMBER
    	* "}}"
    	* COLON
    	* AT
    	* HASH
    	* LINK
    	* CURRENCY
    	* TAG
    	* _EMPTY_LINE
    	* TAGS
    
    Previous tokens: Token('ACCOUNT', 'Expenses:Food:Restaurants')
    
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "/home/user/.local/bin/bean-black", line 8, in <module>
        sys.exit(main())
      File "/usr/lib/python3.10/site-packages/click/core.py", line 1130, in __call__
        return self.main(*args, **kwargs)
      File "/usr/lib/python3.10/site-packages/click/core.py", line 1055, in main
        rv = self.invoke(ctx)
      File "/usr/lib/python3.10/site-packages/click/core.py", line 1404, in invoke
        return ctx.invoke(self.callback, **ctx.params)
      File "/usr/lib/python3.10/site-packages/click/core.py", line 760, in invoke
        return __callback(*args, **kwargs)
      File "/home/user/.local/lib/python3.10/site-packages/beancount_black/main.py", line 47, in main
        tree = parser.parse(input_content)
      File "/home/user/.local/lib/python3.10/site-packages/lark/lark.py", line 625, in parse
        return self.parser.parse(text, start=start, on_error=on_error)
      File "/home/user/.local/lib/python3.10/site-packages/lark/parser_frontends.py", line 96, in parse
        return self.parser.parse(stream, chosen_start, **kw)
      File "/home/user/.local/lib/python3.10/site-packages/lark/parsers/lalr_parser.py", line 41, in parse
        return self.parser.parse(lexer, start)
      File "/home/user/.local/lib/python3.10/site-packages/lark/parsers/lalr_parser.py", line 171, in parse
        return self.parse_from_state(parser_state)
      File "/home/user/.local/lib/python3.10/site-packages/lark/parsers/lalr_parser.py", line 188, in parse_from_state
        raise e
      File "/home/user/.local/lib/python3.10/site-packages/lark/parsers/lalr_parser.py", line 178, in parse_from_state
        for token in state.lexer.lex(state):
      File "/home/user/.local/lib/python3.10/site-packages/lark/lexer.py", line 539, in lex
        raise e  # Raise the original UnexpectedCharacters. The root lexer raises it with the wrong expected set.
      File "/home/user/.local/lib/python3.10/site-packages/lark/lexer.py", line 528, in lex
        yield lexer.next_token(lexer_state, parser_state)
      File "/home/user/.local/lib/python3.10/site-packages/lark/lexer.py", line 466, 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 236 col 42
    
     Expenses:Food:Restaurants              (38.00 / 2) EUR
                                            ^
    Expected one of: 
    	* SIGNED_NUMBER
    	* _NL
    	* COMMENT
    
    Previous tokens: Token('ACCOUNT', 'Expenses:Food:Restaurants')
    
    

    Similar result when removing the parenthesis - then program does not recognize / symbol.

    opened by 0x0013 0
  • Thrown UnicodeEncodeError in non-English language windows environment

    Thrown UnicodeEncodeError in non-English language windows environment

    When bean file contains special characters that are beyond the scope of ASCII,running script thrown error as below:

    >bean-black xxx.bean
    Traceback (most recent call last):
      File "C:\Users\python\current\lib\runpy.py", line 196, in _run_module_as_main
        return _run_code(code, main_globals, None,
      File "C:\Users\python\current\lib\runpy.py", line 86, in _run_code
        exec(code, run_globals)
      File "C:\Users\python\current\Scripts\bean-black.exe\__main__.py", line 7, in <module>
      File "C:\Users\python\current\lib\site-packages\click\core.py", line 1130, in __call__
        return self.main(*args, **kwargs)
      File "C:\Users\python\current\lib\site-packages\click\core.py", line 1055, in main
        rv = self.invoke(ctx)
      File "C:\Users\python\current\lib\site-packages\click\core.py", line 1404, in invoke
        return ctx.invoke(self.callback, **ctx.params)
      File "C:\Users\python\current\lib\site-packages\click\core.py", line 760, in invoke
        return __callback(*args, **kwargs)
      File "C:\Users\python\current\lib\site-packages\beancount_black\main.py", line 49, in main
        formatter.format(tree, output_file)
      File "C:\Users\python\current\lib\site-packages\beancount_black\formatter.py", line 576, in format
        output_file.write("\n\n".join(sections))
      File "C:\Users\python\current\lib\tempfile.py", line 483, in func_wrapper
        return func(*args, **kwargs)
    UnicodeEncodeError: 'gbk' codec can't encode character '\xa5' in position 211620: illegal multibyte sequence
    

    But the file is saved in utf-8 encoding. Then I try to write simple script that use the open function to write, it can be saved normally.

    opened by huruka 0
  • `Token` object has no attribute `data`

    `Token` object has no attribute `data`

    Wrongfully reported as LaunchPlatform/beancount-parser#6.

    I get the following traceback on my ledger:

    Traceback (most recent call last):
      File "/nix/store/lzm05hhhr0ai8zg3d2a0m221rjl8fdsp-python3.9-beancount-black-0.1.9/bin/.bean-black-wrapped", line 9, in <module>
        sys.exit(main())
      File "/nix/store/p3gl9nwd2m0f9sixss1nw5q2pdzmvn6h-python3.8-click-7.1.2/lib/python3.8/site-packages/click/core.py", line 829, in __call__
        return self.main(*args, **kwargs)
      File "/nix/store/p3gl9nwd2m0f9sixss1nw5q2pdzmvn6h-python3.8-click-7.1.2/lib/python3.8/site-packages/click/core.py", line 782, in main
        rv = self.invoke(ctx)
      File "/nix/store/p3gl9nwd2m0f9sixss1nw5q2pdzmvn6h-python3.8-click-7.1.2/lib/python3.8/site-packages/click/core.py", line 1066, in invoke
        return ctx.invoke(self.callback, **ctx.params)
      File "/nix/store/p3gl9nwd2m0f9sixss1nw5q2pdzmvn6h-python3.8-click-7.1.2/lib/python3.8/site-packages/click/core.py", line 610, in invoke
        return callback(*args, **kwargs)
      File "/nix/store/lzm05hhhr0ai8zg3d2a0m221rjl8fdsp-python3.9-beancount-black-0.1.9/lib/python3.9/site-packages/beancount_black/main.py", line 49, in main
        formatter.format(tree, output_file)
      File "/nix/store/lzm05hhhr0ai8zg3d2a0m221rjl8fdsp-python3.9-beancount-black-0.1.9/lib/python3.9/site-packages/beancount_black/formatter.py", line 573, in format
        sections.append(self.format_statement_group(group))
      File "/nix/store/lzm05hhhr0ai8zg3d2a0m221rjl8fdsp-python3.9-beancount-black-0.1.9/lib/python3.9/site-packages/beancount_black/formatter.py", line 521, in format_statement_group
        remain_entries.sort(key=self.get_entry_sorting_key)
      File "/nix/store/lzm05hhhr0ai8zg3d2a0m221rjl8fdsp-python3.9-beancount-black-0.1.9/lib/python3.9/site-packages/beancount_black/formatter.py", line 187, in get_entry_sorting_key
        if first_child.data == "date_directive":
    AttributeError: 'Token' object has no attribute 'data'
    

    I have not yet tried to minimise the ledger to find the problem.

    opened by ambroisie 0
Owner
Launch Platform
We build & launch innovative software products
Launch Platform
Black-Box-Tuning - Black-Box Tuning for Language-Model-as-a-Service

Black-Box-Tuning Source code for paper "Black-Box Tuning for Language-Model-as-a

Tianxiang Sun 149 Jan 4, 2023
Just-Now - This Is Just Now Login Friendlist Cloner Tools

JUST NOW LOGIN FRIENDLIST CLONER TOOLS Install $ apt update $ apt upgrade $ apt

MAHADI HASAN AFRIDI 21 Mar 9, 2022
Like a cowsay but without cows!

Foxsay This is a simple program that generates pictures of a cute fox with a message. It is like a cowsay but without cows! Fox girls are better! Usag

Anastasia Kim 28 Feb 20, 2022
Like Dirt-Samples, but cleaned up

Clean-Samples Like Dirt-Samples, but cleaned up, with clear provenance and license info (generally a permissive creative commons licence but check the

TidalCycles 39 Nov 30, 2022
Like ThreeJS but for Python and based on wgpu

pygfx A render engine, inspired by ThreeJS, but for Python and targeting Vulkan/Metal/DX12 (via wgpu). Introduction This is a Python render engine bui

null 139 Jan 7, 2023
It's like Shape Editor in Maya but works with skeletons (transforms).

Skeleposer What is Skeleposer? Briefly, it's like Shape Editor in Maya, but works with transforms and joints. It can be used to make complex facial ri

Alexander Zagoruyko 1 Nov 11, 2022
Code for "Diversity can be Transferred: Output Diversification for White- and Black-box Attacks"

Output Diversified Sampling (ODS) This is the github repository for the NeurIPS 2020 paper "Diversity can be Transferred: Output Diversification for W

null 50 Dec 11, 2022
Code for "Retrieving Black-box Optimal Images from External Databases" (WSDM 2022)

Retrieving Black-box Optimal Images from External Databases (WSDM 2022) We propose how a user retreives an optimal image from external databases of we

joisino 5 Apr 13, 2022
This repository contains the code and models necessary to replicate the results of paper: How to Robustify Black-Box ML Models? A Zeroth-Order Optimization Perspective

Black-Box-Defense This repository contains the code and models necessary to replicate the results of our recent paper: How to Robustify Black-Box ML M

OPTML Group 2 Oct 5, 2022
This repository contains the code and models necessary to replicate the results of paper: How to Robustify Black-Box ML Models? A Zeroth-Order Optimization Perspective

Black-Box-Defense This repository contains the code and models necessary to replicate the results of our recent paper: How to Robustify Black-Box ML M

OPTML Group 2 Oct 5, 2022
Attack classification models with transferability, black-box attack; unrestricted adversarial attacks on imagenet

Attack classification models with transferability, black-box attack; unrestricted adversarial attacks on imagenet, CVPR2021 安全AI挑战者计划第六期:ImageNet无限制对抗攻击 决赛第四名(team name: Advers)

null 51 Dec 1, 2022
transfer attack; adversarial examples; black-box attack; unrestricted Adversarial Attacks on ImageNet; CVPR2021 天池黑盒竞赛

transfer_adv CVPR-2021 AIC-VI: unrestricted Adversarial Attacks on ImageNet CVPR2021 安全AI挑战者计划第六期赛道2:ImageNet无限制对抗攻击 介绍 : 深度神经网络已经在各种视觉识别问题上取得了最先进的性能。

null 25 Dec 8, 2022
[CVPR 2021] Pytorch implementation of Hijack-GAN: Unintended-Use of Pretrained, Black-Box GANs

Hijack-GAN: Unintended-Use of Pretrained, Black-Box GANs In this work, we propose a framework HijackGAN, which enables non-linear latent space travers

Hui-Po Wang 46 Sep 5, 2022
PyTorch implementation of Interpretable Explanations of Black Boxes by Meaningful Perturbation

PyTorch implementation of Interpretable Explanations of Black Boxes by Meaningful Perturbation The paper: https://arxiv.org/abs/1704.03296 What makes

Jacob Gildenblat 322 Dec 17, 2022
Explainer for black box models that predict molecule properties

Explaining why that molecule exmol is a package to explain black-box predictions of molecules. The package uses model agnostic explanations to help us

White Laboratory 172 Dec 19, 2022
A customisable game where you have to quickly click on black tiles in order of appearance while avoiding clicking on white squares.

W.I.P-Aim-Memory-Game A customisable game where you have to quickly click on black tiles in order of appearance while avoiding clicking on white squar

dE_soot 1 Dec 8, 2021
A method that utilized Generative Adversarial Network (GAN) to interpret the black-box deep image classifier models by PyTorch.

A method that utilized Generative Adversarial Network (GAN) to interpret the black-box deep image classifier models by PyTorch.

Yunxia Zhao 3 Dec 29, 2022
Ray tracing of a Schwarzschild black hole written entirely in TensorFlow.

TensorGeodesic Ray tracing of a Schwarzschild black hole written entirely in TensorFlow. Dependencies: Python 3 TensorFlow 2.x numpy matplotlib About

null 5 Jan 15, 2022