From fecffd01df8af15458c2b5c9096d25db7b09dc1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sat, 18 Jul 2026 20:36:10 +0200 Subject: [PATCH] Fix pylint for bink 0.7.1 --- AGENTS.md | 15 +++ bink/__init__.py | 26 +++- bink/_ffi.py | 112 ++++++++++++---- bink/choices.py | 40 ++++-- bink/story.py | 338 ++++++++++++++++++++++++++++++++++++++--------- bink/tags.py | 22 ++- bink/value.py | 136 +++++++++++++++---- setup.cfg | 2 +- 8 files changed, 549 insertions(+), 142 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6d46124 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# Contribution checks + +For every change to Python code, do not consider the task complete until both +commands pass from the repository root: + +```sh +pylint $(git ls-files 'bink/*.py') +python -m unittest discover -v +``` + +Fix pylint findings in the code; do not silence them with broad disables or +configuration changes unless the warning is demonstrably inapplicable. + +Do not create, update, or publish a release while either check fails. Run the +same checks again immediately before committing or handing off a release. diff --git a/bink/__init__.py b/bink/__init__.py index 578ce92..48d8163 100644 --- a/bink/__init__.py +++ b/bink/__init__.py @@ -1,11 +1,23 @@ """Python bindings for the Blade Ink C API.""" -from ._ffi import (BINK_ERROR_ERROR, BINK_ERROR_WARNING, BINK_FAIL, - BINK_FAIL_INVALID_ARGUMENT, BINK_FAIL_INVALID_UTF8, - BINK_FAIL_NUL_BYTE, BINK_FAIL_NULL_POINTER, - BINK_FAIL_PANIC, BINK_OK, BINK_VALUE_BOOL, - BINK_VALUE_DIVERT_TARGET, BINK_VALUE_FLOAT, BINK_VALUE_INT, - BINK_VALUE_LIST, BINK_VALUE_STRING, - BINK_VALUE_VARIABLE_POINTER, LIB) +from ._ffi import ( + BINK_ERROR_ERROR, + BINK_ERROR_WARNING, + BINK_FAIL, + BINK_FAIL_INVALID_ARGUMENT, + BINK_FAIL_INVALID_UTF8, + BINK_FAIL_NUL_BYTE, + BINK_FAIL_NULL_POINTER, + BINK_FAIL_PANIC, + BINK_OK, + BINK_VALUE_BOOL, + BINK_VALUE_DIVERT_TARGET, + BINK_VALUE_FLOAT, + BINK_VALUE_INT, + BINK_VALUE_LIST, + BINK_VALUE_STRING, + BINK_VALUE_VARIABLE_POINTER, + LIB, +) __all__ = [name for name in globals() if name.startswith("BINK_")] + ["LIB"] diff --git a/bink/_ffi.py b/bink/_ffi.py index aecaac6..e97f00b 100644 --- a/bink/_ffi.py +++ b/bink/_ffi.py @@ -1,12 +1,29 @@ """Low-level ctypes declarations shared by the public wrappers.""" + import ctypes import ctypes.util import os import platform -BINK_OK, BINK_FAIL, BINK_FAIL_NULL_POINTER, BINK_FAIL_INVALID_UTF8, BINK_FAIL_NUL_BYTE, BINK_FAIL_PANIC, BINK_FAIL_INVALID_ARGUMENT = range(7) +( + BINK_OK, + BINK_FAIL, + BINK_FAIL_NULL_POINTER, + BINK_FAIL_INVALID_UTF8, + BINK_FAIL_NUL_BYTE, + BINK_FAIL_PANIC, + BINK_FAIL_INVALID_ARGUMENT, +) = range(7) BINK_ERROR_WARNING, BINK_ERROR_ERROR = range(2) -BINK_VALUE_BOOL, BINK_VALUE_INT, BINK_VALUE_FLOAT, BINK_VALUE_STRING, BINK_VALUE_LIST, BINK_VALUE_DIVERT_TARGET, BINK_VALUE_VARIABLE_POINTER = range(7) +( + BINK_VALUE_BOOL, + BINK_VALUE_INT, + BINK_VALUE_FLOAT, + BINK_VALUE_STRING, + BINK_VALUE_LIST, + BINK_VALUE_DIVERT_TARGET, + BINK_VALUE_VARIABLE_POINTER, +) = range(7) def _load_library(): @@ -29,6 +46,7 @@ def _load_library(): def _declare(name, args, restype=ctypes.c_int): + """Set ctypes argument and result types for an exported C function.""" fn = getattr(LIB, name) fn.argtypes, fn.restype = args, restype @@ -36,47 +54,91 @@ def _declare(name, args, restype=ctypes.c_int): for _name, _args in { "bink_story_new": [ctypes.POINTER(P), ctypes.c_char_p, CP], "bink_story_can_continue": [P, ctypes.POINTER(ctypes.c_bool), CP], - "bink_story_cont": [P, CP, CP], "bink_story_continue_maximally": [P, CP, CP], + "bink_story_cont": [P, CP, CP], + "bink_story_continue_maximally": [P, CP, CP], "bink_story_continue_async": [P, ctypes.c_float, ctypes.POINTER(ctypes.c_bool), CP], "bink_story_get_current_text": [P, CP, CP], "bink_story_get_current_choices": [P, ctypes.POINTER(P), ctypes.POINTER(SZ), CP], "bink_story_choose_choice_index": [P, SZ, CP], "bink_story_get_current_tags": [P, ctypes.POINTER(P), ctypes.POINTER(SZ), CP], "bink_story_get_global_tags": [P, ctypes.POINTER(P), ctypes.POINTER(SZ), CP], - "bink_story_get_tags_for_content_at_path": [P, ctypes.c_char_p, ctypes.POINTER(P), ctypes.POINTER(SZ), CP], + "bink_story_get_tags_for_content_at_path": [ + P, + ctypes.c_char_p, + ctypes.POINTER(P), + ctypes.POINTER(SZ), + CP, + ], "bink_story_choose_path_string": [P, ctypes.c_char_p, CP], - "bink_story_choose_path_string_with_args": [P, ctypes.c_char_p, ctypes.c_bool, P, CP], + "bink_story_choose_path_string_with_args": [ + P, + ctypes.c_char_p, + ctypes.c_bool, + P, + CP, + ], "bink_story_evaluate_function": [P, ctypes.c_char_p, P, ctypes.POINTER(P), CP, CP], - "bink_story_load_state": [P, ctypes.c_char_p, CP], "bink_story_save_state": [P, CP, CP], + "bink_story_load_state": [P, ctypes.c_char_p, CP], + "bink_story_save_state": [P, CP, CP], "bink_story_reset_state": [P, CP], - "bink_story_get_visit_count_at_path_string": [P, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int32), CP], - "bink_story_get_current_path": [P, CP, CP], "bink_story_build_string_of_hierarchy": [P, CP, CP], - "bink_choices_get_text": [P, SZ, CP, CP], "bink_choices_get_tags": [P, SZ, ctypes.POINTER(P), ctypes.POINTER(SZ), CP], + "bink_story_get_visit_count_at_path_string": [ + P, + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_int32), + CP, + ], + "bink_story_get_current_path": [P, CP, CP], + "bink_story_build_string_of_hierarchy": [P, CP, CP], + "bink_choices_get_text": [P, SZ, CP, CP], + "bink_choices_get_tags": [P, SZ, ctypes.POINTER(P), ctypes.POINTER(SZ), CP], "bink_tags_get": [P, SZ, CP, CP], - "bink_value_new_bool": [ctypes.c_bool, ctypes.POINTER(P), CP], "bink_value_new_int": [ctypes.c_int32, ctypes.POINTER(P), CP], - "bink_value_new_float": [ctypes.c_float, ctypes.POINTER(P), CP], "bink_value_new_string": [ctypes.c_char_p, ctypes.POINTER(P), CP], - "bink_value_get_bool": [P, ctypes.POINTER(ctypes.c_bool), CP], "bink_value_get_int": [P, ctypes.POINTER(ctypes.c_int32), CP], - "bink_value_get_float": [P, ctypes.POINTER(ctypes.c_float), CP], "bink_value_get_string": [P, CP, CP], + "bink_value_new_bool": [ctypes.c_bool, ctypes.POINTER(P), CP], + "bink_value_new_int": [ctypes.c_int32, ctypes.POINTER(P), CP], + "bink_value_new_float": [ctypes.c_float, ctypes.POINTER(P), CP], + "bink_value_new_string": [ctypes.c_char_p, ctypes.POINTER(P), CP], + "bink_value_get_bool": [P, ctypes.POINTER(ctypes.c_bool), CP], + "bink_value_get_int": [P, ctypes.POINTER(ctypes.c_int32), CP], + "bink_value_get_float": [P, ctypes.POINTER(ctypes.c_float), CP], + "bink_value_get_string": [P, CP, CP], "bink_value_get_kind": [P, ctypes.POINTER(ctypes.c_int), CP], - "bink_value_array_new": [ctypes.POINTER(P), CP], "bink_value_array_push": [P, P, CP], - "bink_list_new": [ctypes.POINTER(P), CP], "bink_story_list_new_from_origin": [P, ctypes.c_char_p, ctypes.POINTER(P), CP], - "bink_story_list_new_from_item": [P, ctypes.c_char_p, ctypes.POINTER(P), CP], "bink_list_add_item": [P, ctypes.c_char_p, ctypes.c_int32, CP], - "bink_list_get_count": [P, ctypes.POINTER(SZ), CP], "bink_list_get_item": [P, SZ, CP, ctypes.POINTER(ctypes.c_int32), CP], - "bink_list_get_origin_count": [P, ctypes.POINTER(SZ), CP], "bink_list_get_origin": [P, SZ, CP, CP], - "bink_value_new_list": [P, ctypes.POINTER(P), CP], "bink_value_get_list": [P, ctypes.POINTER(P), CP], - "bink_var_get": [P, ctypes.c_char_p, ctypes.POINTER(P), CP], "bink_var_set": [P, ctypes.c_char_p, P, CP], - "bink_story_switch_flow": [P, ctypes.c_char_p, CP], "bink_story_remove_flow": [P, ctypes.c_char_p, CP], - "bink_story_switch_to_default_flow": [P, CP], "bink_story_set_allow_external_function_fallbacks": [P, ctypes.c_bool, CP], + "bink_value_array_new": [ctypes.POINTER(P), CP], + "bink_value_array_push": [P, P, CP], + "bink_list_new": [ctypes.POINTER(P), CP], + "bink_story_list_new_from_origin": [P, ctypes.c_char_p, ctypes.POINTER(P), CP], + "bink_story_list_new_from_item": [P, ctypes.c_char_p, ctypes.POINTER(P), CP], + "bink_list_add_item": [P, ctypes.c_char_p, ctypes.c_int32, CP], + "bink_list_get_count": [P, ctypes.POINTER(SZ), CP], + "bink_list_get_item": [P, SZ, CP, ctypes.POINTER(ctypes.c_int32), CP], + "bink_list_get_origin_count": [P, ctypes.POINTER(SZ), CP], + "bink_list_get_origin": [P, SZ, CP, CP], + "bink_value_new_list": [P, ctypes.POINTER(P), CP], + "bink_value_get_list": [P, ctypes.POINTER(P), CP], + "bink_var_get": [P, ctypes.c_char_p, ctypes.POINTER(P), CP], + "bink_var_set": [P, ctypes.c_char_p, P, CP], + "bink_story_switch_flow": [P, ctypes.c_char_p, CP], + "bink_story_remove_flow": [P, ctypes.c_char_p, CP], + "bink_story_switch_to_default_flow": [P, CP], + "bink_story_set_allow_external_function_fallbacks": [P, ctypes.c_bool, CP], "bink_unbind_external_function": [P, ctypes.c_char_p, CP], - "bink_fun_args_count": [P, ctypes.POINTER(SZ), CP], "bink_fun_args_get": [P, SZ, ctypes.POINTER(P), CP], + "bink_fun_args_count": [P, ctypes.POINTER(SZ), CP], + "bink_fun_args_get": [P, SZ, ctypes.POINTER(P), CP], }.items(): _declare(_name, _args) -for _name, _args in {"bink_story_free": [P], "bink_choices_free": [P], "bink_tags_free": [P], "bink_value_free": [P], "bink_value_array_free": [P], "bink_list_free": [P], "bink_cstring_free": [ctypes.c_char_p]}.items(): +for _name, _args in { + "bink_story_free": [P], + "bink_choices_free": [P], + "bink_tags_free": [P], + "bink_value_free": [P], + "bink_value_array_free": [P], + "bink_list_free": [P], + "bink_cstring_free": [ctypes.c_char_p], +}.items(): _declare(_name, _args, None) def check(result, error): + """Raise the FFI error returned by a non-success status.""" if result == BINK_OK: return message = error.value.decode("utf-8") if error.value else "Blade Ink FFI error" @@ -86,11 +148,13 @@ def check(result, error): def call(name, *args): + """Invoke an FFI function that receives a trailing error-message output.""" error = ctypes.c_char_p() check(getattr(LIB, name)(*args, ctypes.byref(error)), error) def take_string(value): + """Decode and free an FFI-owned C string.""" try: return value.value.decode("utf-8") if value.value else "" finally: diff --git a/bink/choices.py b/bink/choices.py index 8a95f89..92e86da 100644 --- a/bink/choices.py +++ b/bink/choices.py @@ -1,4 +1,5 @@ """Choice collections returned by a story.""" + import ctypes from ._ffi import LIB, call, take_string @@ -7,27 +8,48 @@ class Choices: """An owned sequence of choice text, with access to each choice's tags.""" + def __init__(self, pointer, length): self._choices, self._len = pointer, length - def __len__(self): return self._len - def __bool__(self): return bool(self._len) - def __iter__(self): return (self[index] for index in range(self._len)) + def __len__(self): + return self._len + + def __bool__(self): + return bool(self._len) + + def __iter__(self): + return (self[index] for index in range(self._len)) def _index(self, index): - if not isinstance(index, int): raise TypeError("choice index must be an integer") - if index < 0: index += self._len - if not 0 <= index < self._len: raise IndexError("choice index out of range") + if not isinstance(index, int): + raise TypeError("choice index must be an integer") + if index < 0: + index += self._len + if not 0 <= index < self._len: + raise IndexError("choice index out of range") return index def __getitem__(self, index): value = ctypes.c_char_p() - call("bink_choices_get_text", self._choices, self._index(index), ctypes.byref(value)) + call( + "bink_choices_get_text", + self._choices, + self._index(index), + ctypes.byref(value), + ) return take_string(value) def get_tags(self, index): - pointer, length = ctypes.c_void_p(), ctypes.c_size_t() - call("bink_choices_get_tags", self._choices, self._index(index), ctypes.byref(pointer), ctypes.byref(length)) + """Return the tags associated with the choice at ``index``.""" + pointer, length = ctypes.c_void_p(None), ctypes.c_size_t(0) + call( + "bink_choices_get_tags", + self._choices, + self._index(index), + ctypes.byref(pointer), + ctypes.byref(length), + ) return Tags(pointer, length.value) def __del__(self): diff --git a/bink/story.py b/bink/story.py index d4b93f7..52e28ac 100644 --- a/bink/story.py +++ b/bink/story.py @@ -1,7 +1,8 @@ """High-level Story API over blade-ink-ffi.""" + import ctypes -from ._ffi import (BINK_ERROR_ERROR, LIB, P, call, take_string) +from ._ffi import BINK_ERROR_ERROR, LIB, P, call, take_string from .choices import Choices from .tags import Tags from .value import InkList, Value, ValueArray @@ -9,125 +10,330 @@ _ERROR_HANDLER = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_int, P) _EXTERNAL_FUNCTION = ctypes.CFUNCTYPE(P, ctypes.c_char_p, P, P) _VARIABLE_OBSERVER = ctypes.CFUNCTYPE(None, ctypes.c_char_p, P, P) -LIB.bink_story_set_error_handler.argtypes = [P, _ERROR_HANDLER, P, ctypes.POINTER(ctypes.c_char_p)] -LIB.bink_bind_external_function.argtypes = [P, ctypes.c_char_p, _EXTERNAL_FUNCTION, P, ctypes.POINTER(ctypes.c_char_p)] -LIB.bink_bind_external_function_with_options.argtypes = [P, ctypes.c_char_p, _EXTERNAL_FUNCTION, P, ctypes.c_bool, ctypes.POINTER(ctypes.c_char_p)] -LIB.bink_observe_variable.argtypes = [P, ctypes.c_char_p, _VARIABLE_OBSERVER, P, ctypes.POINTER(ctypes.c_char_p)] -LIB.bink_observe_variable_with_handle.argtypes = [P, ctypes.c_char_p, _VARIABLE_OBSERVER, P, ctypes.POINTER(P), ctypes.POINTER(ctypes.c_char_p)] +LIB.bink_story_set_error_handler.argtypes = [ + P, + _ERROR_HANDLER, + P, + ctypes.POINTER(ctypes.c_char_p), +] +LIB.bink_bind_external_function.argtypes = [ + P, + ctypes.c_char_p, + _EXTERNAL_FUNCTION, + P, + ctypes.POINTER(ctypes.c_char_p), +] +LIB.bink_bind_external_function_with_options.argtypes = [ + P, + ctypes.c_char_p, + _EXTERNAL_FUNCTION, + P, + ctypes.c_bool, + ctypes.POINTER(ctypes.c_char_p), +] +LIB.bink_observe_variable.argtypes = [ + P, + ctypes.c_char_p, + _VARIABLE_OBSERVER, + P, + ctypes.POINTER(ctypes.c_char_p), +] +LIB.bink_observe_variable_with_handle.argtypes = [ + P, + ctypes.c_char_p, + _VARIABLE_OBSERVER, + P, + ctypes.POINTER(P), + ctypes.POINTER(ctypes.c_char_p), +] LIB.bink_variable_observer_remove.argtypes = [P, P, ctypes.POINTER(ctypes.c_char_p)] -class Story: +# The class intentionally mirrors the complete public blade-ink-ffi Story API. +class Story: # pylint: disable=too-many-public-methods """An Ink story. Every feature in the blade-ink-ffi C API is exposed here.""" + def __init__(self, story_string): - self._story = ctypes.c_void_p() + """Create a story from compiled Ink JSON.""" + self._story = ctypes.c_void_p(None) self._callbacks, self._observer_handles = [], [] call("bink_story_new", ctypes.byref(self._story), story_string.encode()) - def __iter__(self): return self + def __iter__(self): + return self + def __next__(self): - if not self.can_continue(): raise StopIteration + if not self.can_continue(): + raise StopIteration return self.cont() + @property - def choices(self): return self.get_current_choices() + def choices(self): + """Return the current choice collection.""" + return self.get_current_choices() + @property - def tags(self): return self.get_current_tags() + def tags(self): + """Return the tags emitted by the latest continuation.""" + return self.get_current_tags() + @property - def current_text(self): return self.get_current_text() + def current_text(self): + """Return text accumulated by the latest continuation.""" + return self.get_current_text() + @property - def current_path(self): return self.get_current_path() + def current_path(self): + """Return the current Ink content path.""" + return self.get_current_path() def can_continue(self): - result = ctypes.c_bool(); call("bink_story_can_continue", self._story, ctypes.byref(result)); return result.value - def cont(self): return self._string("bink_story_cont") - def continue_maximally(self): return self._string("bink_story_continue_maximally") - def get_current_text(self): return self._string("bink_story_get_current_text") - def get_current_path(self): return self._string("bink_story_get_current_path") - def build_string_of_hierarchy(self): return self._string("bink_story_build_string_of_hierarchy") - def save_state(self): return self._string("bink_story_save_state") + """Return whether normal continuation can produce more content.""" + result = ctypes.c_bool(False) + call("bink_story_can_continue", self._story, ctypes.byref(result)) + return result.value + + def cont(self): + """Continue the story by one line.""" + return self._string("bink_story_cont") + + def continue_maximally(self): + """Continue until the next choice or end of story.""" + return self._string("bink_story_continue_maximally") + + def get_current_text(self): + """Return text accumulated by the latest continuation.""" + return self._string("bink_story_get_current_text") + + def get_current_path(self): + """Return the current Ink content path.""" + return self._string("bink_story_get_current_path") + + def build_string_of_hierarchy(self): + """Return a diagnostic representation of the story hierarchy.""" + return self._string("bink_story_build_string_of_hierarchy") + + def save_state(self): + """Serialize the current story state.""" + return self._string("bink_story_save_state") + def _string(self, function): - result = ctypes.c_char_p(); call(function, self._story, ctypes.byref(result)); return take_string(result) + result = ctypes.c_char_p() + call(function, self._story, ctypes.byref(result)) + return take_string(result) def continue_async(self, millisecs_limit_async): - complete = ctypes.c_bool(); call("bink_story_continue_async", self._story, millisecs_limit_async, ctypes.byref(complete)); return complete.value - def get_current_choices(self): return self._collection("bink_story_get_current_choices", Choices) - def get_current_tags(self): return self._collection("bink_story_get_current_tags", Tags) - def get_global_tags(self): return self._collection("bink_story_get_global_tags", Tags) + """Continue within a time budget and return whether that pass completed.""" + complete = ctypes.c_bool(False) + call( + "bink_story_continue_async", + self._story, + millisecs_limit_async, + ctypes.byref(complete), + ) + return complete.value + + def get_current_choices(self): + """Return the choices currently available to the reader.""" + return self._collection("bink_story_get_current_choices", Choices) + + def get_current_tags(self): + """Return the tags emitted by the latest continuation.""" + return self._collection("bink_story_get_current_tags", Tags) + + def get_global_tags(self): + """Return tags declared at story scope.""" + return self._collection("bink_story_get_global_tags", Tags) + def _collection(self, function, cls, *args): - pointer, length = ctypes.c_void_p(), ctypes.c_size_t() - call(function, self._story, *args, ctypes.byref(pointer), ctypes.byref(length)); return cls(pointer, length.value) - def get_tags_for_content_at_path(self, path): return self._collection("bink_story_get_tags_for_content_at_path", Tags, path.encode()) + pointer, length = ctypes.c_void_p(None), ctypes.c_size_t(0) + call(function, self._story, *args, ctypes.byref(pointer), ctypes.byref(length)) + return cls(pointer, length.value) + + def get_tags_for_content_at_path(self, path): + """Return tags declared at an Ink content path.""" + return self._collection( + "bink_story_get_tags_for_content_at_path", Tags, path.encode() + ) + + def choose_choice_index(self, index): + """Select the current choice at ``index``.""" + call("bink_story_choose_choice_index", self._story, index) + + def choose_path_string(self, path): + """Jump to an Ink content path.""" + call("bink_story_choose_path_string", self._story, path.encode()) - def choose_choice_index(self, index): call("bink_story_choose_choice_index", self._story, index) - def choose_path_string(self, path): call("bink_story_choose_path_string", self._story, path.encode()) def choose_path_string_with_args(self, path, args=(), reset_call_stack=True): + """Jump to a path while providing function-style arguments.""" values = args if isinstance(args, ValueArray) else ValueArray(args) - call("bink_story_choose_path_string_with_args", self._story, path.encode(), reset_call_stack, values._values) + call( + "bink_story_choose_path_string_with_args", + self._story, + path.encode(), + reset_call_stack, + values.pointer, + ) + def evaluate_function(self, name, args=()): + """Evaluate an Ink function and return its value and text output.""" values = args if isinstance(args, ValueArray) else ValueArray(args) result, text = ctypes.c_void_p(), ctypes.c_char_p() - call("bink_story_evaluate_function", self._story, name.encode(), values._values, ctypes.byref(result), ctypes.byref(text)) + call( + "bink_story_evaluate_function", + self._story, + name.encode(), + values.pointer, + ctypes.byref(result), + ctypes.byref(text), + ) return Value(_pointer=result).to_python(), take_string(text) - def load_state(self, state): call("bink_story_load_state", self._story, state.encode()) - def reset_state(self): call("bink_story_reset_state", self._story) + + def load_state(self, state): + """Restore a state previously produced by :meth:`save_state`.""" + call("bink_story_load_state", self._story, state.encode()) + + def reset_state(self): + """Reset the story to its initial state.""" + call("bink_story_reset_state", self._story) + def get_visit_count_at_path_string(self, path): - count = ctypes.c_int32(); call("bink_story_get_visit_count_at_path_string", self._story, path.encode(), ctypes.byref(count)); return count.value + """Return the visit count recorded for ``path``.""" + count = ctypes.c_int32(0) + call( + "bink_story_get_visit_count_at_path_string", + self._story, + path.encode(), + ctypes.byref(count), + ) + return count.value def get_variable(self, name): - value = ctypes.c_void_p(); call("bink_var_get", self._story, name.encode(), ctypes.byref(value)); return Value(_pointer=value).to_python() + """Return a global Ink variable as a Python value.""" + value = ctypes.c_void_p(None) + call("bink_var_get", self._story, name.encode(), ctypes.byref(value)) + return Value(_pointer=value).to_python() + def set_variable(self, name, value): - value = value if isinstance(value, Value) else Value(value); call("bink_var_set", self._story, name.encode(), value._value) - def list_from_origin(self, origin): return self._story_list("bink_story_list_new_from_origin", origin) - def list_from_item(self, item): return self._story_list("bink_story_list_new_from_item", item) + """Set a global Ink variable from a Python value.""" + value = value if isinstance(value, Value) else Value(value) + call("bink_var_set", self._story, name.encode(), value.pointer) + + def list_from_origin(self, origin): + """Create an Ink list containing all items from ``origin``.""" + return self._story_list("bink_story_list_new_from_origin", origin) + + def list_from_item(self, item): + """Create an Ink list containing ``item``.""" + return self._story_list("bink_story_list_new_from_item", item) + def _story_list(self, function, name): - result = ctypes.c_void_p(); call(function, self._story, name.encode(), ctypes.byref(result)); return InkList(_pointer=result) + result = ctypes.c_void_p(None) + call(function, self._story, name.encode(), ctypes.byref(result)) + return InkList(_pointer=result) + + def switch_flow(self, name): + """Switch to, or create, the named flow.""" + call("bink_story_switch_flow", self._story, name.encode()) - def switch_flow(self, name): call("bink_story_switch_flow", self._story, name.encode()) - def remove_flow(self, name): call("bink_story_remove_flow", self._story, name.encode()) - def switch_to_default_flow(self): call("bink_story_switch_to_default_flow", self._story) - def set_allow_external_function_fallbacks(self, allow): call("bink_story_set_allow_external_function_fallbacks", self._story, allow) + def remove_flow(self, name): + """Remove the named flow.""" + call("bink_story_remove_flow", self._story, name.encode()) + + def switch_to_default_flow(self): + """Switch back to the default flow.""" + call("bink_story_switch_to_default_flow", self._story) + + def set_allow_external_function_fallbacks(self, allow): + """Enable or disable Ink's fallback for unbound external functions.""" + call("bink_story_set_allow_external_function_fallbacks", self._story, allow) def bind_external_function(self, name, function, lookahead_safe=False): + """Bind a Python callable as an Ink external function.""" + def callback(c_name, c_args, _): try: - count = ctypes.c_size_t(); call("bink_fun_args_count", c_args, ctypes.byref(count)) + count = ctypes.c_size_t(0) + call("bink_fun_args_count", c_args, ctypes.byref(count)) args = [] for index in range(count.value): - value = ctypes.c_void_p(); call("bink_fun_args_get", c_args, index, ctypes.byref(value)); args.append(Value(_pointer=value).to_python()) + value = ctypes.c_void_p(None) + call("bink_fun_args_get", c_args, index, ctypes.byref(value)) + args.append(Value(_pointer=value).to_python()) result = function(c_name.decode(), *args) if result is None: return None - value = Value(result) - value._owned = False # Ownership is transferred to blade-ink-ffi. - return value._value.value - except Exception: + return Value(result).detach() + # A Python exception must never cross a C callback boundary. + except Exception: # pylint: disable=broad-exception-caught return None - callback = _EXTERNAL_FUNCTION(callback); self._callbacks.append(callback) - function_name = "bink_bind_external_function_with_options" if lookahead_safe else "bink_bind_external_function" - if lookahead_safe: call(function_name, self._story, name.encode(), callback, None, True) - else: call(function_name, self._story, name.encode(), callback, None) - def unbind_external_function(self, name): call("bink_unbind_external_function", self._story, name.encode()) + + callback = _EXTERNAL_FUNCTION(callback) + self._callbacks.append(callback) + function_name = ( + "bink_bind_external_function_with_options" + if lookahead_safe + else "bink_bind_external_function" + ) + if lookahead_safe: + call(function_name, self._story, name.encode(), callback, None, True) + else: + call(function_name, self._story, name.encode(), callback, None) + + def unbind_external_function(self, name): + """Remove an external function binding.""" + call("bink_unbind_external_function", self._story, name.encode()) def set_error_handler(self, handler): - callback = _ERROR_HANDLER(lambda message, kind, _: handler(message.decode(), kind == BINK_ERROR_ERROR)) - self._callbacks.append(callback); call("bink_story_set_error_handler", self._story, callback, None) + """Set a callable receiving ``(message, is_error)`` diagnostics.""" + callback = _ERROR_HANDLER( + lambda message, kind, _: handler(message.decode(), kind == BINK_ERROR_ERROR) + ) + self._callbacks.append(callback) + call("bink_story_set_error_handler", self._story, callback, None) + def observe_variable(self, name, observer, removable=False): + """Observe changes to a variable and optionally return a removal handle.""" + def callback(c_name, c_value, _): try: - observer(c_name.decode(), Value(_pointer=c_value, _owned=False).to_python()) - except Exception: + observer( + c_name.decode(), Value(_pointer=c_value, _owned=False).to_python() + ) + # A Python exception must never cross a C callback boundary. + except Exception: # pylint: disable=broad-exception-caught pass - callback = _VARIABLE_OBSERVER(callback); self._callbacks.append(callback) + + callback = _VARIABLE_OBSERVER(callback) + self._callbacks.append(callback) if not removable: - call("bink_observe_variable", self._story, name.encode(), callback, None); return None - handle = ctypes.c_void_p(); call("bink_observe_variable_with_handle", self._story, name.encode(), callback, None, ctypes.byref(handle)); self._observer_handles.append(handle); return handle + call("bink_observe_variable", self._story, name.encode(), callback, None) + return None + handle = ctypes.c_void_p(None) + call( + "bink_observe_variable_with_handle", + self._story, + name.encode(), + callback, + None, + ctypes.byref(handle), + ) + self._observer_handles.append(handle) + return handle + def remove_variable_observer(self, handle): + """Remove an observer returned by :meth:`observe_variable`.""" call("bink_variable_observer_remove", self._story, handle) - if handle in self._observer_handles: self._observer_handles.remove(handle) + if handle in self._observer_handles: + self._observer_handles.remove(handle) def __del__(self): - if getattr(self, "_story", None): LIB.bink_story_free(self._story); self._story = None + if getattr(self, "_story", None): + LIB.bink_story_free(self._story) + self._story = None def story_from_file(story_file): - with open(story_file, encoding="utf-8") as file: return Story(file.read()) + """Create a story from a compiled Ink JSON file.""" + with open(story_file, encoding="utf-8") as file: + return Story(file.read()) diff --git a/bink/tags.py b/bink/tags.py index b5bcba6..4cbf8f7 100644 --- a/bink/tags.py +++ b/bink/tags.py @@ -1,4 +1,5 @@ """Tag collections returned by a story or choice.""" + import ctypes from ._ffi import LIB, call, take_string @@ -6,17 +7,26 @@ class Tags: """An owned, immutable sequence of Ink tags.""" + def __init__(self, pointer, length): self._tags, self._len = pointer, length - def __len__(self): return self._len - def __bool__(self): return bool(self._len) - def __iter__(self): return (self[index] for index in range(self._len)) + def __len__(self): + return self._len + + def __bool__(self): + return bool(self._len) + + def __iter__(self): + return (self[index] for index in range(self._len)) def __getitem__(self, index): - if not isinstance(index, int): raise TypeError("tag index must be an integer") - if index < 0: index += self._len - if not 0 <= index < self._len: raise IndexError("tag index out of range") + if not isinstance(index, int): + raise TypeError("tag index must be an integer") + if index < 0: + index += self._len + if not 0 <= index < self._len: + raise IndexError("tag index out of range") value = ctypes.c_char_p() call("bink_tags_get", self._tags, index, ctypes.byref(value)) return take_string(value) diff --git a/bink/value.py b/bink/value.py index b09c90b..ff96341 100644 --- a/bink/value.py +++ b/bink/value.py @@ -1,83 +1,161 @@ """Typed Ink values, value arrays, and Ink lists.""" + import ctypes -from ._ffi import (BINK_VALUE_BOOL, BINK_VALUE_FLOAT, BINK_VALUE_INT, - BINK_VALUE_LIST, BINK_VALUE_STRING, LIB, call, take_string) +from ._ffi import ( + BINK_VALUE_BOOL, + BINK_VALUE_FLOAT, + BINK_VALUE_INT, + BINK_VALUE_LIST, + BINK_VALUE_STRING, + LIB, + call, + take_string, +) class Value: """An owned FFI value. Construct it from bool, int, float, str, or InkList.""" + def __init__(self, value=None, _pointer=None, _owned=True): self._owned = _owned if _pointer is not None: self._value = _pointer return - self._value = ctypes.c_void_p() - if isinstance(value, InkList): call("bink_value_new_list", value._list, ctypes.byref(self._value)) - elif isinstance(value, bool): call("bink_value_new_bool", value, ctypes.byref(self._value)) - elif isinstance(value, int): call("bink_value_new_int", value, ctypes.byref(self._value)) - elif isinstance(value, float): call("bink_value_new_float", value, ctypes.byref(self._value)) - elif isinstance(value, str): call("bink_value_new_string", value.encode(), ctypes.byref(self._value)) - else: raise TypeError("Value must be bool, int, float, str, or InkList") + self._value = ctypes.c_void_p(None) + if isinstance(value, InkList): + call("bink_value_new_list", value._list, ctypes.byref(self._value)) + elif isinstance(value, bool): + call("bink_value_new_bool", value, ctypes.byref(self._value)) + elif isinstance(value, int): + call("bink_value_new_int", value, ctypes.byref(self._value)) + elif isinstance(value, float): + call("bink_value_new_float", value, ctypes.byref(self._value)) + elif isinstance(value, str): + call("bink_value_new_string", value.encode(), ctypes.byref(self._value)) + else: + raise TypeError("Value must be bool, int, float, str, or InkList") @property def kind(self): - kind = ctypes.c_int(); call("bink_value_get_kind", self._value, ctypes.byref(kind)); return kind.value + """Return the FFI value-kind constant.""" + kind = ctypes.c_int(0) + call("bink_value_get_kind", self._value, ctypes.byref(kind)) + return kind.value def to_python(self): + """Convert a supported FFI value into its Python equivalent.""" if self.kind == BINK_VALUE_BOOL: - out = ctypes.c_bool(); call("bink_value_get_bool", self._value, ctypes.byref(out)); return out.value + out = ctypes.c_bool(False) + call("bink_value_get_bool", self._value, ctypes.byref(out)) + return out.value if self.kind == BINK_VALUE_INT: - out = ctypes.c_int32(); call("bink_value_get_int", self._value, ctypes.byref(out)); return out.value + out = ctypes.c_int32(0) + call("bink_value_get_int", self._value, ctypes.byref(out)) + return out.value if self.kind == BINK_VALUE_FLOAT: - out = ctypes.c_float(); call("bink_value_get_float", self._value, ctypes.byref(out)); return out.value + out = ctypes.c_float(0) + call("bink_value_get_float", self._value, ctypes.byref(out)) + return out.value if self.kind == BINK_VALUE_STRING: - out = ctypes.c_char_p(); call("bink_value_get_string", self._value, ctypes.byref(out)); return take_string(out) + out = ctypes.c_char_p() + call("bink_value_get_string", self._value, ctypes.byref(out)) + return take_string(out) if self.kind == BINK_VALUE_LIST: - out = ctypes.c_void_p(); call("bink_value_get_list", self._value, ctypes.byref(out)); return InkList(_pointer=out) + out = ctypes.c_void_p(None) + call("bink_value_get_list", self._value, ctypes.byref(out)) + return InkList(_pointer=out) raise TypeError("this Ink value has no Python representation") def __del__(self): - if getattr(self, "_value", None) and self._owned: LIB.bink_value_free(self._value) + if getattr(self, "_value", None) and self._owned: + LIB.bink_value_free(self._value) self._value = None + @property + def pointer(self): + """Return the underlying value pointer for internal FFI calls.""" + return self._value + + def detach(self): + """Transfer ownership of the value pointer to the FFI.""" + self._owned = False + return self._value.value + class ValueArray: """Owned argument array for function evaluation and path selection.""" + def __init__(self, values=()): - self._values = ctypes.c_void_p(); call("bink_value_array_new", ctypes.byref(self._values)) - for value in values: self.append(value) + self._values = ctypes.c_void_p(None) + call("bink_value_array_new", ctypes.byref(self._values)) + for value in values: + self.append(value) + def append(self, value): + """Append a Python or :class:`Value` argument.""" owned = value if isinstance(value, Value) else Value(value) - call("bink_value_array_push", self._values, owned._value) + call("bink_value_array_push", self._values, owned.pointer) + + @property + def pointer(self): + """Return the underlying argument-array pointer for internal FFI calls.""" + return self._values + def __del__(self): - if getattr(self, "_values", None): LIB.bink_value_array_free(self._values); self._values = None + if getattr(self, "_values", None): + LIB.bink_value_array_free(self._values) + self._values = None class InkList: """An owned Ink list of ``(origin.item, value)`` entries.""" + def __init__(self, items=(), _pointer=None): - self._list = _pointer or ctypes.c_void_p() + self._list = _pointer or ctypes.c_void_p(None) if _pointer is None: call("bink_list_new", ctypes.byref(self._list)) - for name, value in items: self.add(name, value) - def add(self, full_name, value): call("bink_list_add_item", self._list, full_name.encode(), value) + for name, value in items: + self.add(name, value) + + def add(self, full_name, value): + """Add an ``origin.item`` entry with its numeric value.""" + call("bink_list_add_item", self._list, full_name.encode(), value) + def __len__(self): - count = ctypes.c_size_t(); call("bink_list_get_count", self._list, ctypes.byref(count)); return count.value + count = ctypes.c_size_t(0) + call("bink_list_get_count", self._list, ctypes.byref(count)) + return count.value + @property def items(self): + """Return the list entries as ``(full_name, value)`` tuples.""" result = [] for index in range(len(self)): - name, value = ctypes.c_char_p(), ctypes.c_int32() - call("bink_list_get_item", self._list, index, ctypes.byref(name), ctypes.byref(value)) + name, value = ctypes.c_char_p(), ctypes.c_int32(0) + call( + "bink_list_get_item", + self._list, + index, + ctypes.byref(name), + ctypes.byref(value), + ) result.append((take_string(name), value.value)) return result + @property def origins(self): - count = ctypes.c_size_t(); call("bink_list_get_origin_count", self._list, ctypes.byref(count)) + """Return the origin names reported by the FFI list.""" + count = ctypes.c_size_t(0) + call("bink_list_get_origin_count", self._list, ctypes.byref(count)) result = [] for index in range(count.value): - origin = ctypes.c_char_p(); call("bink_list_get_origin", self._list, index, ctypes.byref(origin)); result.append(take_string(origin)) + origin = ctypes.c_char_p() + call("bink_list_get_origin", self._list, index, ctypes.byref(origin)) + result.append(take_string(origin)) return result + def __del__(self): - if getattr(self, "_list", None): LIB.bink_list_free(self._list); self._list = None + if getattr(self, "_list", None): + LIB.bink_list_free(self._list) + self._list = None diff --git a/setup.cfg b/setup.cfg index d0e2dbf..f5b7754 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = bink -version = 0.7.0 +version = 0.7.1 author = Rafael Garcia description = Runtime for Ink, a scripting language for writing interactive narrative long_description = file: README.rst