def test_findattr(self): """Check if the function can find attributes.""" class TestClass(): my_attr = 47 YourAttr = 53 test_class = TestClass() # Direct access self.assertEqual(stats.findattr(test_class, 'my_attr'), test_class.my_attr) self.assertEqual(stats.findattr(test_class, 'YourAttr'), test_class.YourAttr) # Swapping spaces for capitalization self.assertEqual(stats.findattr(test_class, 'my attr'), test_class.my_attr) self.assertEqual(stats.findattr(test_class, 'your attr'), test_class.YourAttr)
def background(self, bg): if isinstance(bg, background.Background): self._background = bg self._background.owner = self elif isinstance(bg, type) and issubclass(bg, background.Background): self._background = bg(owner=self) elif isinstance(bg, str): try: self._background = findattr(background, bg)(owner=self) except AttributeError: msg = (f'Background "{bg}" not defined. ' f'Please add it to ``background.py``') self._background = background.Background(owner=self) warnings.warn(msg)
def make_gm_sheet( basename: str, gm_props: Mapping, fancy_decorations: bool = False, debug: bool = False, ): """Prepare a PDF character sheet from the given character file. Parameters ---------- basename The basename for saving files. gm_props Properties for creating the GM notes. fancy_decorations Use fancy page layout and decorations for extra sheets, namely the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template. debug Provide extra info and preserve temporary files. """ tex = [ jinja_env.get_template("preamble.tex").render( use_dnd_decorations=fancy_decorations, title=gm_props["session_title"], ) ] # Add the monsters monsters_ = [findattr(monsters, m)() for m in gm_props.get("monsters", [])] if len(monsters_) > 0: tex.append( create_monsters_tex(monsters_, use_dnd_decorations=fancy_decorations) ) # Add the closing TeX tex.append( jinja_env.get_template("postamble.tex").render( use_dnd_decorations=fancy_decorations ) ) # Typeset combined LaTeX file try: if len(tex) > 2: latex.create_latex_pdf( tex="".join(tex), basename=basename, keep_temp_files=debug, use_dnd_decorations=fancy_decorations, ) except exceptions.LatexNotFoundError: log.warning(f"``pdflatex`` not available. Skipping {basename}")
def race(self, newrace): if isinstance(newrace, race.Race): self._race = newrace self._race.owner = self elif isinstance(newrace, type) and issubclass(newrace, race.Race): self._race = newrace(owner=self) elif isinstance(newrace, str): try: self._race = findattr(race, newrace)(owner=self) except AttributeError: msg = f'Race "{newrace}" not defined. Please add it to ``race.py``' self._race = race.Race(owner=self) warnings.warn(msg) elif newrace is None: self._race = race.Race(owner=self)
def wield_shield(self, shield): """Accepts a string or Shield class and replaces the current armor. If a string is given, then a subclass of :py:class:`~dungeonsheets.armor.Shield` is retrived from the ``armor.py`` file. Otherwise, an subclass of :py:class:`~dungeonsheets.armor.Shield` can be provided directly. """ if shield not in ('', 'None', None): try: NewShield = findattr(armor, shield) except AttributeError: # Not a string, so just treat it as Armor NewShield = shield self.shield = NewShield()
def wear_armor(self, new_armor): """Accepts a string or Armor class and replaces the current armor. If a string is given, then a subclass of :py:class:`~dungeonsheets.armor.Armor` is retrived from the ``armor.py`` file. Otherwise, an subclass of :py:class:`~dungeonsheets.armor.Armor` can be provided directly. """ if new_armor not in ('', 'None', None): if isinstance(new_armor, armor.Armor): new_armor = new_armor else: NewArmor = findattr(armor, new_armor) new_armor = NewArmor() self.armor = new_armor
def wild_shapes(self, new_shapes): actual_shapes = [] # Retrieve the actual monster classes if possible for shape in new_shapes: if isinstance(shape, monsters.Monster): # Already a monster shape so just add it as is new_shape = shape else: # Not already a monster so see if we can find one try: NewMonster = findattr(monsters, shape) new_shape = NewMonster() except AttributeError: msg = f'Wild shape "{shape}" not found. Please add it to ``monsters.py``' raise exceptions.MonsterError(msg) actual_shapes.append(new_shape) # Save the updated list for later self._wild_shapes = actual_shapes
def wield_weapon(self, weapon): """Accepts a string and adds it to the list of wielded weapons. Parameters ---------- weapon : str Case-insensitive string with a name of the weapon. """ # Retrieve the weapon class from the weapons module if isinstance(weapon, weapons.Weapon): weapon_ = type(weapon)(wielder=self) elif isinstance(weapon, str): try: NewWeapon = findattr(weapons, weapon) except AttributeError: raise AttributeError(f'Weapon "{weapon}" is not defined') weapon_ = NewWeapon(wielder=self) elif issubclass(weapon, weapons.Weapon): weapon_ = weapon(wielder=self) else: raise AttributeError(f'Weapon "{weapon}" is not defined') # Save it to the array self.weapons.append(weapon_)
def set_attrs(self, **attrs): """Bulk setting of attributes. Useful for loading a character from a dictionary.""" for attr, val in attrs.items(): if attr == 'dungeonsheets_version': pass # Maybe we'll verify this later? elif attr == 'weapons': if isinstance(val, str): val = [val] # Treat weapons specially for weap in val: self.wield_weapon(weap) elif attr == 'magic_items': if isinstance(val, str): val = [val] for mitem in val: try: self.magic_items.append(findattr(magic_items, mitem)(owner=self)) except (AttributeError): msg = (f'Magic Item "{mitem}" not defined. ' f'Please add it to ``magic_items.py``') warnings.warn(msg) elif attr == 'weapon_proficiencies': self.other_weapon_proficiencies = () wps = set([findattr(weapons, w) for w in val]) wps -= set(self.weapon_proficiencies) self.other_weapon_proficiencies = list(wps) elif attr == 'armor': self.wear_armor(val) elif attr == 'shield': self.wield_shield(val) elif attr == 'circle': if hasattr(self, 'Druid'): self.Druid.circle = val elif attr == 'features': if isinstance(val, str): val = [val] _features = [] for f in val: try: _features.append(findattr(features, f)) except AttributeError: msg = (f'Feature "{f}" not defined. ' f'Please add it to ``features.py``') # create temporary feature _features.append(features.create_feature( name=f, source='Unknown', __doc__="""Unknown Feature. Add to features.py""")) warnings.warn(msg) self.custom_features += tuple(F(owner=self) for F in _features) elif (attr == 'spells') or (attr == 'spells_prepared'): # Create a list of actual spell objects _spells = [] for spell_name in val: try: _spells.append(findattr(spells, spell_name)) except AttributeError: msg = (f'Spell "{spell_name}" not defined. ' f'Please add it to ``spells.py``') warnings.warn(msg) # Create temporary spell _spells.append(spells.create_spell(name=spell_name, level=9)) # raise AttributeError(msg) # Sort by name _spells.sort(key=lambda spell: spell.name) # Save list of spells to character atribute if attr == 'spells': # Instantiate them all for the spells list self._spells = tuple(S() for S in _spells) else: # Instantiate them all for the spells list self._spells_prepared = tuple(S() for S in _spells) else: if not hasattr(self, attr): warnings.warn(f"Setting unknown character attribute {attr}", RuntimeWarning) # Lookup general attributes setattr(self, attr, val)
def _resolve_mechanic(mechanic, module, 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:: >>> from dungeonsheets import spells >>> _resolve_mechanic("mage_hand", spells, None) >>> class MySpell(spells.Spell): pass >>> _resolve_mechanic(MySpell, None, spells.Spell) >>> _resolve_mechanic("hocus pocus", spells, None) The acceptable entries for *mechanic*, in priority order, are: 1. A subclass of *SuperClass* 2. A string with the name of a defined spell in *module* 3. The name of an unknown spell (creates generic object using *factory*) 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. module : module A python module in which to look for the defined string in *name*. 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 Mechanic = findattr(module, mechanic) 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