hython-lang 2.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.
- hython/__init__.py +15 -0
- hython/__main__.py +3 -0
- hython/bytecode.py +368 -0
- hython/cli.py +449 -0
- hython/compiler.py +870 -0
- hython/compiler_manager.py +227 -0
- hython/diagnostics.py +158 -0
- hython/environment.py +48 -0
- hython/exe_builder.py +330 -0
- hython/frontend.py +870 -0
- hython/hir.py +93 -0
- hython/importer.py +76 -0
- hython/package_manager.py +343 -0
- hython/phonetics.py +49 -0
- hython/runtime.py +110 -0
- hython/runtime_manager.py +83 -0
- hython/translator.py +244 -0
- hython/updater.py +93 -0
- hython/vm.py +2409 -0
- hython/vocabulary.py +132 -0
- hython_lang-2.0.0.dist-info/METADATA +327 -0
- hython_lang-2.0.0.dist-info/RECORD +26 -0
- hython_lang-2.0.0.dist-info/WHEEL +5 -0
- hython_lang-2.0.0.dist-info/entry_points.txt +2 -0
- hython_lang-2.0.0.dist-info/licenses/LICENSE +22 -0
- hython_lang-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Verified, side-by-side Hython compiler updates.
|
|
2
|
+
|
|
3
|
+
The installed bootstrap package is never overwritten. A verified wheel is
|
|
4
|
+
expanded below HYTHON_HOME and selected by re-executing Hython with that tree
|
|
5
|
+
at the front of PYTHONPATH.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
import urllib.request
|
|
19
|
+
import zipfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from . import __version__
|
|
23
|
+
from .environment import external_source_root, is_frozen
|
|
24
|
+
|
|
25
|
+
PYPI_URL = "https://pypi.org/pypi/hython-lang/json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CompilerUpdateError(RuntimeError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def compiler_root() -> Path:
|
|
33
|
+
root=Path(os.environ.get("HYTHON_HOME",Path.home()/".hython"))/"compilers"
|
|
34
|
+
root.mkdir(parents=True,exist_ok=True)
|
|
35
|
+
return root
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def state_path() -> Path:
|
|
39
|
+
return compiler_root()/"state.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_state() -> dict:
|
|
43
|
+
try:
|
|
44
|
+
payload=json.loads(state_path().read_text(encoding="utf-8"))
|
|
45
|
+
if payload.get("format") != 1:
|
|
46
|
+
return {"format":1,"active":None,"history":[]}
|
|
47
|
+
return payload
|
|
48
|
+
except (OSError,UnicodeError,json.JSONDecodeError):
|
|
49
|
+
return {"format":1,"active":None,"history":[]}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def active_version() -> str | None:
|
|
53
|
+
value=load_state().get("active")
|
|
54
|
+
return value if isinstance(value,str) and (compiler_root()/value).is_dir() else None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _version_key(value: str) -> tuple:
|
|
58
|
+
match=re.match(r"^(\d+)\.(\d+)\.(\d+)(?:(?:[.-]?dev)(\d+))?",value)
|
|
59
|
+
if not match:
|
|
60
|
+
return (-1,value)
|
|
61
|
+
major,minor,patch,dev=match.groups()
|
|
62
|
+
return (int(major),int(minor),int(patch),1 if dev is None else 0,int(dev or 0))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def query_latest(*, url: str = PYPI_URL, timeout: int = 20) -> dict:
|
|
66
|
+
if not url.lower().startswith("https://"):
|
|
67
|
+
raise CompilerUpdateError("컴파일러 인덱스는 HTTPS 주소여야 합니다.")
|
|
68
|
+
request=urllib.request.Request(url,headers={"User-Agent":f"hython/{__version__}"})
|
|
69
|
+
try:
|
|
70
|
+
with urllib.request.urlopen(request,timeout=timeout) as response:
|
|
71
|
+
payload=json.load(response)
|
|
72
|
+
except (OSError,ValueError) as exc:
|
|
73
|
+
raise CompilerUpdateError(f"컴파일러 인덱스 조회 실패: {exc}") from exc
|
|
74
|
+
version=payload.get("info",{}).get("version")
|
|
75
|
+
files=payload.get("releases",{}).get(version,[]) if isinstance(version,str) else []
|
|
76
|
+
candidates=[item for item in files if isinstance(item,dict) and item.get("packagetype")=="bdist_wheel"]
|
|
77
|
+
universal=[item for item in candidates if str(item.get("filename","")).endswith("py3-none-any.whl")]
|
|
78
|
+
candidates=universal or candidates
|
|
79
|
+
if not version or not candidates:
|
|
80
|
+
raise CompilerUpdateError("설치 가능한 공식 Hython compiler wheel이 없습니다.")
|
|
81
|
+
item=candidates[0]
|
|
82
|
+
digest=item.get("digests",{}).get("sha256")
|
|
83
|
+
artifact_url=item.get("url")
|
|
84
|
+
if not isinstance(digest,str) or len(digest)!=64 or not isinstance(artifact_url,str) or not artifact_url.startswith("https://"):
|
|
85
|
+
raise CompilerUpdateError("PyPI wheel의 SHA-256 메타데이터가 올바르지 않습니다.")
|
|
86
|
+
return {"version":version,"url":artifact_url,"sha256":digest,"filename":item.get("filename")}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check() -> dict:
|
|
90
|
+
latest=query_latest()
|
|
91
|
+
current=active_version() or __version__
|
|
92
|
+
latest["current"]=current
|
|
93
|
+
latest["update_available"]=_version_key(latest["version"])>_version_key(current)
|
|
94
|
+
return latest
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _download(url: str, destination: Path, *, timeout: int = 60) -> None:
|
|
98
|
+
request=urllib.request.Request(url,headers={"User-Agent":f"hython/{__version__}"})
|
|
99
|
+
try:
|
|
100
|
+
with urllib.request.urlopen(request,timeout=timeout) as response, destination.open("wb") as output:
|
|
101
|
+
shutil.copyfileobj(response,output)
|
|
102
|
+
except OSError as exc:
|
|
103
|
+
raise CompilerUpdateError(f"compiler wheel 다운로드 실패: {exc}") from exc
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _safe_extract(wheel: Path, destination: Path) -> None:
|
|
107
|
+
try:
|
|
108
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
109
|
+
root=destination.resolve()
|
|
110
|
+
for member in archive.infolist():
|
|
111
|
+
target=(destination/member.filename).resolve()
|
|
112
|
+
if target!=root and root not in target.parents:
|
|
113
|
+
raise CompilerUpdateError("wheel에 안전하지 않은 경로가 포함되어 있습니다.")
|
|
114
|
+
if member.is_dir():
|
|
115
|
+
target.mkdir(parents=True,exist_ok=True)
|
|
116
|
+
else:
|
|
117
|
+
target.parent.mkdir(parents=True,exist_ok=True)
|
|
118
|
+
with archive.open(member) as source,target.open("wb") as output:
|
|
119
|
+
shutil.copyfileobj(source,output)
|
|
120
|
+
except (OSError,zipfile.BadZipFile) as exc:
|
|
121
|
+
raise CompilerUpdateError(f"compiler wheel 압축 해제 실패: {exc}") from exc
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _smoke_test(path: Path, version: str) -> None:
|
|
125
|
+
env=os.environ.copy()
|
|
126
|
+
env["PYTHONPATH"]=str(path)
|
|
127
|
+
env["HYTHON_COMPILER_ACTIVE"]=version
|
|
128
|
+
script=(
|
|
129
|
+
"import pathlib,hython; from hython.compiler import compile_source; "
|
|
130
|
+
"from hython.bytecode import dumps,loads; "
|
|
131
|
+
"p=pathlib.Path(hython.__file__).resolve(); root=pathlib.Path(r'"+str(path).replace("'","''")+"').resolve(); "
|
|
132
|
+
"assert root in p.parents; dumps(loads(dumps(compile_source('값 = 1 + 2'))))"
|
|
133
|
+
)
|
|
134
|
+
result=subprocess.run([sys.executable,"-c",script],env=env,capture_output=True,text=True,timeout=30)
|
|
135
|
+
if result.returncode:
|
|
136
|
+
detail=(result.stderr or result.stdout).strip().splitlines()[-1:] or ["알 수 없는 오류"]
|
|
137
|
+
raise CompilerUpdateError(f"새 컴파일러 smoke test 실패: {detail[0]}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def install_wheel(wheel: Path, *, version: str, sha256: str) -> Path:
|
|
141
|
+
actual=hashlib.sha256(wheel.read_bytes()).hexdigest()
|
|
142
|
+
if not __import__("hmac").compare_digest(actual.lower(),sha256.lower()):
|
|
143
|
+
raise CompilerUpdateError("compiler wheel SHA-256 검증 실패")
|
|
144
|
+
root=compiler_root(); final=root/version
|
|
145
|
+
staging=Path(tempfile.mkdtemp(prefix=f".{version}-",dir=root))
|
|
146
|
+
try:
|
|
147
|
+
_safe_extract(wheel,staging)
|
|
148
|
+
_smoke_test(staging,version)
|
|
149
|
+
if final.exists():
|
|
150
|
+
shutil.rmtree(final)
|
|
151
|
+
staging.replace(final)
|
|
152
|
+
except BaseException:
|
|
153
|
+
shutil.rmtree(staging,ignore_errors=True)
|
|
154
|
+
raise
|
|
155
|
+
_activate(version)
|
|
156
|
+
return final
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def update(*, force: bool = False) -> dict:
|
|
160
|
+
release=check()
|
|
161
|
+
if not release["update_available"] and not force:
|
|
162
|
+
return release|{"installed":False}
|
|
163
|
+
with tempfile.TemporaryDirectory(prefix="hython-compiler-download-") as directory:
|
|
164
|
+
wheel=Path(directory)/(release.get("filename") or "compiler.whl")
|
|
165
|
+
_download(release["url"],wheel)
|
|
166
|
+
install_wheel(wheel,version=release["version"],sha256=release["sha256"])
|
|
167
|
+
return release|{"installed":True}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _activate(version: str | None) -> None:
|
|
171
|
+
state=load_state(); previous=state.get("active"); history=list(state.get("history",[]))
|
|
172
|
+
if previous and previous!=version:
|
|
173
|
+
history=[item for item in history if item!=previous]+[previous]
|
|
174
|
+
state={"format":1,"active":version,"history":history[-10:]}
|
|
175
|
+
_atomic_json(state_path(),state)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def rollback() -> str | None:
|
|
179
|
+
state=load_state(); history=list(state.get("history",[]))
|
|
180
|
+
while history:
|
|
181
|
+
version=history.pop()
|
|
182
|
+
if (compiler_root()/version).is_dir():
|
|
183
|
+
_atomic_json(state_path(),{"format":1,"active":version,"history":history})
|
|
184
|
+
return version
|
|
185
|
+
_activate(None)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def remove(version: str | None = None) -> str:
|
|
190
|
+
state=load_state(); target=version or state.get("active")
|
|
191
|
+
if not isinstance(target,str) or not target:
|
|
192
|
+
raise CompilerUpdateError("삭제할 외부 컴파일러가 없습니다.")
|
|
193
|
+
if state.get("active")==target:
|
|
194
|
+
rollback()
|
|
195
|
+
path=compiler_root()/target
|
|
196
|
+
shutil.rmtree(path,ignore_errors=True)
|
|
197
|
+
state=load_state()
|
|
198
|
+
state["history"]=[item for item in state.get("history",[]) if item!=target]
|
|
199
|
+
_atomic_json(state_path(),state)
|
|
200
|
+
return target
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def reexec_with_active(argv: list[str]) -> None:
|
|
204
|
+
version=active_version()
|
|
205
|
+
if not version or os.environ.get("HYTHON_COMPILER_ACTIVE")==version:
|
|
206
|
+
return
|
|
207
|
+
env=os.environ.copy(); env["HYTHON_COMPILER_ACTIVE"]=version
|
|
208
|
+
env["PYTHONPATH"]=str(compiler_root()/version)+os.pathsep+str(external_source_root())+os.pathsep+env.get("PYTHONPATH","")
|
|
209
|
+
if is_frozen():
|
|
210
|
+
from .runtime_manager import find_manager
|
|
211
|
+
command=[find_manager(),"exec","-V:default","-m","hython",*argv]
|
|
212
|
+
else:
|
|
213
|
+
command=[sys.executable,"-m","hython",*argv]
|
|
214
|
+
result=subprocess.run(command,env=env)
|
|
215
|
+
raise SystemExit(result.returncode)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _atomic_json(output: Path, payload: dict) -> None:
|
|
219
|
+
handle,temporary=tempfile.mkstemp(prefix=output.name+".",suffix=".tmp",dir=output.parent)
|
|
220
|
+
try:
|
|
221
|
+
with os.fdopen(handle,"w",encoding="utf-8",newline="\n") as stream:
|
|
222
|
+
json.dump(payload,stream,ensure_ascii=False,indent=2); stream.write("\n")
|
|
223
|
+
stream.flush(); os.fsync(stream.fileno())
|
|
224
|
+
Path(temporary).replace(output)
|
|
225
|
+
except BaseException:
|
|
226
|
+
Path(temporary).unlink(missing_ok=True)
|
|
227
|
+
raise
|
hython/diagnostics.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Korean user-facing exception and traceback formatting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import traceback
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
TYPE_NAMES={
|
|
12
|
+
SyntaxError:"문법 오류",IndentationError:"들여쓰기 오류",TabError:"탭/공백 오류",
|
|
13
|
+
NameError:"이름 오류",UnboundLocalError:"지역 이름 오류",TypeError:"형식 오류",
|
|
14
|
+
ValueError:"값 오류",AttributeError:"속성 오류",ImportError:"가져오기 오류",
|
|
15
|
+
ModuleNotFoundError:"모듈 없음 오류",KeyError:"키 오류",IndexError:"인덱스 오류",
|
|
16
|
+
ZeroDivisionError:"0 나누기 오류",FileNotFoundError:"파일 없음 오류",
|
|
17
|
+
PermissionError:"권한 오류",IsADirectoryError:"디렉터리 오류",NotADirectoryError:"경로 오류",
|
|
18
|
+
AssertionError:"검증 실패",RuntimeError:"실행 오류",RecursionError:"재귀 한도 오류",
|
|
19
|
+
OverflowError:"범위 초과 오류",MemoryError:"메모리 부족 오류",
|
|
20
|
+
NotImplementedError:"미구현 오류",TimeoutError:"시간 초과 오류",
|
|
21
|
+
EOFError:"입력 종료 오류",OSError:"운영체제 오류",
|
|
22
|
+
BaseExceptionGroup:"예외 묶음",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def exception_name(error: BaseException) -> str:
|
|
27
|
+
for cls in type(error).__mro__:
|
|
28
|
+
if cls in TYPE_NAMES:
|
|
29
|
+
return TYPE_NAMES[cls]
|
|
30
|
+
return type(error).__name__
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _replace(message: str, pattern: str, replacement: str) -> str | None:
|
|
34
|
+
match=re.fullmatch(pattern,message)
|
|
35
|
+
return match.expand(replacement) if match else None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def translated_message(error: BaseException) -> str:
|
|
39
|
+
message=str(getattr(error,"msg",error)) if isinstance(error,SyntaxError) else str(error)
|
|
40
|
+
if isinstance(error,FileNotFoundError):
|
|
41
|
+
target=getattr(error,"filename",None)
|
|
42
|
+
return f"파일을 찾을 수 없습니다: {target}" if target else "파일을 찾을 수 없습니다."
|
|
43
|
+
if isinstance(error,PermissionError):
|
|
44
|
+
target=getattr(error,"filename",None)
|
|
45
|
+
return f"접근 권한이 없습니다: {target}" if target else "접근 권한이 없습니다."
|
|
46
|
+
if isinstance(error,subprocess.CalledProcessError):
|
|
47
|
+
return f"외부 명령이 종료 코드 {error.returncode}로 실패했습니다."
|
|
48
|
+
if isinstance(error,ModuleNotFoundError):
|
|
49
|
+
name=getattr(error,"name",None)
|
|
50
|
+
return f"모듈을 찾을 수 없습니다: {name}" if name else "모듈을 찾을 수 없습니다."
|
|
51
|
+
if isinstance(error,KeyError):
|
|
52
|
+
return f"키를 찾을 수 없습니다: {error.args[0]!r}" if error.args else "키를 찾을 수 없습니다."
|
|
53
|
+
if isinstance(error,AssertionError) and not message:
|
|
54
|
+
return "검증 조건이 참이 아닙니다."
|
|
55
|
+
patterns=[]
|
|
56
|
+
if isinstance(error,(NameError,UnboundLocalError)):
|
|
57
|
+
patterns=[
|
|
58
|
+
(r"name '([^']+)' is not defined",r"이름 '\1'이 정의되지 않았습니다."),
|
|
59
|
+
(r"cannot access local variable '([^']+)' where it is not associated with a value",r"지역 변수 '\1'에 값이 연결되기 전에 접근했습니다."),
|
|
60
|
+
]
|
|
61
|
+
elif isinstance(error,AttributeError):
|
|
62
|
+
patterns=[(r"'([^']+)' object has no attribute '([^']+)'",r"'\1' 객체에 '\2' 속성이 없습니다."),
|
|
63
|
+
(r"module '([^']+)' has no attribute '([^']+)'",r"'\1' 모듈에 '\2' 속성이 없습니다.")]
|
|
64
|
+
elif isinstance(error,ImportError):
|
|
65
|
+
patterns=[(r"cannot import name '([^']+)' from '([^']+)'.*",r"'\2'에서 이름 '\1'을 가져올 수 없습니다.")]
|
|
66
|
+
elif isinstance(error,IndexError):
|
|
67
|
+
patterns=[(r"list index out of range",r"리스트 인덱스가 범위를 벗어났습니다."),
|
|
68
|
+
(r"tuple index out of range",r"튜플 인덱스가 범위를 벗어났습니다."),
|
|
69
|
+
(r"string index out of range",r"문자열 인덱스가 범위를 벗어났습니다.")]
|
|
70
|
+
elif isinstance(error,ZeroDivisionError):
|
|
71
|
+
patterns=[(r"division by zero",r"0으로 나눌 수 없습니다."),
|
|
72
|
+
(r"integer division or modulo by zero",r"0으로 정수 나눗셈 또는 나머지 연산을 할 수 없습니다."),
|
|
73
|
+
(r"float division by zero",r"부동소수점 수를 0으로 나눌 수 없습니다."),
|
|
74
|
+
(r"complex division by zero",r"복소수를 0으로 나눌 수 없습니다.")]
|
|
75
|
+
elif isinstance(error,TypeError):
|
|
76
|
+
patterns=[
|
|
77
|
+
(r"'([^']+)' object is not callable",r"'\1' 객체는 호출할 수 없습니다."),
|
|
78
|
+
(r"'([^']+)' object is not iterable",r"'\1' 객체는 반복할 수 없습니다."),
|
|
79
|
+
(r"'([^']+)' object is not subscriptable",r"'\1' 객체는 첨자로 접근할 수 없습니다."),
|
|
80
|
+
(r"unsupported operand type\(s\) for (.+): '([^']+)' and '([^']+)'",r"연산자 \1은 '\2'와 '\3' 형식에 사용할 수 없습니다."),
|
|
81
|
+
(r"(.+) got an unexpected keyword argument '([^']+)'",r"\1에 예상하지 않은 키워드 인자 '\2'이 전달되었습니다."),
|
|
82
|
+
(r"(.+) got multiple values for argument '([^']+)'",r"\1의 인자 '\2'에 값이 여러 번 전달되었습니다."),
|
|
83
|
+
(r"(.+) missing (\d+) required positional argument(?:s)?: (.+)",r"\1에 필수 위치 인자 \2개가 없습니다: \3"),
|
|
84
|
+
]
|
|
85
|
+
elif isinstance(error,SyntaxError):
|
|
86
|
+
patterns=[(r"invalid syntax",r"잘못된 문법입니다."),(r"unexpected EOF while parsing",r"코드가 끝나기 전에 문법이 완성되지 않았습니다."),
|
|
87
|
+
(r"expected '([^']+)'",r"'\1'이 필요합니다."),(r"unmatched '([^']+)'",r"짝이 맞지 않는 '\1'가 있습니다."),
|
|
88
|
+
(r"unterminated (.+)",r"닫히지 않은 \1입니다.")]
|
|
89
|
+
elif isinstance(error,RecursionError):
|
|
90
|
+
patterns=[(r"maximum recursion depth exceeded.*",r"최대 재귀 깊이를 초과했습니다.")]
|
|
91
|
+
for pattern,replacement in patterns:
|
|
92
|
+
result=_replace(message,pattern,replacement)
|
|
93
|
+
if result is not None:
|
|
94
|
+
return result
|
|
95
|
+
return message or exception_name(error)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _user_frames(error: BaseException) -> list[traceback.FrameSummary]:
|
|
99
|
+
frames=list(traceback.extract_tb(error.__traceback__))
|
|
100
|
+
package=Path(__file__).resolve().parent
|
|
101
|
+
visible=[]
|
|
102
|
+
for frame in frames:
|
|
103
|
+
normalized=frame.filename.replace("\\","/")
|
|
104
|
+
try:
|
|
105
|
+
internal=Path(frame.filename).resolve().is_relative_to(package)
|
|
106
|
+
except (OSError,ValueError):
|
|
107
|
+
internal=False
|
|
108
|
+
internal=internal or normalized.startswith("hython/") or "/hython/" in normalized
|
|
109
|
+
internal=internal or Path(normalized).name in {"code.py","codeop.py"}
|
|
110
|
+
if not internal or frame.filename.startswith("<") or frame.filename.endswith((".hy",".hbc")):
|
|
111
|
+
visible.append(frame)
|
|
112
|
+
return visible
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _single(error: BaseException, *, indent: str = "", traceback_enabled: bool = True) -> list[str]:
|
|
116
|
+
lines=[]
|
|
117
|
+
if isinstance(error,SyntaxError) and getattr(error,"lineno",None):
|
|
118
|
+
lines.append(indent+f' 파일 "{error.filename or "<하이썬>"}", 줄 {error.lineno}')
|
|
119
|
+
source=(error.text or "").rstrip("\r\n")
|
|
120
|
+
if source:
|
|
121
|
+
lines.append(indent+" "+source)
|
|
122
|
+
if error.offset:
|
|
123
|
+
end=max(error.offset,getattr(error,"end_offset",error.offset) or error.offset)
|
|
124
|
+
lines.append(indent+" "+" "*(max(error.offset-1,0))+"^"*max(end-error.offset,1))
|
|
125
|
+
if traceback_enabled:
|
|
126
|
+
frames=_user_frames(error)
|
|
127
|
+
if frames:
|
|
128
|
+
lines.append(indent+"하이썬 추적 (가장 최근 호출이 마지막):")
|
|
129
|
+
for frame in frames:
|
|
130
|
+
lines.append(indent+f' 파일 "{frame.filename}", 줄 {frame.lineno}, 함수 {frame.name}')
|
|
131
|
+
if frame.line: lines.append(indent+" "+frame.line.strip())
|
|
132
|
+
lines.append(indent+f"{exception_name(error)}: {translated_message(error)}")
|
|
133
|
+
for note in getattr(error,"__notes__",()) or ():
|
|
134
|
+
lines.append(indent+f" 참고: {note}")
|
|
135
|
+
return lines
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def format_exception(error: BaseException, *, traceback_enabled: bool = True) -> str:
|
|
139
|
+
"""Format chaining and exception groups without exposing Hython internals."""
|
|
140
|
+
lines=[]; seen=set()
|
|
141
|
+
def render(current: BaseException,indent=""):
|
|
142
|
+
if id(current) in seen: return
|
|
143
|
+
seen.add(id(current))
|
|
144
|
+
cause=current.__cause__
|
|
145
|
+
context=current.__context__ if not current.__suppress_context__ else None
|
|
146
|
+
if cause is not None:
|
|
147
|
+
render(cause,indent); lines.append(indent+"위 오류가 다음 오류의 직접 원인입니다:")
|
|
148
|
+
elif context is not None:
|
|
149
|
+
render(context,indent); lines.append(indent+"위 오류를 처리하는 동안 다른 오류가 발생했습니다:")
|
|
150
|
+
if isinstance(current,BaseExceptionGroup):
|
|
151
|
+
lines.extend(_single(current,indent=indent,traceback_enabled=traceback_enabled))
|
|
152
|
+
for index,child in enumerate(current.exceptions,1):
|
|
153
|
+
lines.append(indent+f" 하위 오류 {index}:")
|
|
154
|
+
render(child,indent+" ")
|
|
155
|
+
else:
|
|
156
|
+
lines.extend(_single(current,indent=indent,traceback_enabled=traceback_enabled))
|
|
157
|
+
render(error)
|
|
158
|
+
return "\n".join(lines)
|
hython/environment.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Shared paths for source and frozen Hython distributions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_frozen() -> bool:
|
|
11
|
+
return bool(getattr(sys,"frozen",False) or "__compiled__" in globals() or os.environ.get("HYTHON_FROZEN"))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def hython_home() -> Path:
|
|
15
|
+
root=Path(os.environ.get("HYTHON_HOME",Path.home()/".hython"))
|
|
16
|
+
root.mkdir(parents=True,exist_ok=True)
|
|
17
|
+
return root
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def external_source_root() -> Path:
|
|
21
|
+
configured=os.environ.get("HYTHON_BUNDLED_SOURCE")
|
|
22
|
+
if configured:
|
|
23
|
+
return Path(configured)
|
|
24
|
+
if is_frozen():
|
|
25
|
+
base=Path(getattr(sys,"_MEIPASS",Path(sys.argv[0]).resolve().parent))
|
|
26
|
+
return base/"hython_source"
|
|
27
|
+
return Path(__file__).resolve().parents[1]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def package_store() -> Path:
|
|
31
|
+
path=hython_home()/"packages"/f"python-{sys.version_info.major}.{sys.version_info.minor}"
|
|
32
|
+
path.mkdir(parents=True,exist_ok=True)
|
|
33
|
+
return path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def activate_package_store() -> Path:
|
|
37
|
+
path=package_store()
|
|
38
|
+
value=str(path)
|
|
39
|
+
if value not in sys.path:
|
|
40
|
+
sys.path.insert(0,value)
|
|
41
|
+
return path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def display_executable() -> Path:
|
|
45
|
+
"""Return the user-launched executable, not a onefile extraction helper."""
|
|
46
|
+
if is_frozen() and sys.argv:
|
|
47
|
+
return Path(sys.argv[0]).resolve()
|
|
48
|
+
return Path(sys.executable).resolve()
|