beancount-format 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- beancount_format-0.0.1/.gitignore +116 -0
- beancount_format-0.0.1/.pre-commit-config.yaml +41 -0
- beancount_format-0.0.1/.pre-commit-hooks.yaml +7 -0
- beancount_format-0.0.1/LICENSE +24 -0
- beancount_format-0.0.1/PKG-INFO +33 -0
- beancount_format-0.0.1/beancount_format/__init__.py +0 -0
- beancount_format-0.0.1/beancount_format/__main__.py +4 -0
- beancount_format-0.0.1/beancount_format/cli.py +48 -0
- beancount_format-0.0.1/beancount_format/format.py +664 -0
- beancount_format-0.0.1/pyproject.toml +128 -0
- beancount_format-0.0.1/readme.md +11 -0
- beancount_format-0.0.1/taskfile.yaml +25 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.task/
|
|
3
|
+
|
|
4
|
+
# Created by .ignore support plugin (hsz.mobi)
|
|
5
|
+
### Python template
|
|
6
|
+
# Byte-compiled / optimized / DLL files
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*$py.class
|
|
10
|
+
# C extensions
|
|
11
|
+
*.so
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
.Python
|
|
15
|
+
docs/source/_build/
|
|
16
|
+
docs/build/
|
|
17
|
+
build/
|
|
18
|
+
develop-eggs/
|
|
19
|
+
dist/
|
|
20
|
+
downloads/
|
|
21
|
+
eggs/
|
|
22
|
+
.eggs/
|
|
23
|
+
lib/
|
|
24
|
+
lib64/
|
|
25
|
+
parts/
|
|
26
|
+
sdist/
|
|
27
|
+
var/
|
|
28
|
+
wheels/
|
|
29
|
+
*.egg-info/
|
|
30
|
+
.installed.cfg
|
|
31
|
+
*.egg
|
|
32
|
+
MANIFEST
|
|
33
|
+
|
|
34
|
+
# PyInstaller
|
|
35
|
+
# Usually these files are written by a python script from a template
|
|
36
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
37
|
+
*.manifest
|
|
38
|
+
*.spec
|
|
39
|
+
|
|
40
|
+
# Installer logs
|
|
41
|
+
pip-log.txt
|
|
42
|
+
pip-delete-this-directory.txt
|
|
43
|
+
pip-wheel-metadata
|
|
44
|
+
|
|
45
|
+
# Unit test / coverage reports
|
|
46
|
+
htmlcov/
|
|
47
|
+
.tox/
|
|
48
|
+
.coverage
|
|
49
|
+
.coverage.*
|
|
50
|
+
.cache
|
|
51
|
+
nosetests.xml
|
|
52
|
+
coverage.xml
|
|
53
|
+
*.cover
|
|
54
|
+
.hypothesis/
|
|
55
|
+
.pytest_cache/
|
|
56
|
+
|
|
57
|
+
# Translations
|
|
58
|
+
*.mo
|
|
59
|
+
*.pot
|
|
60
|
+
|
|
61
|
+
# Django stuff:
|
|
62
|
+
*.log
|
|
63
|
+
local_settings.py
|
|
64
|
+
db.sqlite3
|
|
65
|
+
|
|
66
|
+
# Flask stuff:
|
|
67
|
+
instance/
|
|
68
|
+
.webassets-cache
|
|
69
|
+
|
|
70
|
+
# Scrapy stuff:
|
|
71
|
+
.scrapy
|
|
72
|
+
|
|
73
|
+
# Sphinx documentation
|
|
74
|
+
docs/_build/
|
|
75
|
+
|
|
76
|
+
# PyBuilder
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# pyenv
|
|
83
|
+
.python-version
|
|
84
|
+
|
|
85
|
+
# celery beat schedule file
|
|
86
|
+
celerybeat-schedule
|
|
87
|
+
|
|
88
|
+
# SageMath parsed files
|
|
89
|
+
*.sage.py
|
|
90
|
+
|
|
91
|
+
# Environments
|
|
92
|
+
.env
|
|
93
|
+
.venv
|
|
94
|
+
env/
|
|
95
|
+
venv/
|
|
96
|
+
ENV/
|
|
97
|
+
env.bak/
|
|
98
|
+
venv.bak/
|
|
99
|
+
|
|
100
|
+
# Spyder project settings
|
|
101
|
+
.spyderproject
|
|
102
|
+
.spyproject
|
|
103
|
+
|
|
104
|
+
# Rope project settings
|
|
105
|
+
.ropeproject
|
|
106
|
+
|
|
107
|
+
# mkdocs documentation
|
|
108
|
+
/site
|
|
109
|
+
|
|
110
|
+
# mypy
|
|
111
|
+
.mypy_cache/
|
|
112
|
+
|
|
113
|
+
# IDE
|
|
114
|
+
.vscode/
|
|
115
|
+
.idea/
|
|
116
|
+
main.py
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
default_stages: [commit]
|
|
2
|
+
|
|
3
|
+
repos:
|
|
4
|
+
- repo: https://github.com/abravalheri/validate-pyproject
|
|
5
|
+
rev: v0.19
|
|
6
|
+
hooks:
|
|
7
|
+
- id: validate-pyproject
|
|
8
|
+
# Optional extra validations from SchemaStore:
|
|
9
|
+
additional_dependencies: [ "validate-pyproject-schema-store[all]" ]
|
|
10
|
+
|
|
11
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
12
|
+
rev: v4.6.0
|
|
13
|
+
hooks:
|
|
14
|
+
- id: check-case-conflict
|
|
15
|
+
- id: check-ast
|
|
16
|
+
- id: check-builtin-literals
|
|
17
|
+
- id: check-toml
|
|
18
|
+
- id: check-yaml
|
|
19
|
+
- id: check-json
|
|
20
|
+
- id: check-docstring-first
|
|
21
|
+
- id: check-merge-conflict
|
|
22
|
+
- id: check-added-large-files # check for file bigger than 500kb
|
|
23
|
+
- id: debug-statements
|
|
24
|
+
- id: trailing-whitespace
|
|
25
|
+
- id: mixed-line-ending
|
|
26
|
+
args: [--fix=lf]
|
|
27
|
+
- id: end-of-file-fixer
|
|
28
|
+
- id: fix-byte-order-marker
|
|
29
|
+
- id: fix-encoding-pragma
|
|
30
|
+
args: [--remove]
|
|
31
|
+
|
|
32
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
33
|
+
rev: v0.6.9
|
|
34
|
+
hooks:
|
|
35
|
+
- id: ruff
|
|
36
|
+
args: [ --fix ]
|
|
37
|
+
|
|
38
|
+
- repo: https://github.com/psf/black
|
|
39
|
+
rev: 24.8.0
|
|
40
|
+
hooks:
|
|
41
|
+
- id: black
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Trim21 <trim21me@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person
|
|
6
|
+
obtaining a copy of this software and associated documentation
|
|
7
|
+
files (the "Software"), to deal in the Software without
|
|
8
|
+
restriction, including without limitation the rights to use,
|
|
9
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the
|
|
11
|
+
Software is furnished to do so, subject to the following
|
|
12
|
+
conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be
|
|
15
|
+
included in all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
19
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
21
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
22
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
23
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
24
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: beancount-format
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Typed rtorrent rpc client
|
|
5
|
+
Keywords: rtorrent,rpc
|
|
6
|
+
Author-email: trim21 <trim21me@gmail.com>
|
|
7
|
+
Requires-Python: ~=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Requires-Dist: beancount-parser==1.2.3
|
|
14
|
+
Requires-Dist: click~=8.0
|
|
15
|
+
Requires-Dist: pytest==8.3.2 ; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-github-actions-annotate-failures==0.2.0 ; extra == "dev"
|
|
17
|
+
Requires-Dist: coverage==7.6.1 ; extra == "dev"
|
|
18
|
+
Requires-Dist: pre-commit==3.8.0 ; extra == "dev" and ( python_version >= "3.9")
|
|
19
|
+
Requires-Dist: mypy==1.13.0 ; extra == "dev" and ( python_version >= "3.9")
|
|
20
|
+
Project-URL: Homepage, https://github.com/trim21/beancount-format
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
|
|
23
|
+
format beancount
|
|
24
|
+
|
|
25
|
+
as pre-commit hooks
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
repos:
|
|
29
|
+
- repo: https://github.com/trim21/beancount-format
|
|
30
|
+
rev: 801ab26
|
|
31
|
+
hooks:
|
|
32
|
+
- id: beancount-format
|
|
33
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import pathlib
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Generator, Sequence
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from beancount_parser.parser import make_parser
|
|
8
|
+
|
|
9
|
+
from beancount_format.format import Formatter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __input_files(paths: Sequence[pathlib.Path]) -> Generator[pathlib.Path, None, None]:
|
|
13
|
+
for p in paths:
|
|
14
|
+
if p.is_file():
|
|
15
|
+
yield p
|
|
16
|
+
else:
|
|
17
|
+
yield from __input_files(p.iterdir())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.command
|
|
21
|
+
@click.argument("path", nargs=-1, type=click.Path(path_type=pathlib.Path))
|
|
22
|
+
@click.option("--indent", default=4, type=int)
|
|
23
|
+
def main(path, indent: int):
|
|
24
|
+
parser = make_parser()
|
|
25
|
+
formatter = Formatter(indent_width=indent)
|
|
26
|
+
|
|
27
|
+
exit_code = 0
|
|
28
|
+
|
|
29
|
+
for file in __input_files(path):
|
|
30
|
+
if file.suffix.lower() not in {".bean"}:
|
|
31
|
+
continue
|
|
32
|
+
try:
|
|
33
|
+
input_content = file.read_text(encoding="utf-8")
|
|
34
|
+
tree = parser.parse(input_content)
|
|
35
|
+
output_file = io.StringIO()
|
|
36
|
+
with output_file:
|
|
37
|
+
formatter.format(tree, output_file)
|
|
38
|
+
formatted = output_file.getvalue()
|
|
39
|
+
if input_content == formatted:
|
|
40
|
+
continue
|
|
41
|
+
print("formatting", file)
|
|
42
|
+
file.write_bytes(formatted.encode("utf-8"))
|
|
43
|
+
exit_code = 1
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print("failed to format file", file, e)
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
return sys.exit(exit_code)
|
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import decimal
|
|
3
|
+
import enum
|
|
4
|
+
import io
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
from lark import ParseTree, Token, Tree
|
|
10
|
+
|
|
11
|
+
VERBOSE_LOG_LEVEL = logging.NOTSET + 1
|
|
12
|
+
|
|
13
|
+
COMMENT_PREFIX = re.compile("[;*]+")
|
|
14
|
+
DEFAULT_INDENT_WIDTH = 2
|
|
15
|
+
DEFAULT_ACCOUNT_WIDTH = 30
|
|
16
|
+
DEFAULT_NUMBER_WIDTH = 12
|
|
17
|
+
# the difference of column width we need to make up for balance account field,
|
|
18
|
+
# so a balance statement starts with
|
|
19
|
+
#
|
|
20
|
+
# "2022-01-01 balance "
|
|
21
|
+
#
|
|
22
|
+
BALANCE_PREFIX_WIDTH = 19
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@enum.unique
|
|
26
|
+
class EntryType(enum.Enum):
|
|
27
|
+
# Date directives
|
|
28
|
+
OPEN = "OPEN"
|
|
29
|
+
CLOSE = "CLOSE"
|
|
30
|
+
BALANCE = "BALANCE"
|
|
31
|
+
EVENT = "EVENT"
|
|
32
|
+
COMMODITY = "COMMODITY"
|
|
33
|
+
DOCUMENT = "DOCUMENT"
|
|
34
|
+
PRICE = "PRICE"
|
|
35
|
+
NOTE = "NOTE"
|
|
36
|
+
PAD = "PAD"
|
|
37
|
+
CUSTOM = "CUSTOM"
|
|
38
|
+
TXN = "TXN"
|
|
39
|
+
# Simple directives
|
|
40
|
+
OPTION = "OPTION"
|
|
41
|
+
INCLUDE = "INCLUDE"
|
|
42
|
+
PLUGIN = "PLUGIN"
|
|
43
|
+
# Other
|
|
44
|
+
COMMENTS = "COMMENTS"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# The entries which are going to be listed in groups before all other entries
|
|
48
|
+
LEADING_ENTRY_TYPES: typing.List[EntryType] = [
|
|
49
|
+
EntryType.COMMENTS,
|
|
50
|
+
EntryType.INCLUDE,
|
|
51
|
+
EntryType.PLUGIN,
|
|
52
|
+
EntryType.OPTION,
|
|
53
|
+
EntryType.COMMODITY,
|
|
54
|
+
EntryType.OPEN,
|
|
55
|
+
EntryType.CLOSE,
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
DATE_DIRECTIVE_ENTRY_TYPES = {
|
|
59
|
+
"open": EntryType.OPEN,
|
|
60
|
+
"close": EntryType.CLOSE,
|
|
61
|
+
"balance": EntryType.BALANCE,
|
|
62
|
+
"event": EntryType.EVENT,
|
|
63
|
+
"commodity": EntryType.COMMODITY,
|
|
64
|
+
"document": EntryType.DOCUMENT,
|
|
65
|
+
"price": EntryType.PRICE,
|
|
66
|
+
"note": EntryType.NOTE,
|
|
67
|
+
"pad": EntryType.PAD,
|
|
68
|
+
"custom": EntryType.CUSTOM,
|
|
69
|
+
"txn": EntryType.TXN,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
SIMPLE_DIRECTIVE_ENTRY_TYPES = {
|
|
73
|
+
"option": EntryType.OPTION,
|
|
74
|
+
"include": EntryType.INCLUDE,
|
|
75
|
+
"plugin": EntryType.PLUGIN,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_entry_type(statement: Tree) -> EntryType:
|
|
80
|
+
first_child: Tree = statement.children[0]
|
|
81
|
+
if first_child.data == "date_directive":
|
|
82
|
+
return DATE_DIRECTIVE_ENTRY_TYPES[first_child.children[0].data.value]
|
|
83
|
+
if first_child.data == "simple_directive":
|
|
84
|
+
return SIMPLE_DIRECTIVE_ENTRY_TYPES[first_child.children[0].data.value]
|
|
85
|
+
raise ValueError(f"Unexpected first child type {first_child.data}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class StatementGroup(typing.NamedTuple):
|
|
89
|
+
section_header: typing.Optional[Token]
|
|
90
|
+
statements: typing.List[Tree]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Metadata(typing.NamedTuple):
|
|
94
|
+
comments: typing.List[Token]
|
|
95
|
+
statement: Tree
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Posting(typing.NamedTuple):
|
|
99
|
+
comments: typing.List[Token]
|
|
100
|
+
statement: Tree
|
|
101
|
+
metadata: typing.List[Metadata]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Entry(typing.NamedTuple):
|
|
105
|
+
type: EntryType
|
|
106
|
+
comments: typing.List[Token]
|
|
107
|
+
statement: Tree
|
|
108
|
+
metadata: typing.List[Metadata]
|
|
109
|
+
postings: typing.List[Posting]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_date(date_str: str) -> datetime.date:
|
|
113
|
+
parts = date_str.split("-")
|
|
114
|
+
return datetime.date(*(map(int, parts)))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Collector:
|
|
118
|
+
def __init__(self, logger: typing.Optional[logging.Logger] = None):
|
|
119
|
+
super().__init__()
|
|
120
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
121
|
+
# Collection of the header comments
|
|
122
|
+
self.header_comments: typing.List[Token] = []
|
|
123
|
+
self.statement_groups: typing.List[StatementGroup] = []
|
|
124
|
+
|
|
125
|
+
def collect(self, tree: Tree):
|
|
126
|
+
self.logger.info("Collecting")
|
|
127
|
+
if tree.data != "start":
|
|
128
|
+
raise ValueError("Expected start")
|
|
129
|
+
for child in tree.children:
|
|
130
|
+
if child is None:
|
|
131
|
+
continue
|
|
132
|
+
self.statement(child)
|
|
133
|
+
|
|
134
|
+
def statement(self, tree: Tree):
|
|
135
|
+
if tree.data != "statement":
|
|
136
|
+
raise ValueError("Expected statement")
|
|
137
|
+
self.logger.debug("Collecting statement at line %s", tree.meta.line)
|
|
138
|
+
first_child = tree.children[0]
|
|
139
|
+
if isinstance(first_child, Token):
|
|
140
|
+
# Comment only line
|
|
141
|
+
if first_child.type == "COMMENT":
|
|
142
|
+
if self.comment_token(first_child):
|
|
143
|
+
# already added as part of the header comments, just return
|
|
144
|
+
return
|
|
145
|
+
elif first_child.type == "SECTION_HEADER":
|
|
146
|
+
self.section_header_token(first_child)
|
|
147
|
+
return
|
|
148
|
+
else:
|
|
149
|
+
raise ValueError("Unexpected token type %s", first_child.type)
|
|
150
|
+
if not self.statement_groups:
|
|
151
|
+
self.statement_groups.append(
|
|
152
|
+
StatementGroup(section_header=None, statements=[])
|
|
153
|
+
)
|
|
154
|
+
self.statement_groups[-1].statements.append(tree)
|
|
155
|
+
|
|
156
|
+
def section_header_token(self, token: Token):
|
|
157
|
+
self.logger.debug(
|
|
158
|
+
"New statement group for %r at line %s", token.value, token.line
|
|
159
|
+
)
|
|
160
|
+
self.statement_groups.append(
|
|
161
|
+
StatementGroup(section_header=token, statements=[])
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def comment_token(self, token: Token) -> bool:
|
|
165
|
+
if token.line != len(self.header_comments) + 1:
|
|
166
|
+
return False
|
|
167
|
+
if self.statement_groups:
|
|
168
|
+
return False
|
|
169
|
+
self.logger.debug("Collect header comment %s at line %s", token, token.line)
|
|
170
|
+
self.header_comments.append(token)
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def chunks(lst, n):
|
|
175
|
+
"""Yield successive n-sized chunks from lst."""
|
|
176
|
+
for i in range(0, len(lst), n):
|
|
177
|
+
yield lst[i : i + n]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def format_number(d: decimal.Decimal, n: int = 3):
|
|
181
|
+
ss = str(d)
|
|
182
|
+
|
|
183
|
+
a, _, b = ss.partition(".")
|
|
184
|
+
|
|
185
|
+
parts = []
|
|
186
|
+
|
|
187
|
+
for part in chunks(a[::-1], n):
|
|
188
|
+
parts.append("".join(reversed(part)))
|
|
189
|
+
|
|
190
|
+
parts.reverse()
|
|
191
|
+
|
|
192
|
+
a = ",".join(parts)
|
|
193
|
+
|
|
194
|
+
if b:
|
|
195
|
+
return a + "." + b
|
|
196
|
+
|
|
197
|
+
return a
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Formatter:
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
indent_width: int = DEFAULT_INDENT_WIDTH,
|
|
204
|
+
min_account_width: int = DEFAULT_ACCOUNT_WIDTH,
|
|
205
|
+
min_number_width: int = DEFAULT_NUMBER_WIDTH,
|
|
206
|
+
# num_sep_width: int = 3,
|
|
207
|
+
logger: typing.Optional[logging.Logger] = None,
|
|
208
|
+
):
|
|
209
|
+
self.indent_width: int = indent_width
|
|
210
|
+
self.account_width: int = min_account_width
|
|
211
|
+
self.number_width: int = min_number_width
|
|
212
|
+
# self.num_sep_width: int = num_sep_width
|
|
213
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
214
|
+
|
|
215
|
+
def format_comment(self, token: Token) -> str:
|
|
216
|
+
value = token.value.strip()
|
|
217
|
+
match = COMMENT_PREFIX.match(value)
|
|
218
|
+
prefix = match.group(0)
|
|
219
|
+
remain = value[len(prefix) :].strip()
|
|
220
|
+
if not remain:
|
|
221
|
+
return prefix
|
|
222
|
+
return f"{prefix} {remain}"
|
|
223
|
+
|
|
224
|
+
def format_number(self, token: Token) -> str:
|
|
225
|
+
if token.type != "NUMBER":
|
|
226
|
+
raise ValueError("Expected a NUMBER")
|
|
227
|
+
value = token.value.replace(",", "")
|
|
228
|
+
|
|
229
|
+
return str(decimal.Decimal(value))
|
|
230
|
+
|
|
231
|
+
def format_number_atom(self, tree_or_token: typing.Union[Tree, Token]) -> str:
|
|
232
|
+
if isinstance(tree_or_token, Token):
|
|
233
|
+
token: Token = tree_or_token
|
|
234
|
+
if token.type == "NUMBER":
|
|
235
|
+
return self.format_number(token)
|
|
236
|
+
raise ValueError(f"Unknown token type {token.type}")
|
|
237
|
+
if isinstance(tree_or_token, Tree):
|
|
238
|
+
tree: Tree = tree_or_token
|
|
239
|
+
if tree.data == "number_atom":
|
|
240
|
+
unary_op, number_atom = tree.children
|
|
241
|
+
if unary_op.type != "UNARY_OP":
|
|
242
|
+
raise ValueError(f"Expected to be UNARY_OP but got {unary_op.data}")
|
|
243
|
+
return unary_op.value + self.format_number_atom(number_atom)
|
|
244
|
+
if tree.data == "number_mul_expr":
|
|
245
|
+
return self.format_number_mul_expr(tree)
|
|
246
|
+
if tree.data == "number_add_expr":
|
|
247
|
+
return self.format_number_add_expr(tree)
|
|
248
|
+
raise ValueError(f"Unknown tree {tree.data}")
|
|
249
|
+
raise ValueError(f"Unexpected type {type(tree_or_token)}")
|
|
250
|
+
|
|
251
|
+
def format_number_mul_expr(self, tree: Tree) -> str:
|
|
252
|
+
if tree.data != "number_mul_expr":
|
|
253
|
+
raise ValueError("Expected a number_mul_expr")
|
|
254
|
+
items: typing.List[str] = []
|
|
255
|
+
for child in tree.children:
|
|
256
|
+
if isinstance(child, Token):
|
|
257
|
+
if child.type == "MUL_OP":
|
|
258
|
+
items.append(f" {child.value} ")
|
|
259
|
+
else:
|
|
260
|
+
items.append(self.format_number_atom(child))
|
|
261
|
+
else:
|
|
262
|
+
items.append(self.format_number_atom(child))
|
|
263
|
+
return f'({"".join(items)})'
|
|
264
|
+
|
|
265
|
+
def format_number_add_expr(self, tree: Tree) -> str:
|
|
266
|
+
if tree.data != "number_add_expr":
|
|
267
|
+
raise ValueError("Expected a number_add_expr")
|
|
268
|
+
items: typing.List[str] = []
|
|
269
|
+
for child in tree.children:
|
|
270
|
+
if isinstance(child, Token):
|
|
271
|
+
if child.type == "ADD_OP":
|
|
272
|
+
items.append(f" {child.value} ")
|
|
273
|
+
else:
|
|
274
|
+
items.append(self.format_number_atom(child))
|
|
275
|
+
elif isinstance(child, Tree) and child.data == "number_atom":
|
|
276
|
+
items.append(self.format_number_atom(child))
|
|
277
|
+
else:
|
|
278
|
+
items.append(self.format_number_mul_expr(child))
|
|
279
|
+
return f'({"".join(items)})'
|
|
280
|
+
|
|
281
|
+
def format_number_expr(self, tree: Tree) -> str:
|
|
282
|
+
if tree.data != "number_expr":
|
|
283
|
+
raise ValueError("Expected a number_expr")
|
|
284
|
+
first_child: typing.Union[Tree, Token] = tree.children[0]
|
|
285
|
+
if isinstance(first_child, Tree) and first_child.data == "number_add_expr":
|
|
286
|
+
return self.format_number_add_expr(first_child)
|
|
287
|
+
return self.format_number_atom(first_child)
|
|
288
|
+
|
|
289
|
+
def get_amount_columns(self, tree: Tree) -> typing.List[str]:
|
|
290
|
+
if tree.data != "amount":
|
|
291
|
+
raise ValueError("Expected a amount")
|
|
292
|
+
number, currency = tree.children
|
|
293
|
+
return [self.format_number_expr(number), currency.value]
|
|
294
|
+
|
|
295
|
+
def get_amount_tolerance_columns(self, tree: Tree) -> typing.List[str]:
|
|
296
|
+
if tree.data != "amount_tolerance":
|
|
297
|
+
raise ValueError("Expected a amount")
|
|
298
|
+
number, tolerance, currency = tree.children
|
|
299
|
+
return [
|
|
300
|
+
self.format_number_expr(number),
|
|
301
|
+
"~",
|
|
302
|
+
self.format_number_expr(tolerance),
|
|
303
|
+
currency.value,
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
def format_price(self, tree: Tree) -> str:
|
|
307
|
+
if tree.data not in {"per_unit_price", "total_price"}:
|
|
308
|
+
raise ValueError("Expected a per_unit_price or total_price")
|
|
309
|
+
amount = tree.children[0]
|
|
310
|
+
amount_value = " ".join(self.get_amount_columns(amount))
|
|
311
|
+
if tree.data == "per_unit_price":
|
|
312
|
+
prefix = "@"
|
|
313
|
+
elif tree.data == "total_price":
|
|
314
|
+
prefix = "@@"
|
|
315
|
+
else:
|
|
316
|
+
raise ValueError
|
|
317
|
+
return " ".join([prefix, amount_value])
|
|
318
|
+
|
|
319
|
+
def format_cost_item(self, tree: Tree) -> str:
|
|
320
|
+
if tree.data != "cost_item":
|
|
321
|
+
raise ValueError("Expected a cost_item")
|
|
322
|
+
child = tree.children[0]
|
|
323
|
+
if isinstance(child, Token) and child.type in {
|
|
324
|
+
"DATE",
|
|
325
|
+
"ESCAPED_STRING",
|
|
326
|
+
"ASTERISK",
|
|
327
|
+
}:
|
|
328
|
+
return child.value
|
|
329
|
+
if child.data == "amount":
|
|
330
|
+
return " ".join(self.get_amount_columns(child))
|
|
331
|
+
raise ValueError(f"Unexpected cost item {tree}")
|
|
332
|
+
|
|
333
|
+
def format_cost(self, tree: Tree) -> str:
|
|
334
|
+
if tree.data in {"per_unit_cost", "dated_cost"}:
|
|
335
|
+
raise RuntimeError(
|
|
336
|
+
"You are using an out-dated beancount-parser, version >= 1.0.0 is required"
|
|
337
|
+
)
|
|
338
|
+
if tree.data not in {"total_cost", "both_cost", "cost_spec"}:
|
|
339
|
+
raise ValueError("Expected a total_cost, both_cost or cost_spec")
|
|
340
|
+
if tree.data != "total_cost":
|
|
341
|
+
bracket_start = "{"
|
|
342
|
+
bracket_end = "}"
|
|
343
|
+
else:
|
|
344
|
+
bracket_start = "{{"
|
|
345
|
+
bracket_end = "}}"
|
|
346
|
+
separator = " "
|
|
347
|
+
items: typing.List[str] = []
|
|
348
|
+
if tree.data == "total_cost":
|
|
349
|
+
amount = tree.children[0]
|
|
350
|
+
amount_value = " ".join(self.get_amount_columns(amount))
|
|
351
|
+
items.append(amount_value)
|
|
352
|
+
if tree.data == "both_cost":
|
|
353
|
+
number, amount = tree.children
|
|
354
|
+
number_value = self.format_number_expr(number)
|
|
355
|
+
amount_value = " ".join(self.get_amount_columns(amount))
|
|
356
|
+
items.append(number_value)
|
|
357
|
+
items.append("#")
|
|
358
|
+
items.append(amount_value)
|
|
359
|
+
elif tree.data == "cost_spec":
|
|
360
|
+
separator = ", "
|
|
361
|
+
items.extend(map(self.format_cost_item, tree.children))
|
|
362
|
+
return bracket_start + separator.join(items) + bracket_end
|
|
363
|
+
|
|
364
|
+
def get_directive_child_columns(
|
|
365
|
+
self, child: typing.Union[Token, Tree]
|
|
366
|
+
) -> typing.List[str]:
|
|
367
|
+
if isinstance(child, Token):
|
|
368
|
+
# TODO: some token may need reformat?
|
|
369
|
+
return [child.value]
|
|
370
|
+
tree: Tree = child
|
|
371
|
+
if tree.data == "currencies":
|
|
372
|
+
return [",".join(currency.value for currency in tree.children)]
|
|
373
|
+
if tree.data == "amount":
|
|
374
|
+
return self.get_amount_columns(tree)
|
|
375
|
+
if tree.data == "amount_tolerance":
|
|
376
|
+
return self.get_amount_tolerance_columns(tree)
|
|
377
|
+
if tree.data == "number_expr":
|
|
378
|
+
return [self.format_number_expr(tree)]
|
|
379
|
+
raise ValueError(f"Unknown tree type {tree.data}")
|
|
380
|
+
|
|
381
|
+
def format_metadata_item_value(
|
|
382
|
+
self, tree_or_token: typing.Union[Tree, Token]
|
|
383
|
+
) -> str:
|
|
384
|
+
if isinstance(tree_or_token, Token):
|
|
385
|
+
token: Token = tree_or_token
|
|
386
|
+
if token.type in {"ESCAPED_STRING", "ACCOUNT", "CURRENCY", "DATE", "TAGS"}:
|
|
387
|
+
return token.value
|
|
388
|
+
raise ValueError(f"Unknown token type {token.type}")
|
|
389
|
+
if isinstance(tree_or_token, Tree):
|
|
390
|
+
tree: Tree = tree_or_token
|
|
391
|
+
if tree.data == "number_expr":
|
|
392
|
+
return self.format_number_expr(tree)
|
|
393
|
+
if tree.data == "amount":
|
|
394
|
+
return " ".join(self.get_amount_columns(tree))
|
|
395
|
+
raise ValueError(f"Unknown tree {tree.data}")
|
|
396
|
+
raise ValueError(f"Unexpected type {type(tree_or_token)}")
|
|
397
|
+
|
|
398
|
+
def format_metadata_item(self, tree: Tree) -> str:
|
|
399
|
+
if tree.data != "metadata_item":
|
|
400
|
+
raise ValueError("Expected a metadata item")
|
|
401
|
+
key_token, value_tree_or_token = tree.children
|
|
402
|
+
return (
|
|
403
|
+
f"{key_token.value}: {self.format_metadata_item_value(value_tree_or_token)}"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def format_simple_directive(self, tree: Tree) -> str:
|
|
407
|
+
if tree.data != "simple_directive":
|
|
408
|
+
raise ValueError("Expected a simple directive")
|
|
409
|
+
first_child = tree.children[0]
|
|
410
|
+
items: typing.List[str] = [first_child.data.value] + [
|
|
411
|
+
child.value for child in first_child.children if child is not None
|
|
412
|
+
]
|
|
413
|
+
return " ".join(items)
|
|
414
|
+
|
|
415
|
+
def format_date_directive(self, tree: Tree) -> str:
|
|
416
|
+
if tree.data != "date_directive":
|
|
417
|
+
raise ValueError("Expected a date directive")
|
|
418
|
+
first_child = tree.children[0]
|
|
419
|
+
date = first_child.children[0].value
|
|
420
|
+
directive_type = first_child.data.value
|
|
421
|
+
if directive_type == "txn":
|
|
422
|
+
columns: typing.List[str] = [date]
|
|
423
|
+
flag, payee, narration, annotations = first_child.children[1:]
|
|
424
|
+
if flag is not None:
|
|
425
|
+
columns.append(flag.value)
|
|
426
|
+
if payee is not None:
|
|
427
|
+
columns.append(payee.value)
|
|
428
|
+
if narration is not None:
|
|
429
|
+
columns.append(narration.value)
|
|
430
|
+
if annotations is not None:
|
|
431
|
+
annotation_values = [
|
|
432
|
+
annotation.value for annotation in annotations.children
|
|
433
|
+
]
|
|
434
|
+
links = list(filter(lambda v: v.startswith("^"), annotation_values))
|
|
435
|
+
links.sort()
|
|
436
|
+
hashes = list(filter(lambda v: v.startswith("#"), annotation_values))
|
|
437
|
+
hashes.sort()
|
|
438
|
+
columns.extend(links)
|
|
439
|
+
columns.extend(hashes)
|
|
440
|
+
return " ".join(columns)
|
|
441
|
+
columns: typing.List[str] = [date, directive_type]
|
|
442
|
+
for child in first_child.children[1:]:
|
|
443
|
+
if child is None:
|
|
444
|
+
continue
|
|
445
|
+
columns.extend(self.get_directive_child_columns(child))
|
|
446
|
+
if directive_type == "balance":
|
|
447
|
+
for index, column in enumerate(columns):
|
|
448
|
+
prefix = ""
|
|
449
|
+
# account
|
|
450
|
+
if index == 2:
|
|
451
|
+
width = self.account_width
|
|
452
|
+
# number
|
|
453
|
+
elif index == 3:
|
|
454
|
+
width = self.number_width
|
|
455
|
+
prefix = ">"
|
|
456
|
+
else:
|
|
457
|
+
continue
|
|
458
|
+
new_value = f"{column:{prefix}{width}}"
|
|
459
|
+
columns[index] = new_value
|
|
460
|
+
return " ".join(columns)
|
|
461
|
+
|
|
462
|
+
def format_posting(self, tree: Tree) -> str:
|
|
463
|
+
if tree.data != "posting":
|
|
464
|
+
raise ValueError("Expected a posting")
|
|
465
|
+
# Simple posting
|
|
466
|
+
flag: Token
|
|
467
|
+
account: Token
|
|
468
|
+
amount: typing.Optional[Tree] = None
|
|
469
|
+
cost: typing.Optional[Tree] = None
|
|
470
|
+
price: typing.Optional[Tree] = None
|
|
471
|
+
if tree.children[0].data == "detailed_posting":
|
|
472
|
+
flag, account, amount, cost, price = tree.children[0].children
|
|
473
|
+
else:
|
|
474
|
+
flag, account = tree.children[0].children
|
|
475
|
+
items: typing.List[str] = []
|
|
476
|
+
if flag is not None:
|
|
477
|
+
items.append(flag.value)
|
|
478
|
+
account_value = account.value
|
|
479
|
+
# only need to apply width when it's not short posting format
|
|
480
|
+
if amount is not None:
|
|
481
|
+
# Need to add the difference for balance prefix width
|
|
482
|
+
width = self.account_width + (BALANCE_PREFIX_WIDTH - self.indent_width)
|
|
483
|
+
account_value = f"{account_value:{width}}"
|
|
484
|
+
items.append(account_value)
|
|
485
|
+
if amount is not None:
|
|
486
|
+
number, currency = self.get_amount_columns(amount)
|
|
487
|
+
items.append(f"{number:>{self.number_width}}")
|
|
488
|
+
items.append(currency)
|
|
489
|
+
if cost is not None:
|
|
490
|
+
items.append(self.format_cost(cost))
|
|
491
|
+
if price is not None:
|
|
492
|
+
items.append(self.format_price(price))
|
|
493
|
+
return " ".join(items)
|
|
494
|
+
|
|
495
|
+
def format_metadata_lines(
|
|
496
|
+
self, metadata_list: typing.List[Metadata]
|
|
497
|
+
) -> typing.List[str]:
|
|
498
|
+
lines: typing.List[str] = []
|
|
499
|
+
for metadata in metadata_list:
|
|
500
|
+
for comment in metadata.comments:
|
|
501
|
+
lines.append(self.format_comment(comment))
|
|
502
|
+
line = self.format_metadata_item(metadata.statement.children[0])
|
|
503
|
+
tail_comment = metadata.statement.children[1]
|
|
504
|
+
if tail_comment is not None:
|
|
505
|
+
line += " " + self.format_comment(tail_comment)
|
|
506
|
+
lines.append(line)
|
|
507
|
+
return lines
|
|
508
|
+
|
|
509
|
+
def format_posting_lines(
|
|
510
|
+
self,
|
|
511
|
+
postings: typing.List[Posting],
|
|
512
|
+
) -> typing.List[str]:
|
|
513
|
+
lines: typing.List[str] = []
|
|
514
|
+
for posting in postings:
|
|
515
|
+
for comment in posting.comments:
|
|
516
|
+
lines.append(self.format_comment(comment))
|
|
517
|
+
line = self.format_posting(posting.statement.children[0])
|
|
518
|
+
tail_comment = posting.statement.children[1]
|
|
519
|
+
if tail_comment is not None:
|
|
520
|
+
line += " " + self.format_comment(tail_comment)
|
|
521
|
+
lines.append(line)
|
|
522
|
+
metadata_lines = self.format_metadata_lines(posting.metadata)
|
|
523
|
+
for metadata_line in metadata_lines:
|
|
524
|
+
lines.append(" " * self.indent_width + metadata_line)
|
|
525
|
+
return lines
|
|
526
|
+
|
|
527
|
+
def format_entry(self, entry: Entry) -> str:
|
|
528
|
+
self.logger.debug(
|
|
529
|
+
"Format entry type %s at line %s",
|
|
530
|
+
entry.type.value,
|
|
531
|
+
entry.statement.meta.line,
|
|
532
|
+
)
|
|
533
|
+
self.logger.log(VERBOSE_LOG_LEVEL, "Entry value %s", entry)
|
|
534
|
+
lines = []
|
|
535
|
+
for comment in entry.comments:
|
|
536
|
+
lines.append(self.format_comment(comment))
|
|
537
|
+
|
|
538
|
+
if entry.type != EntryType.COMMENTS:
|
|
539
|
+
first_child = entry.statement.children[0]
|
|
540
|
+
if first_child.data == "date_directive":
|
|
541
|
+
line = self.format_date_directive(first_child)
|
|
542
|
+
tail_comment = entry.statement.children[1]
|
|
543
|
+
if tail_comment is not None:
|
|
544
|
+
line += " " + self.format_comment(tail_comment)
|
|
545
|
+
lines.append(line)
|
|
546
|
+
metadata_lines = self.format_metadata_lines(entry.metadata)
|
|
547
|
+
for metadata_line in metadata_lines:
|
|
548
|
+
lines.append(" " * self.indent_width + metadata_line)
|
|
549
|
+
posting_lines = self.format_posting_lines(entry.postings)
|
|
550
|
+
for posting_line in posting_lines:
|
|
551
|
+
lines.append(" " * self.indent_width + posting_line)
|
|
552
|
+
else:
|
|
553
|
+
line = self.format_simple_directive(first_child)
|
|
554
|
+
tail_comment = entry.statement.children[1]
|
|
555
|
+
if tail_comment is not None:
|
|
556
|
+
line += " " + self.format_comment(tail_comment)
|
|
557
|
+
lines.append(line)
|
|
558
|
+
return "\n".join(lines)
|
|
559
|
+
|
|
560
|
+
def format_statement_group(self, group: StatementGroup) -> str:
|
|
561
|
+
sections: typing.List[str] = []
|
|
562
|
+
if group.section_header is not None:
|
|
563
|
+
sections.append(self.format_comment(group.section_header))
|
|
564
|
+
|
|
565
|
+
entries: typing.List[Entry] = []
|
|
566
|
+
comments: typing.List[Token] = []
|
|
567
|
+
for statement in group.statements:
|
|
568
|
+
first_child = statement.children[0]
|
|
569
|
+
if isinstance(first_child, Token):
|
|
570
|
+
if first_child.type == "COMMENT":
|
|
571
|
+
comments.append(first_child)
|
|
572
|
+
else:
|
|
573
|
+
raise ValueError(f"Unexpected token {first_child.type}")
|
|
574
|
+
else:
|
|
575
|
+
if first_child.data == "posting":
|
|
576
|
+
last_entry = entries[-1]
|
|
577
|
+
if last_entry.type != EntryType.TXN:
|
|
578
|
+
raise ValueError("Transaction expected")
|
|
579
|
+
last_entry.postings.append(
|
|
580
|
+
Posting(comments=comments, statement=statement, metadata=[])
|
|
581
|
+
)
|
|
582
|
+
comments = []
|
|
583
|
+
continue
|
|
584
|
+
if first_child.data == "metadata_item":
|
|
585
|
+
last_entry = entries[-1]
|
|
586
|
+
metadata = Metadata(comments=comments, statement=statement)
|
|
587
|
+
if last_entry.postings:
|
|
588
|
+
last_posting: Posting = last_entry.postings[-1]
|
|
589
|
+
last_posting.metadata.append(metadata)
|
|
590
|
+
else:
|
|
591
|
+
last_entry.metadata.append(metadata)
|
|
592
|
+
comments = []
|
|
593
|
+
continue
|
|
594
|
+
entry = Entry(
|
|
595
|
+
type=get_entry_type(statement),
|
|
596
|
+
comments=comments,
|
|
597
|
+
statement=statement,
|
|
598
|
+
metadata=[],
|
|
599
|
+
postings=[],
|
|
600
|
+
)
|
|
601
|
+
entries.append(entry)
|
|
602
|
+
comments = []
|
|
603
|
+
|
|
604
|
+
for entry in entries:
|
|
605
|
+
sections.append(self.format_entry(entry))
|
|
606
|
+
|
|
607
|
+
return "\n\n".join(sections)
|
|
608
|
+
|
|
609
|
+
def calculate_column_widths(self, tree: ParseTree):
|
|
610
|
+
self.logger.info("Calculate column width")
|
|
611
|
+
for statement in tree.children:
|
|
612
|
+
if statement is None:
|
|
613
|
+
continue
|
|
614
|
+
self.logger.debug(
|
|
615
|
+
"Calculate column width for statement at line %s", statement.meta.line
|
|
616
|
+
)
|
|
617
|
+
self.logger.log(VERBOSE_LOG_LEVEL, "Statement %s", statement)
|
|
618
|
+
first_child = statement.children[0]
|
|
619
|
+
if isinstance(first_child, Token):
|
|
620
|
+
continue
|
|
621
|
+
account: typing.Optional[Token] = None
|
|
622
|
+
amount: typing.Optional[Tree] = None
|
|
623
|
+
if first_child.data == "posting":
|
|
624
|
+
# Simple posting
|
|
625
|
+
if first_child.children[0].data == "detailed_posting":
|
|
626
|
+
_, account, amount, *_ = first_child.children[0].children
|
|
627
|
+
else:
|
|
628
|
+
_, account = first_child.children[0].children
|
|
629
|
+
elif (
|
|
630
|
+
first_child.data == "date_directive"
|
|
631
|
+
and first_child.children[0].data == "balance"
|
|
632
|
+
):
|
|
633
|
+
_, account, amount = first_child.children[0].children
|
|
634
|
+
if account is not None and len(account.value) > self.account_width:
|
|
635
|
+
# bump account width
|
|
636
|
+
self.account_width = len(account.value)
|
|
637
|
+
if amount is not None:
|
|
638
|
+
width = len(self.format_number_expr(amount.children[0]))
|
|
639
|
+
self.number_width = max(width, self.number_width)
|
|
640
|
+
|
|
641
|
+
def format(self, tree: ParseTree, output_file: io.TextIOBase):
|
|
642
|
+
if tree.data != "start":
|
|
643
|
+
raise ValueError("expected start as the root rule")
|
|
644
|
+
self.calculate_column_widths(tree)
|
|
645
|
+
|
|
646
|
+
collector = Collector()
|
|
647
|
+
collector.collect(tree)
|
|
648
|
+
|
|
649
|
+
# write header comments
|
|
650
|
+
sections: typing.List[str] = []
|
|
651
|
+
if collector.header_comments:
|
|
652
|
+
lines: typing.List[str] = [
|
|
653
|
+
self.format_comment(header_comment)
|
|
654
|
+
for header_comment in collector.header_comments
|
|
655
|
+
]
|
|
656
|
+
sections.append("\n".join(lines))
|
|
657
|
+
|
|
658
|
+
for group in collector.statement_groups:
|
|
659
|
+
# Console(emoji=False).print(group)
|
|
660
|
+
sections.append(self.format_statement_group(group))
|
|
661
|
+
|
|
662
|
+
output_file.write("\n\n".join(sections))
|
|
663
|
+
if sections:
|
|
664
|
+
output_file.write("\n")
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit-core~=3.9"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "beancount-format"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Typed rtorrent rpc client"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "trim21", email = "trim21me@gmail.com" },
|
|
11
|
+
]
|
|
12
|
+
readme = 'readme.md'
|
|
13
|
+
license = { text = 'MIT' }
|
|
14
|
+
keywords = ['rtorrent', 'rpc']
|
|
15
|
+
classifiers = [
|
|
16
|
+
'Intended Audience :: Developers',
|
|
17
|
+
'Development Status :: 4 - Beta',
|
|
18
|
+
'License :: OSI Approved :: MIT License',
|
|
19
|
+
'Programming Language :: Python :: 3 :: Only',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
requires-python = "~=3.9"
|
|
23
|
+
|
|
24
|
+
dependencies = [
|
|
25
|
+
'beancount-parser==1.2.3',
|
|
26
|
+
'click~=8.0',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest==8.3.2",
|
|
32
|
+
"pytest-github-actions-annotate-failures==0.2.0",
|
|
33
|
+
"coverage==7.6.1",
|
|
34
|
+
'pre-commit==3.8.0; python_version >= "3.9"',
|
|
35
|
+
'mypy==1.13.0; python_version >= "3.9"',
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/trim21/beancount-format"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
beancount-format = "beancount_format.cli:main"
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
addopts = '-rav -Werror'
|
|
46
|
+
|
|
47
|
+
[tool.mypy]
|
|
48
|
+
python_version = "3.8"
|
|
49
|
+
disallow_untyped_defs = true
|
|
50
|
+
ignore_missing_imports = true
|
|
51
|
+
warn_return_any = false
|
|
52
|
+
warn_unused_configs = true
|
|
53
|
+
show_error_codes = true
|
|
54
|
+
|
|
55
|
+
platform = 'unix'
|
|
56
|
+
|
|
57
|
+
[tool.black]
|
|
58
|
+
target-version = ['py38']
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
target-version = "py38"
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = [
|
|
65
|
+
"B",
|
|
66
|
+
"C",
|
|
67
|
+
"E",
|
|
68
|
+
"F",
|
|
69
|
+
"G",
|
|
70
|
+
"I",
|
|
71
|
+
"N",
|
|
72
|
+
"Q",
|
|
73
|
+
"S",
|
|
74
|
+
"W",
|
|
75
|
+
"BLE",
|
|
76
|
+
"EXE",
|
|
77
|
+
"ICN",
|
|
78
|
+
"INP",
|
|
79
|
+
"ISC",
|
|
80
|
+
"NPY",
|
|
81
|
+
"PD",
|
|
82
|
+
"PGH",
|
|
83
|
+
"PIE",
|
|
84
|
+
"PL",
|
|
85
|
+
"PT",
|
|
86
|
+
"PYI",
|
|
87
|
+
"RET",
|
|
88
|
+
"RSE",
|
|
89
|
+
"RUF",
|
|
90
|
+
"SIM",
|
|
91
|
+
"SLF",
|
|
92
|
+
"TCH",
|
|
93
|
+
"TID",
|
|
94
|
+
"TRY",
|
|
95
|
+
"YTT",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
ignore = [
|
|
99
|
+
'PLR0911',
|
|
100
|
+
'INP001',
|
|
101
|
+
'N806',
|
|
102
|
+
'N802',
|
|
103
|
+
'N803',
|
|
104
|
+
'E501',
|
|
105
|
+
'BLE001',
|
|
106
|
+
'RUF002',
|
|
107
|
+
'S324',
|
|
108
|
+
'S301',
|
|
109
|
+
'S314',
|
|
110
|
+
'S101',
|
|
111
|
+
'N815',
|
|
112
|
+
'S104',
|
|
113
|
+
'C901',
|
|
114
|
+
'PLR0913',
|
|
115
|
+
'RUF001',
|
|
116
|
+
'SIM108',
|
|
117
|
+
'TCH003',
|
|
118
|
+
'RUF003',
|
|
119
|
+
'RET504',
|
|
120
|
+
'TRY300',
|
|
121
|
+
'TRY003',
|
|
122
|
+
'TRY201',
|
|
123
|
+
'TRY301',
|
|
124
|
+
'PLR0912',
|
|
125
|
+
'PLR0915',
|
|
126
|
+
'PLR2004',
|
|
127
|
+
'PGH003',
|
|
128
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
version: 3
|
|
2
|
+
|
|
3
|
+
tasks:
|
|
4
|
+
default:
|
|
5
|
+
- black .
|
|
6
|
+
- ruff check . --fix
|
|
7
|
+
|
|
8
|
+
minor:
|
|
9
|
+
cmds:
|
|
10
|
+
- pyproject-bump minor
|
|
11
|
+
- task: bump
|
|
12
|
+
|
|
13
|
+
patch:
|
|
14
|
+
cmds:
|
|
15
|
+
- pyproject-bump micro
|
|
16
|
+
- task: bump
|
|
17
|
+
|
|
18
|
+
bump:
|
|
19
|
+
vars:
|
|
20
|
+
VERSION:
|
|
21
|
+
sh: yq '.project.version' pyproject.toml
|
|
22
|
+
cmds:
|
|
23
|
+
- git add pyproject.toml
|
|
24
|
+
- 'git commit -m "bump: {{.VERSION}}"'
|
|
25
|
+
- 'git tag "v{{.VERSION}}" -m "v{{.VERSION}}"'
|