forestwalker 0.1__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.
@@ -0,0 +1,4 @@
1
+ __pycache__
2
+ dist
3
+ docs.out
4
+ venv
@@ -0,0 +1,19 @@
1
+ # Read the Docs configuration file
2
+ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3
+
4
+ # Required
5
+ version: 2
6
+
7
+ # Set the OS, Python version, and other tools you might need
8
+ build:
9
+ os: ubuntu-24.04
10
+ tools:
11
+ python: "3.13"
12
+
13
+ # Build documentation in the "docs/" directory with Sphinx
14
+ sphinx:
15
+ configuration: docs/conf.py
16
+
17
+ python:
18
+ install:
19
+ - requirements: docs/requirements.txt
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2026 Martin Mareš
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice (including the next
11
+ paragraph) shall be included in all copies or substantial portions of the
12
+ Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: forestwalker
3
+ Version: 0.1
4
+ Summary: A library for walking JSON-like trees, parsing and validating them.
5
+ Project-URL: Repository, https://github.com/gollux/forestwalker.git
6
+ Project-URL: Documentation, https://forestwalker.readthedocs.io/en/latest/
7
+ Project-URL: Issues, https://github.com/gollux/forestwalker/issues
8
+ Author-email: Martin Mareš <mj@ucw.cz>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: File Formats :: JSON
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+
18
+ # The Forest Walker
19
+
20
+ Python modules for parsing JSON, TOML, YAML, and similar formats
21
+ produce simple tree structures composed of numbers, strings, Booleans,
22
+ arrays, and dictionaries. These structures are then parsed for the second
23
+ time to produce more Pythonic data structures. This usually involves
24
+ validation of some kind.
25
+
26
+ We firmly believe that parsing and validation should not be separate
27
+ steps: structure of data should have a single source of truth (doing
28
+ otherwise opens door to security issues). Also, existing formal descriptions
29
+ of syntax (e.g., JSON schema) are too weak for many purposes and unwieldy
30
+ for others.
31
+
32
+ Enters the ``forestwalker``. A Python module that makes it easy to traverse
33
+ trees, parse values while checking types, and report invalid data with
34
+ their precise location in the tree.
35
+
36
+ ## License
37
+
38
+ The Forest Walker was written by Martin Mareš <mj@ucw.cz>.
39
+ It can be freely used and distributed under the MIT License.
@@ -0,0 +1,22 @@
1
+ # The Forest Walker
2
+
3
+ Python modules for parsing JSON, TOML, YAML, and similar formats
4
+ produce simple tree structures composed of numbers, strings, Booleans,
5
+ arrays, and dictionaries. These structures are then parsed for the second
6
+ time to produce more Pythonic data structures. This usually involves
7
+ validation of some kind.
8
+
9
+ We firmly believe that parsing and validation should not be separate
10
+ steps: structure of data should have a single source of truth (doing
11
+ otherwise opens door to security issues). Also, existing formal descriptions
12
+ of syntax (e.g., JSON schema) are too weak for many purposes and unwieldy
13
+ for others.
14
+
15
+ Enters the ``forestwalker``. A Python module that makes it easy to traverse
16
+ trees, parse values while checking types, and report invalid data with
17
+ their precise location in the tree.
18
+
19
+ ## License
20
+
21
+ The Forest Walker was written by Martin Mareš <mj@ucw.cz>.
22
+ It can be freely used and distributed under the MIT License.
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ API
2
+ ===
3
+
4
+ .. automodule:: forestwalker
5
+ :members:
6
+ :show-inheritance:
7
+ :member-order: bysource
@@ -0,0 +1,29 @@
1
+ # Configuration file for the Sphinx documentation builder.
2
+ #
3
+ # For the full list of built-in configuration values, see the documentation:
4
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html
5
+
6
+ # -- Project information -----------------------------------------------------
7
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8
+
9
+ project = 'The Forest Walker'
10
+ copyright = '2026, Martin Mareš'
11
+ author = 'Martin Mareš'
12
+
13
+ # -- General configuration ---------------------------------------------------
14
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
15
+
16
+ extensions = ['sphinx.ext.autodoc']
17
+
18
+ templates_path = ['_templates']
19
+ exclude_patterns = []
20
+
21
+ autodoc_inherit_docstrings = False
22
+
23
+
24
+
25
+ # -- Options for HTML output -------------------------------------------------
26
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
27
+
28
+ html_theme = 'alabaster'
29
+ html_static_path = ['_static']
@@ -0,0 +1,15 @@
1
+ from forestwalker import Walker, WalkerError
2
+
3
+ tree = {
4
+ 'name': 'Robin Hood',
5
+ 'height': 184,
6
+ }
7
+
8
+ try:
9
+ root = Walker(tree)
10
+ with root.enter_object() as obj:
11
+ name = obj['name'].as_str()
12
+ height = obj['height'].as_float(100)
13
+ print(name, height)
14
+ except WalkerError as err:
15
+ print(f'Parse error: {err}')
@@ -0,0 +1,13 @@
1
+ Examples
2
+ ========
3
+
4
+ ==============
5
+ Object parsing
6
+ ==============
7
+
8
+ The following code parses a simple JSON object (dictionary)
9
+ with two attributes: ``name`` is mandatory, while ``height``
10
+ defaults to 100 if missing. The context manager makes sure
11
+ that the object contains only keys referenced by the parser.
12
+
13
+ .. literalinclude:: example1.py
@@ -0,0 +1,27 @@
1
+ The Forest Walker
2
+ =================
3
+
4
+ Python modules for parsing JSON, TOML, YAML, and similar formats
5
+ produce simple tree structures composed of numbers, strings, Booleans,
6
+ arrays, and dictionaries. These structures are then parsed for the second
7
+ time to produce more Pythonic data structures. This usually involves
8
+ validation of some kind.
9
+
10
+ We firmly believe that parsing and validation should not be separate
11
+ steps: structure of data should have a single source of truth (doing
12
+ otherwise opens door to security issues). Also, existing formal descriptions
13
+ of syntax (e.g., JSON schema) are too weak for many purposes and unwieldy
14
+ for others.
15
+
16
+ Enters the ``forestwalker``. A Python module that makes it easy to traverse
17
+ trees, parse values while checking types, and report invalid data with
18
+ their precise location in the tree.
19
+
20
+ Table of contents:
21
+
22
+ .. toctree::
23
+ :maxdepth: 2
24
+
25
+ self
26
+ examples
27
+ api
@@ -0,0 +1,2 @@
1
+ sphinx
2
+ .
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "forestwalker"
3
+ description = "A library for walking JSON-like trees, parsing and validating them."
4
+ version = "0.1"
5
+ authors = [ { name = "Martin Mareš", email = "mj@ucw.cz" } ]
6
+ readme = "README.md"
7
+ requires-python = ">= 3.11"
8
+ classifiers = [
9
+ "Programming Language :: Python :: 3",
10
+ "Operating System :: OS Independent",
11
+ "Intended Audience :: Developers",
12
+ "Topic :: File Formats :: JSON",
13
+ ]
14
+ license = "MIT"
15
+ license-file = "LICENSE"
16
+
17
+ [project.urls]
18
+ Repository = "https://github.com/gollux/forestwalker.git"
19
+ Documentation = "https://forestwalker.readthedocs.io/en/latest/"
20
+ Issues = "https://github.com/gollux/forestwalker/issues"
21
+
22
+ [build-system]
23
+ requires = ["hatchling >= 1.26"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [pytest]
27
+ testpaths = "tests"
@@ -0,0 +1,4 @@
1
+ sphinx
2
+ pytest
3
+ build
4
+ twine
@@ -0,0 +1,380 @@
1
+ # A simple module for walking through a parsed JSON-like file
2
+ # (c) 2023-2026 Martin Mareš <mj@ucw.cz>
3
+
4
+ from collections.abc import Iterator
5
+ from enum import Enum
6
+ import re
7
+ from typing import Any, Optional, NoReturn, Tuple, Set, Type, TypeVar, Self
8
+
9
+
10
+ T = TypeVar('T')
11
+ E = TypeVar('E', bound=Enum)
12
+
13
+
14
+ class Walker:
15
+ """
16
+ A Walker is a pointer to a particular node of the tree,
17
+ or possible a missing child node. It also remembers the
18
+ path to the node.
19
+ """
20
+
21
+ obj: Any
22
+ """The tree node. :py:class:`MissingValue` if it is a missing node."""
23
+
24
+ parent: Optional['Walker'] = None
25
+ """Walker of the parent node. Used to reconstruct the path."""
26
+
27
+ custom_context: str = ""
28
+
29
+ def __init__(self, root: Any) -> None:
30
+ """Create a walker point to the given root of the tree."""
31
+ self.obj = root
32
+
33
+ def raise_error(self, msg) -> NoReturn:
34
+ """Raise a :py:exc:`WalkerError` on the current node with the given error message."""
35
+ raise WalkerError(self, msg)
36
+
37
+ def is_null(self) -> bool:
38
+ """Tests if the current node is ``None`` (JSON ``null``)."""
39
+ return self.obj is None
40
+
41
+ def is_str(self) -> bool:
42
+ """Tests if the current node is a string."""
43
+ return isinstance(self.obj, str)
44
+
45
+ def is_int(self) -> bool:
46
+ """Tests if the current node is an integer."""
47
+ return isinstance(self.obj, int)
48
+
49
+ def is_number(self) -> bool:
50
+ """Tests if the current node is an integer or float."""
51
+ return isinstance(self.obj, int) or isinstance(self.obj, float)
52
+
53
+ def is_bool(self) -> bool:
54
+ """Tests if the current node is a Boolean value."""
55
+ return isinstance(self.obj, bool)
56
+
57
+ def is_missing(self) -> bool:
58
+ """Tests if the current node is a missing child."""
59
+ return isinstance(self.obj, MissingValue)
60
+
61
+ def is_present(self) -> bool:
62
+ """Tests if the current node is *not* a missing child."""
63
+ return not isinstance(self.obj, MissingValue)
64
+
65
+ def is_array(self) -> bool:
66
+ """Tests if the current node is an array."""
67
+ return isinstance(self.obj, list)
68
+
69
+ def is_object(self) -> bool:
70
+ """Tests if the current node is an object (dictionary)."""
71
+ return isinstance(self.obj, dict)
72
+
73
+ def expect_present(self) -> Self:
74
+ """Raises an error if the current node is a missing child. Returns itself."""
75
+ if self.is_missing():
76
+ self.raise_error('Mandatory key is missing')
77
+ return self
78
+
79
+ def as_type(self, typ: Type[T], msg: str, default: Optional[T] = None) -> T:
80
+ if isinstance(self.obj, typ):
81
+ return self.obj
82
+ elif self.is_missing():
83
+ if default is None:
84
+ self.raise_error('Mandatory key is missing')
85
+ else:
86
+ return default
87
+ else:
88
+ self.raise_error(msg)
89
+
90
+ def as_optional_type(self, typ: Type[T], msg: str) -> Optional[T]:
91
+ if isinstance(self.obj, typ):
92
+ return self.obj
93
+ elif self.is_missing():
94
+ return None
95
+ else:
96
+ self.raise_error(msg)
97
+
98
+ def as_str(self, default: Optional[str] = None) -> str:
99
+ """
100
+ If the current node is a string, returns its value.
101
+ If it is missing and *default* is given, returns *default*.
102
+ Otherwise raises :py:exc:`WalkerError`.
103
+ """
104
+ return self.as_type(str, 'Expected a string', default)
105
+
106
+ def as_int(self, default: Optional[int] = None) -> int:
107
+ """
108
+ If the current node is an integer, returns its value.
109
+ If it is missing and *default* is given, returns *default*.
110
+ Otherwise raises :py:exc:`WalkerError`.
111
+ """
112
+ return self.as_type(int, 'Expected an integer', default)
113
+
114
+ def as_float(self, default: Optional[float] = None) -> float:
115
+ """
116
+ If the current node is a float, returns its value.
117
+ If it is an integer, it is cast to a float.
118
+ If it is missing and *default* is given, returns *default*.
119
+ Otherwise raises :py:exc:`WalkerError`.
120
+ """
121
+ if isinstance(self.obj, int):
122
+ return float(self.obj)
123
+ else:
124
+ return self.as_type(float, 'Expected a number', default)
125
+
126
+ def as_bool(self, default: Optional[bool] = None) -> bool:
127
+ """
128
+ If the current node is a Boolean, returns its value.
129
+ If it is missing and *default* is given, returns *default*.
130
+ Otherwise raises :py:exc:`WalkerError`.
131
+ """
132
+ return self.as_type(bool, 'Expected a Boolean value', default)
133
+
134
+ def as_enum(self, enum: Type[E], default: Optional[E] = None) -> E:
135
+ """
136
+ If the current node is a string, returns its value
137
+ cast to the given enumeration type (descendant of :py:class:`Enum`).
138
+ If the node is missing and *default* is given, returns *default*.
139
+ Otherwise raises :py:exc:`WalkerError`.
140
+ """
141
+ if self.is_missing() and default is not None:
142
+ return default
143
+ try:
144
+ return enum(self.as_str())
145
+ except ValueError:
146
+ self.raise_error('Must be one of ' + '/'.join(sorted(enum.__members__.values()))) # FIXME: type
147
+
148
+ def as_optional_str(self) -> Optional[str]:
149
+ """
150
+ If the current node is a string, returns its value.
151
+ If it is missing, returns ``None``.
152
+ Otherwise raises :py:exc:`WalkerError`.
153
+ """
154
+ return self.as_optional_type(str, 'Expected a string')
155
+
156
+ def as_optional_int(self) -> Optional[int]:
157
+ """
158
+ If the current node is an integer, returns its value.
159
+ If it is missing, returns ``None``.
160
+ Otherwise raises :py:exc:`WalkerError`.
161
+ """
162
+ return self.as_optional_type(int, 'Expected an integer')
163
+
164
+ def as_optional_float(self) -> Optional[float]:
165
+ """
166
+ If the current node is a float, returns its value.
167
+ If it is an integer, it is cast to a float.
168
+ If it is missing, returns ``None``.
169
+ Otherwise raises :py:exc:`WalkerError`.
170
+ """
171
+ if isinstance(self.obj, int):
172
+ return float(self.obj)
173
+ else:
174
+ return self.as_optional_type(float, 'Expected a number')
175
+
176
+ def as_optional_bool(self) -> Optional[bool]:
177
+ """
178
+ If the current node is a Boolean, returns its value.
179
+ If it is missing, returns ``None``.
180
+ Otherwise raises :py:exc:`WalkerError`.
181
+ """
182
+ return self.as_optional_type(bool, 'Expected a Boolean value')
183
+
184
+ def array_values(self) -> Iterator['WalkerInArray']:
185
+ """
186
+ Produces an iterator over an array node, which yields :py:class:`WalkerInArray`
187
+ objects for the elements of the array.
188
+ If the node is not an array, raises :py:exc:`WalkerError`.
189
+ """
190
+ ary = self.as_type(list, 'Expected an array')
191
+ for i, obj in enumerate(ary):
192
+ yield WalkerInArray(obj, self, i)
193
+
194
+ def object_values(self) -> Iterator['WalkerInObject']:
195
+ """
196
+ Produces an iterator over attributes of an object (dictionary), which yields :py:class:`WalkerInObject`
197
+ objects for the values of the attributes.
198
+ If the node is not an object, raises :py:exc:`WalkerError`.
199
+ """
200
+ dct = self.as_type(dict, 'Expected an object')
201
+ for key, obj in dct.items():
202
+ yield WalkerInObject(obj, self, key)
203
+
204
+ def object_items(self) -> Iterator[Tuple[str, 'WalkerInObject']]:
205
+ """
206
+ Produces an iterator over attributes of an object (dictionary), which yields pairs of the
207
+ attribute's name and a :py:class:`WalkerInObject` object for the attribute's value.
208
+ If the node is not an object, raises :py:exc:`WalkerError`.
209
+ """
210
+ dct = self.as_type(dict, 'Expected an object')
211
+ for key, obj in dct.items():
212
+ yield key, WalkerInObject(obj, self, key)
213
+
214
+ def enter_object(self) -> 'ObjectWalker':
215
+ """
216
+ Produces an :py:class:`ObjectWalker` for an object (dictionary),
217
+ which allows indexing of the object's attributes and checking which
218
+ attributes are missing.
219
+ If the node is not an object, raises :py:exc:`WalkerError`.
220
+ """
221
+ dct = self.as_type(dict, 'Expected an object')
222
+ return ObjectWalker(dct, self)
223
+
224
+ def default_to(self, default) -> 'Walker': # XXX: Use Self when available
225
+ """
226
+ If the current node is missing, make the walker point to *default* instead.
227
+ Returns itself, so it is possible to write e.g.
228
+ ``walker.default_to([]).array_values()``.
229
+ """
230
+ if self.is_missing():
231
+ self.obj = default
232
+ return self
233
+
234
+ def context(self) -> str:
235
+ """Construct a path fragment for the current node."""
236
+ return 'root'
237
+
238
+ def set_custom_context(self, ctx: str) -> None:
239
+ """Set a string that is appended to the path fragment returned by :py:meth:`context`."""
240
+ self.custom_context = ctx
241
+
242
+
243
+ class WalkerInArray(Walker):
244
+ """
245
+ A :py:class:`Walker` referring to an array item.
246
+ Never constructed directly, use :py:meth:`Walker.array_items` to obtain it.
247
+ """
248
+
249
+ index: int
250
+ """Position of the item in the array (zero-based)."""
251
+
252
+ def __init__(self, obj: Any, parent: Walker, index: int) -> None:
253
+ super().__init__(obj)
254
+ self.parent = parent
255
+ self.index = index
256
+
257
+ def context(self) -> str:
258
+ return f'[{self.index}]'
259
+
260
+
261
+ class WalkerInObject(Walker):
262
+ """
263
+ A :py:class:`Walker` referring to an object attribute (dictionary item).
264
+ Never constructed directly, use :py:meth:`Walker.object_values`,
265
+ :py:meth:`Walker.object_items`, or indexing in :py:meth:`ObjectWalker`
266
+ to obtain it.
267
+ """
268
+
269
+ key: str
270
+ """Key of the attribute."""
271
+
272
+ def __init__(self, obj: Any, parent: Walker, key: str) -> None:
273
+ super().__init__(obj)
274
+ self.parent = parent
275
+ self.key = key
276
+
277
+ def context(self) -> str:
278
+ if re.fullmatch(r'\w+', self.key):
279
+ return f'.{self.key}'
280
+ else:
281
+ quoted_key = re.sub(r'(\\|")', r'\\\1', self.key)
282
+ return f'."{quoted_key}"'
283
+
284
+ def unexpected(self) -> NoReturn:
285
+ """Raises a :py:exc:`WalkerError` complaining that this key was not expected."""
286
+ self.raise_error('Unexpected key')
287
+
288
+
289
+ class ObjectWalker(Walker):
290
+ """
291
+ A :py:class:`Walker` for inspecting an object (dictionary).
292
+ In addition to the default walker, it allows indexing of attribute
293
+ by the square brackets operator. It also remembers which attributes
294
+ were referenced this way and when used as a context manager, it
295
+ complains upon exit from the context if the object contains
296
+ unreferenced attributes.
297
+
298
+ Never constructed directly, use :py:meth:`Walker.enter_object`
299
+ to obtain it.
300
+ """
301
+
302
+ referenced_keys: Set[str]
303
+ """A set of keys of referenced attributes."""
304
+
305
+ def __init__(self, obj: Any, parent: Walker) -> None:
306
+ super().__init__(obj)
307
+ assert isinstance(obj, dict)
308
+ self.parent = parent
309
+ self.referenced_keys = set()
310
+
311
+ def __enter__(self) -> 'ObjectWalker':
312
+ """Enter a context."""
313
+ return self
314
+
315
+ def __exit__(self, exc_type, exc_value, traceback) -> None:
316
+ """Exit a context, calling :py:meth:`assert_no_other_keys`."""
317
+ if exc_type is None:
318
+ self.assert_no_other_keys()
319
+
320
+ def context(self) -> str:
321
+ return ""
322
+
323
+ def __contains__(self, key: str) -> bool:
324
+ """
325
+ Implements the ``in`` operator for testing if the object contains an
326
+ attribute with the given key.
327
+ """
328
+ return key in self.obj
329
+
330
+ def __getitem__(self, key: str) -> WalkerInObject:
331
+ """
332
+ Implements the ``[]`` operator for accessing an attribute with
333
+ the given key. Returns a :py:class:`WalkerInObject` referring to
334
+ the attribute's value.
335
+ """
336
+ if key in self.obj:
337
+ self.referenced_keys.add(key)
338
+ return WalkerInObject(self.obj[key], self, key)
339
+ else:
340
+ return WalkerInObject(MissingValue(), self, key)
341
+
342
+ def assert_no_other_keys(self) -> None:
343
+ """
344
+ Checks if the object has attributes not accessed by the ``[]``
345
+ operator. Raises an :py:exc:`WalkerError` if there are any.
346
+ """
347
+ for key, val in self.obj.items():
348
+ if key not in self.referenced_keys:
349
+ WalkerInObject(val, self, key).unexpected()
350
+
351
+
352
+ class MissingValue:
353
+ pass
354
+
355
+
356
+ class WalkerError(Exception):
357
+ """An exception for any error occurring during walking the tree."""
358
+
359
+ walker: Walker
360
+ """
361
+ The walker that raised the exception. Used to reconstruct the path
362
+ to the erroneous node.
363
+ """
364
+
365
+ msg: str
366
+ """Error message."""
367
+
368
+ def __init__(self, walker: Walker, msg: str) -> None:
369
+ self.walker = walker
370
+ self.msg = msg
371
+
372
+ def __str__(self) -> str:
373
+ """Returns a string consisting of the path in the tree and the error message."""
374
+ contexts = []
375
+ w: Optional[Walker] = self.walker
376
+ while w is not None:
377
+ contexts.append(w.context())
378
+ contexts.append(w.custom_context)
379
+ w = w.parent
380
+ return "".join(reversed(contexts)) + ": " + self.msg
@@ -0,0 +1,92 @@
1
+ from forestwalker import Walker, WalkerError
2
+ import unittest
3
+
4
+
5
+ class TestWalker(unittest.TestCase):
6
+
7
+ def test_is_int(self):
8
+ self.assertEqual(Walker(42).is_int(), True)
9
+
10
+ def test_is_int_not(self):
11
+ self.assertEqual(Walker('forty-two').is_int(), False)
12
+
13
+ def test_as_int(self):
14
+ self.assertEqual(Walker(42).as_int(), 42)
15
+
16
+ def test_as_int_not(self):
17
+ with self.assertRaises(WalkerError):
18
+ Walker('forty-two').as_int()
19
+
20
+ def test_is_bool(self):
21
+ self.assertEqual(Walker(False).is_bool(), True)
22
+
23
+ def test_array(self):
24
+ w = Walker([1, 2, 3])
25
+ b = []
26
+ for val in w.array_values():
27
+ b.append(val.as_int())
28
+ self.assertEqual(b, [1, 2, 3])
29
+
30
+ def test_array_not(self):
31
+ with self.assertRaises(WalkerError):
32
+ for _ in Walker(42).array_values():
33
+ pass
34
+
35
+ def test_array_context(self):
36
+ with self.assertRaisesRegex(WalkerError, r'^root\[1]: '):
37
+ w = Walker([1, None, 3])
38
+ for val in w.array_values():
39
+ val.as_int()
40
+
41
+ def test_object_values(self):
42
+ d = {"one": 1, "three": 3, "eleven": 11}
43
+ w = Walker(d)
44
+ b = set()
45
+ for val in w.object_values():
46
+ b.add(val.as_int())
47
+ self.assertEqual(b, set(d.values()))
48
+
49
+ def test_object_items(self):
50
+ d = {"one": 1, "three": 3, "eleven": 11}
51
+ w = Walker(d)
52
+ b = {}
53
+ for key, val in w.object_items():
54
+ b[key] = val.as_int()
55
+ self.assertEqual(b, d)
56
+
57
+ def test_object_values_not(self):
58
+ with self.assertRaises(WalkerError):
59
+ for _ in Walker(42).object_values():
60
+ pass
61
+
62
+ def test_complex_context(self):
63
+ with self.assertRaisesRegex(WalkerError, r'^root\.three\[2]: '):
64
+ w = Walker({"one": 1, "three": [5, 6, 'huiiii!', 7], "eleven": 11})
65
+ for key, val in w.object_items():
66
+ if val.is_array():
67
+ for wal in val.array_values():
68
+ wal.as_int()
69
+ else:
70
+ val.as_int()
71
+
72
+ def test_object_walker_not(self):
73
+ with self.assertRaises(WalkerError):
74
+ Walker(42).enter_object()
75
+
76
+ def test_object_walker(self):
77
+ with Walker({'one': 1, 'two': 'dva', 'three': False}).enter_object() as w:
78
+ self.assertEqual(w['one'].as_int(4), 1)
79
+ self.assertEqual(w['two'].as_str(), 'dva')
80
+ self.assertEqual(w['three'].as_bool(), False)
81
+ self.assertEqual(w['four'].as_int(4), 4)
82
+
83
+ def test_object_walker_other(self):
84
+ with self.assertRaisesRegex(WalkerError, r'^root\.three: Unexpected key$'):
85
+ with Walker({'one': 1, 'two': 'dva', 'three': False}).enter_object() as w:
86
+ self.assertEqual(w['one'].as_int(4), 1)
87
+ self.assertEqual(w['two'].as_str(), 'dva')
88
+
89
+ def test_object_walker_missing(self):
90
+ with self.assertRaisesRegex(WalkerError, r'^root\.five: Mandatory key is missing$'):
91
+ with Walker({'one': 1, 'two': 'dva', 'three': False}).enter_object() as w:
92
+ self.assertEqual(w['five'].as_int())
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ exec sphinx-build -M html docs docs.out
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ rm -rf dist
5
+ python3 -m build
6
+ ls -al dist
7
+
8
+ echo -n "Enter to upload... "
9
+ read YES
10
+
11
+ python3 -m twine upload dist/*
12
+
13
+ echo "Done."