diff --git a/libs/inventory/inventory/Equipment.py b/libs/inventory/inventory/Equipment.py index a4b9cdf..f846d0f 100644 --- a/libs/inventory/inventory/Equipment.py +++ b/libs/inventory/inventory/Equipment.py @@ -1,9 +1,14 @@ import os import sys import enum +import inspect +import pennant import narrator import sqlite3 +from typing import Any +from arglite import parser as cliarg + from .Item import RelicSpec from .Instantiator import Instance @@ -12,10 +17,11 @@ class Equipment: # TODO: There are a lot of duplicated methods testing # types, et al. We need to remove/conslidate them. - def choose_equip_side(sides: list = []) -> str: - """ Deprecated, or at least out of current use (RETAIN) """ - if type(sides) == str or len(sides) == 1: - return [sides][-1] + def choose_equip_side(sides) -> str: + if type(sides) == RelicSpec.Slots: + return sides.value + if type(sides) == list and len(sides) == 1: + return sides[-1].value q = narrator.Question({ "question": "Equip to which side?\n", "responses": [ @@ -26,15 +32,24 @@ def choose_equip_side(sides: list = []) -> str: # TODO: Seems to belong in Validator, tho. @staticmethod - def verify_valid_slot(name: str = "", slot: str = "") -> bool: + def verify_valid_slot(name: str = "", slot: Any = "") -> bool: + # Jump the queue if unequipping! + if inspect.stack()[1].function == "unequip": + return True instance = Instance(name) - return instance.get_property("slot")["location"] in RelicSpec.Slots + slots = instance.get_property("slot")["location"] + if type(slots) == RelicSpec.Slots: + slots = [slots] + for slot in slots: + if slot not in RelicSpec.Slots: return False + return True @staticmethod def configure(conn: sqlite3.Connection) -> None: + """ Configure table on first-time run """ cursor = conn.cursor() - # Create equipment table + # Create equipment table cursor.execute( """ CREATE TABLE IF NOT EXISTS equipment ( @@ -60,9 +75,12 @@ def configure(conn: sqlite3.Connection) -> None: (slot.value,) ) conn.commit() + # Set trigger to validate slot assignment on update conn.create_function("verify_valid_slot", 2, Equipment.verify_valid_slot) - sqlite3.enable_callback_tracebacks(True) + + with pennant.FEATURE_FLAG_CODE(cliarg.optional.debug): + sqlite3.enable_callback_tracebacks(True) cursor.execute( """ @@ -78,7 +96,6 @@ def configure(conn: sqlite3.Connection) -> None: ) # Set trigger to prevent additional slot creation - # TODO: Reenable when finished with table creation cursor.execute( """ CREATE TRIGGER IF NOT EXISTS inv_equipment_limit_slots @@ -105,7 +122,9 @@ def discover(cursor: sqlite3.Cursor, name: str = "") -> str: @staticmethod def equip(conn: sqlite3.Connection, name: str = "") -> bool: instance = Instance(name) - slot = instance.get_property("slot")["location"].value + slot = Equipment.choose_equip_side( + instance.get_property("slot")["location"] + ) cursor = conn.cursor() try: cursor.execute( @@ -122,6 +141,36 @@ def equip(conn: sqlite3.Connection, name: str = "") -> bool: sys.exit() return bool(cursor.rowcount) + @staticmethod + def unequip(conn: sqlite3.Connection, name: str = "") -> bool: + instance = Instance(name) + # TODO: Fix for multi-slot cases (iteratives). + slots = instance.get_property("slot")["location"] + if type(slots) == RelicSpec.Slots: + slots = [slots] + cursor = conn.cursor() + for slot in slots: + cursor.execute( + """ + UPDATE equipment + SET name = "" + WHERE name = ? AND slot = ? + """, + (name, slot.value, ) + ) + if cursor.rowcount == 1: + conn.commit() + break + + @staticmethod + def show(cursor: sqlite3.Cursor): + cursor.execute( + """ + SELECT slot, name FROM equipment; + """ + ) + return cursor.fetchall() + class EquipError(Exception): def __init__(self, item:str, *args): diff --git a/libs/inventory/inventory/Instantiator.py b/libs/inventory/inventory/Instantiator.py index 223ea17..0428899 100644 --- a/libs/inventory/inventory/Instantiator.py +++ b/libs/inventory/inventory/Instantiator.py @@ -5,29 +5,14 @@ class Instance: def __init__(self, item: str = ""): """ Instantiate object to access runnable properties """ try: - item_file = importlib.import_module(f"{item}") - self.instance = getattr(item_file, item)() + self.module = importlib.import_module(f"{item}") + self.uninst = getattr(self.module, item) + self.object = self.uninst() + self.serial = self.uninst.dillable(self.uninst) except ModuleNotFoundError: print(f"It seems you don't have any {item}.") exit() - except: + except Exception as e: + print(e) print(f"{item} doesn't seem to be a valid object.") exit() - - def has_property(self, prop: str = "") -> bool: - try: - getattr(self.instance, prop) - return True - except: - pass - return False - - def get_property(self, prop: str = ""): - try: - return getattr(self.instance, prop) - except: - pass - - def is_child_of(self, item_type) -> bool: - res_order = self.instance.__mro__ - print(item_type in res_order) diff --git a/libs/inventory/inventory/Inventory.py b/libs/inventory/inventory/Inventory.py index 44cf115..0b10cbd 100644 --- a/libs/inventory/inventory/Inventory.py +++ b/libs/inventory/inventory/Inventory.py @@ -6,6 +6,7 @@ import importlib import shutil import sqlite3 +import pennant from rich.table import Table from rich.console import Console @@ -14,13 +15,7 @@ from .Config import * from .Equipment import * -# TODO: Why not just import *? -from .Item import ItemSpec -from .Item import FixtureSpec -from .Item import BoxSpec -from .Item import OutOfError -from .Item import IsFixture -from .Item import Factory +from .Item import * from .Validation import Validator from .Instantiator import Instance @@ -61,11 +56,14 @@ def is_relic(self, item) -> bool: def locate(self, filename: str = "") -> None: """ Locates item file in current working directory """ + # TODO: Revise method to look in directory and prompt if there + # are multiple similarly-named items. self.name, self.ext = self.filename.split("/")[-1].split(".") self.name = re.search(r"[a-zA-Z]+", self.name).group(0) self.box = Validator.is_box(self.name) - self.filename = f"{self.name}.{self.ext}" + self.filename = f"{self.name}" # Removed {self.ext}; do we need it? + # TODO: This is how we dill it def move(self): """ Move the file acquired to the inventory directory """ try: @@ -75,21 +73,21 @@ def move(self): if not self.box: instance = Instance(self.name) try: - shutil.copy(self.filename, path) - except: + with open(f"{path}","wb") as fh: + # TODO: Class inheritance not found? + dill.dump(instance.serial, fh) + #shutil.copy(self.filename, path) + except Exception as e: # This operation attempts to move the file # based on real file name; however, if this # fails it might be OK pass - #obj = importlib.import_module(self.name) if "ItemSpec" in dir(instance) or "RelicSpec" in dir(instance): # Remove only the physically present copy os.remove(f"./{self.item}") except Exception as e: # TODO: Differentiate levels of inacquisition. For - # example, change the message here to reflect - # _why_ something couldn't Acquire; see add - # method below as well. + # example, use different exceptions defensively. print(f"Couldn't acquire {self.name}") sys.exit() @@ -109,12 +107,10 @@ def add(self): else: print(f"Couldn't acquire {self.quantity} {self.name}: Max Volume exceeded") sys.exit() - # TODO: Add resistance for certain magical items or level needs? class Registry: - # File operations def __init__(self): """ Constructor """ self.inventory = {} @@ -129,7 +125,6 @@ def __init__(self): self.__convert_json_file() os.unlink(f"{self.path}/.registry") - # Create inventory SQL table def __create_inv_sql_table(self): """ Create tables for inventory and other needs based on WORLD_NAME """ cursor = self.conn.cursor() @@ -145,10 +140,9 @@ def __create_inv_sql_table(self): ); """ ) - if WORLD == "venture": + with pennant.FEATURE_FLAG_CODE(WORLD == "venture"): Equipment.configure(conn = self.conn) - # Convert legacy JSON file (DEPRECATE WHEN PRACTICAL) def __convert_json_file(self): """ Convert JSON file from earlier versions of topia """ with open(os.path.expanduser( @@ -241,7 +235,6 @@ def total_volume(self) -> int: def add(self, item: str, number: int = 1) -> None: """ API to add an item to the inventory DB """ - # TODO: Doesn't add if there aren't any already? cursor = self.conn.cursor() cursor.execute( f""" @@ -255,7 +248,7 @@ def add(self, item: str, number: int = 1) -> None: if cursor.rowcount != 1: self.__add_table_entry( name = item, - filename = f"{item}.py", + filename = f"{item}", quantity = number ) self.__remove_zero_quantity_items() @@ -268,7 +261,7 @@ def remove(self, item: str, number: int = -1) -> None: def search(self, item: str = "") -> dict: """ API to search inventory database """ # TODO: Expand to allow for multiple item search - # using OR logic + # using OR logic...er...nah. cursor = self.conn.cursor() cursor.execute( """ @@ -284,7 +277,6 @@ def search(self, item: str = "") -> dict: } return {} - # Create a nice(r) display def display(self): """ Display contents of inventory to the terminal """ table = Table(title=f"{os.getenv('LOGNAME')}'s inventory") @@ -292,25 +284,28 @@ def display(self): table.add_column("Item count") table.add_column("Consumable") table.add_column("Volume") - if WORLD == "venture": + with pennant.FEATURE_FLAG_CODE(WORLD == "venture"): table.add_column("Equippable") table.add_column("Durability") table.add_column("Equipped") - # TODO: Move query to its own method? cursor = self.conn.cursor() cursor.execute(""" SELECT name, filename, quantity, consumable, volume FROM items """) + path = os.path.expanduser( + f"{Config.values['INV_PATH']}" + ) + for name, filename, quantity, consumable, volume in cursor.fetchall(): - # Feature-flag the rows; columns already are data = [str(name), str(quantity), str(bool(consumable)), str(volume)] - if WORLD == "venture": - instance = Instance(name) + with pennant.FEATURE_FLAG_CODE(WORLD == "venture"): + with open(f"{path}/{name}", "rb") as fh: + instance = dill.load(fh) data += [ - str(True if instance.get_property("slot") else False), - str(instance.get_property("durability") or "-"), + str(True if instance.slot else False), + str(instance.durability or "-"), str(Equipment.discover(cursor, name) or "-") ] table.add_row(*data) @@ -324,21 +319,20 @@ def __init__(self, registry): """ Constructor """ self.inv = registry - def is_fixture(self, item) -> bool: + @staticmethod + def is_fixture(mro: list = []) -> bool: """ Returns fixture specification status """ return "FixtureSpec" in dir(item) - def is_box(self, item) -> bool: + @staticmethod + def is_box(mro: list = []) -> bool: """ Returns box specification status """ return "BoxSpec" in dir(item) - def is_relic(self, item) -> bool: + @staticmethod + def is_relic(mro: list = []) -> bool: return "RelicSpec" in dir(item) - def file_exists(self, item) -> bool: - """ Checks if item file exists in inventory """ - return os.path.exists(f"{self.inv.path}/{item}.py") - def trash(self, item: str, quantity: int = 1) -> None: """ Removes item from the list; tied to the "remove" .bashrc alias """ try: @@ -356,17 +350,17 @@ def drop(self, item: str = "", quantity: int = 1) -> None: result = registry.search(item = item) if not result: raise OutOfError(item) - # Convert the quantity to an integer if not already one - quantity = int(quantity) - # Test if the number being dropped is more than we have - # and limit the drops to only the quantity that we have - if quantity > result["quantity"]: - quantity = result["quantity"] except OutOfError: print(f"It doesn't look like you have any {item}.") sys.exit() except ValueError: quantity = 1 + # Convert the quantity to an integer if not already one + quantity = int(quantity) + # Test if the number being dropped is more than we have + # and limit the drops to only the quantity that we have + if quantity > result["quantity"]: + quantity = result["quantity"] try: for _ in range(quantity): Factory(item) @@ -374,17 +368,40 @@ def drop(self, item: str = "", quantity: int = 1) -> None: except: pass + @pennant.FEATURE_FLAG_FUNCTION(WORLD == "venture") def equip(self, item: str): try: result = registry.search(item = item) if not result: - raise OutOfError + raise OutOfError(item) Equipment.equip(registry.conn, item) except OutOfError: print(f"It doesn't look like you have any {item}.") - exit() + sys.exit() + + @pennant.FEATURE_FLAG_FUNCTION(WORLD == "venture") + def unequip(self, item: str): + try: + result = registry.search(item = item) + if not result: + raise OutOfError(item) + Equipment.unequip(registry.conn, item) + except OutOfError: + print(f"It doesn't look like you have any {item}.") + sys.exit() + + @pennant.FEATURE_FLAG_FUNCTION(WORLD == "venture") + def equipped(self): + table = Table(title=f"{os.getenv('LOGNAME')}'s equipment") + table.add_column("Slot") + table.add_column("Item") + for slot, value in Equipment.show(registry.conn.cursor()): + table.add_row(slot, value) + console = Console() + console.print(table) - def use(self, item: str): + @classmethod + def use(self, item: str = ""): """ Uses an item from the inventory """ # Set up properties and potential kwargs box = False @@ -392,26 +409,21 @@ def use(self, item: str): # Verify that item is in path or inventory try: - # TODO: Replace with Instantiator instance - item_file = importlib.import_module(f"{item}") + path = os.path.expanduser( + f"{Config.values['INV_PATH']}/{item}" + ) + with open(path, "rb") as fh: + instance = dill.load(fh) except ModuleNotFoundError: print(f"You don't seem to have any {item}.") sys.exit() - # Reflect the class - try: - # TODO: Use Instantiator instance - instance = getattr(item_file, item)() - except: - print(f"{item} doesn't seem to be a valid object.") - sys.exit() - # Test type of item; remove if ItemSpec try: record = registry.search(item) - box = self.is_box(item_file) - relic = self.is_relic(item_file) - fixture = self.is_fixture(item_file) + # Retrieves superclasses from MRO; prevents + # incompatible use cases + mro = [cls.__name__ for cls in instance.__mro__] # Only decrease quantity if item is consumable if instance.consumable: registry.remove(item) @@ -422,12 +434,11 @@ def use(self, item: str): registry.remove(item = item) sys.exit() except IsFixture: pass - # Return the result or inbuilt use method if type(instance).__str__ is not object.__str__: - instance.use(**instance.actions) + instance.use(instance, **instance.actions) else: - return instance.use(**instance.actions) + return instance.use(instance, **instance.actions) # Create instances to use as shorthand. I thought this was a bad idea, # but this is actually how the random module works: diff --git a/libs/inventory/inventory/Item.py b/libs/inventory/inventory/Item.py index b4ee2c6..7790c52 100644 --- a/libs/inventory/inventory/Item.py +++ b/libs/inventory/inventory/Item.py @@ -18,22 +18,53 @@ class ItemSpec: def __init__(self, filename: str = ""): + """ Constructor """ self.file = filename self.actions = {} + # TODO: Refactor to use arglite; this is the + # snippet it came from, after all arg_pairs = self.pairs(sys.argv) for arg, val in arg_pairs: if re.match(r"^-{1,2}", arg): arg = arg.replace("-","") self.actions[arg] = val + # Constant properties self.consumable = True self.equippable = False self.unique = False self.VOLUME = 1 self.vars() + # The basis for this portable dill'ing brought to you by the kind folks at: + # https://oegedijk.github.io/blog/pickle/dill/python/2020/11/10/serializing-dill-references.html + + def mainify(self, props: dict = {}): + if self.__module__ != "__main__": + import __main__ + import inspect + source = inspect.getsource(self) + # Inject inhertiable classes before source; they + # don't come over via inspect methods + # TODO: Limit to the actual inheritables? + source = f"from inventory.Item import *\n\n{source}" + co = compile(source, '', 'exec') + exec(co, __main__.__dict__) + + @classmethod + def dillable(self, instance): + import __main__ + instance.mainify(instance) + cls = getattr(__main__, self.__name__) + props = instance().__dict__ + for prop in props: + setattr(cls, str(prop), props[prop]) + return cls + + # TODO: See above note on arglite def pairs(self, args: list = []): return [args[i*2:(i*2)+2] for i in range(len(args)-2)] + # TODO: See above note about above note on arglite def vars(self) -> None: for arg in self.actions: setattr(self, arg, self.actions[arg]) @@ -75,6 +106,8 @@ class Slots(enum.Enum): BELT = "BELT" LEGS = "LEGS" FEET = "FEET" + WEAPON_LEFT = "LEFT WEAPON" + WEAPON_RIGHT = "RIGHT WEAPON" class Sides(enum.Enum): RIGHT = "right" @@ -90,6 +123,8 @@ def __init__(self, filename: str = ""): "location": self.Slots.HANDS, } + # TODO: Determine if the below are really necessary. + #def __validate_slot_value(self, slot: str = ""): # slots = Slots._value2member_map_ # return slot in slots @@ -98,8 +133,16 @@ def __init__(self, filename: str = ""): # sides = Sides._value2member_map_ # return side in sides +class WeaponSpec(RelicSpec): + + def __init__(self, filename: str = ""): + super().__init__(filename) + self.unique = True + class Factory: + # TODO: Finish springform, dammit. + def __init__(self, name, path: str = "", item_type: any = ItemSpec, template: str = "", **kwargs): """ Creates items from templates """ self.path = path diff --git a/libs/inventory/inventory/Validation.py b/libs/inventory/inventory/Validation.py index b4a3366..c423a57 100644 --- a/libs/inventory/inventory/Validation.py +++ b/libs/inventory/inventory/Validation.py @@ -1,4 +1,5 @@ import re +import dill import importlib from .Item import BoxSpec diff --git a/libs/inventory/requirements.txt b/libs/inventory/requirements.txt index 51e315f..ce0cbb3 100644 --- a/libs/inventory/requirements.txt +++ b/libs/inventory/requirements.txt @@ -2,3 +2,4 @@ gitit python-dotenv rich dill +pennant diff --git a/libs/narrator/narrator/Question.py b/libs/narrator/narrator/Question.py index 21d8dc2..3a4c69c 100644 --- a/libs/narrator/narrator/Question.py +++ b/libs/narrator/narrator/Question.py @@ -14,7 +14,7 @@ def is_key(self, char: str) -> bool: return False def set_opt(self, option: dict) -> dict: - choice = option["choice"] + choice = option["choice"].lower() for letter in choice: if not self.is_key(letter): opt = Option(letter, option)