rustfava 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. rustfava/__init__.py +30 -0
  2. rustfava/_ctx_globals_class.py +55 -0
  3. rustfava/api_models.py +36 -0
  4. rustfava/application.py +534 -0
  5. rustfava/beans/__init__.py +6 -0
  6. rustfava/beans/abc.py +327 -0
  7. rustfava/beans/account.py +79 -0
  8. rustfava/beans/create.py +377 -0
  9. rustfava/beans/flags.py +20 -0
  10. rustfava/beans/funcs.py +38 -0
  11. rustfava/beans/helpers.py +52 -0
  12. rustfava/beans/ingest.py +75 -0
  13. rustfava/beans/load.py +31 -0
  14. rustfava/beans/prices.py +151 -0
  15. rustfava/beans/protocols.py +82 -0
  16. rustfava/beans/str.py +454 -0
  17. rustfava/beans/types.py +63 -0
  18. rustfava/cli.py +187 -0
  19. rustfava/context.py +13 -0
  20. rustfava/core/__init__.py +729 -0
  21. rustfava/core/accounts.py +161 -0
  22. rustfava/core/attributes.py +145 -0
  23. rustfava/core/budgets.py +207 -0
  24. rustfava/core/charts.py +301 -0
  25. rustfava/core/commodities.py +37 -0
  26. rustfava/core/conversion.py +229 -0
  27. rustfava/core/documents.py +87 -0
  28. rustfava/core/extensions.py +132 -0
  29. rustfava/core/fava_options.py +255 -0
  30. rustfava/core/file.py +542 -0
  31. rustfava/core/filters.py +484 -0
  32. rustfava/core/group_entries.py +97 -0
  33. rustfava/core/ingest.py +509 -0
  34. rustfava/core/inventory.py +167 -0
  35. rustfava/core/misc.py +105 -0
  36. rustfava/core/module_base.py +18 -0
  37. rustfava/core/number.py +106 -0
  38. rustfava/core/query.py +180 -0
  39. rustfava/core/query_shell.py +301 -0
  40. rustfava/core/tree.py +265 -0
  41. rustfava/core/watcher.py +219 -0
  42. rustfava/ext/__init__.py +232 -0
  43. rustfava/ext/auto_commit.py +61 -0
  44. rustfava/ext/portfolio_list/PortfolioList.js +34 -0
  45. rustfava/ext/portfolio_list/__init__.py +29 -0
  46. rustfava/ext/portfolio_list/templates/PortfolioList.html +15 -0
  47. rustfava/ext/rustfava_ext_test/RustfavaExtTest.js +42 -0
  48. rustfava/ext/rustfava_ext_test/__init__.py +207 -0
  49. rustfava/ext/rustfava_ext_test/templates/RustfavaExtTest.html +45 -0
  50. rustfava/ext/rustfava_ext_test/templates/RustfavaExtTestInclude.html +1 -0
  51. rustfava/help/__init__.py +15 -0
  52. rustfava/help/_index.md +29 -0
  53. rustfava/help/beancount_syntax.md +156 -0
  54. rustfava/help/budgets.md +31 -0
  55. rustfava/help/conversion.md +29 -0
  56. rustfava/help/extensions.md +111 -0
  57. rustfava/help/features.md +179 -0
  58. rustfava/help/filters.md +103 -0
  59. rustfava/help/import.md +27 -0
  60. rustfava/help/options.md +289 -0
  61. rustfava/helpers.py +30 -0
  62. rustfava/internal_api.py +221 -0
  63. rustfava/json_api.py +952 -0
  64. rustfava/plugins/__init__.py +3 -0
  65. rustfava/plugins/link_documents.py +107 -0
  66. rustfava/plugins/tag_discovered_documents.py +44 -0
  67. rustfava/py.typed +0 -0
  68. rustfava/rustledger/__init__.py +31 -0
  69. rustfava/rustledger/constants.py +76 -0
  70. rustfava/rustledger/engine.py +485 -0
  71. rustfava/rustledger/loader.py +273 -0
  72. rustfava/rustledger/options.py +202 -0
  73. rustfava/rustledger/query.py +331 -0
  74. rustfava/rustledger/types.py +830 -0
  75. rustfava/serialisation.py +220 -0
  76. rustfava/static/app.css +2988 -0
  77. rustfava/static/app.css.map +7 -0
  78. rustfava/static/app.js +12854 -0
  79. rustfava/static/app.js.map +7 -0
  80. rustfava/static/beancount-JFV44ZVZ.css +5 -0
  81. rustfava/static/beancount-JFV44ZVZ.css.map +7 -0
  82. rustfava/static/beancount-VTTKRGSK.js +4642 -0
  83. rustfava/static/beancount-VTTKRGSK.js.map +7 -0
  84. rustfava/static/bql-MGFRUMBP.js +333 -0
  85. rustfava/static/bql-MGFRUMBP.js.map +7 -0
  86. rustfava/static/chunk-E7ZF4ASL.js +23061 -0
  87. rustfava/static/chunk-E7ZF4ASL.js.map +7 -0
  88. rustfava/static/chunk-V24TLQHT.js +12673 -0
  89. rustfava/static/chunk-V24TLQHT.js.map +7 -0
  90. rustfava/static/favicon.ico +0 -0
  91. rustfava/static/fira-mono-cyrillic-400-normal-BLAGXRCE.woff2 +0 -0
  92. rustfava/static/fira-mono-cyrillic-500-normal-EN7JUAAW.woff2 +0 -0
  93. rustfava/static/fira-mono-cyrillic-ext-400-normal-EX7VARTS.woff2 +0 -0
  94. rustfava/static/fira-mono-cyrillic-ext-500-normal-ZDPTUPRR.woff2 +0 -0
  95. rustfava/static/fira-mono-greek-400-normal-COGHKMOA.woff2 +0 -0
  96. rustfava/static/fira-mono-greek-500-normal-4EN2PKZT.woff2 +0 -0
  97. rustfava/static/fira-mono-greek-ext-400-normal-DYEQIJH7.woff2 +0 -0
  98. rustfava/static/fira-mono-greek-ext-500-normal-SG73CVKQ.woff2 +0 -0
  99. rustfava/static/fira-mono-latin-400-normal-NA3VLV7E.woff2 +0 -0
  100. rustfava/static/fira-mono-latin-500-normal-YC77GFWD.woff2 +0 -0
  101. rustfava/static/fira-mono-latin-ext-400-normal-DIKTZ5PW.woff2 +0 -0
  102. rustfava/static/fira-mono-latin-ext-500-normal-ZWY4UO4V.woff2 +0 -0
  103. rustfava/static/fira-mono-symbols2-400-normal-UITXT77Q.woff2 +0 -0
  104. rustfava/static/fira-mono-symbols2-500-normal-VWPC2EFN.woff2 +0 -0
  105. rustfava/static/fira-sans-cyrillic-400-normal-KLQMBCA6.woff2 +0 -0
  106. rustfava/static/fira-sans-cyrillic-500-normal-NFG7UD6J.woff2 +0 -0
  107. rustfava/static/fira-sans-cyrillic-ext-400-normal-GWO44OPC.woff2 +0 -0
  108. rustfava/static/fira-sans-cyrillic-ext-500-normal-SP47E5SC.woff2 +0 -0
  109. rustfava/static/fira-sans-greek-400-normal-UMQBTLC3.woff2 +0 -0
  110. rustfava/static/fira-sans-greek-500-normal-4ZKHN4FQ.woff2 +0 -0
  111. rustfava/static/fira-sans-greek-ext-400-normal-O2DVJAJZ.woff2 +0 -0
  112. rustfava/static/fira-sans-greek-ext-500-normal-SK6GNWGO.woff2 +0 -0
  113. rustfava/static/fira-sans-latin-400-normal-OYYTPMAV.woff2 +0 -0
  114. rustfava/static/fira-sans-latin-500-normal-SMQPZW5A.woff2 +0 -0
  115. rustfava/static/fira-sans-latin-ext-400-normal-OAUP3WK5.woff2 +0 -0
  116. rustfava/static/fira-sans-latin-ext-500-normal-LY3YDR5Y.woff2 +0 -0
  117. rustfava/static/fira-sans-vietnamese-400-normal-OBMQ72MR.woff2 +0 -0
  118. rustfava/static/fira-sans-vietnamese-500-normal-Y4NZR5EU.woff2 +0 -0
  119. rustfava/static/source-code-pro-cyrillic-400-normal-TO22V6M3.woff2 +0 -0
  120. rustfava/static/source-code-pro-cyrillic-500-normal-OGBWWWYW.woff2 +0 -0
  121. rustfava/static/source-code-pro-cyrillic-ext-400-normal-XH44UCIA.woff2 +0 -0
  122. rustfava/static/source-code-pro-cyrillic-ext-500-normal-3Z6MMVM6.woff2 +0 -0
  123. rustfava/static/source-code-pro-greek-400-normal-OUXXUQWK.woff2 +0 -0
  124. rustfava/static/source-code-pro-greek-500-normal-JA2Z5UXO.woff2 +0 -0
  125. rustfava/static/source-code-pro-greek-ext-400-normal-WCDKMX7U.woff2 +0 -0
  126. rustfava/static/source-code-pro-greek-ext-500-normal-ZHVI4VKW.woff2 +0 -0
  127. rustfava/static/source-code-pro-latin-400-normal-QOGTXED5.woff2 +0 -0
  128. rustfava/static/source-code-pro-latin-500-normal-X57QEOLQ.woff2 +0 -0
  129. rustfava/static/source-code-pro-latin-ext-400-normal-QXC74NBF.woff2 +0 -0
  130. rustfava/static/source-code-pro-latin-ext-500-normal-QGOY7MTT.woff2 +0 -0
  131. rustfava/static/source-code-pro-vietnamese-400-normal-NPDCDTBA.woff2 +0 -0
  132. rustfava/static/source-code-pro-vietnamese-500-normal-M6PJKTR5.woff2 +0 -0
  133. rustfava/static/tree-sitter-beancount-MLXFQBZ5.wasm +0 -0
  134. rustfava/static/web-tree-sitter-RNOQ6E74.wasm +0 -0
  135. rustfava/template_filters.py +64 -0
  136. rustfava/templates/_journal_table.html +156 -0
  137. rustfava/templates/_layout.html +26 -0
  138. rustfava/templates/_query_table.html +88 -0
  139. rustfava/templates/beancount_file +18 -0
  140. rustfava/templates/help.html +23 -0
  141. rustfava/templates/macros/_account_macros.html +5 -0
  142. rustfava/templates/macros/_commodity_macros.html +13 -0
  143. rustfava/translations/bg/LC_MESSAGES/messages.mo +0 -0
  144. rustfava/translations/bg/LC_MESSAGES/messages.po +618 -0
  145. rustfava/translations/ca/LC_MESSAGES/messages.mo +0 -0
  146. rustfava/translations/ca/LC_MESSAGES/messages.po +618 -0
  147. rustfava/translations/de/LC_MESSAGES/messages.mo +0 -0
  148. rustfava/translations/de/LC_MESSAGES/messages.po +618 -0
  149. rustfava/translations/es/LC_MESSAGES/messages.mo +0 -0
  150. rustfava/translations/es/LC_MESSAGES/messages.po +619 -0
  151. rustfava/translations/fa/LC_MESSAGES/messages.mo +0 -0
  152. rustfava/translations/fa/LC_MESSAGES/messages.po +618 -0
  153. rustfava/translations/fr/LC_MESSAGES/messages.mo +0 -0
  154. rustfava/translations/fr/LC_MESSAGES/messages.po +618 -0
  155. rustfava/translations/ja/LC_MESSAGES/messages.mo +0 -0
  156. rustfava/translations/ja/LC_MESSAGES/messages.po +618 -0
  157. rustfava/translations/nl/LC_MESSAGES/messages.mo +0 -0
  158. rustfava/translations/nl/LC_MESSAGES/messages.po +617 -0
  159. rustfava/translations/pt/LC_MESSAGES/messages.mo +0 -0
  160. rustfava/translations/pt/LC_MESSAGES/messages.po +617 -0
  161. rustfava/translations/pt_BR/LC_MESSAGES/messages.mo +0 -0
  162. rustfava/translations/pt_BR/LC_MESSAGES/messages.po +618 -0
  163. rustfava/translations/ru/LC_MESSAGES/messages.mo +0 -0
  164. rustfava/translations/ru/LC_MESSAGES/messages.po +617 -0
  165. rustfava/translations/sk/LC_MESSAGES/messages.mo +0 -0
  166. rustfava/translations/sk/LC_MESSAGES/messages.po +623 -0
  167. rustfava/translations/sv/LC_MESSAGES/messages.mo +0 -0
  168. rustfava/translations/sv/LC_MESSAGES/messages.po +618 -0
  169. rustfava/translations/uk/LC_MESSAGES/messages.mo +0 -0
  170. rustfava/translations/uk/LC_MESSAGES/messages.po +618 -0
  171. rustfava/translations/zh/LC_MESSAGES/messages.mo +0 -0
  172. rustfava/translations/zh/LC_MESSAGES/messages.po +618 -0
  173. rustfava/translations/zh_Hant_TW/LC_MESSAGES/messages.mo +0 -0
  174. rustfava/translations/zh_Hant_TW/LC_MESSAGES/messages.po +618 -0
  175. rustfava/util/__init__.py +157 -0
  176. rustfava/util/date.py +576 -0
  177. rustfava/util/excel.py +118 -0
  178. rustfava/util/ranking.py +79 -0
  179. rustfava/util/sets.py +18 -0
  180. rustfava/util/unreachable.py +20 -0
  181. rustfava-0.1.0.dist-info/METADATA +102 -0
  182. rustfava-0.1.0.dist-info/RECORD +187 -0
  183. rustfava-0.1.0.dist-info/WHEEL +5 -0
  184. rustfava-0.1.0.dist-info/entry_points.txt +2 -0
  185. rustfava-0.1.0.dist-info/licenses/AUTHORS +11 -0
  186. rustfava-0.1.0.dist-info/licenses/LICENSE +21 -0
  187. rustfava-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,132 @@
1
+ """Fava extensions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rustfava.core.module_base import FavaModule
10
+ from rustfava.ext import ExtensionConfigError
11
+ from rustfava.ext import RustfavaExtensionError
12
+ from rustfava.ext import find_extensions
13
+
14
+ if TYPE_CHECKING: # pragma: no cover
15
+ from collections.abc import Iterable
16
+ from collections.abc import Sequence
17
+
18
+ from rustfava.beans.abc import Directive
19
+ from rustfava.core import RustfavaLedger
20
+ from rustfava.ext import RustfavaExtensionBase
21
+
22
+
23
+ @dataclass
24
+ class ExtensionDetails:
25
+ """The information about an extension that is needed for the frontend."""
26
+
27
+ name: str
28
+ report_title: str | None
29
+ has_js_module: bool
30
+
31
+
32
+ class ExtensionModule(FavaModule):
33
+ """Fava extensions."""
34
+
35
+ def __init__(self, ledger: RustfavaLedger) -> None:
36
+ super().__init__(ledger)
37
+ self._instances: dict[str, RustfavaExtensionBase] = {}
38
+ self._loaded_extensions: set[type[RustfavaExtensionBase]] = set()
39
+ self.errors: list[RustfavaExtensionError] = []
40
+
41
+ def load_file(self) -> None: # noqa: D102
42
+ self.errors = []
43
+
44
+ custom_entries = self.ledger.all_entries_by_type.Custom
45
+
46
+ seen = set()
47
+ for entry in (e for e in custom_entries if e.type == "fava-extension"):
48
+ extension = entry.values[0].value
49
+ if extension in seen: # pragma: no cover
50
+ self.errors.append(
51
+ RustfavaExtensionError(
52
+ entry.meta, f"Duplicate extension '{extension}'", entry
53
+ )
54
+ )
55
+ continue
56
+
57
+ seen.add(extension)
58
+ extensions, errors = find_extensions(
59
+ Path(self.ledger.beancount_file_path).parent,
60
+ extension,
61
+ )
62
+ self.errors.extend(errors)
63
+
64
+ for cls in extensions:
65
+ ext_config = (
66
+ entry.values[1].value if len(entry.values) > 1 else None
67
+ )
68
+ if cls not in self._loaded_extensions:
69
+ self._loaded_extensions.add(cls)
70
+ try:
71
+ ext = cls(self.ledger, ext_config)
72
+ self._instances[ext.name] = ext
73
+ except ExtensionConfigError as error: # pragma: no cover
74
+ self.errors.append(
75
+ RustfavaExtensionError(entry.meta, str(error), entry)
76
+ )
77
+
78
+ @property
79
+ def _exts(self) -> Iterable[RustfavaExtensionBase]:
80
+ return self._instances.values()
81
+
82
+ @property
83
+ def extension_details(self) -> Sequence[ExtensionDetails]:
84
+ """Extension information to provide to the Frontend."""
85
+ return [
86
+ ExtensionDetails(ext.name, ext.report_title, ext.has_js_module)
87
+ for ext in self._exts
88
+ ]
89
+
90
+ def get_extension(self, name: str) -> RustfavaExtensionBase | None:
91
+ """Get the extension with the given name."""
92
+ return self._instances.get(name, None)
93
+
94
+ def after_load_file(self) -> None:
95
+ """Run all `after_load_file` hooks."""
96
+ for ext in self._exts:
97
+ ext.after_load_file()
98
+
99
+ def before_request(self) -> None:
100
+ """Run all `before_request` hooks."""
101
+ for ext in self._exts:
102
+ ext.before_request()
103
+
104
+ def after_entry_modified(self, entry: Directive, new_lines: str) -> None:
105
+ """Run all `after_entry_modified` hooks."""
106
+ for ext in self._exts: # pragma: no cover
107
+ ext.after_entry_modified(entry, new_lines)
108
+
109
+ def after_insert_entry(self, entry: Directive) -> None:
110
+ """Run all `after_insert_entry` hooks."""
111
+ for ext in self._exts: # pragma: no cover
112
+ ext.after_insert_entry(entry)
113
+
114
+ def after_delete_entry(self, entry: Directive) -> None:
115
+ """Run all `after_delete_entry` hooks."""
116
+ for ext in self._exts: # pragma: no cover
117
+ ext.after_delete_entry(entry)
118
+
119
+ def after_insert_metadata(
120
+ self,
121
+ entry: Directive,
122
+ key: str,
123
+ value: str,
124
+ ) -> None:
125
+ """Run all `after_insert_metadata` hooks."""
126
+ for ext in self._exts: # pragma: no cover
127
+ ext.after_insert_metadata(entry, key, value)
128
+
129
+ def after_write_source(self, path: str, source: str) -> None:
130
+ """Run all `after_write_source` hooks."""
131
+ for ext in self._exts:
132
+ ext.after_write_source(path, source)
@@ -0,0 +1,255 @@
1
+ """rustfava's options.
2
+
3
+ Options for rustfava can be specified through Custom entries in the Beancount file.
4
+ This module contains a list of possible options, the defaults and the code for
5
+ parsing the options.
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from dataclasses import dataclass
13
+ from dataclasses import field
14
+ from dataclasses import fields
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING
17
+
18
+ from babel.core import Locale
19
+ from babel.core import UnknownLocaleError
20
+
21
+ from rustfava.beans.funcs import get_position
22
+ from rustfava.helpers import BeancountError
23
+ from rustfava.util import get_translations
24
+ from rustfava.util.date import END_OF_YEAR
25
+ from rustfava.util.date import parse_fye_string
26
+
27
+ if TYPE_CHECKING: # pragma: no cover
28
+ import datetime
29
+ from collections.abc import Sequence
30
+ from re import Pattern
31
+
32
+ from rustfava.beans.abc import Custom
33
+ from rustfava.util.date import FiscalYearEnd
34
+
35
+
36
+ class OptionError(BeancountError):
37
+ """An error for one of the rustfava options."""
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class InsertEntryOption:
42
+ """Insert option.
43
+
44
+ An option that determines where entries for matching accounts should be
45
+ inserted.
46
+ """
47
+
48
+ date: datetime.date
49
+ re: Pattern[str]
50
+ filename: str
51
+ lineno: int
52
+
53
+
54
+ class MissingOptionError(ValueError): # noqa: D101
55
+ def __init__(self) -> None:
56
+ super().__init__("Custom entry is missing option name.")
57
+
58
+
59
+ class UnknownOptionError(ValueError): # noqa: D101
60
+ def __init__(self, key: str) -> None:
61
+ super().__init__(f"Unknown option `{key}`")
62
+
63
+
64
+ class NotARegularExpressionError(TypeError): # noqa: D101
65
+ def __init__(self, value: str) -> None:
66
+ super().__init__(f"Should be a regular expression: '{value}'.")
67
+
68
+
69
+ class NotAStringOptionError(TypeError): # noqa: D101
70
+ def __init__(self, key: str) -> None:
71
+ super().__init__(f"Expected string value for option `{key}`")
72
+
73
+
74
+ class UnknownLocaleOptionError(ValueError): # noqa: D101
75
+ def __init__(self, value: str) -> None:
76
+ super().__init__(f"Unknown locale: '{value}'.")
77
+
78
+
79
+ class UnsupportedLanguageOptionError(ValueError): # noqa: D101
80
+ def __init__(self, value: str) -> None:
81
+ super().__init__(f"Rustfava has no translations for: '{value}'.")
82
+
83
+
84
+ class InvalidFiscalYearEndOptionError(ValueError): # noqa: D101
85
+ def __init__(self, value: str) -> None:
86
+ super().__init__(f"Invalid 'fiscal_year_end' option: '{value}'.")
87
+
88
+
89
+ @dataclass
90
+ class RustfavaOptions:
91
+ """Options for rustfava that can be set in the Beancount file."""
92
+
93
+ account_journal_include_children: bool = True
94
+ auto_reload: bool = False
95
+ collapse_pattern: Sequence[Pattern[str]] = field(default_factory=list)
96
+ conversion_currencies: tuple[str, ...] = ()
97
+ currency_column: int = 61
98
+ default_file: str | None = None
99
+ default_page: str = "income_statement/"
100
+ fiscal_year_end: FiscalYearEnd = END_OF_YEAR
101
+ import_config: str | None = None
102
+ import_dirs: Sequence[str] = field(default_factory=list)
103
+ indent: int = 2
104
+ insert_entry: Sequence[InsertEntryOption] = field(default_factory=list)
105
+ invert_gains_losses_colors: bool = False
106
+ invert_income_liabilities_equity: bool = False
107
+ language: str | None = None
108
+ locale: str | None = None
109
+ show_accounts_with_zero_balance: bool = True
110
+ show_accounts_with_zero_transactions: bool = True
111
+ show_closed_accounts: bool = False
112
+ sidebar_show_queries: int = 5
113
+ unrealized: str = "Unrealized"
114
+ upcoming_events: int = 7
115
+ uptodate_indicator_grey_lookback_days: int = 60
116
+ use_external_editor: bool = False
117
+
118
+ def set_collapse_pattern(self, value: str) -> None:
119
+ """Set the collapse_pattern option."""
120
+ try:
121
+ pattern = re.compile(value)
122
+ except re.error as err:
123
+ raise NotARegularExpressionError(value) from err
124
+ # It's typed as Sequence so that it's not externally mutated
125
+ self.collapse_pattern.append(pattern) # type: ignore[attr-defined]
126
+
127
+ def set_default_file(self, value: str, filename: str) -> None:
128
+ """Set the default_file option."""
129
+ self.default_file = (
130
+ str((Path(filename).parent / value).absolute())
131
+ if value
132
+ else filename
133
+ )
134
+
135
+ def set_fiscal_year_end(self, value: str) -> None:
136
+ """Set the fiscal_year_end option."""
137
+ fye = parse_fye_string(value)
138
+ if fye is None:
139
+ raise InvalidFiscalYearEndOptionError(value)
140
+ self.fiscal_year_end = fye
141
+
142
+ def set_import_dirs(self, value: str) -> None:
143
+ """Add an import directory."""
144
+ # It's typed as Sequence so that it's not externally mutated
145
+ self.import_dirs.append(value) # type: ignore[attr-defined]
146
+
147
+ def set_insert_entry(
148
+ self, value: str, date: datetime.date, filename: str, lineno: int
149
+ ) -> None:
150
+ """Set the insert_entry option."""
151
+ try:
152
+ pattern = re.compile(value)
153
+ except re.error as err:
154
+ raise NotARegularExpressionError(value) from err
155
+ opt = InsertEntryOption(date, pattern, filename, lineno)
156
+ # It's typed as Sequence so that it's not externally mutated
157
+ self.insert_entry.append(opt) # type: ignore[attr-defined]
158
+
159
+ def set_language(self, value: str) -> None:
160
+ """Set the locale option."""
161
+ try:
162
+ locale = Locale.parse(value)
163
+ if (
164
+ not locale.language == "en"
165
+ and get_translations(locale) is None
166
+ ):
167
+ raise UnsupportedLanguageOptionError(value)
168
+ self.language = value
169
+ except UnknownLocaleError as err:
170
+ raise UnknownLocaleOptionError(value) from err
171
+
172
+ def set_locale(self, value: str) -> None:
173
+ """Set the locale option."""
174
+ try:
175
+ Locale.parse(value)
176
+ self.locale = value
177
+ except UnknownLocaleError as err:
178
+ raise UnknownLocaleOptionError(value) from err
179
+
180
+
181
+ _fields = fields(RustfavaOptions)
182
+ All_OPTS = {f.name for f in _fields}
183
+ BOOL_OPTS = {f.name for f in _fields if str(f.type) == "bool"}
184
+ INT_OPTS = {f.name for f in _fields if str(f.type) == "int"}
185
+ TUPLE_OPTS = {f.name for f in _fields if f.type.startswith("tuple[str,")}
186
+ STR_OPTS = {f.name for f in _fields if f.type.startswith("str")}
187
+
188
+
189
+ def parse_option_custom_entry( # noqa: PLR0912
190
+ entry: Custom,
191
+ options: RustfavaOptions,
192
+ ) -> None:
193
+ """Parse a single custom fava-option entry and set option accordingly."""
194
+ key = str(entry.values[0].value).replace("-", "_")
195
+ if key not in All_OPTS:
196
+ raise UnknownOptionError(key)
197
+
198
+ value = entry.values[1].value if len(entry.values) > 1 else ""
199
+ if not isinstance(value, str):
200
+ raise NotAStringOptionError(key)
201
+ filename, lineno = get_position(entry)
202
+
203
+ if key == "collapse_pattern":
204
+ options.set_collapse_pattern(value)
205
+ elif key == "default_file":
206
+ options.set_default_file(value, filename)
207
+ elif key == "fiscal_year_end":
208
+ options.set_fiscal_year_end(value)
209
+ elif key == "import_dirs":
210
+ options.set_import_dirs(value)
211
+ elif key == "insert_entry":
212
+ options.set_insert_entry(value, entry.date, filename, lineno)
213
+ elif key == "language":
214
+ options.set_language(value)
215
+ elif key == "locale":
216
+ options.set_locale(value)
217
+ elif key in STR_OPTS:
218
+ setattr(options, key, value)
219
+ elif key in BOOL_OPTS:
220
+ setattr(options, key, value.lower() == "true")
221
+ elif key in INT_OPTS:
222
+ setattr(options, key, int(value))
223
+ else: # key in TUPLE_OPTS
224
+ setattr(options, key, tuple(value.strip().split(" ")))
225
+
226
+
227
+ def parse_options(
228
+ custom_entries: Sequence[Custom],
229
+ ) -> tuple[RustfavaOptions, list[OptionError]]:
230
+ """Parse custom entries for rustfava options.
231
+
232
+ The format for option entries is the following::
233
+
234
+ 2016-04-01 custom "fava-option" "[name]" "[value]"
235
+
236
+ Args:
237
+ custom_entries: A list of Custom entries.
238
+
239
+ Returns:
240
+ A tuple (options, errors) where options is a dictionary of all options
241
+ to values, and errors contains possible parsing errors.
242
+ """
243
+ options = RustfavaOptions()
244
+ errors = []
245
+
246
+ for entry in (e for e in custom_entries if e.type == "fava-option"):
247
+ try:
248
+ if not entry.values:
249
+ raise MissingOptionError
250
+ parse_option_custom_entry(entry, options)
251
+ except (IndexError, TypeError, ValueError) as err:
252
+ msg = f"Failed to parse fava-option entry: {err!s}"
253
+ errors.append(OptionError(entry.meta, msg, entry))
254
+
255
+ return options, errors