Resolve merge conflicts

This commit is contained in:
Griatch 2020-09-16 23:49:05 +02:00
commit 9cdc37355c
25 changed files with 726 additions and 444 deletions

View file

@ -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(

View file

@ -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.")