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/importer.py ADDED
@@ -0,0 +1,70 @@
1
+ """``.umsn`` 모듈 불러오기 (import hook).
2
+
3
+ ``엄슨가져와 내모듈`` 은 ``sys.path`` 에서 ``내모듈.umsn`` (또는 ``내모듈/__init__.umsn``) 을 찾는다.
4
+ """
5
+
6
+ import importlib.abc
7
+ import importlib.util
8
+ import os
9
+ import sys
10
+
11
+ from .sourceio import read_source
12
+ from .translator import umsn_to_py
13
+
14
+ SUFFIX = ".umsn"
15
+
16
+
17
+ class UmsnLoader(importlib.abc.Loader):
18
+ def __init__(self, path):
19
+ self.path = path
20
+
21
+ def create_module(self, spec):
22
+ return None
23
+
24
+ def exec_module(self, module):
25
+ source = read_source(self.path)
26
+ py = umsn_to_py(source, filename=self.path)
27
+ try:
28
+ compile(py, "<엄슨 %s>" % self.path, "exec")
29
+ except SyntaxError as exc:
30
+ raise syntax_error_to_umsn(exc, source, self.path) from None
31
+ code = compile(py, self.path, "exec")
32
+ module.__file__ = self.path
33
+ exec(code, module.__dict__)
34
+
35
+
36
+ class UmsnFinder(importlib.abc.MetaPathFinder):
37
+ def find_spec(self, fullname, path, target=None):
38
+ name = fullname.rpartition(".")[2]
39
+ search = sys.path if path is None else path
40
+ for entry in list(search):
41
+ if not isinstance(entry, str):
42
+ continue
43
+ base = entry or os.getcwd()
44
+ pkg_init = os.path.join(base, name, "__init__" + SUFFIX)
45
+ if os.path.isfile(pkg_init):
46
+ return importlib.util.spec_from_file_location(
47
+ fullname, pkg_init, loader=UmsnLoader(pkg_init),
48
+ submodule_search_locations=[os.path.join(base, name)])
49
+ candidate = os.path.join(base, name + SUFFIX)
50
+ if os.path.isfile(candidate):
51
+ return importlib.util.spec_from_file_location(
52
+ fullname, candidate, loader=UmsnLoader(candidate))
53
+ return None
54
+
55
+
56
+ def syntax_error_to_umsn(exc, umsn_source, filename):
57
+ """파이썬 SyntaxError 를 엄슨 줄·칸 기준의 UmsnSyntaxError 로."""
58
+ from .errors import UmsnSyntaxError
59
+ from .translator import PositionMap
60
+ col = None
61
+ if exc.lineno and exc.offset:
62
+ col = PositionMap(umsn_source).umsn_col(exc.lineno, exc.offset - 1)
63
+ return UmsnSyntaxError([(exc.lineno, col, 1, "파이썬 문법 오류: %s" % exc.msg)],
64
+ filename=filename, source=umsn_source)
65
+
66
+
67
+ def install():
68
+ """import hook 을 한 번만 설치한다."""
69
+ if not any(isinstance(f, UmsnFinder) for f in sys.meta_path):
70
+ sys.meta_path.append(UmsnFinder())
pyumsn/launcher.py ADDED
@@ -0,0 +1,223 @@
1
+ """하위 프로세스 진입점.
2
+
3
+ python -X utf8 -m pyumsn.launcher <임시.py> <원본.umsn> [인자...]
4
+
5
+ 임시 파이썬 파일을 원본 ``.umsn`` 파일 이름으로 컴파일해서 실행한다.
6
+ 변환 전후 줄 번호가 같으므로 오류 트레이스백이 엄슨 소스 줄을 그대로 보여준다.
7
+ """
8
+
9
+ import builtins
10
+ import os
11
+ import sys
12
+ import traceback
13
+ import types
14
+
15
+ _SYNTAX_HINTS = [
16
+ ("expected ':'", "'..한' (:) 이 빠졌슨"),
17
+ ("invalid syntax", "문법이 잘못됐슨"),
18
+ ("unexpected indent", "들여쓰기가 이상하슨"),
19
+ ("expected an indented block", "들여쓴 블록이 필요하슨"),
20
+ ("unindent does not match", "내어쓰기 위치가 맞지 않슨"),
21
+ ("was never closed", "괄호가 닫히지 않았슨"),
22
+ ("unmatched", "짝이 없는 괄호가 있슨"),
23
+ ("unterminated string", "문자열이 닫히지 않았슨"),
24
+ ("cannot assign", "여기에는 값을 넣을 수 없슨"),
25
+ ("Perhaps you forgot a comma", "'..고' (,) 를 빠뜨린 것 같슨"),
26
+ ]
27
+
28
+
29
+ def _setup_stdio():
30
+ for stream in (sys.stdin, sys.stdout, sys.stderr):
31
+ try:
32
+ stream.reconfigure(encoding="utf-8")
33
+ except (AttributeError, ValueError, OSError):
34
+ pass
35
+
36
+
37
+ def _korean_name(exc_type):
38
+ from . import vocab
39
+ from .translator import transliterate
40
+ name = exc_type.__name__
41
+ if name in vocab.PY2UMSN:
42
+ return vocab.PY2UMSN[name]
43
+ try:
44
+ return transliterate(name)
45
+ except ValueError:
46
+ return name
47
+
48
+
49
+ _MAPS = {}
50
+
51
+
52
+ def _position_map(filename):
53
+ """엄슨 파일의 PositionMap (없거나 엄슨 파일이 아니면 None)."""
54
+ if not filename.lower().endswith(".umsn"):
55
+ return None
56
+ if filename not in _MAPS:
57
+ try:
58
+ from .sourceio import read_source
59
+ from .translator import PositionMap
60
+ _MAPS[filename] = PositionMap(read_source(filename))
61
+ except Exception:
62
+ _MAPS[filename] = None
63
+ return _MAPS[filename]
64
+
65
+
66
+ def _width(text):
67
+ import unicodedata
68
+ return sum(2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1 for ch in text)
69
+
70
+
71
+ def _caret_line(line, start, end):
72
+ stripped = line.lstrip()
73
+ indent = len(line) - len(stripped)
74
+ if start is None or end is None or end <= start or start < indent:
75
+ return None
76
+ if start == indent and end >= len(line.rstrip()):
77
+ return None
78
+ return " " + " " * _width(line[indent:start]) + "^" * max(1, _width(line[start:end]))
79
+
80
+
81
+ def _byte_to_char(line, col):
82
+ if col is None:
83
+ return None
84
+ return len(line.encode("utf-8")[:col].decode("utf-8", "replace"))
85
+
86
+
87
+ def _format_frame(fs):
88
+ import linecache
89
+ out = [' File "%s", line %s, in %s\n' % (fs.filename, fs.lineno, fs.name)]
90
+ line = linecache.getline(fs.filename, fs.lineno).rstrip("\r\n")
91
+ if not line.strip():
92
+ return out
93
+ out.append(" %s\n" % line.strip())
94
+ colno = getattr(fs, "colno", None)
95
+ end_colno = getattr(fs, "end_colno", None)
96
+ if colno is None or end_colno is None or getattr(fs, "end_lineno", fs.lineno) != fs.lineno:
97
+ return out
98
+ pmap = _position_map(fs.filename)
99
+ if pmap is not None:
100
+ py_line = pmap.py_line(fs.lineno)
101
+ start = pmap.umsn_col(fs.lineno, _byte_to_char(py_line, colno))
102
+ end = pmap.umsn_col(fs.lineno, _byte_to_char(py_line, end_colno), end=True)
103
+ else:
104
+ start, end = _byte_to_char(line, colno), _byte_to_char(line, end_colno)
105
+ caret = _caret_line(line, start, end)
106
+ if caret:
107
+ out.append(caret + "\n")
108
+ return out
109
+
110
+
111
+ def _report_syntax(exc, umsn_path):
112
+ hint = None
113
+ for key, text in _SYNTAX_HINTS:
114
+ if key in (exc.msg or ""):
115
+ hint = text
116
+ break
117
+ col = None
118
+ line = None
119
+ pmap = _position_map(umsn_path)
120
+ if pmap is not None and exc.lineno:
121
+ lines = pmap.umsn_src.splitlines()
122
+ if 1 <= exc.lineno <= len(lines):
123
+ line = lines[exc.lineno - 1]
124
+ if exc.offset:
125
+ col = pmap.umsn_col(exc.lineno, exc.offset - 1)
126
+ where = "%s %s줄" % (umsn_path, exc.lineno)
127
+ if col is not None:
128
+ where += " %d칸" % (col + 1)
129
+ sys.stderr.write("엄슨 오류! 문법 오류 (%s): %s\n" % (where, hint or exc.msg))
130
+ if hint:
131
+ sys.stderr.write(" (파이썬: %s)\n" % exc.msg)
132
+ if line is not None:
133
+ sys.stderr.write(" %s\n" % line)
134
+ if col is not None:
135
+ sys.stderr.write(" %s^\n" % (" " * _width(line[:col])))
136
+ sys.stderr.flush()
137
+
138
+
139
+ def _report(exc, _seen=None, outer=True):
140
+ _seen = _seen if _seen is not None else set()
141
+ _seen.add(id(exc))
142
+ if exc.__cause__ is not None and id(exc.__cause__) not in _seen:
143
+ _report(exc.__cause__, _seen, False)
144
+ sys.stderr.write("\n위 예외가 아래 예외의 직접적인 원인이었슨:\n\n")
145
+ elif (exc.__context__ is not None and not exc.__suppress_context__
146
+ and id(exc.__context__) not in _seen):
147
+ _report(exc.__context__, _seen, False)
148
+ sys.stderr.write("\n위 예외를 처리하는 동안 또 다른 예외가 일어났슨:\n\n")
149
+ tb = exc.__traceback__
150
+ here = os.path.abspath(__file__)
151
+ while tb is not None and os.path.abspath(tb.tb_frame.f_code.co_filename) == here:
152
+ tb = tb.tb_next
153
+ sys.stderr.write("엄슨 추적 기록 (가장 최근 호출이 마지막):\n")
154
+ for fs in traceback.extract_tb(tb):
155
+ for piece in _format_frame(fs):
156
+ sys.stderr.write(piece)
157
+ for line in traceback.format_exception_only(type(exc), exc):
158
+ sys.stderr.write(line)
159
+ if outer:
160
+ sys.stderr.write("엄슨 오류! %s (%s): %s\n" % (_korean_name(type(exc)), type(exc).__name__, exc))
161
+ sys.stderr.flush()
162
+
163
+
164
+ def main(argv=None):
165
+ argv = sys.argv[1:] if argv is None else list(argv)
166
+ if len(argv) < 2:
167
+ sys.stderr.write("사용법: python -m pyumsn.launcher <임시.py> <원본.umsn> [인자...]\n")
168
+ return 2
169
+ _setup_stdio()
170
+ from .importer import install
171
+ from .sourceio import read_source
172
+
173
+ py_path, umsn_path = argv[0], os.path.abspath(argv[1])
174
+ source = read_source(py_path)
175
+ if not os.environ.pop("PYUMSN_KEEP_TEMP", None):
176
+ try:
177
+ os.remove(py_path) # 이미 읽었으니 임시 파일은 바로 지운다
178
+ except OSError:
179
+ pass
180
+ sys.argv = [umsn_path] + argv[2:]
181
+ if sys.path and sys.path[0] in ("", os.getcwd()):
182
+ sys.path[0] = os.path.dirname(umsn_path)
183
+ else:
184
+ sys.path.insert(0, os.path.dirname(umsn_path))
185
+ install()
186
+
187
+ # 문법 검사는 임시 .py 이름으로 한다: 실제 .umsn 파일 이름을 주면 파이썬이
188
+ # 오류 칸 번호를 (파이썬 코드가 아닌) 엄슨 줄 기준으로 다시 계산해 버린다.
189
+ try:
190
+ compile(source, py_path, "exec")
191
+ except SyntaxError as exc:
192
+ _report_syntax(exc, umsn_path)
193
+ return 1
194
+ code = compile(source, umsn_path, "exec")
195
+
196
+ keep_alive = sys.modules.get("__main__") # noqa: F841 (실행 중인 모듈 유지)
197
+ module = types.ModuleType("__main__")
198
+ module.__file__ = umsn_path
199
+ module.__builtins__ = builtins
200
+ sys.modules["__main__"] = module
201
+ try:
202
+ exec(code, module.__dict__)
203
+ except SystemExit:
204
+ raise
205
+ except KeyboardInterrupt:
206
+ sys.stderr.write("\n엄슨 중단! (Ctrl+C)\n")
207
+ return 130
208
+ except BaseException as exc: # noqa: B902
209
+ from .errors import UmsnError, UmsnSyntaxError
210
+ if isinstance(exc, UmsnSyntaxError):
211
+ sys.stderr.write(exc.report() + "\n")
212
+ elif isinstance(exc, UmsnError):
213
+ sys.stderr.write("엄슨 오류! %s\n" % exc)
214
+ else:
215
+ _report(exc)
216
+ return 1
217
+ finally:
218
+ sys.stdout.flush()
219
+ return 0
220
+
221
+
222
+ if __name__ == "__main__":
223
+ sys.exit(main())