beehave-bt 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.
beehave/__init__.py ADDED
@@ -0,0 +1,80 @@
1
+ """Beehave: the small branch that matters.
2
+
3
+ Beehave reads a behavior tree and gives back only the actions the robot can
4
+ take, in order, with the conditions that lead to each one.
5
+
6
+ Example:
7
+ >>> import beehave
8
+ >>> result = beehave.analyze("mission.xml") # doctest: +SKIP
9
+ >>> print(beehave.to_text(result.graph)) # doctest: +SKIP
10
+ START --[not IsBatteryCritical]--> Arm
11
+ ...
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from importlib.metadata import PackageNotFoundError, version
17
+
18
+ from beehave.api import Analysis, analyze, analyze_py_trees
19
+ from beehave.config import Config, load_config
20
+ from beehave.errors import (
21
+ BeehaveError,
22
+ ConfigError,
23
+ FlattenError,
24
+ ParseError,
25
+ TreeSelectionError,
26
+ UnsupportedFormatError,
27
+ )
28
+ from beehave.flatten import FlatTree, flatten
29
+ from beehave.graph import ActionGraph, Condition, Edge, Literal, Outcome, Vertex, VertexKind
30
+ from beehave.ids import IdScheme, NodeRef, RuntimeNode, node_refs, runtime_ids
31
+ from beehave.parsers import parse_file, parse_string
32
+ from beehave.parsers.pytrees import from_py_trees, load_py_trees
33
+ from beehave.render import to_dot, to_json, to_mermaid, to_text
34
+ from beehave.stats import Stats, compute_stats
35
+ from beehave.walk import build_graph
36
+
37
+ try:
38
+ __version__ = version("beehave-bt")
39
+ except PackageNotFoundError: # pragma: no cover - running from a source tree
40
+ __version__ = "0.0.0+unknown"
41
+
42
+ __all__ = [
43
+ "ActionGraph",
44
+ "Analysis",
45
+ "BeehaveError",
46
+ "Condition",
47
+ "Config",
48
+ "ConfigError",
49
+ "Edge",
50
+ "FlatTree",
51
+ "FlattenError",
52
+ "IdScheme",
53
+ "Literal",
54
+ "NodeRef",
55
+ "Outcome",
56
+ "ParseError",
57
+ "RuntimeNode",
58
+ "Stats",
59
+ "TreeSelectionError",
60
+ "UnsupportedFormatError",
61
+ "Vertex",
62
+ "VertexKind",
63
+ "__version__",
64
+ "analyze",
65
+ "analyze_py_trees",
66
+ "build_graph",
67
+ "compute_stats",
68
+ "flatten",
69
+ "from_py_trees",
70
+ "load_config",
71
+ "load_py_trees",
72
+ "node_refs",
73
+ "parse_file",
74
+ "parse_string",
75
+ "runtime_ids",
76
+ "to_dot",
77
+ "to_json",
78
+ "to_mermaid",
79
+ "to_text",
80
+ ]
beehave/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Allow ``python -m beehave``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from beehave.cli import main
8
+
9
+ sys.exit(main())
beehave/api.py ADDED
@@ -0,0 +1,105 @@
1
+ """One call to run the whole pipeline: read, flatten, walk."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, replace
6
+ from functools import cached_property
7
+ from typing import TYPE_CHECKING
8
+
9
+ from beehave.flatten import FlatTree, flatten
10
+ from beehave.parsers import parse_file
11
+ from beehave.parsers.pytrees import from_py_trees
12
+ from beehave.stats import Stats, compute_stats
13
+ from beehave.walk import DEFAULT_MAX_PATHS, build_graph
14
+
15
+ if TYPE_CHECKING:
16
+ from pathlib import Path
17
+
18
+ from beehave.config import Config
19
+ from beehave.graph import ActionGraph
20
+ from beehave.model import Document
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Analysis:
25
+ """Everything Beehave computed for one tree.
26
+
27
+ Attributes:
28
+ document: All trees loaded from the input files.
29
+ tree: The selected tree, with subtrees inlined.
30
+ graph: Its action graph. Its diagnostics include the ones found while loading.
31
+ """
32
+
33
+ document: Document
34
+ tree: FlatTree
35
+ graph: ActionGraph
36
+
37
+ @cached_property
38
+ def stats(self) -> Stats:
39
+ """The pilot metrics for this tree."""
40
+ return compute_stats(self.tree, self.graph)
41
+
42
+
43
+ def analyze(
44
+ *paths: str | Path,
45
+ tree: str | None = None,
46
+ config: Config | None = None,
47
+ max_paths: int = DEFAULT_MAX_PATHS,
48
+ ) -> Analysis:
49
+ """Load tree files and build the action graph of one tree.
50
+
51
+ Args:
52
+ *paths: BT.CPP XML files. Trees from all of them are merged.
53
+ tree: The tree to analyse; defaults to the main tree.
54
+ config: Project settings (which custom nodes are conditions).
55
+ max_paths: Cap on guarded transitions per action outcome.
56
+
57
+ Returns:
58
+ The document, the flattened tree and its action graph.
59
+
60
+ Raises:
61
+ beehave.BeehaveError: If the input cannot be read or analysed.
62
+
63
+ Example:
64
+ >>> import beehave
65
+ >>> result = beehave.analyze("mission.xml") # doctest: +SKIP
66
+ >>> print(beehave.to_mermaid(result.graph)) # doctest: +SKIP
67
+ """
68
+ return _analyze(parse_file(*paths, config=config), tree, max_paths)
69
+
70
+
71
+ def analyze_py_trees(
72
+ tree: object,
73
+ *,
74
+ config: Config | None = None,
75
+ max_paths: int = DEFAULT_MAX_PATHS,
76
+ ) -> Analysis:
77
+ """Build the action graph of a live py_trees tree.
78
+
79
+ The tree is only read: no behaviour is set up or ticked. Use
80
+ :func:`beehave.load_py_trees` to import a tree from a module first.
81
+
82
+ Args:
83
+ tree: The root ``Behaviour``, or a ``BehaviourTree``.
84
+ config: Project settings (which behaviour classes are conditions).
85
+ max_paths: Cap on guarded transitions per action outcome.
86
+
87
+ Returns:
88
+ The document, the tree and its action graph.
89
+
90
+ Raises:
91
+ beehave.BeehaveError: If the tree cannot be read, or py_trees is missing.
92
+
93
+ Example:
94
+ >>> import beehave
95
+ >>> result = beehave.analyze_py_trees(create_root()) # doctest: +SKIP
96
+ >>> print(beehave.to_mermaid(result.graph)) # doctest: +SKIP
97
+ """
98
+ return _analyze(from_py_trees(tree, config=config), None, max_paths)
99
+
100
+
101
+ def _analyze(document: Document, tree: str | None, max_paths: int) -> Analysis:
102
+ flat = flatten(document, tree)
103
+ graph = build_graph(flat, max_paths=max_paths)
104
+ graph = replace(graph, diagnostics=document.diagnostics + graph.diagnostics)
105
+ return Analysis(document, flat, graph)
beehave/cli.py ADDED
@@ -0,0 +1,271 @@
1
+ """The ``beehave`` command line.
2
+
3
+ Examples::
4
+
5
+ beehave graph mission.xml # transitions as text
6
+ beehave graph mission.xml -f mermaid -o mission.mmd
7
+ beehave stats mission.xml # size, reduction, breakage
8
+ beehave trees mission.xml # list the trees in the files
9
+ beehave ids mission.xml # runtime ids and paths of every node
10
+ beehave graph --py-trees robot.trees:create_root # a py_trees tree (runs that code)
11
+ beehave graph behavior_trees/ -t NavigateToPose # every tree in a folder
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import sys
19
+ from dataclasses import replace
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING
22
+
23
+ from beehave import __version__
24
+ from beehave.api import analyze, analyze_py_trees
25
+ from beehave.config import Config, find_config, load_config
26
+ from beehave.errors import BeehaveError
27
+ from beehave.ids import IdScheme, runtime_ids
28
+ from beehave.parsers import parse_file
29
+ from beehave.parsers.pytrees import from_py_trees, load_py_trees
30
+ from beehave.render import FORMATS
31
+ from beehave.render.json import node_ref
32
+
33
+ if TYPE_CHECKING:
34
+ from collections.abc import Sequence
35
+
36
+ from beehave.api import Analysis
37
+ from beehave.diagnostics import Diagnostic
38
+ from beehave.model import Document
39
+
40
+
41
+ def main(argv: Sequence[str] | None = None) -> int:
42
+ """Run the command line and return the exit code.
43
+
44
+ Exit codes: 0 on success, 1 when the input cannot be analysed, 2 on a usage error.
45
+ """
46
+ args = _parser().parse_args(argv)
47
+ _check_inputs(args)
48
+ try:
49
+ return int(args.run(args))
50
+ except BeehaveError as exc:
51
+ print(f"beehave: error: {exc}", file=sys.stderr)
52
+ return 1
53
+
54
+
55
+ def _parser() -> argparse.ArgumentParser:
56
+ parser = argparse.ArgumentParser(
57
+ prog="beehave",
58
+ description="Show only the actions a behavior tree can take, and what runs next.",
59
+ )
60
+ parser.add_argument("--version", action="version", version=f"beehave {__version__}")
61
+ commands = parser.add_subparsers(title="commands", required=True, metavar="COMMAND")
62
+
63
+ graph = commands.add_parser("graph", help="build the action graph of a tree")
64
+ _common(graph)
65
+ graph.set_defaults(parser=graph)
66
+ graph.add_argument(
67
+ "-f",
68
+ "--format",
69
+ choices=sorted(FORMATS),
70
+ default="text",
71
+ help="output format (default: text)",
72
+ )
73
+ graph.add_argument("-o", "--output", type=Path, help="write to a file instead of stdout")
74
+ graph.set_defaults(run=_graph)
75
+
76
+ stats = commands.add_parser("stats", help="print size, reduction and breakage metrics")
77
+ _common(stats)
78
+ stats.set_defaults(parser=stats)
79
+ stats.add_argument("--json", action="store_true", help="print the metrics as JSON")
80
+ stats.set_defaults(run=_stats)
81
+
82
+ ids = commands.add_parser(
83
+ "ids", help="list every node with its runtime id, path and action vertex"
84
+ )
85
+ _common(ids)
86
+ ids.set_defaults(parser=ids)
87
+ ids.add_argument(
88
+ "--scheme",
89
+ choices=[scheme.value for scheme in IdScheme],
90
+ default=IdScheme.BTCPP_V4.value,
91
+ help="btcpp-v4: from 1, like BT.CPP v4 UIDs (default); preorder: from 0",
92
+ )
93
+ ids.add_argument("--json", action="store_true", help="print the nodes as JSON")
94
+ ids.set_defaults(run=_ids)
95
+
96
+ trees = commands.add_parser("trees", help="list the trees defined in the files")
97
+ _inputs(trees)
98
+ trees.set_defaults(run=_trees, parser=trees)
99
+ return parser
100
+
101
+
102
+ def _inputs(parser: argparse.ArgumentParser) -> None:
103
+ parser.add_argument(
104
+ "files",
105
+ nargs="*",
106
+ type=Path,
107
+ metavar="FILE",
108
+ help="BT.CPP XML files, or folders: every .xml file directly in a folder is read",
109
+ )
110
+ parser.add_argument(
111
+ "--py-trees",
112
+ metavar="SPEC",
113
+ help="read a py_trees tree instead: module:function or file.py:function. "
114
+ "This imports the module and calls the function, so it runs that code",
115
+ )
116
+ parser.add_argument(
117
+ "--kwargs", metavar="JSON", help="arguments for the --py-trees function, as a JSON object"
118
+ )
119
+ parser.add_argument(
120
+ "-c", "--config", type=Path, help="project config (default: ./beehave.yaml)"
121
+ )
122
+ parser.add_argument(
123
+ "--package",
124
+ action="append",
125
+ default=[],
126
+ metavar="NAME=DIR",
127
+ help='where <include ros_pkg="NAME"> finds the package; repeat for several',
128
+ )
129
+
130
+
131
+ def _common(parser: argparse.ArgumentParser) -> None:
132
+ _inputs(parser)
133
+ parser.add_argument("-t", "--tree", help="tree to analyse (default: the main tree)")
134
+ parser.add_argument("-q", "--quiet", action="store_true", help="do not print warnings")
135
+
136
+
137
+ def _check_inputs(args: argparse.Namespace) -> None:
138
+ error = args.parser.error
139
+ for value in args.package:
140
+ name, _, directory = value.partition("=")
141
+ if not name or not directory:
142
+ error("--package takes NAME=DIR, for example --package my_robot_bt=src/my_robot_bt")
143
+ if args.files and args.py_trees:
144
+ error("give tree files or --py-trees, not both")
145
+ if not args.files and not args.py_trees:
146
+ error("give tree files or --py-trees")
147
+ if args.py_trees and getattr(args, "tree", None):
148
+ error("--tree does not apply to --py-trees")
149
+ if args.kwargs is None:
150
+ return
151
+ if not args.py_trees:
152
+ error("--kwargs needs --py-trees")
153
+ try:
154
+ kwargs = json.loads(args.kwargs)
155
+ except json.JSONDecodeError:
156
+ kwargs = None
157
+ if not isinstance(kwargs, dict):
158
+ error("--kwargs must be a JSON object")
159
+ args.kwargs = kwargs
160
+
161
+
162
+ def _config(args: argparse.Namespace) -> Config:
163
+ path = args.config or find_config()
164
+ config = load_config(path) if path else Config()
165
+ if not args.package:
166
+ return config
167
+ packages = dict(config.packages)
168
+ for value in args.package:
169
+ name, _, directory = value.partition("=")
170
+ packages[name] = Path(directory)
171
+ return replace(config, packages=packages)
172
+
173
+
174
+ def _analysis(args: argparse.Namespace) -> Analysis:
175
+ if args.py_trees:
176
+ tree = load_py_trees(args.py_trees, **(args.kwargs or {}))
177
+ return analyze_py_trees(tree, config=_config(args))
178
+ return analyze(*args.files, tree=args.tree, config=_config(args))
179
+
180
+
181
+ def _document(args: argparse.Namespace) -> Document:
182
+ if args.py_trees:
183
+ tree = load_py_trees(args.py_trees, **(args.kwargs or {}))
184
+ return from_py_trees(tree, config=_config(args))
185
+ return parse_file(*args.files, config=_config(args))
186
+
187
+
188
+ def _graph(args: argparse.Namespace) -> int:
189
+ result = _analysis(args)
190
+ _warn(result.graph.diagnostics, quiet=args.quiet)
191
+ output = FORMATS[args.format](result.graph)
192
+ if args.output:
193
+ args.output.write_text(output, encoding="utf-8")
194
+ else:
195
+ sys.stdout.write(output)
196
+ return 0
197
+
198
+
199
+ def _stats(args: argparse.Namespace) -> int:
200
+ result = _analysis(args)
201
+ _warn(result.graph.diagnostics, quiet=args.quiet)
202
+ if args.json:
203
+ sys.stdout.write(json.dumps(result.stats.to_dict(), indent=2) + "\n")
204
+ else:
205
+ sys.stdout.write(result.stats.to_text())
206
+ return 0
207
+
208
+
209
+ def _ids(args: argparse.Namespace) -> int:
210
+ result = _analysis(args)
211
+ _warn(result.graph.diagnostics, quiet=args.quiet)
212
+ scheme = IdScheme(args.scheme)
213
+ nodes = runtime_ids(result.tree, scheme)
214
+ vertices = {v.node.path: v.id for v in result.graph.actions if v.node}
215
+ if args.json:
216
+ data = {
217
+ "schema": "beehave.runtime-ids",
218
+ "version": 1,
219
+ "tree": result.tree.id,
220
+ "source_format": result.tree.format,
221
+ "scheme": scheme.value,
222
+ "nodes": [
223
+ {
224
+ "id": node.id,
225
+ "parent": node.parent,
226
+ "kind": node.kind.value,
227
+ "type": node.type,
228
+ "name": node.name,
229
+ "subtree": node.subtree,
230
+ "vertex": vertices.get(node.ref.path),
231
+ **(node_ref(node.ref) or {}),
232
+ "location": None
233
+ if node.location is None
234
+ else {"file": node.location.file, "line": node.location.line},
235
+ }
236
+ for node in nodes.values()
237
+ ],
238
+ }
239
+ sys.stdout.write(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
240
+ return 0
241
+ rows = [("ID", "KIND", "VERTEX", "RUNTIME PATH", "PATH")]
242
+ rows += [
243
+ (
244
+ str(node.id),
245
+ node.kind.value,
246
+ vertices.get(node.ref.path, "-"),
247
+ node.ref.runtime_path or "-",
248
+ node.ref.path,
249
+ )
250
+ for node in nodes.values()
251
+ ]
252
+ widths = [max(len(row[i]) for row in rows) for i in range(4)]
253
+ for row in rows:
254
+ cells = [cell.ljust(width) for cell, width in zip(row, widths, strict=False)]
255
+ sys.stdout.write(" ".join([*cells, row[4]]) + "\n")
256
+ return 0
257
+
258
+
259
+ def _trees(args: argparse.Namespace) -> int:
260
+ document = _document(args)
261
+ for tree_id in document.trees:
262
+ suffix = " (main)" if tree_id == document.main_tree else ""
263
+ sys.stdout.write(f"{tree_id}{suffix}\n")
264
+ return 0
265
+
266
+
267
+ def _warn(diagnostics: Sequence[Diagnostic], *, quiet: bool) -> None:
268
+ if quiet:
269
+ return
270
+ for diagnostic in diagnostics:
271
+ print(f"beehave: {diagnostic}", file=sys.stderr)
beehave/config.py ADDED
@@ -0,0 +1,140 @@
1
+ """Per-project configuration.
2
+
3
+ Beehave works on any tree without configuration. A project only needs a config
4
+ file to say which of its custom nodes are conditions "in disguise" (for example
5
+ an action that only checks something), or to force a node to be an action::
6
+
7
+ # beehave.yaml
8
+ conditions:
9
+ - BatteryOK
10
+ - IsEmergency
11
+ actions:
12
+ - Probe
13
+ packages: # where <include ros_pkg="..."> finds a package
14
+ my_robot_bt: src/my_robot_bt
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ import yaml
24
+
25
+ from beehave.errors import ConfigError
26
+
27
+ if TYPE_CHECKING:
28
+ from collections.abc import Mapping
29
+
30
+ DEFAULT_CONFIG_NAMES = ("beehave.yaml", ".beehave.yaml")
31
+ _KNOWN_KEYS = frozenset({"conditions", "actions", "packages"})
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class Config:
36
+ """Project settings that change how nodes are classified.
37
+
38
+ Attributes:
39
+ conditions: Node types to treat as conditions, whatever the XML says.
40
+ actions: Node types to treat as actions, whatever the XML says.
41
+ packages: ROS package directories, by package name, for
42
+ ``<include ros_pkg="...">``. They win over a sourced workspace.
43
+ """
44
+
45
+ conditions: frozenset[str] = frozenset()
46
+ actions: frozenset[str] = frozenset()
47
+ packages: Mapping[str, Path] = field(default_factory=dict, hash=False)
48
+
49
+ def __post_init__(self) -> None:
50
+ both = self.conditions & self.actions
51
+ if both:
52
+ names = ", ".join(sorted(both))
53
+ msg = f"listed as both a condition and an action: {names}"
54
+ raise ConfigError(msg)
55
+
56
+
57
+ def load_config(path: str | Path) -> Config:
58
+ """Read a YAML config file.
59
+
60
+ Args:
61
+ path: The file to read.
62
+
63
+ Returns:
64
+ The parsed configuration.
65
+
66
+ Raises:
67
+ ConfigError: If the file cannot be read or is not a valid config.
68
+ """
69
+ path = Path(path)
70
+ try:
71
+ data: Any = yaml.safe_load(path.read_text(encoding="utf-8"))
72
+ except OSError as exc:
73
+ msg = f"cannot read config file {path}: {exc.strerror}"
74
+ raise ConfigError(msg) from exc
75
+ except yaml.YAMLError as exc:
76
+ msg = f"{path} is not valid YAML: {exc}"
77
+ raise ConfigError(msg) from exc
78
+ return config_from_dict(data, source=str(path), base_dir=path.parent)
79
+
80
+
81
+ def config_from_dict(
82
+ data: Any, *, source: str = "config", base_dir: str | Path | None = None
83
+ ) -> Config:
84
+ """Build a :class:`Config` from already-parsed data.
85
+
86
+ Args:
87
+ data: The mapping read from YAML (``None`` means an empty file).
88
+ source: A name for error messages.
89
+ base_dir: What relative package directories are relative to: the
90
+ config file's folder. Defaults to the current directory.
91
+
92
+ Returns:
93
+ The configuration.
94
+
95
+ Raises:
96
+ ConfigError: If the data has the wrong shape.
97
+ """
98
+ if data is None:
99
+ return Config()
100
+ if not isinstance(data, dict):
101
+ msg = f"{source}: expected a mapping at the top level"
102
+ raise ConfigError(msg)
103
+ unknown = sorted(set(data) - _KNOWN_KEYS)
104
+ if unknown:
105
+ msg = f"{source}: unknown key(s): {', '.join(map(str, unknown))}"
106
+ raise ConfigError(msg)
107
+ return Config(
108
+ conditions=_names(data.get("conditions"), "conditions", source),
109
+ actions=_names(data.get("actions"), "actions", source),
110
+ packages=_packages(data.get("packages"), source, Path(base_dir or ".")),
111
+ )
112
+
113
+
114
+ def find_config(directory: str | Path = ".") -> Path | None:
115
+ """Return the default config file in ``directory``, if there is one."""
116
+ for name in DEFAULT_CONFIG_NAMES:
117
+ candidate = Path(directory) / name
118
+ if candidate.is_file():
119
+ return candidate
120
+ return None
121
+
122
+
123
+ def _names(value: Any, key: str, source: str) -> frozenset[str]:
124
+ if value is None:
125
+ return frozenset()
126
+ if not isinstance(value, list) or not all(isinstance(v, str) for v in value):
127
+ msg = f"{source}: '{key}' must be a list of node names"
128
+ raise ConfigError(msg)
129
+ return frozenset(value)
130
+
131
+
132
+ def _packages(value: Any, source: str, base_dir: Path) -> dict[str, Path]:
133
+ if value is None:
134
+ return {}
135
+ if not isinstance(value, dict) or not all(
136
+ isinstance(k, str) and isinstance(v, str) for k, v in value.items()
137
+ ):
138
+ msg = f"{source}: 'packages' must map package names to directories"
139
+ raise ConfigError(msg)
140
+ return {name: base_dir / directory for name, directory in value.items()}
beehave/diagnostics.py ADDED
@@ -0,0 +1,41 @@
1
+ """Non-fatal findings reported alongside a result."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from beehave.model import SourceLocation
11
+
12
+
13
+ class Severity(str, Enum):
14
+ """How much a diagnostic matters."""
15
+
16
+ INFO = "info"
17
+ WARNING = "warning"
18
+ ERROR = "error"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Diagnostic:
23
+ """Something worth telling the user that does not stop the analysis.
24
+
25
+ Attributes:
26
+ code: A stable, machine-readable identifier such as ``"approximated"``.
27
+ message: A plain-English explanation.
28
+ severity: How much it matters.
29
+ node_type: The node type the diagnostic is about, if any.
30
+ location: Where in the source it was found, if known.
31
+ """
32
+
33
+ code: str
34
+ message: str
35
+ severity: Severity = Severity.WARNING
36
+ node_type: str | None = None
37
+ location: SourceLocation | None = field(default=None, compare=False)
38
+
39
+ def __str__(self) -> str:
40
+ where = f"{self.location}: " if self.location else ""
41
+ return f"{where}{self.severity.value}: {self.message} [{self.code}]"
beehave/errors.py ADDED
@@ -0,0 +1,37 @@
1
+ """Exceptions raised by Beehave."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from beehave.model import SourceLocation
9
+
10
+
11
+ class BeehaveError(Exception):
12
+ """Base class for every error Beehave raises on bad input."""
13
+
14
+ def __init__(self, message: str, location: SourceLocation | None = None) -> None:
15
+ self.message = message
16
+ self.location = location
17
+ super().__init__(f"{location}: {message}" if location else message)
18
+
19
+
20
+ class ParseError(BeehaveError):
21
+ """A tree file could not be read."""
22
+
23
+
24
+ class UnsupportedFormatError(ParseError):
25
+ """A tree file is in a format this version of Beehave cannot read yet."""
26
+
27
+
28
+ class ConfigError(BeehaveError):
29
+ """A Beehave configuration file is invalid."""
30
+
31
+
32
+ class FlattenError(BeehaveError):
33
+ """Subtrees could not be inlined (missing or recursive subtree)."""
34
+
35
+
36
+ class TreeSelectionError(BeehaveError):
37
+ """No tree, or an unknown tree, was selected for analysis."""