Learn how to use class wrappers for cleaner, object-oriented plugin APIs in Python.
Classes in Plugify provide an object-oriented way to work with plugin resources, offering automatic lifecycle management and cleaner APIs. Instead of manually managing handles and calling functions with explicit pointers, you can use intuitive class-based interfaces.
Working with handle-based APIs in the traditional C-style can be verbose and error-prone:
Without Classes (C-style)
from plugify.pps import s2sdk
# Create a KeyValues handle manually
kv_handle = s2sdk.Kv1Create("MyConfig")
# Set properties using the handle
s2sdk.Kv1SetName(kv_handle, "ServerSettings")
# Find a subkey
subkey_handle = s2sdk.Kv1FindKey(kv_handle, "Players")
# Easy to forget cleanup!
s2sdk.Kv1Destroy(subkey_handle)
s2sdk.Kv1Destroy(kv_handle)
With classes, the same code becomes much cleaner and safer:
With Classes (OOP-style)
from plugify.pps import s2sdk
# Create using a class constructor
with s2sdk.KeyValues("MyConfig") as kv:
# Use intuitive methods
kv.SetName("ServerSettings")
# Find a subkey - returns a KeyValues instance
subkey = kv.FindKey("Players")
# Automatic cleanup when exiting the context!
Benefits:
Cleaner syntax - Methods instead of functions with explicit handles
Automatic resource management - No need to manually call destroy functions
Type safety - Better IDE autocomplete and type checking
Less error-prone - Harder to forget cleanup or mix up handles
Returns the underlying handle value. Use this when you need to pass the raw handle to C-style functions:
Using get()
from plugify.pps import s2sdk
kv = s2sdk.KeyValues("Config")
# Get the raw handle
raw_handle = kv.get()
print(f"Handle value: {raw_handle}")
# Pass to a function expecting a raw handle
some_c_function(kv.get())
Warning: Be careful when using get(). The returned handle is still owned by the class instance and will be destroyed when the instance is cleaned up.
Releases ownership of the handle and returns it. After calling release(), the class instance becomes invalid and will not call the destructor:
Using release()
from plugify.pps import s2sdk
def create_and_release():
kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")
# Transfer ownership out
handle = kv.release()
# kv is now invalid, won't clean up
print(kv.valid()) # False
return handle
# We now own the handle and must clean it up manually
raw_handle = create_and_release()
# ... use raw_handle ...
s2sdk.Kv1Destroy(raw_handle) # Manual cleanup required!
Explicitly closes the handle and calls the destructor if the object owns it:
Using close()
from plugify.pps import s2sdk
kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")
# Explicitly close the handle now
kv.close()
# Handle is now invalid
print(kv.valid()) # False
# Methods will raise RuntimeError
try:
kv.SetName("Test")
except RuntimeError as e:
print(f"Error: {e}") # "KeyValues handle is closed"
The close() method is:
Idempotent - Safe to call multiple times
Called automatically - By __del__ and __exit__
Ownership-aware - Only calls destructor if object owns the handle
from plugify.pps import s2sdk
kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")
# Reset is equivalent to close
kv.reset()
print(kv.valid()) # False
When a destructor is defined, classes automatically support the with statement via __enter__ and __exit__:
Context Manager
from plugify.pps import s2sdk
with s2sdk.KeyValues("Config") as kv:
kv.SetName("ServerConfig")
kv.SetString("hostname", "My Server")
# ... use kv ...
# Automatically cleaned up when exiting the 'with' block
The destructor (__del__) is called automatically when the object is garbage collected:
Automatic Cleanup
from plugify.pps import s2sdk
def process_config():
kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")
# ... use kv ...
# kv is automatically destroyed when function returns
process_config() # kv cleaned up here
Important - Plugin Lifecycle: CPython finalizers (__del__) are deterministic and called when the reference count reaches zero. However, you must avoid situations where finalizers are called after your plugin is unloaded. If you store objects globally, you are responsible for cleaning them up in pluginEnd(), otherwise behavior is undefined and may cause crashes.
For immediate resource cleanup, use close() or delete the object:
Manual Cleanup
from plugify.pps import s2sdk
kv = s2sdk.KeyValues("Config")
kv.SetName("ServerConfig")
# ... use kv ...
# Option 1: Explicitly close
kv.close()
# Option 2: Delete the object
kv2 = s2sdk.KeyValues("Config2")
del kv2
# Both result in immediate cleanup
If a class doesn't have a destructor defined in the manifest, it acts as a simple wrapper without automatic cleanup:
Wrapper Without Destructor
from plugify.pps import s2sdk
# Class with no destructor - just a convenience wrapper
wrapper = s2sdk.SomeWrapper()
# Still has utility methods
if wrapper.valid():
handle = wrapper.get()
# No automatic cleanup - handle persists
# Useful for stateless wrappers or global resources
When a method takes ownership of a resource, you should transfer it and not use it afterward:
Ownership Transfer
from plugify.pps import s2sdk
# Create parent and child
parent = s2sdk.KeyValues("Parent")
child = s2sdk.KeyValues("Child")
# AddSubKey takes ownership of child
parent.AddSubKey(child)
# child is now owned by parent
# child.valid() may still be True, but don't use it!
# parent will handle cleanup
Best practice: After transferring ownership, avoid using the object:
Proper Ownership Handling
from plugify.pps import s2sdk
parent = s2sdk.KeyValues("Parent")
child = s2sdk.KeyValues("Child")
# Transfer ownership
parent.AddSubKey(child)
# Release our reference to prevent accidental use
child_handle = child.release() # Now child.valid() == False
# Only use through parent
found = parent.FindKey("Child")
When a method returns a new resource with ownership:
Return Ownership
from plugify.pps import s2sdk
parent = s2sdk.KeyValues("Parent")
# FindKey returns a NEW KeyValues that we own
child = parent.FindKey("Settings")
if child and child.valid():
child.SetName("UpdatedSettings")
# We're responsible for child's lifecycle
# It will be cleaned up automatically when it goes out of scope
When owner: false, the method returns a reference without transferring ownership:
Non-Owning Reference
from plugify.pps import s2sdk
parent = s2sdk.KeyValues("Parent")
# GetFirstSubKey returns a reference, parent still owns it
child_ref = parent.GetFirstSubKey()
if child_ref and child_ref.valid():
# Use the reference, but DON'T delete it
name = child_ref.GetName()
# Don't call del child_ref or use release()
# child_ref will be cleaned up by parent
Important: With non-owning references, the returned object is still valid (has a handle), but you don't own it. Deleting the parent will invalidate these references.
When using classes in plugins, you must be careful about object cleanup during plugin unload. CPython uses reference counting, which means finalizers (__del__) are deterministic and called immediately when the reference count reaches zero. However, if objects are still alive when your plugin is unloaded, their finalizers may be called after the plugin's code is no longer in memory, leading to undefined behavior or crashes.
If you store class instances in global variables or module-level attributes, you must explicitly clean them up in your plugin's pluginEnd() function:
plugin.py
from plugify import PluginEntryPoint
from plugify.pps import s2sdk
# Global object - dangerous if not cleaned up!
g_config = None
class MyPlugin(PluginEntryPoint):
def pluginStart(self):
global g_config
# Create global configuration
g_config = s2sdk.KeyValues("GlobalConfig")
g_config.SetName("ServerSettings")
print("Plugin started with global config")
def pluginEnd(self):
global g_config
# CRITICAL: Clean up global objects before plugin unload
if g_config:
g_config.close() # or del g_config
g_config = None
print("Plugin ended, global config cleaned up")
Critical: Failing to clean up global objects in pluginEnd() may cause finalizers to run after your plugin is unloaded, resulting in crashes or undefined behavior. Always explicitly clean up global resources!
class MyPlugin(PluginEntryPoint):
def onCommand(self, args):
# Local object - automatically cleaned up when function returns
kv = s2sdk.KeyValues("TempConfig")
kv.SetName("CommandConfig")
# ... use kv ...
# Automatically destroyed here - safe!
✅ Safe: Context manager
Safe Pattern
class MyPlugin(PluginEntryPoint):
def onCommand(self, args):
# Context manager ensures cleanup before function returns
with s2sdk.KeyValues("TempConfig") as kv:
kv.SetName("CommandConfig")
# ... use kv ...
# Definitely cleaned up here - safe!
✅ Safe: Instance variable with cleanup
Safe Pattern
class MyPlugin(PluginEntryPoint):
def pluginStart(self):
self.config = s2sdk.KeyValues("PluginConfig")
self.config.SetName("Settings")
def pluginEnd(self):
# Clean up instance variables
if hasattr(self, 'config') and self.config:
self.config.close()
self.config = None
❌ Unsafe: Global without cleanup
Unsafe Pattern
from plugify.pps import s2sdk
# DANGEROUS: Global object
g_config = s2sdk.KeyValues("GlobalConfig")
class MyPlugin(PluginEntryPoint):
def pluginStart(self):
g_config.SetName("ServerSettings")
def pluginEnd(self):
# MISSING: No cleanup!
# g_config finalizer will run after plugin unload - CRASH!
pass
❌ Unsafe: Module-level cache without cleanup
Unsafe Pattern
from plugify.pps import s2sdk
# DANGEROUS: Module-level cache
_kv_cache = {}
class MyPlugin(PluginEntryPoint):
def cacheConfig(self, name):
_kv_cache[name] = s2sdk.KeyValues(name)
def pluginEnd(self):
# MISSING: No cache cleanup!
# Cached objects will finalize after plugin unload - CRASH!
pass
Sometimes you need to work with raw handles alongside class instances:
mixed_usage.py
from plugify.pps import s2sdk
def process_with_mixed_api():
# Create using class
kv = s2sdk.KeyValues("Config")
# Use class methods
kv.SetName("ProcessedConfig")
# Get raw handle for C-style function
raw_handle = kv.get()
# Call legacy C-style function
result = s2sdk.SomeLegacyFunction(raw_handle)
# Continue using class methods
if kv.valid():
kv.SaveToFile("output.kv")
# Automatic cleanup still works
# kv will be destroyed when function returns
def transfer_ownership_example():
# Create and configure
kv = s2sdk.KeyValues("Config")
kv.SetName("TransferTest")
# Release ownership
handle = kv.release()
# kv is now invalid
assert not kv.valid()
# We must manually destroy
s2sdk.Kv1Destroy(handle)
# Or wrap it again
kv2 = s2sdk.KeyValues("NewConfig")
# ... use kv2 ...
Always use context managers - Use with statements for guaranteed cleanup
Use close() for immediate cleanup - More explicit than relying on del or garbage collection
Clean up global objects in pluginEnd() - Critical: Explicitly clean up all global or module-level class instances before plugin unload to avoid crashes
Prefer local scope - Keep class instances in local scope when possible for automatic cleanup
Check valid() for safety - Especially after operations that might fail
Respect ownership - Don't use objects after transferring ownership
Use release() sparingly - Only when you need manual control
Avoid mixing styles - Prefer class API over raw C-style functions
Trust automatic cleanup for local objects - Let __del__ handle cleanup for local objects, but always clean up globals manually
Handle RuntimeError - Be prepared to catch exceptions from methods called on closed handles
Check returned objects - Methods might return None on failure
Attempting to use an object after calling close(), release(), or after destruction will raise a RuntimeError:
Using Closed Handle
from plugify.pps import s2sdk
kv = s2sdk.KeyValues("Config")
kv.close()
# This raises RuntimeError
try:
kv.SetName("Test")
except RuntimeError as e:
print(e) # "KeyValues handle is closed"
# Check valid() first to avoid exceptions
if kv.valid():
kv.SetName("Test")
else:
print("Handle is closed!")
All bound methods automatically validate the handle before calling the underlying C function:
Automatic Validation
from plugify.pps import s2sdk
kv = s2sdk.KeyValues("Config")
handle = kv.release()
# Any method call will fail
try:
name = kv.GetName()
except RuntimeError as e:
print(e) # "KeyValues handle is closed"
Check global objects - Ensure all global class instances are cleaned up in pluginEnd()
Check module-level collections - Clear any lists, dicts, or sets containing class instances
Check instance variables - Clean up class instance variables in pluginEnd()
Add explicit cleanup - Use close() or del on all long-lived objects before plugin unload
Fix Unload Crash
# Global objects that caused crashes
g_config = None
g_cache = {}
class MyPlugin(PluginEntryPoint):
def pluginStart(self):
global g_config, g_cache
g_config = s2sdk.KeyValues("Config")
g_cache["main"] = s2sdk.KeyValues("Main")
def pluginEnd(self):
global g_config, g_cache
# Clean up global objects
if g_config:
g_config.close()
g_config = None
# Clean up cached objects
for kv in g_cache.values():
kv.close()
g_cache.clear()
print("All resources cleaned up safely")