Resolve merge conflicts
This commit is contained in:
commit
9cdc37355c
25 changed files with 726 additions and 444 deletions
|
|
@ -9,9 +9,13 @@ import hashlib
|
|||
import time
|
||||
from ast import literal_eval
|
||||
from django.conf import settings
|
||||
from django.db.models import Q, Subquery
|
||||
from django.core.paginator import Paginator
|
||||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.typeclasses.attributes import Attribute
|
||||
from evennia.utils.create import create_script
|
||||
from evennia.utils.evmore import EvMore
|
||||
from evennia.utils.utils import (
|
||||
all_from_module,
|
||||
make_iter,
|
||||
|
|
@ -163,7 +167,8 @@ for mod in settings.PROTOTYPE_MODULES:
|
|||
if "prototype_locks" in prot
|
||||
else "use:all();edit:false()"
|
||||
),
|
||||
"prototype_tags": list(set(make_iter(prot.get("prototype_tags", [])) + ["module"])),
|
||||
"prototype_tags": list(set(list(
|
||||
make_iter(prot.get("prototype_tags", []))) + ["module"])),
|
||||
}
|
||||
)
|
||||
_MODULE_PROTOTYPES[actual_prot_key] = prot
|
||||
|
|
@ -320,7 +325,7 @@ def delete_prototype(prototype_key, caller=None):
|
|||
return True
|
||||
|
||||
|
||||
def search_prototype(key=None, tags=None, require_single=False):
|
||||
def search_prototype(key=None, tags=None, require_single=False, return_iterators=False):
|
||||
"""
|
||||
Find prototypes based on key and/or tags, or all prototypes.
|
||||
|
||||
|
|
@ -331,11 +336,17 @@ def search_prototype(key=None, tags=None, require_single=False):
|
|||
tag category.
|
||||
require_single (bool): If set, raise KeyError if the result
|
||||
was not found or if there are multiple matches.
|
||||
return_iterators (bool): Optimized return for large numbers of db-prototypes.
|
||||
If set, separate returns of module based prototypes and paginate
|
||||
the db-prototype return.
|
||||
|
||||
Return:
|
||||
matches (list): All found prototype dicts. Empty list if
|
||||
matches (list): Default return, all found prototype dicts. Empty list if
|
||||
no match was found. Note that if neither `key` nor `tags`
|
||||
were given, *all* available prototypes will be returned.
|
||||
list, queryset: If `return_iterators` are found, this is a list of
|
||||
module-based prototypes followed by a *paginated* queryset of
|
||||
db-prototypes.
|
||||
|
||||
Raises:
|
||||
KeyError: If `require_single` is True and there are 0 or >1 matches.
|
||||
|
|
@ -381,33 +392,51 @@ def search_prototype(key=None, tags=None, require_single=False):
|
|||
# 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)
|
||||
db_matches = DbPrototype.objects.get_by_tag(
|
||||
tags, tag_categories)
|
||||
else:
|
||||
db_matches = DbPrototype.objects.all().order_by("id")
|
||||
db_matches = DbPrototype.objects.all()
|
||||
|
||||
if key:
|
||||
# exact or partial match on key
|
||||
db_matches = (
|
||||
db_matches.filter(db_key=key) or db_matches.filter(db_key__icontains=key)
|
||||
).order_by("id")
|
||||
# return prototype
|
||||
db_prototypes = [dbprot.prototype for dbprot in db_matches]
|
||||
exact_match = (
|
||||
db_matches
|
||||
.filter(
|
||||
Q(db_key__iexact=key))
|
||||
.order_by("db_key")
|
||||
)
|
||||
if not exact_match:
|
||||
# try with partial match instead
|
||||
db_matches = (
|
||||
db_matches
|
||||
.filter(
|
||||
Q(db_key__icontains=key))
|
||||
.order_by("db_key")
|
||||
)
|
||||
else:
|
||||
db_matches = exact_match
|
||||
|
||||
matches = db_prototypes + module_prototypes
|
||||
nmatches = len(matches)
|
||||
if nmatches > 1 and key:
|
||||
key = key.lower()
|
||||
# avoid duplicates if an exact match exist between the two types
|
||||
filter_matches = [
|
||||
mta for mta in matches if mta.get("prototype_key") and mta["prototype_key"] == key
|
||||
]
|
||||
if filter_matches and len(filter_matches) < nmatches:
|
||||
matches = filter_matches
|
||||
# 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:
|
||||
nmodules = len(module_prototypes)
|
||||
ndbprots = db_matches.count()
|
||||
if nmodules + ndbprots != 1:
|
||||
raise KeyError(f"Found {nmodules + ndbprots} matching prototypes.")
|
||||
|
||||
nmatches = len(matches)
|
||||
if nmatches != 1 and require_single:
|
||||
raise KeyError("Found {} matching prototypes.".format(nmatches))
|
||||
|
||||
return matches
|
||||
if return_iterators:
|
||||
# trying to get the entire set of prototypes - we must paginate
|
||||
# the result instead of trying to fetch the entire set at once
|
||||
return db_matches, module_prototypes
|
||||
else:
|
||||
# full fetch, no pagination (compatibility mode)
|
||||
return list(db_matches) + module_prototypes
|
||||
|
||||
|
||||
def search_objects_with_prototype(prototype_key):
|
||||
|
|
@ -424,7 +453,109 @@ def search_objects_with_prototype(prototype_key):
|
|||
return ObjectDB.objects.get_by_tag(key=prototype_key, category=PROTOTYPE_TAG_CATEGORY)
|
||||
|
||||
|
||||
def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True):
|
||||
class PrototypeEvMore(EvMore):
|
||||
"""
|
||||
Listing 1000+ prototypes can be very slow. So we customize EvMore to
|
||||
display an EvTable per paginated page rather than to try creating an
|
||||
EvTable for the entire dataset and then paginate it.
|
||||
"""
|
||||
|
||||
def __init__(self, caller, *args, session=None, **kwargs):
|
||||
"""Store some extra properties on the EvMore class"""
|
||||
self.show_non_use = kwargs.pop("show_non_use", False)
|
||||
self.show_non_edit = kwargs.pop("show_non_edit", False)
|
||||
super().__init__(caller, *args, session=session, **kwargs)
|
||||
|
||||
def init_pages(self, inp):
|
||||
"""
|
||||
This will be initialized with a tuple (mod_prototype_list, paginated_db_query)
|
||||
and we must handle these separately since they cannot be paginated in the same
|
||||
way. We will build the prototypes so that the db-prototypes come first (they
|
||||
are likely the most volatile), followed by the mod-prototypes.
|
||||
"""
|
||||
dbprot_query, modprot_list = inp
|
||||
# set the number of entries per page to half the reported height of the screen
|
||||
# to account for long descs etc
|
||||
dbprot_paged = Paginator(dbprot_query, max(1, int(self.height / 2)))
|
||||
|
||||
# we separate the different types of data, so we track how many pages there are
|
||||
# of each.
|
||||
n_mod = len(modprot_list)
|
||||
self._npages_mod = n_mod // self.height + (0 if n_mod % self.height == 0 else 1)
|
||||
self._db_count = dbprot_paged.count
|
||||
self._npages_db = dbprot_paged.num_pages if self._db_count > 0 else 0
|
||||
# total number of pages
|
||||
self._npages = self._npages_mod + self._npages_db
|
||||
self._data = (dbprot_paged, modprot_list)
|
||||
self._paginator = self.prototype_paginator
|
||||
|
||||
def prototype_paginator(self, pageno):
|
||||
"""
|
||||
The listing is separated in db/mod prototypes, so we need to figure out which
|
||||
one to pick based on the page number. Also, pageno starts from 0.
|
||||
"""
|
||||
dbprot_pages, modprot_list = self._data
|
||||
|
||||
if self._db_count and pageno < self._npages_db:
|
||||
return dbprot_pages.page(pageno + 1)
|
||||
else:
|
||||
# get the correct slice, adjusted for the db-prototypes
|
||||
pageno = max(0, pageno - self._npages_db)
|
||||
return modprot_list[pageno * self.height: pageno * self.height + self.height]
|
||||
|
||||
def page_formatter(self, page):
|
||||
"""Input is a queryset page from django.Paginator"""
|
||||
caller = self._caller
|
||||
|
||||
# get use-permissions of readonly attributes (edit is always False)
|
||||
display_tuples = []
|
||||
|
||||
table = EvTable(
|
||||
"|wKey|n",
|
||||
"|wSpawn/Edit|n",
|
||||
"|wTags|n",
|
||||
"|wDesc|n",
|
||||
border="tablecols",
|
||||
crop=True,
|
||||
width=self.width
|
||||
)
|
||||
|
||||
for prototype in page:
|
||||
lock_use = caller.locks.check_lockstring(
|
||||
caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
|
||||
)
|
||||
if not self.show_non_use and not lock_use:
|
||||
continue
|
||||
if prototype.get("prototype_key", "") in _MODULE_PROTOTYPES:
|
||||
lock_edit = False
|
||||
else:
|
||||
lock_edit = caller.locks.check_lockstring(
|
||||
caller, prototype.get("prototype_locks", ""), access_type="edit", default=True
|
||||
)
|
||||
if not self.show_non_edit and not lock_edit:
|
||||
continue
|
||||
ptags = []
|
||||
for ptag in prototype.get("prototype_tags", []):
|
||||
if is_iter(ptag):
|
||||
if len(ptag) > 1:
|
||||
ptags.append("{}".format(ptag[0]))
|
||||
else:
|
||||
ptags.append(ptag[0])
|
||||
else:
|
||||
ptags.append(str(ptag))
|
||||
|
||||
table.add_row(
|
||||
prototype.get("prototype_key", "<unset>"),
|
||||
"{}/{}".format("Y" if lock_use else "N", "Y" if lock_edit else "N"),
|
||||
", ".join(list(set(ptags))),
|
||||
prototype.get("prototype_desc", "<unset>"),
|
||||
)
|
||||
|
||||
return str(table)
|
||||
|
||||
|
||||
def list_prototypes(caller, key=None, tags=None, show_non_use=False,
|
||||
show_non_edit=True, session=None):
|
||||
"""
|
||||
Collate a list of found prototypes based on search criteria and access.
|
||||
|
||||
|
|
@ -434,66 +565,26 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed
|
|||
tags (str or list, optional): Tag key or keys to query for.
|
||||
show_non_use (bool, optional): Show also prototypes the caller may not use.
|
||||
show_non_edit (bool, optional): Show also prototypes the caller may not edit.
|
||||
session (Session, optional): If given, this is used for display formatting.
|
||||
Returns:
|
||||
table (EvTable or None): An EvTable representation of the prototypes. None
|
||||
if no prototypes were found.
|
||||
PrototypeEvMore: An EvMore subclass optimized for prototype listings.
|
||||
None: If no matches were found. In this case the caller has already been notified.
|
||||
|
||||
"""
|
||||
# this allows us to pass lists of empty strings
|
||||
tags = [tag for tag in make_iter(tags) if tag]
|
||||
|
||||
# get prototypes for readonly and db-based prototypes
|
||||
prototypes = search_prototype(key, tags)
|
||||
dbprot_query, modprot_list = search_prototype(key, tags, return_iterators=True)
|
||||
|
||||
# get use-permissions of readonly attributes (edit is always False)
|
||||
display_tuples = []
|
||||
for prototype in sorted(prototypes, key=lambda d: d.get("prototype_key", "")):
|
||||
lock_use = caller.locks.check_lockstring(
|
||||
caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
|
||||
)
|
||||
if not show_non_use and not lock_use:
|
||||
continue
|
||||
if prototype.get("prototype_key", "") in _MODULE_PROTOTYPES:
|
||||
lock_edit = False
|
||||
else:
|
||||
lock_edit = caller.locks.check_lockstring(
|
||||
caller, prototype.get("prototype_locks", ""), access_type="edit", default=True
|
||||
)
|
||||
if not show_non_edit and not lock_edit:
|
||||
continue
|
||||
ptags = []
|
||||
for ptag in prototype.get("prototype_tags", []):
|
||||
if is_iter(ptag):
|
||||
if len(ptag) > 1:
|
||||
ptags.append("{} (category: {}".format(ptag[0], ptag[1]))
|
||||
else:
|
||||
ptags.append(ptag[0])
|
||||
else:
|
||||
ptags.append(str(ptag))
|
||||
|
||||
display_tuples.append(
|
||||
(
|
||||
prototype.get("prototype_key", "<unset>"),
|
||||
prototype.get("prototype_desc", "<unset>"),
|
||||
"{}/{}".format("Y" if lock_use else "N", "Y" if lock_edit else "N"),
|
||||
",".join(ptags),
|
||||
)
|
||||
)
|
||||
|
||||
if not display_tuples:
|
||||
return ""
|
||||
|
||||
table = []
|
||||
width = 78
|
||||
for i in range(len(display_tuples[0])):
|
||||
table.append([str(display_tuple[i]) for display_tuple in display_tuples])
|
||||
table = EvTable("Key", "Desc", "Spawn/Edit", "Tags", table=table, crop=True, width=width)
|
||||
table.reformat_column(0, width=22)
|
||||
table.reformat_column(1, width=29)
|
||||
table.reformat_column(2, width=11, align="c")
|
||||
table.reformat_column(3, width=16)
|
||||
return table
|
||||
if not dbprot_query and not modprot_list:
|
||||
caller.msg("No prototypes found.", session=session)
|
||||
return None
|
||||
|
||||
# get specific prototype (one value or exception)
|
||||
return PrototypeEvMore(caller, (dbprot_query, modprot_list),
|
||||
session=session,
|
||||
show_non_use=show_non_use,
|
||||
show_non_edit=show_non_edit)
|
||||
|
||||
def validate_prototype(
|
||||
prototype, protkey=None, protparents=None, is_prototype_base=True, strict=True, _flags=None
|
||||
|
|
@ -569,7 +660,7 @@ def validate_prototype(
|
|||
protparent = protparents.get(protstring)
|
||||
if not protparent:
|
||||
_flags["errors"].append(
|
||||
"Prototype {}'s prototype_parent '{}' was not found.".format((protkey, protstring))
|
||||
"Prototype {}'s prototype_parent '{}' was not found.".format(protkey, protstring)
|
||||
)
|
||||
if id(prototype) in _flags["visited"]:
|
||||
_flags["errors"].append(
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ Unit tests for the prototypes and spawner
|
|||
|
||||
"""
|
||||
|
||||
from random import randint
|
||||
from random import randint, sample
|
||||
import mock
|
||||
import uuid
|
||||
from time import time
|
||||
from anything import Something
|
||||
from django.test.utils import override_settings
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
|
@ -628,8 +630,10 @@ class TestPrototypeStorage(EvenniaTest):
|
|||
|
||||
# partial match
|
||||
with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}):
|
||||
self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3])
|
||||
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3])
|
||||
self.assertCountEqual(
|
||||
protlib.search_prototype("prot"), [prot1b, prot2, prot3])
|
||||
self.assertCountEqual(
|
||||
protlib.search_prototype(tags="foo1"), [prot1b, prot2, prot3])
|
||||
|
||||
self.assertTrue(str(str(protlib.list_prototypes(self.char1))))
|
||||
|
||||
|
|
@ -1073,3 +1077,29 @@ class TestOLCMenu(TestEvMenu):
|
|||
["node_index", "node_index", "node_index"],
|
||||
],
|
||||
]
|
||||
|
||||
class PrototypeCrashTest(EvenniaTest):
|
||||
|
||||
# increase this to 1000 for optimization testing
|
||||
num_prototypes = 10
|
||||
|
||||
def create(self, num=None):
|
||||
if not num:
|
||||
num = self.num_prototypes
|
||||
# print(f"Creating {num} additional prototypes...")
|
||||
for x in range(num):
|
||||
prot = {
|
||||
'prototype_key': str(uuid.uuid4()),
|
||||
'some_attributes': [str(uuid.uuid4()) for x in range(10)],
|
||||
'prototype_tags': list(sample(['demo', 'test', 'stuff'], 2)),
|
||||
}
|
||||
protlib.save_prototype(prot)
|
||||
|
||||
def test_prototype_dos(self, *args, **kwargs):
|
||||
num_prototypes = self.num_prototypes
|
||||
for x in range(2):
|
||||
self.create(num_prototypes)
|
||||
# print("Attempting to list prototypes...")
|
||||
# start_time = time()
|
||||
self.char1.execute_cmd('spawn/list')
|
||||
# print(f"Prototypes listed in {time()-start_time} seconds.")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue