PyUMSN 1.0.0__py3-none-any.whl
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.
- pyumsn/__init__.py +35 -0
- pyumsn/__main__.py +5 -0
- pyumsn/cli.py +258 -0
- pyumsn/errors.py +75 -0
- pyumsn/ide/umsn_ide.umsn +1269 -0
- pyumsn/importer.py +70 -0
- pyumsn/launcher.py +223 -0
- pyumsn/lexer.py +422 -0
- pyumsn/repl.py +32 -0
- pyumsn/runner.py +118 -0
- pyumsn/sourceio.py +53 -0
- pyumsn/translator.py +393 -0
- pyumsn/vocab.py +949 -0
- pyumsn-1.0.0.dist-info/METADATA +278 -0
- pyumsn-1.0.0.dist-info/RECORD +19 -0
- pyumsn-1.0.0.dist-info/WHEEL +5 -0
- pyumsn-1.0.0.dist-info/entry_points.txt +2 -0
- pyumsn-1.0.0.dist-info/licenses/LICENSE +21 -0
- pyumsn-1.0.0.dist-info/top_level.txt +1 -0
pyumsn/translator.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""엄슨 ↔ 파이썬 변환기.
|
|
2
|
+
|
|
3
|
+
토큰 단위로만 바꾸므로 줄 번호가 그대로 유지된다.
|
|
4
|
+
주석과 문자열(docstring 포함)은 한 글자도 바꾸지 않는다.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from . import vocab
|
|
8
|
+
from .errors import UmsnSyntaxError
|
|
9
|
+
from .lexer import (MODE_PY, MODE_UMSN, Lexer, is_hangul_name, py_prefix_to_umsn,
|
|
10
|
+
tokenize, umsn_prefix_to_py)
|
|
11
|
+
|
|
12
|
+
_CODE_KINDS = frozenset(["name", "word", "dollar", "number", "op", "sigil"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# 음역: polyfit ↔ 외_피오르야프이트
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
def transliterate(name):
|
|
19
|
+
"""영어(ASCII) 이름을 한글 음역으로. 사전에 있는 이름도 음역만 한다."""
|
|
20
|
+
out = [vocab.TRANSLIT_MARK]
|
|
21
|
+
for ch in name:
|
|
22
|
+
low = ch.lower()
|
|
23
|
+
if low in vocab.TRANSLIT_LETTERS:
|
|
24
|
+
syl = vocab.TRANSLIT_LETTERS[low]
|
|
25
|
+
out.append(vocab.UPPER_MARK + syl if ch.isupper() else syl)
|
|
26
|
+
elif ch == "_" or ch in "0123456789":
|
|
27
|
+
out.append(ch)
|
|
28
|
+
else:
|
|
29
|
+
raise ValueError("음역할 수 없는 글자: %r" % ch)
|
|
30
|
+
return "".join(out)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def untransliterate(word):
|
|
34
|
+
"""``외_…`` 음역을 영어 이름으로 되돌린다. 음역 형태가 아니면 None."""
|
|
35
|
+
mark = vocab.TRANSLIT_MARK
|
|
36
|
+
if not word.startswith(mark) or len(word) == len(mark):
|
|
37
|
+
return None
|
|
38
|
+
out = []
|
|
39
|
+
i = len(mark)
|
|
40
|
+
n = len(word)
|
|
41
|
+
while i < n:
|
|
42
|
+
ch = word[i]
|
|
43
|
+
if ch == "_" or ch in "0123456789":
|
|
44
|
+
out.append(ch)
|
|
45
|
+
i += 1
|
|
46
|
+
continue
|
|
47
|
+
upper = False
|
|
48
|
+
if ch == vocab.UPPER_MARK:
|
|
49
|
+
upper = True
|
|
50
|
+
i += 1
|
|
51
|
+
if i >= n:
|
|
52
|
+
return None
|
|
53
|
+
ch = word[i]
|
|
54
|
+
letter = vocab.TRANSLIT_REVERSE.get(ch)
|
|
55
|
+
if letter is None:
|
|
56
|
+
return None
|
|
57
|
+
out.append(letter.upper() if upper else letter)
|
|
58
|
+
i += 1
|
|
59
|
+
result = "".join(out)
|
|
60
|
+
if not any(c.isalpha() for c in result) or result[0] in "0123456789":
|
|
61
|
+
return None
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def suggest_name(name):
|
|
66
|
+
"""영어 이름을 쓴 사람에게 보여줄 제안."""
|
|
67
|
+
if name in vocab.PY2UMSN:
|
|
68
|
+
return vocab.PY2UMSN[name]
|
|
69
|
+
try:
|
|
70
|
+
return transliterate(name)
|
|
71
|
+
except ValueError:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# 이름 변환
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
def _umsn_name_to_py(text):
|
|
79
|
+
"""(파이썬 이름, 오류메시지) 를 돌려준다."""
|
|
80
|
+
py = vocab.UMSN2PY.get(text)
|
|
81
|
+
if py is not None:
|
|
82
|
+
return py, None
|
|
83
|
+
py = untransliterate(text)
|
|
84
|
+
if py is not None:
|
|
85
|
+
return py, None
|
|
86
|
+
if is_hangul_name(text) or all(ch == "_" for ch in text):
|
|
87
|
+
return text, None
|
|
88
|
+
if any(ch.isascii() and ch.isalpha() for ch in text):
|
|
89
|
+
hint = suggest_name(text) if text.isascii() else None
|
|
90
|
+
msg = "영어 이름 '%s' 는 쓸 수 없슨! 한글 이름을 쓰세요." % text
|
|
91
|
+
if hint:
|
|
92
|
+
msg = "영어 이름 '%s' 는 쓸 수 없슨! 한글 이름을 쓰거나 '%s' 를 쓰세요." % (text, hint)
|
|
93
|
+
return text, msg
|
|
94
|
+
return text, "이름 '%s' 에 한글이 아닌 글자가 있슨! 엄슨 이름은 한글·숫자·_ 만 됩니다." % text
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _py_name_to_umsn(text):
|
|
98
|
+
um = vocab.PY2UMSN.get(text)
|
|
99
|
+
if um is not None:
|
|
100
|
+
return um
|
|
101
|
+
if text.isascii():
|
|
102
|
+
if not any(ch.isalpha() for ch in text):
|
|
103
|
+
return text
|
|
104
|
+
return transliterate(text)
|
|
105
|
+
if is_hangul_name(text):
|
|
106
|
+
if text in vocab.UMSN2PY or untransliterate(text) is not None:
|
|
107
|
+
return "$" + text
|
|
108
|
+
return text
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# 엄슨 → 파이썬
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
def _umsn_tokens_to_py(tokens):
|
|
116
|
+
out = []
|
|
117
|
+
problems = []
|
|
118
|
+
for tok in tokens:
|
|
119
|
+
kind = tok.kind
|
|
120
|
+
text = tok.text
|
|
121
|
+
if kind == "name":
|
|
122
|
+
py, err = _umsn_name_to_py(text)
|
|
123
|
+
if err:
|
|
124
|
+
problems.append((tok.start, len(text), err))
|
|
125
|
+
out.append(py)
|
|
126
|
+
elif kind == "word":
|
|
127
|
+
out.append(vocab.UMSN2PY[text])
|
|
128
|
+
elif kind == "dollar":
|
|
129
|
+
name = text[1:]
|
|
130
|
+
if not is_hangul_name(name):
|
|
131
|
+
problems.append((tok.start, len(text),
|
|
132
|
+
"'$' 탈출 이름 '%s' 는 한글이어야 합니다." % name))
|
|
133
|
+
out.append(name)
|
|
134
|
+
elif kind == "sigil":
|
|
135
|
+
out.append(vocab.UMSN2SYMBOL[text])
|
|
136
|
+
elif kind in ("string", "fstart") and tok.prefix:
|
|
137
|
+
py_prefix = umsn_prefix_to_py(tok.prefix)
|
|
138
|
+
if py_prefix is None:
|
|
139
|
+
problems.append((tok.start, len(tok.prefix),
|
|
140
|
+
"영문 문자열 접두사 '%s' 는 쓸 수 없슨!" % tok.prefix))
|
|
141
|
+
py_prefix = tok.prefix
|
|
142
|
+
out.append(py_prefix + text[len(tok.prefix):])
|
|
143
|
+
elif kind == "error":
|
|
144
|
+
problems.append((tok.start, len(text), "알 수 없는 엄슨 기호어 '%s' 입니다." % text))
|
|
145
|
+
out.append(text)
|
|
146
|
+
else:
|
|
147
|
+
out.append(text)
|
|
148
|
+
return "".join(out), problems
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def umsn_to_py(src, filename=None):
|
|
152
|
+
"""엄슨 소스를 파이썬 소스로 바꾼다. 문제가 있으면 (모든 문제를 담은) UmsnSyntaxError."""
|
|
153
|
+
lexer = Lexer(src, MODE_UMSN)
|
|
154
|
+
try:
|
|
155
|
+
tokens = lexer.run()
|
|
156
|
+
except UmsnSyntaxError as exc:
|
|
157
|
+
raise UmsnSyntaxError(check_umsn(src) or exc.problems, filename=filename, source=src,
|
|
158
|
+
incomplete=exc.incomplete) from None
|
|
159
|
+
py, problems = _umsn_tokens_to_py(tokens)
|
|
160
|
+
if problems:
|
|
161
|
+
raise UmsnSyntaxError(_located(lexer, problems), filename=filename, source=src)
|
|
162
|
+
return py
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class PositionMap(object):
|
|
166
|
+
"""변환된 파이썬 코드의 (줄, 칸) 을 엄슨 소스의 칸으로 되돌린다 (오류 표시용)."""
|
|
167
|
+
|
|
168
|
+
def __init__(self, umsn_src):
|
|
169
|
+
from bisect import bisect_right
|
|
170
|
+
self._bisect = bisect_right
|
|
171
|
+
self.umsn_src = umsn_src
|
|
172
|
+
lexer = Lexer(umsn_src, MODE_UMSN, tolerant=True)
|
|
173
|
+
tokens = lexer.run()
|
|
174
|
+
self._lexer = lexer
|
|
175
|
+
py_parts = []
|
|
176
|
+
self._py_starts = []
|
|
177
|
+
self._tokens = []
|
|
178
|
+
pos = 0
|
|
179
|
+
for tok in tokens:
|
|
180
|
+
text, _ = _umsn_tokens_to_py([tok])
|
|
181
|
+
self._py_starts.append(pos)
|
|
182
|
+
self._tokens.append((tok.start, tok.end, len(text)))
|
|
183
|
+
py_parts.append(text)
|
|
184
|
+
pos += len(text)
|
|
185
|
+
self.py_src = "".join(py_parts)
|
|
186
|
+
self._py_line_starts = [0] + [i + 1 for i, ch in enumerate(self.py_src) if ch == "\n"]
|
|
187
|
+
|
|
188
|
+
def py_line(self, lineno):
|
|
189
|
+
lines = self.py_src.splitlines()
|
|
190
|
+
return lines[lineno - 1] if 1 <= lineno <= len(lines) else ""
|
|
191
|
+
|
|
192
|
+
def umsn_col(self, lineno, py_col, end=False):
|
|
193
|
+
"""파이썬 (줄, 칸[문자 단위]) → 엄슨 칸. 모르면 None."""
|
|
194
|
+
if not (1 <= lineno <= len(self._py_line_starts)) or py_col is None:
|
|
195
|
+
return None
|
|
196
|
+
off = self._py_line_starts[lineno - 1] + py_col
|
|
197
|
+
idx = self._bisect(self._py_starts, off) - 1
|
|
198
|
+
if idx < 0:
|
|
199
|
+
return None
|
|
200
|
+
if end and idx > 0 and off == self._py_starts[idx]:
|
|
201
|
+
idx -= 1 # 끝 위치는 앞 토큰의 끝으로
|
|
202
|
+
um_start, um_end, py_len = self._tokens[idx]
|
|
203
|
+
delta = off - self._py_starts[idx]
|
|
204
|
+
if py_len == um_end - um_start:
|
|
205
|
+
um_off = um_start + delta
|
|
206
|
+
elif delta >= py_len:
|
|
207
|
+
um_off = um_end
|
|
208
|
+
else:
|
|
209
|
+
um_off = um_start
|
|
210
|
+
l, c = self._lexer.line_col(um_off)
|
|
211
|
+
return c if l == lineno else None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _located(lexer, problems):
|
|
215
|
+
result = []
|
|
216
|
+
for start, length, msg in problems:
|
|
217
|
+
lineno, col = lexer.line_col(start)
|
|
218
|
+
result.append((lineno, col, length, msg))
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def check_umsn(src):
|
|
223
|
+
"""엄슨 소스의 모든 문제를 ``[(줄, 칸, 길이, 메시지), ...]`` 로 돌려준다 (없으면 빈 목록)."""
|
|
224
|
+
lexer = Lexer(src, MODE_UMSN)
|
|
225
|
+
try:
|
|
226
|
+
tokens = lexer.run()
|
|
227
|
+
except UmsnSyntaxError as exc:
|
|
228
|
+
# 렉서 단계 오류가 있으면 관대 모드로 나머지 문제도 모은다.
|
|
229
|
+
tol = Lexer(src, MODE_UMSN, tolerant=True)
|
|
230
|
+
_, problems = _umsn_tokens_to_py(tol.run())
|
|
231
|
+
merged = {}
|
|
232
|
+
for prob in exc.problems + _located(tol, problems):
|
|
233
|
+
merged.setdefault((prob[0], prob[1]), prob)
|
|
234
|
+
return sorted(merged.values(), key=lambda p: (p[0] or 0, p[1] or 0))
|
|
235
|
+
_, problems = _umsn_tokens_to_py(tokens)
|
|
236
|
+
return _located(lexer, problems)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# 파이썬 → 엄슨
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
def _py_pieces(tokens, keep_symbols, lexer):
|
|
243
|
+
"""(kind, 엄슨텍스트) 조각 목록."""
|
|
244
|
+
pieces = []
|
|
245
|
+
i = 0
|
|
246
|
+
n = len(tokens)
|
|
247
|
+
while i < n:
|
|
248
|
+
tok = tokens[i]
|
|
249
|
+
kind = tok.kind
|
|
250
|
+
text = tok.text
|
|
251
|
+
if kind == "name":
|
|
252
|
+
um = _py_name_to_umsn(text)
|
|
253
|
+
if um is None:
|
|
254
|
+
lineno, col = lexer.line_col(tok.start)
|
|
255
|
+
raise UmsnSyntaxError(
|
|
256
|
+
[(lineno, col, len(text),
|
|
257
|
+
"파이썬 이름 '%s' 는 엄슨으로 바꿀 수 없슨! (한글 또는 영어 이름만 가능)" % text)],
|
|
258
|
+
source=lexer.src)
|
|
259
|
+
pieces.append(("name", um))
|
|
260
|
+
elif kind in ("string", "fstart") and tok.prefix:
|
|
261
|
+
um = py_prefix_to_umsn(tok.prefix)
|
|
262
|
+
if um is None:
|
|
263
|
+
lineno, col = lexer.line_col(tok.start)
|
|
264
|
+
raise UmsnSyntaxError([(lineno, col, len(tok.prefix),
|
|
265
|
+
"알 수 없는 문자열 접두사 '%s'" % tok.prefix)],
|
|
266
|
+
source=lexer.src)
|
|
267
|
+
pieces.append((kind, um + text[len(tok.prefix):]))
|
|
268
|
+
elif kind == "op" and not keep_symbols and tok.fdepth == 0:
|
|
269
|
+
nxt = tokens[i + 1] if i + 1 < n else None
|
|
270
|
+
pair = text + nxt.text if nxt is not None and nxt.kind == "op" else None
|
|
271
|
+
if pair in ("()", "[]", "{}"):
|
|
272
|
+
pieces.append(("sigil", vocab.SYMBOL2UMSN[pair]))
|
|
273
|
+
i += 2
|
|
274
|
+
continue
|
|
275
|
+
um = vocab.SYMBOL2UMSN.get(text)
|
|
276
|
+
pieces.append(("sigil", um) if um else ("op", text))
|
|
277
|
+
else:
|
|
278
|
+
pieces.append((kind, text))
|
|
279
|
+
i += 1
|
|
280
|
+
return pieces
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _lexes_as(a, b):
|
|
284
|
+
"""엄슨 렉서가 a+b 를 정확히 [a, b] 로 나누는가."""
|
|
285
|
+
try:
|
|
286
|
+
toks = Lexer(a + b, MODE_UMSN).run()
|
|
287
|
+
except UmsnSyntaxError:
|
|
288
|
+
return False
|
|
289
|
+
return len(toks) == 2 and toks[0].text == a and toks[1].text == b
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _join(pieces, force_space=False):
|
|
293
|
+
out = []
|
|
294
|
+
prev = None
|
|
295
|
+
for kind, text in pieces:
|
|
296
|
+
if prev is not None and kind in _CODE_KINDS and prev[0] in _CODE_KINDS:
|
|
297
|
+
if force_space or not _lexes_as(prev[1], text):
|
|
298
|
+
out.append(" ")
|
|
299
|
+
out.append(text)
|
|
300
|
+
prev = (kind, text)
|
|
301
|
+
return "".join(out)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _significant(tokens):
|
|
305
|
+
return [t.text for t in tokens if t.kind != "ws"]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def py_to_umsn(src, filename=None, keep_symbols=False):
|
|
309
|
+
"""파이썬 소스를 엄슨 소스로 바꾼다.
|
|
310
|
+
|
|
311
|
+
``keep_symbols=True`` 이면 괄호·연산자 같은 기호는 ASCII 그대로 둔다.
|
|
312
|
+
사전에 없는 영어 이름은 ``외_…`` 로 음역되므로 결과는 항상 한글 전용 규칙을 지킨다.
|
|
313
|
+
"""
|
|
314
|
+
lexer = Lexer(src, MODE_PY)
|
|
315
|
+
try:
|
|
316
|
+
tokens = lexer.run()
|
|
317
|
+
pieces = _py_pieces(tokens, keep_symbols, lexer)
|
|
318
|
+
except UmsnSyntaxError as exc:
|
|
319
|
+
exc.filename = filename
|
|
320
|
+
exc.args = (exc._format(),)
|
|
321
|
+
raise
|
|
322
|
+
expected = _significant(tokens)
|
|
323
|
+
for force in (False, True):
|
|
324
|
+
out = _join(pieces, force_space=force)
|
|
325
|
+
try:
|
|
326
|
+
back = umsn_to_py(out)
|
|
327
|
+
if _significant(tokenize(back, MODE_PY)) == expected:
|
|
328
|
+
return out
|
|
329
|
+
except UmsnSyntaxError:
|
|
330
|
+
pass
|
|
331
|
+
raise UmsnSyntaxError("파이썬 → 엄슨 변환 결과를 되돌려 확인하지 못했슨 (변환기 버그일 수 있습니다).",
|
|
332
|
+
filename=filename)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
# IDE 용: 구문 강조 구간
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
def highlight_spans(src):
|
|
339
|
+
"""구문 강조용 ``[(분류, 시작줄, 시작칸, 끝줄, 끝칸), ...]``.
|
|
340
|
+
|
|
341
|
+
분류: keyword builtin special method library bang translit dollar
|
|
342
|
+
symbol op number string fbrace comment error
|
|
343
|
+
줄은 1부터, 칸은 0부터 센다 (Tkinter Text 인덱스와 같음).
|
|
344
|
+
"""
|
|
345
|
+
lexer = Lexer(src, MODE_UMSN, tolerant=True)
|
|
346
|
+
try:
|
|
347
|
+
tokens = lexer.run()
|
|
348
|
+
except UmsnSyntaxError:
|
|
349
|
+
return []
|
|
350
|
+
spans = []
|
|
351
|
+
for tok in tokens:
|
|
352
|
+
kind = tok.kind
|
|
353
|
+
cat = None
|
|
354
|
+
if kind == "ws":
|
|
355
|
+
continue
|
|
356
|
+
if kind == "comment":
|
|
357
|
+
cat = "comment"
|
|
358
|
+
elif kind in ("string", "fstart", "fmiddle", "fend"):
|
|
359
|
+
cat = "string"
|
|
360
|
+
elif kind in ("fopen", "fclose"):
|
|
361
|
+
cat = "fbrace"
|
|
362
|
+
elif kind == "number":
|
|
363
|
+
cat = "number"
|
|
364
|
+
elif kind == "sigil":
|
|
365
|
+
cat = "symbol"
|
|
366
|
+
elif kind == "word":
|
|
367
|
+
cat = "bang"
|
|
368
|
+
elif kind == "dollar":
|
|
369
|
+
cat = "dollar"
|
|
370
|
+
elif kind == "op":
|
|
371
|
+
cat = "op"
|
|
372
|
+
elif kind == "error":
|
|
373
|
+
cat = "error"
|
|
374
|
+
elif kind == "name":
|
|
375
|
+
cat = vocab.category_of(tok.text)
|
|
376
|
+
if cat is None:
|
|
377
|
+
if untransliterate(tok.text) is not None:
|
|
378
|
+
cat = "translit"
|
|
379
|
+
elif not (is_hangul_name(tok.text) or all(c == "_" for c in tok.text)):
|
|
380
|
+
cat = "error"
|
|
381
|
+
if cat is None:
|
|
382
|
+
continue
|
|
383
|
+
l1, c1 = lexer.line_col(tok.start)
|
|
384
|
+
l2, c2 = lexer.line_col(tok.end)
|
|
385
|
+
spans.append((cat, l1, c1, l2, c2))
|
|
386
|
+
if kind in ("string", "fstart") and tok.prefix and umsn_prefix_to_py(tok.prefix) is None:
|
|
387
|
+
spans.append(("error", l1, c1, l1, c1 + len(tok.prefix)))
|
|
388
|
+
return spans
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def tokenize_umsn(src, tolerant=True):
|
|
392
|
+
"""엄슨 소스를 토큰 목록으로 (IDE 괄호 짝 맞추기 등에 사용)."""
|
|
393
|
+
return tokenize(src, MODE_UMSN, tolerant=tolerant)
|