mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
4f8a0e442b
with check for *improved_version* method. Fixes https://github.com/canismarko/dungeon-sheets/issues/112
188 lines
6.3 KiB
Python
188 lines
6.3 KiB
Python
"""Registries for looking up pre-defined game content.
|
|
|
|
The function *find_content* will find the class for a piece of content
|
|
when given the name of that content. For example:
|
|
``find_content("leather armor")`` will return the
|
|
*dungeonsheets.armor.LeatherArmor* class.
|
|
|
|
The *find_content* function is a shortcut that makes use of the
|
|
*default_content_registry*, which is a *ContentRegistry* instance that
|
|
is aware of all the content included in *dungeonsheets*. **New modules
|
|
should register themselves** with the *default_content_registry*, best
|
|
achieved by::
|
|
|
|
from content_registry import default_content_registry
|
|
default_content_registry.add_module(__name__)
|
|
|
|
In the case of **homebrew content**, the python file may not be in the
|
|
python path and so cannot be imported directly. In this case, the
|
|
*import_homebrew* function will import the python file-name given and
|
|
then register it with the *default_content_registry*, most often
|
|
within a character sheet file. For example, if the *PaperSword* weapon
|
|
class is defined in a separate file "my_homebrew.py", then in the
|
|
character file::
|
|
|
|
from content_registry import import_homebrew
|
|
import_homebrew("my_homebrew.py")
|
|
|
|
weapons = ["paper sword"]
|
|
|
|
If homebrew content shares a name with canonical content, then lookup
|
|
by string will raise an exception. In those situations, the homebrew
|
|
content can be used directly from *import_homebrew*::
|
|
|
|
from content_registry import import_homebrew
|
|
campaign = import_homebrew("my_homebrew.py")
|
|
|
|
weapons = [campaign.PaperSword]
|
|
|
|
"""
|
|
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from functools import lru_cache
|
|
import importlib.util
|
|
from typing import Union, List, Optional
|
|
|
|
from dungeonsheets import exceptions
|
|
|
|
|
|
class ContentRegistry:
|
|
modules = None
|
|
|
|
def __init__(self):
|
|
self.modules = []
|
|
|
|
def add_module(self, new_module):
|
|
"""Register a module with this registry.
|
|
|
|
Adding the same module multiple times has no effect.
|
|
|
|
*new_module* can also be a string, in which case, an attempt
|
|
will be made to load the module from *sys.modules*. This way,
|
|
a module can register itself::
|
|
|
|
# Define classes, etc
|
|
...
|
|
# Register the module
|
|
registry = ContentRegistry()
|
|
registry.add_module(__name__)
|
|
|
|
"""
|
|
# Try and look up the module by name
|
|
try:
|
|
new_module = sys.modules[new_module]
|
|
except KeyError:
|
|
if isinstance(new_module, str):
|
|
raise exceptions.ContentNotFound(f"Module could not be resolved: {repr(new_module)}")
|
|
# Add the imported module to the list for later
|
|
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 and hasattr(attr, 'improved_version'):
|
|
attr = attr.improved_version(bonus)
|
|
return attr
|
|
|
|
|
|
default_content_registry = ContentRegistry()
|
|
|
|
|
|
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
|