Files
2026-10-20 09:24:27 +09:00

490 lines
15 KiB
Python

import os
import shutil
import subprocess
import sys
import tempfile
import warnings
from pathlib import Path
from testutils import assert_raises
print("python executable:", sys.executable)
print(sys.argv)
assert sys.argv[0].endswith(".py")
assert (
sys.platform == "linux"
or sys.platform == "darwin"
or sys.platform == "win32"
or sys.platform == "unknown"
)
if hasattr(sys, "_framework"):
assert type(sys._framework) is str
assert isinstance(sys.builtin_module_names, tuple)
assert "sys" in sys.builtin_module_names
assert isinstance(sys.implementation.name, str)
assert isinstance(sys.implementation.cache_tag, str)
assert sys.version_info[:3] == (3, 15, 0)
assert sys.hexversion >> 8 == 0x030F00
assert sys.version.startswith("3.15.0")
assert sys.implementation.cache_tag == (
f"{sys.implementation.name}-{sys.version_info.major}{sys.version_info.minor}"
)
assert sys.getfilesystemencoding() == "utf-8"
assert sys.getfilesystemencodeerrors().startswith("surrogate")
assert sys.byteorder == "little" or sys.byteorder == "big"
assert isinstance(sys.flags, tuple)
assert type(sys.flags).__name__ == "flags"
assert type(sys.flags.optimize) is int
assert sys.flags[3] == sys.flags.optimize
assert sys.maxunicode == 1114111
# Tracing:
events = []
def trc(frame, event, arg):
fn_name = frame.f_code.co_name
events.append((fn_name, event, arg))
print("trace event:", fn_name, event, arg)
def demo(x):
if x > 0:
demo(x - 1)
sys.settrace(trc)
demo(5)
sys.settrace(None)
assert ("demo", "call", None) in events
# The eager iterator prefix of a generator expression is not traced, but
# monitoring still reports the error it raises.
class GoodIterable:
def __iter__(self):
return iter([1])
class BadIterable:
def __iter__(self):
raise ValueError("eager iterator")
def create_genexpr(iterable):
return (value for value in iterable)
events = []
def trc(frame, event, arg):
if frame.f_code.co_name in ("create_genexpr", "<genexpr>", "__iter__"):
events.append((frame.f_code.co_name, event))
return trc
sys.settrace(trc)
try:
create_genexpr(GoodIterable()).close()
try:
create_genexpr(BadIterable())
except ValueError as error:
tb_names = []
tb = error.__traceback__
while tb is not None:
tb_names.append(tb.tb_frame.f_code.co_name)
tb = tb.tb_next
else:
raise AssertionError("expected eager iterator error")
finally:
sys.settrace(None)
assert events == [
("create_genexpr", "call"),
("create_genexpr", "line"),
("__iter__", "call"),
("__iter__", "line"),
("__iter__", "return"),
("create_genexpr", "return"),
("create_genexpr", "call"),
("create_genexpr", "line"),
("__iter__", "call"),
("__iter__", "line"),
("__iter__", "exception"),
("__iter__", "return"),
("create_genexpr", "exception"),
("create_genexpr", "return"),
("create_genexpr", "exception"),
("create_genexpr", "return"),
], events
assert tb_names == ["<module>", "create_genexpr", "__iter__"], tb_names
monitoring = sys.monitoring
event_names = ("PY_START", "PY_RESUME", "PY_UNWIND", "RAISE", "LINE", "INSTRUCTION")
records = []
monitoring.use_tool_id(2, "genexpr-prefix")
try:
for name in event_names:
def callback(code, *args, name=name):
if code.co_name == "<genexpr>":
records.append(name)
monitoring.register_callback(2, getattr(monitoring.events, name), callback)
monitoring.set_events(
2, sum(getattr(monitoring.events, name) for name in event_names)
)
create_genexpr(GoodIterable()).close()
assert records == [], records
with assert_raises(ValueError):
create_genexpr(BadIterable())
finally:
monitoring.set_events(2, 0)
monitoring.free_tool_id(2)
assert records == ["RAISE", "PY_UNWIND"], records
# A loop body large enough to require EXTENDED_ARG on its jump targets.
namespace = {}
exec(
"def large_loop(skip=False):\n if skip:\n return 'done'\n"
" total = 0\n for x in (1, 2):\n" + " total += x\n" * 100 + " return total\n",
namespace,
)
large_loop = namespace["large_loop"]
def jump_out_of_large_loop(code, line):
global loop_hits
if code is large_loop.__code__ and line == 5:
loop_hits += 1
if loop_hits == 2:
sys._getframe(1).f_lineno = 3
def jump_after_large_loop(code, line):
if code is large_loop.__code__ and line == 5:
sys._getframe(1).f_lineno = 106
monitoring.use_tool_id(2, "extended-arg")
try:
for event in (monitoring.events.LINE, monitoring.events.INSTRUCTION):
monitoring.register_callback(2, event, lambda *args: None)
for events in (
monitoring.events.LINE,
monitoring.events.LINE | monitoring.events.INSTRUCTION,
):
monitoring.set_local_events(2, large_loop.__code__, events)
assert large_loop() == 300
loop_hits = 0
monitoring.register_callback(2, monitoring.events.LINE, jump_out_of_large_loop)
monitoring.set_local_events(2, large_loop.__code__, monitoring.events.LINE)
assert large_loop() == "done"
assert loop_hits == 2
monitoring.register_callback(2, monitoring.events.LINE, jump_after_large_loop)
assert large_loop() == 0
finally:
monitoring.set_local_events(2, large_loop.__code__, 0)
for event in (monitoring.events.LINE, monitoring.events.INSTRUCTION):
monitoring.register_callback(2, event, None)
monitoring.free_tool_id(2)
# Combined instruction events must preserve loop-header line events.
def monitored_loop():
total = 0
for value in (1, 2):
total += value
return total
lines = []
monitoring.use_tool_id(2, "combined-lines")
try:
monitoring.register_callback(
2,
monitoring.events.LINE,
lambda code, line: lines.append(line - code.co_firstlineno),
)
monitoring.register_callback(2, monitoring.events.INSTRUCTION, lambda *args: None)
monitoring.set_local_events(
2,
monitored_loop.__code__,
monitoring.events.LINE | monitoring.events.INSTRUCTION,
)
assert monitored_loop() == 3
assert lines == [1, 2, 3, 2, 3, 2, 4]
finally:
monitoring.set_local_events(2, monitored_loop.__code__, 0)
for event in (monitoring.events.LINE, monitoring.events.INSTRUCTION):
monitoring.register_callback(2, event, None)
monitoring.free_tool_id(2)
assert sys.exc_info() == (None, None, None)
try:
1 / 0
except ZeroDivisionError as exc:
exc_info = sys.exc_info()
assert exc_info[0] == type(exc) == ZeroDivisionError
assert exc_info[1] == exc
# Recursion:
def recursive_call(n):
if n > 0:
recursive_call(n - 1)
sys.setrecursionlimit(200)
assert sys.getrecursionlimit() == 200
with assert_raises(RecursionError):
recursive_call(300)
if sys.platform.startswith("win"):
winver = sys.getwindowsversion()
print(f"winver: {winver} {winver.platform_version}")
# the biggest value of wSuiteMask (https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexa#members).
all_masks = (
0x00000004
| 0x00000400
| 0x00004000
| 0x00000080
| 0x00000002
| 0x00000040
| 0x00000200
| 0x00000100
| 0x00000001
| 0x00000020
| 0x00002000
| 0x00000010
| 0x00008000
| 0x00020000
)
# We really can't test if the results are correct, so it just checks for meaningful value
assert winver.major > 6
assert winver.minor >= 0
assert winver.build > 0
assert winver.platform == 2
assert isinstance(winver.service_pack, str)
assert 0 <= winver.suite_mask <= all_masks
assert 1 <= winver.product_type <= 3
# XXX if platform_version is implemented correctly, this'll break on compatibility mode or a build without manifest
# these fields can mismatch in CPython
assert winver.major == winver.platform_version[0]
assert winver.minor == winver.platform_version[1]
# assert winver.build == winver.platform_version[2]
# test int_max_str_digits getter and setter
assert sys.get_int_max_str_digits() == 4300
sys.set_int_max_str_digits(640)
assert sys.get_int_max_str_digits() == 640
sys.set_int_max_str_digits(0)
assert sys.get_int_max_str_digits() == 0
with assert_raises(ValueError):
sys.set_int_max_str_digits(1)
sys.set_int_max_str_digits(1000)
assert sys.get_int_max_str_digits() == 1000
# Test the PYTHONSAFEPATH environment variable
code = "import sys; print(sys.flags.safe_path)"
env = dict(os.environ)
env.pop("PYTHONSAFEPATH", None)
args = (sys.executable, "-P", "-c", code)
proc = subprocess.run(args, stdout=subprocess.PIPE, universal_newlines=True, env=env)
assert proc.stdout.rstrip() == "True", proc
assert proc.returncode == 0, proc
env["PYTHONSAFEPATH"] = "1"
proc = subprocess.run(args, stdout=subprocess.PIPE, universal_newlines=True, env=env)
assert proc.stdout.rstrip() == "True"
assert proc.returncode == 0, proc
# -S must prevent the site import itself, not just its customizations.
subprocess.run(
[
sys.executable,
"-S",
"-c",
"import sys; assert sys.flags.no_site; assert 'site' not in sys.modules",
],
check=True,
)
assert sys._getframemodulename() == "__main__", sys._getframemodulename()
def test_getframemodulename():
return sys._getframemodulename()
test_getframemodulename.__module__ = "awesome_module"
assert test_getframemodulename() == "awesome_module"
# An unimportable $PYTHONBREAKPOINT warns, and the hook has to survive that
# warning being turned into an exception.
saved_breakpoint_env = os.environ.get("PYTHONBREAKPOINT")
os.environ["PYTHONBREAKPOINT"] = "nonexistent_xyz.foo"
try:
with warnings.catch_warnings():
warnings.simplefilter("error")
with assert_raises(RuntimeWarning):
sys.breakpointhook()
finally:
if saved_breakpoint_env is None:
del os.environ["PYTHONBREAKPOINT"]
else:
os.environ["PYTHONBREAKPOINT"] = saved_breakpoint_env
# A venv's symlink target takes precedence over home/<invoked name>.
if os.name == "posix" and sys.platform not in ("wasi", "emscripten"):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
interpreter = Path(sys.executable).resolve()
# A framework build can also run its embedded app directly. Match the
# installed launcher's file identity before applying launcher semantics.
framework_base = (
Path(sys.base_prefix)
/ "bin"
/ f"python{sys.version_info.major}.{sys.version_info.minor}"
)
framework_launcher = (
sys.platform == "darwin"
and bool(getattr(sys, "_framework", ""))
and framework_base.is_file()
and interpreter.samefile(framework_base)
)
home = root / "home"
home.mkdir()
venv_env = dict(os.environ)
for name in ("PYTHONHOME", "PYTHONEXECUTABLE", "__PYVENV_LAUNCHER__"):
venv_env.pop(name, None)
# Keep startup imports available even with the synthetic home directory.
venv_env["PYTHONPATH"] = os.pathsep.join(sys.path)
path_code = (
"import sys; print(sys.executable); "
"print(sys._base_executable); print(sys.prefix)"
)
def executable_paths(executable, *, cwd=None, env=venv_env):
result = subprocess.run(
[str(executable), "-B", "-S", "-c", path_code],
cwd=cwd,
env=env,
capture_output=True,
text=True,
timeout=10,
)
assert result.returncode == 0, result.stderr
return result.stdout.splitlines()
for kind in ("absolute", "relative", "chain"):
bin_dir = root / kind / "bin"
bin_dir.mkdir(parents=True)
# Exercise both parent and adjacent pyvenv.cfg discovery.
prefix = bin_dir if kind == "chain" else bin_dir.parent
(prefix / "pyvenv.cfg").write_text(f"home = {home}\n")
executable = bin_dir / kind
# This regular file must never be preferred to a symlink's target.
(home / kind).touch()
if kind == "absolute":
executable.symlink_to(interpreter)
elif kind == "relative":
executable.symlink_to(os.path.relpath(interpreter, bin_dir))
else:
(bin_dir / "target").symlink_to(interpreter)
executable.symlink_to("target")
invocation = f"./bin/{kind}" if kind == "relative" else executable
assert executable_paths(invocation, cwd=bin_dir.parent) == [
str(executable),
str(framework_base if framework_launcher else interpreter),
str(prefix),
]
# Framework launchers preset their base; ordinary binaries keep argv[0].
plain = root / "plain" / "python"
plain.parent.mkdir()
plain.symlink_to(interpreter)
assert executable_paths(plain)[:2] == [
str(plain),
str(framework_base if framework_launcher else plain),
]
# Without a launcher preset, copies fall back to home/<invoked name>.
copied = root / "copied" / "python"
copied.parent.mkdir()
shutil.copy2(interpreter, copied)
(home / copied.name).touch()
(copied.parent / "pyvenv.cfg").write_text(f"home = {home}\n")
assert executable_paths(copied) == [
str(copied),
str(framework_base if framework_launcher else home / copied.name),
str(copied.parent),
]
# A directory alias does not turn a copied executable into a symlink.
directory_alias = root / "directory-alias"
directory_alias.symlink_to(copied.parent, target_is_directory=True)
aliased_copy = directory_alias / copied.name
# The framework launcher canonicalizes its directory before dispatch.
assert executable_paths(aliased_copy) == [
str(copied if framework_launcher else aliased_copy),
str(framework_base if framework_launcher else home / copied.name),
str(copied.parent if framework_launcher else directory_alias),
]
# Without a launcher preset, final-link resolution preserves target aliases.
target_link = root / "linked" / "python"
target_link.parent.mkdir()
(target_link.parent / "pyvenv.cfg").write_text(f"home = {home}\n")
target_link.symlink_to("../directory-alias/python")
assert executable_paths(target_link) == [
str(target_link),
str(framework_base if framework_launcher else aliased_copy),
str(target_link.parent),
]
# macOS framework builds use a framework-derived base for overrides.
if sys.platform == "linux":
# Relative targets also preserve a double-leading-slash directory.
double_target = root / "double-target"
double_target.symlink_to("directory-alias/python")
target_link.unlink()
target_link.symlink_to("/" + str(double_target))
assert executable_paths(target_link) == [
str(target_link),
"/" + str(aliased_copy),
str(target_link.parent),
]
override_env = dict(venv_env, __PYVENV_LAUNCHER__=str(executable))
assert executable_paths(plain, env=override_env) == [
str(executable),
str(plain),
str(prefix),
]