Fix spawn issues in xyzgrid. Allow prototype_parent to be a dict itself. Resolve #2494.

This commit is contained in:
Griatch 2021-08-22 20:30:22 +02:00
parent fc323e1ca7
commit ddaf22ea58
12 changed files with 207 additions and 97 deletions

View file

@ -84,6 +84,8 @@ Up requirements to Django 3.2+
on-object Scripts. Moved `CmdScripts` and `CmdObjects` to `commands/default/building.py`. on-object Scripts. Moved `CmdScripts` and `CmdObjects` to `commands/default/building.py`.
- Keep GMCP function case if outputfunc starts with capital letter (so `cmd_name` -> `Cmd.Name` - Keep GMCP function case if outputfunc starts with capital letter (so `cmd_name` -> `Cmd.Name`
but `Cmd_nAmE` -> `Cmd.nAmE`). This helps e.g Mudlet's legacy `Client_GUI` implementation) but `Cmd_nAmE` -> `Cmd.nAmE`). This helps e.g Mudlet's legacy `Client_GUI` implementation)
- Prototypes now allow setting `prototype_parent` directly to a prototype-dict.
This makes it easier when dynamically building in-module prototypes.
### Evennia 0.9.5 (2019-2020) ### Evennia 0.9.5 (2019-2020)

View file

@ -56,6 +56,12 @@ Exits: northeast and east
command line. It will also make the `xyz_room` and `xyz_exit` prototypes command line. It will also make the `xyz_room` and `xyz_exit` prototypes
available for use as prototype-parents when spawning the grid. available for use as prototype-parents when spawning the grid.
3. Run `evennia xyzgrid help` for available options. 3. Run `evennia xyzgrid help` for available options.
4. (Optional): By default, the xyzgrid will only spawn module-based
[prototypes](Prototypes). This is an optimization and usually makes sense
since the grid is entirely defined outside the game anyway. If you want to
also make use of in-game (db-) created prototypes, add
`XYZGRID_USE_DB_PROTOTYPES = True` to settings.
## Overview ## Overview
@ -1002,8 +1008,8 @@ should be included as `prototype_parents` for prototypes on the map. Would it
not be nice to be able to change these and have the change apply to all of the not be nice to be able to change these and have the change apply to all of the
grid? You can, by adding the following to your `mygame/server/conf/settings.py`: grid? You can, by adding the following to your `mygame/server/conf/settings.py`:
XYZROOM_PARENT_PROTOTYPE_OVERRIDE = {"typeclass": "myxyzroom.MyXYZRoom"} XYZROOM_PROTOTYPE_OVERRIDE = {"typeclass": "myxyzroom.MyXYZRoom"}
XYZEXIT_PARENT_PROTOTYPE_OVERRIDE = {...} XYZEXIT_PROTOTYPE_OVERRIDE = {...}
> If you override the typeclass in your prototypes, the typeclass used **MUST** > If you override the typeclass in your prototypes, the typeclass used **MUST**

View file

