diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index 3b14de3..d752bc8 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -69,83 +69,6 @@ multiclass_spellslots_by_level = { } -def _resolve_mechanic(mechanic, SuperClass, warning_message=None): - """Take a raw entry in a character sheet and turn it into a usable object. - - Eg: spells can be defined in many ways. This function accepts all - of those options and returns an actual *Spell* class that can be - used by a character:: - - >>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell) - - >>> _resolve_mechanic("mage_hand", SuperClass=None) - - >>> from dungeonsheets import spells - >>> class MySpell(spells.Spell): pass - >>> _resolve_mechanic(MySpell, SuperClass=spells.Spell) - - >>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell) - - The acceptable entries for *mechanic*, in priority order, are: - 1. A subclass of *SuperClass* - 2. A string with the name of defined content - 3. The name of an unknown spell (creates generic object using *factory*) - - *SuperClass* can be ``None`` to match any class, however this will - raise an exception if more than one content type has this - name. For example, "shield" can refer to both the armor or the - spell, so ``_resolve_mechanic("shield")`` will raise an exception. - - Parameters - ========== - mechanic : str, type - The thing to be resolved, either a string with the name of the - mechanic, or a subclass of *ParentClass* describing the - mechanic. - SuperClass : type - Class to determine whether *mechanic* should just be allowed - through as is. - error_message : str, optional - A string whose ``str.format()`` method (receiving one positional - argument *mechanic*) will be used for displaying a warning when an - unknown mechanic is resolved. If omitted, no warning will be - displayed. - - Returns - ======= - Mechanic - A class representing the resolved game mechanic. This will - likely be a subclass of *SuperClass* if the other parameters are - well behaved, but this is not enforced. - - """ - is_already_resolved = isinstance(mechanic, type) and issubclass( - mechanic, SuperClass - ) - if is_already_resolved: - Mechanic = mechanic - else: - try: - # Retrieve pre-defined mechanic - valid_classes = [SuperClass] if SuperClass is not None else [] - Mechanic = find_content(mechanic, valid_classes=valid_classes) - except AttributeError: - # No pre-defined mechanic available - if warning_message is not None: - # Emit the warning - msg = warning_message.format(mechanic) - warnings.warn(msg) - else: - # Create a generic message so we can make a docstring later. - msg = f'Mechanic "{mechanic}" not defined. Please add it.' - # Create generic mechanic from the factory - class_name = "".join([s.title() for s in mechanic.split("_")]) - mechanic_name = mechanic.replace("_", " ").title() - attrs = {"name": mechanic_name, "__doc__": msg, "source": "Unknown"} - Mechanic = type(class_name, (SuperClass,), attrs) - return Mechanic - - class Character(Entity): """A generic player character.""" @@ -628,7 +551,7 @@ class Character(Entity): f'Magic Item "{mitem}" not defined. ' "Please add it to ``magic_items.py``" ) - ThisMagicItem = _resolve_mechanic( + ThisMagicItem = self._resolve_mechanic( mechanic=mitem, SuperClass=magic_items.MagicItem, warning_message=msg, @@ -639,7 +562,7 @@ class Character(Entity): msg = 'Magic Item "{}" not defined. Please add it to ``weapons.py``' wps = set( [ - _resolve_mechanic( + self._resolve_mechanic( w, SuperClass=weapons.Weapon, warning_message=msg ) for w in val @@ -660,7 +583,7 @@ class Character(Entity): _features = [] for f in val: msg = 'Feature "{}" not defined. Please add it to ``features.py``' - ThisFeature = _resolve_mechanic( + ThisFeature = self._resolve_mechanic( mechanic=f, SuperClass=features.Feature, warning_message=msg, @@ -672,7 +595,7 @@ class Character(Entity): _spells = [] for spell_name in val: msg = 'Spell "{}" not defined. Please add it to ``spells.py``' - ThisSpell = _resolve_mechanic( + ThisSpell = self._resolve_mechanic( mechanic=spell_name, SuperClass=spells.Spell, warning_message=msg, @@ -695,7 +618,7 @@ class Character(Entity): "Infusion '{}' not defined. Please add it to" " ``infusions.py``" ) - ThisInfusion = _resolve_mechanic( + ThisInfusion = self._resolve_mechanic( mechanic=infusion_name, SuperClass=infusions.Infusion, warning_message=msg, @@ -767,6 +690,14 @@ class Character(Entity): final_text += "." return final_text + @proficiencies_text.setter + def proficiencies_text(self, val): + try: + profs = profiencies_text.split(",") + except AttributeError: + profs = proficiencies_text + self._proficiencies_text = profs + @property def features_text(self): s = "\n\n--".join( @@ -804,7 +735,7 @@ class Character(Entity): new_armor = new_armor else: msg = 'Unnown armor "{}". Please add it to ``armor.py``.' - NewArmor = _resolve_mechanic( + NewArmor = self._resolve_mechanic( mechanic=new_armor, SuperClass=armor.Armor, warning_message=msg, @@ -823,11 +754,8 @@ class Character(Entity): """ if shield not in ("", "None", None): - try: - NewShield = find_content(shield, valid_classes=[armor.Shield]) - except AttributeError: - # Not a string, so just treat it as Armor - NewShield = shield + msg = 'Unknown shield "{}". Please ad it to ``shields.py``.' + NewShield = self._resolve_mechanic(shield, SuperClass=armor.Shield, warning_message=msg) self.shield = NewShield() def wield_weapon(self, weapon): @@ -840,15 +768,12 @@ class Character(Entity): """ # Retrieve the weapon class from the weapons module - if isinstance(weapon, weapons.Weapon): - ThisWeapon = type(weapon) - else: - msg = 'Unknown weapon "{}". Please add it to ``weapons.py``.' - ThisWeapon = _resolve_mechanic( - mechanic=weapon, - SuperClass=weapons.Weapon, - warning_message=msg, - ) + msg = 'Unknown weapon "{}". Please add it to ``weapons.py``.' + ThisWeapon = self._resolve_mechanic( + mechanic=weapon, + SuperClass=weapons.Weapon, + warning_message=msg, + ) # Save it to the array self._weapons.append(ThisWeapon(wielder=self)) diff --git a/dungeonsheets/classes/druid.py b/dungeonsheets/classes/druid.py index 0a6fb93..3482bec 100644 --- a/dungeonsheets/classes/druid.py +++ b/dungeonsheets/classes/druid.py @@ -274,7 +274,7 @@ class Druid(CharClass): try: NewMonster = find_content(shape, valid_classes=[monsters.Monster]) new_shape = NewMonster() - except AttributeError: + except exceptions.ContentNotFound: msg = ( f"Wild shape '{shape}' not found. Please add it to" " ``monsters.py``" diff --git a/dungeonsheets/content_registry.py b/dungeonsheets/content_registry.py index 0751029..0c55969 100644 --- a/dungeonsheets/content_registry.py +++ b/dungeonsheets/content_registry.py @@ -13,6 +13,7 @@ from dungeonsheets import ( infusions, magic_items, features, + exceptions, ) @@ -38,7 +39,11 @@ class ContentRegistry: """ # Come up with several options - name = name.strip() + try: + name = name.strip() + except AttributeError as e: + # Probably not a string + raise ValueError('content "%s" is not a valid identifier string.' % name) # check for +X weapons, armor, shields bonus = 0 for i in range(1, 11): @@ -72,9 +77,9 @@ class ContentRegistry: found_attrs = [attr for attr, v in zip(found_attrs, is_valid) if v] # Check that we found a valid, unique attribute if len(found_attrs) == 0: - raise AttributeError(f"Modules {self.modules} have no attribute {name}") + raise exceptions.ContentNotFound(f"Modules {self.modules} have no attribute {name}") elif len(found_attrs) > 1: - raise RuntimeError(f"Found multiple content entries for {name}") + raise exceptions.AmbiguousContent(f"Found multiple content entries for {name}") else: attr = found_attrs[0] # Apply weapon/etc. bonuses @@ -100,7 +105,7 @@ default_content_registry.add_module(magic_items) default_content_registry.add_module(features) -def find_content(name: str, valid_classes: Optional[List]): +def find_content(name: str, valid_classes: Optional[List] = None): """Find content from a previously registered module. Parameters diff --git a/dungeonsheets/entity.py b/dungeonsheets/entity.py index a33b6da..763c8e0 100644 --- a/dungeonsheets/entity.py +++ b/dungeonsheets/entity.py @@ -1,6 +1,9 @@ from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill from abc import ABC import os +import warnings + +from dungeonsheets.content_registry import find_content def read(fname): @@ -109,4 +112,83 @@ class Entity(ABC): self.medicine, self.nature, self.perception, self.performance, self.persuasion, self.religion, self.sleight_of_hand, self.stealth, self.survival,] - + + @staticmethod + def _resolve_mechanic(mechanic, SuperClass, warning_message=None): + """Take a raw entry in a character sheet and turn it into a usable object. + + Eg: spells can be defined in many ways. This function accepts all + of those options and returns an actual *Spell* class that can be + used by a character:: + + >>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell) + + >>> _resolve_mechanic("mage_hand", SuperClass=None) + + >>> from dungeonsheets import spells + >>> class MySpell(spells.Spell): pass + >>> _resolve_mechanic(MySpell, SuperClass=spells.Spell) + + >>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell) + + The acceptable entries for *mechanic*, in priority order, are: + 1. A subclass of *SuperClass* + 2. A string with the name of defined content + 3. The name of an unknown spell (creates generic object using *factory*) + + *SuperClass* can be ``None`` to match any class, however this will + raise an exception if more than one content type has this + name. For example, "shield" can refer to both the armor or the + spell, so ``_resolve_mechanic("shield")`` will raise an exception. + + Parameters + ========== + mechanic : str, type + The thing to be resolved, either a string with the name of the + mechanic, or a subclass of *ParentClass* describing the + mechanic. + SuperClass : type + Class to determine whether *mechanic* should just be allowed + through as is. + error_message : str, optional + A string whose ``str.format()`` method (receiving one positional + argument *mechanic*) will be used for displaying a warning when an + unknown mechanic is resolved. If omitted, no warning will be + displayed. + + Returns + ======= + Mechanic + A class representing the resolved game mechanic. This will + likely be a subclass of *SuperClass* if the other parameters are + well behaved, but this is not enforced. + + """ + is_already_resolved = isinstance(mechanic, type) and issubclass( + mechanic, SuperClass + ) + if is_already_resolved: + Mechanic = mechanic + elif SuperClass is not None and isinstance(mechanic, SuperClass): + # Has been instantiated for some reason + Mechanic = type(Mechanic) + else: + try: + # Retrieve pre-defined mechanic + valid_classes = [SuperClass] if SuperClass is not None else [] + Mechanic = find_content(mechanic, valid_classes=valid_classes) + except ValueError: + # No pre-defined mechanic available + if warning_message is not None: + # Emit the warning + msg = warning_message.format(mechanic) + warnings.warn(msg) + else: + # Create a generic message so we can make a docstring later. + msg = f'Mechanic "{mechanic}" not defined. Please add it.' + # Create generic mechanic from the factory + class_name = "".join([s.title() for s in mechanic.split("_")]) + mechanic_name = mechanic.replace("_", " ").title() + attrs = {"name": mechanic_name, "__doc__": msg, "source": "Unknown"} + Mechanic = type(class_name, (SuperClass,), attrs) + return Mechanic diff --git a/dungeonsheets/exceptions.py b/dungeonsheets/exceptions.py index a8263ac..4273f93 100644 --- a/dungeonsheets/exceptions.py +++ b/dungeonsheets/exceptions.py @@ -28,3 +28,10 @@ class UnknownFileType(RuntimeError): class UnknownOutputFormat(RuntimeError): """The output format requested is not one of the known outputs.""" + + +class ContentNotFound(ValueError): + """The requested content could not be resolved.""" + +class AmbiguousContent(ValueError): + """Multiple valid content entries were found.""" diff --git a/dungeonsheets/forms/spellbook_template.html b/dungeonsheets/forms/spellbook_template.html index 46e2a46..f2daf31 100644 --- a/dungeonsheets/forms/spellbook_template.html +++ b/dungeonsheets/forms/spellbook_template.html @@ -96,8 +96,8 @@