Source code for miney.storage

"""
A place on the server where a script can leave something for the next run.
"""
from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, ValuesView
import logging
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .luanti import Luanti


logger = logging.getLogger(__name__)


[docs] class Storage(MutableMapping): """ The world's key-value store, used like a dictionary. Everything a script does is gone when it ends: variables live in Python, and even the Lua names from :meth:`~miney.Lua.run` disappear with the connection. This is the exception. What you put here is written into the world and is still there for the next run, and after a server restart:: >>> lt.storage["home"] = "10,20,30" >>> lt.storage["home"] '10,20,30' It behaves like a ``dict`` - ``in``, ``len()``, ``for``, ``.get()``, ``.pop()``, ``.update()`` and ``del`` all work:: >>> "home" in lt.storage True >>> for key, value in lt.storage.items(): ... print(key, value) home 10,20,30 >>> del lt.storage["home"] **Keys and values are strings, both of them.** Luanti writes text and nothing else - its ``set_int`` and ``set_float`` only convert to text on the way in, and what comes back out is a string either way. So Miney does not pretend otherwise: assigning a number raises a :class:`TypeError` instead of silently handing back ``"7"`` where you wrote ``7``. Convert on the way in and out:: >>> lt.storage["visits"] = str(41 + 1) >>> int(lt.storage["visits"]) 42 For anything with a shape, use :mod:`json`:: >>> import json >>> lt.storage["seen"] = json.dumps({"players": ["Steve", "Alex"]}) >>> json.loads(lt.storage["seen"])["players"] ['Steve', 'Alex'] There is one store per world, not one per player, and it is not private: every Miney script connecting to that world reads and writes the same keys. Prefix what belongs to one project. No other mod on the server sees them though - Luanti files every key under the mod that wrote it, so nothing here can collide with a game's own data. You do not create this class yourself, it is reached through :attr:`~miney.Luanti.storage`. """ def __init__(self, luanti: "Luanti"): """ :param luanti: The parent :class:`~miney.Luanti` object. """ self.lt = luanti def __repr__(self) -> str: fields = self._fields() if len(fields) > 5: return f"<Luanti Storage: {len(fields)} keys>" return f"<Luanti Storage: {fields!r}>" def _fields(self) -> dict[str, str]: """ The whole store, in one call. Read through ``to_table()`` rather than ``get_string()`` on purpose: Luanti still supports a deprecated ``${key}`` syntax, and ``get_string("${home}")`` returns the value of *home* instead of the text that was stored. ``to_table()`` hands out what was actually written. :return: Every key and value. Empty while nothing is stored. """ fields = self.lt.lua.run("return storage:to_table().fields") # An empty store serializes as nil, not as an empty table. return fields or {} @staticmethod def _check_key(key: str) -> str: """ :param key: The key to validate. :return: The key, unchanged. :raises TypeError: If the key is not a string. :raises ValueError: If the key is empty. """ if not isinstance(key, str): raise TypeError( f"Storage keys are strings, not {type(key).__name__}. " f'Use lt.storage[str({key!r})] if you meant the text.' ) if not key: raise ValueError("Storage keys cannot be empty.") return key
[docs] def __getitem__(self, key: str) -> str: """ :param key: The key to look up. :return: The stored text. :raises KeyError: If the key is not in the store. """ value = self._fields().get(self._check_key(key)) if value is None: raise KeyError(key) return value
[docs] def __setitem__(self, key: str, value: str) -> None: """ :param key: The key to store under. :param value: The text to store. :raises TypeError: If the key or the value is not a string. :raises ValueError: If the key or the value is empty. """ self._check_key(key) if not isinstance(value, str): raise TypeError( f"Storage values are strings, not {type(value).__name__}. " f"Use str({value!r}) for a single value, or json.dumps({value!r}) " f"for a list or a dictionary." ) if not value: raise ValueError( "Storage cannot hold an empty string: Luanti deletes a key when it is " f'set to "". Use "del lt.storage[{key!r}]" if that is what you mean.' ) self.lt.lua.run( f"storage:set_string({self.lt.lua.dumps(key)}, {self.lt.lua.dumps(value)}) " f"return true" )
[docs] def __delitem__(self, key: str) -> None: """ :param key: The key to remove. :raises KeyError: If the key is not in the store. """ self._check_key(key) removed = self.lt.lua.run( f"local key = {self.lt.lua.dumps(key)} " f"if not storage:contains(key) then return false end " f"storage:set_string(key, '') " f"return true" ) if not removed: raise KeyError(key)
[docs] def __iter__(self) -> Iterator[str]: """ :return: An iterator over the keys. """ return iter(self._fields())
[docs] def __len__(self) -> int: """ :return: How many keys are stored. """ return len(self._fields())
[docs] def keys(self) -> KeysView[str]: """ :return: All keys, read in a single call to the server. """ return self._fields().keys()
[docs] def values(self) -> ValuesView[str]: """ :return: All values, read in a single call to the server. """ return self._fields().values()
[docs] def items(self) -> ItemsView[str, str]: """ :return: All key-value pairs, read in a single call to the server. """ return self._fields().items()
[docs] def clear(self) -> None: """ Remove everything from the store. Affects every script using this world, not only yours. """ self.lt.lua.run("storage:from_table(nil) return true") logger.info("Cleared the world storage.")