Files
dungeon-sheets/dungeonsheets/content_registry.py
T

149 lines
4.7 KiB
Python

from pathlib import Path
from functools import lru_cache
import importlib.util
from typing import Union, List, Optional
from dungeonsheets import (
weapons,
monsters,
race,
background,
armor,
spells,
infusions,
magic_items,
features,
exceptions,
)
class ContentRegistry:
modules = None
def __init__(self):
self.modules = []
def add_module(self, new_module):
if new_module not in self.modules:
self.modules.append(new_module)
def findattr(self, name, valid_classes=[]):
"""Resolve the name of a piece of content to the corresponding Class.
Similar to builtin getattr(obj, name) but more forgiving to
whitespace and capitalization.
valid_classes
If given, only subclasses of classes in this list will be
returned.
"""
# Come up with several options
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):
if (f"+{i}" in name) or (f"+ {i}" in name):
bonus = i
name = name.replace(f"+{i}", "").replace(f"+ {i}", "")
break
py_name = (
name.replace("-", "_").replace(" ", "_").replace("'", "").replace("/", "")
)
camel_case = "".join([s.capitalize() for s in py_name.split("_")])
# Check each module in the registry
found_attrs = []
for module in self.modules:
if hasattr(module, py_name):
# Direct lookup
found_attrs.append(getattr(module, py_name))
elif hasattr(module, camel_case):
# CamelCase lookup
found_attrs.append(getattr(module, camel_case))
# Filter by valid classes
if len(valid_classes) > 0:
is_valid = [False for attr in found_attrs]
for cls in valid_classes:
is_valid = [
v
or isinstance(attr, cls)
or (isinstance(attr, type) and issubclass(attr, cls))
for v, attr in zip(is_valid, found_attrs)
]
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 exceptions.ContentNotFound(f"Modules {self.modules} have no attribute {name}")
elif len(found_attrs) > 1:
raise exceptions.AmbiguousContent(f"Found multiple content entries for {name}")
else:
attr = found_attrs[0]
# Apply weapon/etc. bonuses
if bonus > 0:
if (
issubclass(attr, weapons.Weapon)
or issubclass(attr, armor.Shield)
or issubclass(attr, armor.Armor)
):
attr = attr.improved_version(bonus)
return attr
default_content_registry = ContentRegistry()
default_content_registry.add_module(weapons)
default_content_registry.add_module(monsters)
default_content_registry.add_module(race)
default_content_registry.add_module(background)
default_content_registry.add_module(armor)
default_content_registry.add_module(spells)
default_content_registry.add_module(infusions)
default_content_registry.add_module(magic_items)
default_content_registry.add_module(features)
def find_content(name: str, valid_classes: Optional[List] = None):
"""Find content from a previously registered module.
Parameters
==========
name
The name of the item to find. Can be "CamelCase", "under_case",
or "lower case".
valid_classes
A list of parent classes to look for. If ``None`` or ``[]``, all
classes will be considered valid.
"""
if valid_classes is None:
valid_classes = []
return default_content_registry.findattr(name, valid_classes=valid_classes)
@lru_cache()
def import_homebrew(filepath: Union[str, Path]):
"""Import a module file containing homebrew content.
This is intended to be used in a character/GM sheet to load in
homebrew content defined in an external file.
Parameters
==========
filepath
The location of the python file containing the homebrew content.
Returns
=======
mod
The imported module of homebrew content.
"""
spec = importlib.util.spec_from_file_location("module.name", filepath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
default_content_registry.add_module(mod)
return mod