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,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
|