Skip to content

Commit fff0c2e

Browse files
Merge pull request #38 from theme-ontology/feature-tovalidate
Add command "to-validate"
2 parents adec08c + 76fd77e commit fff0c2e

File tree

9 files changed

+197
-49
lines changed

9 files changed

+197
-49
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ to-mergefiles = "totolo.util.mergefiles:main"
2424
to-mergelist = "totolo.util.mergelist:main"
2525
to-makelist = "totolo.util.makelist:main"
2626
to-makejson = "totolo.util.makejson:main"
27+
to-validate = "totolo.util.validate:main"
2728

2829
[tool.pylint.BASIC]
2930
good-names = "a,sa"

tests/test_entries.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import pytest
22

33
from totolo.impl.to_entry import TOEntry
4+
from totolo.impl.to_parser import TOParser
45
from totolo.story import TOStory
56
from totolo.theme import TOTheme
67

8+
79
STORY_DATA = {
810
"Title": "foo title",
911
"Description": "foo description" * 200,
@@ -73,6 +75,22 @@ def test_oddities(self):
7375
with pytest.raises(KeyError):
7476
del entry["foo"]
7577

78+
def test_validate_entries(self):
79+
bad_lines = """
80+
goofy <broken [line] one
81+
goofy }broken [line] two
82+
""".strip()
83+
entry = TOParser.make_story(f"""
84+
foo
85+
===
86+
:: Choice Themes
87+
foo <bar> [baz] {{widget}}
88+
{bad_lines}
89+
""".splitlines())
90+
warnings = list(entry.validate_keywords())
91+
for bad_line in bad_lines.splitlines():
92+
assert any(bad_line in x for x in warnings)
93+
7694

7795
class TestTOStory:
7896
def test_story_subtype(self):

tests/utils/test_validate.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import sys
2+
from unittest.mock import patch
3+
import urllib
4+
5+
import totolo
6+
import totolo.util.validate
7+
8+
from tests.test_totolo import precache_remote_resources
9+
10+
11+
EXPECTED_WARNINGS_20230723 = """
12+
tests/data/sample-2023.07.23/notes/stories/film/film-scifi-1920s.st.txt: In movie: The Hands of Orlac (1924): Missing '{' in: ['body part transplant ', 'hand', '', '']
13+
tests/data/sample-2023.07.23/notes/stories/film/film-scifi-1930s.st.txt: In movie: The Walking Dead (1936): Missing '{' in: ['artificial body part ', 'heart', '', '']
14+
tests/data/sample-2023.07.23/notes/stories/film/film-scifi-1930s.st.txt: In movie: The Man They Could Not Hang (1939): Missing '{' in: ['artificial body part ', 'heart', '', '']
15+
tests/data/sample-2023.07.23/notes/stories/film/film-scifi-1930s.st.txt: In movie: The Return of Doctor X (1939): Missing '{' in: ['artificial body part ', 'blood', '', '']
16+
tests/data/sample-2023.07.23/notes/themes/primary.th.txt: artificial body part: unknown field 'Template'
17+
tests/data/sample-2023.07.23/notes/themes/primary.th.txt: historical figure: unknown field 'Template'
18+
movie: Algol: Tragedy of Power (1920): Undefined 'major theme' with name 'the lust for gold'
19+
movie: Woman in the Moon (1929): Undefined 'minor theme' with name 'the lust for gold'
20+
""".strip()
21+
22+
23+
def validate1(capsys, expected = None):
24+
out, err = capsys.readouterr()
25+
assert all(line.startswith("::") for line in err.strip().splitlines())
26+
assert out.strip() == expected or EXPECTED_WARNINGS_20230723
27+
28+
29+
class TestMakeJson:
30+
def test_from_path(self, capsys):
31+
p1 = "tests/data/sample-2023.07.23/notes"
32+
testargs = ["makejson", "--path", p1]
33+
with patch.object(sys, 'argv', testargs):
34+
totolo.util.validate.main()
35+
validate1(capsys)
36+
37+
def test_from_path_narg(self, capsys):
38+
p1 = "tests/data/sample-2023.07.23/notes"
39+
testargs = ["makejson", p1]
40+
with patch.object(sys, 'argv', testargs):
41+
totolo.util.validate.main()
42+
validate1(capsys)
43+
44+
def test_bad_usage(self, capsys):
45+
testargs = ["makejson", "--path", "foo", "--version", "foo"]
46+
with patch.object(sys, 'argv', testargs):
47+
totolo.util.validate.main()
48+
out, err = capsys.readouterr()
49+
assert all(x in err for x in ["--path", "--version", "positional"])
50+
assert not out
51+
52+
def test_remote_version(self, capsys):
53+
precache_remote_resources()
54+
testargs = ["makejson", "--version", "v2023.06"]
55+
with patch.object(sys, 'argv', testargs):
56+
with open("tests/data/sample-2023.07.23.tar.gz", "rb+") as fh:
57+
with patch.object(urllib.request, 'urlopen', return_value=fh):
58+
totolo.util.validate.main()
59+
validate1(capsys)
60+
61+
def test_remote_version_narg(self, capsys):
62+
precache_remote_resources()
63+
testargs = ["makejson", "v2023.06"]
64+
with patch.object(sys, 'argv', testargs):
65+
with open("tests/data/sample-2023.07.23.tar.gz", "rb+") as fh:
66+
with patch.object(urllib.request, 'urlopen', return_value=fh):
67+
totolo.util.validate.main()
68+
validate1(capsys)
69+
70+
def test_remote_head(self, capsys):
71+
precache_remote_resources()
72+
testargs = ["makejson"]
73+
with patch.object(sys, 'argv', testargs):
74+
with open("tests/data/sample-2023.07.23.tar.gz", "rb+") as fh:
75+
with patch.object(urllib.request, 'urlopen', return_value=fh):
76+
totolo.util.validate.main()
77+
validate1(capsys)

totolo/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
remote = TORemote()
8-
__version__ = "2.1.1"
8+
__version__ = "2.1.2"
99
__ALL__ = [
1010
empty,
1111
files,

totolo/impl/to_entry.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,12 @@ def validate_keywords(self):
119119
from .to_parser import TOParser # pylint: disable=cyclic-import
120120
for field in self.fields.values():
121121
if field.fieldtype == "kwlist":
122-
data_iter = filter(None, (x.strip() for x in field.source[1:]))
123-
try:
124-
list(TOParser.iter_kwitems_strict(data_iter))
125-
except AssertionError as exc:
126-
yield f"In {self.name}: {exc.args[0]}"
122+
for line in (x.strip() for x in field.source[1:]):
123+
if line:
124+
try:
125+
list(TOParser.iter_kwitems_strict([line]))
126+
except AssertionError as exc:
127+
yield f"In {self.name}: {exc.args[0]}"
127128

128129
def text_canonical(self):
129130
lines = [self.name, "=" * len(self.name), ""]

totolo/impl/to_parser.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,15 @@ def iter_kwitems_strict(
105105
state_idx = 0
106106
close_bracket = ""
107107
elif close_bracket:
108-
raise AssertionError(f"Missing '{close_bracket}' in: {row}")
108+
raise AssertionError(
109+
f"Bad bracketing, found '{part}' when expecting '{close_bracket}', "
110+
f"in: {line}"
111+
)
109112
else:
110113
state_idx = ramp.index(part)
111114
if state_idx > 3:
112-
raise AssertionError(f"Unexpected {part} in: {row}")
113-
close_bracket = ramp[state_idx + 3]
115+
raise AssertionError(f"Unexpected {part} in: {line}")
116+
close_bracket = ramp[state_idx + 3] if state_idx > 0 else ""
114117
else:
115118
acc.append(part)
116119

totolo/lib/argparse.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import argparse
2+
import re
3+
import sys
4+
5+
import totolo
6+
7+
8+
VERSION_PATTERN = re.compile("v\\d{4}\\.\\d{2}$")
9+
10+
11+
def get_parser(description, epilog):
12+
parser = argparse.ArgumentParser(
13+
description=description,
14+
epilog=epilog,
15+
)
16+
parser.add_argument("source", nargs="*", help=
17+
"Paths to include or version to download. "
18+
"If a single argument matching tag pattern vYYYY.MM is given, "
19+
"it will be interpreted as a version. "
20+
"Otherwise the arguments will be treated as one or more local paths. "
21+
)
22+
parser.add_argument("-p", "--path", help="Path to the ontology.")
23+
parser.add_argument(
24+
"-v", "--version", help="Named version to use. If not specified the latest version of the "
25+
"master branch will be used."
26+
)
27+
return parser
28+
29+
30+
def get_ontology(args, quiet=True):
31+
if sum(1 for x in [args.path, args.version, args.source] if x) > 1:
32+
print("Can specify at most one of --path, --version, or positional argument.",
33+
file=sys.stderr)
34+
return None
35+
if args.source:
36+
if len(args.source) == 1 and VERSION_PATTERN.match(args.source[0]):
37+
if not quiet:
38+
print(f":: loading TO version {args.source[0]}", file=sys.stderr)
39+
ontology = totolo.remote.version(args.source[0])
40+
else:
41+
if not quiet:
42+
print(f":: loading TO files {args.source}", file=sys.stderr)
43+
ontology = totolo.files(args.source)
44+
elif args.path:
45+
if not quiet:
46+
print(f":: loading TO files {args.path}", file=sys.stderr)
47+
ontology = totolo.files(args.path)
48+
elif args.version:
49+
if not quiet:
50+
print(f":: loading TO version {args.version}", file=sys.stderr)
51+
ontology = totolo.remote.version(args.version)
52+
else:
53+
if not quiet:
54+
print(":: loading TO working HEAD version", file=sys.stderr)
55+
ontology = totolo.remote()
56+
return ontology

totolo/util/makejson.py

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import argparse
22
import json
3-
import re
4-
import sys
53
from collections import defaultdict
64

7-
import totolo
5+
import totolo.lib.argparse
86

97

108
THEME_FIELDS_OFFICIAL = {
@@ -110,31 +108,16 @@ def main():
110108
Example:
111109
"to-makejson v2025.04 -tsc > ontology_v202404.json"
112110
"""
113-
version_patt = re.compile("v\\d{4}\\.\\d{2}$")
114-
parser = argparse.ArgumentParser(
115-
description=(
116-
"Output a version of the ontology as json. "
117-
"Use -t -s -c to select themes, stories, and/or collections respectively. "
118-
"If none of these flags are given, all will be included. "
119-
),
120-
epilog=main.__doc__
121-
)
122-
parser.add_argument("source", nargs="*", help=
123-
"Paths to include or version to download. "
124-
"If a single argument matching tag pattern vYYYY.MM is given, "
125-
"it will be interpreted as a version. "
126-
"Otherwise the arguments will be treated as one or more local paths. "
127-
)
128-
parser.add_argument("-p", "--path", help="Path to the ontology.")
111+
parser = totolo.lib.argparse.get_parser((
112+
"Output a version of the ontology as json. "
113+
"Use -t -s -c to select themes, stories, and/or collections respectively. "
114+
"If none of these flags are given, all will be included. "
115+
), main.__doc__)
129116
parser.add_argument("--verbosity", default="official", help=
130117
"Which fields to include. "
131118
"'official': (default) include fields for official release. "
132119
"'all': include all fields. "
133120
)
134-
parser.add_argument(
135-
"-v", "--version", help="Named version to use. If not specified the latest version of the "
136-
"master branch will be used."
137-
)
138121
parser.add_argument("-t", action='store_true', help="Include themes.")
139122
parser.add_argument("-s", action='store_true', help="Include stories.")
140123
parser.add_argument("-c", action='store_true', help="Include collections.")
@@ -144,25 +127,11 @@ def main():
144127
"component entries on the collections instead. "
145128
)
146129
args = parser.parse_args()
147-
148-
if sum(1 for x in [args.path, args.version, args.source] if x) > 1:
149-
sys.stderr.write("Can specify at most one of --path, --version, or positional argument.")
130+
ontology = totolo.lib.argparse.get_ontology(args)
131+
if not ontology:
150132
return
151-
if args.source:
152-
if len(args.source) == 1 and version_patt.match(args.source[0]):
153-
ontology = totolo.remote.version(args.source[0])
154-
else:
155-
ontology = totolo.files(args.source)
156-
elif args.path:
157-
ontology = totolo.files(args.path)
158-
elif args.version:
159-
ontology = totolo.remote.version(args.version)
160-
else:
161-
ontology = totolo.remote()
162-
163133
if args.reorg:
164134
ontology.organize_collections()
165-
166135
if not any([args.t, args.s, args.c]):
167136
dd = make_json(ontology, verbosity=args.verbosity)
168137
else:
@@ -173,7 +142,6 @@ def main():
173142
with_collections=args.c,
174143
verbosity=args.verbosity,
175144
)
176-
177145
try:
178146
print(json.dumps(dd, indent=4, ensure_ascii=False))
179147
except BrokenPipeError: # pragma: no cover

totolo/util/validate.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import totolo.lib.argparse
2+
3+
4+
def main():
5+
"""
6+
This utility is provided as a command line script.
7+
8+
Example:
9+
"to-validate v2025.04"
10+
"""
11+
parser = totolo.lib.argparse.get_parser(
12+
"Load a version of the ontology and print any warnings about syntax. ",
13+
main.__doc__
14+
)
15+
ontology = totolo.lib.argparse.get_ontology(parser.parse_args(), quiet=False)
16+
try:
17+
if ontology:
18+
ontology.print_warnings()
19+
except BrokenPipeError: # pragma: no cover
20+
pass
21+
22+
23+
if __name__ == "__main__":
24+
main()

0 commit comments

Comments
 (0)