pyalsoft 0.1.0__py3-none-win_amd64.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.
pyalsoft/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ """Generated raw bindings and runtime loading for OpenAL Soft."""
2
+
3
+ from pyalsoft import registry
4
+ from pyalsoft._extensions import Extension
5
+ from pyalsoft._generated import constants, enums, types
6
+ from pyalsoft._generated.constants import * # noqa: F403
7
+ from pyalsoft._generated.enums import * # noqa: F403
8
+ from pyalsoft._generated.extensions import ExtensionCapabilities
9
+ from pyalsoft._generated.objects import (
10
+ AuxiliaryEffectSlot,
11
+ Buffer,
12
+ Effect,
13
+ Filter,
14
+ Listener,
15
+ Source,
16
+ )
17
+ from pyalsoft._generated.types import * # noqa: F403
18
+ from pyalsoft._library import (
19
+ ContextRequiredError,
20
+ ExtensionUnavailableError,
21
+ ForeignFunction,
22
+ FunctionUnavailableError,
23
+ LibraryNotFoundError,
24
+ LibraryPath,
25
+ OpenALError,
26
+ OpenALLibrary,
27
+ load,
28
+ )
29
+
30
+ __all__ = [
31
+ *constants.__all__,
32
+ *enums.__all__,
33
+ *types.__all__,
34
+ "constants",
35
+ "enums",
36
+ "registry",
37
+ "types",
38
+ "AuxiliaryEffectSlot",
39
+ "Buffer",
40
+ "ContextRequiredError",
41
+ "Effect",
42
+ "Extension",
43
+ "ExtensionCapabilities",
44
+ "ExtensionUnavailableError",
45
+ "ForeignFunction",
46
+ "Filter",
47
+ "FunctionUnavailableError",
48
+ "LibraryNotFoundError",
49
+ "LibraryPath",
50
+ "Listener",
51
+ "OpenALError",
52
+ "OpenALLibrary",
53
+ "Source",
54
+ "load",
55
+ ]
pyalsoft/_api.py ADDED
@@ -0,0 +1,302 @@
1
+ """Python-value command marshalling over the generated ctypes layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import TYPE_CHECKING, Any, cast
8
+
9
+ from pyalsoft._generated import enums as _enums
10
+ from pyalsoft._generated import types as _types
11
+ from pyalsoft._generated.semantics import (
12
+ COMMAND_WRAPPERS_BY_NAME,
13
+ ENUM_GROUPS_BY_NAME,
14
+ )
15
+ from pyalsoft._specs import CommandWrapperSpec, WrapperParameterSpec
16
+
17
+ if TYPE_CHECKING:
18
+ from pyalsoft._library import OpenALLibrary
19
+
20
+ type ReadableBuffer = bytes | bytearray | memoryview
21
+
22
+
23
+ def _c_type_parts(c_type: str) -> tuple[str, int, bool]:
24
+ tokens = c_type.replace("*", " * ").split()
25
+ is_const = "const" in tokens
26
+ words = [token for token in tokens if token not in {"const", "*"}]
27
+ return " ".join(words), tokens.count("*"), is_const
28
+
29
+
30
+ def _is_character(base: str) -> bool:
31
+ return base in {"char", "ALchar", "ALCchar"}
32
+
33
+
34
+ def _is_void(base: str) -> bool:
35
+ return base in {"void", "ALvoid", "ALCvoid"}
36
+
37
+
38
+ def _ctype_for(base: str) -> Any:
39
+ if _is_character(base):
40
+ return ctypes.c_char
41
+ if _is_void(base):
42
+ return ctypes.c_void_p
43
+ name = base.removeprefix("struct ")
44
+ try:
45
+ return getattr(_types, name)
46
+ except AttributeError as error:
47
+ raise TypeError(f"no generated ctypes type for {base!r}") from error
48
+
49
+
50
+ def _object_identifier(value: object) -> object:
51
+ identifier = getattr(value, "identifier", None)
52
+ return identifier if isinstance(identifier, int) else value
53
+
54
+
55
+ def _encode_string(value: object, name: str) -> bytes | None:
56
+ if value is None:
57
+ return None
58
+ if isinstance(value, bytes):
59
+ return value
60
+ if isinstance(value, str):
61
+ return value.encode("utf-8")
62
+ raise TypeError(f"{name} must be str, bytes, or None")
63
+
64
+
65
+ def _buffer_input(value: object, *, writable: bool) -> tuple[object, int | None]:
66
+ if isinstance(value, bytes):
67
+ data = (ctypes.c_ubyte * len(value)).from_buffer_copy(value)
68
+ return data, len(value)
69
+ if isinstance(value, bytearray):
70
+ array_type = ctypes.c_ubyte * len(value)
71
+ data = (
72
+ array_type.from_buffer(value)
73
+ if writable
74
+ else array_type.from_buffer_copy(value)
75
+ )
76
+ return data, len(value)
77
+ if isinstance(value, memoryview):
78
+ contiguous = value.cast("B")
79
+ array_type = ctypes.c_ubyte * contiguous.nbytes
80
+ if writable and not contiguous.readonly:
81
+ data = array_type.from_buffer(contiguous)
82
+ else:
83
+ data = array_type.from_buffer_copy(contiguous)
84
+ return data, contiguous.nbytes
85
+ return value, None
86
+
87
+
88
+ def _prepare_input(
89
+ parameter: WrapperParameterSpec, value: object
90
+ ) -> tuple[object, int | None]:
91
+ base, pointer_depth, _is_const = _c_type_parts(parameter.c_type)
92
+ value = _object_identifier(value)
93
+ if pointer_depth == 0:
94
+ if base in {"ALboolean", "ALCboolean"}:
95
+ return (b"\x01" if bool(value) else b"\x00"), None
96
+ return value, None
97
+
98
+ if base in {"ALCdevice", "ALCcontext"} and pointer_depth == 1:
99
+ return value, None
100
+ if _is_character(base):
101
+ encoded = _encode_string(value, parameter.python_name)
102
+ return encoded, len(encoded) if encoded is not None else 0
103
+ if _is_void(base) or base.startswith("struct "):
104
+ return _buffer_input(value, writable=parameter.direction == "inout")
105
+ if value is None:
106
+ return None, 0
107
+ if isinstance(value, (ctypes.Array, ctypes._Pointer)):
108
+ return value, None
109
+ if not isinstance(value, Sequence):
110
+ raise TypeError(f"{parameter.python_name} must be a sequence")
111
+ values = list(value)
112
+ if parameter.name in {"attrlist", "attribs"} and (
113
+ not values or int(values[-1]) != 0
114
+ ):
115
+ values.append(0)
116
+ ctype = _ctype_for(base)
117
+ array = (ctype * len(values))(*values)
118
+ return array, len(values)
119
+
120
+
121
+ def _output_length(
122
+ parameter: WrapperParameterSpec,
123
+ values: Mapping[str, object],
124
+ derived_lengths: Mapping[str, int],
125
+ result_size: int,
126
+ ) -> int:
127
+ if parameter.length is None:
128
+ return result_size
129
+ if parameter.length.isdigit():
130
+ return int(parameter.length)
131
+ raw = values.get(parameter.length, derived_lengths.get(parameter.length))
132
+ if raw is None:
133
+ raise TypeError(
134
+ f"cannot determine output length {parameter.length!r} for "
135
+ f"{parameter.name!r}"
136
+ )
137
+ return int(cast(Any, raw))
138
+
139
+
140
+ def _allocate_output(
141
+ parameter: WrapperParameterSpec,
142
+ values: Mapping[str, object],
143
+ derived_lengths: Mapping[str, int],
144
+ result_size: int,
145
+ ) -> ctypes.Array[Any]:
146
+ base, _pointer_depth, _is_const = _c_type_parts(parameter.c_type)
147
+ length = _output_length(parameter, values, derived_lengths, result_size)
148
+ if length < 0:
149
+ raise ValueError(
150
+ f"output length for {parameter.python_name} cannot be negative"
151
+ )
152
+ return cast(ctypes.Array[Any], (_ctype_for(base) * length)())
153
+
154
+
155
+ def _enum_result(namespace: str, group: str | None, value: int) -> object:
156
+ if group is None:
157
+ return value
158
+ spec = ENUM_GROUPS_BY_NAME.get((namespace, group))
159
+ if spec is None:
160
+ return value
161
+ enum_type = cast(type[Any], getattr(_enums, spec.python_name))
162
+ try:
163
+ return enum_type(value)
164
+ except ValueError:
165
+ return value
166
+
167
+
168
+ def _convert_scalar(base: str, value: object) -> object:
169
+ raw = getattr(value, "value", value)
170
+ if base in {"ALboolean", "ALCboolean"}:
171
+ if isinstance(raw, bytes):
172
+ return raw != b"\x00"
173
+ return bool(raw)
174
+ if _is_character(base):
175
+ if raw is None:
176
+ return None
177
+ if isinstance(raw, bytes):
178
+ return raw.decode("utf-8")
179
+ return raw
180
+
181
+
182
+ def _convert_output(
183
+ parameter: WrapperParameterSpec,
184
+ wrapper: CommandWrapperSpec,
185
+ allocation: ctypes.Array[Any],
186
+ ) -> object:
187
+ base, _pointer_depth, _is_const = _c_type_parts(parameter.c_type)
188
+ if _is_character(base):
189
+ return bytes(allocation).split(b"\x00", 1)[0].decode("utf-8")
190
+ converted = [
191
+ _enum_result(
192
+ wrapper.namespace,
193
+ parameter.group,
194
+ cast(int, _convert_scalar(base, item)),
195
+ )
196
+ if parameter.group is not None
197
+ else _convert_scalar(base, item)
198
+ for item in allocation
199
+ ]
200
+ is_vector = (parameter.length is not None and parameter.length != "1") or (
201
+ parameter.length is None and wrapper.result_size
202
+ )
203
+ return tuple(converted) if is_vector else converted[0]
204
+
205
+
206
+ def _convert_return(wrapper: CommandWrapperSpec, value: object) -> object:
207
+ base, pointer_depth, _is_const = _c_type_parts(wrapper.return_type)
208
+ if base == "void" and pointer_depth == 0:
209
+ return None
210
+ if pointer_depth and _is_character(base):
211
+ if value is None:
212
+ return None
213
+ return cast(bytes, value).decode("utf-8")
214
+ converted = _convert_scalar(base, value)
215
+ if wrapper.return_group is not None:
216
+ return _enum_result(
217
+ wrapper.namespace, wrapper.return_group, cast(int, converted)
218
+ )
219
+ return converted
220
+
221
+
222
+ class CommandNamespace:
223
+ """Base class used by generated AL and ALC Python command namespaces."""
224
+
225
+ def __init__(self, library: OpenALLibrary) -> None:
226
+ self._library = library
227
+
228
+ @property
229
+ def library(self) -> OpenALLibrary:
230
+ """The raw library underlying this namespace."""
231
+
232
+ return self._library
233
+
234
+ def _invoke(
235
+ self,
236
+ name: str,
237
+ values: Mapping[str, object],
238
+ *,
239
+ result_size: int = 1,
240
+ resolution_device: object | None = None,
241
+ ) -> object:
242
+ if result_size < 1:
243
+ raise ValueError("result_size must be at least one")
244
+ wrapper = COMMAND_WRAPPERS_BY_NAME[name]
245
+ prepared: dict[str, object] = {}
246
+ derived_lengths: dict[str, int] = {}
247
+ for parameter in wrapper.parameters:
248
+ if parameter.direction == "out" or not parameter.visible:
249
+ continue
250
+ argument, length = _prepare_input(parameter, values[parameter.name])
251
+ prepared[parameter.name] = argument
252
+ if parameter.length is not None and not parameter.length.isdigit():
253
+ if length is None:
254
+ continue
255
+ previous = derived_lengths.get(parameter.length)
256
+ if previous is not None and previous != length:
257
+ raise ValueError(
258
+ f"inputs controlled by {parameter.length!r} have "
259
+ "different lengths"
260
+ )
261
+ derived_lengths[parameter.length] = length
262
+
263
+ arguments: list[object] = []
264
+ outputs: list[tuple[WrapperParameterSpec, ctypes.Array[Any]]] = []
265
+ for parameter in wrapper.parameters:
266
+ if parameter.direction == "out":
267
+ allocation = _allocate_output(
268
+ parameter, values, derived_lengths, result_size
269
+ )
270
+ outputs.append((parameter, allocation))
271
+ arguments.append(allocation)
272
+ elif parameter.visible:
273
+ arguments.append(prepared[parameter.name])
274
+ else:
275
+ try:
276
+ arguments.append(derived_lengths[parameter.name])
277
+ except KeyError as error:
278
+ raise TypeError(
279
+ f"cannot infer hidden parameter {parameter.name!r}"
280
+ ) from error
281
+
282
+ device = (
283
+ resolution_device if resolution_device is not None else values.get("device")
284
+ )
285
+ function = self._library.get_function(name, device=device)
286
+ native_result = function(*arguments)
287
+ results: list[object] = []
288
+ base, pointer_depth, _is_const = _c_type_parts(wrapper.return_type)
289
+ if not (base == "void" and pointer_depth == 0):
290
+ results.append(_convert_return(wrapper, native_result))
291
+ results.extend(
292
+ _convert_output(parameter, wrapper, allocation)
293
+ for parameter, allocation in outputs
294
+ )
295
+ if not results:
296
+ return None
297
+ if len(results) == 1:
298
+ return results[0]
299
+ return tuple(results)
300
+
301
+
302
+ __all__ = ["CommandNamespace", "ReadableBuffer"]
@@ -0,0 +1,130 @@
1
+ """Runtime support for generated OpenAL extension capability objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Mapping
6
+ from typing import TYPE_CHECKING
7
+
8
+ from pyalsoft._generated.registry import API_SETS
9
+ from pyalsoft._specs import ApiSetSpec
10
+
11
+ if TYPE_CHECKING:
12
+ from pyalsoft._library import ForeignFunction, OpenALLibrary
13
+
14
+ _EXTENSIONS = {item.name: item for item in API_SETS if item.kind == "extension"}
15
+
16
+
17
+ class Extension:
18
+ """Capabilities and runtime access for one registry extension."""
19
+
20
+ def __init__(self, library: OpenALLibrary, spec: ApiSetSpec) -> None:
21
+ self.library = library
22
+ self.spec = spec
23
+
24
+ @property
25
+ def name(self) -> str:
26
+ return self.spec.name
27
+
28
+ @property
29
+ def apis(self) -> tuple[str, ...]:
30
+ return self.spec.apis
31
+
32
+ @property
33
+ def commands(self) -> tuple[str, ...]:
34
+ return tuple(
35
+ dict.fromkeys(
36
+ member.name
37
+ for requirement in self.spec.requirements
38
+ for member in requirement.members
39
+ if member.kind == "command"
40
+ )
41
+ )
42
+
43
+ @property
44
+ def enums(self) -> tuple[str, ...]:
45
+ return tuple(
46
+ dict.fromkeys(
47
+ member.name
48
+ for requirement in self.spec.requirements
49
+ for member in requirement.members
50
+ if member.kind == "enum"
51
+ )
52
+ )
53
+
54
+ @property
55
+ def types(self) -> tuple[str, ...]:
56
+ return tuple(
57
+ dict.fromkeys(
58
+ member.name
59
+ for requirement in self.spec.requirements
60
+ for member in requirement.members
61
+ if member.kind == "type"
62
+ )
63
+ )
64
+
65
+ @property
66
+ def dependencies(self) -> tuple[str, ...]:
67
+ return tuple(
68
+ dict.fromkeys(
69
+ requirement.depends
70
+ for requirement in self.spec.requirements
71
+ if requirement.depends is not None
72
+ )
73
+ )
74
+
75
+ def is_present(self, device: object | None = None) -> bool:
76
+ """Check whether the extension is available for a context or device."""
77
+
78
+ if self.name.startswith("ALC_") or (device is not None and "alc" in self.apis):
79
+ return self.library.is_alc_extension_present(self.name, device)
80
+ return self.library.is_al_extension_present(self.name)
81
+
82
+ def require(self, device: object | None = None) -> None:
83
+ """Raise :class:`ExtensionUnavailableError` when unavailable."""
84
+
85
+ if self.is_present(device):
86
+ return
87
+ from pyalsoft._library import ExtensionUnavailableError
88
+
89
+ raise ExtensionUnavailableError(
90
+ f"OpenAL extension {self.name!r} is not available"
91
+ )
92
+
93
+ def get_function(
94
+ self, name: str, *, device: object | None = None
95
+ ) -> ForeignFunction:
96
+ """Resolve a command declared by this extension."""
97
+
98
+ if name not in self.commands:
99
+ raise KeyError(f"{name!r} is not declared by {self.name}")
100
+ return self.library.get_function(name, device=device)
101
+
102
+ def __repr__(self) -> str:
103
+ return f"Extension(name={self.name!r}, apis={self.apis!r})"
104
+
105
+
106
+ class ExtensionNamespace(Mapping[str, Extension]):
107
+ """Mapping base used by the generated extension capability namespace."""
108
+
109
+ def __init__(self, library: OpenALLibrary) -> None:
110
+ self.library = library
111
+ self._cache: dict[str, Extension] = {}
112
+
113
+ def _get(self, name: str) -> Extension:
114
+ extension = self._cache.get(name)
115
+ if extension is None:
116
+ extension = Extension(self.library, _EXTENSIONS[name])
117
+ self._cache[name] = extension
118
+ return extension
119
+
120
+ def __getitem__(self, name: str) -> Extension:
121
+ return self._get(name)
122
+
123
+ def __iter__(self) -> Iterator[str]:
124
+ return iter(_EXTENSIONS)
125
+
126
+ def __len__(self) -> int:
127
+ return len(_EXTENSIONS)
128
+
129
+
130
+ __all__ = ["Extension", "ExtensionNamespace"]