├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .mypy.ini ├── .readthedocs.yml ├── LICENSE ├── README.md ├── autobean_refactor ├── __init__.py ├── beancount.lark ├── editor.py ├── meta_models │ ├── __init__.py │ ├── base.py │ └── meta_models.py ├── modelgen │ ├── __init__.py │ ├── descriptor.py │ ├── generate.py │ ├── modelgen_test.py │ └── raw_model.mako ├── models │ ├── __init__.py │ ├── account.py │ ├── amount.py │ ├── balance.py │ ├── base.py │ ├── block_comment.py │ ├── bool.py │ ├── close.py │ ├── commodity.py │ ├── compound_amount.py │ ├── cost.py │ ├── cost_component.py │ ├── cost_spec.py │ ├── currency.py │ ├── custom.py │ ├── date.py │ ├── document.py │ ├── escaped_string.py │ ├── event.py │ ├── file.py │ ├── generated │ │ ├── __init__.py │ │ ├── amount.py │ │ ├── balance.py │ │ ├── close.py │ │ ├── commodity.py │ │ ├── compound_amount.py │ │ ├── cost_spec.py │ │ ├── custom.py │ │ ├── document.py │ │ ├── event.py │ │ ├── file.py │ │ ├── ignored_line.py │ │ ├── include.py │ │ ├── meta_item.py │ │ ├── note.py │ │ ├── number_expr.py │ │ ├── number_paren_expr.py │ │ ├── number_unary_expr.py │ │ ├── open.py │ │ ├── option.py │ │ ├── pad.py │ │ ├── plugin.py │ │ ├── popmeta.py │ │ ├── poptag.py │ │ ├── posting.py │ │ ├── price.py │ │ ├── pushmeta.py │ │ ├── pushtag.py │ │ ├── query.py │ │ ├── tolerance.py │ │ ├── total_cost.py │ │ ├── total_price.py │ │ ├── transaction.py │ │ ├── unit_cost.py │ │ └── unit_price.py │ ├── ignored.py │ ├── ignored_line.py │ ├── include.py │ ├── inline_comment.py │ ├── internal │ │ ├── __init__.py │ │ ├── base_property.py │ │ ├── base_token_models.py │ │ ├── fields.py │ │ ├── indexes.py │ │ ├── interleaving_comments.py │ │ ├── placeholder.py │ │ ├── properties.py │ │ ├── registry.py │ │ ├── repeated.py │ │ ├── spacing_accessors.py │ │ ├── surrounding_comments.py │ │ ├── token_models.py │ │ └── value_properties.py │ ├── link.py │ ├── meta_item.py │ ├── meta_item_internal.py │ ├── meta_key.py │ ├── meta_value.py │ ├── meta_value_internal.py │ ├── note.py │ ├── null.py │ ├── number.py │ ├── number_add_expr.py │ ├── number_atom_expr.py │ ├── number_expr.py │ ├── number_mul_expr.py │ ├── number_paren_expr.py │ ├── number_unary_expr.py │ ├── open.py │ ├── option.py │ ├── pad.py │ ├── plugin.py │ ├── popmeta.py │ ├── poptag.py │ ├── posting.py │ ├── posting_flag.py │ ├── price.py │ ├── punctuation.py │ ├── pushmeta.py │ ├── pushtag.py │ ├── query.py │ ├── spacing.py │ ├── tag.py │ ├── tolerance.py │ ├── total_price.py │ ├── transaction.py │ ├── transaction_flag.py │ └── unit_price.py ├── parser.py ├── printer.py ├── py.typed ├── tests │ ├── __init__.py │ ├── base.py │ ├── benchmark │ │ └── benchmark_test.py │ ├── conftest.py │ ├── editor_test.py │ ├── generic │ │ ├── __init__.py │ │ ├── auto_claim_comments_test.py │ │ ├── inline_comment_test.py │ │ ├── interleaving_comment_test.py │ │ ├── meta_test.py │ │ ├── spacing_accessors_test.py │ │ └── surrounding_comment_test.py │ ├── models │ │ ├── __init__.py │ │ ├── account_test.py │ │ ├── amount_test.py │ │ ├── balance_test.py │ │ ├── block_comment_test.py │ │ ├── bool_test.py │ │ ├── close_test.py │ │ ├── commodity_test.py │ │ ├── cost_spec_test.py │ │ ├── currency_test.py │ │ ├── custom_test.py │ │ ├── date_test.py │ │ ├── document_test.py │ │ ├── escaped_string_test.py │ │ ├── event_test.py │ │ ├── file_test.py │ │ ├── ignored_test.py │ │ ├── include_test.py │ │ ├── inline_comment_test.py │ │ ├── link_test.py │ │ ├── meta_item_test.py │ │ ├── meta_key_test.py │ │ ├── newline_test.py │ │ ├── note_test.py │ │ ├── null_test.py │ │ ├── number_expr_test.py │ │ ├── number_test.py │ │ ├── open_test.py │ │ ├── option_test.py │ │ ├── pad_test.py │ │ ├── plugin_test.py │ │ ├── popmeta_test.py │ │ ├── poptag_test.py │ │ ├── posting_flag_test.py │ │ ├── posting_test.py │ │ ├── price_test.py │ │ ├── pushmeta_test.py │ │ ├── pushtag_test.py │ │ ├── query_test.py │ │ ├── tag_test.py │ │ ├── tags_links_test.py │ │ ├── transaction_flag_test.py │ │ ├── transaction_test.py │ │ └── whitespace_test.py │ └── token_store_test.py └── token_store.py ├── docs ├── _config.yml ├── _extensions │ └── local.py ├── _static │ ├── custom.css │ └── wrap-parameters.css ├── _templates │ ├── model.rst │ └── type-alias.rst ├── _toc.yml ├── code │ ├── basics.py │ └── parsing.py ├── examples.md ├── generic │ ├── models.md │ ├── token-models.md │ └── tree-models.md ├── getting-started.md ├── guarantees.md ├── references │ ├── .gitignore │ ├── token-models.md │ ├── tree-models.md │ └── type-aliases.md └── special │ ├── comments.md │ ├── indents.md │ ├── meta.md │ ├── numbers.md │ ├── prices-costs.md │ └── spacing.md ├── pdm.lock └── pyproject.toml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | env: 4 | PY_COLORS: "1" 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ['3.10', '3.x'] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up PDM 14 | uses: pdm-project/setup-pdm@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: pdm sync --dev 19 | - name: Run tests 20 | run: pdm run -v pytest autobean_refactor -k 'not benchmark' --cov autobean_refactor --cov-report xml 21 | - name: Run type checks 22 | run: pdm run -v mypy autobean_refactor 23 | - name: Upload coverage 24 | uses: paambaati/codeclimate-action@v3.2.0 25 | env: 26 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 27 | - name: Run benchmark 28 | run: pdm run -v pytest autobean_refactor -sv -k 'benchmark' 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Set up PDM 12 | uses: pdm-project/setup-pdm@v3 13 | with: 14 | python-version: 3.x 15 | - name: Publish to PyPI 16 | run: pdm publish 17 | env: 18 | PDM_PUBLISH_USERNAME: __token__ 19 | PDM_PUBLISH_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | disallow_untyped_defs = True 3 | ignore_missing_imports = True 4 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.11" 6 | jobs: 7 | post_install: 8 | - pip install pdm 9 | - VIRTUAL_ENV=$(dirname $(dirname $(which python))) pdm install --dev 10 | pre_build: 11 | - pdm run jupyter-book config sphinx docs/ 12 | 13 | python: 14 | install: 15 | - method: pip 16 | path: . 17 | 18 | sphinx: 19 | configuration: docs/conf.py 20 | builder: html 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SEIAROTg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # autobean-refactor 2 | [![build](https://github.com/SEIAROTg/autobean-refactor/actions/workflows/build.yml/badge.svg)](https://github.com/SEIAROTg/autobean-refactor/actions/workflows/build.yml) 3 | [![pypi](https://img.shields.io/pypi/v/autobean-refactor)](https://pypi.org/project/autobean-refactor/) 4 | [![Test Coverage](https://api.codeclimate.com/v1/badges/8acbf50474596bc201ab/test_coverage)](https://codeclimate.com/github/SEIAROTg/autobean-refactor/test_coverage) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/8acbf50474596bc201ab/maintainability)](https://codeclimate.com/github/SEIAROTg/autobean-refactor/maintainability) 6 | [![license](https://img.shields.io/github/license/SEIAROTg/autobean-refactor.svg)](https://github.com/SEIAROTg/autobean-refactor) 7 | 8 | An ergonomic and lossless beancount manipulation library. 9 | 10 | https://autobean-refactor.readthedocs.io/ 11 | -------------------------------------------------------------------------------- /autobean_refactor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SEIAROTg/autobean-refactor/8b0877b695405d95ca140c7fc1930e69fbccc289/autobean_refactor/__init__.py -------------------------------------------------------------------------------- /autobean_refactor/editor.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import contextlib 3 | import glob 4 | import io 5 | import os.path 6 | import pathlib 7 | from typing import Iterator, Optional 8 | from autobean_refactor import parser as parser_lib, models, printer 9 | 10 | 11 | def _get_include_paths(path: str, file: models.File) -> Iterator[str]: 12 | for directive in file.raw_directives: 13 | if not isinstance(directive, models.Include): 14 | continue 15 | matches = glob.glob(os.path.join(os.path.dirname(path), directive.filename), recursive=True) 16 | if not matches: 17 | lineno = directive.token_store.get_position(directive.first_token).line 18 | raise ValueError(f'No files match {directive.filename!r} ({path}:{lineno})') 19 | for match in matches: 20 | yield os.path.normpath(match) 21 | 22 | 23 | class Editor: 24 | 25 | def __init__(self, parser: Optional[parser_lib.Parser] = None) -> None: 26 | self._parser = parser or parser_lib.Parser() 27 | 28 | @contextlib.contextmanager 29 | def edit_file(self, path: os.PathLike) -> Iterator[models.File]: 30 | p = pathlib.Path(path) 31 | text = p.read_text() 32 | file = self._parser.parse(text, models.File) 33 | 34 | yield file 35 | 36 | updated_text = printer.print_model(file, io.StringIO()).getvalue() 37 | if updated_text != text: 38 | p.write_text(updated_text) 39 | 40 | @contextlib.contextmanager 41 | def edit_file_recursive(self, path: os.PathLike) -> Iterator[dict[str, models.File]]: 42 | texts = dict[str, str]() 43 | files = dict[str, models.File]() 44 | queue = collections.deque([os.path.normpath(path)]) 45 | 46 | while queue: 47 | current_path = queue.popleft() 48 | if current_path in texts: 49 | continue 50 | with open(current_path) as f: 51 | texts[current_path] = f.read() 52 | files[current_path] = self._parser.parse(texts[current_path], models.File) 53 | queue.extend(_get_include_paths(current_path, files[current_path])) 54 | 55 | yield files 56 | 57 | for current_path in set(texts) - set(files): 58 | os.unlink(current_path) 59 | for current_path, file in files.items(): 60 | os.makedirs(os.path.dirname(current_path), exist_ok=True) 61 | updated_text = printer.print_model(file, io.StringIO()).getvalue() 62 | if updated_text != texts.get(current_path): 63 | with open(current_path, 'w') as f: 64 | f.write(updated_text) 65 | -------------------------------------------------------------------------------- /autobean_refactor/meta_models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SEIAROTg/autobean-refactor/8b0877b695405d95ca140c7fc1930e69fbccc289/autobean_refactor/meta_models/__init__.py -------------------------------------------------------------------------------- /autobean_refactor/meta_models/base.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import enum 3 | from typing import Any, Optional 4 | 5 | 6 | class MetaModel: 7 | pass 8 | 9 | 10 | class BlockCommentable: 11 | pass 12 | 13 | 14 | class Inline: 15 | pass 16 | 17 | 18 | class Floating(enum.Enum): 19 | LEFT = enum.auto() 20 | RIGHT = enum.auto() 21 | 22 | 23 | @dataclasses.dataclass(frozen=True) 24 | class field: 25 | is_label: bool = False 26 | floating: Optional[Floating] = None 27 | define_as: Optional[str] = None 28 | type_alias: Optional[str] = None 29 | has_circular_dep: bool = False 30 | is_optional: bool = False 31 | is_keyword_only: bool = False 32 | default_value: Any = None 33 | separators: Optional[tuple[str, ...]] = None 34 | separators_before: Optional[tuple[str, ...]] = None 35 | indented: bool = False 36 | has_interleaving_comments: bool = False 37 | -------------------------------------------------------------------------------- /autobean_refactor/modelgen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SEIAROTg/autobean-refactor/8b0877b695405d95ca140c7fc1930e69fbccc289/autobean_refactor/modelgen/__init__.py -------------------------------------------------------------------------------- /autobean_refactor/modelgen/generate.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import pathlib 3 | import sys 4 | from typing import Type 5 | import mako.template # type: ignore[import] 6 | import stringcase # type: ignore[import] 7 | from autobean_refactor.meta_models.base import MetaModel 8 | from autobean_refactor.meta_models import meta_models 9 | from autobean_refactor.modelgen import descriptor 10 | 11 | 12 | _CURRENT_DIR = pathlib.Path(__file__).parent 13 | _RAW_MODEL_TMPL = mako.template.Template((_CURRENT_DIR / 'raw_model.mako').read_text()) 14 | 15 | 16 | def collect_meta_models() -> list[Type[MetaModel]]: 17 | rets = [] 18 | for _, meta_model in inspect.getmembers(meta_models, inspect.isclass): 19 | if issubclass(meta_model, MetaModel) and meta_model is not MetaModel: 20 | rets.append(meta_model) 21 | return rets 22 | 23 | 24 | def generate_raw_models(meta_model: Type[MetaModel]) -> str: 25 | return _RAW_MODEL_TMPL.render(model=descriptor.build_descriptor(meta_model)) 26 | 27 | 28 | def raw_model_path(meta_model: Type[MetaModel]) -> pathlib.Path: 29 | filename = f'{stringcase.snakecase(meta_model.__name__)}.py' 30 | return _CURRENT_DIR / '..' / 'models' / 'generated' / filename 31 | 32 | 33 | if __name__ == '__main__': 34 | _, target = sys.argv 35 | all_meta_models = collect_meta_models() 36 | generated = 0 37 | for meta_model in all_meta_models: 38 | if target == 'all' or target == meta_model.__name__: 39 | raw_model_path(meta_model).write_text(generate_raw_models(meta_model)) 40 | print(f'Generated {meta_model.__name__}.') 41 | generated += 1 42 | print(f'Generated {generated} models.') 43 | -------------------------------------------------------------------------------- /autobean_refactor/modelgen/modelgen_test.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import os 3 | from typing import Type 4 | import pytest 5 | from autobean_refactor.meta_models import base 6 | from . import generate 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 'meta_model', generate.collect_meta_models(), 11 | ) 12 | def test_raw_model_in_sync(meta_model: Type[base.MetaModel]) -> None: 13 | expected = generate.generate_raw_models(meta_model) 14 | if not generate.raw_model_path(meta_model).exists(): 15 | pytest.fail(f'Meta model {meta_model.__name__} is not generated.') 16 | actual = generate.raw_model_path(meta_model).read_text() 17 | assert actual == expected, f'{meta_model.__name__} is out of sync.' 18 | 19 | 20 | def test_raw_model_extra_files() -> None: 21 | files_by_dir = collections.defaultdict(set) 22 | for meta_model in generate.collect_meta_models(): 23 | dirname, filename = os.path.split(generate.raw_model_path(meta_model)) 24 | files_by_dir[dirname].add(filename) 25 | for dirname, filenames in files_by_dir.items(): 26 | actual_filenames = set(os.listdir(dirname)) 27 | extra_files = actual_filenames - filenames - {'__init__.py', '__pycache__'} 28 | assert not extra_files, f'Found extra files {extra_files}.' 29 | -------------------------------------------------------------------------------- /autobean_refactor/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .internal import TOKEN_MODELS, TREE_MODELS 2 | from .base import * 3 | from .account import * 4 | from .amount import * 5 | from .balance import * 6 | from .block_comment import * 7 | from .bool import * 8 | from .close import * 9 | from .commodity import * 10 | from .compound_amount import * 11 | from .cost import * 12 | from .cost_component import * 13 | from .cost_spec import * 14 | from .currency import * 15 | from .custom import * 16 | from .date import * 17 | from .document import * 18 | from .escaped_string import * 19 | from .event import * 20 | from .file import * 21 | from .ignored import * 22 | from .ignored_line import * 23 | from .include import * 24 | from .inline_comment import * 25 | from .link import * 26 | from .meta_item import * 27 | from .meta_key import * 28 | from .meta_value import * 29 | from .note import * 30 | from .null import * 31 | from .number import * 32 | from .number_add_expr import * 33 | from .number_atom_expr import * 34 | from .number_expr import * 35 | from .number_mul_expr import * 36 | from .number_paren_expr import * 37 | from .number_unary_expr import * 38 | from .open import * 39 | from .option import * 40 | from .pad import * 41 | from .plugin import * 42 | from .popmeta import * 43 | from .poptag import * 44 | from .posting import * 45 | from .posting_flag import * 46 | from .price import * 47 | from .punctuation import * 48 | from .pushmeta import * 49 | from .pushtag import * 50 | from .query import * 51 | from .tag import * 52 | from .tolerance import * 53 | from .total_price import * 54 | from .transaction import * 55 | from .transaction_flag import * 56 | from .unit_price import * 57 | -------------------------------------------------------------------------------- /autobean_refactor/models/account.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class Account(internal.SimpleSingleValueRawTokenModel): 6 | """Account (e.g. `Assets:Foo`).""" 7 | RULE = 'ACCOUNT' 8 | -------------------------------------------------------------------------------- /autobean_refactor/models/amount.py: -------------------------------------------------------------------------------- 1 | from .generated.amount import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/balance.py: -------------------------------------------------------------------------------- 1 | from .generated.balance import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/block_comment.py: -------------------------------------------------------------------------------- 1 | from typing import final 2 | from typing_extensions import Self 3 | from . import base 4 | from .internal import registry as _registry 5 | from .internal import spacing_accessors as _spacing_accessors 6 | from .internal import value_properties as _value_properties 7 | 8 | 9 | def _splitlines(s: str) -> list[str]: 10 | lines = s.splitlines(keepends=True) 11 | if not lines or lines[-1].endswith('\n'): 12 | lines.append('') 13 | return lines 14 | 15 | 16 | @_registry.token_model 17 | class BlockComment(base.RawTokenModel, _value_properties.RWValueWithIndent[str], _spacing_accessors.SpacingAccessorsMixin): 18 | """Comment that occupies one or more whole lines.""" 19 | RULE = 'BLOCK_COMMENT' 20 | 21 | @final 22 | def __init__(self, raw_text: str, indent: str, value: str, *, claimed: bool = True) -> None: 23 | super().__init__(raw_text) 24 | self._value = value 25 | self._indent = indent 26 | self._claimed = claimed 27 | 28 | @property 29 | def raw_text(self) -> str: 30 | return super().raw_text 31 | 32 | @raw_text.setter 33 | def raw_text(self, raw_text: str) -> None: 34 | self._update_raw_text(raw_text) 35 | self._indent, self._value = self._parse_value(raw_text) 36 | 37 | @property 38 | def value(self) -> str: 39 | return self._value 40 | 41 | @value.setter 42 | def value(self, value: str) -> None: 43 | self._value = value 44 | self._update_raw_text(self._format_value(self._indent, value)) 45 | 46 | @property 47 | def indent(self) -> str: 48 | return self._indent 49 | 50 | @indent.setter 51 | def indent(self, indent: str) -> None: 52 | self._indent = indent 53 | self._update_raw_text(self._format_value(indent, self._value)) 54 | 55 | @property 56 | def claimed(self) -> bool: 57 | return self._claimed 58 | 59 | @claimed.setter 60 | def claimed(self, claimed: bool) -> None: 61 | self._claimed = claimed 62 | 63 | @classmethod 64 | def from_value(cls, value: str, *, indent: str = '') -> Self: 65 | return cls(cls._format_value(indent, value), indent, value) 66 | 67 | @classmethod 68 | def from_raw_text(cls, raw_text: str) -> Self: 69 | indent, value = cls._parse_value(raw_text) 70 | return cls(raw_text, indent, value) 71 | 72 | @classmethod 73 | def _parse_value(cls, raw_text: str) -> tuple[str, str]: 74 | indents, values = zip(*( 75 | tuple(line.split(';', maxsplit=1)) 76 | for line in _splitlines(raw_text) 77 | )) 78 | lines = list(values) 79 | spaced = all(not line.rstrip('\r\n') or line.startswith(' ') for line in lines) 80 | if spaced: 81 | lines = [line.removeprefix(' ') for line in lines] 82 | 83 | return indents[0], ''.join(lines) 84 | 85 | @classmethod 86 | def _format_value(cls, indent: str, value: str) -> str: 87 | return ''.join( 88 | f'{indent}; {line}' if line.rstrip('\r\n') else f'{indent};{line}' 89 | for line in _splitlines(value) 90 | ) 91 | 92 | def _clone(self: 'BlockComment') -> 'BlockComment': 93 | return type(self)(self.raw_text, self.indent, self.value, claimed=self.claimed) 94 | -------------------------------------------------------------------------------- /autobean_refactor/models/bool.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class Bool(internal.SingleValueRawTokenModel[bool]): 6 | """Boolean value. Contains literal `TRUE` or `FALSE`.""" 7 | RULE = 'BOOL' 8 | 9 | @classmethod 10 | def _parse_value(cls, raw_text: str) -> bool: 11 | return { 12 | 'TRUE': True, 13 | 'FALSE': False, 14 | }[raw_text] 15 | 16 | @classmethod 17 | def _format_value(cls, value: bool) -> str: 18 | return str(value).upper() 19 | -------------------------------------------------------------------------------- /autobean_refactor/models/close.py: -------------------------------------------------------------------------------- 1 | from .generated.close import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/commodity.py: -------------------------------------------------------------------------------- 1 | from .generated.commodity import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/compound_amount.py: -------------------------------------------------------------------------------- 1 | from .generated.compound_amount import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/cost.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | from .generated import unit_cost, total_cost 3 | from .generated.unit_cost import LeftBrace, RightBrace 4 | from .generated.total_cost import DblLeftBrace, DblRightBrace 5 | 6 | 7 | @internal.tree_model 8 | class UnitCost(unit_cost.UnitCost): 9 | def into_total_cost(self) -> 'TotalCost': 10 | dbl_left_brace = DblLeftBrace.from_default() 11 | dbl_right_brace = DblRightBrace.from_default() 12 | self.token_store.replace(self._left_brace, dbl_left_brace) 13 | self.token_store.replace(self._right_brace, dbl_right_brace) 14 | return TotalCost(self.token_store, dbl_left_brace, self._components, dbl_right_brace) 15 | 16 | 17 | @internal.tree_model 18 | class TotalCost(total_cost.TotalCost): 19 | def into_unit_cost(self) -> UnitCost: 20 | left_brace = LeftBrace.from_default() 21 | right_brace = RightBrace.from_default() 22 | self.token_store.replace(self._dbl_left_brace, left_brace) 23 | self.token_store.replace(self._dbl_right_brace, right_brace) 24 | return UnitCost(self.token_store, left_brace, self._components, right_brace) 25 | -------------------------------------------------------------------------------- /autobean_refactor/models/cost_component.py: -------------------------------------------------------------------------------- 1 | from .date import Date 2 | from .punctuation import Asterisk 3 | from .escaped_string import EscapedString 4 | from .currency import Currency 5 | from .number_expr import NumberExpr 6 | from .amount import Amount 7 | from .compound_amount import CompoundAmount 8 | 9 | CostComponent = Date | Asterisk | EscapedString | Currency | NumberExpr | Amount | CompoundAmount 10 | -------------------------------------------------------------------------------- /autobean_refactor/models/currency.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class Currency(internal.SimpleSingleValueRawTokenModel): 6 | """Currency (e.g. `USD`).""" 7 | RULE = 'CURRENCY' 8 | -------------------------------------------------------------------------------- /autobean_refactor/models/date.py: -------------------------------------------------------------------------------- 1 | import re 2 | import datetime 3 | from . import internal 4 | 5 | 6 | @internal.token_model 7 | class Date(internal.SingleValueRawTokenModel[datetime.date]): 8 | """Date (e.g. `2000-01-01`).""" 9 | RULE = 'DATE' 10 | 11 | @classmethod 12 | def _parse_value(cls, raw_text: str) -> datetime.date: 13 | y, m, d = map(int, re.split('[-/]', raw_text)) 14 | return datetime.date(y, m, d) 15 | 16 | @classmethod 17 | def _format_value(cls, value: datetime.date) -> str: 18 | return value.strftime('%Y-%m-%d') 19 | -------------------------------------------------------------------------------- /autobean_refactor/models/document.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import itertools 3 | from typing import Iterable, Mapping, Optional 4 | from typing_extensions import Self 5 | from . import internal, meta_item_internal 6 | from .date import Date 7 | from .account import Account 8 | from .block_comment import BlockComment 9 | from .escaped_string import EscapedString 10 | from .inline_comment import InlineComment 11 | from .link import Link 12 | from .tag import Tag 13 | from .generated import document 14 | from .generated.document import DocumentLabel 15 | from .meta_value import MetaRawValue, MetaValue 16 | 17 | 18 | @internal.tree_model 19 | class Document(document.Document): 20 | tags = internal.repeated_string_property(document.Document.raw_tags_links, Tag) 21 | links = internal.repeated_string_property(document.Document.raw_tags_links, Link) 22 | 23 | @classmethod 24 | def from_value( 25 | cls, 26 | date: datetime.date, 27 | account: str, 28 | filename: str, 29 | *, 30 | tags: Iterable[str] = (), 31 | links: Iterable[str] = (), 32 | leading_comment: Optional[str] = None, 33 | inline_comment: Optional[str] = None, 34 | meta: Optional[Mapping[str, MetaValue | MetaRawValue]] = None, 35 | trailing_comment: Optional[str] = None, 36 | indent_by: str = ' ', 37 | ) -> Self: 38 | return cls.from_children( 39 | date=Date.from_value(date), 40 | account=Account.from_value(account), 41 | filename=EscapedString.from_value(filename), 42 | tags_links=itertools.chain(map(Tag.from_value, tags), map(Link.from_value, links)), 43 | leading_comment=BlockComment.from_value(leading_comment) if leading_comment is not None else None, 44 | inline_comment=InlineComment.from_value(inline_comment) if inline_comment is not None else None, 45 | meta=meta_item_internal.from_mapping(meta, indent=indent_by) if meta is not None else (), 46 | trailing_comment=BlockComment.from_value(trailing_comment) if trailing_comment is not None else None, 47 | indent_by=indent_by, 48 | ) 49 | -------------------------------------------------------------------------------- /autobean_refactor/models/escaped_string.py: -------------------------------------------------------------------------------- 1 | import re 2 | from . import internal 3 | 4 | 5 | @internal.token_model 6 | class EscapedString(internal.SingleValueRawTokenModel[str]): 7 | """String (e.g. `"foo"`).""" 8 | RULE = 'ESCAPED_STRING' 9 | # See: https://github.com/beancount/beancount/blob/d841487ccdda04c159de86b1186e7c2ea997a3e2/beancount/parser/tokens.c#L102 10 | __ESCAPE_MAP = { 11 | '\n': 'n', 12 | '\t': 't', 13 | '\r': 'r', 14 | '\f': 'f', 15 | '\b': 'b', 16 | '"': '"', 17 | '\\': '\\', 18 | } 19 | __ESCAPE_PATTERN = re.compile(r'[\\"]') 20 | __ESCAPE_PATTERN_AGGRESSIVE = re.compile( 21 | '|'.join(map(re.escape, __ESCAPE_MAP.keys()))) 22 | __UNESCAPE_MAP = {value: key for key, value in __ESCAPE_MAP.items()} 23 | __UNESCAPE_PATTERN = re.compile(r'\\(.)') 24 | 25 | @classmethod 26 | def _parse_value(cls, raw_text: str) -> str: 27 | return cls.unescape(raw_text[1:-1]) 28 | 29 | @classmethod 30 | def _format_value(cls, value: str) -> str: 31 | return f'"{cls.escape(value)}"' 32 | 33 | @classmethod 34 | def escape(cls, s: str, aggressive: bool = False) -> str: 35 | pattern = cls.__ESCAPE_PATTERN_AGGRESSIVE if aggressive else cls.__ESCAPE_PATTERN 36 | return re.sub( 37 | pattern, 38 | lambda c: '\\' + cls.__ESCAPE_MAP[c.group(0)], 39 | s) 40 | 41 | @classmethod 42 | def unescape(cls, s: str) -> str: 43 | return re.sub( 44 | cls.__UNESCAPE_PATTERN, 45 | lambda c: cls.__UNESCAPE_MAP.get(c.group(1), c.group(1)), 46 | s) 47 | -------------------------------------------------------------------------------- /autobean_refactor/models/event.py: -------------------------------------------------------------------------------- 1 | from .generated.event import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/file.py: -------------------------------------------------------------------------------- 1 | from . import base 2 | from . import internal 3 | from .generated import file 4 | from .generated.file import Directive 5 | 6 | 7 | @internal.tree_model 8 | class File(file.File): 9 | 10 | @property 11 | def first_token(self) -> base.RawTokenModel: 12 | return self._token_store.get_first() or self._directives.first_token 13 | 14 | @property 15 | def last_token(self) -> base.RawTokenModel: 16 | return self._token_store.get_last() or self._directives.last_token 17 | -------------------------------------------------------------------------------- /autobean_refactor/models/generated/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SEIAROTg/autobean-refactor/8b0877b695405d95ca140c7fc1930e69fbccc289/autobean_refactor/models/generated/__init__.py -------------------------------------------------------------------------------- /autobean_refactor/models/generated/amount.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT 2 | # This file is automatically generated by autobean_refactor.modelgen. 3 | 4 | import decimal 5 | from typing import Iterator, final 6 | from typing_extensions import Self 7 | from .. import base, internal 8 | from ..currency import Currency 9 | from ..number_expr import NumberExpr 10 | from ..spacing import Whitespace 11 | 12 | 13 | @internal.tree_model 14 | class Amount(base.RawTreeModel, internal.SpacingAccessorsMixin): 15 | """Amount (e.g. `100.00 USD`).""" 16 | RULE = 'amount' 17 | INLINE = True 18 | 19 | _number = internal.required_field[NumberExpr]() 20 | _currency = internal.required_field[Currency]() 21 | 22 | raw_number = internal.required_node_property(_number) 23 | raw_currency = internal.required_node_property(_currency) 24 | 25 | number = internal.required_value_property(raw_number) 26 | currency = internal.required_value_property(raw_currency) 27 | 28 | @final 29 | def __init__( 30 | self, 31 | token_store: base.TokenStore, 32 | number: NumberExpr, 33 | currency: Currency, 34 | ): 35 | super().__init__(token_store) 36 | self._number = number 37 | self._currency = currency 38 | 39 | @property 40 | def first_token(self) -> base.RawTokenModel: 41 | return self._number.first_token 42 | 43 | @property 44 | def last_token(self) -> base.RawTokenModel: 45 | return self._currency.last_token 46 | 47 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 48 | return type(self)( 49 | token_store, 50 | type(self)._number.clone(self._number, token_store, token_transformer), 51 | type(self)._currency.clone(self._currency, token_store, token_transformer), 52 | ) 53 | 54 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 55 | self._token_store = token_store 56 | self._number = type(self)._number.reattach(self._number, token_store, token_transformer) 57 | self._currency = type(self)._currency.reattach(self._currency, token_store, token_transformer) 58 | 59 | def _eq(self, other: base.RawTreeModel) -> bool: 60 | return ( 61 | isinstance(other, Amount) 62 | and self._number == other._number 63 | and self._currency == other._currency 64 | ) 65 | 66 | @classmethod 67 | def from_children( 68 | cls, 69 | number: NumberExpr, 70 | currency: Currency, 71 | ) -> Self: 72 | tokens = [ 73 | *number.detach(), 74 | Whitespace.from_default(), 75 | *currency.detach(), 76 | ] 77 | token_store = base.TokenStore.from_tokens(tokens) 78 | cls._number.reattach(number, token_store) 79 | cls._currency.reattach(currency, token_store) 80 | return cls(token_store, number, currency) 81 | 82 | @classmethod 83 | def from_value( 84 | cls, 85 | number: decimal.Decimal, 86 | currency: str, 87 | ) -> Self: 88 | return cls.from_children( 89 | number=NumberExpr.from_value(number), 90 | currency=Currency.from_value(currency), 91 | ) 92 | 93 | def auto_claim_comments(self) -> None: 94 | type(self)._currency.auto_claim_comments(self._currency) 95 | type(self)._number.auto_claim_comments(self._number) 96 | 97 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 98 | yield from type(self)._number.iter_children_formatted(self._number, False) 99 | yield Whitespace.from_default(), False 100 | yield from type(self)._currency.iter_children_formatted(self._currency, False) 101 | -------------------------------------------------------------------------------- /autobean_refactor/models/generated/cost_spec.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT 2 | # This file is automatically generated by autobean_refactor.modelgen. 3 | 4 | from typing import Iterator, final 5 | from typing_extensions import Self 6 | from .. import base, internal 7 | from ..cost import TotalCost, UnitCost 8 | 9 | 10 | @internal.tree_model 11 | class CostSpec(base.RawTreeModel, internal.SpacingAccessorsMixin): 12 | """Unit cost or total cost.""" 13 | RULE = 'cost_spec' 14 | INLINE = True 15 | 16 | _cost = internal.required_field[TotalCost | UnitCost]() 17 | 18 | raw_cost = internal.required_node_property[TotalCost | UnitCost](_cost) 19 | 20 | @final 21 | def __init__( 22 | self, 23 | token_store: base.TokenStore, 24 | cost: TotalCost | UnitCost, 25 | ): 26 | super().__init__(token_store) 27 | self._cost = cost 28 | 29 | @property 30 | def first_token(self) -> base.RawTokenModel: 31 | return self._cost.first_token 32 | 33 | @property 34 | def last_token(self) -> base.RawTokenModel: 35 | return self._cost.last_token 36 | 37 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 38 | return type(self)( 39 | token_store, 40 | type(self)._cost.clone(self._cost, token_store, token_transformer), 41 | ) 42 | 43 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 44 | self._token_store = token_store 45 | self._cost = type(self)._cost.reattach(self._cost, token_store, token_transformer) 46 | 47 | def _eq(self, other: base.RawTreeModel) -> bool: 48 | return ( 49 | isinstance(other, CostSpec) 50 | and self._cost == other._cost 51 | ) 52 | 53 | @classmethod 54 | def from_children( 55 | cls, 56 | cost: TotalCost | UnitCost, 57 | ) -> Self: 58 | tokens = [ 59 | *cost.detach(), 60 | ] 61 | token_store = base.TokenStore.from_tokens(tokens) 62 | cls._cost.reattach(cost, token_store) 63 | return cls(token_store, cost) 64 | 65 | def auto_claim_comments(self) -> None: 66 | type(self)._cost.auto_claim_comments(self._cost) 67 | 68 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 69 | yield from type(self)._cost.iter_children_formatted(self._cost, False) 70 | -------------------------------------------------------------------------------- /autobean_refactor/models/generated/file.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT 2 | # This file is automatically generated by autobean_refactor.modelgen. 3 | 4 | from typing import Iterable, Iterator, final, get_args 5 | from typing_extensions import Self 6 | from .. import base, internal 7 | from ..balance import Balance 8 | from ..block_comment import BlockComment 9 | from ..close import Close 10 | from ..commodity import Commodity 11 | from ..custom import Custom 12 | from ..document import Document 13 | from ..event import Event 14 | from ..ignored_line import IgnoredLine 15 | from ..include import Include 16 | from ..note import Note 17 | from ..open import Open 18 | from ..option import Option 19 | from ..pad import Pad 20 | from ..plugin import Plugin 21 | from ..popmeta import Popmeta 22 | from ..poptag import Poptag 23 | from ..price import Price 24 | from ..pushmeta import Pushmeta 25 | from ..pushtag import Pushtag 26 | from ..query import Query 27 | from ..spacing import Newline 28 | from ..transaction import Transaction 29 | 30 | Directive = Balance | Close | Commodity | Custom | Document | Event | IgnoredLine | Include | Note | Open | Option | Pad | Plugin | Popmeta | Poptag | Price | Pushmeta | Pushtag | Query | Transaction 31 | 32 | 33 | @internal.tree_model 34 | class File(base.RawTreeModel, internal.SpacingAccessorsMixin): 35 | """Contains everything in a file.""" 36 | RULE = 'file' 37 | 38 | _directives = internal.repeated_field[Directive | BlockComment](separators=(Newline.from_default(), Newline.from_default()), separators_before=()) 39 | 40 | raw_directives_with_comments = internal.repeated_node_with_interleaving_comments_property[Directive | BlockComment](_directives) 41 | raw_directives = internal.repeated_filtered_node_property[Directive](raw_directives_with_comments, get_args(Directive)) 42 | 43 | directives = raw_directives 44 | 45 | @final 46 | def __init__( 47 | self, 48 | token_store: base.TokenStore, 49 | repeated_directives: internal.Repeated[Directive | BlockComment], 50 | ): 51 | super().__init__(token_store) 52 | self._directives = repeated_directives 53 | 54 | @property 55 | def first_token(self) -> base.RawTokenModel: 56 | return self._directives.first_token 57 | 58 | @property 59 | def last_token(self) -> base.RawTokenModel: 60 | return self._directives.last_token 61 | 62 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 63 | return type(self)( 64 | token_store, 65 | type(self)._directives.clone(self._directives, token_store, token_transformer), 66 | ) 67 | 68 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 69 | self._token_store = token_store 70 | self._directives = type(self)._directives.reattach(self._directives, token_store, token_transformer) 71 | 72 | def _eq(self, other: base.RawTreeModel) -> bool: 73 | return ( 74 | isinstance(other, File) 75 | and self._directives == other._directives 76 | ) 77 | 78 | @classmethod 79 | def from_children( 80 | cls, 81 | directives: Iterable[Directive | BlockComment], 82 | ) -> Self: 83 | repeated_directives = cls._directives.create_repeated(directives) 84 | tokens = [ 85 | *cls._directives.detach_with_separators(repeated_directives), 86 | ] 87 | token_store = base.TokenStore.from_tokens(tokens) 88 | cls._directives.reattach(repeated_directives, token_store) 89 | return cls(token_store, repeated_directives) 90 | 91 | @classmethod 92 | def from_value( 93 | cls, 94 | directives: Iterable[Directive], 95 | ) -> Self: 96 | return cls.from_children( 97 | directives=directives, 98 | ) 99 | 100 | def auto_claim_comments(self) -> None: 101 | self.raw_directives_with_comments.auto_claim_comments() 102 | 103 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 104 | yield from type(self)._directives.iter_children_formatted(self._directives, False) 105 | -------------------------------------------------------------------------------- /autobean_refactor/models/generated/number_expr.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT 2 | # This file is automatically generated by autobean_refactor.modelgen. 3 | 4 | from typing import Iterator, TYPE_CHECKING, final 5 | from typing_extensions import Self 6 | from .. import base, internal 7 | if TYPE_CHECKING: 8 | from ..number_add_expr import NumberAddExpr 9 | 10 | 11 | @internal.tree_model 12 | class NumberExpr(base.RawTreeModel, internal.SpacingAccessorsMixin): 13 | """Number expression.""" 14 | RULE = 'number_expr' 15 | INLINE = True 16 | 17 | _number_add_expr = internal.required_field['NumberAddExpr']() 18 | 19 | raw_number_add_expr = internal.required_node_property(_number_add_expr) 20 | 21 | @final 22 | def __init__( 23 | self, 24 | token_store: base.TokenStore, 25 | number_add_expr: 'NumberAddExpr', 26 | ): 27 | super().__init__(token_store) 28 | self._number_add_expr = number_add_expr 29 | 30 | @property 31 | def first_token(self) -> base.RawTokenModel: 32 | return self._number_add_expr.first_token 33 | 34 | @property 35 | def last_token(self) -> base.RawTokenModel: 36 | return self._number_add_expr.last_token 37 | 38 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 39 | return type(self)( 40 | token_store, 41 | type(self)._number_add_expr.clone(self._number_add_expr, token_store, token_transformer), 42 | ) 43 | 44 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 45 | self._token_store = token_store 46 | self._number_add_expr = type(self)._number_add_expr.reattach(self._number_add_expr, token_store, token_transformer) 47 | 48 | def _eq(self, other: base.RawTreeModel) -> bool: 49 | return ( 50 | isinstance(other, NumberExpr) 51 | and self._number_add_expr == other._number_add_expr 52 | ) 53 | 54 | @classmethod 55 | def from_children( 56 | cls, 57 | number_add_expr: 'NumberAddExpr', 58 | ) -> Self: 59 | tokens = [ 60 | *number_add_expr.detach(), 61 | ] 62 | token_store = base.TokenStore.from_tokens(tokens) 63 | cls._number_add_expr.reattach(number_add_expr, token_store) 64 | return cls(token_store, number_add_expr) 65 | 66 | def auto_claim_comments(self) -> None: 67 | type(self)._number_add_expr.auto_claim_comments(self._number_add_expr) 68 | 69 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 70 | yield from type(self)._number_add_expr.iter_children_formatted(self._number_add_expr, False) 71 | -------------------------------------------------------------------------------- /autobean_refactor/models/generated/number_paren_expr.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT 2 | # This file is automatically generated by autobean_refactor.modelgen. 3 | 4 | from typing import Iterator, TYPE_CHECKING, final 5 | from typing_extensions import Self 6 | from .. import base, internal 7 | if TYPE_CHECKING: 8 | from ..number_add_expr import NumberAddExpr 9 | 10 | 11 | @internal.token_model 12 | class LeftParen(internal.SimpleDefaultRawTokenModel): 13 | """Contains literal `(`.""" 14 | RULE = 'LEFT_PAREN' 15 | DEFAULT = '(' 16 | 17 | 18 | @internal.token_model 19 | class RightParen(internal.SimpleDefaultRawTokenModel): 20 | """Contains literal `)`.""" 21 | RULE = 'RIGHT_PAREN' 22 | DEFAULT = ')' 23 | 24 | 25 | @internal.tree_model 26 | class NumberParenExpr(base.RawTreeModel, internal.SpacingAccessorsMixin): 27 | """Parentheses-enclosed number expression (e.g. `(42.00)`).""" 28 | RULE = 'number_paren_expr' 29 | INLINE = True 30 | 31 | _left_paren = internal.required_field[LeftParen]() 32 | _inner_expr = internal.required_field['NumberAddExpr']() 33 | _right_paren = internal.required_field[RightParen]() 34 | 35 | raw_inner_expr = internal.required_node_property(_inner_expr) 36 | 37 | @final 38 | def __init__( 39 | self, 40 | token_store: base.TokenStore, 41 | left_paren: LeftParen, 42 | inner_expr: 'NumberAddExpr', 43 | right_paren: RightParen, 44 | ): 45 | super().__init__(token_store) 46 | self._left_paren = left_paren 47 | self._inner_expr = inner_expr 48 | self._right_paren = right_paren 49 | 50 | @property 51 | def first_token(self) -> base.RawTokenModel: 52 | return self._left_paren.first_token 53 | 54 | @property 55 | def last_token(self) -> base.RawTokenModel: 56 | return self._right_paren.last_token 57 | 58 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 59 | return type(self)( 60 | token_store, 61 | type(self)._left_paren.clone(self._left_paren, token_store, token_transformer), 62 | type(self)._inner_expr.clone(self._inner_expr, token_store, token_transformer), 63 | type(self)._right_paren.clone(self._right_paren, token_store, token_transformer), 64 | ) 65 | 66 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 67 | self._token_store = token_store 68 | self._left_paren = type(self)._left_paren.reattach(self._left_paren, token_store, token_transformer) 69 | self._inner_expr = type(self)._inner_expr.reattach(self._inner_expr, token_store, token_transformer) 70 | self._right_paren = type(self)._right_paren.reattach(self._right_paren, token_store, token_transformer) 71 | 72 | def _eq(self, other: base.RawTreeModel) -> bool: 73 | return ( 74 | isinstance(other, NumberParenExpr) 75 | and self._left_paren == other._left_paren 76 | and self._inner_expr == other._inner_expr 77 | and self._right_paren == other._right_paren 78 | ) 79 | 80 | @classmethod 81 | def from_children( 82 | cls, 83 | inner_expr: 'NumberAddExpr', 84 | ) -> Self: 85 | left_paren = LeftParen.from_default() 86 | right_paren = RightParen.from_default() 87 | tokens = [ 88 | *left_paren.detach(), 89 | *inner_expr.detach(), 90 | *right_paren.detach(), 91 | ] 92 | token_store = base.TokenStore.from_tokens(tokens) 93 | cls._left_paren.reattach(left_paren, token_store) 94 | cls._inner_expr.reattach(inner_expr, token_store) 95 | cls._right_paren.reattach(right_paren, token_store) 96 | return cls(token_store, left_paren, inner_expr, right_paren) 97 | 98 | def auto_claim_comments(self) -> None: 99 | type(self)._inner_expr.auto_claim_comments(self._inner_expr) 100 | 101 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 102 | yield from type(self)._left_paren.iter_children_formatted(self._left_paren, False) 103 | yield from type(self)._inner_expr.iter_children_formatted(self._inner_expr, False) 104 | yield from type(self)._right_paren.iter_children_formatted(self._right_paren, False) 105 | -------------------------------------------------------------------------------- /autobean_refactor/models/generated/number_unary_expr.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT 2 | # This file is automatically generated by autobean_refactor.modelgen. 3 | 4 | from typing import Iterator, TYPE_CHECKING, final 5 | from typing_extensions import Self 6 | from .. import base, internal 7 | if TYPE_CHECKING: 8 | from ..number_atom_expr import NumberAtomExpr 9 | 10 | 11 | @internal.token_model 12 | class UnaryOp(internal.SimpleRawTokenModel): 13 | RULE = 'UNARY_OP' 14 | 15 | 16 | @internal.tree_model 17 | class NumberUnaryExpr(base.RawTreeModel, internal.SpacingAccessorsMixin): 18 | """Unary number expression (e.g. `-42.00`).""" 19 | RULE = 'number_unary_expr' 20 | INLINE = True 21 | 22 | _unary_op = internal.required_field[UnaryOp]() 23 | _operand = internal.required_field['NumberAtomExpr']() 24 | 25 | raw_unary_op = internal.required_node_property(_unary_op) 26 | raw_operand = internal.required_node_property(_operand) 27 | 28 | @final 29 | def __init__( 30 | self, 31 | token_store: base.TokenStore, 32 | unary_op: UnaryOp, 33 | operand: 'NumberAtomExpr', 34 | ): 35 | super().__init__(token_store) 36 | self._unary_op = unary_op 37 | self._operand = operand 38 | 39 | @property 40 | def first_token(self) -> base.RawTokenModel: 41 | return self._unary_op.first_token 42 | 43 | @property 44 | def last_token(self) -> base.RawTokenModel: 45 | return self._operand.last_token 46 | 47 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 48 | return type(self)( 49 | token_store, 50 | type(self)._unary_op.clone(self._unary_op, token_store, token_transformer), 51 | type(self)._operand.clone(self._operand, token_store, token_transformer), 52 | ) 53 | 54 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 55 | self._token_store = token_store 56 | self._unary_op = type(self)._unary_op.reattach(self._unary_op, token_store, token_transformer) 57 | self._operand = type(self)._operand.reattach(self._operand, token_store, token_transformer) 58 | 59 | def _eq(self, other: base.RawTreeModel) -> bool: 60 | return ( 61 | isinstance(other, NumberUnaryExpr) 62 | and self._unary_op == other._unary_op 63 | and self._operand == other._operand 64 | ) 65 | 66 | @classmethod 67 | def from_children( 68 | cls, 69 | unary_op: UnaryOp, 70 | operand: 'NumberAtomExpr', 71 | ) -> Self: 72 | tokens = [ 73 | *unary_op.detach(), 74 | *operand.detach(), 75 | ] 76 | token_store = base.TokenStore.from_tokens(tokens) 77 | cls._unary_op.reattach(unary_op, token_store) 78 | cls._operand.reattach(operand, token_store) 79 | return cls(token_store, unary_op, operand) 80 | 81 | def auto_claim_comments(self) -> None: 82 | type(self)._operand.auto_claim_comments(self._operand) 83 | type(self)._unary_op.auto_claim_comments(self._unary_op) 84 | 85 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 86 | yield from type(self)._unary_op.iter_children_formatted(self._unary_op, False) 87 | yield from type(self)._operand.iter_children_formatted(self._operand, False) 88 | -------------------------------------------------------------------------------- /autobean_refactor/models/generated/tolerance.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT 2 | # This file is automatically generated by autobean_refactor.modelgen. 3 | 4 | import decimal 5 | from typing import Iterator, final 6 | from typing_extensions import Self 7 | from .. import base, internal 8 | from ..number_expr import NumberExpr 9 | from ..spacing import Whitespace 10 | 11 | 12 | @internal.token_model 13 | class Tilde(internal.SimpleDefaultRawTokenModel): 14 | """Contains literal `~`.""" 15 | RULE = 'TILDE' 16 | DEFAULT = '~' 17 | 18 | 19 | @internal.tree_model 20 | class Tolerance(base.RawTreeModel, internal.SpacingAccessorsMixin): 21 | """Tolerance (e.g. `~ 0.01`).""" 22 | RULE = 'tolerance' 23 | INLINE = True 24 | 25 | _tilde = internal.required_field[Tilde]() 26 | _number = internal.required_field[NumberExpr]() 27 | 28 | raw_number = internal.required_node_property(_number) 29 | 30 | number = internal.required_value_property(raw_number) 31 | 32 | @final 33 | def __init__( 34 | self, 35 | token_store: base.TokenStore, 36 | tilde: Tilde, 37 | number: NumberExpr, 38 | ): 39 | super().__init__(token_store) 40 | self._tilde = tilde 41 | self._number = number 42 | 43 | @property 44 | def first_token(self) -> base.RawTokenModel: 45 | return self._tilde.first_token 46 | 47 | @property 48 | def last_token(self) -> base.RawTokenModel: 49 | return self._number.last_token 50 | 51 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 52 | return type(self)( 53 | token_store, 54 | type(self)._tilde.clone(self._tilde, token_store, token_transformer), 55 | type(self)._number.clone(self._number, token_store, token_transformer), 56 | ) 57 | 58 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 59 | self._token_store = token_store 60 | self._tilde = type(self)._tilde.reattach(self._tilde, token_store, token_transformer) 61 | self._number = type(self)._number.reattach(self._number, token_store, token_transformer) 62 | 63 | def _eq(self, other: base.RawTreeModel) -> bool: 64 | return ( 65 | isinstance(other, Tolerance) 66 | and self._tilde == other._tilde 67 | and self._number == other._number 68 | ) 69 | 70 | @classmethod 71 | def from_children( 72 | cls, 73 | number: NumberExpr, 74 | ) -> Self: 75 | tilde = Tilde.from_default() 76 | tokens = [ 77 | *tilde.detach(), 78 | Whitespace.from_default(), 79 | *number.detach(), 80 | ] 81 | token_store = base.TokenStore.from_tokens(tokens) 82 | cls._tilde.reattach(tilde, token_store) 83 | cls._number.reattach(number, token_store) 84 | return cls(token_store, tilde, number) 85 | 86 | @classmethod 87 | def from_value( 88 | cls, 89 | number: decimal.Decimal, 90 | ) -> Self: 91 | return cls.from_children( 92 | number=NumberExpr.from_value(number), 93 | ) 94 | 95 | def auto_claim_comments(self) -> None: 96 | type(self)._number.auto_claim_comments(self._number) 97 | 98 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 99 | yield from type(self)._tilde.iter_children_formatted(self._tilde, False) 100 | yield Whitespace.from_default(), False 101 | yield from type(self)._number.iter_children_formatted(self._number, False) 102 | -------------------------------------------------------------------------------- /autobean_refactor/models/generated/total_cost.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT 2 | # This file is automatically generated by autobean_refactor.modelgen. 3 | 4 | from typing import Iterable, Iterator, final 5 | from typing_extensions import Self 6 | from .. import base, internal 7 | from ..cost_component import CostComponent 8 | from ..punctuation import Comma 9 | from ..spacing import Whitespace 10 | 11 | 12 | @internal.token_model 13 | class DblLeftBrace(internal.SimpleDefaultRawTokenModel): 14 | """Contains literal `{{`.""" 15 | RULE = 'DBL_LEFT_BRACE' 16 | DEFAULT = '{{' 17 | 18 | 19 | @internal.token_model 20 | class DblRightBrace(internal.SimpleDefaultRawTokenModel): 21 | """Contains literal `}}`.""" 22 | RULE = 'DBL_RIGHT_BRACE' 23 | DEFAULT = '}}' 24 | 25 | 26 | @internal.tree_model 27 | class TotalCost(base.RawTreeModel, internal.SpacingAccessorsMixin): 28 | """Total cost (e.g. `{{10.00 USD}}`).""" 29 | RULE = 'total_cost' 30 | INLINE = True 31 | 32 | _dbl_left_brace = internal.required_field[DblLeftBrace]() 33 | _components = internal.repeated_field[CostComponent](separators=(Comma.from_default(), Whitespace.from_default()), separators_before=()) 34 | _dbl_right_brace = internal.required_field[DblRightBrace]() 35 | 36 | raw_components = internal.repeated_node_property(_components) 37 | 38 | @final 39 | def __init__( 40 | self, 41 | token_store: base.TokenStore, 42 | dbl_left_brace: DblLeftBrace, 43 | repeated_components: internal.Repeated[CostComponent], 44 | dbl_right_brace: DblRightBrace, 45 | ): 46 | super().__init__(token_store) 47 | self._dbl_left_brace = dbl_left_brace 48 | self._components = repeated_components 49 | self._dbl_right_brace = dbl_right_brace 50 | 51 | @property 52 | def first_token(self) -> base.RawTokenModel: 53 | return self._dbl_left_brace.first_token 54 | 55 | @property 56 | def last_token(self) -> base.RawTokenModel: 57 | return self._dbl_right_brace.last_token 58 | 59 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 60 | return type(self)( 61 | token_store, 62 | type(self)._dbl_left_brace.clone(self._dbl_left_brace, token_store, token_transformer), 63 | type(self)._components.clone(self._components, token_store, token_transformer), 64 | type(self)._dbl_right_brace.clone(self._dbl_right_brace, token_store, token_transformer), 65 | ) 66 | 67 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 68 | self._token_store = token_store 69 | self._dbl_left_brace = type(self)._dbl_left_brace.reattach(self._dbl_left_brace, token_store, token_transformer) 70 | self._components = type(self)._components.reattach(self._components, token_store, token_transformer) 71 | self._dbl_right_brace = type(self)._dbl_right_brace.reattach(self._dbl_right_brace, token_store, token_transformer) 72 | 73 | def _eq(self, other: base.RawTreeModel) -> bool: 74 | return ( 75 | isinstance(other, TotalCost) 76 | and self._dbl_left_brace == other._dbl_left_brace 77 | and self._components == other._components 78 | and self._dbl_right_brace == other._dbl_right_brace 79 | ) 80 | 81 | @classmethod 82 | def from_children( 83 | cls, 84 | components: Iterable[CostComponent], 85 | ) -> Self: 86 | dbl_left_brace = DblLeftBrace.from_default() 87 | repeated_components = cls._components.create_repeated(components) 88 | dbl_right_brace = DblRightBrace.from_default() 89 | tokens = [ 90 | *dbl_left_brace.detach(), 91 | *cls._components.detach_with_separators(repeated_components), 92 | *dbl_right_brace.detach(), 93 | ] 94 | token_store = base.TokenStore.from_tokens(tokens) 95 | cls._dbl_left_brace.reattach(dbl_left_brace, token_store) 96 | cls._components.reattach(repeated_components, token_store) 97 | cls._dbl_right_brace.reattach(dbl_right_brace, token_store) 98 | return cls(token_store, dbl_left_brace, repeated_components, dbl_right_brace) 99 | 100 | def auto_claim_comments(self) -> None: 101 | self.raw_components.auto_claim_comments() 102 | 103 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 104 | yield from type(self)._dbl_left_brace.iter_children_formatted(self._dbl_left_brace, False) 105 | yield from type(self)._components.iter_children_formatted(self._components, False) 106 | yield from type(self)._dbl_right_brace.iter_children_formatted(self._dbl_right_brace, False) 107 | -------------------------------------------------------------------------------- /autobean_refactor/models/generated/unit_cost.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT 2 | # This file is automatically generated by autobean_refactor.modelgen. 3 | 4 | from typing import Iterable, Iterator, final 5 | from typing_extensions import Self 6 | from .. import base, internal 7 | from ..cost_component import CostComponent 8 | from ..punctuation import Comma 9 | from ..spacing import Whitespace 10 | 11 | 12 | @internal.token_model 13 | class LeftBrace(internal.SimpleDefaultRawTokenModel): 14 | """Contains literal `{`.""" 15 | RULE = 'LEFT_BRACE' 16 | DEFAULT = '{' 17 | 18 | 19 | @internal.token_model 20 | class RightBrace(internal.SimpleDefaultRawTokenModel): 21 | """Contains literal `}`.""" 22 | RULE = 'RIGHT_BRACE' 23 | DEFAULT = '}' 24 | 25 | 26 | @internal.tree_model 27 | class UnitCost(base.RawTreeModel, internal.SpacingAccessorsMixin): 28 | """Unit cost (e.g. `{10.00 USD}`).""" 29 | RULE = 'unit_cost' 30 | INLINE = True 31 | 32 | _left_brace = internal.required_field[LeftBrace]() 33 | _components = internal.repeated_field[CostComponent](separators=(Comma.from_default(), Whitespace.from_default()), separators_before=()) 34 | _right_brace = internal.required_field[RightBrace]() 35 | 36 | raw_components = internal.repeated_node_property(_components) 37 | 38 | @final 39 | def __init__( 40 | self, 41 | token_store: base.TokenStore, 42 | left_brace: LeftBrace, 43 | repeated_components: internal.Repeated[CostComponent], 44 | right_brace: RightBrace, 45 | ): 46 | super().__init__(token_store) 47 | self._left_brace = left_brace 48 | self._components = repeated_components 49 | self._right_brace = right_brace 50 | 51 | @property 52 | def first_token(self) -> base.RawTokenModel: 53 | return self._left_brace.first_token 54 | 55 | @property 56 | def last_token(self) -> base.RawTokenModel: 57 | return self._right_brace.last_token 58 | 59 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 60 | return type(self)( 61 | token_store, 62 | type(self)._left_brace.clone(self._left_brace, token_store, token_transformer), 63 | type(self)._components.clone(self._components, token_store, token_transformer), 64 | type(self)._right_brace.clone(self._right_brace, token_store, token_transformer), 65 | ) 66 | 67 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 68 | self._token_store = token_store 69 | self._left_brace = type(self)._left_brace.reattach(self._left_brace, token_store, token_transformer) 70 | self._components = type(self)._components.reattach(self._components, token_store, token_transformer) 71 | self._right_brace = type(self)._right_brace.reattach(self._right_brace, token_store, token_transformer) 72 | 73 | def _eq(self, other: base.RawTreeModel) -> bool: 74 | return ( 75 | isinstance(other, UnitCost) 76 | and self._left_brace == other._left_brace 77 | and self._components == other._components 78 | and self._right_brace == other._right_brace 79 | ) 80 | 81 | @classmethod 82 | def from_children( 83 | cls, 84 | components: Iterable[CostComponent], 85 | ) -> Self: 86 | left_brace = LeftBrace.from_default() 87 | repeated_components = cls._components.create_repeated(components) 88 | right_brace = RightBrace.from_default() 89 | tokens = [ 90 | *left_brace.detach(), 91 | *cls._components.detach_with_separators(repeated_components), 92 | *right_brace.detach(), 93 | ] 94 | token_store = base.TokenStore.from_tokens(tokens) 95 | cls._left_brace.reattach(left_brace, token_store) 96 | cls._components.reattach(repeated_components, token_store) 97 | cls._right_brace.reattach(right_brace, token_store) 98 | return cls(token_store, left_brace, repeated_components, right_brace) 99 | 100 | def auto_claim_comments(self) -> None: 101 | self.raw_components.auto_claim_comments() 102 | 103 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 104 | yield from type(self)._left_brace.iter_children_formatted(self._left_brace, False) 105 | yield from type(self)._components.iter_children_formatted(self._components, False) 106 | yield from type(self)._right_brace.iter_children_formatted(self._right_brace, False) 107 | -------------------------------------------------------------------------------- /autobean_refactor/models/ignored.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class Ignored(internal.SimpleRawTokenModel): 6 | """Ignored line body (see `IgnoredLine`).""" 7 | RULE = 'IGNORED' 8 | -------------------------------------------------------------------------------- /autobean_refactor/models/ignored_line.py: -------------------------------------------------------------------------------- 1 | from .generated.ignored_line import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/include.py: -------------------------------------------------------------------------------- 1 | from .generated.include import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/inline_comment.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class InlineComment(internal.SingleValueRawTokenModel[str]): 6 | """Comment that does not occupies a whole line.""" 7 | RULE = 'INLINE_COMMENT' 8 | 9 | @classmethod 10 | def _parse_value(cls, raw_text: str) -> str: 11 | return raw_text.removeprefix(';').lstrip(' ') 12 | 13 | @classmethod 14 | def _format_value(cls, value: str) -> str: 15 | return f'; {value}' if value else ';' 16 | -------------------------------------------------------------------------------- /autobean_refactor/models/internal/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_property import * 2 | from .fields import * 3 | from .interleaving_comments import * 4 | from .placeholder import * 5 | from .properties import * 6 | from .registry import * 7 | from .repeated import * 8 | from .spacing_accessors import * 9 | from .surrounding_comments import * 10 | from .token_models import * 11 | from .value_properties import * 12 | -------------------------------------------------------------------------------- /autobean_refactor/models/internal/base_property.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Generic, Optional, Type, TypeVar, overload 3 | from typing_extensions import Self 4 | from .. import base 5 | 6 | _U = TypeVar('_U', bound=base.RawTreeModel) 7 | _V = TypeVar('_V') 8 | _V_cov = TypeVar('_V_cov', covariant=True) 9 | 10 | 11 | class base_ro_property(Generic[_V_cov, _U], abc.ABC): 12 | 13 | @abc.abstractmethod 14 | def _get(self, instance: _U) -> _V_cov: 15 | ... 16 | 17 | @overload 18 | def __get__(self, instance: _U, owner: Optional[Type[_U]] = None) -> _V_cov: 19 | ... 20 | 21 | @overload 22 | def __get__(self, instance: None, owner: Type[_U]) -> Self: 23 | ... 24 | 25 | def __get__(self, instance: Optional[_U], owner: Optional[Type[_U]] = None) -> _V_cov | Self: 26 | del owner 27 | if instance is None: 28 | return self 29 | return self._get(instance) 30 | 31 | 32 | class base_rw_property(base_ro_property[_V, _U]): 33 | 34 | @abc.abstractmethod 35 | def __set__(self, instance: _U, value: _V) -> None: 36 | ... 37 | -------------------------------------------------------------------------------- /autobean_refactor/models/internal/base_token_models.py: -------------------------------------------------------------------------------- 1 | 2 | import abc 3 | from typing import TypeVar, final 4 | from typing_extensions import Self 5 | from .. import base 6 | from .value_properties import RWValue 7 | 8 | _V = TypeVar('_V') 9 | 10 | 11 | class SimpleRawTokenModel(base.RawTokenModel): 12 | @final 13 | def __init__(self, raw_text: str) -> None: 14 | super().__init__(raw_text) 15 | 16 | def _clone(self) -> Self: 17 | return type(self)(self.raw_text) 18 | 19 | 20 | class SingleValueRawTokenModel(base.RawTokenModel, RWValue[_V]): 21 | @final 22 | def __init__(self, raw_text: str, value: _V) -> None: 23 | super().__init__(raw_text) 24 | self._value = value 25 | 26 | @classmethod 27 | def from_raw_text(cls, raw_text: str) -> Self: 28 | return cls(raw_text, cls._parse_value(raw_text)) 29 | 30 | @classmethod 31 | def from_value(cls, value: _V) -> Self: 32 | return cls(cls._format_value(value), value) 33 | 34 | @property 35 | def raw_text(self) -> str: 36 | return super().raw_text 37 | 38 | @raw_text.setter 39 | def raw_text(self, raw_text: str) -> None: 40 | self._update_raw_text(raw_text) 41 | self._value = self._parse_value(raw_text) 42 | 43 | @property 44 | def value(self) -> _V: 45 | return self._value 46 | 47 | @value.setter 48 | def value(self, value: _V) -> None: 49 | self._value = value 50 | self._raw_text = self._format_value(value) 51 | 52 | @classmethod 53 | @abc.abstractmethod 54 | def _parse_value(cls, raw_text: str) -> _V: 55 | pass 56 | 57 | @classmethod 58 | @abc.abstractmethod 59 | def _format_value(cls, value: _V) -> str: 60 | pass 61 | 62 | def _clone(self) -> Self: 63 | return type(self)(self.raw_text, self.value) 64 | 65 | 66 | class SimpleSingleValueRawTokenModel(SingleValueRawTokenModel[str]): 67 | @classmethod 68 | def _parse_value(cls, raw_text: str) -> str: 69 | return raw_text 70 | 71 | @classmethod 72 | def _format_value(cls, value: str) -> str: 73 | return value 74 | 75 | 76 | class DefaultRawTokenModel(base.RawTokenModel): 77 | # not using @classmethod here because it suppresses abstractmethod errors. 78 | @property 79 | @abc.abstractmethod 80 | def DEFAULT(self) -> str: 81 | ... 82 | 83 | @classmethod 84 | def from_default(cls) -> Self: 85 | return cls.from_raw_text(cls.DEFAULT) # type: ignore[arg-type] 86 | 87 | 88 | class SimpleDefaultRawTokenModel(SimpleRawTokenModel, DefaultRawTokenModel): 89 | pass 90 | -------------------------------------------------------------------------------- /autobean_refactor/models/internal/indexes.py: -------------------------------------------------------------------------------- 1 | def range_from_index(index: int | slice, length: int) -> range: 2 | if isinstance(index, int): 3 | try: 4 | index = range(length)[index] 5 | except IndexError: 6 | raise IndexError('list assignment index out of range') 7 | return range(index, index + 1) 8 | return range(length)[index] 9 | 10 | 11 | def slice_from_range(r: range) -> slice: 12 | stop = r.stop if r.stop != -1 else None 13 | return slice(r.start, stop, r.step) 14 | -------------------------------------------------------------------------------- /autobean_refactor/models/internal/placeholder.py: -------------------------------------------------------------------------------- 1 | from ..base import RawTokenModel 2 | 3 | 4 | class Placeholder(RawTokenModel): 5 | RULE = 'PLACEHOLDER' 6 | 7 | @classmethod 8 | def from_default(cls) -> 'Placeholder': 9 | return cls('') 10 | 11 | def _clone(self) -> 'Placeholder': 12 | return Placeholder('') 13 | -------------------------------------------------------------------------------- /autobean_refactor/models/internal/registry.py: -------------------------------------------------------------------------------- 1 | from typing import Type, TypeVar 2 | from .. import base 3 | 4 | _TT = TypeVar('_TT', bound=Type[base.RawTokenModel]) 5 | _UT = TypeVar('_UT', bound=Type[base.RawTreeModel]) 6 | 7 | 8 | TOKEN_MODELS: dict[str, Type[base.RawTokenModel]] = {} 9 | TREE_MODELS: dict[str, Type[base.RawTreeModel]] = {} 10 | 11 | 12 | def token_model(cls: _TT) -> _TT: 13 | TOKEN_MODELS[cls.RULE] = cls 14 | return cls 15 | 16 | 17 | def tree_model(cls: _UT) -> _UT: 18 | TREE_MODELS[cls.RULE] = cls 19 | return cls 20 | -------------------------------------------------------------------------------- /autobean_refactor/models/internal/repeated.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from typing import Generic, Iterable, Iterator, Optional, TypeVar 3 | from typing_extensions import Self 4 | from .. import base 5 | from .placeholder import Placeholder 6 | 7 | _M = TypeVar('_M', bound=base.RawModel) 8 | 9 | class Repeated(base.RawTreeModel, Generic[_M]): 10 | def __init__( 11 | self, 12 | token_store: base.TokenStore, 13 | items: Iterable[_M], 14 | placeholder: Placeholder, 15 | ) -> None: 16 | super().__init__(token_store) 17 | self.items = list(items) 18 | self._placeholder = placeholder 19 | 20 | @property 21 | def placeholder(self) -> Placeholder: 22 | return self._placeholder 23 | 24 | @property 25 | def first_token(self) -> base.RawTokenModel: 26 | return self._placeholder 27 | 28 | @property 29 | def last_token(self) -> base.RawTokenModel: 30 | if self.items: 31 | return self.items[-1].last_token 32 | return self._placeholder 33 | 34 | def _eq(self, other: base.RawTreeModel) -> bool: 35 | return isinstance(other, Repeated) and self.items == other.items 36 | 37 | @classmethod 38 | def from_children( 39 | cls, 40 | items: Iterable[_M], 41 | *, 42 | separators: tuple[base.RawTokenModel, ...], 43 | separators_before: Optional[tuple[base.RawTokenModel, ...]] = None, 44 | ) -> Self: 45 | placeholder = Placeholder.from_default() 46 | items = list(items) 47 | tokens: list[base.RawTokenModel] = [placeholder] 48 | for i, item in enumerate(items): 49 | if i == 0 and separators_before is not None: 50 | tokens.extend(copy.deepcopy(separators_before)) 51 | else: 52 | tokens.extend(copy.deepcopy(separators)) 53 | tokens.extend(item.detach()) 54 | token_store = base.TokenStore.from_tokens(tokens) 55 | for item in items: 56 | item.reattach(token_store) 57 | return cls(token_store, items, placeholder) 58 | 59 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 60 | return type(self)( 61 | token_store, 62 | (item.clone(token_store, token_transformer) for item in self.items), 63 | self.placeholder.clone(token_store, token_transformer)) 64 | 65 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 66 | self._token_store = token_store 67 | self.items = [item.reattach(token_store, token_transformer) for item in self.items] 68 | self._placeholder = self._placeholder.reattach(token_store, token_transformer) 69 | 70 | def auto_claim_comments(self) -> None: 71 | for item in reversed(self.items): 72 | item.auto_claim_comments() 73 | 74 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 75 | # should have been handled in repeated_field, who has access to separators 76 | raise NotImplementedError() 77 | -------------------------------------------------------------------------------- /autobean_refactor/models/internal/spacing_accessors.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Callable, Iterable, Optional, Sequence 3 | from .. import base 4 | from ..spacing import Newline, Whitespace 5 | 6 | _SPACING_GROUP_RE = re.compile(r'([ \t]+)|(\r*\n)') 7 | 8 | 9 | def _tokens_to_text(tokens: Iterable[base.RawTokenModel]) -> str: 10 | return ''.join(token.raw_text for token in tokens) 11 | 12 | 13 | def _text_to_tokens(text: str) -> Iterable[base.RawTokenModel]: 14 | for whitespace, newline in _SPACING_GROUP_RE.findall(text): 15 | if whitespace: 16 | yield Whitespace.from_raw_text(whitespace) 17 | if newline: 18 | yield Newline.from_raw_text(newline) 19 | 20 | 21 | def _find_spacing( 22 | token: Optional[base.RawTokenModel], 23 | succ: Callable[[base.RawTokenModel], Optional[base.RawTokenModel]], 24 | ) -> list[base.RawTokenModel]: 25 | tokens: list[base.RawTokenModel] = [] 26 | while token is not None and not token.raw_text: 27 | token = succ(token) 28 | # must not interleave with special tokens to avoid removing them in spacing update. 29 | while isinstance(token, Newline | Whitespace): 30 | if token.raw_text: 31 | tokens.append(token) 32 | token = succ(token) 33 | return tokens 34 | 35 | 36 | class SpacingAccessorsMixin(base.RawModel): 37 | 38 | @property 39 | def raw_spacing_before(self) -> Sequence[base.RawTokenModel]: 40 | if self.token_store is None: 41 | return () 42 | return tuple(reversed(_find_spacing( 43 | self.token_store.get_prev(self.first_token), 44 | self.token_store.get_prev))) 45 | 46 | @raw_spacing_before.setter 47 | def raw_spacing_before(self, tokens: Sequence[base.RawTokenModel]) -> None: 48 | if self.token_store is None: 49 | raise ValueError('Cannot set spacing without a token store.') 50 | current_tokens = self.raw_spacing_before 51 | if current_tokens: 52 | self.token_store.splice(tokens, current_tokens[0], current_tokens[-1]) 53 | else: 54 | self.token_store.insert_before(self.first_token, tokens) 55 | 56 | @property 57 | def spacing_before(self) -> str: 58 | return _tokens_to_text(self.raw_spacing_before) 59 | 60 | @spacing_before.setter 61 | def spacing_before(self, value: str) -> None: 62 | self.raw_spacing_before = tuple(_text_to_tokens(value)) 63 | 64 | @property 65 | def raw_spacing_after(self) -> Sequence[base.RawTokenModel]: 66 | if self.token_store is None: 67 | return () 68 | return tuple(_find_spacing( 69 | self.token_store.get_next(self.last_token), 70 | self.token_store.get_next)) 71 | 72 | @raw_spacing_after.setter 73 | def raw_spacing_after(self, tokens: Sequence[base.RawTokenModel]) -> None: 74 | if self.token_store is None: 75 | raise ValueError('Cannot set spacing without a token store.') 76 | current_tokens = self.raw_spacing_after 77 | if current_tokens: 78 | self.token_store.splice(tokens, current_tokens[0], current_tokens[-1]) 79 | else: 80 | self.token_store.insert_after(self.last_token, tokens) 81 | 82 | @property 83 | def spacing_after(self) -> str: 84 | return _tokens_to_text(self.raw_spacing_after) 85 | 86 | @spacing_after.setter 87 | def spacing_after(self, value: str) -> None: 88 | self.raw_spacing_after = tuple(_text_to_tokens(value)) 89 | -------------------------------------------------------------------------------- /autobean_refactor/models/internal/surrounding_comments.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Optional 2 | from . import fields 3 | from .placeholder import Placeholder 4 | from .. import base 5 | from ..spacing import Newline 6 | from ..block_comment import BlockComment 7 | 8 | 9 | def _take_ignored( 10 | token: Optional[base.RawTokenModel], 11 | succ: Callable[[base.RawTokenModel], Optional[base.RawTokenModel]], 12 | ignored: list[base.RawTokenModel], 13 | ) -> Optional[base.RawTokenModel]: 14 | while isinstance(token, Placeholder): 15 | ignored.append(token) 16 | token = succ(token) 17 | return token 18 | 19 | 20 | def _claim_comment( 21 | current: Optional[BlockComment], 22 | token_store: base.TokenStore, 23 | start: base.RawTokenModel, 24 | *, 25 | backwards: bool, 26 | ignore_if_already_claimed: bool, 27 | ) -> Optional[BlockComment]: 28 | if current is not None: 29 | return current 30 | 31 | ignored: list[base.RawTokenModel] = [] 32 | if backwards: 33 | succ = token_store.get_prev 34 | else: 35 | succ = token_store.get_next 36 | 37 | first = succ(start) 38 | if first is None: 39 | return None 40 | 41 | newline = _take_ignored(first, succ, ignored) 42 | if not isinstance(newline, Newline): 43 | return None 44 | comment = _take_ignored(succ(newline), succ, ignored) 45 | if not isinstance(comment, BlockComment): 46 | return None 47 | 48 | if comment.claimed: 49 | if ignore_if_already_claimed: 50 | return None 51 | raise ValueError('Comment already claimed.') 52 | comment.claimed = True 53 | if ignored: 54 | if backwards: 55 | token_store.splice( 56 | [*reversed(ignored), comment, newline], comment, first) 57 | else: 58 | token_store.splice( 59 | [newline, comment, *ignored], first, comment) 60 | return comment 61 | 62 | 63 | class SurroundingCommentsMixin(base.RawTreeModel): 64 | _leading_comment = fields.optional_right_field[BlockComment](separators=(Newline.from_default(),)) 65 | _trailing_comment = fields.optional_left_field[BlockComment](separators=(Newline.from_default(),)) 66 | 67 | def claim_leading_comment(self, *, ignore_if_already_claimed: bool = False) -> Optional[BlockComment]: 68 | self._leading_comment = _claim_comment( 69 | self._leading_comment, 70 | self.token_store, 71 | self.first_token, 72 | backwards=True, 73 | ignore_if_already_claimed=ignore_if_already_claimed) 74 | return self._leading_comment 75 | 76 | def unclaim_leading_comment(self) -> Optional[BlockComment]: 77 | current = self._leading_comment 78 | if current is not None: 79 | current.claimed = False 80 | self._leading_comment = None 81 | return current 82 | 83 | def claim_trailing_comment(self, *, ignore_if_already_claimed: bool = False) -> Optional[BlockComment]: 84 | self._trailing_comment = _claim_comment( 85 | self._trailing_comment, 86 | self.token_store, 87 | self.last_token, 88 | backwards=False, 89 | ignore_if_already_claimed=ignore_if_already_claimed) 90 | return self._trailing_comment 91 | 92 | def unclaim_trailing_comment(self) -> Optional[BlockComment]: 93 | current = self._trailing_comment 94 | if current is not None: 95 | current.claimed = False 96 | self._trailing_comment = None 97 | return current 98 | -------------------------------------------------------------------------------- /autobean_refactor/models/internal/token_models.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | from . import base_token_models, spacing_accessors 3 | 4 | _V = TypeVar('_V') 5 | 6 | 7 | class SimpleRawTokenModel( 8 | base_token_models.SimpleRawTokenModel, 9 | spacing_accessors.SpacingAccessorsMixin): 10 | pass 11 | 12 | 13 | class SingleValueRawTokenModel( 14 | base_token_models.SingleValueRawTokenModel[_V], 15 | spacing_accessors.SpacingAccessorsMixin): 16 | pass 17 | 18 | 19 | class SimpleSingleValueRawTokenModel( 20 | base_token_models.SimpleSingleValueRawTokenModel, 21 | spacing_accessors.SpacingAccessorsMixin): 22 | pass 23 | 24 | 25 | class DefaultRawTokenModel( 26 | base_token_models.DefaultRawTokenModel, 27 | spacing_accessors.SpacingAccessorsMixin): 28 | pass 29 | 30 | 31 | class SimpleDefaultRawTokenModel( 32 | base_token_models.SimpleDefaultRawTokenModel, 33 | spacing_accessors.SpacingAccessorsMixin): 34 | pass 35 | -------------------------------------------------------------------------------- /autobean_refactor/models/link.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class Link(internal.SingleValueRawTokenModel[str]): 6 | """Link (e.g. `^foo`).""" 7 | RULE = 'LINK' 8 | 9 | @classmethod 10 | def _parse_value(cls, raw_text: str) -> str: 11 | return raw_text[1:] 12 | 13 | @classmethod 14 | def _format_value(cls, value: str) -> str: 15 | return f'^{value}' 16 | -------------------------------------------------------------------------------- /autobean_refactor/models/meta_item.py: -------------------------------------------------------------------------------- 1 | from .generated.meta_item import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/meta_key.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class MetaKey(internal.SingleValueRawTokenModel[str]): 6 | """Meta key (e.g. `foo:`).""" 7 | RULE = 'META_KEY' 8 | 9 | @classmethod 10 | def _parse_value(cls, raw_text: str) -> str: 11 | return raw_text[:-1] 12 | 13 | @classmethod 14 | def _format_value(cls, value: str) -> str: 15 | return f'{value}:' 16 | -------------------------------------------------------------------------------- /autobean_refactor/models/meta_value.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | from typing import Union 4 | from .account import Account 5 | from .amount import Amount 6 | from .bool import Bool 7 | from .currency import Currency 8 | from .date import Date 9 | from .escaped_string import EscapedString 10 | from .null import Null 11 | from .number_expr import NumberExpr 12 | from .tag import Tag 13 | 14 | MetaRawValue = Account | Amount | Bool | Currency | Date | EscapedString | Null | NumberExpr | Tag 15 | _ValueTypeSimplified = str | datetime.date | decimal.Decimal | bool 16 | _ValueTypePreserved = Account | Currency | Tag | Null | Amount 17 | MetaValue = Union[_ValueTypeSimplified, _ValueTypePreserved] 18 | -------------------------------------------------------------------------------- /autobean_refactor/models/meta_value_internal.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | from typing import Generic, Optional, Type, TypeVar, overload 4 | from . import internal 5 | from .base import RawTreeModel 6 | from .bool import Bool 7 | from .date import Date 8 | from .escaped_string import EscapedString 9 | from .number_expr import NumberExpr 10 | from .meta_value import MetaRawValue, MetaValue 11 | 12 | _U = TypeVar('_U', bound=RawTreeModel) 13 | 14 | 15 | # Not inside internal.value_properties to avoid circular dependencies. 16 | class optional_meta_value_property(Generic[_U]): 17 | def __init__(self, inner_property: internal.base_rw_property[Optional[MetaRawValue], _U]): 18 | self.inner_property = inner_property 19 | 20 | def __get__(self, instance: _U, owner: Optional[Type[_U]] = None) -> Optional[MetaValue]: 21 | raw_value = self.inner_property.__get__(instance, owner) 22 | if isinstance(raw_value, EscapedString | Date | NumberExpr | Bool): 23 | return raw_value.value 24 | return raw_value 25 | 26 | def __set__(self, instance: _U, value: Optional[MetaValue | MetaRawValue]) -> None: 27 | current_raw = self.inner_property.__get__(instance, None) 28 | if not update_value(current_raw, value): 29 | self.inner_property.__set__(instance, from_value(value)) 30 | 31 | 32 | def update_value(raw_value: Optional[MetaRawValue], value: Optional[MetaValue | MetaRawValue]) -> bool: 33 | match raw_value, value: 34 | case EscapedString() as m, str() as v: 35 | m.value = v 36 | case Date() as m, datetime.date() as v: 37 | m.value = v 38 | case NumberExpr() as m, decimal.Decimal() as v: 39 | m.value = v 40 | case Bool() as m, bool() as v: 41 | m.value = v 42 | case _: 43 | return False 44 | return True 45 | 46 | 47 | def from_value(value: Optional[MetaValue | MetaRawValue]) -> Optional[MetaRawValue]: 48 | match value: 49 | case str(): 50 | return EscapedString.from_value(value) 51 | case datetime.date(): 52 | return Date.from_value(value) 53 | case decimal.Decimal(): 54 | return NumberExpr.from_value(value) 55 | case bool(): 56 | return Bool.from_value(value) 57 | return value 58 | -------------------------------------------------------------------------------- /autobean_refactor/models/note.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import itertools 3 | from typing import Iterable, Mapping, Optional 4 | from typing_extensions import Self 5 | from . import internal, meta_item_internal 6 | from .date import Date 7 | from .account import Account 8 | from .block_comment import BlockComment 9 | from .escaped_string import EscapedString 10 | from .inline_comment import InlineComment 11 | from .link import Link 12 | from .tag import Tag 13 | from .generated import note 14 | from .generated.note import NoteLabel 15 | from .meta_value import MetaRawValue, MetaValue 16 | 17 | 18 | 19 | @internal.tree_model 20 | class Note(note.Note): 21 | tags = internal.repeated_string_property(note.Note.raw_tags_links, Tag) 22 | links = internal.repeated_string_property(note.Note.raw_tags_links, Link) 23 | 24 | @classmethod 25 | def from_value( 26 | cls, 27 | date: datetime.date, 28 | account: str, 29 | comment: str, 30 | *, 31 | tags: Iterable[str] = (), 32 | links: Iterable[str] = (), 33 | leading_comment: Optional[str] = None, 34 | inline_comment: Optional[str] = None, 35 | meta: Optional[Mapping[str, MetaValue | MetaRawValue]] = None, 36 | trailing_comment: Optional[str] = None, 37 | indent_by: str = ' ', 38 | ) -> Self: 39 | return cls.from_children( 40 | date=Date.from_value(date), 41 | account=Account.from_value(account), 42 | comment=EscapedString.from_value(comment), 43 | tags_links=itertools.chain(map(Tag.from_value, tags), map(Link.from_value, links)), 44 | leading_comment=BlockComment.from_value(leading_comment) if leading_comment is not None else None, 45 | inline_comment=InlineComment.from_value(inline_comment) if inline_comment is not None else None, 46 | meta=meta_item_internal.from_mapping(meta, indent=indent_by) if meta is not None else (), 47 | trailing_comment=BlockComment.from_value(trailing_comment) if trailing_comment is not None else None, 48 | indent_by=indent_by, 49 | ) 50 | -------------------------------------------------------------------------------- /autobean_refactor/models/null.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class Null(internal.SimpleDefaultRawTokenModel): 6 | """Contains literal `NULL`.""" 7 | RULE = 'NULL' 8 | DEFAULT = 'NULL' 9 | -------------------------------------------------------------------------------- /autobean_refactor/models/number.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from . import internal 3 | 4 | 5 | @internal.token_model 6 | class Number(internal.SingleValueRawTokenModel[decimal.Decimal]): 7 | """Number (e.g. `42.00`).""" 8 | RULE = 'NUMBER' 9 | 10 | @classmethod 11 | def _parse_value(cls, raw_text: str) -> decimal.Decimal: 12 | return decimal.Decimal(raw_text.replace(',', '')) 13 | 14 | @classmethod 15 | def _format_value(cls, value: decimal.Decimal) -> str: 16 | return str(value) 17 | -------------------------------------------------------------------------------- /autobean_refactor/models/number_add_expr.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from typing import Any, Iterator, Optional, cast, final, TYPE_CHECKING 3 | from typing_extensions import Self 4 | from . import base 5 | from . import internal 6 | from .spacing import Whitespace 7 | if TYPE_CHECKING: 8 | from .number_mul_expr import NumberMulExpr 9 | else: 10 | NumberMulExpr = Any 11 | 12 | 13 | @internal.token_model 14 | class AddOp(internal.SimpleRawTokenModel): 15 | RULE = 'ADD_OP' 16 | 17 | 18 | @internal.tree_model 19 | class NumberAddExpr(base.RawTreeModel): 20 | RULE = 'number_add_expr' 21 | 22 | @final 23 | def __init__( 24 | self, 25 | token_store: base.TokenStore, 26 | operands: tuple[NumberMulExpr, ...], 27 | ops: tuple[AddOp, ...], 28 | ): 29 | super().__init__(token_store) 30 | self._raw_operands = operands 31 | self._raw_ops = ops 32 | 33 | @classmethod 34 | def from_parsed_children(cls, token_store: base.TokenStore, *children: Optional[base.RawModel]) -> Self: 35 | return cls( 36 | token_store, 37 | cast(tuple[NumberMulExpr, ...], children[::2]), 38 | cast(tuple[AddOp, ...], children[1::2])) 39 | 40 | @property 41 | def first_token(self) -> base.RawTokenModel: 42 | return self._raw_operands[0].first_token 43 | 44 | @property 45 | def last_token(self) -> base.RawTokenModel: 46 | return self._raw_operands[-1].last_token 47 | 48 | @property 49 | def raw_operands(self) -> tuple[NumberMulExpr, ...]: 50 | return self._raw_operands 51 | 52 | @property 53 | def raw_ops(self) -> tuple[AddOp, ...]: 54 | return self._raw_ops 55 | 56 | @property 57 | def value(self) -> decimal.Decimal: 58 | value = self._raw_operands[0].value 59 | for op, operand in zip(self._raw_ops, self._raw_operands[1:]): 60 | if op.raw_text == '+': 61 | value += operand.value 62 | elif op.raw_text == '-': 63 | value -= operand.value 64 | else: 65 | assert False 66 | return value 67 | 68 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 69 | ops = tuple(op.clone(token_store, token_transformer) for op in self._raw_ops) 70 | operands = tuple(operand.clone(token_store, token_transformer) for operand in self._raw_operands) 71 | return type(self)(token_store, operands, ops) 72 | 73 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 74 | self._token_store = token_store 75 | self._raw_ops = tuple(op.reattach(token_store, token_transformer) for op in self._raw_ops) 76 | self._raw_operands = tuple(operand.reattach(token_store, token_transformer) for operand in self._raw_operands) 77 | 78 | def _eq(self, other: base.RawTreeModel) -> bool: 79 | return ( 80 | isinstance(other, NumberAddExpr) 81 | and self._raw_operands == other._raw_operands 82 | and self._raw_ops == other._raw_ops) 83 | 84 | def auto_claim_comments(self) -> None: 85 | pass # no block comments 86 | 87 | @classmethod 88 | def from_children(cls, operands: tuple[NumberMulExpr, ...], ops: tuple[AddOp, ...]) -> Self: 89 | tokens = [] 90 | for operand, op in zip(operands, ops): 91 | tokens.extend(operand.detach()) 92 | tokens.append(Whitespace.from_default()) 93 | tokens.extend(op.detach()) 94 | tokens.append(Whitespace.from_default()) 95 | tokens.extend(operands[-1].detach()) 96 | token_store = base.TokenStore.from_tokens(tokens) 97 | for operand in operands: 98 | operand.reattach(token_store) 99 | for op in ops: 100 | op.reattach(token_store) 101 | return cls(token_store, operands, ops) 102 | 103 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 104 | for operand, op in zip(self._raw_operands, self._raw_ops): 105 | yield operand, False 106 | yield Whitespace.from_default(), False 107 | yield op, False 108 | yield Whitespace.from_default(), False 109 | yield self._raw_operands[-1], False 110 | -------------------------------------------------------------------------------- /autobean_refactor/models/number_atom_expr.py: -------------------------------------------------------------------------------- 1 | from typing import Any, TypeAlias, TYPE_CHECKING 2 | from .number import Number 3 | if TYPE_CHECKING: 4 | from .number_paren_expr import NumberParenExpr 5 | from .number_unary_expr import NumberUnaryExpr 6 | else: 7 | NumberParenExpr = Any 8 | NumberUnaryExpr = Any 9 | 10 | NumberAtomExpr: TypeAlias = Number | NumberParenExpr | NumberUnaryExpr 11 | -------------------------------------------------------------------------------- /autobean_refactor/models/number_mul_expr.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from typing import Any, Iterator, Optional, cast, final, TYPE_CHECKING 3 | from typing_extensions import Self 4 | from . import base 5 | from . import internal 6 | from .spacing import Whitespace 7 | if TYPE_CHECKING: 8 | from .number_atom_expr import NumberAtomExpr 9 | else: 10 | NumberAtomExpr = Any 11 | 12 | 13 | @internal.token_model 14 | class MulOp(internal.SimpleRawTokenModel): 15 | RULE = 'MUL_OP' 16 | 17 | 18 | @internal.tree_model 19 | class NumberMulExpr(base.RawTreeModel): 20 | RULE = 'number_mul_expr' 21 | 22 | @final 23 | def __init__( 24 | self, 25 | token_store: base.TokenStore, 26 | operands: tuple[NumberAtomExpr, ...], 27 | ops: tuple[MulOp, ...], 28 | ): 29 | super().__init__(token_store) 30 | self._raw_operands = operands 31 | self._raw_ops = ops 32 | 33 | @classmethod 34 | def from_parsed_children(cls, token_store: base.TokenStore, *children: Optional[base.RawModel]) -> Self: 35 | return cls( 36 | token_store, 37 | cast(tuple[NumberAtomExpr, ...], children[::2]), 38 | cast(tuple[MulOp, ...], children[1::2])) 39 | 40 | @property 41 | def first_token(self) -> base.RawTokenModel: 42 | return self._raw_operands[0].first_token 43 | 44 | @property 45 | def last_token(self) -> base.RawTokenModel: 46 | return self._raw_operands[-1].last_token 47 | 48 | @property 49 | def raw_operands(self) -> tuple[NumberAtomExpr, ...]: 50 | return self._raw_operands 51 | 52 | @property 53 | def raw_ops(self) -> tuple[MulOp, ...]: 54 | return self._raw_ops 55 | 56 | @property 57 | def value(self) -> decimal.Decimal: 58 | value = self._raw_operands[0].value 59 | for op, operand in zip(self._raw_ops, self._raw_operands[1:]): 60 | if op.raw_text == '*': 61 | value *= operand.value 62 | elif op.raw_text == '/': 63 | value /= operand.value 64 | else: 65 | assert False 66 | return value 67 | 68 | def clone(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> Self: 69 | ops = tuple(op.clone(token_store, token_transformer) for op in self._raw_ops) 70 | operands = tuple(operand.clone(token_store, token_transformer) for operand in self._raw_operands) 71 | return type(self)(token_store, operands, ops) 72 | 73 | def _reattach(self, token_store: base.TokenStore, token_transformer: base.TokenTransformer) -> None: 74 | self._token_store = token_store 75 | self._raw_ops = tuple(op.reattach(token_store, token_transformer) for op in self._raw_ops) 76 | self._raw_operands = tuple(operand.reattach(token_store, token_transformer) for operand in self._raw_operands) 77 | 78 | def _eq(self, other: base.RawTreeModel) -> bool: 79 | return ( 80 | isinstance(other, NumberMulExpr) 81 | and self._raw_operands == other._raw_operands 82 | and self._raw_ops == other._raw_ops) 83 | 84 | def auto_claim_comments(self) -> None: 85 | pass # no block comments 86 | 87 | @classmethod 88 | def from_children(cls, operands: tuple[NumberAtomExpr, ...], ops: tuple[MulOp, ...]) -> Self: 89 | tokens = [] 90 | for operand, op in zip(operands, ops): 91 | tokens.extend(operand.detach()) 92 | tokens.append(Whitespace.from_default()) 93 | tokens.extend(op.detach()) 94 | tokens.append(Whitespace.from_default()) 95 | tokens.extend(operands[-1].detach()) 96 | token_store = base.TokenStore.from_tokens(tokens) 97 | for operand in operands: 98 | operand.reattach(token_store) 99 | for op in ops: 100 | op.reattach(token_store) 101 | return cls(token_store, operands, ops) 102 | 103 | def iter_children_formatted(self) -> Iterator[tuple[base.RawModel, bool]]: 104 | for operand, op in zip(self._raw_operands, self._raw_ops): 105 | yield operand, False 106 | yield Whitespace.from_default(), False 107 | yield op, False 108 | yield Whitespace.from_default(), False 109 | yield self._raw_operands[-1], False 110 | -------------------------------------------------------------------------------- /autobean_refactor/models/number_paren_expr.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from . import internal 3 | from .generated import number_paren_expr 4 | from .generated.number_paren_expr import LeftParen, RightParen 5 | 6 | 7 | @internal.tree_model 8 | class NumberParenExpr(number_paren_expr.NumberParenExpr): 9 | @property 10 | def value(self) -> decimal.Decimal: 11 | return self._inner_expr.value 12 | -------------------------------------------------------------------------------- /autobean_refactor/models/number_unary_expr.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from . import internal 3 | from .generated import number_unary_expr 4 | from .generated.number_unary_expr import UnaryOp 5 | 6 | 7 | @internal.tree_model 8 | class NumberUnaryExpr(number_unary_expr.NumberUnaryExpr): 9 | @property 10 | def value(self) -> decimal.Decimal: 11 | if self._unary_op.raw_text == '+': 12 | return self._operand.value 13 | elif self._unary_op.raw_text == '-': 14 | return -self._operand.value 15 | else: 16 | assert False 17 | -------------------------------------------------------------------------------- /autobean_refactor/models/open.py: -------------------------------------------------------------------------------- 1 | from .generated.open import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/option.py: -------------------------------------------------------------------------------- 1 | from .generated.option import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/pad.py: -------------------------------------------------------------------------------- 1 | from .generated.pad import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/plugin.py: -------------------------------------------------------------------------------- 1 | from .generated.plugin import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/popmeta.py: -------------------------------------------------------------------------------- 1 | from .generated.popmeta import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/poptag.py: -------------------------------------------------------------------------------- 1 | from .generated.poptag import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/posting.py: -------------------------------------------------------------------------------- 1 | from .generated.posting import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/posting_flag.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class PostingFlag(internal.SimpleSingleValueRawTokenModel): 6 | """Posting flag (e.g. `!`).""" 7 | RULE = 'POSTING_FLAG' 8 | -------------------------------------------------------------------------------- /autobean_refactor/models/price.py: -------------------------------------------------------------------------------- 1 | from .generated.price import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/punctuation.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class Eol(internal.SimpleDefaultRawTokenModel): 6 | """End of line. For internal use only.""" 7 | RULE = 'EOL' 8 | DEFAULT = '' 9 | 10 | 11 | @internal.token_model 12 | class Indent(internal.SimpleSingleValueRawTokenModel, internal.DefaultRawTokenModel): 13 | """Contains spacing for indentation.""" 14 | RULE = 'INDENT' 15 | DEFAULT = ' ' * 4 16 | 17 | 18 | @internal.token_model 19 | class DedentMark(internal.SimpleDefaultRawTokenModel): 20 | """Dedent mark. For internal use only.""" 21 | RULE = 'DEDENT_MARK' 22 | DEFAULT = '' 23 | 24 | 25 | @internal.token_model 26 | class Comma(internal.SimpleDefaultRawTokenModel): 27 | """Contains literal `,`.""" 28 | RULE = '_COMMA' 29 | DEFAULT = ',' 30 | 31 | 32 | @internal.token_model 33 | class Asterisk(internal.SimpleDefaultRawTokenModel): 34 | """Contains literal `*`.""" 35 | RULE = 'ASTERISK' 36 | DEFAULT = '*' 37 | -------------------------------------------------------------------------------- /autobean_refactor/models/pushmeta.py: -------------------------------------------------------------------------------- 1 | from .generated.pushmeta import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/pushtag.py: -------------------------------------------------------------------------------- 1 | from .generated.pushtag import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/query.py: -------------------------------------------------------------------------------- 1 | from .generated.query import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/spacing.py: -------------------------------------------------------------------------------- 1 | from .internal import registry as _registry 2 | from .internal import base_token_models as _base_token_models 3 | 4 | 5 | @_registry.token_model 6 | class Newline(_base_token_models.SimpleDefaultRawTokenModel): 7 | """Newline.""" 8 | RULE = '_NEWLINE' 9 | DEFAULT = '\n' 10 | 11 | 12 | @_registry.token_model 13 | class Whitespace(_base_token_models.SimpleDefaultRawTokenModel): 14 | """Whitespaces and/or tabs.""" 15 | RULE = 'WHITESPACE' 16 | DEFAULT = ' ' 17 | -------------------------------------------------------------------------------- /autobean_refactor/models/tag.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class Tag(internal.SingleValueRawTokenModel[str]): 6 | """Tag (e.g. `#foo`).""" 7 | RULE = 'TAG' 8 | 9 | @classmethod 10 | def _parse_value(cls, raw_text: str) -> str: 11 | return raw_text[1:] 12 | 13 | @classmethod 14 | def _format_value(cls, value: str) -> str: 15 | return f'#{value}' 16 | -------------------------------------------------------------------------------- /autobean_refactor/models/tolerance.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from typing_extensions import Self 3 | from . import internal 4 | from .generated import tolerance 5 | from .generated.tolerance import Tilde 6 | 7 | 8 | @internal.tree_model 9 | class Tolerance(tolerance.Tolerance, internal.RWValue[decimal.Decimal]): 10 | 11 | @property 12 | def value(self) -> decimal.Decimal: 13 | return self.raw_number.value 14 | 15 | @value.setter 16 | def value(self, value: decimal.Decimal) -> None: 17 | self.raw_number.value = value 18 | 19 | @classmethod 20 | def from_value( 21 | cls, 22 | number: decimal.Decimal, 23 | ) -> Self: 24 | return super().from_value(number) 25 | -------------------------------------------------------------------------------- /autobean_refactor/models/total_price.py: -------------------------------------------------------------------------------- 1 | from .generated.total_price import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/models/transaction_flag.py: -------------------------------------------------------------------------------- 1 | from . import internal 2 | 3 | 4 | @internal.token_model 5 | class TransactionFlag(internal.SingleValueRawTokenModel[str]): 6 | """Transaction flag (e.g. `*`).""" 7 | RULE = 'TRANSACTION_FLAG' 8 | 9 | @classmethod 10 | def _parse_value(cls, raw_text: str) -> str: 11 | if raw_text == 'txn': 12 | return '*' 13 | return raw_text 14 | 15 | @classmethod 16 | def _format_value(cls, value: str) -> str: 17 | return value 18 | -------------------------------------------------------------------------------- /autobean_refactor/models/unit_price.py: -------------------------------------------------------------------------------- 1 | from .generated.unit_price import * 2 | -------------------------------------------------------------------------------- /autobean_refactor/printer.py: -------------------------------------------------------------------------------- 1 | import io 2 | from typing import TypeVar 3 | from autobean_refactor import models 4 | 5 | _T = TypeVar('_T', bound=io.TextIOBase) 6 | 7 | 8 | def print_model(model: models.RawModel, file: _T) -> _T: 9 | for token in model.tokens: 10 | file.write(token.raw_text) 11 | return file 12 | -------------------------------------------------------------------------------- /autobean_refactor/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SEIAROTg/autobean-refactor/8b0877b695405d95ca140c7fc1930e69fbccc289/autobean_refactor/py.typed -------------------------------------------------------------------------------- /autobean_refactor/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SEIAROTg/autobean-refactor/8b0877b695405d95ca140c7fc1930e69fbccc289/autobean_refactor/tests/__init__.py -------------------------------------------------------------------------------- /autobean_refactor/tests/benchmark/benchmark_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest_benchmark.fixture import BenchmarkFixture # type: ignore[import] 3 | from autobean_refactor import parser as parser_lib 4 | from autobean_refactor import models 5 | 6 | _FILE_SIMPLE = '''\ 7 | 2000-01-01 * 8 | Assets:Foo 100.00 USD 9 | Assets:Bar -100.00 USD 10 | 11 | ''' 12 | 13 | _FILE_COMPLEX = '''\ 14 | ; comment 15 | 2000-01-01 * "payee" "narration" #tag-a #tag-b ^link-a 16 | meta-a: 1 17 | ; comment 18 | meta-b: 2 19 | ; comment 20 | Assets:Foo 100.00 USD 21 | ; comment 22 | meta-c: 3 23 | Assets:Bar -100.00 DSU {{}} 24 | ; comment 25 | 26 | ''' 27 | 28 | 29 | def _parse_file(parser: parser_lib.Parser, text: str) -> models.File: 30 | return parser.parse(text, models.File) 31 | 32 | 33 | def _update_comment(transaction: models.Transaction) -> None: 34 | transaction.leading_comment = transaction.leading_comment[::-1] # type: ignore[index] 35 | 36 | 37 | def _getitem_repeated(file: models.File) -> models.Directive | models.BlockComment: 38 | return file.raw_directives_with_comments[-1] 39 | 40 | 41 | def _getitem_repeated_filtered(file: models.File) -> models.Directive: 42 | return file.raw_directives[-1] 43 | 44 | 45 | def _insert_meta(transaction: models.Transaction) -> None: 46 | meta = transaction.raw_meta_with_comments.pop(-1) 47 | transaction.raw_meta_with_comments.insert(0, meta) 48 | 49 | 50 | @pytest.mark.benchmark(group='parse_simple') 51 | @pytest.mark.parametrize('repeat', [1, 10, 100, 1000]) 52 | def test_parse_simple(repeat: int, benchmark: BenchmarkFixture, parser: parser_lib.Parser) -> None: 53 | benchmark(_parse_file, parser, _FILE_SIMPLE * repeat) 54 | 55 | 56 | @pytest.mark.benchmark(group='parse_complex') 57 | @pytest.mark.parametrize('repeat', [1, 10, 100, 1000]) 58 | def test_parse_complex(repeat: int, benchmark: BenchmarkFixture, parser: parser_lib.Parser) -> None: 59 | benchmark(_parse_file, parser, _FILE_COMPLEX * repeat) 60 | 61 | 62 | @pytest.mark.benchmark(group='update_end') 63 | @pytest.mark.parametrize('repeat', [1, 10, 100, 1000, 10000]) 64 | def test_update_end(repeat: int, benchmark: BenchmarkFixture, parser: parser_lib.Parser) -> None: 65 | file = parser.parse(_FILE_COMPLEX * repeat, models.File, auto_claim_comments=False) 66 | txn = file.raw_directives_with_comments[-1] 67 | txn.auto_claim_comments() 68 | assert isinstance(txn, models.Transaction) 69 | assert txn.leading_comment is not None 70 | 71 | benchmark(_update_comment, txn) 72 | 73 | 74 | @pytest.mark.benchmark(group='update_start') 75 | @pytest.mark.parametrize('repeat', [1, 10, 100, 1000, 10000]) 76 | def test_update_start(repeat: int, benchmark: BenchmarkFixture, parser: parser_lib.Parser) -> None: 77 | file = parser.parse(_FILE_COMPLEX * repeat, models.File, auto_claim_comments=False) 78 | txn = file.raw_directives_with_comments[0] 79 | txn.auto_claim_comments() 80 | assert isinstance(txn, models.Transaction) 81 | assert txn.leading_comment is not None 82 | 83 | benchmark(_update_comment, txn) 84 | 85 | 86 | @pytest.mark.benchmark(group='getitem_repeated') 87 | @pytest.mark.parametrize('repeat', [1, 10, 100, 1000, 10000]) 88 | def test_getitem_repeated(repeat: int, benchmark: BenchmarkFixture, parser: parser_lib.Parser) -> None: 89 | file = parser.parse(_FILE_COMPLEX * repeat, models.File) 90 | benchmark(_getitem_repeated, file) 91 | 92 | 93 | @pytest.mark.benchmark(group='getitem_repeated_filtered') 94 | @pytest.mark.parametrize('repeat', [1, 10, 100, 1000, 10000]) 95 | def test_getitem_repeated_filtered(repeat: int, benchmark: BenchmarkFixture, parser: parser_lib.Parser) -> None: 96 | file = parser.parse(_FILE_COMPLEX * repeat, models.File) 97 | benchmark(_getitem_repeated_filtered, file) 98 | 99 | 100 | @pytest.mark.benchmark(group='insert_meta_end') 101 | @pytest.mark.parametrize('repeat', [1, 10, 100, 1000, 10000]) 102 | def test_insert_meta_end(repeat: int, benchmark: BenchmarkFixture, parser: parser_lib.Parser) -> None: 103 | file = parser.parse(_FILE_COMPLEX * repeat, models.File) 104 | txn = file.raw_directives_with_comments[-1] 105 | assert isinstance(txn, models.Transaction) 106 | benchmark(_insert_meta, txn) 107 | 108 | 109 | @pytest.mark.benchmark(group='insert_meta_start') 110 | @pytest.mark.parametrize('repeat', [1, 10, 100, 1000, 10000]) 111 | def test_insert_meta_start(repeat: int, benchmark: BenchmarkFixture, parser: parser_lib.Parser) -> None: 112 | file = parser.parse(_FILE_COMPLEX * repeat, models.File) 113 | txn = file.raw_directives_with_comments[0] 114 | assert isinstance(txn, models.Transaction) 115 | benchmark(_insert_meta, txn) 116 | -------------------------------------------------------------------------------- /autobean_refactor/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from autobean_refactor import parser as parser_lib 3 | from autobean_refactor import models 4 | 5 | 6 | @pytest.fixture(scope='package') 7 | def parser() -> parser_lib.Parser: 8 | return parser_lib.Parser() 9 | -------------------------------------------------------------------------------- /autobean_refactor/tests/editor_test.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import tempfile 3 | from typing import Iterator 4 | import pytest 5 | from autobean_refactor import editor as editor_lib 6 | from autobean_refactor import models 7 | 8 | _FILES_FOO = { 9 | 'index.bean': '''\ 10 | plugin "foo" 11 | include "2020/index.bean" 12 | include "2021/index.bean" 13 | ''', 14 | '2020/index.bean': '''\ 15 | include "*.bean" 16 | 2020-01-01 * 17 | ''', 18 | '2020/02.bean': '''\ 19 | 2020-02-01 * 20 | ''', 21 | '2020/03/01.bean': '''\ 22 | 2020-03-01 * 23 | ''', 24 | '2021/index.bean': '''\ 25 | include "**/*.bean" 26 | 2021-01-01 * 27 | ''', 28 | '2021/02.bean': '''\ 29 | 2021-02-01 * 30 | ''', 31 | '2021/03/01.bean': '''\ 32 | 2021-03-01 * 33 | include "../index.bean" 34 | ''', 35 | } 36 | 37 | 38 | @pytest.fixture() 39 | def testdir() -> Iterator[pathlib.Path]: 40 | with tempfile.TemporaryDirectory() as tmpdir: 41 | d = pathlib.Path(tmpdir) 42 | for filename, content in _FILES_FOO.items(): 43 | p = d / filename 44 | p.parent.mkdir(parents=True, exist_ok=True) 45 | p.write_text(content) 46 | yield d 47 | 48 | 49 | @pytest.fixture(scope='module') 50 | def editor() -> editor_lib.Editor: 51 | return editor_lib.Editor() 52 | 53 | 54 | class TestEditor: 55 | 56 | @pytest.fixture(autouse=True) 57 | def editor(self, editor: editor_lib.Editor) -> None: 58 | self._editor = editor 59 | 60 | def test_read_file(self, testdir: pathlib.Path) -> None: 61 | path = testdir / 'index.bean' 62 | mtime = path.stat().st_mtime_ns 63 | with self._editor.edit_file(path) as file: 64 | assert len(file.raw_directives) == 3 65 | assert path.stat().st_mtime_ns == mtime 66 | 67 | def test_edit_file(self, testdir: pathlib.Path) -> None: 68 | path = testdir / 'index.bean' 69 | with self._editor.edit_file(path) as file: 70 | assert len(file.raw_directives) == 3 71 | file.directives.pop(0) 72 | assert path.read_text() == '''\ 73 | include "2020/index.bean" 74 | include "2021/index.bean" 75 | ''' 76 | 77 | def test_edit_file_recursive(self, testdir: pathlib.Path) -> None: 78 | expected_paths = { 79 | 'index.bean': 3, 80 | '2020/index.bean': 2, 81 | '2020/02.bean': 1, 82 | # '2020/03/01.bean': 1, # '*' doesn't match children directories 83 | '2021/index.bean': 2, 84 | '2021/02.bean': 1, 85 | '2021/03/01.bean': 2, 86 | } 87 | mtimes = { 88 | p: (testdir / p).stat().st_mtime_ns 89 | for p in expected_paths 90 | } 91 | path = testdir / 'index.bean' 92 | with self._editor.edit_file_recursive(path) as files: 93 | assert { 94 | p: len(f.directives) 95 | for p, f in files.items() 96 | } == { 97 | str(testdir / p): n 98 | for p, n in expected_paths.items() 99 | } 100 | # update 101 | files[str(testdir / '2020/02.bean')].raw_directives_with_comments.append( 102 | models.BlockComment.from_value('updated')) 103 | # delete 104 | files.pop(str(testdir / '2021/02.bean')) 105 | # create 106 | files[str(testdir / '2022/03.bean')] = models.File.from_children([ 107 | models.BlockComment.from_value('created') 108 | ]) 109 | 110 | for p in expected_paths: 111 | if p == '2021/02.bean': 112 | assert not (testdir / p).exists() 113 | continue 114 | mtime = (testdir / p).stat().st_mtime_ns 115 | updated_text = (testdir / p).read_text() 116 | if p == '2020/02.bean': 117 | assert updated_text == '2020-02-01 *\n\n; updated\n' 118 | else: 119 | assert mtime == mtimes[p] 120 | assert updated_text == _FILES_FOO[p] 121 | 122 | assert (testdir / '2022/03.bean').read_text() == '; created' 123 | -------------------------------------------------------------------------------- /autobean_refactor/tests/generic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SEIAROTg/autobean-refactor/8b0877b695405d95ca140c7fc1930e69fbccc289/autobean_refactor/tests/generic/__init__.py -------------------------------------------------------------------------------- /autobean_refactor/tests/generic/auto_claim_comments_test.py: -------------------------------------------------------------------------------- 1 | from autobean_refactor import models 2 | from .. import base 3 | 4 | _FILE_FOO = '''\ 5 | ; comment 0 6 | 7 | ; comment 1 8 | 2000-01-01 open Assets:Foo 9 | ; comment 2 10 | foo: 1 11 | ; comment 3 12 | ; comment 4 13 | 14 | ; comment 5 15 | 16 | ; comment 6 17 | 2000-01-01 * 18 | ; comment 7 19 | bar: 1 20 | ; comment 8 21 | baz: 2 22 | ; comment 9 23 | Assets:Foo 100.00 USD 24 | ; comment 10 25 | Assets:Bar -100.00 USD 26 | ; comment 11 27 | ; comment 12 28 | 29 | ; comment 13\ 30 | ''' 31 | 32 | 33 | class TestAutoClaimComments(base.BaseTestModel): 34 | 35 | def test_auto_claim_comments(self) -> None: 36 | file = self.parser.parse(_FILE_FOO, models.File) 37 | file.auto_claim_comments() 38 | 39 | # all claimed 40 | for token in file.token_store: 41 | if isinstance(token, models.BlockComment): 42 | assert token.claimed 43 | 44 | assert len(file.raw_directives_with_comments) == 5 45 | c0, open_foo, c5, txn, c13 = file.raw_directives_with_comments 46 | assert isinstance(c0, models.BlockComment) 47 | assert c0.value == 'comment 0' 48 | assert isinstance(c5, models.BlockComment) 49 | assert c5.value == 'comment 5' 50 | assert isinstance(c13, models.BlockComment) 51 | assert c13.value == 'comment 13' 52 | assert isinstance(open_foo, models.Open) 53 | assert open_foo.leading_comment == 'comment 1' 54 | assert open_foo.trailing_comment == 'comment 4' 55 | assert len(open_foo.raw_meta_with_comments) == 1 56 | 57 | meta_foo, = open_foo.raw_meta 58 | assert meta_foo.leading_comment == 'comment 2' 59 | assert meta_foo.trailing_comment == 'comment 3' 60 | 61 | assert isinstance(txn, models.Transaction) 62 | assert txn.leading_comment == 'comment 6' 63 | assert txn.trailing_comment == 'comment 12' 64 | meta_bar, meta_baz = txn.raw_meta 65 | assert meta_bar.leading_comment == 'comment 7' 66 | assert meta_bar.trailing_comment is None 67 | assert meta_baz.leading_comment == 'comment 8' 68 | assert meta_baz.trailing_comment is None 69 | posting_foo, posting_bar = txn.raw_postings 70 | assert posting_foo.leading_comment == 'comment 9' 71 | assert posting_foo.trailing_comment is None 72 | assert posting_bar.leading_comment == 'comment 10' 73 | assert posting_bar.trailing_comment == 'comment 11' 74 | -------------------------------------------------------------------------------- /autobean_refactor/tests/generic/inline_comment_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional, Type 3 | import pytest 4 | from autobean_refactor import models 5 | from .. import base 6 | 7 | 8 | class TestInlineComment(base.BaseTestModel): 9 | 10 | @pytest.mark.parametrize( 11 | 'model_type,text,inline_comment', [ 12 | (models.Close, '2000-01-01 close Assets:Foo ; foo', 'foo'), 13 | (models.Close, '2000-01-01 close Assets:Foo', None), 14 | (models.MetaItem, ' foo: "bar" ; baz', 'baz'), 15 | (models.MetaItem, ' foo: "bar" ', None), 16 | (models.Posting, ' Assets:Foo ; foo\n bar: "baz" ; bar', 'foo'), 17 | (models.Posting, ' Assets:Foo \n bar: "baz" ; bar', None), 18 | (models.Transaction, '2000-01-01 * ; foo\n Assets:Foo ; bar\n bar: "baz" ; baz', 'foo'), 19 | (models.Transaction, '2000-01-01 * \n Assets:Foo ; bar\n bar: "baz" ; baz', None), 20 | ], 21 | ) 22 | def test_parse_success(self, model_type: Type[models.RawTreeModel], text: str, inline_comment: Optional[str]) -> None: 23 | model = self.parser.parse(text, model_type) 24 | assert getattr(model, 'inline_comment') == inline_comment 25 | assert self.print_model(model) == text 26 | 27 | @pytest.mark.parametrize( 28 | 'text,inline_comment,expected_text', [ 29 | ('2000-01-01 close Assets:Foo ; bar', 'baz', '2000-01-01 close Assets:Foo ; baz'), 30 | ('2000-01-01 close Assets:Foo ; bar', None, '2000-01-01 close Assets:Foo'), 31 | ('2000-01-01 close Assets:Foo', None, '2000-01-01 close Assets:Foo'), 32 | ('2000-01-01 close Assets:Foo', 'bar', '2000-01-01 close Assets:Foo ; bar'), 33 | ], 34 | ) 35 | def test_set_raw_inline_comment(self, text: str, inline_comment: Optional[str], expected_text: str) -> None: 36 | close = self.parser.parse(text, models.Close) 37 | raw_inline_comment = models.InlineComment.from_value(inline_comment) if inline_comment is not None else None 38 | close.raw_inline_comment = raw_inline_comment 39 | assert close.raw_inline_comment is raw_inline_comment 40 | assert close.inline_comment == inline_comment 41 | assert self.print_model(close) == expected_text 42 | 43 | @pytest.mark.parametrize( 44 | 'text,inline_comment,expected_text', [ 45 | ('2000-01-01 close Assets:Foo ; bar', 'baz', '2000-01-01 close Assets:Foo ; baz'), 46 | ('2000-01-01 close Assets:Foo ; bar', None, '2000-01-01 close Assets:Foo'), 47 | ('2000-01-01 close Assets:Foo', None, '2000-01-01 close Assets:Foo'), 48 | ('2000-01-01 close Assets:Foo', 'bar', '2000-01-01 close Assets:Foo ; bar'), 49 | ], 50 | ) 51 | def test_set_value(self, text: str, inline_comment: Optional[str], expected_text: str) -> None: 52 | close = self.parser.parse(text, models.Close) 53 | close.inline_comment = inline_comment 54 | assert close.inline_comment == inline_comment 55 | assert self.print_model(close) == expected_text 56 | 57 | def test_from_children(self) -> None: 58 | date = models.Date.from_value(datetime.date(2000, 1, 1)) 59 | account = models.Account.from_value('Assets:Foo') 60 | inline_comment = models.InlineComment.from_value('bar') 61 | close = models.Close.from_children(date, account, inline_comment=inline_comment) 62 | assert close.raw_inline_comment is inline_comment 63 | assert self.print_model(close) == '2000-01-01 close Assets:Foo ; bar' 64 | 65 | def test_from_value(self) -> None: 66 | close = models.Close.from_value(datetime.date(2000, 1, 1), 'Assets:Foo', inline_comment='bar') 67 | assert close.inline_comment == 'bar' 68 | assert self.print_model(close) == '2000-01-01 close Assets:Foo ; bar' 69 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SEIAROTg/autobean-refactor/8b0877b695405d95ca140c7fc1930e69fbccc289/autobean_refactor/tests/models/__init__.py -------------------------------------------------------------------------------- /autobean_refactor/tests/models/account_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestAccount(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text', [ 11 | 'Assets:Foo', 12 | 'Assets:Foo:Bar', 13 | 'Assets:X', 14 | 'Assets:X银行', 15 | # This is an invalid beancount account name but does pass beancount lexer. 16 | # The validation happens in the parser here: https://github.com/beancount/beancount/blob/89bf061b60777be3ae050c5c44fef67d93029130/beancount/parser/grammar.py#L243. 17 | 'Assets:银行', 18 | ], 19 | ) 20 | def test_parse_success(self, text: str) -> None: 21 | token = self.parser.parse_token(text, models.Account) 22 | assert token.raw_text == text 23 | assert token.value == text 24 | self.check_deepcopy_token(token) 25 | 26 | @pytest.mark.parametrize( 27 | 'text', [ 28 | 'Assets', 29 | ], 30 | ) 31 | def test_parse_failure(self, text: str) -> None: 32 | with pytest.raises(exceptions.UnexpectedInput): 33 | self.parser.parse_token(text, models.Account) 34 | 35 | def test_set_raw_text(self) -> None: 36 | token = self.parser.parse_token('Assets:Foo', models.Account) 37 | token.raw_text = 'Liabilities:Foo' 38 | assert token.raw_text == 'Liabilities:Foo' 39 | assert token.value == 'Liabilities:Foo' 40 | 41 | def test_set_value(self) -> None: 42 | token = self.parser.parse_token('Assets:Foo', models.Account) 43 | token.value = 'Liabilities:Foo' 44 | assert token.value == 'Liabilities:Foo' 45 | assert token.raw_text == 'Liabilities:Foo' 46 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/amount_test.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from lark import exceptions 3 | import pytest 4 | from autobean_refactor import models 5 | from .. import base 6 | 7 | 8 | class TestAmount(base.BaseTestModel): 9 | 10 | @pytest.mark.parametrize( 11 | 'text,number,currency', [ 12 | ('100.00 USD', decimal.Decimal('100.00'), 'USD'), 13 | ('100.00USD', decimal.Decimal('100.00'), 'USD'), 14 | ('100.00 \t USD', decimal.Decimal('100.00'), 'USD'), 15 | ('-100.00 + 20 USD', decimal.Decimal('-80.00'), 'USD'), 16 | ('(10+20) USD', decimal.Decimal('30.00'), 'USD'), 17 | ], 18 | ) 19 | def test_parse_success(self, text: str, number: decimal.Decimal, currency: str) -> None: 20 | amount = self.parser.parse(text, models.Amount) 21 | assert amount.raw_number.value == number 22 | assert amount.raw_currency.value == currency 23 | assert self.print_model(amount) == text 24 | self.check_deepcopy_tree(amount) 25 | self.check_reattach_tree(amount) 26 | self.check_iter_children_formatted(amount) 27 | 28 | @pytest.mark.parametrize( 29 | 'text', [ 30 | '100.00', 31 | 'USD', 32 | '10+ USD', 33 | ], 34 | ) 35 | def test_parse_failure(self, text: str) -> None: 36 | with pytest.raises(exceptions.UnexpectedInput): 37 | self.parser.parse(text, models.Amount) 38 | 39 | def test_set_raw_number(self) -> None: 40 | amount = self.parser.parse('100.00 USD', models.Amount) 41 | new_number = self.parser.parse('(100.00 + 20.00)', models.NumberExpr) 42 | amount.raw_number = new_number 43 | assert amount.raw_number is new_number 44 | assert self.print_model(amount) == '(100.00 + 20.00) USD' 45 | 46 | def test_set_raw_currency(self) -> None: 47 | amount = self.parser.parse('(100.00 + 20.00) USD', models.Amount) 48 | new_currency = models.Currency.from_value('EUR') 49 | amount.raw_currency = new_currency 50 | assert amount.raw_currency is new_currency 51 | assert self.print_model(amount) == '(100.00 + 20.00) EUR' 52 | 53 | def test_set_number(self) -> None: 54 | amount = self.parser.parse('(100.00 + 20.00) USD', models.Amount) 55 | assert amount.number == decimal.Decimal('120.00') 56 | amount.number = decimal.Decimal('-12.34') 57 | assert amount.number == decimal.Decimal('-12.34') 58 | assert self.print_model(amount) == '-12.34 USD' 59 | 60 | def test_set_currency(self) -> None: 61 | amount = self.parser.parse('(100.00 + 20.00) USD', models.Amount) 62 | assert amount.currency == 'USD' 63 | amount.currency = 'EUR' 64 | assert amount.currency == 'EUR' 65 | assert self.print_model(amount) == '(100.00 + 20.00) EUR' 66 | 67 | def test_from_children(self) -> None: 68 | number = models.NumberExpr.from_value(decimal.Decimal('100.00')) 69 | currency = models.Currency.from_value('USD') 70 | amount = models.Amount.from_children(number, currency) 71 | assert amount.raw_number is number 72 | assert amount.raw_currency is currency 73 | assert self.print_model(amount) == '100.00 USD' 74 | self.check_consistency(amount) 75 | 76 | def test_from_value(self) -> None: 77 | amount = models.Amount.from_value(decimal.Decimal('100.00'), 'USD') 78 | assert amount.raw_number.value == decimal.Decimal('100.00') 79 | assert amount.raw_currency.value == 'USD' 80 | assert self.print_model(amount) == '100.00 USD' 81 | self.check_consistency(amount) 82 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/block_comment_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from lark import exceptions 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | _PARSE_TESTCASES = [ 7 | (';foo', '', 'foo'), 8 | ('; foo', '', 'foo'), 9 | (';', '', ''), 10 | (' ;', ' ', ''), 11 | (';foo\n;bar', '', 'foo\nbar'), 12 | (';foo\r\n;bar', '', 'foo\r\nbar'), 13 | ('\t; foo', '\t', 'foo'), 14 | ('\t; foo\n ;bar', '\t', ' foo\nbar'), 15 | (' ; foo\n ; bar', ' ', 'foo\nbar'), 16 | (' ; foo\n ; bar', ' ', 'foo\n bar'), 17 | ('; a\n;\n;\n; b', '', 'a\n\n\nb'), 18 | ] 19 | _FORMAT_TESTCASES = [ 20 | ('; foo', '', 'foo'), 21 | (';', '', ''), 22 | ('; foo\n; bar', '', 'foo\nbar'), 23 | (' ; foo', ' ', 'foo'), 24 | (' ; foo\n ; bar', ' ', 'foo\nbar'), 25 | ('\t; foo\r\n\t; bar', '\t', 'foo\r\nbar'), 26 | (' ; foo\n ; bar', ' ', 'foo\n bar'), 27 | ('; a\n;\n;\n; b', '', 'a\n\n\nb'), 28 | ('; a\n;\n;\n;', '', 'a\n\n\n'), 29 | ] 30 | 31 | 32 | class TestBlockComment(base.BaseTestModel): 33 | 34 | @pytest.mark.parametrize( 35 | 'text,indent,value', _PARSE_TESTCASES, 36 | ) 37 | def test_parse_success(self, text: str, indent: str, value: str) -> None: 38 | token = self.parser.parse_token(text, models.BlockComment) 39 | assert token.raw_text == text 40 | assert token.indent == indent 41 | assert token.value == value 42 | self.check_deepcopy_token(token) 43 | 44 | @pytest.mark.parametrize( 45 | 'text', [ 46 | '', 47 | ';foo\n', 48 | ';foo;bar\n', 49 | ' ; foo\n;bar', 50 | '; foo\n ;bar', 51 | ], 52 | ) 53 | def test_parse_failure(self, text: str) -> None: 54 | with pytest.raises(exceptions.UnexpectedInput): 55 | self.parser.parse_token(text, models.BlockComment) 56 | 57 | @pytest.mark.parametrize( 58 | 'text,indent,value', _PARSE_TESTCASES, 59 | ) 60 | def test_from_raw_text(self, text: str, indent: str, value: str) -> None: 61 | comment = models.BlockComment.from_raw_text(text) 62 | assert comment.raw_text == text 63 | assert comment.indent == indent 64 | assert comment.value == value 65 | 66 | @pytest.mark.parametrize( 67 | 'text,indent,value', _FORMAT_TESTCASES, 68 | ) 69 | def test_from_value(self, text: str, indent: str, value: str) -> None: 70 | comment = models.BlockComment.from_value(value, indent=indent) 71 | assert comment.raw_text == text 72 | assert comment.indent == indent 73 | assert comment.value == value 74 | 75 | @pytest.mark.parametrize( 76 | 'text,indent,value', _FORMAT_TESTCASES 77 | ) 78 | def test_set_indent_value(self, text: str, indent: str, value: str) -> None: 79 | comment = models.BlockComment.from_raw_text(' ; foo') 80 | comment.indent = indent 81 | comment.value = value 82 | assert comment.raw_text == text 83 | assert comment.indent == indent 84 | assert comment.value == value 85 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/bool_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestBool(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text,value', [ 11 | ('TRUE', True), 12 | ('FALSE', False), 13 | ], 14 | ) 15 | def test_parse_success(self, text: str, value: bool) -> None: 16 | token = self.parser.parse_token(text, models.Bool) 17 | assert token.raw_text == text 18 | assert token.value == value 19 | self.check_deepcopy_token(token) 20 | 21 | @pytest.mark.parametrize( 22 | 'text', [ 23 | 'True', 24 | 'False', 25 | 'true', 26 | 'false', 27 | ], 28 | ) 29 | def test_parse_failure(self, text: str) -> None: 30 | with pytest.raises(exceptions.UnexpectedInput): 31 | self.parser.parse_token(text, models.Bool) 32 | 33 | @pytest.mark.parametrize( 34 | 'raw_text,new_text,expected_value', [ 35 | ('FALSE', 'TRUE', True), 36 | ('TRUE', 'FALSE', False), 37 | ], 38 | ) 39 | def test_set_raw_text(self, raw_text: str, new_text: str, expected_value: bool) -> None: 40 | token = self.parser.parse_token(raw_text, models.Bool) 41 | token.raw_text = new_text 42 | assert token.raw_text == new_text 43 | assert token.value == expected_value 44 | 45 | @pytest.mark.parametrize( 46 | 'raw_text,new_value,expected_text', [ 47 | ('FALSE', True, 'TRUE'), 48 | ('TRUE', False, 'FALSE'), 49 | ], 50 | ) 51 | def test_set_value(self, raw_text: str, new_value: bool, expected_text: str) -> None: 52 | token = self.parser.parse_token(raw_text, models.Bool) 53 | token.value = new_value 54 | assert token.value == new_value 55 | assert token.raw_text == expected_text 56 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/close_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from lark import exceptions 3 | import pytest 4 | from autobean_refactor import models 5 | from .. import base 6 | 7 | 8 | class TestClose(base.BaseTestModel): 9 | 10 | @pytest.mark.parametrize( 11 | 'text,date,account', [ 12 | ('2000-01-01 close Assets:Foo', datetime.date(2000, 1, 1), 'Assets:Foo'), 13 | ('2012-12-12 close Assets:Bar', datetime.date(2012, 12, 12), 'Assets:Bar'), 14 | ], 15 | ) 16 | def test_parse_success( 17 | self, 18 | text: str, 19 | date: datetime.date, 20 | account: str, 21 | ) -> None: 22 | close = self.parser.parse(text, models.Close) 23 | assert close.raw_date.value == date 24 | assert close.date == date 25 | assert close.raw_account.value == account 26 | assert close.account == account 27 | assert self.print_model(close) == text 28 | self.check_deepcopy_tree(close) 29 | self.check_reattach_tree(close) 30 | self.check_iter_children_formatted(close) 31 | 32 | @pytest.mark.parametrize( 33 | 'text', [ 34 | '2000-01-01 cLose Assets:Foo', 35 | 'close Assets:Foo', 36 | '2000-01-01 close', 37 | ], 38 | ) 39 | def test_parse_failure(self, text: str) -> None: 40 | with pytest.raises(exceptions.UnexpectedInput): 41 | self.parser.parse(text, models.Close) 42 | 43 | def test_set_raw_date(self) -> None: 44 | close = self.parser.parse('2000-01-01 close Assets:Foo', models.Close) 45 | new_date = models.Date.from_value(datetime.date(2012, 12, 12)) 46 | close.raw_date = new_date 47 | assert close.raw_date is new_date 48 | assert self.print_model(close) == '2012-12-12 close Assets:Foo' 49 | 50 | def test_set_date(self) -> None: 51 | close = self.parser.parse('2000-01-01 close Assets:Foo', models.Close) 52 | assert close.date == datetime.date(2000, 1, 1) 53 | close.date = datetime.date(2012, 12, 12) 54 | assert close.date == datetime.date(2012, 12, 12) 55 | assert self.print_model(close) == '2012-12-12 close Assets:Foo' 56 | 57 | def test_set_raw_account(self) -> None: 58 | close = self.parser.parse('2000-01-01 close Assets:Foo', models.Close) 59 | new_account = models.Account.from_value('Assets:Bar') 60 | close.raw_account = new_account 61 | assert close.raw_account is new_account 62 | assert self.print_model(close) == '2000-01-01 close Assets:Bar' 63 | 64 | def test_set_account(self) -> None: 65 | close = self.parser.parse('2000-01-01 close Assets:Foo', models.Close) 66 | assert close.account == 'Assets:Foo' 67 | close.account = 'Assets:Bar' 68 | assert close.account == 'Assets:Bar' 69 | assert self.print_model(close) == '2000-01-01 close Assets:Bar' 70 | 71 | def test_from_children(self) -> None: 72 | date = models.Date.from_value(datetime.date(2012, 12, 12)) 73 | account = models.Account.from_value('Assets:Bar') 74 | close = models.Close.from_children(date, account) 75 | assert close.raw_date is date 76 | assert close.raw_account is account 77 | assert close.date == datetime.date(2012, 12, 12) 78 | assert close.account == 'Assets:Bar' 79 | assert self.print_model(close) == '2012-12-12 close Assets:Bar' 80 | 81 | def test_from_value(self) -> None: 82 | close = models.Close.from_value(datetime.date(2012, 12, 12), 'Assets:Bar') 83 | assert close.date == datetime.date(2012, 12, 12) 84 | assert close.account == 'Assets:Bar' 85 | assert self.print_model(close) == '2012-12-12 close Assets:Bar' 86 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/commodity_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from lark import exceptions 3 | import pytest 4 | from autobean_refactor import models 5 | from .. import base 6 | 7 | 8 | class TestCommodity(base.BaseTestModel): 9 | 10 | @pytest.mark.parametrize( 11 | 'text,date,currency', [ 12 | ('2000-01-01 commodity USD', datetime.date(2000, 1, 1), 'USD'), 13 | ('2012-12-12 commodity EUR', datetime.date(2012, 12, 12), 'EUR'), 14 | ], 15 | ) 16 | def test_parse_success( 17 | self, 18 | text: str, 19 | date: datetime.date, 20 | currency: str, 21 | ) -> None: 22 | commodity = self.parser.parse(text, models.Commodity) 23 | assert commodity.raw_date.value == date 24 | assert commodity.date == date 25 | assert commodity.raw_currency.value == currency 26 | assert commodity.currency == currency 27 | assert self.print_model(commodity) == text 28 | self.check_deepcopy_tree(commodity) 29 | self.check_reattach_tree(commodity) 30 | self.check_iter_children_formatted(commodity) 31 | 32 | @pytest.mark.parametrize( 33 | 'text', [ 34 | '2000-01-01 commodIty USD', 35 | 'commodity USD', 36 | '2000-01-01 commodity', 37 | ], 38 | ) 39 | def test_parse_failure(self, text: str) -> None: 40 | with pytest.raises(exceptions.UnexpectedInput): 41 | self.parser.parse(text, models.Commodity) 42 | 43 | def test_set_raw_date(self) -> None: 44 | commodity = self.parser.parse('2000-01-01 commodity USD', models.Commodity) 45 | new_date = models.Date.from_value(datetime.date(2012, 12, 12)) 46 | commodity.raw_date = new_date 47 | assert commodity.raw_date is new_date 48 | assert self.print_model(commodity) == '2012-12-12 commodity USD' 49 | 50 | def test_set_date(self) -> None: 51 | commodity = self.parser.parse('2000-01-01 commodity USD', models.Commodity) 52 | assert commodity.date == datetime.date(2000, 1, 1) 53 | commodity.date = datetime.date(2012, 12, 12) 54 | assert commodity.date == datetime.date(2012, 12, 12) 55 | assert self.print_model(commodity) == '2012-12-12 commodity USD' 56 | 57 | def test_set_raw_currency(self) -> None: 58 | commodity = self.parser.parse('2000-01-01 commodity USD', models.Commodity) 59 | new_currency = models.Currency.from_value('EUR') 60 | commodity.raw_currency = new_currency 61 | assert commodity.raw_currency is new_currency 62 | assert self.print_model(commodity) == '2000-01-01 commodity EUR' 63 | 64 | def test_set_currency(self) -> None: 65 | commodity = self.parser.parse('2000-01-01 commodity USD', models.Commodity) 66 | assert commodity.currency == 'USD' 67 | commodity.currency = 'EUR' 68 | assert commodity.currency == 'EUR' 69 | assert self.print_model(commodity) == '2000-01-01 commodity EUR' 70 | 71 | def test_from_children(self) -> None: 72 | date = models.Date.from_value(datetime.date(2012, 12, 12)) 73 | currency = models.Currency.from_value('EUR') 74 | commodity = models.Commodity.from_children(date, currency) 75 | assert commodity.raw_date is date 76 | assert commodity.raw_currency is currency 77 | assert commodity.date == datetime.date(2012, 12, 12) 78 | assert commodity.currency == 'EUR' 79 | assert self.print_model(commodity) == '2012-12-12 commodity EUR' 80 | 81 | def test_from_value(self) -> None: 82 | commodity = models.Commodity.from_value(datetime.date(2012, 12, 12), 'EUR') 83 | assert commodity.date == datetime.date(2012, 12, 12) 84 | assert commodity.currency == 'EUR' 85 | assert self.print_model(commodity) == '2012-12-12 commodity EUR' 86 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/currency_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestCurrency(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text', [ 11 | 'USD', 12 | 'AAPL', 13 | 'NT.TO', 14 | 'TLT_040921C144', 15 | '/6J', 16 | '/NQH21', 17 | '/NQH21_QNEG21C13100', 18 | 'C345', 19 | # These are technically invalid currencies but it's difficult to reject them under contextual lexer. 20 | 'TRUE', 21 | 'FALSE', 22 | 'NULL', 23 | ], 24 | ) 25 | def test_parse_success(self, text: str) -> None: 26 | token = self.parser.parse_token(text, models.Currency) 27 | assert token.raw_text == text 28 | assert token.value == text 29 | self.check_deepcopy_token(token) 30 | 31 | @pytest.mark.parametrize( 32 | 'text', [ 33 | '/6.3', 34 | '/CAC_', 35 | 'C_', 36 | 'V', # it is valid in v3 syntax 37 | 'Asset', 38 | ], 39 | ) 40 | def test_parse_failure(self, text: str) -> None: 41 | with pytest.raises(exceptions.UnexpectedInput): 42 | self.parser.parse_token(text, models.Currency) 43 | 44 | def test_set_raw_text(self) -> None: 45 | token = self.parser.parse_token('USD', models.Currency) 46 | token.raw_text = 'AAPL' 47 | assert token.raw_text == 'AAPL' 48 | assert token.value == 'AAPL' 49 | 50 | def test_set_value(self) -> None: 51 | token = self.parser.parse_token('USD', models.Currency) 52 | token.value = 'AAPL' 53 | assert token.value == 'AAPL' 54 | assert token.raw_text == 'AAPL' 55 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/date_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from lark import exceptions 3 | import pytest 4 | from autobean_refactor import models 5 | from .. import base 6 | 7 | 8 | class TestDate(base.BaseTestModel): 9 | 10 | @pytest.mark.parametrize( 11 | 'text,value', [ 12 | ('4321-01-23', datetime.date(4321, 1, 23)), 13 | ('4321-12-01', datetime.date(4321, 12, 1)), 14 | ('4321-1-3', datetime.date(4321, 1, 3)), 15 | ('4321/01/23', datetime.date(4321, 1, 23)), 16 | ], 17 | ) 18 | def test_parse_success(self, text: str, value: datetime.date) -> None: 19 | token = self.parser.parse_token(text, models.Date) 20 | assert token.raw_text == text 21 | assert token.value == value 22 | self.check_deepcopy_token(token) 23 | 24 | @pytest.mark.parametrize( 25 | 'text', [ 26 | '123-01-23', 27 | '1234-001-23', 28 | '1234-01-001', 29 | '01-01-1234', 30 | ], 31 | ) 32 | def test_parse_failure(self, text: str) -> None: 33 | with pytest.raises(exceptions.UnexpectedInput): 34 | self.parser.parse_token(text, models.Date) 35 | 36 | def test_set_raw_text(self) -> None: 37 | token = self.parser.parse_token('4321-01-23', models.Date) 38 | token.raw_text = '1234-03-21' 39 | assert token.raw_text == '1234-03-21' 40 | assert token.value == datetime.date(1234, 3, 21) 41 | 42 | def test_set_raw_text_format(self) -> None: 43 | token = self.parser.parse_token('4321-01-23', models.Date) 44 | token.raw_text = '4321/01/23' 45 | assert token.raw_text == '4321/01/23' 46 | assert token.value == datetime.date(4321, 1, 23) 47 | 48 | def test_set_value(self) -> None: 49 | token = self.parser.parse_token('4321-01-23', models.Date) 50 | token.value = datetime.date(1234, 3, 21) 51 | assert token.value == datetime.date(1234, 3, 21) 52 | assert token.raw_text == '1234-03-21' 53 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/escaped_string_test.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from lark import exceptions 3 | import pytest 4 | from autobean_refactor import models 5 | from .. import base 6 | 7 | # (text, value) 8 | _ESCAPE_TEST_CASES_COMMON = [ 9 | ('""', ''), 10 | ('"foo"', 'foo'), 11 | ('"\'\'"', "''"), 12 | (r'"\""', '"'), 13 | (r'"\\"', '\\'), 14 | (r'"你好"', '你好'), 15 | (r'"\\\\n"', '\\\\n'), 16 | ] 17 | _UNESCAPE_TEST_CASE_LOSS = [ 18 | (r'"\u4f60\u597d"', 'u4f60u597d'), 19 | ] 20 | _ESCAPE_TEST_CASES_CONSERVATIVE = [ 21 | ('"\n"', '\n'), 22 | ('"\t"', '\t'), 23 | ('"multiple\nlines"', 'multiple\nlines'), 24 | ] 25 | _ESCAPE_TEST_CASES_AGGRESSIVE = [ 26 | (r'"\n"', '\n'), 27 | (r'"\t"', '\t'), 28 | (r'"multiple\nlines"', 'multiple\nlines'), 29 | ] 30 | 31 | 32 | class TestEscapedString(base.BaseTestModel): 33 | 34 | @pytest.mark.parametrize( 35 | 'text,value', itertools.chain( 36 | _ESCAPE_TEST_CASES_COMMON, 37 | _UNESCAPE_TEST_CASE_LOSS, 38 | _ESCAPE_TEST_CASES_CONSERVATIVE, 39 | _ESCAPE_TEST_CASES_AGGRESSIVE, 40 | ), 41 | ) 42 | def test_parse_success(self, text: str, value: str) -> None: 43 | token = self.parser.parse_token(text, models.EscapedString) 44 | assert token.value == value 45 | assert token.raw_text == text 46 | self.check_deepcopy_token(token) 47 | 48 | @pytest.mark.parametrize( 49 | 'text', [ 50 | 'foo', 51 | "'foo'", 52 | '"foo', 53 | 'foo"', 54 | '"""', 55 | '', 56 | ], 57 | ) 58 | def test_parse_failure(self, text: str) -> None: 59 | with pytest.raises(exceptions.UnexpectedInput): 60 | self.parser.parse_token(text, models.EscapedString) 61 | 62 | @pytest.mark.parametrize( 63 | 'text,value', itertools.chain( 64 | _ESCAPE_TEST_CASES_COMMON, 65 | _UNESCAPE_TEST_CASE_LOSS, 66 | _ESCAPE_TEST_CASES_CONSERVATIVE, 67 | _ESCAPE_TEST_CASES_AGGRESSIVE, 68 | ), 69 | ) 70 | def test_set_raw_text(self, text: str, value: str) -> None: 71 | token = self.parser.parse_token('"dummy"', models.EscapedString) 72 | token.raw_text = text 73 | assert token.value == value 74 | assert token.raw_text == text 75 | 76 | @pytest.mark.parametrize( 77 | 'text,value', itertools.chain( 78 | _ESCAPE_TEST_CASES_COMMON, 79 | _ESCAPE_TEST_CASES_CONSERVATIVE, 80 | ), 81 | ) 82 | def test_set_value(self, text: str, value: str) -> None: 83 | token = self.parser.parse_token('"dummy"', models.EscapedString) 84 | token.value = value 85 | assert token.value == value 86 | assert token.raw_text == text 87 | 88 | @pytest.mark.parametrize( 89 | 'text,value', itertools.chain( 90 | _ESCAPE_TEST_CASES_COMMON, 91 | _UNESCAPE_TEST_CASE_LOSS, 92 | _ESCAPE_TEST_CASES_CONSERVATIVE, 93 | _ESCAPE_TEST_CASES_AGGRESSIVE, 94 | ), 95 | ) 96 | def test_unesacpe(self, text: str, value: str) -> None: 97 | actual_value = models.EscapedString.unescape(text[1:-1]) 98 | assert actual_value == value 99 | 100 | @pytest.mark.parametrize( 101 | 'text,value', itertools.chain( 102 | _ESCAPE_TEST_CASES_COMMON, 103 | _ESCAPE_TEST_CASES_CONSERVATIVE, 104 | ), 105 | ) 106 | def test_esacpe_conservative(self, text: str, value: str) -> None: 107 | actual_text = models.EscapedString.escape(value) 108 | assert actual_text == text[1:-1] 109 | 110 | @pytest.mark.parametrize( 111 | 'text,value', itertools.chain( 112 | _ESCAPE_TEST_CASES_COMMON, 113 | _ESCAPE_TEST_CASES_AGGRESSIVE, 114 | ), 115 | ) 116 | def test_esacpe_aggressive(self, text: str, value: str) -> None: 117 | actual_text = models.EscapedString.escape(value, aggressive=True) 118 | assert actual_text == text[1:-1] 119 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/ignored_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from lark import exceptions 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestIgnored(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text', [ 11 | '* foo', 12 | '** foo', 13 | ': foo', 14 | '# foo', 15 | 'T foo', 16 | '**', 17 | '*', 18 | ], 19 | ) 20 | def test_parse_success(self, text: str) -> None: 21 | token = self.parser.parse_token(text, models.Ignored) 22 | assert token.raw_text == text 23 | self.check_deepcopy_token(token) 24 | 25 | @pytest.mark.parametrize( 26 | 'text', [ 27 | 'txn 123', 28 | 'txn', 29 | ' * foo', 30 | '', 31 | ], 32 | ) 33 | def test_parse_failure(self, text: str) -> None: 34 | with pytest.raises(exceptions.UnexpectedInput): 35 | self.parser.parse_token(text, models.Ignored) 36 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/include_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestInclude(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text,filename', [ 11 | ('include "foo"', 'foo'), 12 | ('include "multiple\nlines"', 'multiple\nlines'), 13 | ], 14 | ) 15 | def test_parse_success(self, text: str, filename: str) -> None: 16 | include = self.parser.parse(text, models.Include) 17 | assert include.raw_filename.value == filename 18 | assert self.print_model(include) == text 19 | self.check_deepcopy_tree(include) 20 | self.check_reattach_tree(include) 21 | self.check_iter_children_formatted(include) 22 | 23 | @pytest.mark.parametrize( 24 | 'text', [ 25 | ' include "foo"', 26 | 'incLude "foo"', 27 | 'include\n"foo"', 28 | 'include "foo" "bar"', 29 | 'include ', 30 | ], 31 | ) 32 | def test_parse_failure(self, text: str) -> None: 33 | with pytest.raises(exceptions.UnexpectedInput): 34 | self.parser.parse(text, models.Include) 35 | 36 | def test_set_raw_filename(self) -> None: 37 | include = self.parser.parse('include "filename"', models.Include) 38 | new_filename = models.EscapedString.from_value('new_filename') 39 | include.raw_filename = new_filename 40 | assert include.raw_filename is new_filename 41 | assert self.print_model(include) == 'include "new_filename"' 42 | 43 | def test_set_filename(self) -> None: 44 | include = self.parser.parse('include "filename"', models.Include) 45 | assert include.filename == 'filename' 46 | include.filename = 'new_filename' 47 | assert include.filename == 'new_filename' 48 | assert self.print_model(include) == 'include "new_filename"' 49 | 50 | def test_from_children(self) -> None: 51 | filename = models.EscapedString.from_value('filename') 52 | include = models.Include.from_children(filename) 53 | assert include.raw_filename is filename 54 | assert self.print_model(include) == 'include "filename"' 55 | self.check_consistency(include) 56 | 57 | def test_from_value(self) -> None: 58 | include = models.Include.from_value('foo') 59 | assert include.raw_filename.value == 'foo' 60 | assert self.print_model(include) == 'include "foo"' 61 | self.check_consistency(include) 62 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/inline_comment_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from lark import exceptions 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestInlineComment(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text,value', [ 11 | (';foo', 'foo'), 12 | ('; foo', 'foo'), 13 | ('; 你好!', '你好!'), 14 | (';', ''), 15 | (';"', '"'), 16 | (';""', '""'), 17 | ], 18 | ) 19 | def test_parse_success(self, text: str, value: str) -> None: 20 | token = self.parser.parse_token(text, models.InlineComment) 21 | assert token.raw_text == text 22 | assert token.value == value 23 | self.check_deepcopy_token(token) 24 | 25 | @pytest.mark.parametrize( 26 | 'text', [ 27 | '\n;foo', 28 | '', 29 | ' ;foo', 30 | ' ;foo\n', 31 | ';foo\n', 32 | ' ;foo', 33 | ' ;foo\n', 34 | ], 35 | ) 36 | def test_parse_failure(self, text: str) -> None: 37 | with pytest.raises(exceptions.UnexpectedInput): 38 | self.parser.parse_token(text, models.InlineComment) 39 | 40 | @pytest.mark.parametrize( 41 | 'text,value', [ 42 | ('; foo', 'foo'), 43 | (';', ''), 44 | ], 45 | ) 46 | def test_from_value(self, text: str, value: str) -> None: 47 | comment = models.InlineComment.from_value(value) 48 | assert comment.value == value 49 | assert comment.raw_text == text 50 | 51 | @pytest.mark.parametrize( 52 | 'text,value', [ 53 | ('; foo', 'foo'), 54 | (';', ''), 55 | ], 56 | ) 57 | def test_set_value(self, text: str, value: str) -> None: 58 | comment = models.InlineComment.from_value('bar') 59 | comment.value = value 60 | assert comment.value == value 61 | assert comment.raw_text == text 62 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/link_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestLink(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text,value', [ 11 | ('^foo', 'foo'), 12 | ('^XX.YY', 'XX.YY'), 13 | ('^000', '000'), 14 | ], 15 | ) 16 | def test_parse_success(self, text: str, value: str) -> None: 17 | token = self.parser.parse_token(text, models.Link) 18 | assert token.raw_text == text 19 | assert token.value == value 20 | self.check_deepcopy_token(token) 21 | 22 | @pytest.mark.parametrize( 23 | 'text', [ 24 | 'foo', 25 | '^标签', 26 | '^x!', 27 | '#foo', 28 | '^^foo', 29 | ], 30 | ) 31 | def test_parse_failure(self, text: str) -> None: 32 | with pytest.raises(exceptions.UnexpectedInput): 33 | self.parser.parse_token(text, models.Link) 34 | 35 | def test_set_raw_text(self) -> None: 36 | token = self.parser.parse_token('^foo', models.Link) 37 | token.raw_text = '^bar' 38 | assert token.raw_text == '^bar' 39 | assert token.value == 'bar' 40 | 41 | def test_set_value(self) -> None: 42 | token = self.parser.parse_token('^foo', models.Link) 43 | token.value = 'bar' 44 | assert token.value == 'bar' 45 | assert token.raw_text == '^bar' 46 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/meta_key_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestLink(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text,value', [ 11 | ('foo:', 'foo'), 12 | ('foo-bar:', 'foo-bar'), 13 | ('foo_bar:' ,'foo_bar'), 14 | ], 15 | ) 16 | def test_parse_success(self, text: str, value: str) -> None: 17 | token = self.parser.parse_token(text, models.MetaKey) 18 | assert token.raw_text == text 19 | assert token.value == value 20 | self.check_deepcopy_token(token) 21 | 22 | @pytest.mark.parametrize( 23 | 'text', [ 24 | 'x', 25 | 'foo', 26 | 'foo :', 27 | '-foo:', 28 | 'Foo:', 29 | 'FOO:', 30 | '#foo:', 31 | '!foo:', 32 | '你好:', 33 | 'foo-你好:', 34 | ], 35 | ) 36 | def test_parse_failure(self, text: str) -> None: 37 | with pytest.raises(exceptions.UnexpectedInput): 38 | self.parser.parse_token(text, models.MetaKey) 39 | 40 | def test_set_raw_text(self) -> None: 41 | token = self.parser.parse_token('foo:', models.MetaKey) 42 | token.raw_text = 'bar:' 43 | assert token.raw_text == 'bar:' 44 | assert token.value == 'bar' 45 | 46 | def test_set_value(self) -> None: 47 | token = self.parser.parse_token('foo:', models.MetaKey) 48 | token.value = 'bar' 49 | assert token.value == 'bar' 50 | assert token.raw_text == 'bar:' 51 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/newline_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestNewline(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text', [ 11 | '\n', 12 | '\r\n', 13 | '\r\r\n', 14 | ], 15 | ) 16 | def test_parse_success(self, text: str) -> None: 17 | token = self.parser.parse_token(text, models.Newline) 18 | assert token.raw_text == text 19 | self.check_deepcopy_token(token) 20 | 21 | @pytest.mark.parametrize( 22 | 'text', [ 23 | '\r', 24 | '\n ', 25 | '\n\r', 26 | '', 27 | ], 28 | ) 29 | def test_parse_failure(self, text: str) -> None: 30 | with pytest.raises(exceptions.UnexpectedInput): 31 | self.parser.parse_token(text, models.Newline) 32 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/null_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestNull(base.BaseTestModel): 8 | 9 | def test_parse_success(self) -> None: 10 | token = self.parser.parse_token('NULL', models.Null) 11 | assert token.raw_text == 'NULL' 12 | self.check_deepcopy_token(token) 13 | 14 | @pytest.mark.parametrize( 15 | 'text', [ 16 | 'Null', 17 | 'None', 18 | 'null', 19 | ], 20 | ) 21 | def test_parse_failure(self, text: str) -> None: 22 | with pytest.raises(exceptions.UnexpectedInput): 23 | self.parser.parse_token(text, models.Null) 24 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/number_test.py: -------------------------------------------------------------------------------- 1 | 2 | import decimal 3 | from lark import exceptions 4 | import pytest 5 | from autobean_refactor import models 6 | from .. import base 7 | 8 | 9 | class TestNumber(base.BaseTestModel): 10 | 11 | @pytest.mark.parametrize( 12 | 'text,value', [ 13 | ('123', decimal.Decimal(123)), 14 | ('0', decimal.Decimal(0)), 15 | ('1.', decimal.Decimal(1)), 16 | ('000', decimal.Decimal(0)), 17 | ('010', decimal.Decimal(10)), 18 | ('123.456', decimal.Decimal('123.456')), 19 | ('1,234', decimal.Decimal(1234)), 20 | ('12,345', decimal.Decimal(12345)), 21 | ('123,456', decimal.Decimal(123456)), 22 | ('1,234,567', decimal.Decimal(1234567)), 23 | ('12,345,678', decimal.Decimal(12345678)), 24 | ('123,456,789', decimal.Decimal(123456789)), 25 | ('1,234,567.89', decimal.Decimal('1234567.89')), 26 | ('0,000,000', decimal.Decimal(0)), 27 | ('1.23456789', decimal.Decimal('1.23456789')), 28 | ], 29 | ) 30 | def test_parse_success(self, text: str, value: decimal.Decimal) -> None: 31 | token = self.parser.parse_token(text, models.Number) 32 | assert token.raw_text == text 33 | assert token.value == value 34 | self.check_deepcopy_token(token) 35 | 36 | @pytest.mark.parametrize( 37 | 'text', [ 38 | '-1', 39 | '1,2', 40 | '1,23', 41 | '1,2345', 42 | '1234,567', 43 | '1+2', 44 | '.1', 45 | '.', 46 | ], 47 | ) 48 | def test_parse_failure(self, text: str) -> None: 49 | with pytest.raises(exceptions.UnexpectedInput): 50 | self.parser.parse_token(text, models.Number) 51 | 52 | @pytest.mark.parametrize( 53 | 'raw_text,new_text,expected_value', [ 54 | ('1234', '1,234', decimal.Decimal(1234)), 55 | ('1234', '9876.54321', decimal.Decimal('9876.54321')), 56 | ], 57 | ) 58 | def test_set_raw_text(self, raw_text: str, new_text: str, expected_value: decimal.Decimal) -> None: 59 | token = self.parser.parse_token(raw_text, models.Number) 60 | token.raw_text = new_text 61 | assert token.raw_text == new_text 62 | assert token.value == expected_value 63 | 64 | @pytest.mark.parametrize( 65 | 'raw_text,new_value,expected_text', [ 66 | ('1,234', decimal.Decimal(1234), '1234'), 67 | ('1234', decimal.Decimal('9876.54321'), '9876.54321'), 68 | ], 69 | ) 70 | def test_set_value(self, raw_text: str, new_value: decimal.Decimal, expected_text: str) -> None: 71 | token = self.parser.parse_token(raw_text, models.Number) 72 | token.value = new_value 73 | assert token.value == new_value 74 | assert token.raw_text == expected_text 75 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/option_test.py: -------------------------------------------------------------------------------- 1 | 2 | from lark import exceptions 3 | import pytest 4 | from autobean_refactor import models 5 | from .. import base 6 | 7 | 8 | class TestOption(base.BaseTestModel): 9 | 10 | @pytest.mark.parametrize( 11 | 'text,key,value', [ 12 | ('option "foo" "bar"', 'foo', 'bar'), 13 | ('option "foo" "multiple\nlines"', 'foo', 'multiple\nlines'), 14 | ], 15 | ) 16 | def test_parse_success(self, text: str, key: str, value: str) -> None: 17 | option = self.parser.parse(text, models.Option) 18 | assert option.raw_key.value == key 19 | assert option.raw_value.value == value 20 | assert self.print_model(option) == text 21 | self.check_deepcopy_tree(option) 22 | self.check_reattach_tree(option) 23 | self.check_iter_children_formatted(option) 24 | 25 | @pytest.mark.parametrize( 26 | 'text', [ 27 | ' option "foo" "bar"', 28 | 'optIon "foo" "bar"', 29 | 'option "foo"\n"bar"', 30 | 'option "foo" "bar" "baz"', 31 | 'option "foo"', 32 | 'option ', 33 | ], 34 | ) 35 | def test_parse_failure(self, text: str) -> None: 36 | with pytest.raises(exceptions.UnexpectedInput): 37 | self.parser.parse(text, models.Option) 38 | 39 | def test_set_raw_key(self) -> None: 40 | option = self.parser.parse('option "key" "value"', models.Option) 41 | new_key = models.EscapedString.from_value('new_key') 42 | option.raw_key = new_key 43 | assert option.raw_key is new_key 44 | assert self.print_model(option) == 'option "new_key" "value"' 45 | 46 | def test_set_key(self) -> None: 47 | option = self.parser.parse('option "key" "value"', models.Option) 48 | assert option.key == 'key' 49 | option.key = 'new_key' 50 | assert option.key == 'new_key' 51 | assert self.print_model(option) == 'option "new_key" "value"' 52 | 53 | def test_set_raw_value(self) -> None: 54 | option = self.parser.parse('option "key" "value"', models.Option) 55 | new_value = models.EscapedString.from_value('new_value') 56 | option.raw_value = new_value 57 | assert option.raw_value is new_value 58 | assert self.print_model(option) == 'option "key" "new_value"' 59 | 60 | def test_set_value(self) -> None: 61 | option = self.parser.parse('option "key" "value"', models.Option) 62 | assert option.key == 'key' 63 | option.key = 'new_key' 64 | assert option.key == 'new_key' 65 | assert self.print_model(option) == 'option "new_key" "value"' 66 | 67 | def test_noop_set_raw_key(self) -> None: 68 | option = self.parser.parse('option "key" "value"', models.Option) 69 | initial_key = option.raw_key 70 | option.raw_key = option.raw_key 71 | assert option.raw_key is initial_key 72 | assert self.print_model(option) == 'option "key" "value"' 73 | 74 | def test_reuse_active_token(self) -> None: 75 | option = self.parser.parse('option "key" "value"', models.Option) 76 | with pytest.raises(ValueError): 77 | option.raw_key = option.raw_value 78 | 79 | def test_reuse_inactive_token(self) -> None: 80 | option = self.parser.parse('option "key" "value"', models.Option) 81 | initial_key = option.raw_key 82 | option.raw_key = models.EscapedString.from_value('new_key') 83 | option.raw_key = initial_key 84 | assert option.raw_key is initial_key 85 | assert self.print_model(option) == 'option "key" "value"' 86 | 87 | def test_from_children(self) -> None: 88 | key = models.EscapedString.from_value('foo') 89 | value = models.EscapedString.from_value('bar') 90 | option = models.Option.from_children(key, value) 91 | assert option.raw_key is key 92 | assert option.raw_value is value 93 | assert self.print_model(option) == 'option "foo" "bar"' 94 | self.check_consistency(option) 95 | 96 | def test_from_value(self) -> None: 97 | option = models.Option.from_value('foo', 'bar') 98 | assert option.raw_key.value == 'foo' 99 | assert option.raw_value.value == 'bar' 100 | assert self.print_model(option) == 'option "foo" "bar"' 101 | self.check_consistency(option) 102 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/popmeta_test.py: -------------------------------------------------------------------------------- 1 | 2 | from lark import exceptions 3 | import pytest 4 | from autobean_refactor import models 5 | from .. import base 6 | 7 | 8 | class TestPopmeta(base.BaseTestModel): 9 | 10 | @pytest.mark.parametrize( 11 | 'text,key', [ 12 | ('popmeta foo:', 'foo'), 13 | ('popmeta\t foo:', 'foo'), 14 | ], 15 | ) 16 | def test_parse_success(self, text: str, key: str) -> None: 17 | popmeta = self.parser.parse(text, models.Popmeta) 18 | assert popmeta.raw_key.value == key 19 | assert self.print_model(popmeta) == text 20 | self.check_deepcopy_tree(popmeta) 21 | self.check_reattach_tree(popmeta) 22 | self.check_iter_children_formatted(popmeta) 23 | 24 | @pytest.mark.parametrize( 25 | 'text', [ 26 | 'popMeta foo:', 27 | 'popmeta foo', 28 | 'popmeta ', 29 | ' popmeta foo:', 30 | 'popmeta foo: 123', 31 | ], 32 | ) 33 | def test_parse_failure(self, text: str) -> None: 34 | with pytest.raises(exceptions.UnexpectedInput): 35 | self.parser.parse(text, models.Popmeta) 36 | 37 | def test_set_raw_key(self) -> None: 38 | popmeta = self.parser.parse('popmeta foo:', models.Popmeta) 39 | new_key = models.MetaKey.from_value('bar') 40 | popmeta.raw_key = new_key 41 | assert popmeta.raw_key is new_key 42 | assert self.print_model(popmeta) == 'popmeta bar:' 43 | 44 | def test_set_key(self) -> None: 45 | popmeta = self.parser.parse('popmeta foo:', models.Popmeta) 46 | assert popmeta.key == 'foo' 47 | popmeta.key = 'bar' 48 | assert popmeta.key == 'bar' 49 | assert self.print_model(popmeta) == 'popmeta bar:' 50 | 51 | def test_from_children(self) -> None: 52 | popmeta = models.Popmeta.from_children(models.MetaKey.from_value('foo')) 53 | assert popmeta.raw_key.value == 'foo' 54 | assert self.print_model(popmeta) == 'popmeta foo:' 55 | self.check_consistency(popmeta) 56 | 57 | def test_from_value(self) -> None: 58 | popmeta = models.Popmeta.from_value('foo') 59 | assert popmeta.raw_key.value == 'foo' 60 | assert self.print_model(popmeta) == 'popmeta foo:' 61 | self.check_consistency(popmeta) 62 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/poptag_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestPoptag(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text,tag', [ 11 | ('poptag #foo', 'foo'), 12 | ('poptag\t#foo', 'foo'), 13 | ], 14 | ) 15 | def test_parse_success(self, text: str, tag: str) -> None: 16 | poptag = self.parser.parse(text, models.Poptag) 17 | assert poptag.raw_tag.value == tag 18 | assert self.print_model(poptag) == text 19 | self.check_deepcopy_tree(poptag) 20 | self.check_reattach_tree(poptag) 21 | self.check_iter_children_formatted(poptag) 22 | 23 | @pytest.mark.parametrize( 24 | 'text', [ 25 | 'popTag #foo', 26 | 'poptag foo', 27 | 'poptag ', 28 | ' poptag #foo', 29 | ], 30 | ) 31 | def test_parse_failure(self, text: str) -> None: 32 | with pytest.raises(exceptions.UnexpectedInput): 33 | self.parser.parse(text, models.Poptag) 34 | 35 | def test_set_raw_tag(self) -> None: 36 | poptag = self.parser.parse('poptag #foo', models.Poptag) 37 | new_tag = models.Tag.from_value('bar') 38 | poptag.raw_tag = new_tag 39 | assert poptag.raw_tag is new_tag 40 | assert self.print_model(poptag) == 'poptag #bar' 41 | 42 | def test_set_tag(self) -> None: 43 | poptag = self.parser.parse('poptag #foo', models.Poptag) 44 | assert poptag.tag == 'foo' 45 | poptag.tag = 'bar' 46 | assert poptag.tag == 'bar' 47 | assert self.print_model(poptag) == 'poptag #bar' 48 | 49 | def test_from_children(self) -> None: 50 | tag = models.Tag.from_value('foo') 51 | poptag = models.Poptag.from_children(tag) 52 | assert poptag.raw_tag is tag 53 | assert self.print_model(poptag) == 'poptag #foo' 54 | self.check_consistency(poptag) 55 | 56 | def test_from_value(self) -> None: 57 | poptag = models.Poptag.from_value('foo') 58 | assert poptag.raw_tag.value == 'foo' 59 | assert self.print_model(poptag) == 'poptag #foo' 60 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/posting_flag_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestPostingFlag(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text', '*!&#?%PSTCURM', 11 | ) 12 | def test_parse_success(self, text: str) -> None: 13 | flag = self.parser.parse_token(text, models.PostingFlag) 14 | assert flag.raw_text == text 15 | self.check_deepcopy_token(flag) 16 | 17 | @pytest.mark.parametrize( 18 | 'text', [ 19 | 'txn', 20 | '**', 21 | '!!', 22 | 'A' 23 | ], 24 | ) 25 | def test_parse_failure(self, text: str) -> None: 26 | with pytest.raises(exceptions.UnexpectedInput): 27 | self.parser.parse_token(text, models.PostingFlag) 28 | 29 | @pytest.mark.parametrize( 30 | 'text,new_text', [ 31 | ('*', '!'), 32 | ('!', '*'), 33 | ], 34 | ) 35 | def test_set_raw_text(self, text: str, new_text: str) -> None: 36 | flag = self.parser.parse_token(text, models.PostingFlag) 37 | assert flag.raw_text == text 38 | flag.raw_text = new_text 39 | assert flag.raw_text == new_text 40 | 41 | @pytest.mark.parametrize( 42 | 'text,new_value', [ 43 | ('*', '!'), 44 | ('!', '*'), 45 | ], 46 | ) 47 | def test_set_value(self, text: str, new_value: str) -> None: 48 | flag = self.parser.parse_token(text, models.PostingFlag) 49 | assert flag.value == text 50 | flag.value = new_value 51 | assert flag.value == new_value 52 | assert flag.raw_text == new_value 53 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/pushtag_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestPushtag(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text,tag', [ 11 | ('pushtag #foo', 'foo'), 12 | ('pushtag\t#foo', 'foo'), 13 | ], 14 | ) 15 | def test_parse_success(self, text: str, tag: str) -> None: 16 | pushtag = self.parser.parse(text, models.Pushtag) 17 | assert pushtag.raw_tag.value == tag 18 | assert self.print_model(pushtag) == text 19 | self.check_deepcopy_tree(pushtag) 20 | self.check_reattach_tree(pushtag) 21 | self.check_iter_children_formatted(pushtag) 22 | 23 | @pytest.mark.parametrize( 24 | 'text', [ 25 | 'pushTag #foo', 26 | 'pushtag foo', 27 | 'pushtag ', 28 | ' pushtag #foo', 29 | ], 30 | ) 31 | def test_parse_failure(self, text: str) -> None: 32 | with pytest.raises(exceptions.UnexpectedInput): 33 | self.parser.parse(text, models.Pushtag) 34 | 35 | def test_set_raw_tag(self) -> None: 36 | pushtag = self.parser.parse('pushtag #foo', models.Pushtag) 37 | new_tag = models.Tag.from_value('bar') 38 | pushtag.raw_tag = new_tag 39 | assert pushtag.raw_tag is new_tag 40 | assert self.print_model(pushtag) == 'pushtag #bar' 41 | 42 | def test_set_tag(self) -> None: 43 | pushtag = self.parser.parse('pushtag #foo', models.Pushtag) 44 | assert pushtag.tag == 'foo' 45 | pushtag.tag = 'bar' 46 | assert pushtag.tag == 'bar' 47 | assert self.print_model(pushtag) == 'pushtag #bar' 48 | 49 | def test_from_children(self) -> None: 50 | tag = models.Tag.from_value('foo') 51 | pushtag = models.Pushtag.from_children(tag) 52 | assert pushtag.raw_tag is tag 53 | assert self.print_model(pushtag) == 'pushtag #foo' 54 | self.check_consistency(pushtag) 55 | 56 | def test_from_value(self) -> None: 57 | pushtag = models.Pushtag.from_value('foo') 58 | assert pushtag.raw_tag.value == 'foo' 59 | assert self.print_model(pushtag) == 'pushtag #foo' 60 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/tag_test.py: -------------------------------------------------------------------------------- 1 | 2 | from lark import exceptions 3 | import pytest 4 | from autobean_refactor import models 5 | from .. import base 6 | 7 | 8 | class TestTag(base.BaseTestModel): 9 | 10 | @pytest.mark.parametrize( 11 | 'text,value', [ 12 | ('#foo', 'foo'), 13 | ('#XX.YY', 'XX.YY'), 14 | ('#000', '000'), 15 | ], 16 | ) 17 | def test_parse_success(self, text: str, value: str) -> None: 18 | token = self.parser.parse_token(text, models.Tag) 19 | assert token.raw_text == text 20 | assert token.value == value 21 | self.check_deepcopy_token(token) 22 | 23 | @pytest.mark.parametrize( 24 | 'text', [ 25 | 'foo', 26 | '#标签', 27 | '#x!', 28 | '^foo', 29 | '##foo', 30 | ], 31 | ) 32 | def test_parse_failure(self, text: str) -> None: 33 | with pytest.raises(exceptions.UnexpectedInput): 34 | self.parser.parse_token(text, models.Tag) 35 | 36 | def test_set_raw_text(self) -> None: 37 | token = self.parser.parse_token('#foo', models.Tag) 38 | token.raw_text = '#bar' 39 | assert token.raw_text == '#bar' 40 | assert token.value == 'bar' 41 | 42 | def test_set_value(self) -> None: 43 | token = self.parser.parse_token('#foo', models.Tag) 44 | token.value = 'bar' 45 | assert token.value == 'bar' 46 | assert token.raw_text == '#bar' 47 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/transaction_flag_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestTransactionFlag(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text', '*!&#?%PSTCURM', 11 | ) 12 | def test_parse_success(self, text: str) -> None: 13 | flag = self.parser.parse_token(text, models.TransactionFlag) 14 | assert flag.raw_text == text 15 | assert flag.value == text 16 | self.check_deepcopy_token(flag) 17 | 18 | def test_parse_success_txn(self) -> None: 19 | flag = self.parser.parse_token('txn', models.TransactionFlag) 20 | assert flag.raw_text == 'txn' 21 | assert flag.value == '*' 22 | 23 | @pytest.mark.parametrize( 24 | 'text', [ 25 | 'TXN', 26 | '**', 27 | '!!', 28 | 'A' 29 | ], 30 | ) 31 | def test_parse_failure(self, text: str) -> None: 32 | with pytest.raises(exceptions.UnexpectedInput): 33 | self.parser.parse_token(text, models.TransactionFlag) 34 | 35 | @pytest.mark.parametrize( 36 | 'text,new_text', [ 37 | ('*', '!'), 38 | ('!', '*'), 39 | ('*', 'txn'), 40 | ('!', 'txn'), 41 | ('txn', '!'), 42 | ('txn', '*'), 43 | ], 44 | ) 45 | def test_set_raw_text(self, text: str, new_text: str) -> None: 46 | flag = self.parser.parse_token(text, models.TransactionFlag) 47 | assert flag.raw_text == text 48 | flag.raw_text = new_text 49 | assert flag.raw_text == new_text 50 | 51 | @pytest.mark.parametrize( 52 | 'text,expected_value,new_value', [ 53 | ('*', '*', '!'), 54 | ('!', '!', '*'), 55 | ('txn', '*', '!'), 56 | ('txn', '*', '*'), 57 | ], 58 | ) 59 | def test_set_value(self, text: str, expected_value: str, new_value: str) -> None: 60 | flag = self.parser.parse_token(text, models.TransactionFlag) 61 | assert flag.value == expected_value 62 | flag.value = new_value 63 | assert flag.value == new_value 64 | assert flag.raw_text == new_value 65 | -------------------------------------------------------------------------------- /autobean_refactor/tests/models/whitespace_test.py: -------------------------------------------------------------------------------- 1 | from lark import exceptions 2 | import pytest 3 | from autobean_refactor import models 4 | from .. import base 5 | 6 | 7 | class TestWhitespace(base.BaseTestModel): 8 | 9 | @pytest.mark.parametrize( 10 | 'text', [ 11 | ' ', 12 | ' ', 13 | '\t', 14 | '\t\t\t\t', 15 | ' \t \t', 16 | ], 17 | ) 18 | def test_parse_success(self, text: str) -> None: 19 | token = self.parser.parse_token(text, models.Whitespace) 20 | assert token.raw_text == text 21 | self.check_deepcopy_token(token) 22 | 23 | @pytest.mark.parametrize( 24 | 'text', [ 25 | '' 26 | '\n', 27 | '\u00a0', 28 | '\u2000', 29 | ], 30 | ) 31 | def test_parse_failure(self, text: str) -> None: 32 | with pytest.raises(exceptions.UnexpectedInput): 33 | self.parser.parse_token(text, models.Whitespace) 34 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: autobean-refactor 2 | author: SEIAROTg 3 | 4 | execute: 5 | execute_notebooks: force 6 | 7 | repository: 8 | url: https://github.com/SEIAROTg/autobean-refactor 9 | path_to_book: docs 10 | branch: main 11 | 12 | html: 13 | use_issues_button: true 14 | use_repository_button: true 15 | 16 | sphinx: 17 | extra_extensions: 18 | - 'sphinx.ext.autodoc' 19 | - 'sphinx.ext.autosummary' 20 | - 'sphinx.ext.napoleon' 21 | - 'sphinx.ext.intersphinx' 22 | - 'sphinx.ext.viewcode' 23 | local_extensions: 24 | # Ideally `local: '_extensions/'` but that doesn't work with `jupyter-book config` 25 | '_extensions.local': './' 26 | config: 27 | myst_heading_anchors: 2 28 | intersphinx_mapping: 29 | python: ['https://docs.python.org/3', null] 30 | add_module_names: false 31 | templates_path: 32 | - '_templates' 33 | autodoc_class_signature: separated 34 | autodoc_inherit_docstrings: false 35 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | .bd-main { 2 | flex-grow: 1 !important; 3 | } 4 | -------------------------------------------------------------------------------- /docs/_static/wrap-parameters.css: -------------------------------------------------------------------------------- 1 | .sig { 2 | padding-left: 4rem; 3 | } 4 | 5 | .sig>:first-child { 6 | margin-left: -4rem; 7 | } 8 | 9 | .sig-param::before { 10 | content: "\a"; 11 | white-space: pre; 12 | } 13 | 14 | .sig>.sig-param:last-of-type::after { 15 | content: ",\a"; 16 | white-space: pre; 17 | } 18 | 19 | .sig-param+.sig-paren { 20 | margin-left: -4rem; 21 | } 22 | 23 | .sig-param>:first-child { 24 | margin-left: -2rem; 25 | } 26 | -------------------------------------------------------------------------------- /docs/_templates/model.rst: -------------------------------------------------------------------------------- 1 | {{ objname | underline }} 2 | 3 | .. currentmodule:: autobean_refactor.models 4 | 5 | .. autoclass:: {{ objname }} 6 | :exclude-members: __init__, store_handle, token_store, first_token, last_token, detach, clone, iter_children_formatted, reattach, size, tokens, from_parsed_children, RULE, INLINE 7 | :inherited-members: 8 | :undoc-members: 9 | -------------------------------------------------------------------------------- /docs/_templates/type-alias.rst: -------------------------------------------------------------------------------- 1 | {{ objname | underline }} 2 | 3 | .. currentmodule:: autobean_refactor.models 4 | 5 | .. autoclass:: {{ objname }} 6 | -------------------------------------------------------------------------------- /docs/_toc.yml: -------------------------------------------------------------------------------- 1 | format: jb-book 2 | root: getting-started 3 | parts: 4 | - caption: 5 | chapters: 6 | - file: examples 7 | - file: guarantees 8 | - caption: Generic interfaces 9 | chapters: 10 | - file: generic/models 11 | - file: generic/token-models 12 | - file: generic/tree-models 13 | - caption: Sepcial interfaces 14 | chapters: 15 | - file: special/meta 16 | - file: special/numbers 17 | - file: special/prices-costs 18 | - file: special/comments 19 | - file: special/spacing 20 | - file: special/indents 21 | - caption: References 22 | chapters: 23 | - file: references/token-models 24 | - file: references/tree-models 25 | - file: references/type-aliases 26 | -------------------------------------------------------------------------------- /docs/code/basics.py: -------------------------------------------------------------------------------- 1 | %xmode minimal 2 | import copy 3 | import datetime 4 | import decimal 5 | import io 6 | from autobean_refactor import models, parser, printer 7 | 8 | p = parser.Parser() 9 | 10 | 11 | def _print_model(model: models.RawModel) -> None: 12 | print(printer.print_model(model, io.StringIO()).getvalue()) 13 | -------------------------------------------------------------------------------- /docs/code/parsing.py: -------------------------------------------------------------------------------- 1 | from autobean_refactor import models, parser 2 | 3 | p = parser.Parser() 4 | 5 | # Most commonly, you'll want to parse a file 6 | file = p.parse('2000-01-01 open Assets:Foo\n2000-01-02 close Assets:Foo', models.File) 7 | 8 | # You can parse into other models as well 9 | close = p.parse('2000-01-02 close Assets:Foo', models.Close) 10 | 11 | # Token needs to be parsed with `parse_token` instead. 12 | comment = p.parse_token('; comment', models.BlockComment) 13 | string1 = p.parse_token(r'"foo\n"', models.EscapedString) 14 | string2 = p.parse_token('"foo\n"', models.EscapedString) 15 | number = p.parse_token('1,234.56', models.Number) 16 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Examples 8 | 9 | ```{code-cell} python 10 | :load: ./code/basics.py 11 | :tags: [hide-input, remove-output] 12 | ``` 13 | 14 | ## Rename accounts 15 | 16 | ```{code-cell} python 17 | def rename_account(model: models.RawModel, from_account: str, to_account: str): 18 | for token in model.tokens: 19 | if isinstance(token, models.Account) and token.value == from_account: 20 | token.value = to_account 21 | 22 | 23 | file = p.parse('''\ 24 | ; leading comment 25 | 2000-01-01 open Assets:Foo ; inline comment 26 | 2000-01-01 open Assets:Bar 27 | 2000-01-01 * 28 | Assets:Foo -100.00 USD 29 | Assets:Bar 100.00 USD 30 | 2000-01-02 balance Assets:Foo -100.00 USD 31 | ''', models.File) 32 | 33 | rename_account(file, 'Assets:Foo', 'Liabilities:Foo') 34 | 35 | _print_model(file) 36 | ``` 37 | 38 | ## Rename accounts based on narration 39 | 40 | ```{code-cell} python 41 | file = p.parse('''\ 42 | 2000-01-01 * "groceries" 43 | Assets:Foo -100.00 USD 44 | Expenses:Unclassified 100.00 USD ; inline comment 45 | ; trailing comment 46 | 2000-01-01 * "fuel" 47 | Assets:Foo -100.00 USD 48 | Expenses:Unclassified 100.00 USD 49 | 2000-01-01 * 50 | Assets:Foo -100.00 USD 51 | Expenses:Unclassified 100.00 USD 52 | ''', models.File) 53 | 54 | for directive in file.directives: 55 | if isinstance(directive, models.Transaction) and directive.narration == 'groceries': 56 | rename_account(directive, 'Expenses:Unclassified', 'Expenses:Groceries') 57 | 58 | _print_model(file) 59 | ``` 60 | 61 | ## Reorder meta 62 | 63 | ```{code-cell} python 64 | file = p.parse('''\ 65 | 2000-01-01 * 66 | time: "08:00" 67 | foo: "xxx" 68 | 2000-01-01 * 69 | foo: "xxx" 70 | ; leading comment 71 | time: "10:00" ; inline comment 72 | bar: "xxx" 73 | 2000-01-01 * 74 | foo: "xxx" 75 | ''', models.File) 76 | 77 | for directive in file.directives: 78 | if isinstance(directive, models.Transaction): 79 | # make "time" the first meta 80 | time_meta = directive.raw_meta.pop('time', None) 81 | if time_meta: 82 | directive.raw_meta.insert(0, time_meta) 83 | 84 | _print_model(file) 85 | ``` 86 | -------------------------------------------------------------------------------- /docs/generic/models.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Models 8 | 9 | ```{code-cell} python 10 | :load: ../code/basics.py 11 | :tags: [remove-cell] 12 | ``` 13 | 14 | Models model beancount AST nodes. There are two types of models, [token models](./token-models.md) and [tree models](./tree-models.md). 15 | 16 | ## Common interfaces 17 | 18 | The following interfaces are shared by all models, whether token or not. 19 | 20 | ### `__deepcopy__` 21 | 22 | All models can be deep-copied, which makes an exact copy of everything in the model, into a separate token store. 23 | 24 | ```{code-cell} python 25 | :tags: [raises-exception] 26 | file = p.parse('''\ 27 | 2000-01-01 * ; inline comment 28 | 2000-01-02 * 29 | ''', models.File) 30 | file.directives.append(file.directives[0]) 31 | ``` 32 | 33 | ```{code-cell} python 34 | txn_copy = copy.deepcopy(file.directives[0]) 35 | file.directives.append(txn_copy) 36 | 37 | _print_model(file) 38 | ``` 39 | 40 | ### `__eq__` 41 | 42 | All models supports equality check. Two models are only equal if and only if: 43 | 44 | * They have the exact same type, and 45 | * They have the exact same text representations, and 46 | * They have the exact same structure. 47 | 48 | ### `tokens` 49 | 50 | All models have a property `tokens` which returns a list of tokens inside that model. For a token, this returns itself. 51 | 52 | ```{code-cell} python 53 | close = p.parse('2000-01-01 close Assets:Foo ; inline comment', models.Close) 54 | close.tokens 55 | ``` 56 | 57 | ```{code-cell} python 58 | close.raw_inline_comment.tokens 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/generic/token-models.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Token Models 8 | 9 | Token models model leaf nodes in the AST, who may not have children. 10 | 11 | ```{code-cell} python 12 | :load: ../code/parsing.py 13 | :tags: [hide-input] 14 | ``` 15 | 16 | ## Common interfaces 17 | 18 | ### `raw_text` 19 | 20 | All token models have a string property `raw_text` which returns the raw text of the token. 21 | 22 | `value` usually gives a more useful representation of the token, but `raw_text` allows more precise manipulation of the token. 23 | 24 | ```{code-cell} python 25 | (comment.raw_text, string1.raw_text, string2.raw_text, number.raw_text) 26 | ``` 27 | 28 | ### `value` 29 | 30 | Many token models have a property `value` which returns the semantic value of the token. 31 | 32 | ```{code-cell} python 33 | (comment.value, string1.value, string2.value, number.value) 34 | ``` 35 | 36 | ### `from_raw_text` 37 | 38 | All token models have a class method `from_raw_text` which constructs a new token model from the raw text. 39 | 40 | This constructs a token model with the exact raw text, and therefore: 41 | * ✅ `type(token).from_raw_text(token.raw_text) == token` 42 | * ✅ `type(token).from_value(token.value).value == token.value` (if `value` is supported) 43 | 44 | ```python 45 | >>> comment == models.BlockComment.from_raw_text('; comment') 46 | True 47 | >>> comment == models.BlockComment.from_raw_text(';comment') 48 | False 49 | ``` 50 | 51 | ### `from_value` 52 | 53 | Many token models have a class method `from_value` which constructs a new token model from the semantic value. 54 | 55 | Some default formatting will apply, and therefore: 56 | * ❌ `type(token).from_value(token.value) == token` 57 | * ✅ `type(token).from_value(x).value == token.value` 58 | 59 | ```python 60 | >>> models.BlockComment.from_value('comment').raw_text 61 | '; comment' 62 | >>> models.EscapedString.from_value('foo\n').raw_text 63 | 'foo\n' 64 | >>> models.Number.from_value(decimal.Decimal('1234.56')).raw_text 65 | '1234.56' 66 | ``` 67 | 68 | ## List of all token models 69 | 70 | ```{code-cell} python 71 | sorted([ 72 | model.__name__ for model in models.__dict__.values() 73 | if isinstance(model, type) and issubclass(model, models.RawTokenModel)]) 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/generic/tree-models.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Tree Models 8 | 9 | ```{code-cell} python 10 | :load: ../code/basics.py 11 | :tags: [remove-cell] 12 | ``` 13 | 14 | Tree models model the AST nodes that may have children. 15 | 16 | ## Raw and value properties 17 | 18 | Tree models have many properties, some have a `raw_` prefix (raw properties) and many raw properties have a corresponding value property without the `raw_` prefix. The raw properties provide raw access to children, while the value properties provide access to the semantic value of those children. For example: 19 | 20 | ```{code-cell} python 21 | balance = p.parse('2000-01-01 balance Assets:Foo 100 * 2.00 USD ; comment', models.Balance) 22 | _print_model(balance.raw_number) 23 | ``` 24 | 25 | ```{code-cell} python 26 | (balance.number, balance.raw_number.value) 27 | ``` 28 | 29 | Value properties should suffice for most cases but raw properties are provided to allow more fine-grained and lossless access to the tree structure, for example, when you want to access the expression above. 30 | 31 | Value properties sometimes also returns models, usually to make distinction between different types of values (e.g. unit cost vs total cost; string vs currency). 32 | 33 | Not all raw properties have corresponding value properties. For example, `File.raw_directives_with_comments` doesn't have a obvious way to simplify. 34 | 35 | ## Required properties 36 | 37 | Some children of a model is required by the beancount syntax, and those children are modeled in required properties. 38 | 39 | Required properties always have values. They may be read, updated, but cannot be deleted, or created. 40 | 41 | ```{code-cell} python 42 | (balance.raw_date, balance.date) 43 | ``` 44 | 45 | ```{code-cell} python 46 | balance.date = datetime.date(2000, 1, 2) 47 | balance.raw_currency = models.Currency.from_value('GBP') 48 | 49 | _print_model(balance) 50 | ``` 51 | 52 | ## Optional properties 53 | 54 | Some children of a model is optional, and those children are modeled in optional properties. 55 | 56 | Optional properties may be `None`, and may be set to and from `None` in order to delete or create them. 57 | 58 | ```{code-cell} python 59 | balance.raw_inline_comment = None 60 | balance.tolerance = decimal.Decimal('0.01') 61 | 62 | _print_model(balance) 63 | ``` 64 | 65 | ## Repeated properties 66 | 67 | Repeated properties returns a wrapper that implements {py:class}`MutableSequence `. 68 | 69 | ```{code-cell} python 70 | custom = p.parse('2000-01-01 custom "type" 123.45 Assets:Foo TRUE "foo"', models.Custom) 71 | 72 | (custom.values[1], list(custom.raw_values)) 73 | ``` 74 | 75 | ```{code-cell} python 76 | custom.values[3] = datetime.date(2000, 1, 2) 77 | del custom.values[1:3] 78 | 79 | _print_model(custom) 80 | ``` 81 | 82 | ### Filtered repeated properties 83 | 84 | Filtered repeated properties is a special type of repeated properties. This can be useful when the gramma allows multiple types of children to interleave, but you are only interested in a certain type. 85 | 86 | ```{code-cell} python 87 | note = p.parse('2000-01-01 note Assets:Foo "note" #tag0 ^link0 #tag1 ^link1', models.Note) 88 | note.tags[1] = 'updated' 89 | 90 | _print_model(note) 91 | ``` 92 | 93 | ## Construction 94 | 95 | Most tree models can be constructed with class methods `from_children`, which takes children models, or `from_value`, which takes semantic values. Refer to the method signature for accepted arguments. 96 | 97 | ```{warning} 98 | Do not construct models directly with `ModelType(...)`. 99 | ``` 100 | 101 | ```{code-cell} python 102 | close1 = models.Close.from_children( 103 | date=models.Date.from_value(datetime.date(2000, 1, 2)), 104 | account=models.Account.from_value('Assets:Foo'), 105 | inline_comment=models.InlineComment.from_value('comment'), 106 | ) 107 | close2 = models.Close.from_value( 108 | date=datetime.date(2000, 1, 2), 109 | account='Assets:Foo', 110 | inline_comment='comment', 111 | ) 112 | _print_model(close1) 113 | _print_model(close2) 114 | ``` 115 | 116 | ## List of all tree models 117 | 118 | ```{code-cell} python 119 | sorted([ 120 | model.__name__ for model in models.__dict__.values() 121 | if isinstance(model, type) and issubclass(model, models.RawTreeModel)]) 122 | ``` 123 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Getting started 8 | 9 | autobean-refactor is an ergonomic and lossless beancount manipulation library, offering easy interfaces to parse, manipulate, construct, and print [beancount](https://github.com/beancount/beancount) ledger. 10 | 11 | ## Install 12 | 13 | ```sh 14 | pip install autobean-refactor 15 | ``` 16 | 17 | ## Parsing 18 | 19 | ```{code-cell} python 20 | :load: ./code/parsing.py 21 | ``` 22 | 23 | ## Printing 24 | 25 | ```{code-cell} python 26 | import io 27 | from autobean_refactor import printer 28 | 29 | printer.print_model(file, io.StringIO()).getvalue() 30 | ``` 31 | 32 | ## In-place editing 33 | 34 | ```{code-cell}python 35 | import pathlib 36 | import tempfile 37 | from autobean_refactor import editor, models 38 | 39 | e = editor.Editor() 40 | 41 | with tempfile.TemporaryDirectory() as tmpdir: 42 | p = pathlib.Path(tmpdir) 43 | 44 | # creates some files 45 | (p / 'index.bean').write_text('include "??.bean"') 46 | (p / '01.bean').write_text('2000-01-01 *') 47 | (p / '02.bean').write_text('2000-01-01 *') 48 | 49 | # edits a single file 50 | with e.edit_file(p / '01.bean') as file: 51 | # appends a comment to 01.bean 52 | file.raw_directives_with_comments.append(models.BlockComment.from_value('updated1')) 53 | 54 | # edits files recursively (follows includes) 55 | with e.edit_file_recursive(p / 'index.bean') as files: 56 | # appends a comment to 01.bean 57 | files[str(p / '01.bean')].raw_directives_with_comments.append( 58 | models.BlockComment.from_value('updated2')) 59 | # deletes 02.bean 60 | files.pop(str(p / '02.bean')) 61 | 62 | for path in p.iterdir(): 63 | print(f'{"=" * 20}[{path}]{"=" * 20}') 64 | print((p / path).read_text()) 65 | ``` 66 | 67 | ```{tableofcontents} 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/guarantees.md: -------------------------------------------------------------------------------- 1 | # Guarantees 2 | 3 | `autobean-refactor` is a **lossless** beancount manipulation library by providing the following guarantees, not subject to legal liability: 4 | * ✅ If no operations are performed, the output is **character-by-character** idential to the input. 5 | * ✅ If a fragment is modified, everything outside that fragment remains character-by-character identical to the input. 6 | * ✅ If a fragment is added or removed, everything outside that fragment remains character-by-character identical to the input, except that its surrounding spaces may be added or removed. 7 | 8 | Notably, the following are usually true but **NOT** 100% guaranteed: 9 | * ❗ `autobean-refactor` and beancount v2 parser has exactly same grammar. 10 | * Out-of-line tags / links in transaction are not supported yet. 11 | * `autobean-refactor` is based on Unicode while the beancount v2 parser is based on bytes. 12 | * ❗ The output is always syntatically valid. 13 | * It's possible to remove necessary spaces or indent and make it no longer valid. 14 | * It's possible to forcefully put string into a number token and make it no longer valid. 15 | * ❗ If no operations are performed, the output is **byte-to-byte** identical to the input. 16 | * `autobean-refactor` is based on Unicode and does not guarantee byte-level preservation. 17 | * ❗ `model.value = model.value` does not change anything. 18 | * [Example](./special/numbers.md#value-access). 19 | -------------------------------------------------------------------------------- /docs/references/.gitignore: -------------------------------------------------------------------------------- 1 | token-models 2 | tree-models 3 | -------------------------------------------------------------------------------- /docs/references/token-models.md: -------------------------------------------------------------------------------- 1 | # Token Models 2 | 3 | ```{eval-rst} 4 | .. currentmodule:: autobean_refactor.models 5 | ``` 6 | 7 | ```{eval-rst} 8 | .. autobean-refactor-token-models:: 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/references/tree-models.md: -------------------------------------------------------------------------------- 1 | # Tree Models 2 | 3 | ```{eval-rst} 4 | .. currentmodule:: autobean_refactor.models 5 | ``` 6 | 7 | ```{eval-rst} 8 | .. autobean-refactor-tree-models:: 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/references/type-aliases.md: -------------------------------------------------------------------------------- 1 | # Type Aliases 2 | 3 | ```{eval-rst} 4 | .. currentmodule:: autobean_refactor.models 5 | .. autobean-refactor-type-aliases:: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/special/comments.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Comments 8 | 9 | ```{code-cell} python 10 | :load: ../code/basics.py 11 | :tags: [remove-cell] 12 | ``` 13 | 14 | ## Leading, trailing, and inline comments 15 | 16 | Leading, trailing, and inline comments can be accessed similar to other optional string properties, through `raw_leading_comment`, `leading_comment`, `raw_inline_comment`, `inline_comment`, `raw_trailing_comment`, and `trailing_comment` attributes. 17 | 18 | ```{code-cell} python 19 | close = p.parse('''\ 20 | ; leading 21 | 2000-01-01 close Assets:Foo ; inline 22 | ; trailing\ 23 | ''', models.Close) 24 | ``` 25 | 26 | Read: 27 | 28 | ```{code-cell} python 29 | (close.leading_comment, close.inline_comment, close.raw_trailing_comment.raw_text) 30 | ``` 31 | 32 | Write: 33 | 34 | ```{code-cell} python 35 | close.leading_comment = 'updated' 36 | close.inline_comment = None 37 | close.raw_trailing_comment = models.BlockComment.from_raw_text(';nospace') 38 | 39 | _print_model(close) 40 | ``` 41 | 42 | ## Standalone comments 43 | 44 | Some comments don't really belong to a model. These are standalone comments (or sometimes referred to as interleaving comments), which is accessible in the same repeated fields with its peer models. 45 | 46 | ```{code-cell} python 47 | file = p.parse('''\ 48 | ; standalone 49 | 50 | 2000-01-01 open Assets:Foo''', models.File) 51 | ``` 52 | 53 | Read: 54 | 55 | ```{code-cell} python 56 | file.raw_directives_with_comments[0].value 57 | ``` 58 | 59 | Write: 60 | 61 | ```{code-cell} python 62 | file.raw_directives_with_comments[0].value = 'updated' 63 | 64 | _print_model(file) 65 | ``` 66 | 67 | ## Ownership 68 | 69 | Same as other models, comments have unique ownership and can only be accessed through its owner. In the example below, `; inline comment` clearly belongs to `open` and can therefore be accessed through `open.inline_comment`. `; block comment` is more complex. It may be the trailing comment for `open`, or the leading comment for `close`, or a standalone comment between them, but it may only belong to one of them at any given time. 70 | 71 | ```beancount 72 | 2000-01-01 open Assets:Foo ; inline comment 73 | ; block comment 74 | 2000-01-01 close Assets:Foo 75 | ``` 76 | 77 | But which one exactly? As a generic library, `autobean-refactor` doesn't know the answer for sure. It therefore tries to do something reasonable, while allowing manual adjustment if needed. 78 | 79 | ### Automatic comment attribution 80 | 81 | All models have a method `auto_claim_comments` which attributes comments to children models following the default rules: 82 | 83 | * If it's immediately before a model with the same indentation and no blank lines in betwen, it's a leading comment. 84 | * Otherwise, if it's immediately after a model with the same indentation and no blank lines in betwen, it's a trailing comment. 85 | * Otherwise, it's a standalone comment. 86 | 87 | This happens by default at the end of `parse` but you may opt-out with `auto_claim_comments=False`. 88 | 89 | ```{code-cell} python 90 | file = p.parse('''\ 91 | ; standalone 92 | 2000-01-01 open Assets:Foo\ 93 | ''', models.File, auto_claim_comments=False) 94 | 95 | (len(file.raw_directives_with_comments), file.directives[0].leading_comment) 96 | ``` 97 | 98 | ### Manual comment attribution 99 | 100 | If you don't want the default automatic comment attribution, or want to make some adjustments, you may do so with: 101 | * (for leading and trailing comments) `claim_leading_comment`, `unclaim_leading_comment`, `claim_trailing_comment`, and `unclaim_trailing_comment`. 102 | * (for standalone comments) `claim_interleaving_comments`, and `unclaim_interleaving_comments`. 103 | 104 | ```{code-cell} python 105 | file = p.parse('''\ 106 | 2000-01-01 open Assets:Foo 107 | ; comment 108 | 2000-01-02 close Assets:Foo\ 109 | ''', models.File) 110 | 111 | open, close = file.raw_directives_with_comments 112 | close.unclaim_leading_comment() 113 | open.claim_trailing_comment() 114 | 115 | (open.trailing_comment, close.leading_comment) 116 | ``` 117 | 118 | ```{code-cell} python 119 | file.raw_directives_with_comments.claim_interleaving_comments([ 120 | open.unclaim_trailing_comment(), 121 | ]) 122 | 123 | (open.trailing_comment, file.raw_directives_with_comments[1].raw_text) 124 | ``` 125 | -------------------------------------------------------------------------------- /docs/special/indents.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Indent 8 | 9 | ```{code-cell} python 10 | :load: ../code/basics.py 11 | :tags: [remove-cell] 12 | ``` 13 | 14 | In `autobean-refactor`, indent is not considered spacing and handled separately. 15 | 16 | ## The beancount indentation 17 | 18 | The indentation in beancount v2 is handled in a somewhat surprising way. The only distinction is whether a line is indented or not, while the indentation characters / levels don't matter. 19 | 20 | ``````{list-table} 21 | :header-rows: 1 22 | 23 | * - Valid 24 | - Formatted 25 | * - ```beancount 26 | 2000-01-01 * 27 | foo: 1 28 | Assets:Foo 100.00 USD 29 | Assets:Bar -100.00 USD 30 | bar: 4 31 | ```` 32 | - ```beancount 33 | 2000-01-01 * 34 | foo: 1 35 | Assets:Foo 100.00 USD 36 | Assets:Bar -100.00 USD 37 | bar: 4 38 | ``` 39 | `````` 40 | 41 | ## Basic access 42 | 43 | In `autobean-refactor`, indent is a children of indentable models (e.g. {py:class}`Posting`, {py:class}`Meta`, {py:class}`BlockComment`), which can be accessed as a simple string model. 44 | 45 | ```{code-cell} python 46 | txn = p.parse('''\ 47 | 2000-01-01 * 48 | foo: 1 49 | Assets:Foo 100.00 USD 50 | Assets:Bar -100.00 USD 51 | bar: 4''', models.Transaction) 52 | txn.postings[1].indent 53 | ``` 54 | 55 | ```{code-cell} python 56 | txn.postings[1].indent = ' ' * 4 57 | _print_model(txn) 58 | ``` 59 | 60 | Note that the indent of each line is independent and therefore changing the indent of a posting will have no impact on its meta. 61 | 62 | ## `indent_by` 63 | 64 | When new children are added to a repeated field, its indent is determined as follows: 65 | * For raw models, their own indent is used as is; 66 | * Otherwise, if there are existing children, the indent of the last one is copied over; 67 | * Otherwise, the indent is constructed from the parent's indentation and its `indent_by`. 68 | 69 | `indent_by` defaults to four spaces and can be explicitly set at construction in `from_children` or `from_value`, or after construction with the `indent_by` attribute. 70 | 71 | ```{code-cell} python 72 | txn = p.parse('2000-01-01 *', models.Transaction) 73 | txn.indent_by = ' ' * 2 74 | txn.meta['foo'] = 'foo' 75 | txn.indent_by = ' ' * 8 76 | txn.meta['bar'] = 'bar' 77 | _print_model(txn) 78 | ``` 79 | 80 | ```{code-cell} python 81 | txn.meta.clear() 82 | txn.meta['bar'] = 'bar' 83 | _print_model(txn) 84 | ``` 85 | 86 | ```{code-cell} python 87 | txn.meta.clear() 88 | meta_item = models.MetaItem.from_value( 89 | key='qux', 90 | value=decimal.Decimal(4), 91 | indent=' ' * 1) 92 | txn.raw_meta.append(meta_item) 93 | _print_model(txn) 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/special/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Meta 8 | 9 | ```{code-cell} python 10 | :load: ../code/basics.py 11 | :tags: [remove-cell] 12 | ``` 13 | 14 | For models that could carry meta, they are accessible through three attributes: 15 | * `raw_meta_with_comments`: {py:class}`MutableSequence[MetaItem | BlockComment] `, providing by-index access to {py:class}`MetaItem ` and [](../special/comments.md#standalone-comments). 16 | * `raw_meta`: {py:class}`MutableSequence[MetaItem] ` and {py:class}`MutableMapping[str, MetaItem] `, providing by-index and by-key access to {py:class}`MetaItem `. 17 | * `meta`: {py:class}`MutableSequence[MetaItem] ` and {py:class}`MutableMapping[str, MetaValue | MetaRawValue] `, providing by-index access to {py:class}`MetaItem ` and by-key access to {py:class}`MetaValue `. 18 | 19 | ```{code-cell} python 20 | :tags: [remove-output] 21 | 22 | txn = p.parse('''\ 23 | 2000-01-01 * 24 | foo: 1 25 | ; comment 26 | bar: 2''', models.Transaction, auto_claim_comments=False) 27 | txn.raw_meta_with_comments.claim_interleaving_comments() 28 | ``` 29 | 30 | Read: 31 | 32 | ```{code-cell} python 33 | _print_model(txn.raw_meta_with_comments[1]) 34 | _print_model(txn.raw_meta[1]) 35 | _print_model(txn.raw_meta['bar']) 36 | print(txn.raw_meta[1].key) 37 | print(repr(txn.meta['bar'])) 38 | print(list(txn.raw_meta.keys())) 39 | print(list(txn.meta.values())) 40 | ``` 41 | 42 | Write: 43 | 44 | ```{code-cell} python 45 | for i, item in enumerate(txn.raw_meta): 46 | item.key += '-updated' 47 | item.value = str(i) 48 | _print_model(txn) 49 | 50 | txn.raw_meta.pop(0) 51 | txn.meta['baz'] = models.Account.from_value('Assets:Foo') 52 | _print_model(txn) 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/special/numbers.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Numbers 8 | 9 | ```{code-cell} python 10 | :load: ../code/basics.py 11 | :tags: [remove-cell] 12 | ``` 13 | 14 | Beancount supports arithmetic expressions almost wherever numbers are accepted. 15 | 16 | ## Construction 17 | 18 | {py:class}`NumberExpr ` supports construction with {py:meth}`from_value `: 19 | 20 | ```{code-cell} python 21 | expr = models.NumberExpr.from_value(decimal.Decimal(42)) 22 | assert isinstance(expr, models.NumberExpr) 23 | ``` 24 | 25 | ## Value access 26 | 27 | {py:class}`NumberExpr ` supports value reading and writing. Note that value writing essentially replaces the whole expression. 28 | 29 | ```{code-cell} python 30 | expr = p.parse('1+2*3', models.NumberExpr) 31 | _print_model(expr) 32 | print(expr.value) 33 | 34 | expr.value = 8 35 | _print_model(expr) 36 | print(expr.value) 37 | ``` 38 | 39 | ## Arithmetic 40 | 41 | The following arithmetic operations are supported on {py:class}`NumberExpr `: 42 | * `+`, `-`, `*`, `/`, `//` 43 | * `+=`, `-=`, `*=`, `/=`, `//=` 44 | * `+`, `-` (unary) 45 | 46 | The operands may be {py:class}`int`, {py:class}`decimal.Decimal`, or {py:class}`NumberExpr `. 47 | 48 | ```{code-cell} python 49 | expr = p.parse('1*2+3', models.NumberExpr) 50 | expr *= 4 51 | expr += 5 52 | expr = -expr 53 | 54 | _print_model(expr) 55 | print(expr.value) 56 | ``` 57 | 58 | ## Ambiguity 59 | 60 | One confusing issue with beancount arithmetic expressions is about `custom` where multiple of them can appear consecutively. For example, what does the following `custom` encode? 61 | 62 | ```beancount 63 | 2000-01-01 custom "foo" 1 2 -3 64 | ``` 65 | 66 | Somewhat surprisingly: 67 | ```sh 68 | $ echo '2000-01-01 custom "foo" 1 2 -3' | bean-report /dev/stdin print 69 | 2000-01-01 custom "foo" 1 -1 70 | ``` 71 | 72 | If we want the arguments to be `[1, 2, -3]`, it must be represented as `2000-01-01 "foo" 1 2 (-3)`: 73 | ```sh 74 | $ echo '2000-01-01 custom "foo" 1 2 (-3)' | bean-report /dev/stdin print 75 | 2000-01-01 custom "foo" 1 2 -3 76 | ``` 77 | 78 | `autobean-refactor` automatically disambiguates `custom` when constructed with {py:meth}`Custom.from_children ` or {py:meth}`Custom.from_value `: 79 | ```{code-cell} python 80 | custom = models.Custom.from_value( 81 | date=datetime.date(2000, 1, 1), 82 | type='foo', 83 | values=map(decimal.Decimal, [1, 2, -3])) 84 | _print_model(custom) 85 | ``` 86 | 87 | However, that doesn't cover all the cases, you may still accidentally create ambiguity when adding, removing, or modifying arguments, in which case you may manually disambiguate with {py:meth}`NumberExpr.wrap_with_parenthesis `. 88 | 89 | ```{code-cell} python 90 | custom = p.parse('2000-01-01 custom "foo" 1 2', models.Custom) 91 | 92 | custom.values[1] = decimal.Decimal('-2') 93 | _print_model(custom) 94 | 95 | custom.raw_values[1].wrap_with_parenthesis() 96 | _print_model(custom) 97 | -------------------------------------------------------------------------------- /docs/special/prices-costs.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Prices and Costs 8 | 9 | ```{code-cell} python 10 | :load: ../code/basics.py 11 | :tags: [remove-cell] 12 | ``` 13 | 14 | ## Beancount limitation 15 | 16 | One limitation of the official beancount data model is that the distinction of total price and unit price disappears during parsing, and therefore with the official library, there is no way to tell which form the original definition uses, to read the total cost, or to output a total cost. 17 | 18 | ## Basic access 19 | 20 | Fortunately, `autobean-refactor` does not have the same issue. In `autobean-refactor`, there are {py:class}`UnitPrice `, {py:class}`TotalPrice `, {py:class}`UnitCost `, and {py:class}`TotalCost `. The `Unit-` and `Total-` versions are almost identical except being different type, allowing case distinction. 21 | 22 | ## Price access 23 | 24 | To check type: 25 | 26 | ```{code-cell} python 27 | posting = p.parse(' Assets:Foo 100.00 GBP @@ 130.00 USD', models.Posting) 28 | assert isinstance(posting.price, models.TotalPrice) 29 | assert not isinstance(posting.price, models.UnitPrice) 30 | _print_model(posting) 31 | ``` 32 | 33 | To read value: 34 | 35 | ```{code-cell} python 36 | (posting.price.number, posting.price.currency) 37 | ``` 38 | 39 | To modify value: 40 | 41 | ```{code-cell} python 42 | posting.price.number = decimal.Decimal('135.00') 43 | _print_model(posting) 44 | ``` 45 | 46 | To flip type: 47 | 48 | ```{code-cell} python 49 | posting.price = models.UnitPrice.from_value( 50 | number=posting.price.number / posting.number, 51 | currency=posting.price.currency) 52 | _print_model(posting) 53 | ``` 54 | 55 | ## Cost access 56 | 57 | The interface for costs is more complex as costs can carry more information. 58 | 59 | To check type: 60 | 61 | ```{code-cell} python 62 | posting = p.parse( 63 | ' Assets:Foo 100.00 GBP { 1.30 USD, 2000-01-01, "foo", * }', 64 | models.Posting) 65 | assert isinstance(posting.cost.raw_cost, models.UnitCost) 66 | assert not isinstance(posting.cost.raw_cost, models.TotalCost) 67 | _print_model(posting) 68 | ``` 69 | 70 | To read value: 71 | 72 | ```{code-cell} python 73 | (posting.cost.number_per, posting.cost.number_total, posting.cost.currency) 74 | ``` 75 | 76 | To modify value: 77 | 78 | ```{code-cell} python 79 | posting.cost.number_per = decimal.Decimal('1.35') 80 | posting.cost.label = 'bar' 81 | _print_model(posting) 82 | ``` 83 | 84 | To flip type: 85 | 86 | ```{code-cell} python 87 | num = posting.cost.number_per 88 | posting.cost.number_per = None 89 | posting.cost.number_total = num * posting.number 90 | _print_model(posting) 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/special/spacing.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | --- 6 | 7 | # Spacing 8 | 9 | ## Basic access 10 | 11 | Spacing (whitespace, tab, or newline) can be accessed in the similar way as other optional properties, through `raw_spacing_before`, `spacing_before`, `raw_spacing_after`, and `spacing_after`. 12 | 13 | ```{note} 14 | Indentation is NOT considered as spacing here. 15 | ``` 16 | 17 | ```{code-cell} python 18 | :load: ../code/basics.py 19 | :tags: [remove-cell] 20 | ``` 21 | 22 | ```{code-cell} python 23 | file = p.parse('''\ 24 | 2000-01-01 open Assets:Foo 25 | 26 | \t 27 | 28 | 2000-01-02close Assets:Foo 29 | ''', models.File) 30 | open, close = file.directives 31 | ``` 32 | 33 | Read: 34 | 35 | ```{code-cell} python 36 | (close.spacing_before, close.raw_spacing_before) 37 | ``` 38 | 39 | ```{code-cell} python 40 | (close.raw_date.spacing_after, close.raw_date.raw_spacing_after) 41 | ``` 42 | 43 | Write: 44 | 45 | ```{code-cell} python 46 | close.spacing_before = '\n\n' 47 | 48 | _print_model(file) 49 | ``` 50 | 51 | ## Ownership 52 | 53 | Unlikely other models, spacing has no ownership. In the example above, the space between `open` and `close` can be accessed through **both** `open.spacing_after` and `close.spacing_before`. 54 | 55 | ```{code-cell} python 56 | (open.raw_spacing_after, close.raw_spacing_before) 57 | ``` 58 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "autobean-refactor" 3 | version = "0.2.6" 4 | description = "An ergonomic and losess beancount manipulation library" 5 | authors = [ 6 | {name = "SEIAROTg", email = "seiarotg@gmail.com"}, 7 | ] 8 | dependencies = [ 9 | "lark>=1.1.5", 10 | "typing-extensions>=4.4.0", # for 3.10 compatibility 11 | ] 12 | requires-python = ">=3.10" 13 | readme = "README.md" 14 | license = {text = "MIT"} 15 | 16 | [build-system] 17 | requires = ["pdm-pep517>=1.0"] 18 | build-backend = "pdm.pep517.api" 19 | 20 | [tool.pdm] 21 | [tool.pdm.build] 22 | includes = [ 23 | "autobean_refactor/", 24 | ] 25 | excludes = [ 26 | "autobean_refactor/tests/", 27 | "autobean_refactor/modelgen/", 28 | "autobean_refactor/meta_models/", 29 | ] 30 | 31 | [tool.pdm.dev-dependencies] 32 | dev = [ 33 | "mypy>=0.991", 34 | "pytest>=7.2.0", 35 | "pytest-cov>=4.0.0", 36 | "pytest-benchmark>=4.0.0", 37 | "mako>=1.2.4", 38 | "stringcase>=1.2.0", 39 | # Limit version of sphinx-book-theme so I can specify template_path. 40 | # https://github.com/executablebooks/sphinx-book-theme/issues/719#issuecomment-1514498494 41 | "sphinx-book-theme>=0.3.3,<1.0.0", 42 | # Limit version of jupyter-book so I can lower the version of sphinx-book-theme. 43 | "jupyter-book>=0.14.0,<0.15.0", 44 | "docutils>=0.18.1", 45 | "types-docutils>=0.20.0.1", 46 | ] 47 | --------------------------------------------------------------------------------