Resolve merge conflicts
This commit is contained in:
commit
19bd7ce0b7
19 changed files with 32231 additions and 105 deletions
58
.github/workflows/github_action_test_suite.yml
vendored
58
.github/workflows/github_action_test_suite.yml
vendored
|
|
@ -33,6 +33,7 @@ jobs:
|
||||||
postgresql db: 'evennia'
|
postgresql db: 'evennia'
|
||||||
postgresql user: 'evennia'
|
postgresql user: 'evennia'
|
||||||
postgresql password: 'password'
|
postgresql password: 'password'
|
||||||
|
|
||||||
- name: Set up MySQL server
|
- name: Set up MySQL server
|
||||||
uses: mirromutth/mysql-action@v1.1
|
uses: mirromutth/mysql-action@v1.1
|
||||||
if: ${{ matrix.TESTING_DB == 'mysql'}}
|
if: ${{ matrix.TESTING_DB == 'mysql'}}
|
||||||
|
|
@ -46,16 +47,48 @@ jobs:
|
||||||
mysql database: 'evennia'
|
mysql database: 'evennia'
|
||||||
mysql user: 'evennia'
|
mysql user: 'evennia'
|
||||||
mysql password: 'password'
|
mysql password: 'password'
|
||||||
|
mysql root password: root_password
|
||||||
|
|
||||||
# wait for db to activage, get logs from their start
|
# wait for db to activate
|
||||||
- name: Wait / sleep
|
- name: wait for db to activate
|
||||||
uses: jakejarvis/wait-action@v0.1.0
|
if: matrix.TESTING_DB == 'postgresql' || matrix.TESTING_DB == 'mysql'
|
||||||
if: ${{ matrix.TESTING_DB == 'postgresql' || matrix.TESTING_DB == 'mysql' }}
|
run: |
|
||||||
with:
|
|
||||||
time: '10s'
|
if [ ${{ matrix.TESTING_DB }} = mysql ]
|
||||||
|
then
|
||||||
|
while ! mysqladmin ping -h 127.0.0.1 -u root -proot_password -s >/dev/null 2>&1
|
||||||
|
do
|
||||||
|
sleep 1
|
||||||
|
echo -n .
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
while ! pg_isready -h 127.0.0.1 -q >/dev/null 2>&1
|
||||||
|
do
|
||||||
|
sleep 1
|
||||||
|
echo -n .
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: mysql privileges
|
||||||
|
if: matrix.TESTING_DB == 'mysql'
|
||||||
|
run: |
|
||||||
|
|
||||||
|
cat <<EOF | mysql -u root -proot_password -h 127.0.0.1 mysql
|
||||||
|
create user 'evennia'@'%' identified by 'password';
|
||||||
|
grant all on \`evennia%\`.* to 'evennia'@'%';
|
||||||
|
grant process on *.* to 'evennia'@'%';
|
||||||
|
flush privileges
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# get logs from db start
|
||||||
- name: Database container logs
|
- name: Database container logs
|
||||||
|
if: matrix.TESTING_DB == 'postgresql' || matrix.TESTING_DB == 'mysql'
|
||||||
uses: jwalton/gh-docker-logs@v1.0.0
|
uses: jwalton/gh-docker-logs@v1.0.0
|
||||||
|
|
||||||
- name: Check running containers
|
- name: Check running containers
|
||||||
|
if: matrix.TESTING_DB == 'postgresql' || matrix.TESTING_DB == 'mysql'
|
||||||
run: docker ps -a
|
run: docker ps -a
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
|
@ -71,6 +104,7 @@ jobs:
|
||||||
pip install mysqlclient
|
pip install mysqlclient
|
||||||
pip install coveralls
|
pip install coveralls
|
||||||
pip install codacy-coverage
|
pip install codacy-coverage
|
||||||
|
pip install tblib
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
- name: Install extra dependencies
|
- name: Install extra dependencies
|
||||||
|
|
@ -87,7 +121,15 @@ jobs:
|
||||||
- name: Run test suite
|
- name: Run test suite
|
||||||
run: |
|
run: |
|
||||||
cd testing_mygame
|
cd testing_mygame
|
||||||
coverage run --source=../evennia --omit=*/migrations/*,*/urls.py,*/test*.py,*.sh,*.txt,*.md,*.pyc,*.service ../bin/unix/evennia test --settings=settings --keepdb evennia
|
coverage run \
|
||||||
|
--source=../evennia \
|
||||||
|
--omit=*/migrations/*,*/urls.py,*/test*.py,*.sh,*.txt,*.md,*.pyc,*.service \
|
||||||
|
../bin/unix/evennia test \
|
||||||
|
--settings=settings \
|
||||||
|
--keepdb \
|
||||||
|
--parallel 4 \
|
||||||
|
--timing \
|
||||||
|
evennia
|
||||||
coverage xml
|
coverage xml
|
||||||
|
|
||||||
# we only want to run coverall/codacy once, so we only do it for one of the matrix combinations
|
# we only want to run coverall/codacy once, so we only do it for one of the matrix combinations
|
||||||
|
|
@ -109,7 +151,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||||
coverage-reports: ./testing_mygame/coverage.xml
|
coverage-reports: ./testing_mygame/coverage.xml
|
||||||
|
|
||||||
# docker setup and push
|
# docker setup and push
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,15 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
||||||
e.g. webclient pane filtering where desired. (volund)
|
e.g. webclient pane filtering where desired. (volund)
|
||||||
- Added `Account.uses_screenreader(session=None)` as a quick shortcut for
|
- Added `Account.uses_screenreader(session=None)` as a quick shortcut for
|
||||||
finding if a user uses a screenreader (and adjust display accordingly).
|
finding if a user uses a screenreader (and adjust display accordingly).
|
||||||
|
- Fixed bug in `cmdset.remove()` where a command could not be deleted by `key`,
|
||||||
|
even though doc suggested one could (ChrisLR)
|
||||||
|
- New contrib `name_generator` for building random real-world based or fantasy-names
|
||||||
|
based on phonetic rules.
|
||||||
|
- Enable proper serialization of dict subclasses in Attributes (aogier)
|
||||||
|
- `object.search` fuzzy-matching now uses `icontains` instead of `istartswith`
|
||||||
|
to better match how search works elsewhere (volund)
|
||||||
|
- The `.at_traverse` hook now receives a `exit_obj` kwarg, linking back to the
|
||||||
|
exit triggering the hook (volund)
|
||||||
|
|
||||||
|
|
||||||
## Evennia 0.9.5
|
## Evennia 0.9.5
|
||||||
|
|
|
||||||
44
LICENSE.txt
44
LICENSE.txt
|
|
@ -1,35 +1,19 @@
|
||||||
BSD license
|
BSD 3-Clause License
|
||||||
===========
|
|
||||||
|
|
||||||
Evennia MU* creation system
|
Copyright 2012- Griatch (griatch <AT> gmail <DOT> com), Gregory Taylor
|
||||||
Copyright (c) 2012-, Griatch (griatch <AT> gmail <DOT> com), Gregory Taylor
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
- Redistributions of source code must retain the above copyright
|
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
- Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer in the
|
|
||||||
documentation and/or other materials provided with the distribution.
|
|
||||||
- Neither the name of the Copyright Holders nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
|
||||||
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
|
in the documentation and/or other materials provided with the distribution.
|
||||||
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
|
||||||
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
||||||
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
||||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
|
||||||
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
|
||||||
|
from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||||
|
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
|
||||||
|
|
@ -314,7 +314,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
||||||
return
|
return
|
||||||
if not obj.access(self, "puppet"):
|
if not obj.access(self, "puppet"):
|
||||||
# no access
|
# no access
|
||||||
self.msg("You don't have permission to puppet '{obj.key}'.")
|
self.msg(f"You don't have permission to puppet '{obj.key}'.")
|
||||||
return
|
return
|
||||||
if obj.account:
|
if obj.account:
|
||||||
# object already puppeted
|
# object already puppeted
|
||||||
|
|
|
||||||
|
|
@ -61,38 +61,14 @@ def build_matches(raw_string, cmdset, include_prefixes=False):
|
||||||
"""
|
"""
|
||||||
matches = []
|
matches = []
|
||||||
try:
|
try:
|
||||||
if include_prefixes:
|
orig_string = raw_string
|
||||||
# use the cmdname as-is
|
if not include_prefixes and len(raw_string) > 1:
|
||||||
l_raw_string = raw_string.lower()
|
raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES)
|
||||||
for cmd in cmdset:
|
search_string = raw_string.lower()
|
||||||
matches.extend(
|
for cmd in cmdset:
|
||||||
[
|
cmdname, raw_cmdname = cmd.match(search_string, include_prefixes=include_prefixes)
|
||||||
create_match(cmdname, raw_string, cmd, cmdname)
|
if cmdname:
|
||||||
for cmdname in [cmd.key] + cmd.aliases
|
matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
|
||||||
if cmdname
|
|
||||||
and l_raw_string.startswith(cmdname.lower())
|
|
||||||
and (not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname) :]))
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# strip prefixes set in settings
|
|
||||||
raw_string = (
|
|
||||||
raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string
|
|
||||||
)
|
|
||||||
l_raw_string = raw_string.lower()
|
|
||||||
for cmd in cmdset:
|
|
||||||
for raw_cmdname in [cmd.key] + cmd.aliases:
|
|
||||||
cmdname = (
|
|
||||||
raw_cmdname.lstrip(_CMD_IGNORE_PREFIXES)
|
|
||||||
if len(raw_cmdname) > 1
|
|
||||||
else raw_cmdname
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
cmdname
|
|
||||||
and l_raw_string.startswith(cmdname.lower())
|
|
||||||
and (not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname) :]))
|
|
||||||
):
|
|
||||||
matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log_trace("cmdhandler error. raw_input:%s" % raw_string)
|
log_trace("cmdhandler error. raw_input:%s" % raw_string)
|
||||||
return matches
|
return matches
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ Set theory.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from weakref import WeakKeyDictionary
|
from weakref import WeakKeyDictionary
|
||||||
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from evennia.utils.utils import inherits_from, is_iter
|
from evennia.utils.utils import inherits_from, is_iter
|
||||||
|
|
||||||
|
|
@ -546,10 +547,7 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
||||||
commands[ic] = cmd # replace
|
commands[ic] = cmd # replace
|
||||||
except ValueError:
|
except ValueError:
|
||||||
commands.append(cmd)
|
commands.append(cmd)
|
||||||
self.commands = commands
|
|
||||||
if not allow_duplicates:
|
|
||||||
# extra run to make sure to avoid doublets
|
|
||||||
self.commands = list(set(self.commands))
|
|
||||||
# add system_command to separate list as well,
|
# add system_command to separate list as well,
|
||||||
# for quick look-up
|
# for quick look-up
|
||||||
if cmd.key.startswith("__"):
|
if cmd.key.startswith("__"):
|
||||||
|
|
@ -559,6 +557,11 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
system_commands.append(cmd)
|
system_commands.append(cmd)
|
||||||
|
|
||||||
|
self.commands = commands
|
||||||
|
if not allow_duplicates:
|
||||||
|
# extra run to make sure to avoid doublets
|
||||||
|
self.commands = list(set(self.commands))
|
||||||
|
|
||||||
def remove(self, cmd):
|
def remove(self, cmd):
|
||||||
"""
|
"""
|
||||||
Remove a command instance from the cmdset.
|
Remove a command instance from the cmdset.
|
||||||
|
|
@ -568,6 +571,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
||||||
or the key of such a command.
|
or the key of such a command.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if isinstance(cmd, str):
|
||||||
|
_cmd = next((_cmd for _cmd in self.commands if _cmd.key == cmd), None)
|
||||||
|
if _cmd is None:
|
||||||
|
if not cmd.startswith("__"):
|
||||||
|
# if a syscommand, keep the original string and instantiate on it
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
cmd = _cmd
|
||||||
|
|
||||||
cmd = self._instantiate(cmd)
|
cmd = self._instantiate(cmd)
|
||||||
if cmd.key.startswith("__"):
|
if cmd.key.startswith("__"):
|
||||||
try:
|
try:
|
||||||
|
|
@ -591,6 +603,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
||||||
cmd (Command): The first matching Command in the set.
|
cmd (Command): The first matching Command in the set.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if isinstance(cmd, str):
|
||||||
|
_cmd = next((_cmd for _cmd in self.commands if _cmd.key == cmd), None)
|
||||||
|
if _cmd is None:
|
||||||
|
if not cmd.startswith("__"):
|
||||||
|
# if a syscommand, keep the original string and instantiate on it
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
cmd = _cmd
|
||||||
|
|
||||||
cmd = self._instantiate(cmd)
|
cmd = self._instantiate(cmd)
|
||||||
for thiscmd in self.commands:
|
for thiscmd in self.commands:
|
||||||
if thiscmd == cmd:
|
if thiscmd == cmd:
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@ class Command(metaclass=CommandMeta):
|
||||||
"""
|
"""
|
||||||
if kwargs:
|
if kwargs:
|
||||||
_init_command(self, **kwargs)
|
_init_command(self, **kwargs)
|
||||||
|
self._optimize()
|
||||||
|
|
||||||
@lazy_property
|
@lazy_property
|
||||||
def lockhandler(self):
|
def lockhandler(self):
|
||||||
|
|
@ -295,10 +296,15 @@ class Command(metaclass=CommandMeta):
|
||||||
Optimize the key and aliases for lookups.
|
Optimize the key and aliases for lookups.
|
||||||
"""
|
"""
|
||||||
# optimization - a set is much faster to match against than a list
|
# optimization - a set is much faster to match against than a list
|
||||||
self._matchset = set([self.key] + self.aliases)
|
matches = [self.key.lower()]
|
||||||
|
matches.extend(x.lower() for x in self.aliases)
|
||||||
|
|
||||||
|
self._matchset = set(matches)
|
||||||
# optimization for looping over keys+aliases
|
# optimization for looping over keys+aliases
|
||||||
self._keyaliases = tuple(self._matchset)
|
self._keyaliases = tuple(self._matchset)
|
||||||
|
|
||||||
|
self._noprefix_aliases = {x.lstrip(CMD_IGNORE_PREFIXES): x for x in matches}
|
||||||
|
|
||||||
def set_key(self, new_key):
|
def set_key(self, new_key):
|
||||||
"""
|
"""
|
||||||
Update key.
|
Update key.
|
||||||
|
|
@ -334,7 +340,7 @@ class Command(metaclass=CommandMeta):
|
||||||
self.aliases = list(set(alias for alias in aliases if alias != self.key))
|
self.aliases = list(set(alias for alias in aliases if alias != self.key))
|
||||||
self._optimize()
|
self._optimize()
|
||||||
|
|
||||||
def match(self, cmdname):
|
def match(self, cmdname, include_prefixes=True):
|
||||||
"""
|
"""
|
||||||
This is called by the system when searching the available commands,
|
This is called by the system when searching the available commands,
|
||||||
in order to determine if this is the one we wanted. cmdname was
|
in order to determine if this is the one we wanted. cmdname was
|
||||||
|
|
@ -343,11 +349,23 @@ class Command(metaclass=CommandMeta):
|
||||||
Args:
|
Args:
|
||||||
cmdname (str): Always lowercase when reaching this point.
|
cmdname (str): Always lowercase when reaching this point.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
include_prefixes (bool): If false, will compare against the _noprefix
|
||||||
|
variants of commandnames.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
result (bool): Match result.
|
result (bool): Match result.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return cmdname in self._matchset
|
if include_prefixes:
|
||||||
|
for cmd_key in self._keyaliases:
|
||||||
|
if cmdname.startswith(cmd_key) and (not self.arg_regex or self.arg_regex.match(cmdname[len(cmd_key) :])):
|
||||||
|
return cmd_key, cmd_key
|
||||||
|
else:
|
||||||
|
for k, v in self._noprefix_aliases.items():
|
||||||
|
if cmdname.startswith(k) and (not self.arg_regex or self.arg_regex.match(cmdname[len(k) :])):
|
||||||
|
return k, v
|
||||||
|
return None, None
|
||||||
|
|
||||||
def access(self, srcobj, access_type="cmd", default=False):
|
def access(self, srcobj, access_type="cmd", default=False):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1927,7 +1927,7 @@ class CmdSetAttribute(ObjManipCommand):
|
||||||
if self.rhs is None:
|
if self.rhs is None:
|
||||||
# no = means we inspect the attribute(s)
|
# no = means we inspect the attribute(s)
|
||||||
if not attrs:
|
if not attrs:
|
||||||
attrs = [attr.key for attr in obj.attributes.get(category=None, return_obj=True)]
|
attrs = [attr.key for attr in obj.attributes.get(category=None, return_obj=True, return_list=True)]
|
||||||
for attr in attrs:
|
for attr in attrs:
|
||||||
if not self.check_attr(obj, attr, category):
|
if not self.check_attr(obj, attr, category):
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -874,14 +874,14 @@ class CmdSetHelp(CmdHelp):
|
||||||
if isinstance(match, HelpCategory):
|
if isinstance(match, HelpCategory):
|
||||||
warning = (
|
warning = (
|
||||||
f"'{querystr}' matches (or partially matches) the name of "
|
f"'{querystr}' matches (or partially matches) the name of "
|
||||||
"help-category '{match.key}'. If you continue, your help entry will "
|
f"help-category '{match.key}'. If you continue, your help entry will "
|
||||||
"take precedence and the category (or part of its name) *may* not "
|
"take precedence and the category (or part of its name) *may* not "
|
||||||
"be usable for grouping help entries anymore."
|
"be usable for grouping help entries anymore."
|
||||||
)
|
)
|
||||||
elif inherits_from(match, "evennia.commands.command.Command"):
|
elif inherits_from(match, "evennia.commands.command.Command"):
|
||||||
warning = (
|
warning = (
|
||||||
f"'{querystr}' matches (or partially matches) the key/alias of "
|
f"'{querystr}' matches (or partially matches) the key/alias of "
|
||||||
"Command '{match.key}'. Command-help take precedence over other "
|
f"Command '{match.key}'. Command-help take precedence over other "
|
||||||
"help entries so your help *may* be impossible to reach for those "
|
"help entries so your help *may* be impossible to reach for those "
|
||||||
"with access to that command."
|
"with access to that command."
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1199,3 +1199,21 @@ class TestCmdSetNesting(BaseEvenniaTest):
|
||||||
|
|
||||||
cmd = self.char1.cmdset.cmdset_stack[-1].commands[0]
|
cmd = self.char1.cmdset.cmdset_stack[-1].commands[0]
|
||||||
self.assertEqual(cmd.obj, self.char1)
|
self.assertEqual(cmd.obj, self.char1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCmdSet(BaseEvenniaTest):
|
||||||
|
"""
|
||||||
|
General tests for cmdsets
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_cmdset_remove_by_key(self):
|
||||||
|
test_cmd_set = _CmdSetTest()
|
||||||
|
test_cmd_set.remove("another command")
|
||||||
|
|
||||||
|
self.assertNotIn(_CmdTest2, test_cmd_set.commands)
|
||||||
|
|
||||||
|
def test_cmdset_gets_by_key(self):
|
||||||
|
test_cmd_set = _CmdSetTest()
|
||||||
|
result = test_cmd_set.get("another command")
|
||||||
|
|
||||||
|
self.assertIsInstance(result, _CmdTest2)
|
||||||
|
|
|
||||||
277
evennia/contrib/utils/name_generator/README.md
Normal file
277
evennia/contrib/utils/name_generator/README.md
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
# Random Name Generator
|
||||||
|
|
||||||
|
Contribution by InspectorCaracal (2022)
|
||||||
|
|
||||||
|
A module for generating random names, both real-world and fantasy. Real-world
|
||||||
|
names can be generated either as first (personal) names, family (last) names, or
|
||||||
|
full names (first, optional middles, and last). The name data is from [Behind the Name](https://www.behindthename.com/)
|
||||||
|
and used under the [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||||
|
|
||||||
|
Fantasy names are generated from basic phonetic rules, using CVC syllable syntax.
|
||||||
|
|
||||||
|
Both real-world and fantasy name generation can be extended to include additional
|
||||||
|
information via your game's `settings.py`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This is a stand-alone utility. Just import this module (`from evennia.contrib.utils import name_generator`) and use its functions wherever you like.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Import the module where you need it with the following:
|
||||||
|
```py
|
||||||
|
from evennia.contrib.utils.name_generator import namegen
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, all of the functions will return a string with one generated name.
|
||||||
|
If you specify more than one, or pass `return_list=True` as a keyword argument, the returned value will be a list of strings.
|
||||||
|
|
||||||
|
The module is especially useful for naming newly-created NPCs, like so:
|
||||||
|
```py
|
||||||
|
npc_name = namegen.full_name()
|
||||||
|
npc_obj = create_object(key=npc_name, typeclass="typeclasses.characters.NPC")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Settings
|
||||||
|
|
||||||
|
These settings can all be defined in your game's `server/conf/settings.py` file.
|
||||||
|
|
||||||
|
- `NAMEGEN_FIRST_NAMES` adds a new list of first (personal) names.
|
||||||
|
- `NAMEGEN_LAST_NAMES` adds a new list of last (family) names.
|
||||||
|
- `NAMEGEN_REPLACE_LISTS` - set to `True` if you want to use only the names defined in your settings.
|
||||||
|
- `NAMEGEN_FANTASY_RULES` lets you add new phonetic rules for generating entirely made-up names. See the section "Custom Fantasy Name style rules" for details on how this should look.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```py
|
||||||
|
NAMEGEN_FIRST_NAMES = [
|
||||||
|
("Evennia", 'mf'),
|
||||||
|
("Green Tea", 'f'),
|
||||||
|
]
|
||||||
|
|
||||||
|
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
|
||||||
|
|
||||||
|
NAMEGEN_FANTASY_RULES = {
|
||||||
|
"example_style": {
|
||||||
|
"syllable": "(C)VC",
|
||||||
|
"consonants": [ 'z','z','ph','sh','r','n' ],
|
||||||
|
"start": ['m'],
|
||||||
|
"end": ['x','n'],
|
||||||
|
"vowels": [ "e","e","e","a","i","i","u","o", ],
|
||||||
|
"length": (2,4),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Generating Real Names
|
||||||
|
|
||||||
|
The contrib offers three functions for generating random real-world names:
|
||||||
|
`first_name()`, `last_name()`, and `full_name()`. If you want more than one name
|
||||||
|
generated at once, you can use the `num` keyword argument to specify how many.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
>>> namegen.first_name(num=5)
|
||||||
|
['Genesis', 'Tali', 'Budur', 'Dominykas', 'Kamau']
|
||||||
|
>>> namegen.first_name(gender='m')
|
||||||
|
'Blanchard'
|
||||||
|
```
|
||||||
|
|
||||||
|
The `first_name` function also takes a `gender` keyword argument to filter names
|
||||||
|
by gender association. 'f' for feminine, 'm' for masculine, 'mf' for feminine
|
||||||
|
_and_ masculine, or the default `None` to match any gendering.
|
||||||
|
|
||||||
|
The `full_name` function also takes the `gender` keyword, as well as `parts` which
|
||||||
|
defines how many names make up the full name. The minimum is two: a first name and
|
||||||
|
a last name. You can also generate names with the family name first by setting
|
||||||
|
the keyword arg `surname_first` to `True`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
>>> namegen.full_name()
|
||||||
|
'Keeva Bernat'
|
||||||
|
>>> namegen.full_name(parts=4)
|
||||||
|
'Suzu Shabnam Kafka Baier'
|
||||||
|
>>> namegen.full_name(parts=3, surname_first=True)
|
||||||
|
'Ó Muircheartach Torunn Dyson'
|
||||||
|
>>> namegen.full_name(gender='f')
|
||||||
|
'Wikolia Ó Deasmhumhnaigh'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding your own names
|
||||||
|
|
||||||
|
You can add additional names with the settings `NAMEGEN_FIRST_NAMES` and
|
||||||
|
`NAMEGEN_LAST_NAMES`
|
||||||
|
|
||||||
|
`NAMEGEN_FIRST_NAMES` should be a list of tuples, where the first value is the name
|
||||||
|
and then second value is the gender flag - 'm' for masculine-only, 'f' for feminine-
|
||||||
|
only, and 'mf' for either one.
|
||||||
|
|
||||||
|
`NAMEGEN_LAST_NAMES` should be a list of strings, where each item is an available
|
||||||
|
surname.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```py
|
||||||
|
NAMEGEN_FIRST_NAMES = [
|
||||||
|
("Evennia", 'mf'),
|
||||||
|
("Green Tea", 'f'),
|
||||||
|
]
|
||||||
|
|
||||||
|
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `NAMEGEN_REPLACE_LISTS = True` if you want your custom lists above to entirely replace the built-in lists rather than extend them.
|
||||||
|
|
||||||
|
## Generating Fantasy Names
|
||||||
|
|
||||||
|
Generating completely made-up names is done with the `fantasy_name` function. The
|
||||||
|
contrib comes with three built-in styles of names which you can use, or you can
|
||||||
|
put a dictionary of custom name rules into `settings.py`
|
||||||
|
|
||||||
|
Generating a fantasy name takes the ruleset key as the "style" keyword, and can
|
||||||
|
return either a single name or multiple names. By default, it will return a
|
||||||
|
single name in the built-in "harsh" style. The contrib also comes with "fluid" and "alien" styles.
|
||||||
|
|
||||||
|
```py
|
||||||
|
>>> namegen.fantasy_name()
|
||||||
|
'Vhon'
|
||||||
|
>>> namegen.fantasy_name(num=3, style="harsh")
|
||||||
|
['Kha', 'Kizdhu', 'Godögäk']
|
||||||
|
>>> namegen.fantasy_name(num=3, style="fluid")
|
||||||
|
['Aewalisash', 'Ayi', 'Iaa']
|
||||||
|
>>> namegen.fantasy_name(num=5, style="alien")
|
||||||
|
["Qz'vko'", "Xv'w'hk'hxyxyz", "Wxqv'hv'k", "Wh'k", "Xbx'qk'vz"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Word Fantasy Names
|
||||||
|
|
||||||
|
The `fantasy_name` function will only generate one name-word at a time, so for multi-word names
|
||||||
|
you'll need to combine pieces together. Depending on what kind of end result you want, there are
|
||||||
|
several approaches.
|
||||||
|
|
||||||
|
|
||||||
|
#### The simple approach
|
||||||
|
|
||||||
|
If all you need is for it to have multiple parts, you can generate multiple names at once and `join` them.
|
||||||
|
|
||||||
|
```py
|
||||||
|
>>> name = " ".join(namegen.fantasy_name(num=2))
|
||||||
|
>>> name
|
||||||
|
'Dezhvözh Khäk'
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want a little more variation between first/last names, you can also generate names for
|
||||||
|
different styles and then combine them.
|
||||||
|
|
||||||
|
```py
|
||||||
|
>>> first = namegen.fantasy_name(style="fluid")
|
||||||
|
>>> last = namegen.fantasy_name(style="harsh")
|
||||||
|
>>> name = f"{first} {last}"
|
||||||
|
>>> name
|
||||||
|
'Ofasa Käkudhu'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "Nakku Silversmith"
|
||||||
|
|
||||||
|
One common fantasy name practice is profession- or title-based surnames. To achieve this effect,
|
||||||
|
you can use the `last_name` function with a custom list of last names and combine it with your generated
|
||||||
|
fantasy name.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```py
|
||||||
|
NAMEGEN_LAST_NAMES = [ "Silversmith", "the Traveller", "Destroyer of Worlds" ]
|
||||||
|
NAMEGEN_REPLACE_LISTS = True
|
||||||
|
|
||||||
|
>>> first = namegen.fantasy_name()
|
||||||
|
>>> last = namegen.last_name()
|
||||||
|
>>> name = f"{first} {last}"
|
||||||
|
>>> name
|
||||||
|
'Tözhkheko the Traveller'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Elarion d'Yrinea, Thror Obinson
|
||||||
|
|
||||||
|
Another common flavor of fantasy names is to use a surname suffix or prefix. For that, you'll
|
||||||
|
need to add in the extra bit yourself.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```py
|
||||||
|
>>> names = namegen.fantasy_name(num=2)
|
||||||
|
>>> name = f"{names[0]} za'{names[1]}"
|
||||||
|
>>> name
|
||||||
|
"Tithe za'Dhudozkok"
|
||||||
|
|
||||||
|
>>> names = namegen.fantasy_name(num=2)
|
||||||
|
>>> name = f"{names[0]} {names[1]}son"
|
||||||
|
>>> name
|
||||||
|
'Kön Ködhöddoson'
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Custom Fantasy Name style rules
|
||||||
|
|
||||||
|
The style rules are contained in a dictionary of dictionaries, where the style name
|
||||||
|
is the key and the style rules are the dictionary value.
|
||||||
|
|
||||||
|
The following is how you would add a custom style to `settings.py`:
|
||||||
|
```py
|
||||||
|
NAMEGEN_FANTASY_RULES = {
|
||||||
|
"example_style": {
|
||||||
|
"syllable": "(C)VC",
|
||||||
|
"consonants": [ 'z','z','ph','sh','r','n' ],
|
||||||
|
"start": ['m'],
|
||||||
|
"end": ['x','n'],
|
||||||
|
"vowels": [ "e","e","e","a","i","i","u","o", ],
|
||||||
|
"length": (2,4),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you could generate names following that ruleset with `namegen.fantasy_name(style="example_style")`.
|
||||||
|
|
||||||
|
The keys `syllable`, `consonants`, `vowels`, and `length` must be present, and `length` must be the minimum and maximum syllable counts. `start` and `end` are optional.
|
||||||
|
|
||||||
|
|
||||||
|
#### syllable
|
||||||
|
The "syllable" field defines the structure of each syllable. C is consonant, V is vowel,
|
||||||
|
and parentheses mean it's optional. So, the example `(C)VC` means that every syllable
|
||||||
|
will always have a vowel followed by a consonant, and will *sometimes* have another
|
||||||
|
consonant at the beginning. e.g. `en`, `bak`
|
||||||
|
|
||||||
|
*Note:* While it's not standard, the contrib lets you nest parentheses, with each layer
|
||||||
|
being less likely to show up. Additionally, any other characters put into the syllable
|
||||||
|
structure - e.g. an apostrophe - will be read and inserted as written. The
|
||||||
|
"alien" style rules in the module gives an example of both: the syllable structure is `C(C(V))(')(C)`
|
||||||
|
which results in syllables such as `khq`, `xho'q`, and `q'` with a much lower frequency of vowels than
|
||||||
|
`C(C)(V)(')(C)` would have given.
|
||||||
|
|
||||||
|
#### consonants
|
||||||
|
A simple list of consonant phonemes that can be chosen from. Multi-character strings are
|
||||||
|
perfectly acceptable, such as "th", but each one will be treated as a single consonant.
|
||||||
|
|
||||||
|
The function uses a naive form of weighting, where you make a phoneme more likely to
|
||||||
|
occur by putting more copies of it into the list.
|
||||||
|
|
||||||
|
#### start and end
|
||||||
|
These are **optional** lists for the first and last letters of a syllable, if they're
|
||||||
|
a consonant. You can add on additional consonants which can only occur at the beginning
|
||||||
|
or end of a syllable, or you can add extra copies of already-defined consonants to
|
||||||
|
increase the frequency of them at the start/end of syllables.
|
||||||
|
|
||||||
|
For example, in the `example_style` above, we have a `start` of m, and `end` of x and n.
|
||||||
|
Taken with the rest of the consonants/vowels, this means you can have the syllables of `mez`
|
||||||
|
but not `zem`, and you can have `phex` or `phen` but not `xeph` or `neph`.
|
||||||
|
|
||||||
|
They can be left out of custom rulesets entirely.
|
||||||
|
|
||||||
|
#### vowels
|
||||||
|
Vowels is a simple list of vowel phonemes - exactly like consonants, but instead used for the
|
||||||
|
vowel selection. Single-or multi-character strings are equally fine. It uses the same naive weighting system
|
||||||
|
as consonants - you can increase the frequency of any given vowel by putting it into the list multiple times.
|
||||||
|
|
||||||
|
#### length
|
||||||
|
A tuple with the minimum and maximum number of syllables a name can have.
|
||||||
|
|
||||||
|
When setting this, keep in mind how long your syllables can get! 4 syllables might
|
||||||
|
not seem like very many, but if you have a (C)(V)VC structure with one- and
|
||||||
|
two-letter phonemes, you can get up to eight characters per syllable.
|
||||||
24215
evennia/contrib/utils/name_generator/btn_givennames.txt
Normal file
24215
evennia/contrib/utils/name_generator/btn_givennames.txt
Normal file
File diff suppressed because it is too large
Load diff
6939
evennia/contrib/utils/name_generator/btn_surnames.txt
Normal file
6939
evennia/contrib/utils/name_generator/btn_surnames.txt
Normal file
File diff suppressed because it is too large
Load diff
355
evennia/contrib/utils/name_generator/namegen.py
Normal file
355
evennia/contrib/utils/name_generator/namegen.py
Normal file
|
|
@ -0,0 +1,355 @@
|
||||||
|
"""
|
||||||
|
Random Name Generator
|
||||||
|
|
||||||
|
Contribution by InspectorCaracal (2022)
|
||||||
|
|
||||||
|
A module for generating random names, both real-world and fantasy. Real-world
|
||||||
|
names can be generated either as first (personal) names, family (last) names, or
|
||||||
|
full names (first, optional middles, and last). The name data is from [Behind the Name](https://www.behindthename.com/)
|
||||||
|
and used under the [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||||
|
|
||||||
|
Fantasy names are generated from basic phonetic rules, using CVC syllable syntax.
|
||||||
|
|
||||||
|
Both real-world and fantasy name generation can be extended to include additional
|
||||||
|
information via your game's `settings.py`
|
||||||
|
|
||||||
|
|
||||||
|
Available Methods:
|
||||||
|
|
||||||
|
first_name - Selects a random a first (personal) name from the name lists.
|
||||||
|
last_name - Selects a random last (family) name from the name lists.
|
||||||
|
full_name - Generates a randomized full name, optionally including middle names, by selecting first/last names from the name lists.
|
||||||
|
fantasy_name - Generates a completely new made-up name based on phonetic rules.
|
||||||
|
|
||||||
|
Method examples:
|
||||||
|
|
||||||
|
>>> namegen.first_name(num=5)
|
||||||
|
['Genesis', 'Tali', 'Budur', 'Dominykas', 'Kamau']
|
||||||
|
|
||||||
|
>>> namegen.full_name(parts=3, surname_first=True)
|
||||||
|
'Ó Muircheartach Torunn Dyson'
|
||||||
|
>>> namegen.full_name(gender='f')
|
||||||
|
'Wikolia Ó Deasmhumhnaigh'
|
||||||
|
|
||||||
|
>>> namegen.fantasy_name(num=3, style="fluid")
|
||||||
|
['Aewalisash', 'Ayi', 'Iaa']
|
||||||
|
|
||||||
|
|
||||||
|
Available Settings (define these in your `settings.py`)
|
||||||
|
|
||||||
|
NAMEGEN_FIRST_NAMES - Option to add a new list of first (personal) names.
|
||||||
|
NAMEGEN_LAST_NAMES - Option to add a new list of last (family) names.
|
||||||
|
NAMEGEN_REPLACE_LISTS - Set to True if you want to use ONLY your name lists and not the ones that come with the contrib.
|
||||||
|
NAMEGEN_FANTASY_RULES - Option to add new fantasy-name style rules.
|
||||||
|
Must be a dictionary that includes "syllable", "consonants", "vowels", and "length" - see the example.
|
||||||
|
"start" and "end" keys are optional.
|
||||||
|
|
||||||
|
Settings examples:
|
||||||
|
|
||||||
|
NAMEGEN_FIRST_NAMES = [
|
||||||
|
("Evennia", 'mf'),
|
||||||
|
("Green Tea", 'f'),
|
||||||
|
]
|
||||||
|
|
||||||
|
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
|
||||||
|
|
||||||
|
NAMEGEN_FANTASY_RULES = {
|
||||||
|
"example_style": {
|
||||||
|
"syllable": "(C)VC",
|
||||||
|
"consonants": [ 'z','z','ph','sh','r','n' ],
|
||||||
|
"start": ['m'],
|
||||||
|
"end": ['x','n'],
|
||||||
|
"vowels": [ "e","e","e","a","i","i","u","o", ],
|
||||||
|
"length": (2,4),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
from os import path
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from evennia.utils.utils import is_iter
|
||||||
|
|
||||||
|
# Load name data from Behind the Name lists
|
||||||
|
dirpath = path.dirname(path.abspath(__file__))
|
||||||
|
_FIRSTNAME_LIST = []
|
||||||
|
with open(path.join(dirpath, "btn_givennames.txt"),'r', encoding='utf-8') as file:
|
||||||
|
_FIRSTNAME_LIST = [ line.strip().rsplit(" ") for line in file if line and not line.startswith("#") ]
|
||||||
|
|
||||||
|
_SURNAME_LIST = []
|
||||||
|
with open(path.join(dirpath, "btn_surnames.txt"),'r', encoding='utf-8') as file:
|
||||||
|
_SURNAME_LIST = [ line.strip() for line in file if line and not line.startswith("#") ]
|
||||||
|
|
||||||
|
_REQUIRED_KEYS = { "syllable", "consonants", "vowels", "length" }
|
||||||
|
# Define phoneme structure for built-in fantasy name generators.
|
||||||
|
_FANTASY_NAME_STRUCTURES = {
|
||||||
|
"harsh": {
|
||||||
|
"syllable": "CV(C)",
|
||||||
|
"consonants": [ "k", "k", "k", "z", "zh", "g", "v", "t", "th", "w", "n", "d", "d", ],
|
||||||
|
"start": ["dh", "kh", "kh", "kh", "vh", ],
|
||||||
|
"end": ["n", "x", ],
|
||||||
|
"vowels": [ "o", "o", "o", "a", "y", "u", "u", "u", "ä", "ö", "e", "i", "i", ],
|
||||||
|
"length": (1,3),
|
||||||
|
},
|
||||||
|
"fluid": {
|
||||||
|
"syllable": "V(C)",
|
||||||
|
"consonants": [ 'r','r','l','l','l','l','s','s','s','sh','m','n','n','f','v','w','th' ],
|
||||||
|
"start": [],
|
||||||
|
"end": [],
|
||||||
|
"vowels": [ "a","a","a","a","a","e","i","i","i","y","u","o", ],
|
||||||
|
"length": (3,5),
|
||||||
|
},
|
||||||
|
"alien": {
|
||||||
|
"syllable": "C(C(V))(')(C)",
|
||||||
|
"consonants": [ 'q','q','x','z','v','w','k','h','b' ],
|
||||||
|
"start": ['x',],
|
||||||
|
"end": [],
|
||||||
|
"vowels": [ 'y','w','o','y' ],
|
||||||
|
"length": (1,5),
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_RE_DOUBLES = re.compile(r'(\w)\1{2,}')
|
||||||
|
|
||||||
|
# Load in optional settings
|
||||||
|
|
||||||
|
custom_first_names = settings.NAMEGEN_FIRST_NAMES if hasattr(settings, "NAMEGEN_FIRST_NAMES") else []
|
||||||
|
custom_last_names = settings.NAMEGEN_LAST_NAMES if hasattr(settings, "NAMEGEN_LAST_NAMES") else []
|
||||||
|
|
||||||
|
if hasattr(settings, "NAMEGEN_FANTASY_RULES"):
|
||||||
|
_FANTASY_NAME_STRUCTURES |= settings.NAMEGEN_FANTASY_RULES
|
||||||
|
|
||||||
|
if hasattr(settings, "NAMEGEN_REPLACE_LISTS") and settings.NAMEGEN_REPLACE_LISTS:
|
||||||
|
_FIRSTNAME_LIST = custom_first_names or _FIRSTNAME_LIST
|
||||||
|
_SURNAME_LIST = custom_last_names or _SURNAME_LIST
|
||||||
|
|
||||||
|
else:
|
||||||
|
_FIRSTNAME_LIST += custom_first_names
|
||||||
|
_SURNAME_LIST += custom_last_names
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def fantasy_name(num=1, style="harsh", return_list=False):
|
||||||
|
"""
|
||||||
|
Generate made-up names in one of a number of "styles".
|
||||||
|
|
||||||
|
Keyword args:
|
||||||
|
num (int) - How many names to return.
|
||||||
|
style (string) - The "style" of name. This references an existing algorithm.
|
||||||
|
return_list (bool) - Whether to always return a list. `False` by default,
|
||||||
|
which returns a string if there is only one value and a list if more.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _validate(style_name):
|
||||||
|
if style_name not in _FANTASY_NAME_STRUCTURES:
|
||||||
|
raise ValueError(f"Invalid style name: '{style_name}'. Available style names: {' '.join(_FANTASY_NAME_STRUCTURES.keys())}")
|
||||||
|
style_dict = _FANTASY_NAME_STRUCTURES[style_name]
|
||||||
|
|
||||||
|
if type(style_dict) is not dict:
|
||||||
|
raise ValueError(f"Style {style_name} must be a dictionary.")
|
||||||
|
|
||||||
|
keys = set(style_dict.keys())
|
||||||
|
missing_keys = _REQUIRED_KEYS - keys
|
||||||
|
if len(missing_keys):
|
||||||
|
raise KeyError(f"Style dictionary {style_name} is missing required keys: {' '.join(missing_keys)}")
|
||||||
|
|
||||||
|
if not (type(style_dict['consonants']) is list and type(style_dict['vowels']) is list):
|
||||||
|
raise TypeError(f"'consonants' and 'vowels' for style {style_name} must be lists.")
|
||||||
|
|
||||||
|
if not (is_iter(style_dict['length']) and len(style_dict['length']) == 2):
|
||||||
|
raise ValueError(f"'length' key for {style_name} must have a minimum and maximum number of syllables.")
|
||||||
|
|
||||||
|
return style_dict
|
||||||
|
|
||||||
|
# validate num first
|
||||||
|
num = int(num)
|
||||||
|
if num < 1:
|
||||||
|
raise ValueError("Number of names to generate must be positive.")
|
||||||
|
|
||||||
|
style_dict = _validate(style)
|
||||||
|
|
||||||
|
syllable = []
|
||||||
|
weight = 8
|
||||||
|
# parse out the syllable structure with weights
|
||||||
|
for key in style_dict["syllable"]:
|
||||||
|
# parentheses mean optional - allow nested parens
|
||||||
|
if key == "(":
|
||||||
|
weight = weight/2
|
||||||
|
elif key == ")":
|
||||||
|
weight = weight*2
|
||||||
|
else:
|
||||||
|
if key == "C":
|
||||||
|
sound_type = "consonants"
|
||||||
|
elif key == "V":
|
||||||
|
sound_type = "vowels"
|
||||||
|
else:
|
||||||
|
sound_type = key
|
||||||
|
# append the sound type and weight
|
||||||
|
syllable.append( (sound_type, int(weight)) )
|
||||||
|
|
||||||
|
name_list = []
|
||||||
|
|
||||||
|
# time to generate a name!
|
||||||
|
for n in range(num):
|
||||||
|
# build a list of syllables
|
||||||
|
length = random.randint(*style_dict['length'])
|
||||||
|
name = ""
|
||||||
|
for i in range(length):
|
||||||
|
# build the syllable itself
|
||||||
|
syll = ""
|
||||||
|
for sound, weight in syllable:
|
||||||
|
# random chance to skip this key; lower weights mean less likely
|
||||||
|
if random.randint(0,8) > weight:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if sound not in style_dict:
|
||||||
|
# extra character, like apostrophes
|
||||||
|
syll += sound
|
||||||
|
continue
|
||||||
|
|
||||||
|
# get a random sound from the sound list
|
||||||
|
choices = list(style_dict[sound])
|
||||||
|
|
||||||
|
if sound == "consonants":
|
||||||
|
# if it's a starting consonant, add starting-sounds to the options
|
||||||
|
if not len(syll):
|
||||||
|
choices += style_dict.get('start',[])
|
||||||
|
# if it's an ending consonant, add ending-sounds to the options
|
||||||
|
elif i+1 == length:
|
||||||
|
choices += style_dict.get('end',[])
|
||||||
|
|
||||||
|
syll += random.choice(choices)
|
||||||
|
|
||||||
|
name += syll
|
||||||
|
|
||||||
|
# condense repeating letters down to a maximum of 2
|
||||||
|
name = _RE_DOUBLES.sub(lambda m: m.group(1)*2, name)
|
||||||
|
# capitalize the first letter
|
||||||
|
name = name[0].upper() + name[1:] if len(name) > 1 else name.upper()
|
||||||
|
name_list.append(name)
|
||||||
|
|
||||||
|
if len(name_list) == 1 and not return_list:
|
||||||
|
return name_list[0]
|
||||||
|
return name_list
|
||||||
|
|
||||||
|
def first_name(num=1, gender=None, return_list=False, ):
|
||||||
|
"""
|
||||||
|
Generate first names, also known as personal names.
|
||||||
|
|
||||||
|
Keyword args:
|
||||||
|
num (int) - How many names to return.
|
||||||
|
gender (str) - Restrict names by gender association. `None` by default, which selects from
|
||||||
|
all possible names. Set to "m" for masculine, "f" for feminine, "mf" for androgynous
|
||||||
|
return_list (bool) - Whether to always return a list. `False` by default,
|
||||||
|
which returns a string if there is only one value and a list if more.
|
||||||
|
"""
|
||||||
|
# validate num first
|
||||||
|
num = int(num)
|
||||||
|
if num < 1:
|
||||||
|
raise ValueError("Number of names to generate must be positive.")
|
||||||
|
|
||||||
|
if gender:
|
||||||
|
# filter the options by gender
|
||||||
|
name_options = [ name_data[0] for name_data in _FIRSTNAME_LIST if all([gender_key in gender for gender_key in name_data[1]])]
|
||||||
|
if not len(name_options):
|
||||||
|
raise ValueError(f"Invalid gender '{gender}'.")
|
||||||
|
else:
|
||||||
|
name_options = [ name_data[0] for name_data in _FIRSTNAME_LIST ]
|
||||||
|
|
||||||
|
# take a random selection of `num` names, without repeats
|
||||||
|
results = random.sample(name_options,num)
|
||||||
|
|
||||||
|
if len(results) == 1 and not return_list:
|
||||||
|
# return single value as a string
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def last_name(num=1, return_list=False):
|
||||||
|
"""
|
||||||
|
Generate family names, also known as surnames or last names.
|
||||||
|
|
||||||
|
Keyword args:
|
||||||
|
num (int) - How many names to return.
|
||||||
|
return_list (bool) - Whether to always return a list. `False` by default,
|
||||||
|
which returns a string if there is only one value and a list if more.
|
||||||
|
"""
|
||||||
|
# validate num first
|
||||||
|
num = int(num)
|
||||||
|
if num < 1:
|
||||||
|
raise ValueError("Number of names to generate must be positive.")
|
||||||
|
|
||||||
|
# take a random selection of `num` names, without repeats
|
||||||
|
results = random.sample(_SURNAME_LIST,num)
|
||||||
|
|
||||||
|
if len(results) == 1 and not return_list:
|
||||||
|
# return single value as a string
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def full_name(num=1, parts=2, gender=None, return_list=False, surname_first=False):
|
||||||
|
"""
|
||||||
|
Generate complete names with a personal name, family name, and optionally middle names.
|
||||||
|
|
||||||
|
Keyword args:
|
||||||
|
num (int) - How many names to return.
|
||||||
|
parts (int) - How many parts the name should have. By default two: first and last.
|
||||||
|
gender (str) - Restrict names by gender association. `None` by default, which selects from
|
||||||
|
all possible names. Set to "m" for masculine, "f" for feminine, "mf" for androgynous
|
||||||
|
return_list (bool) - Whether to always return a list. `False` by default,
|
||||||
|
which returns a string if there is only one value and a list if more.
|
||||||
|
surname_first (bool) - Default `False`. Set to `True` if you want the family name to be
|
||||||
|
placed at the beginning of the name instead of the end.
|
||||||
|
"""
|
||||||
|
# validate num first
|
||||||
|
num = int(num)
|
||||||
|
if num < 1:
|
||||||
|
raise ValueError("Number of names to generate must be positive.")
|
||||||
|
# validate parts next
|
||||||
|
parts = int(parts)
|
||||||
|
if parts < 2:
|
||||||
|
raise ValueError("Number of name parts to generate must be at least 2.")
|
||||||
|
|
||||||
|
name_lists = []
|
||||||
|
|
||||||
|
middle = parts-2
|
||||||
|
if middle:
|
||||||
|
# calculate "middle" names.
|
||||||
|
# we want them to be an intelligent mix of personal names and family names
|
||||||
|
# first, split the total number of middle-name parts into "personal" and "family" at a random point
|
||||||
|
total_mids = middle*num
|
||||||
|
personals = random.randint(1,total_mids)
|
||||||
|
familys = total_mids - personals
|
||||||
|
# then get the names for each
|
||||||
|
personal_mids = first_name(num=personals, gender=gender, return_list=True)
|
||||||
|
family_mids = last_name(num=familys, return_list=True) if familys else []
|
||||||
|
# splice them together according to surname_first....
|
||||||
|
middle_names = family_mids+personal_mids if surname_first else personal_mids+family_mids
|
||||||
|
# ...and then split into `num`-length lists to be used for the final names
|
||||||
|
name_lists = [ middle_names[num*i:num*(i+1)] for i in range(0,middle) ]
|
||||||
|
|
||||||
|
# get personal and family names
|
||||||
|
personal_names = first_name(num=num, gender=gender, return_list=True)
|
||||||
|
last_names = last_name(num=num, return_list=True)
|
||||||
|
|
||||||
|
# attach personal/family names to the list of name lists, according to surname_first
|
||||||
|
if surname_first:
|
||||||
|
name_lists = [last_names] + name_lists + [personal_names]
|
||||||
|
else:
|
||||||
|
name_lists = [personal_names] + name_lists + [last_names]
|
||||||
|
|
||||||
|
# lastly, zip them all up and join them together
|
||||||
|
names = list(zip(*name_lists))
|
||||||
|
names = [ " ".join(name) for name in names ]
|
||||||
|
|
||||||
|
if len(names) == 1 and not return_list:
|
||||||
|
# return single value as a string
|
||||||
|
return names[0]
|
||||||
|
|
||||||
|
return names
|
||||||
158
evennia/contrib/utils/name_generator/tests.py
Normal file
158
evennia/contrib/utils/name_generator/tests.py
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
|
||||||
|
"""
|
||||||
|
Tests for the Random Name Generator
|
||||||
|
"""
|
||||||
|
|
||||||
|
from evennia.utils.test_resources import BaseEvenniaTest
|
||||||
|
from evennia.contrib.utils.name_generator import namegen
|
||||||
|
|
||||||
|
_INVALID_STYLES = {
|
||||||
|
"missing_keys": {
|
||||||
|
"consonants": ['c','d'],
|
||||||
|
"length": (1,2),
|
||||||
|
},
|
||||||
|
"invalid_vowels": {
|
||||||
|
"syllable": "CVC",
|
||||||
|
"consonants": ['c','d'],
|
||||||
|
"vowels": "aeiou",
|
||||||
|
"length": (1,2),
|
||||||
|
},
|
||||||
|
"invalid_length": {
|
||||||
|
"syllable": "CVC",
|
||||||
|
"consonants": ['c','d'],
|
||||||
|
"vowels": ['a','e'],
|
||||||
|
"length": 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
namegen._FANTASY_NAME_STRUCTURES |= _INVALID_STYLES
|
||||||
|
|
||||||
|
class TestNameGenerator(BaseEvenniaTest):
|
||||||
|
def test_fantasy_name(self):
|
||||||
|
"""
|
||||||
|
Verify output types and lengths.
|
||||||
|
|
||||||
|
fantasy_name() - str
|
||||||
|
fantasy_name(style="fluid") - str
|
||||||
|
fantasy_name(num=3) - list of length 3
|
||||||
|
fantasy_name(return_list=True) - list of length 1
|
||||||
|
|
||||||
|
raises KeyError on missing style or ValueError on num
|
||||||
|
"""
|
||||||
|
single_name = namegen.fantasy_name()
|
||||||
|
self.assertEqual(type(single_name), str)
|
||||||
|
|
||||||
|
fluid_name = namegen.fantasy_name(style="fluid")
|
||||||
|
self.assertEqual(type(fluid_name), str)
|
||||||
|
|
||||||
|
three_names = namegen.fantasy_name(num=3)
|
||||||
|
self.assertEqual(type(three_names), list)
|
||||||
|
self.assertEqual(len(three_names), 3)
|
||||||
|
|
||||||
|
single_list = namegen.fantasy_name(return_list=True)
|
||||||
|
self.assertEqual(type(single_list), list)
|
||||||
|
self.assertEqual(len(single_list), 1)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
namegen.fantasy_name(num=-1)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
namegen.fantasy_name(style="dummy")
|
||||||
|
|
||||||
|
def test_structure_validation(self):
|
||||||
|
"""
|
||||||
|
Verify that validation raises the correct errors for invalid inputs.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
namegen.fantasy_name(style="missing_keys")
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
namegen.fantasy_name(style="invalid_vowels")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
namegen.fantasy_name(style="invalid_length")
|
||||||
|
|
||||||
|
def test_first_name(self):
|
||||||
|
"""
|
||||||
|
Verify output types and lengths.
|
||||||
|
|
||||||
|
first_name() - str
|
||||||
|
first_name(num=3) - list of length 3
|
||||||
|
first_name(gender='f') - str
|
||||||
|
first_name(return_list=True) - list of length 1
|
||||||
|
"""
|
||||||
|
single_name = namegen.first_name()
|
||||||
|
self.assertEqual(type(single_name), str)
|
||||||
|
|
||||||
|
three_names = namegen.first_name(num=3)
|
||||||
|
self.assertEqual(type(three_names), list)
|
||||||
|
self.assertEqual(len(three_names), 3)
|
||||||
|
|
||||||
|
gendered_name = namegen.first_name(gender='f')
|
||||||
|
self.assertEqual(type(gendered_name), str)
|
||||||
|
|
||||||
|
single_list = namegen.first_name(return_list=True)
|
||||||
|
self.assertEqual(type(single_list), list)
|
||||||
|
self.assertEqual(len(single_list), 1)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
namegen.first_name(gender='x')
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
namegen.first_name(num=-1)
|
||||||
|
|
||||||
|
def test_last_name(self):
|
||||||
|
"""
|
||||||
|
Verify output types and lengths.
|
||||||
|
|
||||||
|
last_name() - str
|
||||||
|
last_name(num=3) - list of length 3
|
||||||
|
last_name(return_list=True) - list of length 1
|
||||||
|
"""
|
||||||
|
single_name = namegen.last_name()
|
||||||
|
self.assertEqual(type(single_name), str)
|
||||||
|
|
||||||
|
three_names = namegen.last_name(num=3)
|
||||||
|
self.assertEqual(type(three_names), list)
|
||||||
|
self.assertEqual(len(three_names), 3)
|
||||||
|
|
||||||
|
single_list = namegen.last_name(return_list=True)
|
||||||
|
self.assertEqual(type(single_list), list)
|
||||||
|
self.assertEqual(len(single_list), 1)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
namegen.last_name(num=-1)
|
||||||
|
|
||||||
|
def test_full_name(self):
|
||||||
|
"""
|
||||||
|
Verify output types and lengths.
|
||||||
|
|
||||||
|
full_name() - str
|
||||||
|
full_name(num=3) - list of length 3
|
||||||
|
full_name(gender='f') - str
|
||||||
|
full_name(return_list=True) - list of length 1
|
||||||
|
"""
|
||||||
|
single_name = namegen.full_name()
|
||||||
|
self.assertEqual(type(single_name), str)
|
||||||
|
|
||||||
|
three_names = namegen.full_name(num=3)
|
||||||
|
self.assertEqual(type(three_names), list)
|
||||||
|
self.assertEqual(len(three_names), 3)
|
||||||
|
|
||||||
|
gendered_name = namegen.full_name(gender='f')
|
||||||
|
self.assertEqual(type(gendered_name), str)
|
||||||
|
|
||||||
|
single_list = namegen.full_name(return_list=True)
|
||||||
|
self.assertEqual(type(single_list), list)
|
||||||
|
self.assertEqual(len(single_list), 1)
|
||||||
|
|
||||||
|
parts_name = namegen.full_name(parts=4)
|
||||||
|
# a name made of 4 parts must have at least 3 spaces, but may have more
|
||||||
|
parts = parts_name.split(" ")
|
||||||
|
self.assertGreaterEqual(len(parts), 3)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
namegen.full_name(parts=1)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
namegen.full_name(num=-1)
|
||||||
|
|
@ -324,7 +324,7 @@ class ObjectDBManager(TypedObjectManager):
|
||||||
search_candidates = (
|
search_candidates = (
|
||||||
self.filter(
|
self.filter(
|
||||||
type_restriction
|
type_restriction
|
||||||
& (Q(db_key__istartswith=ostring) | Q(db_tags__db_key__istartswith=ostring))
|
& (Q(db_key__icontains=ostring) | Q(db_tags__db_key__icontains=ostring))
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
.order_by("id")
|
.order_by("id")
|
||||||
|
|
|
||||||
|
|
@ -815,6 +815,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
Passed on to announce_move_to and announce_move_from hooks.
|
Passed on to announce_move_to and announce_move_from hooks.
|
||||||
|
Exits will set the "exit_obj" kwarg to themselves.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
result (bool): True/False depending on if there were problems with the move.
|
result (bool): True/False depending on if there were problems with the move.
|
||||||
|
|
@ -2973,8 +2974,8 @@ class DefaultExit(DefaultObject):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# an exit should have a destination (this is replaced at creation time)
|
# an exit should have a destination - try to make sure it does
|
||||||
if self.location:
|
if self.location and not self.destination:
|
||||||
self.destination = self.location
|
self.destination = self.location
|
||||||
|
|
||||||
def at_cmdset_get(self, **kwargs):
|
def at_cmdset_get(self, **kwargs):
|
||||||
|
|
@ -3016,7 +3017,7 @@ class DefaultExit(DefaultObject):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
source_location = traversing_object.location
|
source_location = traversing_object.location
|
||||||
if traversing_object.move_to(target_location, move_type="traverse"):
|
if traversing_object.move_to(target_location, move_type="traverse", exit_obj=self):
|
||||||
self.at_post_traverse(traversing_object, source_location)
|
self.at_post_traverse(traversing_object, source_location)
|
||||||
else:
|
else:
|
||||||
if self.db.err_traverse:
|
if self.db.err_traverse:
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@
|
||||||
Unit tests for typeclass base system
|
Unit tests for typeclass base system
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
|
|
||||||
from evennia.typeclasses import attributes
|
from evennia.typeclasses import attributes
|
||||||
|
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
|
||||||
from mock import patch
|
from mock import patch
|
||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
|
@ -13,6 +14,10 @@ from parameterized import parameterized
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class DictSubclass(dict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestAttributes(BaseEvenniaTest):
|
class TestAttributes(BaseEvenniaTest):
|
||||||
def test_attrhandler(self):
|
def test_attrhandler(self):
|
||||||
key = "testattr"
|
key = "testattr"
|
||||||
|
|
@ -22,6 +27,25 @@ class TestAttributes(BaseEvenniaTest):
|
||||||
self.obj1.db.testattr = value
|
self.obj1.db.testattr = value
|
||||||
self.assertEqual(self.obj1.db.testattr, value)
|
self.assertEqual(self.obj1.db.testattr, value)
|
||||||
|
|
||||||
|
# "plain" subclasses
|
||||||
|
value = DictSubclass({"fo": "foo", "bar": "bar"})
|
||||||
|
self.obj1.db.testattr = value
|
||||||
|
self.assertEqual(self.obj1.db.testattr, value)
|
||||||
|
|
||||||
|
self.obj1.db.testattr["fo"] = "foo2"
|
||||||
|
value.update({"fo": "foo2"})
|
||||||
|
self.assertEqual(self.obj1.db.testattr, value)
|
||||||
|
self.assertEqual(self.obj1.attributes.get("testattr"), value)
|
||||||
|
|
||||||
|
# nested subclasses
|
||||||
|
value = DictSubclass({"nested": True, "deep": DictSubclass({"fo": "foo", "bar": "bar"})})
|
||||||
|
self.obj1.db.testattr = value
|
||||||
|
|
||||||
|
self.obj1.db.testattr["deep"]["fo"] = "nemo"
|
||||||
|
value["deep"].update({"fo": "nemo"})
|
||||||
|
self.assertEqual(self.obj1.db.testattr, value)
|
||||||
|
self.assertEqual(self.obj1.attributes.get("testattr"), value)
|
||||||
|
|
||||||
@override_settings(TYPECLASS_AGGRESSIVE_CACHE=False)
|
@override_settings(TYPECLASS_AGGRESSIVE_CACHE=False)
|
||||||
@patch("evennia.typeclasses.attributes._TYPECLASS_AGGRESSIVE_CACHE", False)
|
@patch("evennia.typeclasses.attributes._TYPECLASS_AGGRESSIVE_CACHE", False)
|
||||||
def test_attrhandler_nocache(self):
|
def test_attrhandler_nocache(self):
|
||||||
|
|
@ -35,6 +59,27 @@ class TestAttributes(BaseEvenniaTest):
|
||||||
self.assertEqual(self.obj1.db.testattr, value)
|
self.assertEqual(self.obj1.db.testattr, value)
|
||||||
self.assertFalse(self.obj1.attributes.backend._cache)
|
self.assertFalse(self.obj1.attributes.backend._cache)
|
||||||
|
|
||||||
|
# "plain" subclasses
|
||||||
|
value = DictSubclass({"fo": "foo", "bar": "bar"})
|
||||||
|
self.obj1.db.testattr = value
|
||||||
|
self.assertEqual(self.obj1.db.testattr, value)
|
||||||
|
|
||||||
|
self.obj1.db.testattr["fo"] = "foo2"
|
||||||
|
value.update({"fo": "foo2"})
|
||||||
|
self.assertEqual(self.obj1.db.testattr, value)
|
||||||
|
self.assertEqual(self.obj1.attributes.get("testattr"), value)
|
||||||
|
self.assertFalse(self.obj1.attributes.backend._cache)
|
||||||
|
|
||||||
|
# nested subclasses
|
||||||
|
value = DictSubclass({"nested": True, "deep": DictSubclass({"fo": "foo", "bar": "bar"})})
|
||||||
|
self.obj1.db.testattr = value
|
||||||
|
|
||||||
|
self.obj1.db.testattr["deep"]["fo"] = "nemo"
|
||||||
|
value["deep"].update({"fo": "nemo"})
|
||||||
|
self.assertEqual(self.obj1.db.testattr, value)
|
||||||
|
self.assertEqual(self.obj1.attributes.get("testattr"), value)
|
||||||
|
self.assertFalse(self.obj1.attributes.backend._cache)
|
||||||
|
|
||||||
def test_weird_text_save(self):
|
def test_weird_text_save(self):
|
||||||
"test 'weird' text type (different in py2 vs py3)"
|
"test 'weird' text type (different in py2 vs py3)"
|
||||||
from django.utils.safestring import SafeText
|
from django.utils.safestring import SafeText
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,9 @@ class _SaverMutable(object):
|
||||||
def __or__(self, other):
|
def __or__(self, other):
|
||||||
return self._data | other
|
return self._data | other
|
||||||
|
|
||||||
|
def __ror__(self, other):
|
||||||
|
return self._data | other
|
||||||
|
|
||||||
@_save
|
@_save
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
self._data.__setitem__(key, self._convert_mutables(value))
|
self._data.__setitem__(key, self._convert_mutables(value))
|
||||||
|
|
@ -263,7 +266,7 @@ class _SaverList(_SaverMutable, MutableSequence):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._data = list()
|
self._data = kwargs.pop("_class", list)()
|
||||||
|
|
||||||
@_save
|
@_save
|
||||||
def __iadd__(self, otherlist):
|
def __iadd__(self, otherlist):
|
||||||
|
|
@ -307,7 +310,7 @@ class _SaverDict(_SaverMutable, MutableMapping):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._data = dict()
|
self._data = kwargs.pop("_class", dict)()
|
||||||
|
|
||||||
def has_key(self, key):
|
def has_key(self, key):
|
||||||
return key in self._data
|
return key in self._data
|
||||||
|
|
@ -645,11 +648,20 @@ def to_pickle(data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if hasattr(item, "__iter__"):
|
if hasattr(item, "__iter__"):
|
||||||
# we try to conserve the iterable class, if not convert to list
|
|
||||||
try:
|
try:
|
||||||
return item.__class__([process_item(val) for val in item])
|
# we try to conserve the iterable class, if not convert to dict
|
||||||
except (AttributeError, TypeError):
|
try:
|
||||||
return [process_item(val) for val in item]
|
return item.__class__(
|
||||||
|
(process_item(key), process_item(val)) for key, val in item.items()
|
||||||
|
)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
return {process_item(key): process_item(val) for key, val in item.items()}
|
||||||
|
except Exception:
|
||||||
|
# we try to conserve the iterable class, if not convert to list
|
||||||
|
try:
|
||||||
|
return item.__class__([process_item(val) for val in item])
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
return [process_item(val) for val in item]
|
||||||
elif hasattr(item, "sessid") and hasattr(item, "conn_time"):
|
elif hasattr(item, "sessid") and hasattr(item, "conn_time"):
|
||||||
return pack_session(item)
|
return pack_session(item)
|
||||||
try:
|
try:
|
||||||
|
|
@ -714,11 +726,20 @@ def from_pickle(data, db_obj=None):
|
||||||
return deque(process_item(val) for val in item)
|
return deque(process_item(val) for val in item)
|
||||||
elif hasattr(item, "__iter__"):
|
elif hasattr(item, "__iter__"):
|
||||||
try:
|
try:
|
||||||
# we try to conserve the iterable class if
|
# we try to conserve the iterable class, if not convert to dict
|
||||||
# it accepts an iterator
|
try:
|
||||||
return item.__class__(process_item(val) for val in item)
|
return item.__class__(
|
||||||
except (AttributeError, TypeError):
|
(process_item(key), process_item(val)) for key, val in item.items()
|
||||||
return [process_item(val) for val in item]
|
)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
return {process_item(key): process_item(val) for key, val in item.items()}
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
# we try to conserve the iterable class if
|
||||||
|
# it accepts an iterator
|
||||||
|
return item.__class__(process_item(val) for val in item)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
return [process_item(val) for val in item]
|
||||||
|
|
||||||
if hasattr(item, "__deserialize_dbobjs__"):
|
if hasattr(item, "__deserialize_dbobjs__"):
|
||||||
# this allows the object to custom-deserialize any embedded dbobjs
|
# this allows the object to custom-deserialize any embedded dbobjs
|
||||||
|
|
@ -780,13 +801,30 @@ def from_pickle(data, db_obj=None):
|
||||||
return dat
|
return dat
|
||||||
elif hasattr(item, "__iter__"):
|
elif hasattr(item, "__iter__"):
|
||||||
try:
|
try:
|
||||||
# we try to conserve the iterable class if it
|
# we try to conserve the iterable class, if not convert to dict
|
||||||
# accepts an iterator
|
try:
|
||||||
return item.__class__(process_tree(val, parent) for val in item)
|
dat = _SaverDict(_parent=parent, _class=item.__class__)
|
||||||
except (AttributeError, TypeError):
|
dat._data.update(
|
||||||
dat = _SaverList(_parent=parent)
|
(process_item(key), process_tree(val, dat)) for key, val in item.items()
|
||||||
dat._data.extend(process_tree(val, dat) for val in item)
|
)
|
||||||
return dat
|
return dat
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
dat = _SaverDict(_parent=parent)
|
||||||
|
dat._data.update(
|
||||||
|
(process_item(key), process_tree(val, dat)) for key, val in item.items()
|
||||||
|
)
|
||||||
|
return dat
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
# we try to conserve the iterable class if it
|
||||||
|
# accepts an iterator
|
||||||
|
dat = _SaverList(_parent=parent, _class=item.__class__)
|
||||||
|
dat._data.extend(process_tree(val, dat) for val in item)
|
||||||
|
return dat
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
dat = _SaverList(_parent=parent)
|
||||||
|
dat._data.extend(process_tree(val, dat) for val in item)
|
||||||
|
return dat
|
||||||
|
|
||||||
if hasattr(item, "__deserialize_dbobjs__"):
|
if hasattr(item, "__deserialize_dbobjs__"):
|
||||||
try:
|
try:
|
||||||
|
|
@ -800,7 +838,9 @@ def from_pickle(data, db_obj=None):
|
||||||
# convert lists, dicts and sets to their Saved* counterparts. It
|
# convert lists, dicts and sets to their Saved* counterparts. It
|
||||||
# is only relevant if the "root" is an iterable of the right type.
|
# is only relevant if the "root" is an iterable of the right type.
|
||||||
dtype = type(data)
|
dtype = type(data)
|
||||||
if dtype == list:
|
if dtype in (str, int, float, bool, bytes, SafeString, tuple):
|
||||||
|
return process_item(data)
|
||||||
|
elif dtype == list:
|
||||||
dat = _SaverList(_db_obj=db_obj)
|
dat = _SaverList(_db_obj=db_obj)
|
||||||
dat._data.extend(process_tree(val, dat) for val in data)
|
dat._data.extend(process_tree(val, dat) for val in data)
|
||||||
return dat
|
return dat
|
||||||
|
|
@ -830,6 +870,34 @@ def from_pickle(data, db_obj=None):
|
||||||
dat = _SaverDeque(_db_obj=db_obj)
|
dat = _SaverDeque(_db_obj=db_obj)
|
||||||
dat._data.extend(process_item(val) for val in data)
|
dat._data.extend(process_item(val) for val in data)
|
||||||
return dat
|
return dat
|
||||||
|
elif hasattr(data, "__iter__"):
|
||||||
|
try:
|
||||||
|
# we try to conserve the iterable class, if not convert to dict
|
||||||
|
try:
|
||||||
|
dat = _SaverDict(_db_obj=db_obj, _class=data.__class__)
|
||||||
|
dat._data.update(
|
||||||
|
(process_item(key), process_tree(val, dat)) for key, val in data.items()
|
||||||
|
)
|
||||||
|
return dat
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
dat = _SaverDict(_db_obj=db_obj)
|
||||||
|
dat._data.update(
|
||||||
|
(process_item(key), process_tree(val, dat)) for key, val in data.items()
|
||||||
|
)
|
||||||
|
return dat
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
# we try to conserve the iterable class if it
|
||||||
|
# accepts an iterator
|
||||||
|
dat = _SaverList(_db_obj=db_obj, _class=data.__class__)
|
||||||
|
dat._data.extend(process_tree(val, dat) for val in data)
|
||||||
|
return dat
|
||||||
|
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
dat = _SaverList(_db_obj=db_obj)
|
||||||
|
dat._data.extend(process_tree(val, dat) for val in data)
|
||||||
|
return dat
|
||||||
|
|
||||||
return process_item(data)
|
return process_item(data)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue