pbark 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pbark-1.0.0/PKG-INFO +71 -0
- pbark-1.0.0/README.md +62 -0
- pbark-1.0.0/pbark/__init__.py +3 -0
- pbark-1.0.0/pbark/__main__.py +4 -0
- pbark-1.0.0/pbark/bark.py +109 -0
- pbark-1.0.0/pbark/cli/dog_art.py +75 -0
- pbark-1.0.0/pbark/cli/version.py +32 -0
- pbark-1.0.0/pbark/config/__init__.py +0 -0
- pbark-1.0.0/pbark/config/_resources.py +12 -0
- pbark-1.0.0/pbark/config/config_loader.py +239 -0
- pbark-1.0.0/pbark/config/dog_topics.py +203 -0
- pbark-1.0.0/pbark/config/name_hints.py +50 -0
- pbark-1.0.0/pbark/config/trait_loader.py +30 -0
- pbark-1.0.0/pbark/config/variable_type.py +12 -0
- pbark-1.0.0/pbark/errors.py +29 -0
- pbark-1.0.0/pbark/interpreter/bark_value.py +62 -0
- pbark-1.0.0/pbark/interpreter/comparators.py +45 -0
- pbark-1.0.0/pbark/interpreter/dog.py +136 -0
- pbark-1.0.0/pbark/interpreter/interpreter.py +823 -0
- pbark-1.0.0/pbark/interpreter/loop_signal.py +11 -0
- pbark-1.0.0/pbark/interpreter/print_styles.py +39 -0
- pbark-1.0.0/pbark/interpreter/prop.py +11 -0
- pbark-1.0.0/pbark/interpreter/stdin_reader.py +18 -0
- pbark-1.0.0/pbark/lexer.py +184 -0
- pbark-1.0.0/pbark/options.py +13 -0
- pbark-1.0.0/pbark/parser/__init__.py +0 -0
- pbark-1.0.0/pbark/parser/assign/__init__.py +0 -0
- pbark-1.0.0/pbark/parser/assign/assign_parser.py +403 -0
- pbark-1.0.0/pbark/parser/ast_node.py +209 -0
- pbark-1.0.0/pbark/parser/collection/__init__.py +0 -0
- pbark-1.0.0/pbark/parser/collection/pile_action_parser.py +125 -0
- pbark-1.0.0/pbark/parser/collection/pile_parser.py +77 -0
- pbark-1.0.0/pbark/parser/collection/smart_ordinal_parser.py +53 -0
- pbark-1.0.0/pbark/parser/collection/stash_parser.py +170 -0
- pbark-1.0.0/pbark/parser/collection/stash_spots.py +34 -0
- pbark-1.0.0/pbark/parser/collection/take_parser.py +118 -0
- pbark-1.0.0/pbark/parser/controlflow/__init__.py +0 -0
- pbark-1.0.0/pbark/parser/controlflow/foreach_parser.py +128 -0
- pbark-1.0.0/pbark/parser/controlflow/function_parser.py +91 -0
- pbark-1.0.0/pbark/parser/controlflow/if_parser.py +139 -0
- pbark-1.0.0/pbark/parser/controlflow/inline_body_parser.py +25 -0
- pbark-1.0.0/pbark/parser/controlflow/loop_control_parser.py +28 -0
- pbark-1.0.0/pbark/parser/controlflow/until_parser.py +84 -0
- pbark-1.0.0/pbark/parser/controlflow/while_parser.py +84 -0
- pbark-1.0.0/pbark/parser/expression/__init__.py +0 -0
- pbark-1.0.0/pbark/parser/expression/comparison_op.py +12 -0
- pbark-1.0.0/pbark/parser/expression/condition_parser.py +247 -0
- pbark-1.0.0/pbark/parser/expression/value_parser.py +308 -0
- pbark-1.0.0/pbark/parser/input/__init__.py +0 -0
- pbark-1.0.0/pbark/parser/input/input_parser.py +63 -0
- pbark-1.0.0/pbark/parser/keywords/__init__.py +429 -0
- pbark-1.0.0/pbark/parser/keywords/attribute_keyword_groups.py +42 -0
- pbark-1.0.0/pbark/parser/keywords/collection_keyword_groups.py +41 -0
- pbark-1.0.0/pbark/parser/keywords/control_flow_keyword_groups.py +23 -0
- pbark-1.0.0/pbark/parser/keywords/keyword_registry.py +18 -0
- pbark-1.0.0/pbark/parser/keywords/logic_keyword_groups.py +35 -0
- pbark-1.0.0/pbark/parser/keywords/print_keyword_groups.py +26 -0
- pbark-1.0.0/pbark/parser/parse_expression.py +184 -0
- pbark-1.0.0/pbark/parser/parser.py +299 -0
- pbark-1.0.0/pbark/parser/printing/__init__.py +0 -0
- pbark-1.0.0/pbark/parser/printing/print_parser.py +434 -0
- pbark-1.0.0/pbark/parser/printing/print_verb.py +9 -0
- pbark-1.0.0/pbark/parser/share/__init__.py +0 -0
- pbark-1.0.0/pbark/parser/share/share_parser.py +142 -0
- pbark-1.0.0/pbark/print_style.py +14 -0
- pbark-1.0.0/pbark/resources/breeds.txt +182 -0
- pbark-1.0.0/pbark/resources/dogfacts.txt +52 -0
- pbark-1.0.0/pbark/resources/objects.txt +16 -0
- pbark-1.0.0/pbark/resources/piles.txt +5 -0
- pbark-1.0.0/pbark/resources/stashes.txt +13 -0
- pbark-1.0.0/pbark/resources/traits.json +62 -0
- pbark-1.0.0/pbark.egg-info/PKG-INFO +71 -0
- pbark-1.0.0/pbark.egg-info/SOURCES.txt +84 -0
- pbark-1.0.0/pbark.egg-info/dependency_links.txt +1 -0
- pbark-1.0.0/pbark.egg-info/entry_points.txt +2 -0
- pbark-1.0.0/pbark.egg-info/requires.txt +3 -0
- pbark-1.0.0/pbark.egg-info/top_level.txt +1 -0
- pbark-1.0.0/pyproject.toml +27 -0
- pbark-1.0.0/setup.cfg +4 -0
- pbark-1.0.0/tests/test_feature.py +613 -0
- pbark-1.0.0/tests/test_golden.py +86 -0
- pbark-1.0.0/tests/test_parser_register.py +45 -0
- pbark-1.0.0/tests/test_parser_shape.py +64 -0
- pbark-1.0.0/tests/test_parser_stash.py +100 -0
- pbark-1.0.0/tests/test_parser_value.py +168 -0
- pbark-1.0.0/tests/test_script_mode_parse.py +28 -0
pbark-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pbark
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python port of the jbark dog-themed story programming language
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
9
|
+
|
|
10
|
+
# pbark
|
|
11
|
+
|
|
12
|
+
Python port of [jbark](../src/main/java/dev/klomptech/jbark/), the dog-themed story programming language.
|
|
13
|
+
|
|
14
|
+
**Semi-automated port:** pbark was translated from the Java source. It aims for parity with jbark and shares the same test suite. If behaviour diverges, jbark is canonical.
|
|
15
|
+
|
|
16
|
+
## Release status
|
|
17
|
+
|
|
18
|
+
**Already in this repo:** interpreter, 95 tests, CLI, registry files in `pbark/resources/`, `pip install .` works from a clone.
|
|
19
|
+
|
|
20
|
+
**Still to do (you):** PyPI account → `twine upload` so others can `pip install pbark`. Full steps: [docs/RELEASE-PYTHON.md](../docs/RELEASE-PYTHON.md).
|
|
21
|
+
|
|
22
|
+
When you edit breeds/objects in `src/main/resources/`, refresh the Python copy:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
./scripts/sync-pbark-resources.sh # from repo root
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
Requires **Python 3.10+**.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd pbark
|
|
34
|
+
python3 -m venv .venv
|
|
35
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
36
|
+
pip install -e .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Run
|
|
40
|
+
|
|
41
|
+
From the repo root (after install):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
./pbark/bin/pbark examples/woof/goodboy.woof
|
|
45
|
+
./pbark/bin/pbark --version
|
|
46
|
+
./pbark/bin/pbark --list-breeds
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or with the module directly:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 -m pbark examples/woof/bimba.woof
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Flags match jbark: `--help`, `--version`, `--strict`, `--quiet`, `--list-breeds`, `--list-objects`.
|
|
56
|
+
|
|
57
|
+
## Tests
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e ".[dev]"
|
|
61
|
+
pytest tests/ -q
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The test suite is ported from jbark's JUnit tests. Golden output, parser shape, and feature coverage.
|
|
65
|
+
|
|
66
|
+
## Docs
|
|
67
|
+
|
|
68
|
+
- [docs/MANUAL.md](../docs/MANUAL.md) - grammar and runtime rules
|
|
69
|
+
- [docs/AUTHOR.md](../docs/AUTHOR.md) - writing stories
|
|
70
|
+
- [docs/RELEASE-PYTHON.md](../docs/RELEASE-PYTHON.md) - what's done vs what you still do for PyPI
|
|
71
|
+
- [examples/](../examples/) - runnable `.woof` programs
|
pbark-1.0.0/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# pbark
|
|
2
|
+
|
|
3
|
+
Python port of [jbark](../src/main/java/dev/klomptech/jbark/), the dog-themed story programming language.
|
|
4
|
+
|
|
5
|
+
**Semi-automated port:** pbark was translated from the Java source. It aims for parity with jbark and shares the same test suite. If behaviour diverges, jbark is canonical.
|
|
6
|
+
|
|
7
|
+
## Release status
|
|
8
|
+
|
|
9
|
+
**Already in this repo:** interpreter, 95 tests, CLI, registry files in `pbark/resources/`, `pip install .` works from a clone.
|
|
10
|
+
|
|
11
|
+
**Still to do (you):** PyPI account → `twine upload` so others can `pip install pbark`. Full steps: [docs/RELEASE-PYTHON.md](../docs/RELEASE-PYTHON.md).
|
|
12
|
+
|
|
13
|
+
When you edit breeds/objects in `src/main/resources/`, refresh the Python copy:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
./scripts/sync-pbark-resources.sh # from repo root
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Requires **Python 3.10+**.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd pbark
|
|
25
|
+
python3 -m venv .venv
|
|
26
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Run
|
|
31
|
+
|
|
32
|
+
From the repo root (after install):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
./pbark/bin/pbark examples/woof/goodboy.woof
|
|
36
|
+
./pbark/bin/pbark --version
|
|
37
|
+
./pbark/bin/pbark --list-breeds
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or with the module directly:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python3 -m pbark examples/woof/bimba.woof
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Flags match jbark: `--help`, `--version`, `--strict`, `--quiet`, `--list-breeds`, `--list-objects`.
|
|
47
|
+
|
|
48
|
+
## Tests
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install -e ".[dev]"
|
|
52
|
+
pytest tests/ -q
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The test suite is ported from jbark's JUnit tests. Golden output, parser shape, and feature coverage.
|
|
56
|
+
|
|
57
|
+
## Docs
|
|
58
|
+
|
|
59
|
+
- [docs/MANUAL.md](../docs/MANUAL.md) - grammar and runtime rules
|
|
60
|
+
- [docs/AUTHOR.md](../docs/AUTHOR.md) - writing stories
|
|
61
|
+
- [docs/RELEASE-PYTHON.md](../docs/RELEASE-PYTHON.md) - what's done vs what you still do for PyPI
|
|
62
|
+
- [examples/](../examples/) - runnable `.woof` programs
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pbark.cli import dog_art as DogArt
|
|
7
|
+
from pbark.cli import version as Version
|
|
8
|
+
from pbark.config.config_loader import ConfigLoader
|
|
9
|
+
from pbark.errors import BarkError, ExitCode
|
|
10
|
+
from pbark.interpreter.interpreter import Interpreter
|
|
11
|
+
from pbark.lexer import Lexer
|
|
12
|
+
from pbark.options import BarkOptions
|
|
13
|
+
from pbark.parser.parser import Parser
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def print_help() -> None:
|
|
17
|
+
print(
|
|
18
|
+
"""pbark: a dog-themed story programming language (Python port of jbark)
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
pbark [script.woof] Run a .woof file
|
|
22
|
+
pbark Read lines from stdin (Ctrl+D to finish)
|
|
23
|
+
pbark --version Show version and a dog fact
|
|
24
|
+
pbark --list-breeds List registered breeds
|
|
25
|
+
pbark --list-objects List registered objects
|
|
26
|
+
pbark --strict script.woof Warn on story-only lines that do nothing
|
|
27
|
+
pbark --quiet script.woof Suppress startup banner and goodbye
|
|
28
|
+
|
|
29
|
+
Docs: docs/AUTHOR.md · docs/MANUAL.md
|
|
30
|
+
"""
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _list_names(label: str, names: list[str]) -> None:
|
|
35
|
+
print(f"{label}:")
|
|
36
|
+
for name in names:
|
|
37
|
+
print(f" {name}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_help_flag(arg: str) -> bool:
|
|
41
|
+
return arg in ("--help", "-h")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_version_flag(arg: str) -> bool:
|
|
45
|
+
return arg in ("--version", "-version")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run(source: str, options: BarkOptions | None = None) -> int:
|
|
49
|
+
options = options or BarkOptions.defaults()
|
|
50
|
+
try:
|
|
51
|
+
if not options.quiet:
|
|
52
|
+
DogArt.print_banner()
|
|
53
|
+
lexer = Lexer(source)
|
|
54
|
+
parser = Parser(lexer.tokenise(), options)
|
|
55
|
+
program = parser.parse()
|
|
56
|
+
Interpreter().run(program)
|
|
57
|
+
if not options.quiet:
|
|
58
|
+
DogArt.print_goodbye()
|
|
59
|
+
return ExitCode.OK
|
|
60
|
+
except BarkError as e:
|
|
61
|
+
print(e, file=sys.stderr)
|
|
62
|
+
return ExitCode.DATA_ERROR
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> None:
|
|
66
|
+
options = BarkOptions.defaults()
|
|
67
|
+
script_path: str | None = None
|
|
68
|
+
for arg in sys.argv[1:]:
|
|
69
|
+
if _is_help_flag(arg):
|
|
70
|
+
print_help()
|
|
71
|
+
sys.exit(ExitCode.OK)
|
|
72
|
+
if _is_version_flag(arg):
|
|
73
|
+
Version.print_version()
|
|
74
|
+
sys.exit(ExitCode.OK)
|
|
75
|
+
if arg == "--strict":
|
|
76
|
+
options = BarkOptions(True, options.quiet)
|
|
77
|
+
continue
|
|
78
|
+
if arg == "--quiet":
|
|
79
|
+
options = BarkOptions(options.strict, True)
|
|
80
|
+
continue
|
|
81
|
+
if arg == "--list-breeds":
|
|
82
|
+
_list_names("Breeds", ConfigLoader.list_breeds())
|
|
83
|
+
sys.exit(ExitCode.OK)
|
|
84
|
+
if arg == "--list-objects":
|
|
85
|
+
_list_names("Objects", ConfigLoader.list_objects())
|
|
86
|
+
sys.exit(ExitCode.OK)
|
|
87
|
+
if arg.startswith("-"):
|
|
88
|
+
print(f"Unknown flag: {arg}. Try pbark --help", file=sys.stderr)
|
|
89
|
+
sys.exit(ExitCode.USAGE)
|
|
90
|
+
if script_path is not None:
|
|
91
|
+
print("Usage: pbark [flags] [script.woof]. Try pbark --help", file=sys.stderr)
|
|
92
|
+
sys.exit(ExitCode.USAGE)
|
|
93
|
+
script_path = arg
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
if script_path is not None:
|
|
97
|
+
source = Path(script_path).read_text(encoding="utf-8")
|
|
98
|
+
else:
|
|
99
|
+
print("> ", end="")
|
|
100
|
+
source = sys.stdin.read()
|
|
101
|
+
except OSError as e:
|
|
102
|
+
print(f"Couldn't dig up that file: {e}", file=sys.stderr)
|
|
103
|
+
sys.exit(ExitCode.INVALID_ARGUMENTS)
|
|
104
|
+
|
|
105
|
+
sys.exit(run(source, options))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
from pbark.config._resources import resources_dir
|
|
6
|
+
|
|
7
|
+
EMOJI = "\U0001f415"
|
|
8
|
+
PAW = "\U0001f43e"
|
|
9
|
+
FACT_PREFIX = "Funny fact: "
|
|
10
|
+
|
|
11
|
+
_EMOJIS = [EMOJI, "\U0001f436", "woof."]
|
|
12
|
+
|
|
13
|
+
_BANNERS = [
|
|
14
|
+
"""
|
|
15
|
+
___ __
|
|
16
|
+
/(. .)\\ )
|
|
17
|
+
(*)_____/|
|
|
18
|
+
/ |
|
|
19
|
+
/ |--\\ |
|
|
20
|
+
(_)(_) (_)
|
|
21
|
+
""",
|
|
22
|
+
"""
|
|
23
|
+
/ \\__
|
|
24
|
+
( @\\___
|
|
25
|
+
/ O
|
|
26
|
+
/ (_____/
|
|
27
|
+
/_____/ U
|
|
28
|
+
woof!
|
|
29
|
+
""",
|
|
30
|
+
"""
|
|
31
|
+
__
|
|
32
|
+
(\\,--------'()'--o
|
|
33
|
+
(_ ___ /~"
|
|
34
|
+
(_)_) (_)_)
|
|
35
|
+
""",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_goodbyes() -> list[str]:
|
|
40
|
+
lines = [
|
|
41
|
+
"The dogs are now chilling on your bed.",
|
|
42
|
+
"After dragging the bed through the house, no more energy to play.",
|
|
43
|
+
"That was a good walk, time to relax.",
|
|
44
|
+
"The dogs had enough of playing, they went upstairs.",
|
|
45
|
+
]
|
|
46
|
+
path = resources_dir() / "dogfacts.txt"
|
|
47
|
+
if path.is_file():
|
|
48
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
49
|
+
stripped = line.strip()
|
|
50
|
+
if stripped:
|
|
51
|
+
lines.append(FACT_PREFIX + stripped)
|
|
52
|
+
return lines
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_GOODBYES = _load_goodbyes()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def print_banner() -> None:
|
|
59
|
+
print(random.choice(_BANNERS))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def random_bare_bark() -> str:
|
|
63
|
+
return random.choice(_EMOJIS)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def print_goodbye() -> None:
|
|
67
|
+
print(random.choice(_GOODBYES))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def paws_art() -> str:
|
|
71
|
+
return f"{PAW} {PAW} {PAW}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def print_paws() -> None:
|
|
75
|
+
print(paws_art())
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
from pbark.cli.dog_art import FACT_PREFIX
|
|
6
|
+
from pbark.config._resources import resources_dir
|
|
7
|
+
|
|
8
|
+
NAME = "pbark"
|
|
9
|
+
NUMBER = "1.0.0"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _load_facts() -> list[str]:
|
|
13
|
+
path = resources_dir() / "dogfacts.txt"
|
|
14
|
+
if not path.is_file():
|
|
15
|
+
return []
|
|
16
|
+
return [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_DOG_FACTS = _load_facts()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def print_version() -> None:
|
|
23
|
+
print(f"{NAME} {NUMBER}")
|
|
24
|
+
fact = random_dog_fact()
|
|
25
|
+
if fact is not None:
|
|
26
|
+
print(FACT_PREFIX + fact)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def random_dog_fact() -> str | None:
|
|
30
|
+
if not _DOG_FACTS:
|
|
31
|
+
return None
|
|
32
|
+
return random.choice(_DOG_FACTS)
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def resources_dir() -> Path:
|
|
7
|
+
# Bundled copies ship inside the wheel (pip install pbark).
|
|
8
|
+
bundled = Path(__file__).resolve().parent.parent / "resources"
|
|
9
|
+
if (bundled / "breeds.txt").is_file():
|
|
10
|
+
return bundled
|
|
11
|
+
# Git clone dev layout: shared monorepo registry files.
|
|
12
|
+
return Path(__file__).resolve().parent.parent.parent.parent / "src" / "main" / "resources"
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pbark.config._resources import resources_dir
|
|
4
|
+
from pbark.config.variable_type import VariableType
|
|
5
|
+
from pbark.errors import BarkError
|
|
6
|
+
from pbark.lexer import Token, TokenType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigLoader:
|
|
10
|
+
MEMORY = "memory"
|
|
11
|
+
JOURNAL = "journal"
|
|
12
|
+
|
|
13
|
+
_INDEX: dict[str, VariableType] = {}
|
|
14
|
+
_PHRASE_ALIASES: dict[str, str] = {}
|
|
15
|
+
_loaded = False
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def _ensure_loaded(cls) -> None:
|
|
19
|
+
if cls._loaded:
|
|
20
|
+
return
|
|
21
|
+
cls._load("/breeds.txt", VariableType.BREED)
|
|
22
|
+
cls._load("/objects.txt", VariableType.OBJECT)
|
|
23
|
+
cls._load("/stashes.txt", VariableType.STASH)
|
|
24
|
+
cls._load("/piles.txt", VariableType.PILE)
|
|
25
|
+
cls._INDEX[cls.MEMORY] = VariableType.STORY_NUMBER
|
|
26
|
+
cls._INDEX[cls.JOURNAL] = VariableType.STORY_TEXT
|
|
27
|
+
cls._build_phrase_aliases()
|
|
28
|
+
cls._loaded = True
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def _load(cls, filename: str, vtype: VariableType) -> None:
|
|
32
|
+
path = resources_dir() / filename.lstrip("/")
|
|
33
|
+
if not path.is_file():
|
|
34
|
+
raise IllegalStateError(f"Resource not found: {filename}")
|
|
35
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
36
|
+
word = line.strip()
|
|
37
|
+
if word:
|
|
38
|
+
cls._INDEX[cls.normalise(word)] = vtype
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def _build_phrase_aliases(cls) -> None:
|
|
42
|
+
for name in list(cls._INDEX.keys()):
|
|
43
|
+
cls._PHRASE_ALIASES[name] = name
|
|
44
|
+
if "_" in name:
|
|
45
|
+
cls._PHRASE_ALIASES[name.replace("_", " ")] = name
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def normalise(name: str) -> str:
|
|
49
|
+
return name.strip().lower()
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def type_of(cls, name: str) -> VariableType | None:
|
|
53
|
+
cls._ensure_loaded()
|
|
54
|
+
return cls._INDEX.get(cls.normalise(name))
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def is_valid_name(cls, name: str) -> bool:
|
|
58
|
+
return cls.type_of(name) is not None
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def is_breed(cls, name: str) -> bool:
|
|
62
|
+
return cls.type_of(name) == VariableType.BREED
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def is_stash(cls, name: str) -> bool:
|
|
66
|
+
return cls.type_of(name) == VariableType.STASH
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def is_pile(cls, name: str) -> bool:
|
|
70
|
+
return cls.type_of(name) == VariableType.PILE
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def is_story_number_constant(cls, name: str) -> bool:
|
|
74
|
+
return cls.type_of(name) == VariableType.STORY_NUMBER
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def is_story_text_constant(cls, name: str) -> bool:
|
|
78
|
+
return cls.type_of(name) == VariableType.STORY_TEXT
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def is_story_constant(cls, name: str) -> bool:
|
|
82
|
+
return cls.is_story_number_constant(name)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def memory_name(cls) -> str:
|
|
86
|
+
return cls.MEMORY
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def journal_name(cls) -> str:
|
|
90
|
+
return cls.JOURNAL
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def token_index_after_resolved_name(
|
|
94
|
+
cls, tokens: list[Token], fr: int, to: int, resolved_name: str
|
|
95
|
+
) -> int:
|
|
96
|
+
cls._ensure_loaded()
|
|
97
|
+
words: list[str] = []
|
|
98
|
+
positions: list[int] = []
|
|
99
|
+
for i in range(fr, to):
|
|
100
|
+
token = tokens[i]
|
|
101
|
+
if token.is_(TokenType.IDENTIFIER):
|
|
102
|
+
words.append(cls.normalise(token.value))
|
|
103
|
+
positions.append(i)
|
|
104
|
+
for length in range(min(4, len(words)), 0, -1):
|
|
105
|
+
for start in range(len(words) - length + 1):
|
|
106
|
+
phrase = " ".join(words[start : start + length])
|
|
107
|
+
if resolved_name == cls.resolve_name_phrase(phrase):
|
|
108
|
+
return positions[start + length - 1] + 1
|
|
109
|
+
key = cls.normalise(resolved_name)
|
|
110
|
+
for i in range(fr, to):
|
|
111
|
+
token = tokens[i]
|
|
112
|
+
if token.is_(TokenType.IDENTIFIER) and key == cls.normalise(token.value):
|
|
113
|
+
return i + 1
|
|
114
|
+
return -1
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def list_names(cls, vtype: VariableType) -> list[str]:
|
|
118
|
+
cls._ensure_loaded()
|
|
119
|
+
return sorted(k for k, v in cls._INDEX.items() if v == vtype)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def list_breeds(cls) -> list[str]:
|
|
123
|
+
return cls.list_names(VariableType.BREED)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def list_objects(cls) -> list[str]:
|
|
127
|
+
return cls.list_names(VariableType.OBJECT)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def list_stashes(cls) -> list[str]:
|
|
131
|
+
return cls.list_names(VariableType.STASH)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def list_piles(cls) -> list[str]:
|
|
135
|
+
return cls.list_names(VariableType.PILE)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def resolve_name_phrase(cls, phrase: str) -> str | None:
|
|
139
|
+
cls._ensure_loaded()
|
|
140
|
+
return cls._PHRASE_ALIASES.get(cls.normalise(phrase))
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def resolve_name_from_tokens(cls, tokens: list[Token], fr: int, to: int) -> str | None:
|
|
144
|
+
cls._ensure_loaded()
|
|
145
|
+
words = cls._identifier_words(tokens, fr, to)
|
|
146
|
+
cls._skip_leading_story_noise(words)
|
|
147
|
+
return cls._match_longest_name_phrase(words)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def resolve_collection_from_tokens(
|
|
151
|
+
cls, tokens: list[Token], fr: int, to: int
|
|
152
|
+
) -> str | None:
|
|
153
|
+
cls._ensure_loaded()
|
|
154
|
+
words = cls._identifier_words(tokens, fr, to)
|
|
155
|
+
cls._skip_leading_story_noise(words)
|
|
156
|
+
best = None
|
|
157
|
+
best_length = 0
|
|
158
|
+
for length in range(min(4, len(words)), 0, -1):
|
|
159
|
+
for start in range(len(words) - length + 1):
|
|
160
|
+
phrase = " ".join(words[start : start + length])
|
|
161
|
+
resolved = cls.resolve_name_phrase(phrase)
|
|
162
|
+
if (
|
|
163
|
+
resolved is not None
|
|
164
|
+
and (cls.is_stash(resolved) or cls.is_pile(resolved))
|
|
165
|
+
and length >= best_length
|
|
166
|
+
):
|
|
167
|
+
best = resolved
|
|
168
|
+
best_length = length
|
|
169
|
+
return best
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def _skip_leading_story_noise(cls, words: list[str]) -> None:
|
|
173
|
+
while words:
|
|
174
|
+
if cls.is_valid_name(words[0]) or cls.resolve_name_phrase(words[0]) is not None:
|
|
175
|
+
break
|
|
176
|
+
name_ahead = False
|
|
177
|
+
for length in range(1, min(4, len(words)) + 1):
|
|
178
|
+
if cls.resolve_name_phrase(" ".join(words[:length])) is not None:
|
|
179
|
+
name_ahead = True
|
|
180
|
+
break
|
|
181
|
+
if name_ahead:
|
|
182
|
+
break
|
|
183
|
+
words.pop(0)
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _identifier_words(tokens: list[Token], fr: int, to: int) -> list[str]:
|
|
187
|
+
words: list[str] = []
|
|
188
|
+
for i in range(fr, to):
|
|
189
|
+
token = tokens[i]
|
|
190
|
+
if token.is_(TokenType.IDENTIFIER):
|
|
191
|
+
words.append(ConfigLoader.normalise(token.value))
|
|
192
|
+
return words
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def _match_longest_name_phrase(cls, words: list[str]) -> str | None:
|
|
196
|
+
for length in range(min(4, len(words)), 0, -1):
|
|
197
|
+
for start in range(len(words) - length + 1):
|
|
198
|
+
phrase = " ".join(words[start : start + length])
|
|
199
|
+
resolved = cls.resolve_name_phrase(phrase)
|
|
200
|
+
if resolved is not None:
|
|
201
|
+
return resolved
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
@classmethod
|
|
205
|
+
def require_stash(cls, name: str, line: int) -> None:
|
|
206
|
+
if not cls.is_stash(name):
|
|
207
|
+
from pbark.config.name_hints import NameHints
|
|
208
|
+
|
|
209
|
+
raise BarkError(
|
|
210
|
+
line,
|
|
211
|
+
f"'{name}' is not a not something the dogs can get stuff from or bury in."
|
|
212
|
+
+ NameHints.hint_phrase(name, cls.list_stashes()),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def require_pile(cls, name: str, line: int) -> None:
|
|
217
|
+
if not cls.is_pile(name):
|
|
218
|
+
from pbark.config.name_hints import NameHints
|
|
219
|
+
|
|
220
|
+
raise BarkError(
|
|
221
|
+
line,
|
|
222
|
+
f"'{name}' is not a registered pile. wrong spot."
|
|
223
|
+
+ NameHints.hint_phrase(name, cls.list_piles()),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def require_breed(cls, name: str, line: int) -> None:
|
|
228
|
+
if not cls.is_breed(name):
|
|
229
|
+
from pbark.config.name_hints import NameHints
|
|
230
|
+
|
|
231
|
+
raise BarkError(
|
|
232
|
+
line,
|
|
233
|
+
f'"{name}" isn\'t a breed we know about. Garbage bin mix??'
|
|
234
|
+
+ NameHints.hint_phrase(name, cls.list_breeds()),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class IllegalStateError(RuntimeError):
|
|
239
|
+
pass
|