Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.

Commit 62033db

Browse files
Add tokenize functions (#57)
* Add tokenize_json, tokenize_yaml * Tweak error message. Expose Message and Position exports * Tweak error message
1 parent c90a7e0 commit 62033db

10 files changed

Lines changed: 236 additions & 246 deletions

docs/tokenized_errors.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ text = '''{
1313
"enable_auto_reload": "true"
1414
}'''
1515

16-
value, messages = typesystem.validate_json(text, validator=Config)
17-
18-
assert value is None
19-
for message in messages:
20-
line_no = message.start_position.line_no
21-
column_no = message.start_position.column_no
22-
print(f"Error {message.text!r} at line {line_no}, column {column_no}.")
16+
try:
17+
typesystem.validate_json(text, validator=Config)
18+
except (typesystem.ValidationError, typesystem.ParseError) as exc:
19+
for message in exc.messages():
20+
line_no = message.start_position.line_no
21+
column_no = message.start_position.column_no
22+
print(f"Error {message.text!r} at line {line_no}, column {column_no}.")
2323
# Error 'Must be a number.' at line 2, column 29.
2424
```
2525

@@ -29,6 +29,3 @@ The two functions for parsing content and providing positional error messages ar
2929
* `validate_yaml(text_or_bytes, validator)`
3030

3131
In both cases `validator` may either be a `Schema` class, or a `Field` instance.
32-
33-
Both functions return a two-tuple of `(value, messages)`. If the `messages` list
34-
is non-empty, then `value` will be `None`.

tests/tokenize/test_tokenize_json.py

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
from json.decoder import JSONDecodeError
2-
31
import pytest
42

5-
from typesystem.tokenize.tokenize_json import tokenize_json
3+
from typesystem import ParseError, tokenize_json
64
from typesystem.tokenize.tokens import DictToken, ListToken, ScalarToken
75

86

@@ -89,38 +87,62 @@ def test_tokenize_whitespace():
8987

9088

9189
def test_tokenize_parse_errors():
92-
with pytest.raises(JSONDecodeError) as exc_info:
90+
with pytest.raises(ParseError) as exc_info:
91+
tokenize_json(b"")
92+
exc = exc_info.value
93+
message = exc.messages()[0]
94+
assert message.text == "No content."
95+
assert message.start_position.char_index == 0
96+
assert (
97+
repr(message)
98+
== "Message(text='No content.', code='no_content', position=Position(line_no=1, column_no=1, char_index=0))"
99+
)
100+
101+
with pytest.raises(ParseError) as exc_info:
93102
tokenize_json("{")
94103
exc = exc_info.value
95-
assert exc.msg == "Expecting property name enclosed in double quotes"
96-
assert exc.pos == 1
104+
message = exc.messages()[0]
105+
assert message.text == "Expecting property name enclosed in double quotes."
106+
assert message.start_position.char_index == 1
97107

98-
with pytest.raises(JSONDecodeError) as exc_info:
108+
with pytest.raises(ParseError) as exc_info:
99109
tokenize_json('{"a"')
100110
exc = exc_info.value
101-
assert exc.msg == "Expecting ':' delimiter"
102-
assert exc.pos == 4
111+
message = exc.messages()[0]
112+
assert message.text == "Expecting ':' delimiter."
113+
assert message.start_position.char_index == 4
103114

104-
with pytest.raises(JSONDecodeError) as exc_info:
115+
with pytest.raises(ParseError) as exc_info:
105116
tokenize_json('{"a":')
106117
exc = exc_info.value
107-
assert exc.msg == "Expecting value"
108-
assert exc.pos == 5
118+
message = exc.messages()[0]
119+
assert message.text == "Expecting value."
120+
assert message.start_position.char_index == 5
109121

110-
with pytest.raises(JSONDecodeError) as exc_info:
122+
with pytest.raises(ParseError) as exc_info:
111123
tokenize_json('{"a":1')
112124
exc = exc_info.value
113-
assert exc.msg == "Expecting ',' delimiter"
114-
assert exc.pos == 6
125+
message = exc.messages()[0]
126+
assert message.text == "Expecting ',' delimiter."
127+
assert message.start_position.char_index == 6
115128

116-
with pytest.raises(JSONDecodeError) as exc_info:
129+
with pytest.raises(ParseError) as exc_info:
117130
tokenize_json('{"a":1,1')
118131
exc = exc_info.value
119-
assert exc.msg == "Expecting property name enclosed in double quotes"
120-
assert exc.pos == 7
132+
message = exc.messages()[0]
133+
assert message.text == "Expecting property name enclosed in double quotes."
134+
assert message.start_position.char_index == 7
121135

122-
with pytest.raises(JSONDecodeError) as exc_info:
136+
with pytest.raises(ParseError) as exc_info:
123137
tokenize_json('{"a":1 "b"')
124138
exc = exc_info.value
125-
assert exc.msg == "Expecting ',' delimiter"
126-
assert exc.pos == 7
139+
message = exc.messages()[0]
140+
assert message.text == "Expecting ',' delimiter."
141+
assert message.start_position.char_index == 7
142+
143+
with pytest.raises(ParseError) as exc_info:
144+
tokenize_json('{"a" 123}')
145+
exc = exc_info.value
146+
message = exc.messages()[0]
147+
assert message.text == "Expecting ':' delimiter."
148+
assert message.start_position.char_index == 5

tests/tokenize/test_tokenize_yaml.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from typesystem.tokenize.tokenize_yaml import tokenize_yaml
1+
import pytest
2+
3+
from typesystem import ParseError, tokenize_yaml
24
from typesystem.tokenize.tokens import DictToken, ListToken, ScalarToken
35

46
YAML_OBJECT = """
@@ -56,3 +58,19 @@ def test_tokenize_floats():
5658
token = tokenize_yaml(YAML_FLOATS)
5759
expected = ListToken([ScalarToken(100.0, 3, 7), ScalarToken(100.0, 11, 16)], 1, 17)
5860
assert token == expected
61+
62+
63+
def test_tokenize_parse_errors():
64+
with pytest.raises(ParseError) as exc_info:
65+
tokenize_yaml(b"")
66+
exc = exc_info.value
67+
message = exc.messages()[0]
68+
assert message.text == "No content."
69+
assert message.start_position.char_index == 0
70+
71+
with pytest.raises(ParseError) as exc_info:
72+
tokenize_yaml('{"a" 1}')
73+
exc = exc_info.value
74+
message = exc.messages()[0]
75+
assert message.text == "expected ',' or '}', but got '<scalar>'."
76+
assert message.start_position.char_index == 5

tests/tokenize/test_validate_json.py

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,24 @@
1-
from typesystem import Integer, Object, Schema
1+
import pytest
2+
3+
from typesystem import Integer, Object, Schema, ValidationError
24
from typesystem.base import Message, Position
35
from typesystem.tokenize.tokenize_json import validate_json
46

57

68
def test_validate_json():
7-
value, messages = validate_json("")
8-
assert value is None
9-
assert messages == [
10-
Message(
11-
text="No content.",
12-
code="no_content",
13-
position=Position(line_no=1, column_no=1, char_index=0),
14-
)
15-
]
16-
assert (
17-
repr(messages[0])
18-
== "Message(text='No content.', code='no_content', position=Position(line_no=1, column_no=1, char_index=0))"
19-
)
20-
21-
value, messages = validate_json('{"a": 123}')
22-
assert value == {"a": 123}
23-
assert messages == []
24-
25-
value, messages = validate_json(b'{"a": 123}')
26-
assert value == {"a": 123}
27-
assert messages == []
28-
29-
value, messages = validate_json('{"a" 123}')
30-
assert value is None
31-
assert messages == [
32-
Message(
33-
text="Expecting ':' delimiter.",
34-
code="parse_error",
35-
position=Position(line_no=1, column_no=6, char_index=5),
36-
)
37-
]
9+
validator = Object(properties=Integer())
10+
text = '{\n "a": "123",\n "b": "456"}'
11+
value = validate_json(text, validator=validator)
12+
assert value == {"a": 123, "b": 456}
3813

3914
validator = Object(properties=Integer())
4015
text = '{\n "a": "123",\n "b": "abc"}'
41-
value, messages = validate_json(text, validator=validator)
42-
assert value is None
43-
assert messages == [
16+
17+
with pytest.raises(ValidationError) as exc_info:
18+
validate_json(text, validator=validator)
19+
exc = exc_info.value
20+
21+
assert exc.messages() == [
4422
Message(
4523
text="Must be a number.",
4624
code="type",
@@ -50,24 +28,19 @@ def test_validate_json():
5028
)
5129
]
5230
assert (
53-
repr(messages[0])
31+
repr(exc.messages()[0])
5432
== "Message(text='Must be a number.', code='type', index=['b'], start_position=Position(line_no=3, column_no=10, char_index=27), end_position=Position(line_no=3, column_no=14, char_index=31))"
5533
)
5634

57-
validator = Object(properties=Integer())
58-
text = '{\n "a": "123",\n "b": "456"}'
59-
value, messages = validate_json(text, validator=validator)
60-
assert value == {"a": 123, "b": 456}
61-
assert messages == []
62-
6335
class Validator(Schema):
6436
a = Integer()
6537
b = Integer()
6638

6739
text = '{\n "a": "123",\n "b": "abc"}'
68-
value, messages = validate_json(text, validator=Validator)
69-
assert value is None
70-
assert messages == [
40+
with pytest.raises(ValidationError) as exc_info:
41+
validate_json(text, validator=Validator)
42+
exc = exc_info.value
43+
assert exc.messages() == [
7144
Message(
7245
text="Must be a number.",
7346
code="type",
@@ -78,9 +51,10 @@ class Validator(Schema):
7851
]
7952

8053
text = '{"a": "123"}'
81-
value, messages = validate_json(text, validator=Validator)
82-
assert value is None
83-
assert messages == [
54+
with pytest.raises(ValidationError) as exc_info:
55+
validate_json(text, validator=Validator)
56+
exc = exc_info.value
57+
assert exc.messages() == [
8458
Message(
8559
text="The field 'b' is required.",
8660
code="required",

tests/tokenize/test_validate_yaml.py

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,23 @@
1-
from typesystem import Integer, Object, Schema
1+
import pytest
2+
3+
from typesystem import Integer, Object, Schema, ValidationError, validate_yaml
24
from typesystem.base import Message, Position
3-
from typesystem.tokenize.tokenize_yaml import validate_yaml
45

56

67
def test_validate_yaml():
7-
value, messages = validate_yaml("")
8-
assert value is None
9-
assert messages == [
10-
Message(
11-
text="No content.",
12-
code="no_content",
13-
position=Position(line_no=1, column_no=1, char_index=0),
14-
)
15-
]
16-
17-
value, messages = validate_yaml("a: 123")
18-
assert value == {"a": 123}
19-
assert messages == []
20-
21-
value, messages = validate_yaml(b"a: 123")
22-
assert value == {"a": 123}
23-
assert messages == []
24-
25-
value, messages = validate_yaml('{"a" 1}')
26-
assert value is None
27-
assert messages == [
28-
Message(
29-
text="expected ',' or '}', but got '<scalar>'.",
30-
code="parse_error",
31-
position=Position(line_no=1, column_no=6, char_index=5),
32-
)
33-
]
8+
validator = Object(properties=Integer())
9+
text = "a: 123\nb: 456\n"
10+
value = validate_yaml(text, validator=validator)
11+
assert value == {"a": 123, "b": 456}
3412

3513
validator = Object(properties=Integer())
3614
text = "a: 123\nb: abc\n"
37-
value, messages = validate_yaml(text, validator=validator)
38-
assert value is None
39-
assert messages == [
15+
16+
with pytest.raises(ValidationError) as exc_info:
17+
validate_yaml(text, validator=validator)
18+
19+
exc = exc_info.value
20+
assert exc.messages() == [
4021
Message(
4122
text="Must be a number.",
4223
code="type",
@@ -46,20 +27,17 @@ def test_validate_yaml():
4627
)
4728
]
4829

49-
validator = Object(properties=Integer())
50-
text = "a: 123\nb: 456\n"
51-
value, messages = validate_yaml(text, validator=validator)
52-
assert value == {"a": 123, "b": 456}
53-
assert messages == []
54-
5530
class Validator(Schema):
5631
a = Integer()
5732
b = Integer()
5833

5934
text = "a: 123\nb: abc\n"
60-
value, messages = validate_yaml(text, validator=Validator)
61-
assert value is None
62-
assert messages == [
35+
36+
with pytest.raises(ValidationError) as exc_info:
37+
validate_yaml(text, validator=Validator)
38+
39+
exc = exc_info.value
40+
assert exc.messages() == [
6341
Message(
6442
text="Must be a number.",
6543
code="type",
@@ -70,9 +48,10 @@ class Validator(Schema):
7048
]
7149

7250
text = "a: 123"
73-
value, messages = validate_yaml(text, validator=Validator)
74-
assert value is None
75-
assert messages == [
51+
with pytest.raises(ValidationError) as exc_info:
52+
validate_yaml(text, validator=Validator)
53+
exc = exc_info.value
54+
assert exc.messages() == [
7655
Message(
7756
text="The field 'b' is required.",
7857
code="required",

typesystem/__init__.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typesystem.base import ValidationError
1+
from typesystem.base import ParseError, ValidationError, Message, Position
22
from typesystem.fields import (
3-
Array,
43
Any,
4+
Array,
55
Boolean,
66
Choice,
77
Date,
@@ -20,8 +20,9 @@
2020
from typesystem.forms import Jinja2Forms
2121
from typesystem.json_schema import from_json_schema, to_json_schema
2222
from typesystem.schemas import Reference, Schema, SchemaDefinitions
23-
from typesystem.tokenize.tokenize_json import validate_json
24-
from typesystem.tokenize.tokenize_yaml import validate_yaml
23+
from typesystem.tokenize.positional_validation import validate_with_positions
24+
from typesystem.tokenize.tokenize_json import tokenize_json, validate_json
25+
from typesystem.tokenize.tokenize_yaml import tokenize_yaml, validate_yaml
2526

2627
__version__ = "0.1.12"
2728
__all__ = [
@@ -39,15 +40,25 @@
3940
"Number",
4041
"Object",
4142
"Reference",
42-
"Schema",
4343
"String",
4444
"Text",
4545
"Time",
4646
"Union",
47-
"ValidationError",
47+
# Schemas
48+
"Schema",
4849
"SchemaDefinitions",
50+
# Exceptions
51+
"ParseError",
52+
"ValidationError",
53+
"Message",
54+
"Position",
55+
# JSON Schema
4956
"from_json_schema",
5057
"to_json_schema",
58+
# Positional error marking
59+
"tokenize_json",
60+
"tokenize_yaml",
5161
"validate_json",
5262
"validate_yaml",
63+
"validate_with_positions",
5364
]

0 commit comments

Comments
 (0)