hitori 1.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.
- hitori-1.0.1/LICENSE.md +55 -0
- hitori-1.0.1/PKG-INFO +54 -0
- hitori-1.0.1/README.md +33 -0
- hitori-1.0.1/hitori/__init__.py +166 -0
- hitori-1.0.1/hitori/py.typed +0 -0
- hitori-1.0.1/pyproject.toml +27 -0
hitori-1.0.1/LICENSE.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Blue Oak Model License
|
|
2
|
+
|
|
3
|
+
Version 1.0.0
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This license gives everyone as much permission to work with
|
|
8
|
+
this software as possible, while protecting contributors
|
|
9
|
+
from liability.
|
|
10
|
+
|
|
11
|
+
## Acceptance
|
|
12
|
+
|
|
13
|
+
In order to receive this license, you must agree to its
|
|
14
|
+
rules. The rules of this license are both obligations
|
|
15
|
+
under that agreement and conditions to your license.
|
|
16
|
+
You must not do anything with this software that triggers
|
|
17
|
+
a rule that you cannot or will not follow.
|
|
18
|
+
|
|
19
|
+
## Copyright
|
|
20
|
+
|
|
21
|
+
Each contributor licenses you to do everything with this
|
|
22
|
+
software that would otherwise infringe that contributor's
|
|
23
|
+
copyright in it.
|
|
24
|
+
|
|
25
|
+
## Notices
|
|
26
|
+
|
|
27
|
+
You must ensure that everyone who gets a copy of
|
|
28
|
+
any part of this software from you, with or without
|
|
29
|
+
changes, also gets the text of this license or a link to
|
|
30
|
+
<https://blueoakcouncil.org/license/1.0.0>.
|
|
31
|
+
|
|
32
|
+
## Excuse
|
|
33
|
+
|
|
34
|
+
If anyone notifies you in writing that you have not
|
|
35
|
+
complied with [Notices](#notices), you can keep your
|
|
36
|
+
license by taking all practical steps to comply within 30
|
|
37
|
+
days after the notice. If you do not do so, your license
|
|
38
|
+
ends immediately.
|
|
39
|
+
|
|
40
|
+
## Patent
|
|
41
|
+
|
|
42
|
+
Each contributor licenses you to do everything with this
|
|
43
|
+
software that would otherwise infringe any patent claims
|
|
44
|
+
they can license or become able to license.
|
|
45
|
+
|
|
46
|
+
## Reliability
|
|
47
|
+
|
|
48
|
+
No contributor can revoke this license.
|
|
49
|
+
|
|
50
|
+
## No Liability
|
|
51
|
+
|
|
52
|
+
***As far as the law allows, this software comes as is,
|
|
53
|
+
without any warranty or condition, and no contributor
|
|
54
|
+
will be liable to anyone for any damages related to this
|
|
55
|
+
software or this license, under any kind of legal claim.***
|
hitori-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hitori
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Parsing and serializing Sakura Expressions
|
|
5
|
+
Keywords: Sakura Expressions,S-Expression,Parser
|
|
6
|
+
Author-email: Hannes Braun <hannes@hannesbraun.net>
|
|
7
|
+
Requires-Python: >= 3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-Expression: BlueOak-1.0.0
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Topic :: Software Development
|
|
14
|
+
License-File: LICENSE.md
|
|
15
|
+
Project-URL: Changelog, https://git.sr.ht/~hannes/hitori/refs
|
|
16
|
+
Project-URL: Documentation, https://hannesbraun.net/share/doc/hitori/index.html
|
|
17
|
+
Project-URL: Homepage, https://git.sr.ht/~hannes/hitori
|
|
18
|
+
Project-URL: Repository, https://git.sr.ht/~hannes/hitori
|
|
19
|
+
Import-Name: hitori
|
|
20
|
+
|
|
21
|
+
# hitori
|
|
22
|
+
|
|
23
|
+
*a Python library for [Sakura Expressions](https://git.sr.ht/~hannes/sakura-expressions)*
|
|
24
|
+
|
|
25
|
+
This library also supports parsing in fragments. The input can be given piece by
|
|
26
|
+
piece to the library before the final result is returned.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
The package can be installed via the PyPI:
|
|
31
|
+
|
|
32
|
+
pip install hitori
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
You can find a documentation of the API here:
|
|
37
|
+
https://hannesbraun.net/share/doc/hitori
|
|
38
|
+
|
|
39
|
+
Here is a small example:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import hitori
|
|
43
|
+
|
|
44
|
+
root = hitori.parse("(foo bar baz)")
|
|
45
|
+
print(root[0][0]) # prints "foo"
|
|
46
|
+
serialized = hitori.serialize(root)
|
|
47
|
+
print(serialized) # prints "(foo bar baz)"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
hitori is published under the terms of the [Blue Oak Model License](LICENSE.md).
|
|
53
|
+
|
|
54
|
+
|
hitori-1.0.1/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# hitori
|
|
2
|
+
|
|
3
|
+
*a Python library for [Sakura Expressions](https://git.sr.ht/~hannes/sakura-expressions)*
|
|
4
|
+
|
|
5
|
+
This library also supports parsing in fragments. The input can be given piece by
|
|
6
|
+
piece to the library before the final result is returned.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
The package can be installed via the PyPI:
|
|
11
|
+
|
|
12
|
+
pip install hitori
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
You can find a documentation of the API here:
|
|
17
|
+
https://hannesbraun.net/share/doc/hitori
|
|
18
|
+
|
|
19
|
+
Here is a small example:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import hitori
|
|
23
|
+
|
|
24
|
+
root = hitori.parse("(foo bar baz)")
|
|
25
|
+
print(root[0][0]) # prints "foo"
|
|
26
|
+
serialized = hitori.serialize(root)
|
|
27
|
+
print(serialized) # prints "(foo bar baz)"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
hitori is published under the terms of the [Blue Oak Model License](LICENSE.md).
|
|
33
|
+
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ParseException(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SerializationException(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
type SExpression = list[SExpression] | str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Parser:
|
|
16
|
+
"""An S-Expression parser"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._cur_atom = ""
|
|
20
|
+
self._stack = [[]]
|
|
21
|
+
self._inside_comment = False
|
|
22
|
+
self._need_whitespace = False
|
|
23
|
+
self._inside_unquoted = False
|
|
24
|
+
self._inside_quoted = False
|
|
25
|
+
self._escape_active = False
|
|
26
|
+
|
|
27
|
+
def _push_char(self, c: str):
|
|
28
|
+
self._cur_atom += c
|
|
29
|
+
|
|
30
|
+
def _push_atom(self):
|
|
31
|
+
self._stack[-1].append(self._cur_atom)
|
|
32
|
+
self._cur_atom = ""
|
|
33
|
+
|
|
34
|
+
def _start_list(self):
|
|
35
|
+
self._stack.append([])
|
|
36
|
+
|
|
37
|
+
def _end_list(self):
|
|
38
|
+
self._stack[-2].append(self._stack[-1])
|
|
39
|
+
self._stack.pop()
|
|
40
|
+
|
|
41
|
+
def parse(self, s: str):
|
|
42
|
+
"""
|
|
43
|
+
Parses the given string into an S-Expression.
|
|
44
|
+
|
|
45
|
+
This method can be called multiple times which might be useful if
|
|
46
|
+
the expression is received over the network and therefore split across
|
|
47
|
+
multiple strings. For example: calling `parse` with "a", "b" and then
|
|
48
|
+
"c" is the same as calling it once with "abc".
|
|
49
|
+
To retrieve the parsed value, call `Parser.finish`.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
for val in s:
|
|
53
|
+
c = ord(val)
|
|
54
|
+
if (c < 0x20 and c != 0x9 and c != 0xa and c != 0xd) or c == 0x7f:
|
|
55
|
+
raise ParseException("invalid character")
|
|
56
|
+
if self._inside_comment:
|
|
57
|
+
if val == "\n":
|
|
58
|
+
self._inside_comment = False
|
|
59
|
+
elif self._escape_active:
|
|
60
|
+
self._push_char(val)
|
|
61
|
+
self._escape_active = False
|
|
62
|
+
elif val == "\\":
|
|
63
|
+
if self._need_whitespace:
|
|
64
|
+
raise ParseException("atom must be followed by a whitespace character or comment before the next atom starts")
|
|
65
|
+
self._escape_active = True
|
|
66
|
+
if not self._inside_quoted:
|
|
67
|
+
self._inside_unquoted = True
|
|
68
|
+
elif val == ";" and not self._inside_quoted:
|
|
69
|
+
if self._inside_unquoted:
|
|
70
|
+
self._inside_unquoted = False
|
|
71
|
+
self._push_atom()
|
|
72
|
+
self._inside_comment = True
|
|
73
|
+
self._need_whitespace = False
|
|
74
|
+
elif val == '"':
|
|
75
|
+
if self._inside_unquoted:
|
|
76
|
+
raise ParseException("a double quote is not allowed inside of an unquoted atom")
|
|
77
|
+
if self._need_whitespace:
|
|
78
|
+
raise ParseException("atom must be followed by a whitespace character or comment before the next atom starts")
|
|
79
|
+
|
|
80
|
+
if self._inside_quoted:
|
|
81
|
+
self._inside_quoted = False
|
|
82
|
+
self._push_atom()
|
|
83
|
+
self._need_whitespace = True
|
|
84
|
+
else:
|
|
85
|
+
self._inside_quoted = True
|
|
86
|
+
elif val == "(" and not self._inside_quoted:
|
|
87
|
+
if self._cur_atom != "":
|
|
88
|
+
self._push_atom()
|
|
89
|
+
self._inside_unquoted = False
|
|
90
|
+
self._need_whitespace = False
|
|
91
|
+
self._start_list()
|
|
92
|
+
elif val == ")" and not self._inside_quoted:
|
|
93
|
+
if self._cur_atom != "":
|
|
94
|
+
self._push_atom()
|
|
95
|
+
self._inside_unquoted = False
|
|
96
|
+
self._need_whitespace = False
|
|
97
|
+
self._end_list()
|
|
98
|
+
elif not self._inside_quoted and (val == " " or c == 0xa or c == 0x9 or c == 0xd):
|
|
99
|
+
if self._cur_atom != "":
|
|
100
|
+
self._push_atom()
|
|
101
|
+
self._inside_unquoted = False
|
|
102
|
+
self._need_whitespace = False
|
|
103
|
+
else:
|
|
104
|
+
if self._need_whitespace:
|
|
105
|
+
raise ParseException("atom must be followed by a whitespace character or comment before the next atom starts")
|
|
106
|
+
if not self._inside_quoted:
|
|
107
|
+
self._inside_unquoted = True
|
|
108
|
+
self._push_char(val)
|
|
109
|
+
|
|
110
|
+
def finish(self) -> list[SExpression]:
|
|
111
|
+
"""Signals that no more data will be passed and returns the parsed S-Expression."""
|
|
112
|
+
if len(self._stack) > 1:
|
|
113
|
+
raise ParseException("at least one list is not closed")
|
|
114
|
+
if len(self._stack) == 0:
|
|
115
|
+
raise ParseException("too many closing parenthesis")
|
|
116
|
+
if self._escape_active:
|
|
117
|
+
raise ParseException("incomplete escape sequence")
|
|
118
|
+
elif self._cur_atom != "" and self._inside_unquoted:
|
|
119
|
+
self._push_atom()
|
|
120
|
+
elif self._inside_quoted:
|
|
121
|
+
raise ParseException("missing trailing quote for atom")
|
|
122
|
+
return self._stack[0]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse(data: str) -> list[SExpression]:
|
|
126
|
+
"""Parses a string into an S-Expression"""
|
|
127
|
+
parser = Parser()
|
|
128
|
+
parser.parse(data)
|
|
129
|
+
return parser.finish()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def serialize(expressions: list[SExpression], compact: bool = False) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Serializes a list of S-Expressions to a string
|
|
135
|
+
|
|
136
|
+
The argument `compact` controls if spaces between elements in a list should
|
|
137
|
+
be left out when they are not necessary.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
s = ""
|
|
141
|
+
was_atom = False
|
|
142
|
+
for i, sexp in enumerate(expressions):
|
|
143
|
+
is_atom = isinstance(sexp, str)
|
|
144
|
+
if compact and was_atom and is_atom:
|
|
145
|
+
s += " "
|
|
146
|
+
if is_atom:
|
|
147
|
+
quote = sexp == "" or any(c == " " or c == "(" or c == ")" or c == ";" or c == "\n" or c == "\r" or c == "\t" for c in sexp)
|
|
148
|
+
if quote:
|
|
149
|
+
s += '"'
|
|
150
|
+
for c in sexp:
|
|
151
|
+
if c == '"' or c == "\\":
|
|
152
|
+
s += "\\"
|
|
153
|
+
s += c
|
|
154
|
+
if quote:
|
|
155
|
+
s += '"'
|
|
156
|
+
elif isinstance(sexp, list):
|
|
157
|
+
s += "("
|
|
158
|
+
s += serialize(sexp, compact)
|
|
159
|
+
s += ")"
|
|
160
|
+
else:
|
|
161
|
+
raise SerializationException(f"the value {sexp} of type {type(sexp)} cannot be serialized into an s-expression")
|
|
162
|
+
if not compact and i < len(expressions) - 1:
|
|
163
|
+
s += " "
|
|
164
|
+
was_atom = is_atom
|
|
165
|
+
return s
|
|
166
|
+
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hitori"
|
|
3
|
+
version = "1.0.1"
|
|
4
|
+
description = "Parsing and serializing Sakura Expressions"
|
|
5
|
+
authors = [{name = "Hannes Braun", email = "hannes@hannesbraun.net"}]
|
|
6
|
+
requires-python = ">= 3.12"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
license = "BlueOak-1.0.0"
|
|
9
|
+
license-files = ["LICENSE.md"]
|
|
10
|
+
keywords = ["Sakura Expressions", "S-Expression", "Parser"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 5 - Production/Stable",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Programming Language :: Python",
|
|
15
|
+
"Topic :: Software Development"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://git.sr.ht/~hannes/hitori"
|
|
20
|
+
Documentation = "https://hannesbraun.net/share/doc/hitori/index.html"
|
|
21
|
+
Repository = "https://git.sr.ht/~hannes/hitori"
|
|
22
|
+
Changelog = "https://git.sr.ht/~hannes/hitori/refs"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["flit_core >= 3.12, < 5"]
|
|
26
|
+
build-backend = "flit_core.buildapi"
|
|
27
|
+
|