From ba090e0f4469a0a2b033e60c443a6f57a2625d23 Mon Sep 17 00:00:00 2001 From: serprinss <62165076+serprinss@users.noreply.github.com> Date: Sat, 28 Mar 2020 13:34:10 +1100 Subject: [PATCH 01/32] documented return value for create_account --- evennia/utils/create.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evennia/utils/create.py b/evennia/utils/create.py index 5076f60ee3..dbf19f0332 100644 --- a/evennia/utils/create.py +++ b/evennia/utils/create.py @@ -501,6 +501,8 @@ def create_account( report_to (Object): An object with a msg() method to report errors to. If not given, errors will be logged. + Returns: + (object) The acount object of the new account. Raises: ValueError: If `key` already exists in database. From 7a428b703b5fb24e40e3104f27f440007c2359d3 Mon Sep 17 00:00:00 2001 From: David Estrada Date: Thu, 9 Apr 2020 13:47:17 -0700 Subject: [PATCH 02/32] Manager.py is returning a couple of querysets instead of lists. Fix for bug #2088 --- evennia/objects/manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index 1217d0a91d..f66c1fe253 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -178,11 +178,11 @@ class ObjectDBManager(TypedObjectManager): # This doesn't work if attribute_value is an object. Workaround below if isinstance(attribute_value, (str, int, float, bool)): - return self.filter( + return list(self.filter( cand_restriction & type_restriction & Q(db_attributes__db_key=attribute_name, db_attributes__db_value=attribute_value) - ).order_by("id") + ).order_by("id")) else: # We must loop for safety since the referenced lookup gives deepcopy error if attribute value is an object. global _ATTR @@ -278,7 +278,7 @@ class ObjectDBManager(TypedObjectManager): exclude_restriction = ( Q(pk__in=[_GA(obj, "id") for obj in make_iter(excludeobj)]) if excludeobj else Q() ) - return self.filter(db_location=location).exclude(exclude_restriction).order_by("id") + return list(self.filter(db_location=location).exclude(exclude_restriction).order_by("id")) def get_objs_with_key_or_alias(self, ostring, exact=True, candidates=None, typeclasses=None): """ @@ -309,7 +309,7 @@ class ObjectDBManager(TypedObjectManager): type_restriction = typeclasses and Q(db_typeclass_path__in=make_iter(typeclasses)) or Q() if exact: # exact match - do direct search - return ( + return list( ( self.filter( cand_restriction From 109627090e86e622db0fd8ec377cac12dd0b4aae Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Mon, 13 Apr 2020 14:16:22 -0700 Subject: [PATCH 03/32] Made initial_setup.py replaceable in settings.py --- CHANGELOG.md | 2 +- evennia/server/server.py | 3 ++- evennia/settings_default.py | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c72d7deda..f6bb5ad088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - Added `content_types` indexing to DefaultObject's ContentsHandler. (volund) - Made most of the networking classes such as Protocols and the SessionHandlers replaceable via `settings.py` for modding enthusiasts. (volund) - +- The `initial_setup.py` file can now be substituted in `settings.py` to customize initial game database state. (volund) ### Already in master - `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False diff --git a/evennia/server/server.py b/evennia/server/server.py index 2093b5b88c..5e65b7429b 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -22,6 +22,7 @@ import django django.setup() import evennia +import importlib evennia._init() @@ -31,7 +32,6 @@ from django.conf import settings from evennia.accounts.models import AccountDB from evennia.scripts.models import ScriptDB from evennia.server.models import ServerConfig -from evennia.server import initial_setup from evennia.utils.utils import get_evennia_version, mod_import, make_iter from evennia.utils import logger @@ -333,6 +333,7 @@ class Evennia(object): Once finished the last_initial_setup_step is set to -1. """ global INFO_DICT + initial_setup = importlib.import_module(settings.INITIAL_SETUP_MODULE) last_initial_setup_step = ServerConfig.objects.conf("last_initial_setup_step") if not last_initial_setup_step: # None is only returned if the config does not exist, diff --git a/evennia/settings_default.py b/evennia/settings_default.py index e9e7e98bb2..517562c675 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -332,6 +332,10 @@ CONNECTION_SCREEN_MODULE = "server.conf.connection_screens" # cause issues with menu-logins and autoconnects since the menu will not have # started when the autoconnects starts sending menu commands. DELAY_CMD_LOGINSTART = 0.3 +# A module that must exist - this holds the instructions Evennia will use to +# first prepare the database for use. Generally should not be changed. If this +# cannot be imported, bad things will happen. +INITIAL_SETUP_MODULE = "evennia.server.initial_setup" # An optional module that, if existing, must hold a function # named at_initial_setup(). This hook method can be used to customize # the server's initial setup sequence (the very first startup of the system). From 0d6988244b565903ddad71a74690ac89443d20ed Mon Sep 17 00:00:00 2001 From: Jaidyn Ann Date: Wed, 15 Apr 2020 21:12:42 -0500 Subject: [PATCH 04/32] comms i18n --- evennia/comms/channelhandler.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/evennia/comms/channelhandler.py b/evennia/comms/channelhandler.py index 98e28786a0..c99288c337 100644 --- a/evennia/comms/channelhandler.py +++ b/evennia/comms/channelhandler.py @@ -119,17 +119,17 @@ class ChannelCommand(command.Command): caller = caller if not hasattr(caller, "account") else caller.account unmuted = channel.unmute(caller) if unmuted: - self.msg("You start listening to %s." % channel) + self.msg(_("You start listening to %s.") % channel) return - self.msg("You were already listening to %s." % channel) + self.msg(_("You were already listening to %s.") % channel) return if msg == "off": caller = caller if not hasattr(caller, "account") else caller.account muted = channel.mute(caller) if muted: - self.msg("You stop listening to %s." % channel) + self.msg(_("You stop listening to %s.") % channel) return - self.msg("You were already not listening to %s." % channel) + self.msg(_("You were already not listening to %s.") % channel) return if self.history_start is not None: # Try to view history @@ -144,7 +144,7 @@ class ChannelCommand(command.Command): else: caller = caller if not hasattr(caller, "account") else caller.account if caller in channel.mutelist: - self.msg("You currently have %s muted." % channel) + self.msg(_("You currently have %s muted.") % channel) return channel.msg(msg, senders=self.caller, online=True) From 95fe5c060f88923d0909affa3ace7d5a5672811e Mon Sep 17 00:00:00 2001 From: trhr Date: Wed, 15 Apr 2020 21:21:16 -0500 Subject: [PATCH 05/32] i18n --- evennia/accounts/accounts.py | 46 ++++++++++++++++----------------- evennia/accounts/bots.py | 5 ++-- evennia/help/manager.py | 2 +- evennia/objects/admin.py | 4 +-- evennia/utils/validatorfuncs.py | 5 ++-- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 397ea287b6..24c551fdbb 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -275,11 +275,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): raise RuntimeError("Session not found") if self.get_puppet(session) == obj: # already puppeting this object - self.msg("You are already puppeting this object.") + self.msg(_("You are already puppeting this object.") return if not obj.access(self, "puppet"): # no access - self.msg(f"You don't have permission to puppet '{obj.key}'.") + self.msg(_(f"You don't have permission to puppet '{obj.key}'.") return if obj.account: # object already puppeted @@ -295,12 +295,12 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): else: txt1 = f"Taking over |c{obj.name}|n from another of your sessions." txt2 = f"|c{obj.name}|n|R is now acted from another of your sessions.|n" - self.msg(txt1, session=session) - self.msg(txt2, session=obj.sessions.all()) + self.msg(_(txt1), session=session) + self.msg(_(txt2), session=obj.sessions.all()) self.unpuppet_object(obj.sessions.get()) elif obj.account.is_connected: # controlled by another account - self.msg(f"|c{obj.key}|R is already puppeted by another Account.") + self.msg(_(f"|c{obj.key}|R is already puppeted by another Account.")) return # do the puppeting @@ -496,7 +496,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): # See if authentication is currently being throttled if ip and LOGIN_THROTTLE.check(ip): - errors.append("Too many login failures; please try again in a few minutes.") + errors.append(_("Too many login failures; please try again in a few minutes.")) # With throttle active, do not log continued hits-- it is a # waste of storage and can be abused to make your logs harder to @@ -508,8 +508,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if banned: # this is a banned IP or name! errors.append( - "|rYou have been banned and cannot continue from here." - "\nIf you feel this ban is in error, please email an admin.|x" + _("|rYou have been banned and cannot continue from here." + "\nIf you feel this ban is in error, please email an admin.|x") ) logger.log_sec(f"Authentication Denied (Banned): {username} (IP: {ip}).") LOGIN_THROTTLE.update(ip, "Too many sightings of banned artifact.") @@ -519,7 +519,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): account = authenticate(username=username, password=password) if not account: # User-facing message - errors.append("Username and/or password is incorrect.") + errors.append(_("Username and/or password is incorrect.")) # Log auth failures while throttle is inactive logger.log_sec(f"Authentication Failure: {username} (IP: {ip}).") @@ -688,7 +688,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): ip = kwargs.get("ip", "") if ip and CREATION_THROTTLE.check(ip): errors.append( - "You are creating too many accounts. Please log into an existing account." + _("You are creating too many accounts. Please log into an existing account.") ) return None, errors @@ -717,8 +717,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if banned: # this is a banned IP or name! string = ( - "|rYou have been banned and cannot continue from here." - "\nIf you feel this ban is in error, please email an admin.|x" + _("|rYou have been banned and cannot continue from here." + "\nIf you feel this ban is in error, please email an admin.|x") ) errors.append(string) return None, errors @@ -733,7 +733,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): except Exception as e: errors.append( - "There was an error creating the Account. If this problem persists, contact an admin." + _("There was an error creating the Account. If this problem persists, contact an admin.") ) logger.log_trace() return None, errors @@ -785,7 +785,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): # We are in the middle between logged in and -not, so we have # to handle tracebacks ourselves at this point. If we don't, # we won't see any errors at all. - errors.append("An error occurred. Please e-mail an admin if the problem persists.") + errors.append(_("An error occurred. Please e-mail an admin if the problem persists.")) logger.log_trace() # Update the throttle to indicate a new account was created from this IP @@ -1260,14 +1260,14 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): try: self.puppet_object(session, self.db._last_puppet) except RuntimeError: - self.msg("The Character does not exist.") + self.msg(_("The Character does not exist.")) return elif _MULTISESSION_MODE == 1: # in this mode all sessions connect to the same puppet. try: self.puppet_object(session, self.db._last_puppet) except RuntimeError: - self.msg("The Character does not exist.") + self.msg(_("The Character does not exist.")) return elif _MULTISESSION_MODE in (2, 3): # In this mode we by default end up at a character selection @@ -1305,7 +1305,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ reason = f" ({reason if reason else ''})" - self._send_to_connect_channel(f"|R{self.key} disconnected{reason}|n") + self._send_to_connect_channel(_(f"|R{self.key} disconnected{reason}|n")) def at_post_disconnect(self, **kwargs): """ @@ -1411,7 +1411,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if hasattr(target, "return_appearance"): return target.return_appearance(self) else: - return "{} has no in-game appearance.".format(target) + return _(f"{target} has no in-game appearance.") else: # list of targets - make list to disconnect from db characters = list(tar for tar in target if tar) if target else [] @@ -1454,7 +1454,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if is_su or len(characters) < charmax: if not characters: result.append( - "\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one." + _("\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one.") ) else: result.append("\n |w@charcreate [=description]|n - create new character") @@ -1534,7 +1534,7 @@ class DefaultGuest(DefaultAccount): # check if guests are enabled. if not settings.GUEST_ENABLED: - errors.append("Guest accounts are not enabled on this server.") + errors.append(_("Guest accounts are not enabled on this server.")) return None, errors try: @@ -1544,7 +1544,7 @@ class DefaultGuest(DefaultAccount): username = name break if not username: - errors.append("All guest accounts are in use. Please try again later.") + errors.append(_("All guest accounts are in use. Please try again later.")) if ip: LOGIN_THROTTLE.update(ip, "Too many requests for Guest access.") return None, errors @@ -1572,7 +1572,7 @@ class DefaultGuest(DefaultAccount): # We are in the middle between logged in and -not, so we have # to handle tracebacks ourselves at this point. If we don't, # we won't see any errors at all. - errors.append("An error occurred. Please e-mail an admin if the problem persists.") + errors.append(_("An error occurred. Please e-mail an admin if the problem persists.")) logger.log_trace() return None, errors @@ -1589,7 +1589,7 @@ class DefaultGuest(DefaultAccount): overriding the call (unused by default). """ - self._send_to_connect_channel(f"|G{self.key} connected|n") + self._send_to_connect_channel(_(f"|G{self.key} connected|n")) self.puppet_object(session, self.db._last_puppet) def at_server_shutdown(self): diff --git a/evennia/accounts/bots.py b/evennia/accounts/bots.py index 455e10b629..1c1fc7d300 100644 --- a/evennia/accounts/bots.py +++ b/evennia/accounts/bots.py @@ -10,6 +10,7 @@ from evennia.accounts.accounts import DefaultAccount from evennia.scripts.scripts import DefaultScript from evennia.utils import search from evennia.utils import utils +from django.utils.translation import gettext as _ _IDLE_TIMEOUT = settings.IDLE_TIMEOUT @@ -328,7 +329,7 @@ class IRCBot(Bot): chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower())) for obj in self._nicklist_callers: - obj.msg(f"Nicks at {chstr}:\n {nicklist}") + obj.msg(_(f"Nicks at {chstr}:\n {nicklist}") self._nicklist_callers = [] return @@ -337,7 +338,7 @@ class IRCBot(Bot): if hasattr(self, "_ping_callers") and self._ping_callers: chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" for obj in self._ping_callers: - obj.msg(f"IRC ping return from {chstr} took {kwargs['timing']}s.") + obj.msg(_(f"IRC ping return from {chstr} took {kwargs['timing']}s.") self._ping_callers = [] return diff --git a/evennia/help/manager.py b/evennia/help/manager.py index 3459efe951..b218eeb9fc 100644 --- a/evennia/help/manager.py +++ b/evennia/help/manager.py @@ -131,7 +131,7 @@ class HelpEntryManager(TypedObjectManager): for topic in topics: topic.help_category = default_category topic.save() - string = "Help database moved to category %s" % default_category + string = _(f"Help database moved to category {default_category}") logger.log_info(string) def search_help(self, ostring, help_category=None): diff --git a/evennia/objects/admin.py b/evennia/objects/admin.py index 49bec928c3..6e765b9523 100644 --- a/evennia/objects/admin.py +++ b/evennia/objects/admin.py @@ -8,7 +8,7 @@ from django.contrib import admin from evennia.typeclasses.admin import AttributeInline, TagInline from evennia.objects.models import ObjectDB from django.contrib.admin.utils import flatten_fieldsets - +from django.utils.translation import gettext as _ class ObjectAttributeInline(AttributeInline): """ @@ -61,7 +61,7 @@ class ObjectCreateForm(forms.ModelForm): required=False, widget=forms.TextInput(attrs={"size": "78"}), help_text="Most non-character objects don't need a cmdset" - " and can leave this field blank.", + " and can leave this field blank.") ) raw_id_fields = ("db_destination", "db_location", "db_home") diff --git a/evennia/utils/validatorfuncs.py b/evennia/utils/validatorfuncs.py index bca25874cb..d368cc4ddd 100644 --- a/evennia/utils/validatorfuncs.py +++ b/evennia/utils/validatorfuncs.py @@ -15,6 +15,7 @@ from django.core.exceptions import ValidationError as _error from django.core.validators import validate_email as _val_email from evennia.utils.ansi import strip_ansi from evennia.utils.utils import string_partial_matching as _partial +from django.utils.translation import gettext as _ _TZ_DICT = {str(tz): _pytz.timezone(tz) for tz in _pytz.common_timezones} @@ -58,7 +59,7 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs) """ if not entry: - raise ValueError(f"No {option_key} entered!") + raise ValueError(_(f"No {option_key} entered!")) if not from_tz: from_tz = _pytz.UTC if account: @@ -66,7 +67,7 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs) try: from_tz = _pytz.timezone(acct_tz) except Exception as err: - raise ValueError(f"Timezone string '{acct_tz}' is not a valid timezone ({err})") + raise ValueError(_(f"Timezone string '{acct_tz}' is not a valid timezone ({err})")) else: from_tz = _pytz.UTC From a4a6df421937661754c54233fda6340a77f6b4d1 Mon Sep 17 00:00:00 2001 From: trhr Date: Wed, 15 Apr 2020 21:58:28 -0500 Subject: [PATCH 06/32] i18n --- evennia/accounts/accounts.py | 4 ++-- evennia/accounts/bots.py | 2 +- evennia/objects/admin.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 24c551fdbb..b361f4f910 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -275,11 +275,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): raise RuntimeError("Session not found") if self.get_puppet(session) == obj: # already puppeting this object - self.msg(_("You are already puppeting this object.") + self.msg(_("You are already puppeting this object.")) return if not obj.access(self, "puppet"): # no access - self.msg(_(f"You don't have permission to puppet '{obj.key}'.") + self.msg(_(f"You don't have permission to puppet '{obj.key}'.")) return if obj.account: # object already puppeted diff --git a/evennia/accounts/bots.py b/evennia/accounts/bots.py index 1c1fc7d300..b9ce7f7062 100644 --- a/evennia/accounts/bots.py +++ b/evennia/accounts/bots.py @@ -338,7 +338,7 @@ class IRCBot(Bot): if hasattr(self, "_ping_callers") and self._ping_callers: chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" for obj in self._ping_callers: - obj.msg(_(f"IRC ping return from {chstr} took {kwargs['timing']}s.") + obj.msg(_(f"IRC ping return from {chstr} took {kwargs['timing']}s.")) self._ping_callers = [] return diff --git a/evennia/objects/admin.py b/evennia/objects/admin.py index 6e765b9523..5e18e8e0a4 100644 --- a/evennia/objects/admin.py +++ b/evennia/objects/admin.py @@ -61,7 +61,7 @@ class ObjectCreateForm(forms.ModelForm): required=False, widget=forms.TextInput(attrs={"size": "78"}), help_text="Most non-character objects don't need a cmdset" - " and can leave this field blank.") + " and can leave this field blank." ) raw_id_fields = ("db_destination", "db_location", "db_home") From e3426d2124329169f3a88838eed3e56df347d8a8 Mon Sep 17 00:00:00 2001 From: trhr Date: Wed, 15 Apr 2020 21:59:09 -0500 Subject: [PATCH 07/32] i18n --- evennia/accounts/bots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/accounts/bots.py b/evennia/accounts/bots.py index b9ce7f7062..db68fa09f6 100644 --- a/evennia/accounts/bots.py +++ b/evennia/accounts/bots.py @@ -329,7 +329,7 @@ class IRCBot(Bot): chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower())) for obj in self._nicklist_callers: - obj.msg(_(f"Nicks at {chstr}:\n {nicklist}") + obj.msg(_(f"Nicks at {chstr}:\n {nicklist}")) self._nicklist_callers = [] return From 0bc7866da38e526ac19d1763ddd78dc2d6c964fa Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 16 Apr 2020 09:44:10 +0200 Subject: [PATCH 08/32] Testing out github's inbuilt fund-link feature --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..30ef101a02 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: griatch +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: https://www.paypal.me/GriatchEvennia From 0f8c0ad9bf28eadaa605b908cdd726c0262b3a05 Mon Sep 17 00:00:00 2001 From: trhr Date: Thu, 16 Apr 2020 05:53:51 -0500 Subject: [PATCH 09/32] fixing fstring --- evennia/accounts/accounts.py | 10 +++++----- evennia/accounts/bots.py | 4 ++-- evennia/commands/cmdhandler.py | 6 ++---- evennia/commands/cmdsethandler.py | 4 ++-- evennia/help/manager.py | 2 +- evennia/locks/lockhandler.py | 2 +- evennia/objects/objects.py | 2 +- evennia/utils/utils.py | 2 +- evennia/utils/validatorfuncs.py | 4 ++-- 9 files changed, 17 insertions(+), 19 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index b361f4f910..1f41b577da 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -279,7 +279,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): return if not obj.access(self, "puppet"): # no access - self.msg(_(f"You don't have permission to puppet '{obj.key}'.")) + self.msg(_("You don't have permission to puppet '{key}'.".format(key=obj.key))) return if obj.account: # object already puppeted @@ -300,7 +300,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): self.unpuppet_object(obj.sessions.get()) elif obj.account.is_connected: # controlled by another account - self.msg(_(f"|c{obj.key}|R is already puppeted by another Account.")) + self.msg(_("|c{key}|R is already puppeted by another Account.".format(key=obj.key))) return # do the puppeting @@ -1305,7 +1305,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ reason = f" ({reason if reason else ''})" - self._send_to_connect_channel(_(f"|R{self.key} disconnected{reason}|n")) + self._send_to_connect_channel(_("|R{key} disconnected{reason}|n".format(key=self.key))) def at_post_disconnect(self, **kwargs): """ @@ -1411,7 +1411,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if hasattr(target, "return_appearance"): return target.return_appearance(self) else: - return _(f"{target} has no in-game appearance.") + return _("{target} has no in-game appearance.".format(target=target)) else: # list of targets - make list to disconnect from db characters = list(tar for tar in target if tar) if target else [] @@ -1589,7 +1589,7 @@ class DefaultGuest(DefaultAccount): overriding the call (unused by default). """ - self._send_to_connect_channel(_(f"|G{self.key} connected|n")) + self._send_to_connect_channel(_("|G{key} connected|n".format(key=self.key))) self.puppet_object(session, self.db._last_puppet) def at_server_shutdown(self): diff --git a/evennia/accounts/bots.py b/evennia/accounts/bots.py index db68fa09f6..270addac73 100644 --- a/evennia/accounts/bots.py +++ b/evennia/accounts/bots.py @@ -329,7 +329,7 @@ class IRCBot(Bot): chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower())) for obj in self._nicklist_callers: - obj.msg(_(f"Nicks at {chstr}:\n {nicklist}")) + obj.msg(_("Nicks at {chstr}:\n {nicklist}".format(chstr=chstr, nicklist=nicklist))) self._nicklist_callers = [] return @@ -338,7 +338,7 @@ class IRCBot(Bot): if hasattr(self, "_ping_callers") and self._ping_callers: chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" for obj in self._ping_callers: - obj.msg(_(f"IRC ping return from {chstr} took {kwargs['timing']}s.")) + obj.msg(_("IRC ping return from {chstr} took {time}s.".format(chstr=chstr, time=kwargs['timing']))) self._ping_callers = [] return diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 6f8f247a72..7898b9aedd 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -743,7 +743,7 @@ def cmdhandler( sysarg = raw_string else: # fallback to default error text - sysarg = _("Command '%s' is not available.") % raw_string + sysarg = _("Command '{command}' is not available.".format(command=raw_string)) suggestions = string_suggestions( raw_string, cmdset.get_all_cmd_keys_and_aliases(caller), @@ -751,9 +751,7 @@ def cmdhandler( maxnum=3, ) if suggestions: - sysarg += _(" Maybe you meant %s?") % utils.list_to_string( - suggestions, _("or"), addquote=True - ) + sysarg += _(" Maybe you meant {command}?".format(command=utils.list_to_string(suggestions, _("or"), addquote=True))) else: sysarg += _(' Type "help" for help.') raise ExecSystemCommand(syscmd, sysarg) diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index 395c9c2ba3..4b596e6a50 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -184,7 +184,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): raise exc.with_traceback(tb) else: # try next suggested path - errstring += _("\n(Unsuccessfully tried '%s')." % python_path) + errstring += _("\n(Unsuccessfully tried '{path}').".format(path=python_path)) continue try: cmdsetclass = getattr(module, classname) @@ -194,7 +194,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): dum, dum, tb = sys.exc_info() raise exc.with_traceback(tb) else: - errstring += _("\n(Unsuccessfully tried '%s')." % python_path) + errstring += _("\n(Unsuccessfully tried '{path}').".format(path=python_path)) continue _CACHED_CMDSETS[python_path] = cmdsetclass diff --git a/evennia/help/manager.py b/evennia/help/manager.py index b218eeb9fc..02ce32c927 100644 --- a/evennia/help/manager.py +++ b/evennia/help/manager.py @@ -131,7 +131,7 @@ class HelpEntryManager(TypedObjectManager): for topic in topics: topic.help_category = default_category topic.save() - string = _(f"Help database moved to category {default_category}") + string = _("Help database moved to category {default_category}".format(default_category=default_category)) logger.log_info(string) def search_help(self, ostring, help_category=None): diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index ac8c85abc8..a1b935ddd1 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -246,7 +246,7 @@ class LockHandler(object): evalstring = " ".join(_RE_OK.findall(evalstring)) eval(evalstring % tuple(True for func in funclist), {}, {}) except Exception: - elist.append(_("Lock: definition '%s' has syntax errors.") % raw_lockstring) + elist.append(_("Lock: definition '{lock_string}' has syntax errors.".format(lock_string=raw_lockstring))) continue if access_type in locks: duplicates += 1 diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index de9d09abae..f977996fa1 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1055,7 +1055,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # See if we need to kick the account off. for session in self.sessions.all(): - session.msg(_("Your character %s has been destroyed.") % self.key) + session.msg(_("Your character {key} has been destroyed.".format(key=self.key))) # no need to disconnect, Account just jumps to OOC mode. # sever the connection (important!) if self.account: diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index cc191fdd53..ac32801899 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -1916,7 +1916,7 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs): if multimatch_string: error = "%s\n" % multimatch_string else: - error = _("More than one match for '%s' (please narrow target):\n" % query) + error = _("More than one match for '{query}' (please narrow target):\n".format(query=query)) for num, result in enumerate(matches): # we need to consider Commands, where .aliases is a list diff --git a/evennia/utils/validatorfuncs.py b/evennia/utils/validatorfuncs.py index d368cc4ddd..7e4f05cf69 100644 --- a/evennia/utils/validatorfuncs.py +++ b/evennia/utils/validatorfuncs.py @@ -59,7 +59,7 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs) """ if not entry: - raise ValueError(_(f"No {option_key} entered!")) + raise ValueError(_("No {option_key} entered!".format(option_key=option_key))) if not from_tz: from_tz = _pytz.UTC if account: @@ -67,7 +67,7 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs) try: from_tz = _pytz.timezone(acct_tz) except Exception as err: - raise ValueError(_(f"Timezone string '{acct_tz}' is not a valid timezone ({err})")) + raise ValueError(_("Timezone string '{acct_tz}' is not a valid timezone ({err})".format(acct_tz=acct_tz, err=err))) else: from_tz = _pytz.UTC From 518c21e661561c7f4661f0c44625c8835066d720 Mon Sep 17 00:00:00 2001 From: trhr Date: Thu, 16 Apr 2020 05:57:51 -0500 Subject: [PATCH 10/32] fixing fstring --- evennia/accounts/accounts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 1f41b577da..1a7260d947 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -1253,7 +1253,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if session: session.msg(logged_in={}) - self._send_to_connect_channel(f"|G{self.key} connected|n") + self._send_to_connect_channel(_("|G{key} connected|n".format(key=self.key))) if _MULTISESSION_MODE == 0: # in this mode we should have only one character available. We # try to auto-connect to our last conneted object, if any @@ -1305,7 +1305,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ reason = f" ({reason if reason else ''})" - self._send_to_connect_channel(_("|R{key} disconnected{reason}|n".format(key=self.key))) + self._send_to_connect_channel(_("|R{key} disconnected{reason}|n".format(key=self.key, reason=reason))) def at_post_disconnect(self, **kwargs): """ From f088207091deb54d8524c3eb145175bb55b452f0 Mon Sep 17 00:00:00 2001 From: trhr Date: Thu, 16 Apr 2020 06:14:03 -0500 Subject: [PATCH 11/32] fixing fstring --- evennia/accounts/accounts.py | 12 ++++++------ evennia/accounts/bots.py | 4 ++-- evennia/commands/cmdhandler.py | 4 ++-- evennia/commands/cmdsethandler.py | 4 ++-- evennia/help/manager.py | 2 +- evennia/locks/lockhandler.py | 2 +- evennia/objects/objects.py | 2 +- evennia/utils/utils.py | 2 +- evennia/utils/validatorfuncs.py | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 1a7260d947..8986d18473 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -279,7 +279,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): return if not obj.access(self, "puppet"): # no access - self.msg(_("You don't have permission to puppet '{key}'.".format(key=obj.key))) + self.msg(_("You don't have permission to puppet '{key}'.").format(key=obj.key)) return if obj.account: # object already puppeted @@ -300,7 +300,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): self.unpuppet_object(obj.sessions.get()) elif obj.account.is_connected: # controlled by another account - self.msg(_("|c{key}|R is already puppeted by another Account.".format(key=obj.key))) + self.msg(_("|c{key}|R is already puppeted by another Account.").format(key=obj.key)) return # do the puppeting @@ -1253,7 +1253,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if session: session.msg(logged_in={}) - self._send_to_connect_channel(_("|G{key} connected|n".format(key=self.key))) + self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key)) if _MULTISESSION_MODE == 0: # in this mode we should have only one character available. We # try to auto-connect to our last conneted object, if any @@ -1305,7 +1305,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ reason = f" ({reason if reason else ''})" - self._send_to_connect_channel(_("|R{key} disconnected{reason}|n".format(key=self.key, reason=reason))) + self._send_to_connect_channel(_("|R{key} disconnected{reason}|n").format(key=self.key, reason=reason)) def at_post_disconnect(self, **kwargs): """ @@ -1411,7 +1411,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if hasattr(target, "return_appearance"): return target.return_appearance(self) else: - return _("{target} has no in-game appearance.".format(target=target)) + return _("{target} has no in-game appearance.").format(target=target) else: # list of targets - make list to disconnect from db characters = list(tar for tar in target if tar) if target else [] @@ -1589,7 +1589,7 @@ class DefaultGuest(DefaultAccount): overriding the call (unused by default). """ - self._send_to_connect_channel(_("|G{key} connected|n".format(key=self.key))) + self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key)) self.puppet_object(session, self.db._last_puppet) def at_server_shutdown(self): diff --git a/evennia/accounts/bots.py b/evennia/accounts/bots.py index 270addac73..6c6a40d0db 100644 --- a/evennia/accounts/bots.py +++ b/evennia/accounts/bots.py @@ -329,7 +329,7 @@ class IRCBot(Bot): chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower())) for obj in self._nicklist_callers: - obj.msg(_("Nicks at {chstr}:\n {nicklist}".format(chstr=chstr, nicklist=nicklist))) + obj.msg(_("Nicks at {chstr}:\n {nicklist}").format(chstr=chstr, nicklist=nicklist)) self._nicklist_callers = [] return @@ -338,7 +338,7 @@ class IRCBot(Bot): if hasattr(self, "_ping_callers") and self._ping_callers: chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" for obj in self._ping_callers: - obj.msg(_("IRC ping return from {chstr} took {time}s.".format(chstr=chstr, time=kwargs['timing']))) + obj.msg(_("IRC ping return from {chstr} took {time}s.").format(chstr=chstr, time=kwargs['timing'])) self._ping_callers = [] return diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 7898b9aedd..a0d9be8842 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -743,7 +743,7 @@ def cmdhandler( sysarg = raw_string else: # fallback to default error text - sysarg = _("Command '{command}' is not available.".format(command=raw_string)) + sysarg = _("Command '{command}' is not available.").format(command=raw_string) suggestions = string_suggestions( raw_string, cmdset.get_all_cmd_keys_and_aliases(caller), @@ -751,7 +751,7 @@ def cmdhandler( maxnum=3, ) if suggestions: - sysarg += _(" Maybe you meant {command}?".format(command=utils.list_to_string(suggestions, _("or"), addquote=True))) + sysarg += _(" Maybe you meant {command}?").format(command=utils.list_to_string(suggestions, _("or"), addquote=True)) else: sysarg += _(' Type "help" for help.') raise ExecSystemCommand(syscmd, sysarg) diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index 4b596e6a50..4a746df33e 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -184,7 +184,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): raise exc.with_traceback(tb) else: # try next suggested path - errstring += _("\n(Unsuccessfully tried '{path}').".format(path=python_path)) + errstring += _("\n(Unsuccessfully tried '{path}').").format(path=python_path) continue try: cmdsetclass = getattr(module, classname) @@ -194,7 +194,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): dum, dum, tb = sys.exc_info() raise exc.with_traceback(tb) else: - errstring += _("\n(Unsuccessfully tried '{path}').".format(path=python_path)) + errstring += _("\n(Unsuccessfully tried '{path}').").format(path=python_path) continue _CACHED_CMDSETS[python_path] = cmdsetclass diff --git a/evennia/help/manager.py b/evennia/help/manager.py index 02ce32c927..646758d202 100644 --- a/evennia/help/manager.py +++ b/evennia/help/manager.py @@ -131,7 +131,7 @@ class HelpEntryManager(TypedObjectManager): for topic in topics: topic.help_category = default_category topic.save() - string = _("Help database moved to category {default_category}".format(default_category=default_category)) + string = _("Help database moved to category {default_category}").format(default_category=default_category) logger.log_info(string) def search_help(self, ostring, help_category=None): diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index a1b935ddd1..0f96b1d6a7 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -246,7 +246,7 @@ class LockHandler(object): evalstring = " ".join(_RE_OK.findall(evalstring)) eval(evalstring % tuple(True for func in funclist), {}, {}) except Exception: - elist.append(_("Lock: definition '{lock_string}' has syntax errors.".format(lock_string=raw_lockstring))) + elist.append(_("Lock: definition '{lock_string}' has syntax errors.").format(lock_string=raw_lockstring)) continue if access_type in locks: duplicates += 1 diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index f977996fa1..211e0f9fc3 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1055,7 +1055,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # See if we need to kick the account off. for session in self.sessions.all(): - session.msg(_("Your character {key} has been destroyed.".format(key=self.key))) + session.msg(_("Your character {key} has been destroyed.").format(key=self.key)) # no need to disconnect, Account just jumps to OOC mode. # sever the connection (important!) if self.account: diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index ac32801899..33da9c31cb 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -1916,7 +1916,7 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs): if multimatch_string: error = "%s\n" % multimatch_string else: - error = _("More than one match for '{query}' (please narrow target):\n".format(query=query)) + error = _("More than one match for '{query}' (please narrow target):\n").format(query=query) for num, result in enumerate(matches): # we need to consider Commands, where .aliases is a list diff --git a/evennia/utils/validatorfuncs.py b/evennia/utils/validatorfuncs.py index 7e4f05cf69..e49bb716a8 100644 --- a/evennia/utils/validatorfuncs.py +++ b/evennia/utils/validatorfuncs.py @@ -59,7 +59,7 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs) """ if not entry: - raise ValueError(_("No {option_key} entered!".format(option_key=option_key))) + raise ValueError(_("No {option_key} entered!").format(option_key=option_key)) if not from_tz: from_tz = _pytz.UTC if account: @@ -67,7 +67,7 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs) try: from_tz = _pytz.timezone(acct_tz) except Exception as err: - raise ValueError(_("Timezone string '{acct_tz}' is not a valid timezone ({err})".format(acct_tz=acct_tz, err=err))) + raise ValueError(_("Timezone string '{acct_tz}' is not a valid timezone ({err})").format(acct_tz=acct_tz, err=err)) else: from_tz = _pytz.UTC From 5f69d29b486fd15315be6332ba1d85a20b60352e Mon Sep 17 00:00:00 2001 From: dayport Date: Thu, 16 Apr 2020 16:23:55 -0700 Subject: [PATCH 12/32] Fix ssh server getKeyPair fun Correct the ssh server getKeyPair function to correctly use the cryptography module instead of the Crypto module. --- evennia/server/portal/ssh.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/evennia/server/portal/ssh.py b/evennia/server/portal/ssh.py index b2a891710f..6d7f46eb20 100644 --- a/evennia/server/portal/ssh.py +++ b/evennia/server/portal/ssh.py @@ -464,11 +464,12 @@ def getKeyPair(pubkeyfile, privkeyfile): if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)): # No keypair exists. Generate a new RSA keypair - from Crypto.PublicKey import RSA + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.asymmetric import rsa - rsa_key = Key(RSA.generate(_KEY_LENGTH)) - public_key_string = rsa_key.public().toString(type="OPENSSH") - private_key_string = rsa_key.toString(type="OPENSSH") + rsa_key = Key(rsa.generate_private_key(public_exponent=65537, key_size=_KEY_LENGTH, backend=default_backend())) + public_key_string = rsa_key.public().toString(type="OPENSSH").decode() + private_key_string = rsa_key.toString(type="OPENSSH").decode() # save keys for the future. with open(privkeyfile, "wt") as pfile: From 6cf005a6451c7137bd01eff2618dbc5141ef5cfb Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sat, 18 Apr 2020 08:12:16 -0700 Subject: [PATCH 13/32] Minor fix to sync attrs to make it less stupid. --- evennia/server/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/server/session.py b/evennia/server/session.py index f322c551cf..1918c6998e 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -101,7 +101,7 @@ class Session(object): the keys given by self._attrs_to_sync. """ - return {attr: getattr(self, attr, None) for attr in settings.SESSION_SYNC_ATTRS} + return {attr: getattr(self, attr) for attr in settings.SESSION_SYNC_ATTRS if hasattr(self, attr)} def load_sync_data(self, sessdata): """ From 0f6858a63011994e62be1b9a9811db0d7341da86 Mon Sep 17 00:00:00 2001 From: 3eluk <45859673+3eluk@users.noreply.github.com> Date: Sun, 19 Apr 2020 22:34:40 +0300 Subject: [PATCH 14/32] Create t.txt --- evennia/locale/ru/LC_MESSAGES/t.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 evennia/locale/ru/LC_MESSAGES/t.txt diff --git a/evennia/locale/ru/LC_MESSAGES/t.txt b/evennia/locale/ru/LC_MESSAGES/t.txt new file mode 100644 index 0000000000..5e3700bbc0 --- /dev/null +++ b/evennia/locale/ru/LC_MESSAGES/t.txt @@ -0,0 +1 @@ +uykl,y6tforl From f65c059220747f52c2d1b0edb1ac3c6a39e645d5 Mon Sep 17 00:00:00 2001 From: 3eluk <45859673+3eluk@users.noreply.github.com> Date: Sun, 19 Apr 2020 22:35:10 +0300 Subject: [PATCH 15/32] Add files via upload --- evennia/locale/ru/LC_MESSAGES/django.mo | Bin 0 -> 4806 bytes evennia/locale/ru/LC_MESSAGES/django.po | 299 ++++++++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 evennia/locale/ru/LC_MESSAGES/django.mo create mode 100644 evennia/locale/ru/LC_MESSAGES/django.po diff --git a/evennia/locale/ru/LC_MESSAGES/django.mo b/evennia/locale/ru/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..8a3f6b3aba60d7f382c3fe9f1ce55f8fcfe6a711 GIT binary patch literal 4806 zcmbW3U2Ggz6~_nKQdlUI@(~IYPLig!!8_hf18O&M(>5O{BB4>@l=dka?_PT+ot;_c z!yBUrHAzESVl|{{i^NB(76ea7wreL&Y{%jaiI>^Of&>!0lt&~a1QHUzb7$9y+aOd% z_WtM2J@+FSn?+{F4%;FrOFfL{VP-BIZMI=Gef82EAUB&hn%gZG0!1=XM5 zfZM>of&4j}KIAxq;Cg0bd98++V;lco)Gx4DJNg-wf1zy$tRK7r+esJNQlT1i^k7ybL}Ku7S^h_Y&Oa zz;mG1=QVH!Tm*N3kAKo}c7YvG<6Qu^gTDv&gLmCiByrsY|tyQbK?uAm1jh7nnq+wdoNSdWl?0bQDJeH21Hu3$@aWAbmRg)Jsq%U&36U9Np z9nburaVm66!sWPqoynTG{ZhVJ19MBu(Y%z~I(xjiJ6!IFL%)#MGcAt!T=~ zV8LtI1LwSYs)2e9YT-Hu8e}c?TPDgQr)HH^r;V1AjH1(AHxz0hqH6A$!qsd8F zPqrttpe9I6r)gs2%HECQhoV?3r0EQew3K+35H@{-)Kp6xdT|_e1WHYsbf{Kxz7+`o0d+~OLsd*2{CPIB|Gp&I&lij=>o8 z6Od<1+=xt~i83esBy}CO(bdtPb>>;ml}=+#iq7A$gg)3>b)CL#!& zQIM9OLPHfxj5gsd#ADSEQzHz~IZZE-abw^&p2yL&HlKg1k~vZA9FC(CRIGfUQGU|I z2|TXJWAQVGj_sAhQQ#*{IPHZ=P{cn~ajT^xcJP!BC6j)m{CGB*l*gi)lny^pK4PYr zYf)I<%Vep^_R983xw4~NeORgw)gIjb)e2YXpqHfOv7XW>uE{<#8O1yfCo^x-l*f$M zs>zWoNqopKSlkS)@7E;G%14fs4hLE61?Bzll`aX}cA1PmxKoPT(ZO(AwK6(dl?NUW zy{&8>tyX2XR0MI>^2n%c?{DrJ-NCIrQrT6loH}JsSdq%mZmDu+`_RtPp#z8Z-N;kb zt(25A@T;tItj4rDnU1sr&kuLnnj|J2JvO$#{Ll?K4Sd4H<$Ym2YCzALJT&g7rKidV zqk2?6%8R3J^xg~57iY%_^{MhVOlV?{30)HvvoDT>H%Fjyeq!8l|GOi79f!Rb%GHF; z=y+Qf$J-Gvul!QJm|y9h?Virp^2P4+`D%V$^0n@>`SpB`$1kqG$?})&BR1IQ_HsU( zFN5>>Z1+reM)G;-o?&&JGpZ$@x5x9P{H^Y@-Lu2O&go)lnCza#Csk+NTS3VS-I-zOp5yMk#->iH=el*?Zk=YO zalVi*6)~_-KcC0zx%?WDog0>bg_7I2<`Huk(@|9Px!!%w zx`s9l=Zf-O38`sb?#_Tq-E&F-+aF^03V#r|g1}1ml63;VX3=C%X#){D4U}J{q!-l3 zLZvd1$lk>oB7qso1dT{}hluja7OMC%QNQ3bT;`#6p>nIJ4F+f%jKlmwXie#(&D*29 z7RMID3cR}@*4PbJ&E7DGTXZ@uoh3Z%Zj|g^c+ba;CfVlGmRf^Y*ZV)Ei?V5t=L_#S z`EJEOyJa@$=Hk03zxBRW1&8eH5Q0*MEYn?@Y0cOACzCzjFN@6q0|yEg?6cB#P-@>^ zErtTQUR?jE%HKfxA|&t*4Y%!)VxskDojy|6EoUvU-YRlLYum^c zUCCd&rHM;6eKYEcQgcE5$vJKF%Pg>^>|V4}#6H%hgQl8q6f)k6@mG8UG6^&n=Zoy3 z5)w09x-dXIG*EiK5zF$?_8X{9beNn%)oo|}& zw7E6q7=|Q%Jq3 zLmZ167}33yukt`&HN6SM;gx=4{ai!nrOo;(nm0xvyzFsuiIn2ya<3#xtaVj=H7~LG nyjF&}g)DDAlw8)&+ag%)PVGrs&h=3@uU6h)e>fzBf$jVUZ`oO4 literal 0 HcmV?d00001 diff --git a/evennia/locale/ru/LC_MESSAGES/django.po b/evennia/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000000..e42efc8362 --- /dev/null +++ b/evennia/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,299 @@ +msgid "" +msgstr "" +"Project-Id-Version: Evennia Russian Translation v0.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-02-20 12:13+0000\n" +"PO-Revision-Date: 2020-04-19 18:32+0000\n" +"Last-Translator: 3eluk\n" +"Language-Team: Russian (Russia)\n" +"Language: ru-RU\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10 >= 2 && " +"n%10<=4 &&(n%100<10||n%100 >= 20)? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Loco-Source-Locale: ru_RU\n" +"X-Generator: Loco https://localise.biz/\n" +"X-Loco-Parser: loco_parse_po" + +#: accounts/accounts.py:440 +msgid "Account being deleted." +msgstr "Аккаунт удаляется." + +#: commands/cmdhandler.py:681 +msgid "There were multiple matches." +msgstr "Здесь было несколько совпадений." + +#: commands/cmdhandler.py:704 +#, python-format +msgid "Command '%s' is not available." +msgstr "Команда '%s' недоступна." + +#: commands/cmdhandler.py:709 +#, python-format +msgid " Maybe you meant %s?" +msgstr "Возможно, вы имели ввиду %s?" + +#: commands/cmdhandler.py:709 +msgid "or" +msgstr "или" + +#: commands/cmdhandler.py:711 +msgid " Type \"help\" for help." +msgstr " Введи \"справка\" для получения помощи." + +#: commands/cmdsethandler.py:89 +msgid "" +"{traceback}\n" +"Error loading cmdset '{path}'\n" +"(Traceback was logged {timestamp})" +msgstr "" + +#: commands/cmdsethandler.py:94 +msgid "" +"Error loading cmdset: No cmdset class '{classname}' in '{path}'.\n" +"(Traceback was logged {timestamp})" +msgstr "" + +#: commands/cmdsethandler.py:98 +msgid "" +"{traceback}\n" +"SyntaxError encountered when loading cmdset '{path}'.\n" +"(Traceback was logged {timestamp})" +msgstr "" + +#: commands/cmdsethandler.py:103 +msgid "" +"{traceback}\n" +"Compile/Run error when loading cmdset '{path}'.\",\n" +"(Traceback was logged {timestamp})" +msgstr "" + +#: commands/cmdsethandler.py:108 +msgid "" +"\n" +"Error encountered for cmdset at path '{path}'.\n" +"Replacing with fallback '{fallback_path}'.\n" +msgstr "" + +#: commands/cmdsethandler.py:114 +msgid "Fallback path '{fallback_path}' failed to generate a cmdset." +msgstr "" + +#: commands/cmdsethandler.py:182 commands/cmdsethandler.py:192 +#, python-format +msgid "" +"\n" +"(Unsuccessfully tried '%s')." +msgstr "" +"\n" +"(Безуспешно пробую '%s')." + +#: commands/cmdsethandler.py:311 +msgid "custom {mergetype} on cmdset '{cmdset}'" +msgstr "" + +#: commands/cmdsethandler.py:314 +msgid " : {current}" +msgstr "" + +#: commands/cmdsethandler.py:322 +msgid "" +" <{key} ({mergetype}, prio {prio}, {permstring})>:\n" +" {keylist}" +msgstr "" + +#: commands/cmdsethandler.py:426 +msgid "Only CmdSets can be added to the cmdsethandler!" +msgstr "" + +#: comms/channelhandler.py:100 +msgid "Say what?" +msgstr "Сказать что?" + +#: comms/channelhandler.py:105 +#, python-format +msgid "Channel '%s' not found." +msgstr "Канал '%s' не обнаружен." + +#: comms/channelhandler.py:108 +#, python-format +msgid "You are not connected to channel '%s'." +msgstr "Ты не соединён с каналом '%s'." + +#: comms/channelhandler.py:112 +#, python-format +msgid "You are not permitted to send to channel '%s'." +msgstr "У тебя нет разрешения слать в канал '%s'." + +#: comms/channelhandler.py:155 +msgid " (channel)" +msgstr " (канал)" + +#: locks/lockhandler.py:236 +#, python-format +msgid "Lock: lock-function '%s' is not available." +msgstr "" + +#: locks/lockhandler.py:249 +#, python-format +msgid "Lock: definition '%s' has syntax errors." +msgstr "" + +#: locks/lockhandler.py:253 +#, python-format +msgid "" +"LockHandler on %(obj)s: access type '%(access_type)s' changed from '%(source)" +"s' to '%(goal)s' " +msgstr "" + +#: locks/lockhandler.py:320 +msgid "Lock: '{lockdef}' contains no colon (:)." +msgstr "" + +#: locks/lockhandler.py:328 +msgid "Lock: '{lockdef}' has no access_type (left-side of colon is empty)." +msgstr "" + +#: locks/lockhandler.py:336 +msgid "Lock: '{lockdef}' has mismatched parentheses." +msgstr "" + +#: locks/lockhandler.py:343 +msgid "Lock: '{lockdef}' has no valid lock functions." +msgstr "" + +#: objects/objects.py:732 +#, python-format +msgid "Couldn't perform move ('%s'). Contact an admin." +msgstr "Не удалось выполнить действие ('%s'). Свяжитесь с администратором." + +#: objects/objects.py:742 +msgid "The destination doesn't exist." +msgstr "Такой точки назначения нету." + +#: objects/objects.py:833 +#, python-format +msgid "Could not find default home '(#%d)'." +msgstr "Не обнаружен дом по умолчанию '(#%d)'." + +#: objects/objects.py:849 +msgid "Something went wrong! You are dumped into nowhere. Contact an admin." +msgstr "" +"Что-то пошло не так! Тебя выбрасывает в пустоту. Свяжитесь с администратором." + +#: objects/objects.py:915 +#, python-format +msgid "Your character %s has been destroyed." +msgstr "Ваш персонаж %s был уничтожен." + +#: scripts/scripthandler.py:53 +#, python-format +msgid "" +"\n" +" '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s" +msgstr "" + +#: scripts/scripts.py:205 +#, python-format +msgid "" +"Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error '%(err)s'." +msgstr "" + +#: server/initial_setup.py:28 +msgid "" +"\n" +"Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if " +"you need\n" +"help, want to contribute, report issues or just join the community.\n" +"As Account #1 you can create a demo/tutorial area with |w@batchcommand " +"tutorial_world.build|n.\n" +" " +msgstr "" +"\n" +"Добро пожаловать в твою новую игру, основанную на |wEvennia|n! Посети http:" +"//www.evennia.com\n" +"если тебе нужна помощь, хочешь помочь, сообщить об ошибках, lили просто " +"присоединиться к сообществу.\n" +"Как Аккаунт №1, ты можешь создать зону для демонстрации/обучения командой " +"|w@batchcommand tutorial_world.build|n.\n" +" " + +#: server/initial_setup.py:92 +msgid "This is User #1." +msgstr "Это Пользователь №1." + +#: server/initial_setup.py:105 +msgid "Limbo" +msgstr "Лимб" + +#: server/server.py:139 +msgid "idle timeout exceeded" +msgstr "время бездействия превышено" + +#: server/sessionhandler.py:386 +msgid " ... Server restarted." +msgstr " ... Сервер перезапущен." + +#: server/sessionhandler.py:606 +msgid "Logged in from elsewhere. Disconnecting." +msgstr "Выполнено соединение в другом месте. Отключение." + +#: server/sessionhandler.py:634 +msgid "Idle timeout exceeded, disconnecting." +msgstr "Время бездействия превышено, отключение." + +#: server/validators.py:50 +#, python-format +msgid "" +"%s From a terminal client, you can also use a phrase of multiple words if " +"you enclose the password in double quotes." +msgstr "" +"%s Если вы используете терминал, вы можете использовать фразу из нескольких " +"слов если возьмёте пароль в двойные скобки." + +#: utils/evmenu.py:192 +msgid "" +"Menu node '{nodename}' is either not implemented or caused an error. Make " +"another choice." +msgstr "" + +#: utils/evmenu.py:194 +msgid "Error in menu node '{nodename}'." +msgstr "" + +#: utils/evmenu.py:195 +msgid "No description." +msgstr "Нет описания." + +#: utils/evmenu.py:196 +msgid "Commands: , help, quit" +msgstr "Команды: , справка, выход" + +#: utils/evmenu.py:197 +msgid "Commands: , help" +msgstr "Команды: , справка" + +#: utils/evmenu.py:198 +msgid "Commands: help, quit" +msgstr "" + +#: utils/evmenu.py:199 +msgid "Commands: help" +msgstr "Команды: справка" + +#: utils/evmenu.py:200 +msgid "Choose an option or try 'help'." +msgstr "Выберите опцию или введите \"справка\"." + +#: utils/utils.py:1866 +#, python-format +msgid "Could not find '%s'." +msgstr "Не обнаружено '%s'." + +#: utils/utils.py:1873 +#, python-format +msgid "" +"More than one match for '%s' (please narrow target):\n" +msgstr "" +"Больше одного подходящего варианта для '%s' (уточните цель):\n" From 42c87df73bfbb0e2be5b3956733c91e28fbb3411 Mon Sep 17 00:00:00 2001 From: 3eluk <45859673+3eluk@users.noreply.github.com> Date: Sun, 19 Apr 2020 22:37:07 +0300 Subject: [PATCH 16/32] Russian translation added --- evennia/locale/ru/LC_MESSAGES/t.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 evennia/locale/ru/LC_MESSAGES/t.txt diff --git a/evennia/locale/ru/LC_MESSAGES/t.txt b/evennia/locale/ru/LC_MESSAGES/t.txt deleted file mode 100644 index 5e3700bbc0..0000000000 --- a/evennia/locale/ru/LC_MESSAGES/t.txt +++ /dev/null @@ -1 +0,0 @@ -uykl,y6tforl From e2b878ff3ced841417b3c178154131ed9e956a85 Mon Sep 17 00:00:00 2001 From: trhr Date: Mon, 20 Apr 2020 00:54:33 -0500 Subject: [PATCH 17/32] EvenniaLocalTest - uses settings-defined typeclasses --- evennia/utils/test_resources.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/evennia/utils/test_resources.py b/evennia/utils/test_resources.py index 053541391f..d9d8967cdf 100644 --- a/evennia/utils/test_resources.py +++ b/evennia/utils/test_resources.py @@ -157,3 +157,11 @@ class EvenniaTest(TestCase): self.account.delete() self.account2.delete() super().tearDown() + +class LocalEvenniaTest(EvenniaTest): + account_typeclass = settings.BASE_ACCOUNT_TYPECLASS + object_typeclass = settings.BASE_OBJECT_TYPECLASS + character_typeclass = settings.BASE_CHARACTER_TYPECLASS + exit_typeclass = settings.BASE_EXIT_TYPECLASS + room_typeclass = settings.BASE_ROOM_TYPECLASS + script_typeclass = settings.BASE_SCRIPT_TYPECLASS From 399dc3640d069362f3a2bff4ccd39426cef82ab3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 20 Apr 2020 20:15:39 +0200 Subject: [PATCH 18/32] Update utils.search argument strings. Resolves #2106. --- evennia/typeclasses/managers.py | 14 ++++++--- evennia/utils/search.py | 56 +++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/evennia/typeclasses/managers.py b/evennia/typeclasses/managers.py index e9ad42ed11..b53755ad45 100644 --- a/evennia/typeclasses/managers.py +++ b/evennia/typeclasses/managers.py @@ -31,7 +31,8 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): # Attribute manager methods def get_attribute( - self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None + self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None, **kwargs + ): """ Return Attribute objects by key, by category, by value, by @@ -55,6 +56,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): attrype (str, optional): An attribute-type to search for. By default this is either `None` (normal Attributes) or `"nick"`. + kwargs (any): Currently unused. Reserved for future use. Returns: attributes (list): The matching Attributes. @@ -102,7 +104,8 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): key=key, category=category, value=value, strvalue=strvalue, obj=obj ) - def get_by_attribute(self, key=None, category=None, value=None, strvalue=None, attrtype=None): + def get_by_attribute(self, key=None, category=None, value=None, + strvalue=None, attrtype=None, **kwargs): """ Return objects having attributes with the given key, category, value, strvalue or combination of those criteria. @@ -122,6 +125,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): attrype (str, optional): An attribute-type to search for. By default this is either `None` (normal Attributes) or `"nick"`. + kwargs (any): Currently unused. Reserved for future use. Returns: obj (list): Objects having the matching Attributes. @@ -488,12 +492,12 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): def get_typeclass_totals(self, *args, **kwargs) -> object: """ Returns a queryset of typeclass composition statistics. - + Returns: - qs (Queryset): A queryset of dicts containing the typeclass (name), + qs (Queryset): A queryset of dicts containing the typeclass (name), the count of objects with that typeclass and a float representing the percentage of objects associated with the typeclass. - + """ return ( self.values("db_typeclass_path") diff --git a/evennia/utils/search.py b/evennia/utils/search.py index 1a788300cf..45bdc86a87 100644 --- a/evennia/utils/search.py +++ b/evennia/utils/search.py @@ -205,27 +205,31 @@ help_entries = search_help # not the attribute object itself (this is usually what you want) -def search_object_attribute(key=None, category=None, value=None, strvalue=None): +def search_object_attribute(key=None, category=None, value=None, + strvalue=None, attrtype=None, **kwargs): return ObjectDB.objects.get_by_attribute( - key=key, category=category, value=value, strvalue=strvalue + key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs ) -def search_account_attribute(key=None, category=None, value=None, strvalue=None): +def search_account_attribute(key=None, category=None, value=None, + strvalue=None, attrtype=None, **kwargs): return AccountDB.objects.get_by_attribute( - key=key, category=category, value=value, strvalue=strvalue + key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs ) -def search_script_attribute(key=None, category=None, value=None, strvalue=None): +def search_script_attribute(key=None, category=None, value=None, + strvalue=None, attrtype=None, **kwargs): return ScriptDB.objects.get_by_attribute( - key=key, category=category, value=value, strvalue=strvalue + key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs ) -def search_channel_attribute(key=None, category=None, value=None, strvalue=None): +def search_channel_attribute(key=None, category=None, value=None, + strvalue=None, attrtype=None, **kwargs): return Channel.objects.get_by_attribute( - key=key, category=category, value=value, strvalue=strvalue + key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs ) @@ -243,7 +247,7 @@ search_attribute_object = ObjectDB.objects.get_attribute # object itself (this is usually what you want) -def search_object_by_tag(key=None, category=None): +def search_object_by_tag(key=None, category=None, tagtype=None, **kwargs): """ Find object based on tag or category. @@ -252,6 +256,11 @@ def search_object_by_tag(key=None, category=None): category (str, optional): The category of tag to search for. If not set, uncategorized tags will be searched. + tagtype (str, optional): 'type' of Tag, by default + this is either `None` (a normal Tag), `alias` or + `permission`. This always apply to all queried tags. + kwargs (any): Other optional parameter that may be supported + by the manager method. Returns: matches (list): List of Objects with tags matching @@ -259,13 +268,13 @@ def search_object_by_tag(key=None, category=None): matches were found. """ - return ObjectDB.objects.get_by_tag(key=key, category=category) + return ObjectDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs) search_tag = search_object_by_tag # this is the most common case -def search_account_tag(key=None, category=None): +def search_account_tag(key=None, category=None, tagtype=None, **kwargs): """ Find account based on tag or category. @@ -274,6 +283,11 @@ def search_account_tag(key=None, category=None): category (str, optional): The category of tag to search for. If not set, uncategorized tags will be searched. + tagtype (str, optional): 'type' of Tag, by default + this is either `None` (a normal Tag), `alias` or + `permission`. This always apply to all queried tags. + kwargs (any): Other optional parameter that may be supported + by the manager method. Returns: matches (list): List of Accounts with tags matching @@ -281,10 +295,10 @@ def search_account_tag(key=None, category=None): matches were found. """ - return AccountDB.objects.get_by_tag(key=key, category=category) + return AccountDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs) -def search_script_tag(key=None, category=None): +def search_script_tag(key=None, category=None, tagtype=None, **kwargs): """ Find script based on tag or category. @@ -293,6 +307,11 @@ def search_script_tag(key=None, category=None): category (str, optional): The category of tag to search for. If not set, uncategorized tags will be searched. + tagtype (str, optional): 'type' of Tag, by default + this is either `None` (a normal Tag), `alias` or + `permission`. This always apply to all queried tags. + kwargs (any): Other optional parameter that may be supported + by the manager method. Returns: matches (list): List of Scripts with tags matching @@ -300,10 +319,10 @@ def search_script_tag(key=None, category=None): matches were found. """ - return ScriptDB.objects.get_by_tag(key=key, category=category) + return ScriptDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs) -def search_channel_tag(key=None, category=None): +def search_channel_tag(key=None, category=None, tagtype=None, **kwargs): """ Find channel based on tag or category. @@ -312,6 +331,11 @@ def search_channel_tag(key=None, category=None): category (str, optional): The category of tag to search for. If not set, uncategorized tags will be searched. + tagtype (str, optional): 'type' of Tag, by default + this is either `None` (a normal Tag), `alias` or + `permission`. This always apply to all queried tags. + kwargs (any): Other optional parameter that may be supported + by the manager method. Returns: matches (list): List of Channels with tags matching @@ -319,7 +343,7 @@ def search_channel_tag(key=None, category=None): matches were found. """ - return Channel.objects.get_by_tag(key=key, category=category) + return Channel.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs) # search for tag objects (not the objects they are attached to From 3520b76dcb0d278be5a6812a9f3b94c604a56804 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 20 Apr 2020 20:33:40 +0200 Subject: [PATCH 19/32] Fix wrong path reference in settings_default. Resolve #2111 --- evennia/settings_default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 5ecafb75b5..62a00f090f 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -4,7 +4,7 @@ Master configuration file for Evennia. NOTE: NO MODIFICATIONS SHOULD BE MADE TO THIS FILE! All settings changes should be done by copy-pasting the variable and -its value to /conf/settings.py. +its value to /server/conf/settings.py. Hint: Don't copy&paste over more from this file than you actually want to change. Anything you don't copy&paste will thus retain its default From cd5beed8d75d9ed7cc0b5b7e62e60d53da37e33a Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 20 Apr 2020 23:28:33 +0200 Subject: [PATCH 20/32] Fixed typos and docstring style --- evennia/utils/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/utils/create.py b/evennia/utils/create.py index dbf19f0332..182a1e4ba3 100644 --- a/evennia/utils/create.py +++ b/evennia/utils/create.py @@ -502,7 +502,7 @@ def create_account( errors to. If not given, errors will be logged. Returns: - (object) The acount object of the new account. + Account: The newly created Account. Raises: ValueError: If `key` already exists in database. From 8b2b23873f43e33e0913ba0493b64f75b1245abd Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 20 Apr 2020 23:39:09 +0200 Subject: [PATCH 21/32] Run black on sources --- evennia/accounts/accounts.py | 24 ++++++++++++------- evennia/accounts/bots.py | 10 ++++++-- evennia/commands/cmdhandler.py | 8 +++++-- evennia/commands/cmdsethandler.py | 8 +++++-- evennia/help/manager.py | 4 +++- evennia/locks/lockhandler.py | 6 ++++- evennia/objects/admin.py | 3 ++- evennia/server/portal/portal.py | 9 ++++--- evennia/server/portal/ssh.py | 6 ++++- evennia/server/portal/telnet.py | 1 - evennia/server/session.py | 4 +++- evennia/server/sessionhandler.py | 2 +- evennia/settings_default.py | 40 +++++++++++++++---------------- evennia/typeclasses/managers.py | 6 ++--- evennia/utils/search.py | 20 +++++++++------- evennia/utils/utils.py | 4 +++- evennia/utils/validatorfuncs.py | 6 ++++- 17 files changed, 102 insertions(+), 59 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 8986d18473..49d3f550cc 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -508,8 +508,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if banned: # this is a banned IP or name! errors.append( - _("|rYou have been banned and cannot continue from here." - "\nIf you feel this ban is in error, please email an admin.|x") + _( + "|rYou have been banned and cannot continue from here." + "\nIf you feel this ban is in error, please email an admin.|x" + ) ) logger.log_sec(f"Authentication Denied (Banned): {username} (IP: {ip}).") LOGIN_THROTTLE.update(ip, "Too many sightings of banned artifact.") @@ -716,9 +718,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): banned = cls.is_banned(username=username, ip=ip) if banned: # this is a banned IP or name! - string = ( - _("|rYou have been banned and cannot continue from here." - "\nIf you feel this ban is in error, please email an admin.|x") + string = _( + "|rYou have been banned and cannot continue from here." + "\nIf you feel this ban is in error, please email an admin.|x" ) errors.append(string) return None, errors @@ -733,7 +735,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): except Exception as e: errors.append( - _("There was an error creating the Account. If this problem persists, contact an admin.") + _( + "There was an error creating the Account. If this problem persists, contact an admin." + ) ) logger.log_trace() return None, errors @@ -1305,7 +1309,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ reason = f" ({reason if reason else ''})" - self._send_to_connect_channel(_("|R{key} disconnected{reason}|n").format(key=self.key, reason=reason)) + self._send_to_connect_channel( + _("|R{key} disconnected{reason}|n").format(key=self.key, reason=reason) + ) def at_post_disconnect(self, **kwargs): """ @@ -1454,7 +1460,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if is_su or len(characters) < charmax: if not characters: result.append( - _("\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one.") + _( + "\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one." + ) ) else: result.append("\n |w@charcreate [=description]|n - create new character") diff --git a/evennia/accounts/bots.py b/evennia/accounts/bots.py index 6c6a40d0db..420d96053d 100644 --- a/evennia/accounts/bots.py +++ b/evennia/accounts/bots.py @@ -329,7 +329,9 @@ class IRCBot(Bot): chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower())) for obj in self._nicklist_callers: - obj.msg(_("Nicks at {chstr}:\n {nicklist}").format(chstr=chstr, nicklist=nicklist)) + obj.msg( + _("Nicks at {chstr}:\n {nicklist}").format(chstr=chstr, nicklist=nicklist) + ) self._nicklist_callers = [] return @@ -338,7 +340,11 @@ class IRCBot(Bot): if hasattr(self, "_ping_callers") and self._ping_callers: chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" for obj in self._ping_callers: - obj.msg(_("IRC ping return from {chstr} took {time}s.").format(chstr=chstr, time=kwargs['timing'])) + obj.msg( + _("IRC ping return from {chstr} took {time}s.").format( + chstr=chstr, time=kwargs["timing"] + ) + ) self._ping_callers = [] return diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index a0d9be8842..8ae1a7f285 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -743,7 +743,9 @@ def cmdhandler( sysarg = raw_string else: # fallback to default error text - sysarg = _("Command '{command}' is not available.").format(command=raw_string) + sysarg = _("Command '{command}' is not available.").format( + command=raw_string + ) suggestions = string_suggestions( raw_string, cmdset.get_all_cmd_keys_and_aliases(caller), @@ -751,7 +753,9 @@ def cmdhandler( maxnum=3, ) if suggestions: - sysarg += _(" Maybe you meant {command}?").format(command=utils.list_to_string(suggestions, _("or"), addquote=True)) + sysarg += _(" Maybe you meant {command}?").format( + command=utils.list_to_string(suggestions, _("or"), addquote=True) + ) else: sysarg += _(' Type "help" for help.') raise ExecSystemCommand(syscmd, sysarg) diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index 4a746df33e..b2eb95c886 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -184,7 +184,9 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): raise exc.with_traceback(tb) else: # try next suggested path - errstring += _("\n(Unsuccessfully tried '{path}').").format(path=python_path) + errstring += _("\n(Unsuccessfully tried '{path}').").format( + path=python_path + ) continue try: cmdsetclass = getattr(module, classname) @@ -194,7 +196,9 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): dum, dum, tb = sys.exc_info() raise exc.with_traceback(tb) else: - errstring += _("\n(Unsuccessfully tried '{path}').").format(path=python_path) + errstring += _("\n(Unsuccessfully tried '{path}').").format( + path=python_path + ) continue _CACHED_CMDSETS[python_path] = cmdsetclass diff --git a/evennia/help/manager.py b/evennia/help/manager.py index 646758d202..398312a203 100644 --- a/evennia/help/manager.py +++ b/evennia/help/manager.py @@ -131,7 +131,9 @@ class HelpEntryManager(TypedObjectManager): for topic in topics: topic.help_category = default_category topic.save() - string = _("Help database moved to category {default_category}").format(default_category=default_category) + string = _("Help database moved to category {default_category}").format( + default_category=default_category + ) logger.log_info(string) def search_help(self, ostring, help_category=None): diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index 0f96b1d6a7..d2385a0aa7 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -246,7 +246,11 @@ class LockHandler(object): evalstring = " ".join(_RE_OK.findall(evalstring)) eval(evalstring % tuple(True for func in funclist), {}, {}) except Exception: - elist.append(_("Lock: definition '{lock_string}' has syntax errors.").format(lock_string=raw_lockstring)) + elist.append( + _("Lock: definition '{lock_string}' has syntax errors.").format( + lock_string=raw_lockstring + ) + ) continue if access_type in locks: duplicates += 1 diff --git a/evennia/objects/admin.py b/evennia/objects/admin.py index 5e18e8e0a4..59a1d85c68 100644 --- a/evennia/objects/admin.py +++ b/evennia/objects/admin.py @@ -10,6 +10,7 @@ from evennia.objects.models import ObjectDB from django.contrib.admin.utils import flatten_fieldsets from django.utils.translation import gettext as _ + class ObjectAttributeInline(AttributeInline): """ Defines inline descriptions of Attributes (experimental) @@ -61,7 +62,7 @@ class ObjectCreateForm(forms.ModelForm): required=False, widget=forms.TextInput(attrs={"size": "78"}), help_text="Most non-character objects don't need a cmdset" - " and can leave this field blank." + " and can leave this field blank.", ) raw_id_fields = ("db_destination", "db_location", "db_home") diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index e16a9b6910..d883671d7f 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -261,6 +261,7 @@ if TELNET_ENABLED: # Start telnet game connections from evennia.server.portal import telnet + _telnet_protocol = class_from_module(settings.TELNET_PROTOCOL_CLASS) for interface in TELNET_INTERFACES: @@ -285,6 +286,7 @@ if SSL_ENABLED: # Start Telnet+SSL game connection (requires PyOpenSSL). from evennia.server.portal import telnet_ssl + _ssl_protocol = class_from_module(settings.SSL_PROTOCOL_CLASS) for interface in SSL_INTERFACES: @@ -319,6 +321,7 @@ if SSH_ENABLED: # evennia/game if necessary. from evennia.server.portal import ssh + _ssh_protocol = class_from_module(settings.SSH_PROTOCOL_CLASS) for interface in SSH_INTERFACES: @@ -328,11 +331,7 @@ if SSH_ENABLED: for port in SSH_PORTS: pstring = "%s:%s" % (ifacestr, port) factory = ssh.makeFactory( - { - "protocolFactory": _ssh_protocol, - "protocolArgs": (), - "sessions": PORTAL_SESSIONS, - } + {"protocolFactory": _ssh_protocol, "protocolArgs": (), "sessions": PORTAL_SESSIONS,} ) factory.noisy = False ssh_service = internet.TCPServer(port, factory, interface=interface) diff --git a/evennia/server/portal/ssh.py b/evennia/server/portal/ssh.py index 73d9773583..19de7153da 100644 --- a/evennia/server/portal/ssh.py +++ b/evennia/server/portal/ssh.py @@ -468,7 +468,11 @@ def getKeyPair(pubkeyfile, privkeyfile): from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa - rsa_key = Key(rsa.generate_private_key(public_exponent=65537, key_size=_KEY_LENGTH, backend=default_backend())) + rsa_key = Key( + rsa.generate_private_key( + public_exponent=65537, key_size=_KEY_LENGTH, backend=default_backend() + ) + ) public_key_string = rsa_key.public().toString(type="OPENSSH").decode() private_key_string = rsa_key.toString(type="OPENSSH").decode() diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 62ef3d7338..ba6f025fed 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -58,7 +58,6 @@ _HTTP_WARNING = bytes( _BASE_SESSION_CLASS = class_from_module(settings.BASE_SESSION_CLASS) - class TelnetServerFactory(protocol.ServerFactory): "This is only to name this better in logs" noisy = False diff --git a/evennia/server/session.py b/evennia/server/session.py index 1918c6998e..888b05ae45 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -101,7 +101,9 @@ class Session(object): the keys given by self._attrs_to_sync. """ - return {attr: getattr(self, attr) for attr in settings.SESSION_SYNC_ATTRS if hasattr(self, attr)} + return { + attr: getattr(self, attr) for attr in settings.SESSION_SYNC_ATTRS if hasattr(self, attr) + } def load_sync_data(self, sessdata): """ diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index e315dc7a82..1a38d44279 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -23,7 +23,7 @@ from evennia.utils.utils import ( make_iter, delay, callables_from_module, - class_from_module + class_from_module, ) from evennia.server.portal import amp from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 67f6331e9c..28bb6cb97e 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -972,7 +972,7 @@ REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 25, # require logged in users to call API so that access checks can work on them - "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated", ], + "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated",], # These are the different ways people can authenticate for API requests - via # session or with user/password. Other ways are possible, such as via tokens # or oauth, but require additional dependencies. @@ -1040,28 +1040,28 @@ PORTAL_SESSION_HANDLER_CLASS = "evennia.server.portal.portalsessionhandler.Porta # so the additions have somewhere to go. These must be simple things that # can be pickled - stuff you could serialize to JSON is best. SESSION_SYNC_ATTRS = ( - "protocol_key", - "address", - "suid", - "sessid", - "uid", - "csessid", - "uname", - "logged_in", - "puid", - "conn_time", - "cmd_last", - "cmd_last_visible", - "cmd_total", - "protocol_flags", - "server_data", - "cmdset_storage_string" - ) + "protocol_key", + "address", + "suid", + "sessid", + "uid", + "csessid", + "uname", + "logged_in", + "puid", + "conn_time", + "cmd_last", + "cmd_last_visible", + "cmd_total", + "protocol_flags", + "server_data", + "cmdset_storage_string", +) # The following are used for the communications between the Portal and Server. # Very dragons territory. -AMP_SERVER_PROTOCOL_CLASS = 'evennia.server.portal.amp_server.AMPServerProtocol' -AMP_CLIENT_PROTOCOL_CLASS = 'evennia.server.amp_client.AMPServerClientProtocol' +AMP_SERVER_PROTOCOL_CLASS = "evennia.server.portal.amp_server.AMPServerProtocol" +AMP_CLIENT_PROTOCOL_CLASS = "evennia.server.amp_client.AMPServerClientProtocol" ###################################################################### diff --git a/evennia/typeclasses/managers.py b/evennia/typeclasses/managers.py index 4b83160e4c..fe2ffef3b2 100644 --- a/evennia/typeclasses/managers.py +++ b/evennia/typeclasses/managers.py @@ -32,7 +32,6 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): # Attribute manager methods def get_attribute( self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None, **kwargs - ): """ Return Attribute objects by key, by category, by value, by @@ -104,8 +103,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): key=key, category=category, value=value, strvalue=strvalue, obj=obj ) - def get_by_attribute(self, key=None, category=None, value=None, - strvalue=None, attrtype=None, **kwargs): + def get_by_attribute( + self, key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs + ): """ Return objects having attributes with the given key, category, value, strvalue or combination of those criteria. diff --git a/evennia/utils/search.py b/evennia/utils/search.py index 45bdc86a87..c7231309ac 100644 --- a/evennia/utils/search.py +++ b/evennia/utils/search.py @@ -205,29 +205,33 @@ help_entries = search_help # not the attribute object itself (this is usually what you want) -def search_object_attribute(key=None, category=None, value=None, - strvalue=None, attrtype=None, **kwargs): +def search_object_attribute( + key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs +): return ObjectDB.objects.get_by_attribute( key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs ) -def search_account_attribute(key=None, category=None, value=None, - strvalue=None, attrtype=None, **kwargs): +def search_account_attribute( + key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs +): return AccountDB.objects.get_by_attribute( key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs ) -def search_script_attribute(key=None, category=None, value=None, - strvalue=None, attrtype=None, **kwargs): +def search_script_attribute( + key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs +): return ScriptDB.objects.get_by_attribute( key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs ) -def search_channel_attribute(key=None, category=None, value=None, - strvalue=None, attrtype=None, **kwargs): +def search_channel_attribute( + key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs +): return Channel.objects.get_by_attribute( key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs ) diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 5b6aa0708b..dd8d7a2855 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -2041,7 +2041,9 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs): if multimatch_string: error = "%s\n" % multimatch_string else: - error = _("More than one match for '{query}' (please narrow target):\n").format(query=query) + error = _("More than one match for '{query}' (please narrow target):\n").format( + query=query + ) for num, result in enumerate(matches): # we need to consider Commands, where .aliases is a list diff --git a/evennia/utils/validatorfuncs.py b/evennia/utils/validatorfuncs.py index e49bb716a8..3443c47d5c 100644 --- a/evennia/utils/validatorfuncs.py +++ b/evennia/utils/validatorfuncs.py @@ -67,7 +67,11 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs) try: from_tz = _pytz.timezone(acct_tz) except Exception as err: - raise ValueError(_("Timezone string '{acct_tz}' is not a valid timezone ({err})").format(acct_tz=acct_tz, err=err)) + raise ValueError( + _("Timezone string '{acct_tz}' is not a valid timezone ({err})").format( + acct_tz=acct_tz, err=err + ) + ) else: from_tz = _pytz.UTC From 8512364560ef8e59af068e5a9eb8f77cc289da9a Mon Sep 17 00:00:00 2001 From: David Estrada Date: Mon, 20 Apr 2020 18:36:08 -0700 Subject: [PATCH 22/32] Some of the doc strings were incorrect in manager.py. Manager.py methods should return querySets, not lists --- evennia/objects/manager.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index f66c1fe253..3e609991ff 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -159,7 +159,7 @@ class ObjectDBManager(TypedObjectManager): typeclasses (list, optional): Python pats to restrict matches with. Returns: - matches (list): Objects fullfilling both the `attribute_name` and + matches (query): Objects fullfilling both the `attribute_name` and `attribute_value` criterions. Notes: @@ -178,11 +178,11 @@ class ObjectDBManager(TypedObjectManager): # This doesn't work if attribute_value is an object. Workaround below if isinstance(attribute_value, (str, int, float, bool)): - return list(self.filter( + return self.filter( cand_restriction & type_restriction & Q(db_attributes__db_key=attribute_name, db_attributes__db_value=attribute_value) - ).order_by("id")) + ).order_by("id") else: # We must loop for safety since the referenced lookup gives deepcopy error if attribute value is an object. global _ATTR @@ -273,12 +273,12 @@ class ObjectDBManager(TypedObjectManager): to exclude from the match. Returns: - contents (list): Matching contents, without excludeobj, if given. + contents (query): Matching contents, without excludeobj, if given. """ exclude_restriction = ( Q(pk__in=[_GA(obj, "id") for obj in make_iter(excludeobj)]) if excludeobj else Q() ) - return list(self.filter(db_location=location).exclude(exclude_restriction).order_by("id")) + return self.filter(db_location=location).exclude(exclude_restriction).order_by("id") def get_objs_with_key_or_alias(self, ostring, exact=True, candidates=None, typeclasses=None): """ @@ -291,7 +291,7 @@ class ObjectDBManager(TypedObjectManager): typeclasses (list): Only match objects with typeclasses having thess path strings. Returns: - matches (list): A list of matches of length 0, 1 or more. + matches (query): A list of matches of length 0, 1 or more. """ if not isinstance(ostring, str): if hasattr(ostring, "key"): @@ -309,7 +309,7 @@ class ObjectDBManager(TypedObjectManager): type_restriction = typeclasses and Q(db_typeclass_path__in=make_iter(typeclasses)) or Q() if exact: # exact match - do direct search - return list( + return ( ( self.filter( cand_restriction From 508052ed2e63cf5990cbd1001abe47c831c0b339 Mon Sep 17 00:00:00 2001 From: David Estrada Date: Mon, 20 Apr 2020 18:36:49 -0700 Subject: [PATCH 23/32] Send Search Results through _AT_SEARCH_RESULT in DefaultObject class --- evennia/objects/objects.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index de9d09abae..b5ec9a7647 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -498,12 +498,11 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): use_dbref=use_dbref, ) - if quiet: - return results return _AT_SEARCH_RESULT( results, self, query=searchdata, + quiet=quiet, nofound_string=nofound_string, multimatch_string=multimatch_string, ) From 8e294e68ffffa6f5601e13b605a6f261303c5ef0 Mon Sep 17 00:00:00 2001 From: David Estrada Date: Mon, 20 Apr 2020 18:59:55 -0700 Subject: [PATCH 24/32] Seems DefaultObject search should return a list, even if _AT_SEARCH_RESULT returns None --- evennia/objects/objects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index b5ec9a7647..c51b8079ba 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -498,7 +498,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): use_dbref=use_dbref, ) - return _AT_SEARCH_RESULT( + matches = _AT_SEARCH_RESULT( results, self, query=searchdata, @@ -506,6 +506,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): nofound_string=nofound_string, multimatch_string=multimatch_string, ) + if matches: + return matches + return [] def search_account(self, searchdata, quiet=False): """ From 638bb2f4e7cdd27630d2c01d145635d2fd94e695 Mon Sep 17 00:00:00 2001 From: David Estrada Date: Mon, 20 Apr 2020 19:20:50 -0700 Subject: [PATCH 25/32] Misunderstood what the quiet flag was doing. If quiet flag is set, DefaultObject will return a list --- evennia/objects/objects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index c51b8079ba..d9d355172a 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -498,7 +498,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): use_dbref=use_dbref, ) - matches = _AT_SEARCH_RESULT( + if quiet: + return list(results) + + return _AT_SEARCH_RESULT( results, self, query=searchdata, @@ -506,9 +509,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): nofound_string=nofound_string, multimatch_string=multimatch_string, ) - if matches: - return matches - return [] def search_account(self, searchdata, quiet=False): """ From 2c8603bdc12fccbe4cee8d20878648af3c4961f2 Mon Sep 17 00:00:00 2001 From: David Estrada Date: Mon, 20 Apr 2020 19:23:04 -0700 Subject: [PATCH 26/32] No longer passing quiet argument to _AT_SEARCH_RESULT. Took out empty line --- evennia/objects/objects.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index d9d355172a..2d093f15ab 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -500,12 +500,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): if quiet: return list(results) - return _AT_SEARCH_RESULT( results, self, query=searchdata, - quiet=quiet, nofound_string=nofound_string, multimatch_string=multimatch_string, ) From 43ce2285a51f207f31f093ed0c77f8ed5afa6fac Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Mon, 13 Apr 2020 15:48:38 -0700 Subject: [PATCH 27/32] Completely refactored the Attribute Refactor into a single fresh branch. --- evennia/accounts/accounts.py | 4 +- evennia/objects/objects.py | 4 +- evennia/server/serversession.py | 128 +--- evennia/typeclasses/attributes.py | 1076 +++++++++++++++++++---------- evennia/typeclasses/models.py | 34 +- evennia/typeclasses/tests.py | 4 +- 6 files changed, 733 insertions(+), 517 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 49d3f550cc..305b841956 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -32,7 +32,7 @@ from evennia.server.signals import ( SIGNAL_OBJECT_POST_PUPPET, SIGNAL_OBJECT_POST_UNPUPPET, ) -from evennia.typeclasses.attributes import NickHandler +from evennia.typeclasses.attributes import NickHandler, ModelAttributeBackend from evennia.scripts.scripthandler import ScriptHandler from evennia.commands.cmdsethandler import CmdSetHandler from evennia.utils.optionhandler import OptionHandler @@ -199,7 +199,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): @lazy_property def nicks(self): - return NickHandler(self) + return NickHandler(self, ModelAttributeBackend) @lazy_property def sessions(self): diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 8ee0d5bfa4..e95481773d 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -12,7 +12,7 @@ from collections import defaultdict from django.conf import settings from evennia.typeclasses.models import TypeclassBase -from evennia.typeclasses.attributes import NickHandler +from evennia.typeclasses.attributes import NickHandler, ModelAttributeBackend from evennia.objects.manager import ObjectManager from evennia.objects.models import ObjectDB from evennia.scripts.scripthandler import ScriptHandler @@ -225,7 +225,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): @lazy_property def nicks(self): - return NickHandler(self) + return NickHandler(self, ModelAttributeBackend) @lazy_property def sessions(self): diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index 2443758b12..15ffa7a42b 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -6,7 +6,6 @@ connection actually happens (so it's the same for telnet, web, ssh etc). It is stored on the Server side (as opposed to protocol-specific sessions which are stored on the Portal side) """ -import weakref import time from django.utils import timezone from django.conf import settings @@ -16,6 +15,7 @@ from evennia.utils.utils import make_iter, lazy_property from evennia.commands.cmdsethandler import CmdSetHandler from evennia.server.session import Session from evennia.scripts.monitorhandler import MONITOR_HANDLER +from evennia.typeclasses.attributes import AttributeHandler, InMemoryAttributeBackend, DbHolder _GA = object.__getattribute__ _SA = object.__setattr__ @@ -25,124 +25,6 @@ _ANSI = None # i18n from django.utils.translation import gettext as _ -# Handlers for Session.db/ndb operation - - -class NDbHolder(object): - """Holder for allowing property access of attributes""" - - def __init__(self, obj, name, manager_name="attributes"): - _SA(self, name, _GA(obj, manager_name)) - _SA(self, "name", name) - - def __getattribute__(self, attrname): - if attrname == "all": - # we allow to overload our default .all - attr = _GA(self, _GA(self, "name")).get("all") - return attr if attr else _GA(self, "all") - return _GA(self, _GA(self, "name")).get(attrname) - - def __setattr__(self, attrname, value): - _GA(self, _GA(self, "name")).add(attrname, value) - - def __delattr__(self, attrname): - _GA(self, _GA(self, "name")).remove(attrname) - - def get_all(self): - return _GA(self, _GA(self, "name")).all() - - all = property(get_all) - - -class NAttributeHandler(object): - """ - NAttributeHandler version without recache protection. - This stand-alone handler manages non-database saving. - It is similar to `AttributeHandler` and is used - by the `.ndb` handler in the same way as `.db` does - for the `AttributeHandler`. - """ - - def __init__(self, obj): - """ - Initialized on the object - """ - self._store = {} - self.obj = weakref.proxy(obj) - - def has(self, key): - """ - Check if object has this attribute or not. - - Args: - key (str): The Nattribute key to check. - - Returns: - has_nattribute (bool): If Nattribute is set or not. - - """ - return key in self._store - - def get(self, key, default=None): - """ - Get the named key value. - - Args: - key (str): The Nattribute key to get. - - Returns: - the value of the Nattribute. - - """ - return self._store.get(key, default) - - def add(self, key, value): - """ - Add new key and value. - - Args: - key (str): The name of Nattribute to add. - value (any): The value to store. - - """ - self._store[key] = value - - def remove(self, key): - """ - Remove Nattribute from storage. - - Args: - key (str): The name of the Nattribute to remove. - - """ - if key in self._store: - del self._store[key] - - def clear(self): - """ - Remove all NAttributes from handler. - - """ - self._store = {} - - def all(self, return_tuples=False): - """ - List the contents of the handler. - - Args: - return_tuples (bool, optional): Defines if the Nattributes - are returns as a list of keys or as a list of `(key, value)`. - - Returns: - nattributes (list): A list of keys `[key, key, ...]` or a - list of tuples `[(key, value), ...]` depending on the - setting of `return_tuples`. - - """ - if return_tuples: - return [(key, value) for (key, value) in self._store.items() if not key.startswith("_")] - return [key for key in self._store if not key.startswith("_")] - # ------------------------------------------------------------- # Server Session @@ -175,6 +57,10 @@ class ServerSession(Session): cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set) + @property + def id(self): + return self.sessid + def at_sync(self): """ This is called whenever a session has been resynced with the @@ -490,7 +376,7 @@ class ServerSession(Session): @lazy_property def nattributes(self): - return NAttributeHandler(self) + return AttributeHandler(self, InMemoryAttributeBackend) @lazy_property def attributes(self): @@ -508,7 +394,7 @@ class ServerSession(Session): try: return self._ndb_holder except AttributeError: - self._ndb_holder = NDbHolder(self, "nattrhandler", manager_name="nattributes") + self._ndb_holder = DbHolder(self, "nattrhandler", manager_name="nattributes") return self._ndb_holder # @ndb.setter diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 6fb4870dae..001cee929d 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -12,6 +12,8 @@ import re import fnmatch import weakref +from collections import defaultdict + from django.db import models from django.conf import settings from django.utils.encoding import smart_str @@ -31,7 +33,7 @@ _TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE # ------------------------------------------------------------- -class Attribute(SharedMemoryModel): +class IAttribute: """ Attributes are things that are specific to different types of objects. For example, a drink container needs to store its fill level, whereas an exit @@ -53,8 +55,113 @@ class Attribute(SharedMemoryModel): - category (str): Optional character string for grouping the Attribute. + This class is an API/Interface/Abstract base class; do not instantiate it directly. """ + @lazy_property + def locks(self): + return LockHandler(self) + + key = property(lambda self: self.db_key) + strvalue = property(lambda self: self.db_strvalue) + category = property(lambda self: self.db_category) + model = property(lambda self: self.db_model) + attrtype = property(lambda self: self.db_attrtype) + date_created = property(lambda self: self.db_date_created) + + def __lock_storage_get(self): + return self.db_lock_storage + + def __lock_storage_set(self, value): + self.db_lock_storage = value + + def __lock_storage_del(self): + self.db_lock_storage = "" + + lock_storage = property(__lock_storage_get, __lock_storage_set, __lock_storage_del) + + def access(self, accessing_obj, access_type="read", default=False, **kwargs): + """ + Determines if another object has permission to access. + + Args: + accessing_obj (object): Entity trying to access this one. + access_type (str, optional): Type of access sought, see + the lock documentation. + default (bool, optional): What result to return if no lock + of access_type was found. The default, `False`, means a lockdown + policy, only allowing explicit access. + kwargs (any, optional): Not used; here to make the API consistent with + other access calls. + + Returns: + result (bool): If the lock was passed or not. + + """ + result = self.locks.check(accessing_obj, access_type=access_type, default=default) + return result + + # + # + # Attribute methods + # + # + + def __str__(self): + return smart_str("%s(%s)" % (self.db_key, self.id)) + + def __repr__(self): + return "%s(%s)" % (self.db_key, self.id) + + +class InMemoryAttribute(IAttribute): + """ + This Attribute is used purely for NAttributes/NAttributeHandler. It has no database backend. + """ + + # Primary Key has no meaning for an InMemoryAttribute. This merely serves to satisfy other code. + + def __init__(self, pk, **kwargs): + """ + Create an Attribute that exists only in Memory. + + Args: + pk (int): This is a fake 'primary key' / id-field. It doesn't actually have to be unique, but is fed an + incrementing number from the InMemoryBackend by default. This is needed only so Attributes can be + sorted. Some parts of the API also see the lack of a .pk field as a sign that the Attribute was + deleted. + **kwargs: Other keyword arguments are used to construct the actual Attribute. + """ + self.id = pk + self.pk = pk + + # Copy all kwargs to local properties. We use db_ for compatability here. + for key, value in kwargs.items(): + # Value and locks are special. We must call the wrappers. + if key == "value": + self.value = value + elif key == "lock_storage": + self.lock_storage = value + else: + setattr(self, f"db_{key}", value) + + # value property (wraps db_value) + def __value_get(self): + return self.db_value + + def __value_set(self, new_value): + self.db_value = new_value + + def __value_del(self): + pass + + value = property(__value_get, __value_set, __value_del) + + +class Attribute(IAttribute, SharedMemoryModel): + """ + This attribute is stored via Django. Most Attributes will be using this class. + """ # # Attribute Database Model setup # @@ -109,35 +216,10 @@ class Attribute(SharedMemoryModel): # Database manager # objects = managers.AttributeManager() - @lazy_property - def locks(self): - return LockHandler(self) - class Meta(object): "Define Django meta options" verbose_name = "Evennia Attribute" - # read-only wrappers - key = property(lambda self: self.db_key) - strvalue = property(lambda self: self.db_strvalue) - category = property(lambda self: self.db_category) - model = property(lambda self: self.db_model) - attrtype = property(lambda self: self.db_attrtype) - date_created = property(lambda self: self.db_date_created) - - def __lock_storage_get(self): - return self.db_lock_storage - - def __lock_storage_set(self, value): - self.db_lock_storage = value - self.save(update_fields=["db_lock_storage"]) - - def __lock_storage_del(self): - self.db_lock_storage = "" - self.save(update_fields=["db_lock_storage"]) - - lock_storage = property(__lock_storage_get, __lock_storage_set, __lock_storage_del) - # Wrapper properties to easily set database fields. These are # @property decorators that allows to access these fields using # normal python operations (without having to remember to save() @@ -146,6 +228,20 @@ class Attribute(SharedMemoryModel): # value = self.attr and del self.attr respectively (where self # is the object in question). + # lock_storage wrapper. Overloaded for saving to database. + def __lock_storage_get(self): + return self.db_lock_storage + + def __lock_storage_set(self, value): + super().__lock_storage_set(value) + self.save(update_fields=["db_lock_storage"]) + + def __lock_storage_del(self): + super().__lock_storage_del() + self.save(update_fields=["db_lock_storage"]) + + lock_storage = property(__lock_storage_get, __lock_storage_set, __lock_storage_del) + # value property (wraps db_value) # @property def __value_get(self): @@ -164,7 +260,6 @@ class Attribute(SharedMemoryModel): see self.__value_get. """ self.db_value = to_pickle(new_value) - # print("value_set, self.db_value:", repr(self.db_value)) # DEBUG self.save(update_fields=["db_value"]) # @value.deleter @@ -174,98 +269,147 @@ class Attribute(SharedMemoryModel): value = property(__value_get, __value_set, __value_del) - # - # - # Attribute methods - # - # - - def __str__(self): - return smart_str("%s(%s)" % (self.db_key, self.id)) - - def __repr__(self): - return "%s(%s)" % (self.db_key, self.id) - - def access(self, accessing_obj, access_type="read", default=False, **kwargs): - """ - Determines if another object has permission to access. - - Args: - accessing_obj (object): Entity trying to access this one. - access_type (str, optional): Type of access sought, see - the lock documentation. - default (bool, optional): What result to return if no lock - of access_type was found. The default, `False`, means a lockdown - policy, only allowing explicit access. - kwargs (any, optional): Not used; here to make the API consistent with - other access calls. - - Returns: - result (bool): If the lock was passed or not. - - """ - result = self.locks.check(accessing_obj, access_type=access_type, default=default) - return result - - # # Handlers making use of the Attribute model # - -class AttributeHandler(object): +class IAttributeBackend: """ - Handler for adding Attributes to the object. + Abstract interface for the backends used by the Attribute Handler. + + All Backends must implement this base class. """ - _m2m_fieldname = "db_attributes" _attrcreate = "attrcreate" _attredit = "attredit" _attrread = "attrread" - _attrtype = None + _attrclass = None - def __init__(self, obj): - """Initialize handler.""" - self.obj = obj - self._objid = obj.id - self._model = to_str(obj.__dbclass__.__name__.lower()) + def __init__(self, handler, attrtype): + self.handler = handler + self.obj = handler.obj + self._attrtype = attrtype + self._objid = handler.obj.id self._cache = {} # store category names fully cached self._catcache = {} # full cache was run on all attributes self._cache_complete = False - def _query_all(self): - "Fetch all Attributes on this object" - query = { - "%s__id" % self._model: self._objid, - "attribute__db_model__iexact": self._model, - "attribute__db_attrtype": self._attrtype, - } - return [ - conn.attribute - for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query) - ] + def query_all(self): + """ + Fetch all Attributes from this object. - def _fullcache(self): + Returns: + attrlist (list): A list of Attribute objects. + """ + raise NotImplementedError() + + def query_key(self, key, category): + """ + + Args: + key (str): The key of the Attribute being searched for. + category (str or None): The category of the desired Attribute. + + Returns: + attribute (IAttribute): A single Attribute. + """ + raise NotImplementedError() + + def query_category(self, category): + """ + Returns every matching Attribute as a list, given a category. + + This method calls up whatever storage the backend uses. + + Args: + category (str or None): The category to query. + + Returns: + attrs (list): The discovered Attributes. + """ + raise NotImplementedError() + + def _full_cache(self): """Cache all attributes of this object""" if not _TYPECLASS_AGGRESSIVE_CACHE: return - attrs = self._query_all() - self._cache = dict( - ( - "%s-%s" - % ( - to_str(attr.db_key).lower(), - attr.db_category.lower() if attr.db_category else None, - ), - attr, - ) - for attr in attrs - ) + attrs = self.query_all() + self._cache = {f"{to_str(attr.key).lower()}-{attr.category.lower() if attr.category else None}": attr + for attr in attrs} self._cache_complete = True - def _getcache(self, key=None, category=None): + def _get_cache_key(self, key, category): + """ + + + Args: + key (str): The key of the Attribute being searched for. + category (str or None): The category of the desired Attribute. + + Returns: + attribute (IAttribute): A single Attribute. + """ + cachekey = "%s-%s" % (key, category) + cachefound = False + try: + attr = _TYPECLASS_AGGRESSIVE_CACHE and self._cache[cachekey] + cachefound = True + except KeyError: + attr = None + + if attr and (not hasattr(attr, "pk") and attr.pk is None): + # clear out Attributes deleted from elsewhere. We must search this anew. + attr = None + cachefound = False + del self._cache[cachekey] + if cachefound and _TYPECLASS_AGGRESSIVE_CACHE: + if attr: + return [attr] # return cached entity + else: + return [] # no such attribute: return an empty list + else: + conn = self.query_key(key, category) + if conn: + attr = conn[0].attribute + if _TYPECLASS_AGGRESSIVE_CACHE: + self._cache[cachekey] = attr + return [attr] if attr.pk else [] + else: + # There is no such attribute. We will explicitly save that + # in our cache to avoid firing another query if we try to + # retrieve that (non-existent) attribute again. + if _TYPECLASS_AGGRESSIVE_CACHE: + self._cache[cachekey] = None + return [] + + def _get_cache_category(self, category): + """ + Retrieves Attribute list (by category) from cache. + + Args: + category (str or None): The category to query. + + Returns: + attrs (list): The discovered Attributes. + """ + catkey = "-%s" % category + if _TYPECLASS_AGGRESSIVE_CACHE and catkey in self._catcache: + return [attr for key, attr in self._cache.items() if key.endswith(catkey) and attr] + else: + # we have to query to make this category up-date in the cache + attrs = self.query_category(category) + if _TYPECLASS_AGGRESSIVE_CACHE: + for attr in attrs: + if attr.pk: + cachekey = "%s-%s" % (attr.key, category) + self._cache[cachekey] = attr + # mark category cache as up-to-date + self._catcache[catkey] = True + return attrs + + def _get_cache(self, key=None, category=None): """ Retrieve from cache or database (always caches) @@ -291,85 +435,31 @@ class AttributeHandler(object): key = key.strip().lower() if key else None category = category.strip().lower() if category else None if key: - cachekey = "%s-%s" % (key, category) - cachefound = False - try: - attr = _TYPECLASS_AGGRESSIVE_CACHE and self._cache[cachekey] - cachefound = True - except KeyError: - attr = None + return self._get_cache_key(key, category) + return self._get_cache_category(category) - if attr and (not hasattr(attr, "pk") and attr.pk is None): - # clear out Attributes deleted from elsewhere. We must search this anew. - attr = None - cachefound = False - del self._cache[cachekey] - if cachefound and _TYPECLASS_AGGRESSIVE_CACHE: - if attr: - return [attr] # return cached entity - else: - return [] # no such attribute: return an empty list - else: - query = { - "%s__id" % self._model: self._objid, - "attribute__db_model__iexact": self._model, - "attribute__db_attrtype": self._attrtype, - "attribute__db_key__iexact": key.lower(), - "attribute__db_category__iexact": category.lower() if category else None, - } - if not self.obj.pk: - return [] - conn = getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query) - if conn: - attr = conn[0].attribute - if _TYPECLASS_AGGRESSIVE_CACHE: - self._cache[cachekey] = attr - return [attr] if attr.pk else [] - else: - # There is no such attribute. We will explicitly save that - # in our cache to avoid firing another query if we try to - # retrieve that (non-existent) attribute again. - if _TYPECLASS_AGGRESSIVE_CACHE: - self._cache[cachekey] = None - return [] - else: - # only category given (even if it's None) - we can't - # assume the cache to be complete unless we have queried - # for this category before - catkey = "-%s" % category - if _TYPECLASS_AGGRESSIVE_CACHE and catkey in self._catcache: - return [attr for key, attr in self._cache.items() if key.endswith(catkey) and attr] - else: - # we have to query to make this category up-date in the cache - query = { - "%s__id" % self._model: self._objid, - "attribute__db_model__iexact": self._model, - "attribute__db_attrtype": self._attrtype, - "attribute__db_category__iexact": category.lower() if category else None, - } - attrs = [ - conn.attribute - for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter( - **query - ) - ] - if _TYPECLASS_AGGRESSIVE_CACHE: - for attr in attrs: - if attr.pk: - cachekey = "%s-%s" % (attr.db_key, category) - self._cache[cachekey] = attr - # mark category cache as up-to-date - self._catcache[catkey] = True - return attrs + def get(self, key=None, category=None): + """ + Frontend for .get_cache. Retrieves Attribute(s). - def _setcache(self, key, category, attr_obj): + Args: + key (str, optional): Attribute key to query for + category (str, optional): Attribiute category + + Returns: + args (list): Returns a list of zero or more matches + found from cache or database. + """ + return self._get_cache(key, category) + + def _set_cache(self, key, category, attr_obj): """ Update cache. Args: key (str): A cleaned key string category (str or None): A cleaned category name - attr_obj (Attribute): The newly saved attribute + attr_obj (IAttribute): The newly saved attribute """ if not _TYPECLASS_AGGRESSIVE_CACHE: @@ -383,7 +473,7 @@ class AttributeHandler(object): self._catcache.pop(catkey, None) self._cache_complete = False - def _delcache(self, key, category): + def _delete_cache(self, key, category): """ Remove attribute from cache @@ -414,6 +504,419 @@ class AttributeHandler(object): self._cache = {} self._catcache = {} + def do_create_attribute(self, key, category, lockstring, value, strvalue): + """ + Does the hard work of actually creating Attributes, whatever is needed. + + Args: + key (str): The Attribute's key. + category (str or None): The Attribute's category, or None + lockstring (str): Any locks for the Attribute. + value (obj): The Value of the Attribute. + strvalue (bool): Signifies if this is a strvalue Attribute. Value MUST be a string or + this will lead to Trouble. Ignored for InMemory attributes. + + Returns: + attr (IAttribute): The new Attribute. + """ + raise NotImplementedError() + + def create_attribute(self, key, category, lockstring, value, strvalue=False, cache=True): + """ + Creates Attribute (using the class specified for the backend), (optionally) caches it, and returns it. + + This MUST actively save the Attribute to whatever database backend is used, AND + call self.set_cache(key, category, new_attrobj) + + Args: + key (str): The Attribute's key. + category (str or None): The Attribute's category, or None + lockstring (str): Any locks for the Attribute. + value (obj): The Value of the Attribute. + strvalue (bool): Signifies if this is a strvalue Attribute. Value MUST be a string or + this will lead to Trouble. Ignored for InMemory attributes. + cache (bool): Whether to cache the new Attribute + + Returns: + attr (IAttribute): The new Attribute. + """ + attr = self.do_create_attribute(key, category, lockstring, value, strvalue) + if cache: + self._set_cache(key, category, attr) + return attr + + def do_update_attribute(self, attr, value): + """ + Simply sets a new Value to an Attribute. + + Args: + attr (IAttribute): The Attribute being changed. + value (obj): The Value for the Attribute. + + """ + raise NotImplementedError() + + def do_batch_update_attribute(self, attr_obj, category, lock_storage, new_value, strvalue): + """ + Called opnly by batch add. For the database backend, this is a method + of updating that can alter category and lock-storage. + + Args: + attr_obj (IAttribute): The Attribute being altered. + category (str or None): The attribute's (new) category. + lock_storage (str): The attribute's new locks. + new_value (obj): The Attribute's new value. + strvalue (bool): Signifies if this is a strvalue Attribute. Value MUST be a string or + this will lead to Trouble. Ignored for InMemory attributes. + """ + raise NotImplementedError() + + def do_batch_finish(self, attr_objs): + """ + Called only by batch_add. Used for handling database operations and/or caching complications. + + Args: + attr_objs (list of IAttribute): The Attributes created/updated thus far. + """ + raise NotImplementedError() + + def batch_add(self, *args, **kwargs): + """ + Batch-version of `add()`. This is more efficient than + repeat-calling add when having many Attributes to add. + + Args: + indata (list): List of tuples of varying length representing the + Attribute to add to this object. Supported tuples are + - `(key, value)` + - `(key, value, category)` + - `(key, value, category, lockstring)` + - `(key, value, category, lockstring, default_access)` + + Raises: + RuntimeError: If trying to pass a non-iterable as argument. + + Notes: + The indata tuple order matters, so if you want a lockstring + but no category, set the category to `None`. This method + does not have the ability to check editing permissions like + normal .add does, and is mainly used internally. It does not + use the normal self.add but apply the Attributes directly + to the database. + + """ + new_attrobjs = [] + strattr = kwargs.get("strattr", False) + for tup in args: + if not is_iter(tup) or len(tup) < 2: + raise RuntimeError("batch_add requires iterables as arguments (got %r)." % tup) + ntup = len(tup) + keystr = str(tup[0]).strip().lower() + new_value = tup[1] + category = str(tup[2]).strip().lower() if ntup > 2 and tup[2] is not None else None + lockstring = tup[3] if ntup > 3 else "" + + attr_objs = self._get_cache(keystr, category) + + if attr_objs: + attr_obj = attr_objs[0] + # update an existing attribute object + self.do_batch_update_attribute(attr_obj, category, lockstring, new_value, strattr) + else: + new_attr = self.do_create_attribute(keystr, category, lockstring, new_value, strvalue=strattr) + new_attrobjs.append(new_attr) + if new_attrobjs: + self.do_batch_finish(new_attrobjs) + + def do_delete_attribute(self, attr): + """ + Does the hard work of actually deleting things. + + Args: + attr (IAttribute): The attribute to delete. + """ + raise NotImplementedError() + + def delete_attribute(self, attr): + """ + Given an Attribute, deletes it. Also remove it from cache. + + Args: + attr (IAttribute): The attribute to delete. + """ + if not attr: + return + self._delete_cache(attr.key, attr.category) + self.do_delete_attribute(attr) + + def update_attribute(self, attr, value): + """ + Simply updates an Attribute. + + Args: + attr (IAttribute): The attribute to delete. + value (obj): The new value. + """ + self.do_update_attribute(attr, value) + + def do_batch_delete(self, attribute_list): + """ + Given a list of attributes, deletes them all. + The default implementation is fine, but this is overridable since some databases may allow + for a better method. + + Args: + attribute_list (list of IAttribute): + """ + for attribute in attribute_list: + self.delete_attribute(attribute) + + def clear_attributes(self, category, accessing_obj, default_access): + """ + Remove all Attributes on this object. + + Args: + category (str, optional): If given, clear only Attributes + of this category. + accessing_obj (object, optional): If given, check the + `attredit` lock on each Attribute before continuing. + default_access (bool, optional): Use this permission as + fallback if `access_obj` is given but there is no lock of + type `attredit` on the Attribute in question. + + """ + category = category.strip().lower() if category is not None else None + + if not self._cache_complete: + self._full_cache() + + if category is not None: + attrs = [attr for attr in self._cache.values() if attr.category == category] + else: + attrs = self._cache.values() + + if accessing_obj: + self.do_batch_delete([attr for attr in attrs if attr.access(accessing_obj, self._attredit, + default=default_access)]) + else: + # have to cast the results to a list or we'll get a RuntimeError for removing from the dict we're iterating + self.do_batch_delete(list(attrs)) + self.reset_cache() + + def get_all_attributes(self): + """ + Simply returns all Attributes of this object, sorted by their IDs. + + Returns: + attributes (list of IAttribute) + """ + if _TYPECLASS_AGGRESSIVE_CACHE: + if not self._cache_complete: + self._full_cache() + return sorted([attr for attr in self._cache.values() if attr], key=lambda o: o.id) + else: + return sorted([attr for attr in self.query_all() if attr], key=lambda o: o.id) + + +class InMemoryAttributeBackend(IAttributeBackend): + """ + This Backend for Attributes stores NOTHING in the database. Everything is kept in memory, and normally lost + on a crash, reload, shared memory flush, etc. It generates IDs for the Attributes it manages, but these are + of little importance beyond sorting and satisfying the caching logic to know an Attribute hasn't been + deleted out from under the cache's nose. + + """ + + _attrclass = InMemoryAttribute + + def __init__(self, handler, attrtype): + super().__init__(handler, attrtype) + self._storage = dict() + self._category_storage = defaultdict(list) + self._id_counter = 0 + + def _next_id(self): + """ + Increments the internal ID counter and returns the new value. + + Returns: + next_id (int): A simple integer. + """ + self._id_counter += 1 + return self._id_counter + + def query_all(self): + return self._storage.values() + + def query_key(self, key, category): + found = self._storage.get((key, category), None) + if found: + return [found] + return [] + + def query_category(self, category): + if category is None: + return self._storage.values() + return self._category_storage.get(category, []) + + def do_create_attribute(self, key, category, lockstring, value, strvalue): + """ + See parent class. + + strvalue has no meaning for InMemory attributes. + """ + new_attr = self._attrclass(pk=self._next_id(), key=key, category=category, lock_storage=lockstring, value=value) + self._storage[(key, category)] = new_attr + self._category_storage[category].append(new_attr) + return new_attr + + def do_update_attribute(self, attr, value): + attr.value = value + + def do_batch_update_attribute(self, attr_obj, category, lock_storage, new_value, strvalue): + """ + No need to bother saving anything. Just set some values. + """ + attr_obj.db_category = category + attr_obj.db_lock_storage = lock_storage if lock_storage else "" + attr_obj.value = new_value + + def do_batch_finish(self, attr_objs): + """ + Nothing to do here for In-Memory. + + Args: + attr_objs (list of IAttribute): The Attributes created/updated thus far. + """ + pass + + def do_delete_attribute(self, attr): + """ + Removes the Attribute from local storage. Once it's out of the cache, garbage collection will handle the rest. + + Args: + attr (IAttribute): The attribute to delete. + """ + del self._storage[(attr.key, attr.category)] + self._category_storage[attr.category].remove(attr) + + +class ModelAttributeBackend(IAttributeBackend): + """ + Uses Django models for storing Attributes. + """ + _attrclass = Attribute + _m2m_fieldname = "db_attributes" + + def __init__(self, handler, attrtype): + super().__init__(handler, attrtype) + self._model = to_str(handler.obj.__dbclass__.__name__.lower()) + + def query_all(self): + query = { + "%s__id" % self._model: self._objid, + "attribute__db_model__iexact": self._model, + "attribute__db_attrtype": self._attrtype, + } + return [ + conn.attribute + for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query) + ] + + def query_key(self, key, category): + query = { + "%s__id" % self._model: self._objid, + "attribute__db_model__iexact": self._model, + "attribute__db_attrtype": self._attrtype, + "attribute__db_key__iexact": key.lower(), + "attribute__db_category__iexact": category.lower() if category else None, + } + if not self.obj.pk: + return [] + return getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query) + + def query_category(self, category): + query = { + "%s__id" % self._model: self._objid, + "attribute__db_model__iexact": self._model, + "attribute__db_attrtype": self._attrtype, + "attribute__db_category__iexact": category.lower() if category else None, + } + return [ + conn.attribute + for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter( + **query + ) + ] + + def do_create_attribute(self, key, category, lockstring, value, strvalue): + kwargs = { + "db_key": key, + "db_category": category, + "db_model": self._model, + "db_lock_storage": lockstring if lockstring else "", + "db_attrtype": self._attrtype + } + if strvalue: + kwargs["db_value"] = None + kwargs["db_strvalue"] = value + else: + kwargs["db_value"] = to_pickle(value) + kwargs["db_strvalue"] = None + new_attr = self._attrclass(**kwargs) + new_attr.save() + getattr(self.obj, self._m2m_fieldname).add(new_attr) + self._set_cache(key, category, new_attr) + return new_attr + + def do_update_attribute(self, attr, value): + attr.value = value + + def do_batch_update_attribute(self, attr_obj, category, lock_storage, new_value, strvalue): + attr_obj.db_category = category + attr_obj.db_lock_storage = lock_storage if lock_storage else "" + if strvalue: + # store as a simple string (will not notify OOB handlers) + attr_obj.db_strvalue = new_value + attr_obj.value = None + else: + # store normally (this will also notify OOB handlers) + attr_obj.value = new_value + attr_obj.db_strvalue = None + attr_obj.save(update_fields=["db_strvalue", "db_value", "db_category", "db_lock_storage"]) + + def do_batch_finish(self, attr_objs): + # Add new objects to m2m field all at once + getattr(self.obj, self._m2m_fieldname).add(*attr_objs) + + def do_delete_attribute(self, attr): + try: + attr.delete() + except AssertionError: + # This could happen if the Attribute has already been deleted. + pass + + +class AttributeHandler: + """ + Handler for adding Attributes to the object. + """ + _attrcreate = "attrcreate" + _attredit = "attredit" + _attrread = "attrread" + _attrtype = None + + def __init__(self, obj, backend_class): + """ + Setup the AttributeHandler. + + Args: + obj (TypedObject): An Account, Object, Channel, ServerSession (not technically a typed object), etc. + backend_class (IAttributeBackend class): The class of the backend to use. + """ + self.obj = obj + self.backend = backend_class(self, self._attrtype) + def has(self, key=None, category=None): """ Checks if the given Attribute (or list of Attributes) exists on @@ -435,7 +938,7 @@ class AttributeHandler(object): category = category.strip().lower() if category is not None else None for keystr in make_iter(key): keystr = key.strip().lower() - ret.extend(bool(attr) for attr in self._getcache(keystr, category)) + ret.extend(bool(attr) for attr in self.backend.get(keystr, category)) return ret[0] if len(ret) == 1 else ret def get( @@ -493,7 +996,7 @@ class AttributeHandler(object): ret = [] for keystr in make_iter(key): # it's okay to send a None key - attr_objs = self._getcache(keystr, category) + attr_objs = self.backend.get(keystr, category) if attr_objs: ret.extend(attr_objs) elif raise_exception: @@ -559,33 +1062,15 @@ class AttributeHandler(object): category = category.strip().lower() if category is not None else None keystr = key.strip().lower() - attr_obj = self._getcache(key, category) + attr_obj = self.backend.get(key, category) if attr_obj: # update an existing attribute object attr_obj = attr_obj[0] - if strattr: - # store as a simple string (will not notify OOB handlers) - attr_obj.db_strvalue = value - attr_obj.save(update_fields=["db_strvalue"]) - else: - # store normally (this will also notify OOB handlers) - attr_obj.value = value + self.backend.update_attribute(attr_obj, value) else: # create a new Attribute (no OOB handlers can be notified) - kwargs = { - "db_key": keystr, - "db_category": category, - "db_model": self._model, - "db_attrtype": self._attrtype, - "db_value": None if strattr else to_pickle(value), - "db_strvalue": value if strattr else None, - } - new_attr = Attribute(**kwargs) - new_attr.save() - getattr(self.obj, self._m2m_fieldname).add(new_attr) - # update cache - self._setcache(keystr, category, new_attr) + self.backend.create_attribute(keystr, category, lockstring, value, strattr) def batch_add(self, *args, **kwargs): """ @@ -618,50 +1103,7 @@ class AttributeHandler(object): to the database. """ - new_attrobjs = [] - strattr = kwargs.get("strattr", False) - for tup in args: - if not is_iter(tup) or len(tup) < 2: - raise RuntimeError("batch_add requires iterables as arguments (got %r)." % tup) - ntup = len(tup) - keystr = str(tup[0]).strip().lower() - new_value = tup[1] - category = str(tup[2]).strip().lower() if ntup > 2 and tup[2] is not None else None - lockstring = tup[3] if ntup > 3 else "" - - attr_objs = self._getcache(keystr, category) - - if attr_objs: - attr_obj = attr_objs[0] - # update an existing attribute object - attr_obj.db_category = category - attr_obj.db_lock_storage = lockstring or "" - attr_obj.save(update_fields=["db_category", "db_lock_storage"]) - if strattr: - # store as a simple string (will not notify OOB handlers) - attr_obj.db_strvalue = new_value - attr_obj.save(update_fields=["db_strvalue"]) - else: - # store normally (this will also notify OOB handlers) - attr_obj.value = new_value - else: - # create a new Attribute (no OOB handlers can be notified) - kwargs = { - "db_key": keystr, - "db_category": category, - "db_model": self._model, - "db_attrtype": self._attrtype, - "db_value": None if strattr else to_pickle(new_value), - "db_strvalue": new_value if strattr else None, - "db_lock_storage": lockstring or "", - } - new_attr = Attribute(**kwargs) - new_attr.save() - new_attrobjs.append(new_attr) - self._setcache(keystr, category, new_attr) - if new_attrobjs: - # Add new objects to m2m field all at once - getattr(self.obj, self._m2m_fieldname).add(*new_attrobjs) + self.backend.batch_add(*args, **kwargs) def remove( self, @@ -710,20 +1152,13 @@ class AttributeHandler(object): for keystr in make_iter(key): keystr = keystr.lower() - attr_objs = self._getcache(keystr, category) + attr_objs = self.backend.get(keystr, category) for attr_obj in attr_objs: if not ( accessing_obj and not attr_obj.access(accessing_obj, self._attredit, default=default_access) ): - try: - attr_obj.delete() - except AssertionError: - print("Assertionerror for attr.delete()") - # this happens if the attr was already deleted - pass - finally: - self._delcache(keystr, category) + self.backend.delete_attribute(attr_obj) if not attr_objs and raise_exception: raise AttributeError @@ -741,27 +1176,7 @@ class AttributeHandler(object): type `attredit` on the Attribute in question. """ - category = category.strip().lower() if category is not None else None - - if not self._cache_complete: - self._fullcache() - - if category is not None: - attrs = [attr for attr in self._cache.values() if attr.category == category] - else: - attrs = self._cache.values() - - if accessing_obj: - [ - attr.delete() - for attr in attrs - if attr and attr.access(accessing_obj, self._attredit, default=default_access) - ] - else: - [attr.delete() for attr in attrs if attr and attr.pk] - self._cache = {} - self._catcache = {} - self._cache_complete = False + self.backend.clear_attributes(category, accessing_obj, default_access) def all(self, accessing_obj=None, default_access=True): """ @@ -780,12 +1195,7 @@ class AttributeHandler(object): their values!) in the handler. """ - if _TYPECLASS_AGGRESSIVE_CACHE: - if not self._cache_complete: - self._fullcache() - attrs = sorted([attr for attr in self._cache.values() if attr], key=lambda o: o.id) - else: - attrs = sorted([attr for attr in self._query_all() if attr], key=lambda o: o.id) + attrs = self.backend.get_all_attributes() if accessing_obj: return [ @@ -796,6 +1206,41 @@ class AttributeHandler(object): else: return attrs + def reset_cache(self): + self.backend.reset_cache() + + +# DbHolders for .db and .ndb properties on Typeclasses. + +_GA = object.__getattribute__ +_SA = object.__setattr__ + + +class DbHolder(object): + "Holder for allowing property access of attributes" + + def __init__(self, obj, name, manager_name="attributes"): + _SA(self, name, _GA(obj, manager_name)) + _SA(self, "name", name) + + def __getattribute__(self, attrname): + if attrname == "all": + # we allow to overload our default .all + attr = _GA(self, _GA(self, "name")).get("all") + return attr if attr else _GA(self, "all") + return _GA(self, _GA(self, "name")).get(attrname) + + def __setattr__(self, attrname, value): + _GA(self, _GA(self, "name")).add(attrname, value) + + def __delattr__(self, attrname): + _GA(self, _GA(self, "name")).remove(attrname) + + def get_all(self): + return _GA(self, _GA(self, "name")).get_all_attributes() + + all = property(get_all) + # Nick templating # @@ -1037,92 +1482,3 @@ class NickHandler(AttributeHandler): if is_match: break return raw_string - - -class NAttributeHandler(object): - """ - This stand-alone handler manages non-database saving. - It is similar to `AttributeHandler` and is used - by the `.ndb` handler in the same way as `.db` does - for the `AttributeHandler`. - """ - - def __init__(self, obj): - """ - Initialized on the object - """ - self._store = {} - self.obj = weakref.proxy(obj) - - def has(self, key): - """ - Check if object has this attribute or not. - - Args: - key (str): The Nattribute key to check. - - Returns: - has_nattribute (bool): If Nattribute is set or not. - - """ - return key in self._store - - def get(self, key): - """ - Get the named key value. - - Args: - key (str): The Nattribute key to get. - - Returns: - the value of the Nattribute. - - """ - return self._store.get(key, None) - - def add(self, key, value): - """ - Add new key and value. - - Args: - key (str): The name of Nattribute to add. - value (any): The value to store. - - """ - self._store[key] = value - - def remove(self, key): - """ - Remove Nattribute from storage. - - Args: - key (str): The name of the Nattribute to remove. - - """ - if key in self._store: - del self._store[key] - - def clear(self): - """ - Remove all NAttributes from handler. - - """ - self._store = {} - - def all(self, return_tuples=False): - """ - List the contents of the handler. - - Args: - return_tuples (bool, optional): Defines if the Nattributes - are returns as a list of keys or as a list of `(key, value)`. - - Returns: - nattributes (list): A list of keys `[key, key, ...]` or a - list of tuples `[(key, value), ...]` depending on the - setting of `return_tuples`. - - """ - if return_tuples: - return [(key, value) for (key, value) in self._store.items() if not key.startswith("_")] - return [key for key in self._store if not key.startswith("_")] diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index c6869b339f..84ec1dd408 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -36,7 +36,8 @@ from django.urls import reverse from django.utils.encoding import smart_str from django.utils.text import slugify -from evennia.typeclasses.attributes import Attribute, AttributeHandler, NAttributeHandler +from evennia.typeclasses.attributes import Attribute, AttributeHandler, ModelAttributeBackend, InMemoryAttributeBackend +from evennia.typeclasses.attributes import DbHolder from evennia.typeclasses.tags import Tag, TagHandler, AliasHandler, PermissionHandler from evennia.utils.idmapper.models import SharedMemoryModel, SharedMemoryModelBase @@ -121,33 +122,6 @@ class TypeclassBase(SharedMemoryModelBase): signals.pre_delete.connect(remove_attributes_on_delete, sender=new_class) return new_class - -class DbHolder(object): - "Holder for allowing property access of attributes" - - def __init__(self, obj, name, manager_name="attributes"): - _SA(self, name, _GA(obj, manager_name)) - _SA(self, "name", name) - - def __getattribute__(self, attrname): - if attrname == "all": - # we allow to overload our default .all - attr = _GA(self, _GA(self, "name")).get("all") - return attr if attr else _GA(self, "all") - return _GA(self, _GA(self, "name")).get(attrname) - - def __setattr__(self, attrname, value): - _GA(self, _GA(self, "name")).add(attrname, value) - - def __delattr__(self, attrname): - _GA(self, _GA(self, "name")).remove(attrname) - - def get_all(self): - return _GA(self, _GA(self, "name")).all() - - all = property(get_all) - - # # Main TypedObject abstraction # @@ -301,7 +275,7 @@ class TypedObject(SharedMemoryModel): # initialize all handlers in a lazy fashion @lazy_property def attributes(self): - return AttributeHandler(self) + return AttributeHandler(self, ModelAttributeBackend) @lazy_property def locks(self): @@ -321,7 +295,7 @@ class TypedObject(SharedMemoryModel): @lazy_property def nattributes(self): - return NAttributeHandler(self) + return AttributeHandler(self, InMemoryAttributeBackend) class Meta(object): """ diff --git a/evennia/typeclasses/tests.py b/evennia/typeclasses/tests.py index 8632db5fc6..eb2f8e45e1 100644 --- a/evennia/typeclasses/tests.py +++ b/evennia/typeclasses/tests.py @@ -26,12 +26,12 @@ class TestAttributes(EvenniaTest): key = "testattr" value = "test attr value " self.obj1.attributes.add(key, value) - self.assertFalse(self.obj1.attributes._cache) + self.assertFalse(self.obj1.attributes.backend._cache) self.assertEqual(self.obj1.attributes.get(key), value) self.obj1.db.testattr = value self.assertEqual(self.obj1.db.testattr, value) - self.assertFalse(self.obj1.attributes._cache) + self.assertFalse(self.obj1.attributes.backend._cache) def test_weird_text_save(self): "test 'weird' text type (different in py2 vs py3)" From e270ea86519f85af73c85fe0efdf7ab99cb71394 Mon Sep 17 00:00:00 2001 From: trhr Date: Wed, 22 Apr 2020 19:50:49 -0500 Subject: [PATCH 28/32] added docstring --- evennia/utils/test_resources.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/evennia/utils/test_resources.py b/evennia/utils/test_resources.py index d9d8967cdf..f9809b8c7f 100644 --- a/evennia/utils/test_resources.py +++ b/evennia/utils/test_resources.py @@ -159,6 +159,10 @@ class EvenniaTest(TestCase): super().tearDown() class LocalEvenniaTest(EvenniaTest): + """ + This test class is intended for inheriting in mygame tests. + It helps ensure your tests are run with your own objects. + """ account_typeclass = settings.BASE_ACCOUNT_TYPECLASS object_typeclass = settings.BASE_OBJECT_TYPECLASS character_typeclass = settings.BASE_CHARACTER_TYPECLASS From 586414247f7d61acc676885990a8b199ded5d39e Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 23 Apr 2020 19:08:23 +0200 Subject: [PATCH 29/32] Server timekeeper no longer assume 60s between snapshots --- evennia/server/server.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/evennia/server/server.py b/evennia/server/server.py index 2093b5b88c..90c46766ef 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -105,6 +105,7 @@ _IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE _GAMETIME_MODULE = None _IDLE_TIMEOUT = settings.IDLE_TIMEOUT +_LAST_SERVER_TIME_SNAPSHOT = None def _server_maintenance(): @@ -113,6 +114,8 @@ def _server_maintenance(): the server needs to do. It is called every minute. """ global EVENNIA, _MAINTENANCE_COUNT, _FLUSH_CACHE, _GAMETIME_MODULE + global _LAST_SERVER_TIME_SNAPSHOT + if not _FLUSH_CACHE: from evennia.utils.idmapper.models import conditional_flush as _FLUSH_CACHE if not _GAMETIME_MODULE: @@ -126,7 +129,11 @@ def _server_maintenance(): _GAMETIME_MODULE.SERVER_START_TIME = now _GAMETIME_MODULE.SERVER_RUNTIME = ServerConfig.objects.conf("runtime", default=0.0) else: - _GAMETIME_MODULE.SERVER_RUNTIME += 60.0 + # adjust the runtime not with 60s but with the actual elapsed time + # in case this may varies slightly from 60s. + _GAMETIME_MODULE.SERVER_RUNTIME += (now - _LAST_SERVER_TIME_SNAPSHOT) + _LAST_SERVER_TIME_SNAPSHOT = now + # update game time and save it across reloads _GAMETIME_MODULE.SERVER_RUNTIME_LAST_UPDATED = now ServerConfig.objects.conf("runtime", _GAMETIME_MODULE.SERVER_RUNTIME) From 60ca9f19cfd2a7470524d115fd578d9ff9f6fff7 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Fri, 15 May 2020 22:12:23 -0400 Subject: [PATCH 30/32] Bug: Puzzles do not survive server reload (because their scripts aren't persisted). --- evennia/contrib/puzzles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 1ca661cd5d..5eef1f77e2 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -289,7 +289,7 @@ class CmdCreatePuzzleRecipe(MuxCommand): proto_parts = [proto_def(obj) for obj in parts] proto_results = [proto_def(obj) for obj in results] - puzzle = create_script(PuzzleRecipe, key=puzzle_name) + puzzle = create_script(PuzzleRecipe, key=puzzle_name, persistent=True) puzzle.save_recipe(puzzle_name, proto_parts, proto_results) puzzle.locks.add("control:id(%s) or perm(Builder)" % caller.dbref[1:]) @@ -488,7 +488,7 @@ class CmdArmPuzzle(MuxCommand): Notes: Create puzzles with `@puzzle`; get list of - defined puzzles using `@lspuzlerecipies`. + defined puzzles using `@lspuzzlerecipes`. """ From dfea32ec82dc4fe715133df8c9cbb5f5b7f2eb9e Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 16 May 2020 09:56:13 +0200 Subject: [PATCH 31/32] Fix server-time-snapshot unittest patch issue --- evennia/server/server.py | 3 ++- evennia/server/tests/test_server.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/evennia/server/server.py b/evennia/server/server.py index 90c46766ef..880e9a1843 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -105,7 +105,7 @@ _IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE _GAMETIME_MODULE = None _IDLE_TIMEOUT = settings.IDLE_TIMEOUT -_LAST_SERVER_TIME_SNAPSHOT = None +_LAST_SERVER_TIME_SNAPSHOT = 0 def _server_maintenance(): @@ -128,6 +128,7 @@ def _server_maintenance(): # first call after a reload _GAMETIME_MODULE.SERVER_START_TIME = now _GAMETIME_MODULE.SERVER_RUNTIME = ServerConfig.objects.conf("runtime", default=0.0) + _LAST_SERVER_TIME_SNAPSHOT = now else: # adjust the runtime not with 60s but with the actual elapsed time # in case this may varies slightly from 60s. diff --git a/evennia/server/tests/test_server.py b/evennia/server/tests/test_server.py index 076a03a879..33a9341cae 100644 --- a/evennia/server/tests/test_server.py +++ b/evennia/server/tests/test_server.py @@ -66,6 +66,7 @@ class TestServer(TestCase): connection=DEFAULT, _IDMAPPER_CACHE_MAXSIZE=1000, _MAINTENANCE_COUNT=3600 - 1, + _LAST_SERVER_TIME_SNAPSHOT=0, ServerConfig=DEFAULT, ) as mocks: mocks["connection"].close = MagicMock() @@ -84,6 +85,7 @@ class TestServer(TestCase): connection=DEFAULT, _IDMAPPER_CACHE_MAXSIZE=1000, _MAINTENANCE_COUNT=3700 - 1, + _LAST_SERVER_TIME_SNAPSHOT=0, ServerConfig=DEFAULT, ) as mocks: mocks["connection"].close = MagicMock() @@ -101,6 +103,7 @@ class TestServer(TestCase): connection=DEFAULT, _IDMAPPER_CACHE_MAXSIZE=1000, _MAINTENANCE_COUNT=(3600 * 7) - 1, + _LAST_SERVER_TIME_SNAPSHOT=0, ServerConfig=DEFAULT, ) as mocks: mocks["connection"].close = MagicMock() @@ -117,6 +120,7 @@ class TestServer(TestCase): connection=DEFAULT, _IDMAPPER_CACHE_MAXSIZE=1000, _MAINTENANCE_COUNT=(3600 * 7) - 1, + _LAST_SERVER_TIME_SNAPSHOT=0, SESSIONS=DEFAULT, _IDLE_TIMEOUT=10, time=DEFAULT, From f989f3514b70f5a1ec88ad76ccdd6803254dc4a8 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 16 May 2020 10:30:01 +0200 Subject: [PATCH 32/32] Add evennia-launcher support for createsuperuser --- evennia/server/evennia_launcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index e4a1518a75..90c40747c6 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -2262,7 +2262,7 @@ def main(): if option in ("makemessages", "compilemessages"): # some commands don't require the presence of a game directory to work need_gamedir = False - if option in ("shell", "check", "makemigrations"): + if option in ("shell", "check", "makemigrations", "createsuperuser"): # some django commands requires the database to exist, # or evennia._init to have run before they work right. check_db = True