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.
@@ -0,0 +1,83 @@
1
+ """Adapter for the official Windows Python install manager."""
2
+ from __future__ import annotations
3
+ import os
4
+ import json
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+ from .environment import external_source_root
9
+
10
+ class RuntimeManagerError(RuntimeError):
11
+ pass
12
+
13
+ def find_manager() -> str:
14
+ manager = shutil.which("pymanager")
15
+ if manager:
16
+ return manager
17
+ py = shutil.which("py")
18
+ if py:
19
+ probe = subprocess.run([py, "help", "install"], capture_output=True, text=True, timeout=10)
20
+ if probe.returncode == 0:
21
+ return py
22
+ raise RuntimeManagerError("공식 Python install manager가 없습니다. Microsoft Store에서 설치하거나 `winget install 9NQ7512CXL7T`를 실행하세요.")
23
+
24
+ def _command(*args: str, capture: bool = False):
25
+ return subprocess.run([find_manager(), *args], check=True, text=True, capture_output=capture)
26
+
27
+ def list_runtimes(*, online: bool = False, tag: str | None = None) -> str:
28
+ args = ["list"]
29
+ if online:
30
+ args.append("--online")
31
+ if tag:
32
+ args.extend(["--one", tag])
33
+ return _command(*args, capture=True).stdout
34
+
35
+ def install_runtime(tag: str = "default", *, update: bool = False, dry_run: bool = False) -> None:
36
+ args = ["install"]
37
+ if update:
38
+ args.append("--update")
39
+ if dry_run:
40
+ args.append("--dry-run")
41
+ _command(*args, tag)
42
+
43
+ def set_preference(root: Path, tag: str) -> Path:
44
+ if not tag or any(c.isspace() for c in tag):
45
+ raise ValueError("런타임 태그는 비어 있거나 공백을 포함할 수 없습니다.")
46
+ output = root / ".hython-runtime"
47
+ output.write_text(tag + "\n", encoding="utf-8")
48
+ return output
49
+
50
+ def get_preference(start: Path) -> str | None:
51
+ current = start.resolve()
52
+ if current.is_file():
53
+ current = current.parent
54
+ for directory in (current, *current.parents):
55
+ candidate = directory / ".hython-runtime"
56
+ if candidate.is_file():
57
+ return candidate.read_text(encoding="utf-8").strip() or None
58
+ try:
59
+ home=Path(os.environ.get("HYTHON_HOME",Path.home()/".hython"))
60
+ payload=json.loads((home/"state.json").read_text(encoding="utf-8"))
61
+ tag=payload.get("runtime_tag")
62
+ return tag if isinstance(tag,str) and tag else None
63
+ except (OSError,UnicodeError,json.JSONDecodeError):
64
+ return None
65
+
66
+ def run_hython_with_runtime(tag: str, argv: list[str]) -> int:
67
+ """Run this installed Hython package inside a managed Python runtime."""
68
+ env=os.environ.copy()
69
+ env["HYTHON_RUNTIME_ACTIVE"]=tag
70
+ source_root=str(external_source_root())
71
+ env["PYTHONPATH"]=source_root+os.pathsep+env.get("PYTHONPATH","")
72
+ return subprocess.run([find_manager(),"exec",f"-V:{tag}","-m","hython",*argv],env=env).returncode
73
+
74
+ def reexec_with_preferred_runtime(argv: list[str], start: Path) -> None:
75
+ tag = get_preference(start)
76
+ if not tag or os.environ.get("HYTHON_RUNTIME_ACTIVE") == tag:
77
+ return
78
+ env = os.environ.copy()
79
+ env["HYTHON_RUNTIME_ACTIVE"] = tag
80
+ source_root = str(external_source_root())
81
+ env["PYTHONPATH"] = source_root + os.pathsep + env.get("PYTHONPATH", "")
82
+ result = subprocess.run([find_manager(), "exec", f"-V:{tag}", "-m", "hython", *argv], env=env)
83
+ raise SystemExit(result.returncode)
hython/translator.py ADDED
@@ -0,0 +1,244 @@
1
+ """Token-safe translation between Hython and Python."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import re
7
+ import tokenize
8
+ from dataclasses import dataclass
9
+ from collections.abc import Mapping
10
+
11
+ from .phonetics import pronounce_identifier
12
+ from .vocabulary import (
13
+ BUILTINS, KEYWORDS, LIBRARY_NAMES, SPECIAL_NAMES,
14
+ HYTHON_TO_PYTHON, PYTHON_TO_HYTHON,
15
+ )
16
+
17
+ LITERAL_PREFIXES = {
18
+ "에프": "f", "알": "r", "비": "b", "유": "u",
19
+ "알에프": "rf", "에프알": "fr", "비알": "br", "알비": "rb",
20
+ }
21
+ PYTHON_LITERAL_PREFIXES = {
22
+ python: hython for hython, python in LITERAL_PREFIXES.items()
23
+ }
24
+
25
+
26
+ def normalize_literal_prefixes(source: str) -> str:
27
+ """Turn adjacent Hangul string prefixes into Python lexical prefixes."""
28
+ try:
29
+ tokens = list(tokenize.generate_tokens(io.StringIO(source).readline))
30
+ except (tokenize.TokenError, IndentationError) as exc:
31
+ raise TranslationError(str(exc)) from exc
32
+ lines = source.splitlines(keepends=True)
33
+ offsets: list[int] = []
34
+ total = 0
35
+ for line in lines:
36
+ offsets.append(total)
37
+ total += len(line)
38
+ replacements: list[tuple[int, int, str]] = []
39
+ for index, token in enumerate(tokens[:-1]):
40
+ following = tokens[index + 1]
41
+ if (
42
+ token.type == tokenize.NAME
43
+ and token.string in LITERAL_PREFIXES
44
+ and following.type == tokenize.STRING
45
+ and token.end == following.start
46
+ ):
47
+ start = offsets[token.start[0] - 1] + token.start[1]
48
+ end = offsets[token.end[0] - 1] + token.end[1]
49
+ replacements.append((start, end, LITERAL_PREFIXES[token.string]))
50
+ for start, end, replacement in reversed(replacements):
51
+ source = source[:start] + replacement + source[end:]
52
+ return source
53
+
54
+
55
+ def _hython_names() -> dict[str, str]:
56
+ # Lazy import keeps startup cheap and allows newly generated dictionaries to
57
+ # become active without restarting a long-running process.
58
+ from .package_manager import load_dictionaries
59
+ from .runtime import load_runtime_spellings
60
+ # Core syntax wins if an untrusted/generated package dictionary collides.
61
+ return load_dictionaries() | load_runtime_spellings() | HYTHON_TO_PYTHON
62
+
63
+
64
+ def _python_names() -> dict[str, str]:
65
+ from .package_manager import load_python_dictionaries
66
+ from .runtime import load_runtime_spellings
67
+ runtime = {python: spoken for spoken, python in load_runtime_spellings().items()}
68
+ return load_python_dictionaries() | runtime | PYTHON_TO_HYTHON
69
+
70
+
71
+ class TranslationError(SyntaxError):
72
+ """Raised when a source stream cannot be tokenized."""
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class EnglishName:
77
+ """One executable English identifier left in Hython source."""
78
+
79
+ name: str
80
+ line: int
81
+ column: int
82
+ suggestion: str
83
+
84
+
85
+ def _translate(source: str, names: Mapping[str, str], *, preserve_aliases: bool = False) -> str:
86
+ source = normalize_literal_prefixes(source)
87
+ try:
88
+ tokens = list(tokenize.generate_tokens(io.StringIO(source).readline))
89
+ except (tokenize.TokenError, IndentationError) as exc:
90
+ raise TranslationError(str(exc)) from exc
91
+
92
+ aliases = {
93
+ tokens[index + 1].string
94
+ for index, token in enumerate(tokens[:-1])
95
+ if preserve_aliases and token.type == tokenize.NAME
96
+ and token.string in ("as", "애즈")
97
+ and tokens[index + 1].type == tokenize.NAME
98
+ }
99
+ changed = []
100
+ for index, token in enumerate(tokens):
101
+ # A Korean import alias is user-owned, but an identically spelled API
102
+ # member after a dot still needs translation (티케이.티케이 -> 티케이.Tk).
103
+ alias_reference = token.string in aliases and (
104
+ index == 0 or tokens[index - 1].string != "."
105
+ )
106
+ changed.append(
107
+ token._replace(string=names.get(token.string, token.string))
108
+ if token.type == tokenize.NAME and not alias_reference
109
+ else token
110
+ )
111
+ return tokenize.untokenize(changed)
112
+
113
+
114
+ def to_python(source: str) -> str:
115
+ """Translate Hython source to executable Python without touching text/comments."""
116
+ return _translate(source, _hython_names(), preserve_aliases=True)
117
+
118
+
119
+ def to_hython(source: str) -> str:
120
+ """Translate supported Python names to canonical Hython spellings."""
121
+ try:
122
+ tokens = list(tokenize.generate_tokens(io.StringIO(source).readline))
123
+ except (tokenize.TokenError, IndentationError) as exc:
124
+ raise TranslationError(str(exc)) from exc
125
+ from .runtime import load_runtime_spellings
126
+ runtime = {
127
+ python: spoken for spoken, python in load_runtime_spellings().items()
128
+ }
129
+ core = KEYWORDS | BUILTINS | SPECIAL_NAMES | runtime
130
+ api = _python_names() | LIBRARY_NAMES
131
+ imported: set[str] = set()
132
+ in_import = False
133
+ after_as = False
134
+ import_contexts: list[bool] = []
135
+ for token in tokens:
136
+ if token.type in (tokenize.NEWLINE, tokenize.NL):
137
+ in_import = False
138
+ after_as = False
139
+ elif token.type == tokenize.NAME:
140
+ if token.string in ("import", "from"):
141
+ in_import = True
142
+ elif in_import and token.string == "as":
143
+ after_as = True
144
+ elif in_import and not after_as:
145
+ imported.add(token.string)
146
+ elif after_as:
147
+ after_as = False
148
+ import_contexts.append(in_import)
149
+ changed = []
150
+ for index, token in enumerate(tokens):
151
+ previous = tokens[index - 1].string if index else ""
152
+ import_context = import_contexts[index]
153
+ if token.type == tokenize.NAME:
154
+ if token.string in core:
155
+ token = token._replace(string=core[token.string])
156
+ elif (
157
+ token.string in api
158
+ and (previous == "." or import_context or token.string in imported)
159
+ ):
160
+ token = token._replace(string=api[token.string])
161
+ changed.append(token)
162
+ return tokenize.untokenize(changed)
163
+
164
+
165
+ def audit_english(source: str) -> list[EnglishName]:
166
+ """Find ASCII identifiers in code while ignoring strings and comments."""
167
+ known = _python_names()
168
+ try:
169
+ tokens = tokenize.generate_tokens(io.StringIO(source).readline)
170
+ result = [
171
+ EnglishName(
172
+ token.string,
173
+ token.start[0],
174
+ token.start[1] + 1,
175
+ known.get(token.string, pronounce_identifier(token.string)),
176
+ )
177
+ for token in tokens
178
+ if token.type == tokenize.NAME
179
+ and re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", token.string)
180
+ ]
181
+ for token in tokenize.generate_tokens(io.StringIO(source).readline):
182
+ prefix = ""
183
+ if token.type == tokenize.STRING:
184
+ match = re.match(r"(?i)(rf|fr|rb|br|r|f|b|u)(?=['\"])", token.string)
185
+ prefix = match.group(1).lower() if match else ""
186
+ elif token.type == getattr(tokenize, "FSTRING_START", -1):
187
+ prefix = token.string[:-1].lower()
188
+ if prefix in PYTHON_LITERAL_PREFIXES:
189
+ result.append(EnglishName(
190
+ prefix, token.start[0], token.start[1] + 1,
191
+ PYTHON_LITERAL_PREFIXES[prefix],
192
+ ))
193
+ return result
194
+ except (tokenize.TokenError, IndentationError) as exc:
195
+ raise TranslationError(str(exc)) from exc
196
+
197
+
198
+ def koreanize(source: str) -> str:
199
+ """Convert every ASCII code identifier to a deterministic Hangul spelling.
200
+
201
+ Known Python/package API names use their registered spelling. Unknown names
202
+ are treated as user identifiers, so renaming all their occurrences is safe.
203
+ """
204
+ known = _python_names()
205
+ try:
206
+ tokens = list(tokenize.generate_tokens(io.StringIO(source).readline))
207
+ except (tokenize.TokenError, IndentationError) as exc:
208
+ raise TranslationError(str(exc)) from exc
209
+ reverse_names = _hython_names()
210
+ generated: dict[str, str] = {}
211
+ occupied = set(reverse_names)
212
+ changed = []
213
+ for token in tokens:
214
+ if token.type == tokenize.NAME and re.fullmatch(
215
+ r"[A-Za-z_][A-Za-z0-9_]*", token.string
216
+ ):
217
+ spoken = known.get(token.string)
218
+ if spoken is None:
219
+ spoken = generated.get(token.string)
220
+ if spoken is None:
221
+ base = pronounce_identifier(token.string)
222
+ spoken = base
223
+ suffix = 2
224
+ while spoken in occupied:
225
+ spoken = f"{base}{suffix}"
226
+ suffix += 1
227
+ generated[token.string] = spoken
228
+ occupied.add(spoken)
229
+ token = token._replace(string=spoken)
230
+ elif token.type in (tokenize.STRING, getattr(tokenize, "FSTRING_START", -1)):
231
+ match = re.match(r"(?i)(rf|fr|rb|br|r|f|b|u)(?=['\"])", token.string)
232
+ if match and match.group(1).lower() in PYTHON_LITERAL_PREFIXES:
233
+ prefix = match.group(1)
234
+ token = token._replace(
235
+ string=PYTHON_LITERAL_PREFIXES[prefix.lower()]
236
+ + token.string[len(prefix):]
237
+ )
238
+ changed.append(token)
239
+ return tokenize.untokenize(changed)
240
+
241
+
242
+ def compile_hython(source: str, filename: str = "<하이썬>", mode: str = "exec"):
243
+ """Translate and compile Hython source."""
244
+ return compile(to_python(source), filename, mode)
hython/updater.py ADDED
@@ -0,0 +1,93 @@
1
+ """Lifecycle for generated syntax state kept outside the installed core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import platform
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ from . import __version__
12
+ from .package_manager import refresh_dictionaries
13
+ from .runtime import sync_runtime
14
+
15
+
16
+ def state_root() -> Path:
17
+ root = Path(os.environ.get("HYTHON_HOME", Path.home() / ".hython"))
18
+ root.mkdir(parents=True, exist_ok=True)
19
+ return root
20
+
21
+
22
+ def marker_path() -> Path:
23
+ return state_root() / "state.json"
24
+
25
+
26
+ def is_initialized() -> bool:
27
+ try:
28
+ payload = json.loads(marker_path().read_text(encoding="utf-8"))
29
+ return (
30
+ payload.get("format") == 1
31
+ and payload.get("hython_version") == __version__
32
+ and payload.get("python_version") == platform.python_version()
33
+ )
34
+ except (OSError, UnicodeError, json.JSONDecodeError):
35
+ return False
36
+
37
+
38
+ def initialize(*, force: bool = False) -> dict:
39
+ if is_initialized() and not force:
40
+ return {"initialized": False, "runtime": None}
41
+ previous = {}
42
+ try:
43
+ previous=json.loads(marker_path().read_text(encoding="utf-8"))
44
+ except (OSError,UnicodeError,json.JSONDecodeError):
45
+ pass
46
+ runtime = sync_runtime()
47
+ if previous:
48
+ refresh_dictionaries()
49
+ payload = {
50
+ "format": 1,
51
+ "hython_version": __version__,
52
+ "python_version": platform.python_version(),
53
+ }
54
+ if isinstance(previous.get("runtime_tag"),str):
55
+ payload["runtime_tag"]=previous["runtime_tag"]
56
+ _atomic_json(marker_path(), payload)
57
+ return {"initialized": True, "runtime": runtime}
58
+
59
+
60
+ def refresh(*, runtime_tag: str | None = None) -> dict:
61
+ """Regenerate active runtime and package overlays, pruning stale packages."""
62
+ runtime = sync_runtime()
63
+ refreshed, removed = refresh_dictionaries()
64
+ payload = {
65
+ "format": 1,
66
+ "hython_version": __version__,
67
+ "python_version": platform.python_version(),
68
+ }
69
+ if runtime_tag:
70
+ payload["runtime_tag"] = runtime_tag
71
+ else:
72
+ try:
73
+ previous=json.loads(marker_path().read_text(encoding="utf-8"))
74
+ if isinstance(previous.get("runtime_tag"),str):
75
+ payload["runtime_tag"]=previous["runtime_tag"]
76
+ except (OSError,UnicodeError,json.JSONDecodeError):
77
+ pass
78
+ _atomic_json(marker_path(), payload)
79
+ return {"runtime": runtime, "refreshed": refreshed, "removed": removed}
80
+
81
+
82
+ def _atomic_json(output: Path, payload: dict) -> None:
83
+ handle, temporary = tempfile.mkstemp(prefix=output.name + ".", suffix=".tmp", dir=output.parent)
84
+ try:
85
+ with os.fdopen(handle, "w", encoding="utf-8", newline="\n") as stream:
86
+ json.dump(payload, stream, ensure_ascii=False, indent=2)
87
+ stream.write("\n")
88
+ stream.flush()
89
+ os.fsync(stream.fileno())
90
+ Path(temporary).replace(output)
91
+ except BaseException:
92
+ Path(temporary).unlink(missing_ok=True)
93
+ raise