@ -23,16 +23,24 @@ from evennia.contrib.xyzgrid import xymap_legend
# the typeclass inherits from the XYZRoom (or XYZExit) # the typeclass inherits from the XYZRoom (or XYZExit)
# if adding the evennia.contrib.xyzgrid.prototypes to # if adding the evennia.contrib.xyzgrid.prototypes to
# settings.PROTOTYPE_MODULES, one could just set the # settings.PROTOTYPE_MODULES, one could just set the
# prototype_parent to 'xyz_room' and 'xyz_exit' respectively # prototype_parent to 'xyz_room' and 'xyz_exit' here
# instead. # instead.
PARENT = { ROOM_PARENT = {
"key": "An empty room", "key": "An empty room",
"prototype_key": "xyzmap_room_map1", "prototype_key": "xyz_exit_prototype",
"typeclass": "evennia.contrib.xyzgrid.xyzroom.XYZRoom", "prototype_parent": "xyz_room",
# "typeclass": "evennia.contrib.xyzgrid.xyzroom.XYZRoom",
"desc": "An empty room.", "desc": "An empty room.",
} }
EXIT_PARENT = {
"prototype_key": "xyz_exit_prototype",
"prototype_parent": "xyz_exit",
# "typeclass": "evennia.contrib.xyzgrid.xyzroom.XYZExit",
"desc": "A path to the next location.",
}
# ---------------------------------------- map1 # ---------------------------------------- map1
# The large tree # The large tree
@ -134,13 +142,17 @@ PROTOTYPES_MAP1 = {
"desc": "These branches are wide enough to easily walk on. There's green all around." "desc": "These branches are wide enough to easily walk on. There's green all around."
}, },
# directional prototypes # directional prototypes
(3, 0, 'w'): { (3, 0, 'e'): {
"desc": "A dark passage into the underworld." "desc": "A dark passage into the underworld."
}, },
} }
for prot in PROTOTYPES_MAP1.values(): for key, prot in PROTOTYPES_MAP1.items():
prot['prototype_parent'] = PARENT if len(key) == 2:
# we don't want to give exits the room typeclass!
prot['prototype_parent'] = ROOM_PARENT
else:
prot['prototype_parent'] = EXIT_PARENT
XYMAP_DATA_MAP1 = { XYMAP_DATA_MAP1 = {
@ -253,8 +265,12 @@ PROTOTYPES_MAP2 = {
# this is required by the prototypes, but we add it all at once so we don't # this is required by the prototypes, but we add it all at once so we don't
# need to add it to every line above # need to add it to every line above
for prot in PROTOTYPES_MAP2.values(): for key, prot in PROTOTYPES_MAP2.items():
prot['prototype_parent'] = PARENT if len(key) == 2:
# we don't want to give exits the room typeclass!
prot['prototype_parent'] = ROOM_PARENT
else:
prot['prototype_parent'] = EXIT_PARENT
XYMAP_DATA_MAP2 = { XYMAP_DATA_MAP2 = {

View file

@ -18,9 +18,11 @@ Use `evennia xyzgrid help` for usage help.
from os.path import join as pathjoin from os.path import join as pathjoin
from django.conf import settings from django.conf import settings
import evennia
from evennia.utils import ansi from evennia.utils import ansi
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid
_HELP_SHORT = """ _HELP_SHORT = """
evennia xyzgrid help | list | init | add | spawn | initpath | delete [<options>] evennia xyzgrid help | list | init | add | spawn | initpath | delete [<options>]
Manages the XYZ grid. Use 'xyzgrid help <option>' for documentation. Manages the XYZ grid. Use 'xyzgrid help <option>' for documentation.
@ -161,6 +163,8 @@ _TOPICS_MAP = {
"delete": _HELP_DELETE "delete": _HELP_DELETE
} }
evennia._init()
def _option_help(*suboptions): def _option_help(*suboptions):
""" """
Show help <command> aid. Show help <command> aid.

View file

@ -1057,19 +1057,21 @@ class TestMapStressTest(TestCase):
return f"{edge}\n{(l1 + l2) * Ysize}{l1}\n\n{edge}" return f"{edge}\n{(l1 + l2) * Ysize}{l1}\n\n{edge}"
@parameterized.expand([ @parameterized.expand([
((10, 10), 0.01), ((10, 10), 0.03),
((100, 100), 1), ((100, 100), 5),
]) ])
def test_grid_creation(self, gridsize, max_time): def test_grid_creation(self, gridsize, max_time):
""" """
Test of grid-creataion performance for Nx, Ny grid. Test of grid-creataion performance for Nx, Ny grid.
""" """
# import cProfile
Xmax, Ymax = gridsize Xmax, Ymax = gridsize
grid = self._get_grid(Xmax, Ymax) grid = self._get_grid(Xmax, Ymax)
t0 = time()
mapobj = xymap.XYMap({'map': grid}, Z="testmap") mapobj = xymap.XYMap({'map': grid}, Z="testmap")
t0 = time()
mapobj.parse() mapobj.parse()
# cProfile.runctx('mapobj.parse()', globals(), locals())
t1 = time() t1 = time()
self.assertLess(t1 - t0, max_time, f"Map creation of ({Xmax}x{Ymax}) grid slower " self.assertLess(t1 - t0, max_time, f"Map creation of ({Xmax}x{Ymax}) grid slower "
f"than expected {max_time}s.") f"than expected {max_time}s.")

View file

@ -109,10 +109,15 @@ from django.conf import settings
from evennia.utils.utils import variable_from_module, mod_import, is_iter from evennia.utils.utils import variable_from_module, mod_import, is_iter
from evennia.utils import logger from evennia.utils import logger
from evennia.prototypes import prototypes as protlib from evennia.prototypes import prototypes as protlib
from evennia.prototypes.spawner import flatten_prototype
from .utils import MapError, MapParserError, BIGVAL from .utils import MapError, MapParserError, BIGVAL
from . import xymap_legend from . import xymap_legend
_NO_DB_PROTOTYPES = True
if hasattr(settings, "XYZGRID_USE_DB_PROTOTYPES"):
_NO_DB_PROTOTYPES = not settings.XYZGRID_USE_DB_PROTOTYPES
_CACHE_DIR = settings.CACHE_DIR _CACHE_DIR = settings.CACHE_DIR
_LOADED_PROTOTYPES = None _LOADED_PROTOTYPES = None
_XYZROOMCLASS = None _XYZROOMCLASS = None
@ -351,8 +356,9 @@ class XYMap:
if not prototype or isinstance(prototype, dict): if not prototype or isinstance(prototype, dict):
# nothing more to do # nothing more to do
continue continue
# we need to load the prototype dict onto each for ease of access # we need to load the prototype dict onto each for ease of access. Note that
proto = protlib.search_prototype(prototype, require_single=True)[0] proto = protlib.search_prototype(prototype, require_single=True,
no_db=_NO_DB_PROTOTYPES)[0]
node_or_link_class.prototype = proto node_or_link_class.prototype = proto
def parse(self): def parse(self):
@ -492,13 +498,24 @@ class XYMap:
if node.prototype: if node.prototype:
node_coord = (node.X, node.Y) node_coord = (node.X, node.Y)
# load prototype from override, or use default # load prototype from override, or use default
node.prototype = self.prototypes.get( try:
node_coord, self.prototypes.get(('*', '*'), node.prototype)) node.prototype = flatten_prototype(self.prototypes.get(
node_coord,
self.prototypes.get(('*', '*'), node.prototype)),
no_db=_NO_DB_PROTOTYPES
)
except Exception as err:
raise MapParserError(f"Room prototype malformed: {err}", node)
# do the same for links (x, y, direction) coords # do the same for links (x, y, direction) coords
for direction, maplink in node.first_links.items(): for direction, maplink in node.first_links.items():
maplink.prototype = self.prototypes.get( try:
node_coord + (direction,), maplink.prototype = flatten_prototype(self.prototypes.get(
self.prototypes.get(('*', '*', '*'), maplink.prototype)) node_coord + (direction,),
self.prototypes.get(('*', '*', '*'), maplink.prototype)),
no_db=_NO_DB_PROTOTYPES
)
except Exception as err:
raise MapParserError(f"Exit prototype malformed: {err}", maplink)
# store # store
self.display_map = display_map self.display_map = display_map
@ -625,8 +642,8 @@ class XYMap:
spawned = [] spawned = []
# find existing nodes, in case some rooms need to be removed # find existing nodes, in case some rooms need to be removed
map_coords = ((node.X, node.Y) for node in map_coords = [(node.X, node.Y) for node in
sorted(self.node_index_map.values(), key=lambda n: (n.Y, n.X))) sorted(self.node_index_map.values(), key=lambda n: (n.Y, n.X))]
for existing_room in _XYZROOMCLASS.objects.filter_xyz(xyz=(x, y, self.Z)): for existing_room in _XYZROOMCLASS.objects.filter_xyz(xyz=(x, y, self.Z)):
roomX, roomY, _ = existing_room.xyz roomX, roomY, _ = existing_room.xyz
if (roomX, roomY) not in map_coords: if (roomX, roomY) not in map_coords:

View file

@ -311,7 +311,11 @@ class MapNode:
nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz) nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
except NodeTypeclass.DoesNotExist: except NodeTypeclass.DoesNotExist:
# create a new entity with proper coordinates etc # create a new entity with proper coordinates etc
self.log(f" spawning room at xyz={xyz}") tclass = self.prototype['typeclass']
tclass = (f' ({tclass})'
if tclass != 'evennia.contrib.xyzgrid.xyzroom.XYZRoom'
else '')
self.log(f" spawning room at xyz={xyz}{tclass}")
nodeobj, err = NodeTypeclass.create( nodeobj, err = NodeTypeclass.create(
self.prototype.get('key', 'An empty room'), self.prototype.get('key', 'An empty room'),
xyz=xyz xyz=xyz
@ -327,7 +331,6 @@ class MapNode:
# apply prototype to node. This will not override the XYZ tags since # apply prototype to node. This will not override the XYZ tags since
# these are not in the prototype and exact=False # these are not in the prototype and exact=False
spawner.batch_update_objects_with_prototype( spawner.batch_update_objects_with_prototype(
self.prototype, objects=[nodeobj], exact=False) self.prototype, objects=[nodeobj], exact=False)
@ -364,8 +367,6 @@ class MapNode:
link.prototype['prototype_key'] = self.generate_prototype_key() link.prototype['prototype_key'] = self.generate_prototype_key()
maplinks[key.lower()] = (key, aliases, direction, link) maplinks[key.lower()] = (key, aliases, direction, link)
# if xyz == (8, 1, 'the large tree'):
# from evennia import set_trace;set_trace()
# remove duplicates # remove duplicates
linkobjs = defaultdict(list) linkobjs = defaultdict(list)
for exitobj in ExitTypeclass.objects.filter_xyz(xyz=xyz): for exitobj in ExitTypeclass.objects.filter_xyz(xyz=xyz):
@ -384,7 +385,6 @@ class MapNode:
# build all exits first run) # build all exits first run)
differing_keys = set(maplinks.keys()).symmetric_difference(set(linkobjs.keys())) differing_keys = set(maplinks.keys()).symmetric_difference(set(linkobjs.keys()))
for differing_key in differing_keys: for differing_key in differing_keys:
# from evennia import set_trace;set_trace()
if differing_key not in maplinks: if differing_key not in maplinks:
# an exit without a maplink - delete the exit-object # an exit without a maplink - delete the exit-object
@ -408,7 +408,12 @@ class MapNode:
if err: if err:
raise RuntimeError(err) raise RuntimeError(err)
linkobjs[key.lower()] = exi linkobjs[key.lower()] = exi
self.log(f" spawning/updating exit xyz={xyz}, direction={key}") prot = maplinks[key.lower()][3].prototype
tclass = prot['typeclass']
tclass = (f' ({tclass})'
if tclass != 'evennia.contrib.xyzgrid.xyzroom.XYZExit'
else '')
self.log(f" spawning/updating exit xyz={xyz}, direction={key}{tclass}")
# apply prototypes to catch any changes # apply prototypes to catch any changes
for key, linkobj in linkobjs.items(): for key, linkobj in linkobjs.items():

View file

@ -124,6 +124,9 @@ class XYZGrid(DefaultScript):
map_data_list = [variable_from_module(module_path, "XYMAP_DATA")] map_data_list = [variable_from_module(module_path, "XYMAP_DATA")]
# inject the python path in the map data # inject the python path in the map data
for mapdata in map_data_list: for mapdata in map_data_list:
if not mapdata:
self.log(f"Could not find or load map from {module_path}.")
return
mapdata['module_path'] = module_path mapdata['module_path'] = module_path
return map_data_list return map_data_list
@ -137,10 +140,14 @@ class XYZGrid(DefaultScript):
nmaps = 0 nmaps = 0
loaded_mapdata = {} loaded_mapdata = {}
changed = [] changed = []
mapdata = self.db.map_data
if not mapdata:
self.db.mapdata = mapdata = {}
# generate all Maps - this will also initialize their components # generate all Maps - this will also initialize their components
# and bake any pathfinding paths (or load from disk-cache) # and bake any pathfinding paths (or load from disk-cache)
for zcoord, old_mapdata in self.db.map_data.items(): for zcoord, old_mapdata in mapdata.items():
self.log(f"Loading map '{zcoord}'...") self.log(f"Loading map '{zcoord}'...")
@ -168,7 +175,7 @@ class XYZGrid(DefaultScript):
# re-store changed data # re-store changed data
for zcoord in changed: for zcoord in changed:
self.db.map_data[zcoord] = loaded_mapdata['zcoord'] self.db.map_data[zcoord] = loaded_mapdata[zcoord]
# store # store
self.log(f"Loaded and linked {nmaps} map(s).") self.log(f"Loaded and linked {nmaps} map(s).")
@ -222,7 +229,9 @@ class XYZGrid(DefaultScript):
Clear the entire grid, including database entities, then the grid too. Clear the entire grid, including database entities, then the grid too.
""" """
self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True) mapdata = self.db.map_data
if mapdata:
self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True)
super().delete() super().delete()
def spawn(self, xyz=('*', '*', '*'), directions=None): def spawn(self, xyz=('*', '*', '*'), directions=None):
@ -291,6 +300,7 @@ def get_xyzgrid(print_errors=True):
if not xyzgrid.ndb.loaded: if not xyzgrid.ndb.loaded:
xyzgrid.reload() xyzgrid.reload()
except Exception as err: except Exception as err:
raise
if print_errors: if print_errors:
print(err) print(err)
else: else:

View file

@ -86,27 +86,20 @@ class XYZManager(ObjectManager):
possible with a unique combination of x,y,z). possible with a unique combination of x,y,z).
""" """
# filter by tags, then figure out of we got a single match or not
query = self.filter_xyz(xyz=xyz, **kwargs)
ncount = query.count()
if ncount == 1:
return query.first()
# error - mimic default get() behavior but with a little more info
x, y, z = xyz x, y, z = xyz
inp = (f"Query: xyz=({x},{y},{z}), " +
# mimic get_family ",".join(f"{key}={val}" for key, val in kwargs.items()))
paths = [self.model.path] + [ if ncount > 1:
"%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model) raise self.model.MultipleObjectsReturned(inp)
] else:
kwargs["db_typeclass_path__in"] = paths raise self.model.DoesNotExist(inp)
try:
return (
self
.filter(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
.filter(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
.filter(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
.get(**kwargs)
)
except self.model.DoesNotExist:
inp = (f"xyz=({x},{y},{z}), " +
",".join(f"{key}={val}" for key, val in kwargs.items()))
raise self.model.DoesNotExist(f"{self.model.__name__} "
f"matching query {inp} does not exist.")
class XYZExitManager(XYZManager): class XYZExitManager(XYZManager):

View file

@ -2580,7 +2580,7 @@ def node_prototype_spawn(caller, **kwargs):
# prototype load node # prototype load node
def _prototype_load_select(caller, prototype_key): def _prototype_load_select(caller, prototype_key, **kwargs):
matches = protlib.search_prototype(key=prototype_key) matches = protlib.search_prototype(key=prototype_key)
if matches: if matches:
prototype = matches[0] prototype = matches[0]

View file

@ -105,17 +105,17 @@ def homogenize_prototype(prototype, custom_keys=None):
elif protkey in ("prototype_key", "prototype_desc"): elif protkey in ("prototype_key", "prototype_desc"):
prototype[protkey] = "" prototype[protkey] = ""
attrs = list(prototype.get("attrs", [])) # break reference homogenized = {}
tags = make_iter(prototype.get("tags", []))
homogenized_tags = [] homogenized_tags = []
homogenized_attrs = [] homogenized_attrs = []
homogenized_parents = []
homogenized = {}
for key, val in prototype.items(): for key, val in prototype.items():
if key in reserved: if key in reserved:
# check all reserved keys # check all reserved keys
if key == "tags": if key == "tags":
# tags must be on form [(tag, category, data), ...] # tags must be on form [(tag, category, data), ...]
tags = make_iter(prototype.get("tags", []))
for tag in tags: for tag in tags:
if not is_iter(tag): if not is_iter(tag):
homogenized_tags.append((tag, None, None)) homogenized_tags.append((tag, None, None))
@ -127,7 +127,9 @@ def homogenize_prototype(prototype, custom_keys=None):
homogenized_tags.append((tag[0], tag[1], None)) homogenized_tags.append((tag[0], tag[1], None))
else: else:
homogenized_tags.append(tag[:3]) homogenized_tags.append(tag[:3])
if key == "attrs":
elif key == "attrs":
attrs = list(prototype.get("attrs", [])) # break reference
for attr in attrs: for attr in attrs:
# attrs must be on form [(key, value, category, lockstr)] # attrs must be on form [(key, value, category, lockstr)]
if not is_iter(attr): if not is_iter(attr):
@ -144,6 +146,21 @@ def homogenize_prototype(prototype, custom_keys=None):
homogenized_attrs.append(attr[0], attr[1], attr[2], "") homogenized_attrs.append(attr[0], attr[1], attr[2], "")
else: else:
homogenized_attrs.append(attr[:4]) homogenized_attrs.append(attr[:4])
elif key == "prototype_parent":
# homogenize any prototype-parents embedded directly as dicts
protparents = prototype.get('prototype_parent', [])
if isinstance(protparents, dict):
protparents = [protparents]
for parent in make_iter(protparents):
if isinstance(parent, dict):
# recursively homogenize directly embedded prototype parents
homogenized_parents.append(
homogenize_prototype(parent, custom_keys=custom_keys))
else:
# normal prototype-parent names are added as-is
homogenized_parents.append(parent)
else: else:
# another reserved key # another reserved key
homogenized[key] = val homogenized[key] = val
@ -154,6 +171,8 @@ def homogenize_prototype(prototype, custom_keys=None):
homogenized["attrs"] = homogenized_attrs homogenized["attrs"] = homogenized_attrs
if homogenized_tags: if homogenized_tags:
homogenized["tags"] = homogenized_tags homogenized["tags"] = homogenized_tags
if homogenized_parents:
homogenized['prototype_parent'] = homogenized_parents
# add required missing parts that had defaults before # add required missing parts that had defaults before
@ -460,7 +479,8 @@ def delete_prototype(prototype_key, caller=None):
return True return True
def search_prototype(key=None, tags=None, require_single=False, return_iterators=False): def search_prototype(key=None, tags=None, require_single=False, return_iterators=False,
no_db=False):
""" """
Find prototypes based on key and/or tags, or all prototypes. Find prototypes based on key and/or tags, or all prototypes.
@ -474,6 +494,9 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators
return_iterators (bool): Optimized return for large numbers of db-prototypes. return_iterators (bool): Optimized return for large numbers of db-prototypes.
If set, separate returns of module based prototypes and paginate If set, separate returns of module based prototypes and paginate
the db-prototype return. the db-prototype return.
no_db (bool): Optimization. If set, skip querying for database-generated prototypes and only
include module-based prototypes. This can lead to a dramatic speedup since
module-prototypes are static and require no db-lookup.
Return: Return:
matches (list): Default return, all found prototype dicts. Empty list if matches (list): Default return, all found prototype dicts. Empty list if
@ -525,35 +548,38 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators
# prototype_from_object will modify the base prototype for every object # prototype_from_object will modify the base prototype for every object
module_prototypes = [match.copy() for match in mod_matches.values()] module_prototypes = [match.copy() for match in mod_matches.values()]
# search db-stored prototypes if no_db:
db_matches = []
if tags:
# exact match on tag(s)
tags = make_iter(tags)
tag_categories = ["db_prototype" for _ in tags]
db_matches = DbPrototype.objects.get_by_tag(tags, tag_categories)
else: else:
db_matches = DbPrototype.objects.all() # search db-stored prototypes
if tags:
if key: # exact match on tag(s)
# exact or partial match on key tags = make_iter(tags)
exact_match = db_matches.filter(Q(db_key__iexact=key)).order_by("db_key") tag_categories = ["db_prototype" for _ in tags]
if not exact_match and allow_fuzzy: db_matches = DbPrototype.objects.get_by_tag(tags, tag_categories)
# try with partial match instead
db_matches = db_matches.filter(Q(db_key__icontains=key)).order_by("db_key")
else: else:
db_matches = exact_match db_matches = DbPrototype.objects.all()
if key:
# exact or partial match on key
exact_match = db_matches.filter(Q(db_key__iexact=key)).order_by("db_key")
if not exact_match and allow_fuzzy:
# try with partial match instead
db_matches = db_matches.filter(Q(db_key__icontains=key)).order_by("db_key")
else:
db_matches = exact_match
# convert to prototype
db_ids = db_matches.values_list("id", flat=True)
db_matches = (
Attribute.objects.filter(scriptdb__pk__in=db_ids, db_key="prototype")
.values_list("db_value", flat=True)
.order_by("scriptdb__db_key")
)
# convert to prototype
db_ids = db_matches.values_list("id", flat=True)
db_matches = (
Attribute.objects.filter(scriptdb__pk__in=db_ids, db_key="prototype")
.values_list("db_value", flat=True)
.order_by("scriptdb__db_key")
)
if key and require_single: if key and require_single:
nmodules = len(module_prototypes) nmodules = len(module_prototypes)
ndbprots = db_matches.count() ndbprots = db_matches.count() if db_matches else 0
if nmodules + ndbprots != 1: if nmodules + ndbprots != 1:
raise KeyError(_( raise KeyError(_(
"Found {num} matching prototypes among {module_prototypes}.").format( "Found {num} matching prototypes among {module_prototypes}.").format(
@ -795,19 +821,29 @@ def validate_prototype(
err=err, protkey=protkey, typeclass=typeclass) err=err, protkey=protkey, typeclass=typeclass)
) )
# recursively traverse prototype_parent chain if prototype_parent and isinstance(prototype_parent, dict):
# the protparent is already embedded as a dict;
prototype_parent = [prototype_parent]
# recursively traverse prototype_parent chain
for protstring in make_iter(prototype_parent): for protstring in make_iter(prototype_parent):
protstring = protstring.lower() if isinstance(protstring, dict):
if protkey is not None and protstring == protkey: # an already embedded prototype_parent
_flags["errors"].append(_("Prototype {protkey} tries to parent itself.").format( protparent = protstring
protkey=protkey)) protstring = None
protparent = protparents.get(protstring) else:
if not protparent: protstring = protstring.lower()
_flags["errors"].append( if protkey is not None and protstring == protkey:
_("Prototype {protkey}'s prototype_parent '{parent}' was not found.").format( _flags["errors"].append(_("Prototype {protkey} tries to parent itself.").format(
protkey=protkey, parent=protstring) protkey=protkey))
) protparent = protparents.get(protstring)
if not protparent:
_flags["errors"].append(
_("Prototype {protkey}'s `prototype_parent` (named '{parent}') "
"was not found.").format(protkey=protkey, parent=protstring)
)
# check for infinite recursion
if id(prototype) in _flags["visited"]: if id(prototype) in _flags["visited"]:
_flags["errors"].append( _flags["errors"].append(
_("{protkey} has infinite nesting of prototypes.").format( _("{protkey} has infinite nesting of prototypes.").format(
@ -818,9 +854,12 @@ def validate_prototype(
raise RuntimeError(f"{_ERRSTR}: " + f"\n{_ERRSTR}: ".join(_flags["errors"])) raise RuntimeError(f"{_ERRSTR}: " + f"\n{_ERRSTR}: ".join(_flags["errors"]))
_flags["visited"].append(id(prototype)) _flags["visited"].append(id(prototype))
_flags["depth"] += 1 _flags["depth"] += 1
# next step of recursive validation
validate_prototype( validate_prototype(
protparent, protstring, protparents, is_prototype_base=is_prototype_base, _flags=_flags protparent, protstring, protparents, is_prototype_base=is_prototype_base, _flags=_flags
) )
_flags["visited"].pop() _flags["visited"].pop()
_flags["depth"] -= 1 _flags["depth"] -= 1

View file

@ -220,10 +220,23 @@ def _get_prototype(inprot, protparents, uninherited=None, _workprot=None):
_workprot = {} if _workprot is None else _workprot _workprot = {} if _workprot is None else _workprot
if "prototype_parent" in inprot: if "prototype_parent" in inprot:
# move backwards through the inheritance # move backwards through the inheritance
for prototype in make_iter(inprot["prototype_parent"]):
prototype_parents = inprot["prototype_parent"]
if isinstance(prototype_parents, dict):
# protparent already embedded as-is
prototype_parents = [prototype_parents]
for prototype in make_iter(prototype_parents):
if isinstance(prototype, dict):
# protparent already embedded as-is
parent_prototype = prototype
else:
# protparent given by-name
parent_prototype = protparents.get(prototype.lower(), {})
# Build the prot dictionary in reverse order, overloading # Build the prot dictionary in reverse order, overloading
new_prot = _get_prototype( new_prot = _get_prototype(
protparents.get(prototype.lower(), {}), protparents, _workprot=_workprot parent_prototype, protparents, _workprot=_workprot
) )
# attrs, tags have internal structure that should be inherited separately # attrs, tags have internal structure that should be inherited separately
@ -245,7 +258,7 @@ def _get_prototype(inprot, protparents, uninherited=None, _workprot=None):
return _workprot return _workprot
def flatten_prototype(prototype, validate=False): def flatten_prototype(prototype, validate=False, no_db=False):
""" """
Produce a 'flattened' prototype, where all prototype parents in the inheritance tree have been Produce a 'flattened' prototype, where all prototype parents in the inheritance tree have been
merged into a final prototype. merged into a final prototype.
@ -253,6 +266,8 @@ def flatten_prototype(prototype, validate=False):
Args: Args:
prototype (dict): Prototype to flatten. Its `prototype_parent` field will be parsed. prototype (dict): Prototype to flatten. Its `prototype_parent` field will be parsed.
validate (bool, optional): Validate for valid keys etc. validate (bool, optional): Validate for valid keys etc.
no_db (bool, optional): Don't search db-based prototypes. This can speed up
searching dramatically since module-based prototypes are static.
Returns: Returns:
flattened (dict): The final, flattened prototype. flattened (dict): The final, flattened prototype.
@ -261,7 +276,8 @@ def flatten_prototype(prototype, validate=False):
if prototype: if prototype:
prototype = protlib.homogenize_prototype(prototype) prototype = protlib.homogenize_prototype(prototype)
protparents = {prot["prototype_key"].lower(): prot for prot in protlib.search_prototype()} protparents = {prot["prototype_key"].lower(): prot
for prot in protlib.search_prototype(no_db=no_db)}
protlib.validate_prototype( protlib.validate_prototype(
prototype, None, protparents, is_prototype_base=validate, strict=validate prototype, None, protparents, is_prototype_base=validate, strict=validate
) )