diff --git a/create_stub_pyray.py b/create_stub_pyray.py index fb334e7..18b11b1 100644 --- a/create_stub_pyray.py +++ b/create_stub_pyray.py @@ -13,13 +13,14 @@ # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 from pathlib import Path -from raylib import rl, ffi +from raylib import rl, ffi, defines from pyray import _underscore from inspect import ismethod, getmembers, isbuiltin import inflection, sys, json known_functions = {} known_structs = {} +emitted_top_level_names = set() for filename in (Path("raylib.json"), Path("raymath.json"), Path("rlgl.json"), Path("raygui.json"), Path("physac.json"), Path("glfw3.json")): f = open(filename, "r") @@ -78,6 +79,37 @@ def ctype_to_python_type(t): return t +def value_to_python_type(value): + if isinstance(value, bool): + return "bool" + if isinstance(value, int): + return "int" + if isinstance(value, float): + return "float" + if isinstance(value, str): + return "str" + return str(type(value))[8:-2] + + +def emit_top_level_name(name, type_name): + if name.startswith("_"): + return + if name in emitted_top_level_names: + return + emitted_top_level_names.add(name) + print(f"{name}: {type_name}") + + +def should_emit_module_constant(name): + # Keep only standalone rlgl/raylib constants that do not have enum-class replacements. + if not name.startswith("RL_"): + return False + # Backward-compat aliases from raylib.h that are intentionally discouraged. + if name in {"RL_SHADER_LOC_MAP_DIFFUSE", "RL_SHADER_LOC_MAP_SPECULAR"}: + return False + return True + + print("""from typing import Any from warnings import deprecated import _cffi_backend # type: ignore @@ -86,6 +118,13 @@ def ctype_to_python_type(t): PhysicsShapeType = int """) +for name in sorted(dir(defines)): + if not name.isupper(): + continue + if not should_emit_module_constant(name): + continue + emit_top_level_name(name, value_to_python_type(getattr(defines, name))) + # These words can be used for c arg names, but not in python reserved_words = ("in", "list", "tuple", "set", "dict", "from", "range", "min", "max", "any", "all", "len") @@ -132,14 +171,25 @@ def ctype_to_python_type(t): f'def {uname}(*args) -> {ctype_to_python_type(return_type)}:\n """VARARG FUNCTION - MAY NOT BE SUPPORTED BY CFFI"""\n ...') else: # print("*****", str(type(attr))) - t = str(type(attr))[8:-2] # this isolates the type - if t != "int": - print(f"{name}: {t}") - + type_name = value_to_python_type(attr) + if type_name != "int": + emit_top_level_name(name, type_name) + +struct_aliases = { + "Texture2D": "Texture", + "TextureCubemap": "Texture", + "RenderTexture2D": "RenderTexture", +} +emitted_structs = set() +pending_aliases = {} for struct in ffi.list_types()[0]: print("processing", struct, file=sys.stderr) if ffi.typeof(struct).kind == "struct": + alias_target = struct_aliases.get(struct, None) + if alias_target is not None: + pending_aliases[struct] = alias_target + continue json_object = known_structs.get(struct, None) if json_object is None: # this is _not_ an exported struct from raylib, raymath, rlgl raygui or physac @@ -160,12 +210,17 @@ def ctype_to_python_type(t): for arg in ffi.typeof(struct).fields: print(f" self.{arg[0]}:{ctype_to_python_type(arg[1].type.cname)} = {arg[0]} # type: ignore") + emitted_structs.add(struct) # elif ffi.typeof(struct).kind == "enum": # print(f"{struct}: int") else: print("WARNING: SKIPPING UNKNOWN TYPE", ffi.typeof(struct), file=sys.stderr) +for alias, target in sorted(pending_aliases.items()): + if target in emitted_structs: + print(f"{alias} = {target}") + print(""" LIGHTGRAY : Color GRAY : Color diff --git a/pyray/__init__.pyi b/pyray/__init__.pyi index e8e8acb..a7ef7a1 100644 --- a/pyray/__init__.pyi +++ b/pyray/__init__.pyi @@ -910,6 +910,222 @@ import _cffi_backend # type: ignore ffi: _cffi_backend.FFI PhysicsShapeType = int +# BEGIN GENERATED MODULE CONSTANTS +RL_ATTACHMENT_COLOR_CHANNEL0: int +RL_ATTACHMENT_COLOR_CHANNEL1: int +RL_ATTACHMENT_COLOR_CHANNEL2: int +RL_ATTACHMENT_COLOR_CHANNEL3: int +RL_ATTACHMENT_COLOR_CHANNEL4: int +RL_ATTACHMENT_COLOR_CHANNEL5: int +RL_ATTACHMENT_COLOR_CHANNEL6: int +RL_ATTACHMENT_COLOR_CHANNEL7: int +RL_ATTACHMENT_CUBEMAP_NEGATIVE_X: int +RL_ATTACHMENT_CUBEMAP_NEGATIVE_Y: int +RL_ATTACHMENT_CUBEMAP_NEGATIVE_Z: int +RL_ATTACHMENT_CUBEMAP_POSITIVE_X: int +RL_ATTACHMENT_CUBEMAP_POSITIVE_Y: int +RL_ATTACHMENT_CUBEMAP_POSITIVE_Z: int +RL_ATTACHMENT_DEPTH: int +RL_ATTACHMENT_RENDERBUFFER: int +RL_ATTACHMENT_STENCIL: int +RL_ATTACHMENT_TEXTURE2D: int +RL_BLEND_ADDITIVE: int +RL_BLEND_ADD_COLORS: int +RL_BLEND_ALPHA: int +RL_BLEND_ALPHA_PREMULTIPLY: int +RL_BLEND_COLOR: int +RL_BLEND_CUSTOM: int +RL_BLEND_CUSTOM_SEPARATE: int +RL_BLEND_DST_ALPHA: int +RL_BLEND_DST_RGB: int +RL_BLEND_EQUATION: int +RL_BLEND_EQUATION_ALPHA: int +RL_BLEND_EQUATION_RGB: int +RL_BLEND_MULTIPLIED: int +RL_BLEND_SRC_ALPHA: int +RL_BLEND_SRC_RGB: int +RL_BLEND_SUBTRACT_COLORS: int +RL_COMPUTE_SHADER: int +RL_CONSTANT_ALPHA: int +RL_CONSTANT_COLOR: int +RL_CULL_FACE_BACK: int +RL_CULL_FACE_FRONT: int +RL_DEFAULT_BATCH_BUFFERS: int +RL_DEFAULT_BATCH_BUFFER_ELEMENTS: int +RL_DEFAULT_BATCH_DRAWCALLS: int +RL_DEFAULT_BATCH_MAX_TEXTURE_UNITS: int +RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS: int +RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS: int +RL_DEFAULT_SHADER_ATTRIB_LOCATION_COLOR: int +RL_DEFAULT_SHADER_ATTRIB_LOCATION_INDICES: int +RL_DEFAULT_SHADER_ATTRIB_LOCATION_NORMAL: int +RL_DEFAULT_SHADER_ATTRIB_LOCATION_POSITION: int +RL_DEFAULT_SHADER_ATTRIB_LOCATION_TANGENT: int +RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD: int +RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD2: int +RL_DEFAULT_SHADER_ATTRIB_NAME_BONEIDS: str +RL_DEFAULT_SHADER_ATTRIB_NAME_BONEWEIGHTS: str +RL_DEFAULT_SHADER_ATTRIB_NAME_COLOR: str +RL_DEFAULT_SHADER_ATTRIB_NAME_NORMAL: str +RL_DEFAULT_SHADER_ATTRIB_NAME_POSITION: str +RL_DEFAULT_SHADER_ATTRIB_NAME_TANGENT: str +RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD: str +RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD2: str +RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE0: str +RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE1: str +RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE2: str +RL_DEFAULT_SHADER_UNIFORM_NAME_BONE_MATRICES: str +RL_DEFAULT_SHADER_UNIFORM_NAME_COLOR: str +RL_DEFAULT_SHADER_UNIFORM_NAME_MODEL: str +RL_DEFAULT_SHADER_UNIFORM_NAME_MVP: str +RL_DEFAULT_SHADER_UNIFORM_NAME_NORMAL: str +RL_DEFAULT_SHADER_UNIFORM_NAME_PROJECTION: str +RL_DEFAULT_SHADER_UNIFORM_NAME_VIEW: str +RL_DRAW_FRAMEBUFFER: int +RL_DST_ALPHA: int +RL_DST_COLOR: int +RL_DYNAMIC_COPY: int +RL_DYNAMIC_DRAW: int +RL_DYNAMIC_READ: int +RL_FLOAT: int +RL_FRAGMENT_SHADER: int +RL_FUNC_ADD: int +RL_FUNC_REVERSE_SUBTRACT: int +RL_FUNC_SUBTRACT: int +RL_LINES: int +RL_LOG_ALL: int +RL_LOG_DEBUG: int +RL_LOG_ERROR: int +RL_LOG_FATAL: int +RL_LOG_INFO: int +RL_LOG_NONE: int +RL_LOG_TRACE: int +RL_LOG_WARNING: int +RL_MAX: int +RL_MAX_MATRIX_STACK_SIZE: int +RL_MAX_SHADER_LOCATIONS: int +RL_MIN: int +RL_MODELVIEW: int +RL_ONE: int +RL_ONE_MINUS_CONSTANT_ALPHA: int +RL_ONE_MINUS_CONSTANT_COLOR: int +RL_ONE_MINUS_DST_ALPHA: int +RL_ONE_MINUS_DST_COLOR: int +RL_ONE_MINUS_SRC_ALPHA: int +RL_ONE_MINUS_SRC_COLOR: int +RL_OPENGL_11: int +RL_OPENGL_21: int +RL_OPENGL_33: int +RL_OPENGL_43: int +RL_OPENGL_ES_20: int +RL_OPENGL_ES_30: int +RL_PIXELFORMAT_COMPRESSED_DXT1_RGB: int +RL_PIXELFORMAT_COMPRESSED_DXT1_RGBA: int +RL_PIXELFORMAT_COMPRESSED_DXT3_RGBA: int +RL_PIXELFORMAT_COMPRESSED_DXT5_RGBA: int +RL_PIXELFORMAT_COMPRESSED_ETC1_RGB: int +RL_PIXELFORMAT_COMPRESSED_ETC2_EAC_RGBA: int +RL_PIXELFORMAT_COMPRESSED_ETC2_RGB: int +RL_PIXELFORMAT_COMPRESSED_PVRT_RGB: int +RL_PIXELFORMAT_COMPRESSED_PVRT_RGBA: int +RL_PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: int +RL_PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: int +RL_PIXELFORMAT_UNCOMPRESSED_R16: int +RL_PIXELFORMAT_UNCOMPRESSED_R16G16B16: int +RL_PIXELFORMAT_UNCOMPRESSED_R16G16B16A16: int +RL_PIXELFORMAT_UNCOMPRESSED_R32: int +RL_PIXELFORMAT_UNCOMPRESSED_R32G32B32: int +RL_PIXELFORMAT_UNCOMPRESSED_R32G32B32A32: int +RL_PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: int +RL_PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: int +RL_PIXELFORMAT_UNCOMPRESSED_R5G6B5: int +RL_PIXELFORMAT_UNCOMPRESSED_R8G8B8: int +RL_PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: int +RL_PROJECTION: int +RL_QUADS: int +RL_READ_FRAMEBUFFER: int +RL_SHADER_ATTRIB_FLOAT: int +RL_SHADER_ATTRIB_VEC2: int +RL_SHADER_ATTRIB_VEC3: int +RL_SHADER_ATTRIB_VEC4: int +RL_SHADER_LOC_COLOR_AMBIENT: int +RL_SHADER_LOC_COLOR_DIFFUSE: int +RL_SHADER_LOC_COLOR_SPECULAR: int +RL_SHADER_LOC_MAP_ALBEDO: int +RL_SHADER_LOC_MAP_BRDF: int +RL_SHADER_LOC_MAP_CUBEMAP: int +RL_SHADER_LOC_MAP_EMISSION: int +RL_SHADER_LOC_MAP_HEIGHT: int +RL_SHADER_LOC_MAP_IRRADIANCE: int +RL_SHADER_LOC_MAP_METALNESS: int +RL_SHADER_LOC_MAP_NORMAL: int +RL_SHADER_LOC_MAP_OCCLUSION: int +RL_SHADER_LOC_MAP_PREFILTER: int +RL_SHADER_LOC_MAP_ROUGHNESS: int +RL_SHADER_LOC_MATRIX_MODEL: int +RL_SHADER_LOC_MATRIX_MVP: int +RL_SHADER_LOC_MATRIX_NORMAL: int +RL_SHADER_LOC_MATRIX_PROJECTION: int +RL_SHADER_LOC_MATRIX_VIEW: int +RL_SHADER_LOC_VECTOR_VIEW: int +RL_SHADER_LOC_VERTEX_COLOR: int +RL_SHADER_LOC_VERTEX_NORMAL: int +RL_SHADER_LOC_VERTEX_POSITION: int +RL_SHADER_LOC_VERTEX_TANGENT: int +RL_SHADER_LOC_VERTEX_TEXCOORD01: int +RL_SHADER_LOC_VERTEX_TEXCOORD02: int +RL_SHADER_UNIFORM_FLOAT: int +RL_SHADER_UNIFORM_INT: int +RL_SHADER_UNIFORM_IVEC2: int +RL_SHADER_UNIFORM_IVEC3: int +RL_SHADER_UNIFORM_IVEC4: int +RL_SHADER_UNIFORM_SAMPLER2D: int +RL_SHADER_UNIFORM_UINT: int +RL_SHADER_UNIFORM_UIVEC2: int +RL_SHADER_UNIFORM_UIVEC3: int +RL_SHADER_UNIFORM_UIVEC4: int +RL_SHADER_UNIFORM_VEC2: int +RL_SHADER_UNIFORM_VEC3: int +RL_SHADER_UNIFORM_VEC4: int +RL_SRC_ALPHA: int +RL_SRC_ALPHA_SATURATE: int +RL_SRC_COLOR: int +RL_STATIC_COPY: int +RL_STATIC_DRAW: int +RL_STATIC_READ: int +RL_STREAM_COPY: int +RL_STREAM_DRAW: int +RL_STREAM_READ: int +RL_TEXTURE: int +RL_TEXTURE_FILTER_ANISOTROPIC: int +RL_TEXTURE_FILTER_ANISOTROPIC_16X: int +RL_TEXTURE_FILTER_ANISOTROPIC_4X: int +RL_TEXTURE_FILTER_ANISOTROPIC_8X: int +RL_TEXTURE_FILTER_BILINEAR: int +RL_TEXTURE_FILTER_LINEAR: int +RL_TEXTURE_FILTER_LINEAR_MIP_NEAREST: int +RL_TEXTURE_FILTER_MIP_LINEAR: int +RL_TEXTURE_FILTER_MIP_NEAREST: int +RL_TEXTURE_FILTER_NEAREST: int +RL_TEXTURE_FILTER_NEAREST_MIP_LINEAR: int +RL_TEXTURE_FILTER_POINT: int +RL_TEXTURE_FILTER_TRILINEAR: int +RL_TEXTURE_MAG_FILTER: int +RL_TEXTURE_MIN_FILTER: int +RL_TEXTURE_MIPMAP_BIAS_RATIO: int +RL_TEXTURE_WRAP_CLAMP: int +RL_TEXTURE_WRAP_MIRROR_CLAMP: int +RL_TEXTURE_WRAP_MIRROR_REPEAT: int +RL_TEXTURE_WRAP_REPEAT: int +RL_TEXTURE_WRAP_S: int +RL_TEXTURE_WRAP_T: int +RL_TRIANGLES: int +RL_UNSIGNED_BYTE: int +RL_VERTEX_SHADER: int +RL_ZERO: int +# END GENERATED MODULE CONSTANTS + + def attach_audio_mixed_processor(processor: Any,) -> None: """Attach audio stream processor to the entire audio pipeline, receives the samples as 'float'.""" ... @@ -4427,14 +4643,7 @@ class Texture: self.height:int = height # type: ignore self.mipmaps:int = mipmaps # type: ignore self.format:int = format # type: ignore -class Texture2D: - """It should be redesigned to be provided by user.""" - def __init__(self, id: int|None = None, width: int|None = None, height: int|None = None, mipmaps: int|None = None, format: int|None = None): - self.id:int = id # type: ignore - self.width:int = width # type: ignore - self.height:int = height # type: ignore - self.mipmaps:int = mipmaps # type: ignore - self.format:int = format # type: ignore +Texture2D = Texture class Transform: """Transform, vertex transformation data.""" def __init__(self, translation: Vector3|list|tuple|None = None, rotation: Vector4|list|tuple|None = None, scale: Vector3|list|tuple|None = None): @@ -4552,4 +4761,3 @@ BLACK : Color BLANK : Color MAGENTA : Color RAYWHITE : Color - diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 653a442..49c346b 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -1,17 +1,33 @@ #!/usr/bin/env bash -rm raylib pyray examples +set -euo pipefail + +cleanup() { + rm -f raylib pyray examples +} + +cleanup +trap cleanup EXIT + ln -s ../raylib ln -s ../pyray ln -s ../examples for FILE in *.py do - if python3 $FILE; then - echo $FILE returned true + if python3 "$FILE"; then + echo "$FILE returned true" else - echo $FILE returned some error - rm raylib pyray examples - exit + echo "$FILE returned some error" + exit 1 fi done -rm raylib pyray examples \ No newline at end of file + +# Execute pytest-style stub parity tests that do not run when files are executed directly. +python3 -m pytest test_pyray_stub_parity.py + +# Run typing smoke checks when ty is available, but do not require it. +if command -v ty >/dev/null 2>&1; then + ty check typing/pyray_stub_smoke.py +else + echo "Skipping typing smoke check: ty CLI is not installed." +fi diff --git a/tests/run_tests_dynamic.sh b/tests/run_tests_dynamic.sh index 1942448..d189a86 100755 --- a/tests/run_tests_dynamic.sh +++ b/tests/run_tests_dynamic.sh @@ -1,17 +1,42 @@ #!/usr/bin/env bash -rm raylib pyray examples +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" + +cd "${SCRIPT_DIR}" + +cleanup() { + rm -f raylib pyray examples +} + +cleanup +trap cleanup EXIT + ln -s ../dynamic/raylib ln -s ../dynamic/pyray ln -s ../examples for FILE in test_*.py do - if python3 $FILE; then - echo $FILE returned true + if python3 "$FILE"; then + echo "$FILE returned true" else - echo $FILE returned some error - rm raylib pyray examples - exit + echo "$FILE returned some error" + exit 1 fi done -rm raylib pyray examples \ No newline at end of file + +# Execute pytest-style stub parity tests that do not run when files are executed directly. +PYRAY_STUB_PATH="${REPO_ROOT}/dynamic/pyray/__init__.pyi" \ + python3 -m pytest test_pyray_stub_parity.py + +# Run typing smoke checks when ty is available, but do not require it. +if command -v ty >/dev/null 2>&1; then + ty check \ + --project "${REPO_ROOT}/dynamic" \ + --extra-search-path "${REPO_ROOT}/dynamic" \ + typing/pyray_stub_smoke.py +else + echo "Skipping typing smoke check: ty CLI is not installed." +fi diff --git a/tests/test_pyray_stub_parity.py b/tests/test_pyray_stub_parity.py new file mode 100644 index 0000000..889d219 --- /dev/null +++ b/tests/test_pyray_stub_parity.py @@ -0,0 +1,76 @@ +from pathlib import Path +import ast +import os + + +ROOT = Path(__file__).resolve().parents[1] +_DEFAULT_PYRAY_STUB = ROOT / "pyray" / "__init__.pyi" +_stub_override = os.environ.get("PYRAY_STUB_PATH") +PYRAY_STUB = ( + Path(_stub_override).expanduser() + if _stub_override + else _DEFAULT_PYRAY_STUB +) +if not PYRAY_STUB.is_absolute(): + PYRAY_STUB = ROOT / PYRAY_STUB + + +def _parse_stub() -> ast.Module: + return ast.parse(PYRAY_STUB.read_text(encoding="utf-8")) + + +def _top_level_names(module: ast.Module) -> set[str]: + names: set[str] = set() + for node in module.body: + if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): + names.add(node.target.id) + elif isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name): + names.add(target.id) + return names + + +def test_stub_includes_expected_smoke_constants() -> None: + module_names = _top_level_names(_parse_stub()) + required_names = { + "RL_FUNC_ADD", + "RL_ONE", + "RL_ONE_MINUS_SRC_ALPHA", + "RL_QUADS", + "RL_SRC_ALPHA", + "RL_ZERO", + } + assert required_names.issubset(module_names) + + +def test_stub_excludes_deprecated_shorthand_constants() -> None: + module_names = _top_level_names(_parse_stub()) + omitted_names = { + "ARROWS_SIZE", + "BLEND_ALPHA", + "MOUSE_BUTTON_LEFT", + "SHADER_LOC_MATRIX_MVP", + "SHADER_UNIFORM_FLOAT", + "TEXTURE_FILTER_POINT", + } + assert omitted_names.isdisjoint(module_names) + + +def test_texture2d_is_a_texture_alias() -> None: + module = _parse_stub() + class_names = { + node.name for node in module.body if isinstance(node, ast.ClassDef) + } + aliases: dict[str, str] = {} + for node in module.body: + if not isinstance(node, ast.Assign): + continue + if len(node.targets) != 1: + continue + target = node.targets[0] + if isinstance(target, ast.Name) and isinstance(node.value, ast.Name): + aliases[target.id] = node.value.id + + assert aliases.get("Texture2D") == "Texture" + assert "Texture2D" not in class_names diff --git a/tests/typing/pyray_stub_smoke.py b/tests/typing/pyray_stub_smoke.py new file mode 100644 index 0000000..835dff8 --- /dev/null +++ b/tests/typing/pyray_stub_smoke.py @@ -0,0 +1,16 @@ +import pyray as rl + + +blend: int = rl.BlendMode.BLEND_ALPHA +mode: int = rl.RL_QUADS +src_alpha: int = rl.RL_SRC_ALPHA +mouse: int = rl.MouseButton.MOUSE_BUTTON_LEFT +flt: int = rl.TextureFilter.TEXTURE_FILTER_POINT + + +def accepts_texture(texture: rl.Texture | None) -> None: + return None + + +tex2d: rl.Texture2D | None = None +accepts_texture(tex2d)