blob: a116c549cbf624775d37667fb346e399fb9d8b4e [file]
from __future__ import annotations
import re
import sys
import sysconfig
from typing import Final, final
from mypy.defaults import PYTHON3_VERSION_MIN
from mypyc.common import EXT_SUFFIX
# Is the running interpreter a free-threaded build (GIL disabled)? Use
# TargetPython.host().free_threaded instead of this.
_HOST_FREE_THREADED: Final = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
@final
class TargetPython:
"""The Python build that generated code targets.
This defaults to the running interpreter, but can be overridden so that
the C code can be generated on a different Python than the one that will
compile and run it.
This is currently mostly meant for users that bring their own monolithic
build system (e.g. Bazel or Buck2), which call mypyc_build() to only generate
C and then compile it themselves against the headers of the target Python.
It's not exposed through the mypyc command line or mypycify().
WARNING: Never compile the generated C for a different Python than the
target. setuptools-based builds always compile using the running interpreter,
so a different target there produces extension modules that fail to compile
or, worse, compile but don't work (e.g. code for a GIL build has data races
on a free-threaded build).
"""
def __init__(self, version: tuple[int, int], free_threaded: bool = False) -> None:
if version[0] != 3 or version < PYTHON3_VERSION_MIN:
min_version = ".".join(str(v) for v in PYTHON3_VERSION_MIN)
raise ValueError(
f"Unsupported target Python {version[0]}.{version[1]} "
f"({min_version} or later is required)"
)
if free_threaded and version < (3, 13):
raise ValueError(
f"Free-threaded builds require Python 3.13 or later "
f"(got {version[0]}.{version[1]})"
)
# Python (C API) version, such as (3, 13)
self.version: Final = version
# Is this a free-threaded (GIL disabled) build?
self.free_threaded: Final = free_threaded
# Python 3.12 introduced immortal objects, specified via a special reference
# count value. The reference counts of immortal objects are normally not
# modified, but it's not strictly wrong to modify them. See PEP 683 for more
# information, but note that some details in the PEP are out of date.
self.have_immortal: Final = version >= (3, 12)
# File name suffix of extension modules, e.g. ".cpython-314t-x86_64-linux-gnu.so".
# Only the Python version (not the platform) can differ from the host.
is_host = version == sys.version_info[:2] and free_threaded == _HOST_FREE_THREADED
self.ext_suffix: Final = (
EXT_SUFFIX
if is_host
else replace_ext_suffix_version(EXT_SUFFIX, version, free_threaded)
)
@staticmethod
def host() -> TargetPython:
"""Target the running interpreter."""
return TargetPython(sys.version_info[:2], _HOST_FREE_THREADED)
@staticmethod
def parse(target: str) -> TargetPython:
"""Parse a target such as "3.13" or "3.14t" ("t" means free-threaded)."""
m = re.fullmatch(r"(\d+)\.(\d+)(t?)", target.strip())
if m is None:
raise ValueError(f'Invalid target Python "{target}" (expected e.g. "3.13" or "3.14t")')
return TargetPython((int(m.group(1)), int(m.group(2))), m.group(3) == "t")
def __eq__(self, other: object) -> bool:
return (
isinstance(other, TargetPython)
and self.version == other.version
and self.free_threaded == other.free_threaded
)
def __hash__(self) -> int:
return hash((self.version, self.free_threaded))
def __str__(self) -> str:
return f"{self.version[0]}.{self.version[1]}{'t' if self.free_threaded else ''}"
def __repr__(self) -> str:
return f"TargetPython({str(self)!r})"
def replace_ext_suffix_version(suffix: str, version: tuple[int, int], free_threaded: bool) -> str:
"""Replace the Python version tag in an extension suffix such as ".cp313-win_amd64.pyd"."""
tag = f"{version[0]}{version[1]}{'t' if free_threaded else ''}"
result, n = re.subn(r"(\.cpython-|\.cp)\d+t?(?=-)", rf"\g<1>{tag}", suffix, count=1)
if n == 0:
raise ValueError(
f'Can\'t target Python {version[0]}.{version[1]}{"t" if free_threaded else ""}: '
f'unrecognized extension module suffix "{suffix}"'
)
return result
class CompilerOptions:
def __init__(
self,
strip_asserts: bool = False,
multi_file: bool = False,
verbose: bool = False,
separate: bool = False,
target_dir: str | None = None,
include_runtime_files: bool | None = None,
strict_dunder_typing: bool = False,
group_name: str | None = None,
log_trace: bool = False,
depends_on_librt_internal: bool = False,
experimental_features: bool = False,
strict_traceback_checks: bool = False,
target_python: TargetPython | None = None,
) -> None:
self.strip_asserts = strip_asserts
self.multi_file = multi_file
self.verbose = verbose
self.separate = separate
self.global_opts = not separate
self.target_dir = target_dir or "build"
self.include_runtime_files = (
include_runtime_files if include_runtime_files is not None else not multi_file
)
# The Python build to generate code for (see TargetPython, including the
# warning there). This determines both the C API version and the Python
# version used for type checking.
self.target_python = target_python or TargetPython.host()
# Make possible to inline dunder methods in the generated code.
# Typically, the convention is the dunder methods can return `NotImplemented`
# even when its return type is just `bool`.
# By enabling this option, this convention is no longer valid and the dunder
# will assume the return type of the method strictly, which can lead to
# more optimization opportunities.
self.strict_dunders_typing = strict_dunder_typing
# Override the automatic group name derived from the hash of module names.
# This affects the names of generated .c, .h and shared library files.
# This is only supported when compiling exactly one group, and a shared
# library is generated (with shims). This can be used to make the output
# file names more predictable.
self.group_name = group_name
# If enabled, write a trace log of events based on executed operations to
# mypyc_trace.txt when compiled module is executed. This is useful for
# performance analysis.
self.log_trace = log_trace
# If enabled, add capsule imports of librt.internal API. This should be used
# only for mypy itself, third-party code compiled with mypyc should not use
# librt.internal.
self.depends_on_librt_internal = depends_on_librt_internal
# Some experimental features are only available when building librt in
# experimental mode (e.g. use _experimental suffix in librt run test).
# These can't be used with a librt wheel installed from PyPI.
self.experimental_features = experimental_features
# If enabled, mypyc will assert that every traceback it generates has a
# positive line number.
# Currently each AST node is assigned line number -1 by default to indicate
# that it's unset. If the line number is never set and a traceback is
# generated that points at such node, then the line number will be interpreted
# as None instead of an integer by Python and potentially crash code that
# expects an integer, such as pytest.
# The goal is to prevent the incorrect tracebacks but it will require a lot
# of changes across mypyc. In the meantime, this option should be enabled in
# tests to make sure that no new code which leads to incorrect tracebacks is
# added.
self.strict_traceback_checks = strict_traceback_checks