I18n string cleanup and refactoring

This commit is contained in:
Griatch 2021-05-26 21:55:05 +02:00
parent 59dd0b007a
commit 7ff8cbb341
62 changed files with 890 additions and 738 deletions

View file

@ -1,88 +1,99 @@
# Internationalization
*Internationalization* (often abbreviated *i18n* since there are 18 characters between the first "i"
and the last "n" in that word) allows Evennia's core server to return texts in other languages than
English - without anyone having to edit the source code. Take a look at the `locale` directory of
the Evennia installation, there you will find which languages are currently supported.
*Internationalization* (often abbreviated *i18n* since there are 18 characters
between the first "i" and the last "n" in that word) allows Evennia's core
server to return texts in other languages than English - without anyone having
to edit the source code. Take a look at the `locale` directory of the Evennia
installation, there you will find which languages are currently supported.
## Changing server language
Change language by adding the following to your `mygame/server/conf/settings.py` file:
Change language by adding the following to your `mygame/server/conf/settings.py`
file:
```python
USE_I18N = True
LANGUAGE_CODE = 'en'
```
Here `'en'` should be changed to the abbreviation for one of the supported languages found in
`locale/`. Restart the server to activate i18n. The two-character international language codes are
found [here](http://www.science.co.il/Language/Codes.asp).
Here `'en'` should be changed to the abbreviation for one of the supported
languages found in `locale/`. Restart the server to activate i18n. The
two-character international language codes are found
[here](http://www.science.co.il/Language/Codes.asp).
> Windows Note: If you get errors concerning `gettext` or `xgettext` on Windows, see the [Django
documentation](https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#gettext-on-windows). A
self-installing and up-to-date version of gettext for Windows (32/64-bit) is available on
[Github](https://github.com/mlocati/gettext-iconv-windows).
> Windows Note: If you get errors concerning `gettext` or `xgettext` on Windows,
> see the
> [Django documentation](https://docs.djangoproject.com/en/3.2/topics/i18n/translation/#gettext-on-windows).
> A self-installing and up-to-date version of gettext for Windows (32/64-bit) is
> available on [Github](https://github.com/mlocati/gettext-iconv-windows).
## Translating Evennia
> **Important Note:** Evennia offers translations of hard-coded strings in the server, things like
"Connection closed" or "Server restarted", strings that end users will see and which game devs are
not supposed to change on their own. Text you see in the log file or on the command line (like error
messages) are generally *not* translated (this is a part of Python).
```important::
> In addition, text in default Commands and in default Typeclasses will *not* be translated by
switching *i18n* language. To translate Commands and Typeclass hooks you must overload them in your
game directory and translate their returns to the language you want. This is because from Evennia's
perspective, adding *i18n* code to commands tend to add complexity to code that is *meant* to be
changed anyway. One of the goals of Evennia is to keep the user-changeable code as clean and easy-
to-read as possible.
Evennia offers translations of hard-coded strings in the server, things like
"Connection closed" or "Server restarted", strings that end users will see and
which game devs are not supposed to change on their own. Text you see in the log
file or on the command line (like error messages) are generally *not* translated
(this is a part of Python).
If you cannot find your language in `evennia/locale/` it's because noone has translated it yet.
Alternatively you might have the language but find the translation bad ... You are welcome to help
improve the situation!
In addition, text in default Commands and in default Typeclasses will *not* be
translated by switching *i18n* language. To translate Commands and Typeclass
hooks you must overload them in your game directory and translate their returns
to the language you want. This is because from Evennia's perspective, adding
*i18n* code to commands tend to add complexity to code that is *meant* to be
changed anyway. One of the goals of Evennia is to keep the user-changeable code
as clean and easy- to-read as possible.
```
To start a new translation you need to first have cloned the Evennia repositry with GIT and
activated a python virtualenv as described on the [Setup Quickstart](../Setup/Setup-Quickstart) page. You now
need to `cd` to the `evennia/` directory. This is *not* your created game folder but the main
Evennia library folder. If you see a folder `locale/` then you are in the right place. From here you
run:
If you cannot find your language in `evennia/locale/` it's because noone has
translated it yet. Alternatively you might have the language but find the
translation bad ... You are welcome to help improve the situation!
evennia makemessages <language-code>
To start a new translation you need to first have cloned the Evennia repositry
with GIT and activated a python virtualenv as described on the [Setup
Quickstart](../Setup/Setup-Quickstart) page.
Go to your game dir and make sure your `virtualenv` is active so the `evennia`
command is available. Then run
evennia makemessages --locale <language-code>
where `<language-code>` is the [two-letter locale code](http://www.science.co.il/Language/Codes.asp)
for the language you want, like 'sv' for Swedish or 'es' for Spanish. After a moment it will tell
you the language has been processed. For instance:
for the language you want to translate, like 'sv' for Swedish or 'es' for
Spanish. After a moment it will tell you the language has been processed. For
instance:
evennia makemessages sv
evennia makemessages --locale sv
If you started a new language a new folder for that language will have emerged in the `locale/`
folder. Otherwise the system will just have updated the existing translation with eventual new
strings found in the server. Running this command will not overwrite any existing strings so you can
run it as much as you want.
If you started a new language, a new folder for that language will have emerged
in the `locale/` folder. Otherwise the system will just have updated the
existing translation with eventual new strings found in the server. Running this
command will not overwrite any existing strings so you can run it as much as you
want.
> Note: in Django, the `makemessages` command prefixes the locale name by the `-l` option (`...
makemessages -l sv` for instance). This syntax is not allowed in Evennia, due to the fact that `-l`
is the option to tail log files. Hence, `makemessages` doesn't use the `-l` flag.
Next head to `locale/<language-code>/LC_MESSAGES` and edit the `**.po` file you
find there. You can edit this with a normal text editor but it is easiest if
you use a special po-file editor from the web (search the web for "po editor"
for many free alternatives).
Next head to `locale/<language-code>/LC_MESSAGES` and edit the `**.po` file you find there. You can
edit this with a normal text editor but it is easiest if you use a special po-file editor from the
web (search the web for "po editor" for many free alternatives).
The concept of translating is simple, it's just a matter of taking the english strings you find in
the `**.po` file and add your language's translation best you can. The `**.po` format (and many
supporting editors) allow you to mark translations as "fuzzy". This tells the system (and future
translators) that you are unsure about the translation, or that you couldn't find a translation that
exactly matched the intention of the original text. Other translators will see this and might be
able to improve it later.
Finally, you need to compile your translation into a more efficient form. Do so from the `evennia`
folder
again:
The concept of translating is simple, it's just a matter of taking the english
strings you find in the `**.po` file and add your language's translation best
you can. The `**.po` format (and many supporting editors) allow you to mark
translations as "fuzzy". This tells the system (and future translators) that you
are unsure about the translation, or that you couldn't find a translation that
exactly matched the intention of the original text. Other translators will see
this and might be able to improve it later. Finally, you need to compile your
translation into a more efficient form. Do so from the `evennia` folder again:
evennia compilemessages
This will go through all languages and create/update compiled files (`**.mo`) for them. This needs
to be done whenever a `**.po` file is updated.
This will go through all languages and create/update compiled files (`**.mo`)
for them. This needs to be done whenever a `**.po` file is updated.
When you are done, send the `**.po` and `*.mo` file to the Evennia developer list (or push it into
your own repository clone) so we can integrate your translation into Evennia!
When you are done, make sure that everyone can benefit from your translation!
Make a PR against Evennia with the updated `**.po` and `*.mo` files. Less
ideally (if git is not your thing) you can also attach them to a new post in our
forums.

View file

@ -1,5 +1,5 @@
"""
Typeclass for Account objects
Typeclass for Account objects.
Note that this object is primarily intended to
store OOC information, not game info! This
@ -54,7 +54,8 @@ _CMDHANDLER = None
# Create throttles for too many account-creations and login attempts
CREATION_THROTTLE = Throttle(
name='creation', limit=settings.CREATION_THROTTLE_LIMIT, timeout=settings.CREATION_THROTTLE_TIMEOUT
name='creation', limit=settings.CREATION_THROTTLE_LIMIT,
timeout=settings.CREATION_THROTTLE_TIMEOUT
)
LOGIN_THROTTLE = Throttle(
name='login', limit=settings.LOGIN_THROTTLE_LIMIT, timeout=settings.LOGIN_THROTTLE_TIMEOUT
@ -292,11 +293,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(_("You don't have permission to puppet '{key}'.").format(key=obj.key))
self.msg("You don't have permission to puppet '{obj.key}'.")
return
if obj.account:
# object already puppeted
@ -312,8 +313,8 @@ 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
@ -543,7 +544,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# Update throttle
if ip:
LOGIN_THROTTLE.update(ip, "Too many authentication failures.")
LOGIN_THROTTLE.update(ip, _("Too many authentication failures."))
# Try to call post-failure hook
session = kwargs.get("session", None)
@ -658,8 +659,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
password (str): Password to set.
Notes:
This is called by Django also when logging in; it should not be mixed up with validation, since that
would mean old passwords in the database (pre validation checks) could get invalidated.
This is called by Django also when logging in; it should not be mixed up with
validation, since that would mean old passwords in the database (pre validation checks)
could get invalidated.
"""
super(DefaultAccount, self).set_password(password)
@ -798,12 +800,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
)
logger.log_sec(f"Account Created: {account} (IP: {ip}).")
except Exception as e:
except Exception:
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
@ -819,7 +819,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# join the new account to the public channel
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
if not pchannel or not pchannel.connect(account):
string = f"New account '{account.key}' could not connect to public channel!"
string = "New account '{account.key}' could not connect to public channel!"
errors.append(string)
logger.log_err(string)
@ -1574,7 +1574,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 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 []
@ -1617,19 +1617,18 @@ 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 <name> [=description]|n - create new character")
result.append("\n |wcharcreate <name> [=description]|n - create new character")
result.append(
"\n |w@chardelete <name>|n - delete a character (cannot be undone!)"
"\n |wchardelete <name>|n - delete a character (cannot be undone!)"
)
if characters:
string_s_ending = len(characters) > 1 and "s" or ""
result.append("\n |w@ic <character>|n - enter the game (|w@ooc|n to get back here)")
result.append("\n |wic <character>|n - enter the game (|wooc|n to get back here)")
if is_su:
result.append(
f"\n\nAvailable character{string_s_ending} ({len(characters)}/unlimited):"
@ -1651,11 +1650,12 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
sid = sess in sessions and sessions.index(sess) + 1
if sess and sid:
result.append(
f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] (played by you in session {sid})"
)
f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] "
f"(played by you in session {sid})")
else:
result.append(
f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] (played by someone else)"
f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] "
"(played by someone else)"
)
else:
# character is "free to puppet"
@ -1668,6 +1668,7 @@ class DefaultGuest(DefaultAccount):
"""
This class is used for guest logins. Unlike Accounts, Guests and
their characters are deleted after disconnection.
"""
@classmethod
@ -1675,6 +1676,7 @@ class DefaultGuest(DefaultAccount):
"""
Forwards request to cls.authenticate(); returns a DefaultGuest object
if one is available for use.
"""
return cls.authenticate(**kwargs)
@ -1742,7 +1744,7 @@ class DefaultGuest(DefaultAccount):
return account, errors
except Exception as e:
except Exception:
# 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.

View file

@ -330,7 +330,7 @@ class IRCBot(Bot):
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)
"Nicks at {chstr}:\n {nicklist}".format(chstr=chstr, nicklist=nicklist)
)
self._nicklist_callers = []
return
@ -341,7 +341,7 @@ class IRCBot(Bot):
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(
"IRC ping return from {chstr} took {time}s.".format(
chstr=chstr, time=kwargs["timing"]
)
)
@ -397,6 +397,7 @@ class IRCBot(Bot):
#
# RSS
#
class RSSBot(Bot):

View file

@ -94,7 +94,8 @@ class AccountDB(TypedObject, AbstractUser):
"cmdset",
max_length=255,
null=True,
help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.",
help_text="optional python path to a cmdset class. If creating a Character, this will "
"default to settings.CMDSET_CHARACTER.",
)
# marks if this is a "virtual" bot account object
db_is_bot = models.BooleanField(

View file

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
import sys
from mock import Mock, MagicMock, patch
from random import randint
from unittest import TestCase
@ -12,8 +11,6 @@ from evennia.utils.test_resources import EvenniaTest
from evennia.utils import create
from evennia.utils.utils import uses_database
from django.conf import settings
class TestAccountSessionHandler(TestCase):
"Check AccountSessionHandler class"

View file

@ -80,50 +80,50 @@ _SEARCH_AT_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit
# is the normal "production message to echo to the account.
_ERROR_UNTRAPPED = (
"""
_("""
An untrapped error occurred.
""",
"""
"""),
_("""
An untrapped error occurred. Please file a bug report detailing the steps to reproduce.
""",
"""),
)
_ERROR_CMDSETS = (
"""
_("""
A cmdset merger-error occurred. This is often due to a syntax
error in one of the cmdsets to merge.
""",
"""
"""),
_("""
A cmdset merger-error occurred. Please file a bug report detailing the
steps to reproduce.
""",
"""),
)
_ERROR_NOCMDSETS = (
"""
_("""
No command sets found! This is a critical bug that can have
multiple causes.
""",
"""
"""),
_("""
No command sets found! This is a sign of a critical bug. If
disconnecting/reconnecting doesn't" solve the problem, try to contact
the server admin through" some other means for assistance.
""",
"""),
)
_ERROR_CMDHANDLER = (
"""
_("""
A command handler bug occurred. If this is not due to a local change,
please file a bug report with the Evennia project, including the
traceback and steps to reproduce.
""",
"""
"""),
_("""
A command handler bug occurred. Please notify staff - they should
likely file a bug report with the Evennia project.
""",
"""),
)
_ERROR_RECURSION_LIMIT = (
_ERROR_RECURSION_LIMIT = _(
"Command recursion limit ({recursion_limit}) " "reached for '{raw_cmdname}' ({cmdclass})."
)
@ -146,7 +146,7 @@ def _msg_err(receiver, stringtuple):
production string (with a timestamp) to be shown to the user.
"""
string = "{traceback}\n{errmsg}\n(Traceback was logged {timestamp})."
string = _("{traceback}\n{errmsg}\n(Traceback was logged {timestamp}).")
timestamp = logger.timeformat()
tracestring = format_exc()
logger.log_trace()
@ -299,6 +299,7 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
def _get_local_obj_cmdsets(obj):
"""
Helper-method; Get Object-level cmdsets
"""
# Gather cmdsets from location, objects in location or carried
try:
@ -352,6 +353,7 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
"""
Helper method; Get cmdset while making sure to trigger all
hooks safely. Returns the stack and the valid options.
"""
try:
yield obj.at_cmdset_get()
@ -384,13 +386,6 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
]
cmdsets += local_obj_cmdsets
# if not current.no_channels:
# # also objs may have channels
# channel_cmdsets = yield _get_channel_cmdset(obj)
# cmdsets += channel_cmdsets
# if not current.no_channels:
# channel_cmdsets = yield _get_channel_cmdset(account)
# cmdsets += channel_cmdsets
elif callertype == "account":
# we are calling the command from the account level
@ -408,11 +403,6 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
]
cmdsets += local_obj_cmdsets
# if not current.no_channels:
# # also objs may have channels
# cmdsets += yield _get_channel_cmdset(obj)
# if not current.no_channels:
# cmdsets += yield _get_channel_cmdset(account)
elif callertype == "object":
# we are calling the command from the object level
@ -426,9 +416,6 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
]
cmdsets += yield local_obj_cmdsets
# if not current.no_channels:
# # also objs may have channels
# cmdsets += yield _get_channel_cmdset(obj)
else:
raise Exception("get_and_merge_cmdsets: callertype %s is not valid." % callertype)

View file

@ -364,8 +364,8 @@ class CmdSet(object, metaclass=_CmdSetMeta):
if getattr(self, opt) is not None
])
options = (", " + options) if options else ""
return f"<CmdSet {self.key}, {self.mergetype}, {perm}, prio {self.priority}{options}>: " + ", ".join(
[str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])
return (f"<CmdSet {self.key}, {self.mergetype}, {perm}, prio {self.priority}{options}>: "
+ ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)]))
def __iter__(self):
"""
@ -477,7 +477,8 @@ class CmdSet(object, metaclass=_CmdSetMeta):
# This is used for diagnosis.
cmdset_c.actual_mergetype = mergetype
# print "__add__ for %s (prio %i) called with %s (prio %i)." % (self.key, self.priority, cmdset_a.key, cmdset_a.priority)
# print "__add__ for %s (prio %i) called with %s (prio %i)." % (self.key, self.priority,
# cmdset_a.key, cmdset_a.priority)
# return the system commands to the cmdset
cmdset_c.add(sys_commands, allow_duplicates=True)
@ -670,5 +671,6 @@ class CmdSet(object, metaclass=_CmdSetMeta):
Hook method - this should be overloaded in the inheriting
class, and should take care of populating the cmdset by use of
self.add().
"""
pass

View file

@ -62,6 +62,7 @@ Since any number of CommandSets can be piled on top of each other, you
can then implement separate sets for different situations. For
example, you can have a 'On a boat' set, onto which you then tack on
the 'Fishing' set. Fishing from a boat? No problem!
"""
import sys
from traceback import format_exc
@ -168,7 +169,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
if "." in path:
modpath, classname = python_path.rsplit(".", 1)
else:
raise ImportError("The path '%s' is not on the form modulepath.ClassName" % path)
raise ImportError(f"The path '{path}' is not on the form modulepath.ClassName")
try:
# first try to get from cache
@ -312,6 +313,7 @@ class CmdSetHandler(object):
def __str__(self):
"""
Display current commands
"""
strings = ["<CmdSetHandler> stack:"]
@ -331,7 +333,7 @@ class CmdSetHandler(object):
if mergelist:
# current is a result of mergers
mergelist="+".join(mergelist)
mergelist = "+".join(mergelist)
strings.append(f" <Merged {mergelist}>: {self.current}")
else:
# current is a single cmdset
@ -421,7 +423,8 @@ class CmdSetHandler(object):
self.mergetype_stack.append(new_current.actual_mergetype)
self.current = new_current
def add(self, cmdset, emit_to_obj=None, persistent=True, permanent=True, default_cmdset=False, **kwargs):
def add(self, cmdset, emit_to_obj=None, persistent=True, default_cmdset=False,
**kwargs):
"""
Add a cmdset to the handler, on top of the old ones, unless it
is set as the default one (it will then end up at the bottom of the stack)
@ -431,8 +434,6 @@ class CmdSetHandler(object):
to such an object.
emit_to_obj (Object, optional): An object to receive error messages.
persistent (bool, optional): Let cmdset remain across server reload.
permanent (bool, optional): DEPRECATED. This has the same use as
`persistent`.
default_cmdset (Cmdset, optional): Insert this to replace the
default cmdset position (there is only one such position,
always at the bottom of the stack).
@ -450,10 +451,9 @@ class CmdSetHandler(object):
"""
if "permanent" in kwargs:
logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed to "
logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed name to "
"'persistent' and now defaults to True.")
permanent = persistent or permanent
persistent = kwargs['permanent'] if persistent is None else persistent
if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)):
string = _("Only CmdSets can be added to the cmdsethandler!")
@ -465,8 +465,8 @@ class CmdSetHandler(object):
# this is (maybe) a python path. Try to import from cache.
cmdset = self._import_cmdset(cmdset)
if cmdset and cmdset.key != "_CMDSET_ERROR":
cmdset.permanent = permanent
if permanent and cmdset.key != "_CMDSET_ERROR":
cmdset.permanent = persistent # TODO change on cmdset too
if persistent and cmdset.key != "_CMDSET_ERROR":
# store the path permanently
storage = self.obj.cmdset_storage or [""]
if default_cmdset:
@ -480,17 +480,21 @@ class CmdSetHandler(object):
self.cmdset_stack.append(cmdset)
self.update()
def add_default(self, cmdset, emit_to_obj=None, permanent=True):
def add_default(self, cmdset, emit_to_obj=None, persistent=True, **kwargs):
"""
Shortcut for adding a default cmdset.
Args:
cmdset (Cmdset): The Cmdset to add.
emit_to_obj (Object, optional): Gets error messages
permanent (bool, optional): The new Cmdset should survive a server reboot.
persistent (bool, optional): The new Cmdset should survive a server reboot.
"""
self.add(cmdset, emit_to_obj=emit_to_obj, permanent=permanent, default_cmdset=True)
if "permanent" in kwargs:
logger.log_dep("obj.cmdset.add_default() kwarg 'permanent' has changed name to "
"'persistent'.")
persistent = kwargs['permanent'] if persistent is None else persistent
self.add(cmdset, emit_to_obj=emit_to_obj, persistent=persistent, default_cmdset=True)
def remove(self, cmdset=None, default_cmdset=False):
"""

View file

@ -572,6 +572,7 @@ Command {self} has no defined `func()` - showing on-command variables:
border_left_char=border_left_char,
border_right_char=border_right_char,
border_top_char=border_top_char,
border_bottom_char=border_bottom_char,
**kwargs,
)
return table

View file

@ -7,7 +7,7 @@ from django.urls import reverse
from django.utils.text import slugify
from evennia.typeclasses.models import TypeclassBase
from evennia.comms.models import TempMsg, ChannelDB
from evennia.comms.models import ChannelDB
from evennia.comms.managers import ChannelManager
from evennia.utils import create, logger
from evennia.utils.utils import make_iter
@ -49,7 +49,6 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
channel_msg_nick_pattern = r"{alias}\s*?|{alias}\s+?(?P<arg1>.+?)"
channel_msg_nick_replacement = "channel {channelname} = $1"
def at_first_save(self):
"""
Called by the typeclass system the very first time the channel
@ -706,7 +705,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
try:
return reverse("%s-create" % slugify(cls._meta.verbose_name))
except:
except Exception:
return "#"
def web_get_detail_url(self):
@ -721,8 +720,10 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
a named view of 'channel-detail' would be referenced by this method.
ex.
url(r'channels/(?P<slug>[\w\d\-]+)/$',
ChannelDetailView.as_view(), name='channel-detail')
::
url(r'channels/(?P<slug>[\w\d\-]+)/$',
ChannelDetailView.as_view(), name='channel-detail')
If no View has been created and defined in urls.py, returns an
HTML anchor.
@ -740,7 +741,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"%s-detail" % slugify(self._meta.verbose_name),
kwargs={"slug": slugify(self.db_key)},
)
except:
except Exception:
return "#"
def web_get_update_url(self):
@ -755,8 +756,10 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
a named view of 'channel-update' would be referenced by this method.
ex.
url(r'channels/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
ChannelUpdateView.as_view(), name='channel-update')
::
url(r'channels/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
ChannelUpdateView.as_view(), name='channel-update')
If no View has been created and defined in urls.py, returns an
HTML anchor.
@ -774,7 +777,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"%s-update" % slugify(self._meta.verbose_name),
kwargs={"slug": slugify(self.db_key)},
)
except:
except Exception:
return "#"
def web_get_delete_url(self):
@ -807,7 +810,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"%s-delete" % slugify(self._meta.verbose_name),
kwargs={"slug": slugify(self.db_key)},
)
except:
except Exception:
return "#"
# Used by Django Sites/Admin

View file

@ -218,7 +218,6 @@ class MsgManager(TypedObjectManager):
else:
raise CommError
def search_message(self, sender=None, receiver=None, freetext=None, dbref=None):
"""
Search the message database for particular messages. At least

View file

@ -16,6 +16,7 @@ database.
Channels are central objects that act as targets for Msgs. Accounts can
connect to channels by use of a ChannelConnect object (this object is
necessary to easily be able to delete connections on the fly).
"""
from django.conf import settings
from django.utils import timezone
@ -165,7 +166,8 @@ class Msg(SharedMemoryModel):
db_tags = models.ManyToManyField(
Tag,
blank=True,
help_text="tags on this message. Tags are simple string markers to identify, group and alias messages.",
help_text="tags on this message. Tags are simple string markers to "
"identify, group and alias messages.",
)
# Database manager
@ -309,7 +311,6 @@ class Msg(SharedMemoryModel):
self.db_receiver_external = ""
self.save()
def remove_receiver(self, receivers):
"""
Remove a single receiver, a list of receivers, or a single extral receiver.
@ -337,8 +338,8 @@ class Msg(SharedMemoryModel):
elif clsname == "ScriptDB":
self.db_receivers_scripts.remove(receiver)
def __hide_from_get(self):
@property
def hide_from(self):
"""
Getter. Allows for value = self.hide_from.
Returns two lists of accounts and objects.
@ -349,9 +350,12 @@ class Msg(SharedMemoryModel):
self.db_hide_from_objects.all(),
)
# @hide_from_sender.setter
def __hide_from_set(self, hiders):
"Setter. Allows for self.hide_from = value. Will append to hiders"
@hide_from.setter
def hide_from(self, hiders):
"""
Setter. Allows for self.hide_from = value. Will append to hiders.
"""
for hider in make_iter(hiders):
if not hider:
continue
@ -363,21 +367,25 @@ class Msg(SharedMemoryModel):
elif clsname == "ObjectDB":
self.db_hide_from_objects.add(hider.__dbclass__)
# @hide_from_sender.deleter
def __hide_from_del(self):
"Deleter. Allows for del self.hide_from_senders"
@hide_from.deleter
def hide_from(self):
"""
Deleter. Allows for del self.hide_from_senders
"""
self.db_hide_from_accounts.clear()
self.db_hide_from_objects.clear()
self.save()
hide_from = property(__hide_from_get, __hide_from_set, __hide_from_del)
#
# Msg class methods
#
def __str__(self):
"This handles what is shown when e.g. printing the message"
"""
This handles what is shown when e.g. printing the message.
"""
senders = ",".join(getattr(obj, "key", str(obj)) for obj in self.senders)
receivers = ",".join(getattr(obj, "key", str(obj)) for obj in self.receivers)
return "%s->%s: %s" % (senders, receivers, crop(self.message, width=40))
@ -407,9 +415,8 @@ class Msg(SharedMemoryModel):
class TempMsg(object):
"""
This is a non-persistent object for sending temporary messages
that will not be stored. It mimics the "real" Msg object, but
doesn't require sender to be given.
This is a non-persistent object for sending temporary messages that will not be stored. It
mimics the "real" Msg object, but doesn't require sender to be given.
"""
@ -452,6 +459,7 @@ class TempMsg(object):
def __str__(self):
"""
This handles what is shown when e.g. printing the message.
"""
senders = ",".join(obj.key for obj in self.senders)
receivers = ",".join(obj.key for obj in self.receivers)
@ -477,6 +485,7 @@ class TempMsg(object):
Args:
receiver (Object, Account, Script, str or list): Receivers to remove.
"""
for o in make_iter(receiver):
@ -513,6 +522,7 @@ class SubscriptionHandler(object):
This handler manages subscriptions to the
channel and hides away which type of entity is
subscribing (Account or Object)
"""
def __init__(self, obj):
@ -634,7 +644,8 @@ class SubscriptionHandler(object):
if not obj.is_connected:
continue
except ObjectDoesNotExist:
# a subscribed object has already been deleted. Mark that we need a recache and ignore it
# a subscribed object has already been deleted. Mark that we need a recache and
# ignore it
recache_needed = True
continue
subs.append(obj)
@ -688,7 +699,7 @@ class ChannelDB(TypedObject):
__defaultclasspath__ = "evennia.comms.comms.DefaultChannel"
__applabel__ = "comms"
class Meta(object):
class Meta:
"Define Django meta options"
verbose_name = "Channel"
verbose_name_plural = "Channels"

View file

@ -113,6 +113,7 @@ class FileHelpStorageHandler:
Note that this is not meant to any searching/lookup - that is all handled
by the help command.
"""
def __init__(self, help_file_modules=settings.FILE_HELP_ENTRY_MODULES):

View file

@ -1,7 +1,6 @@
"""
Custom manager for HelpEntry objects.
"""
from django.db import models
from evennia.utils import logger, utils
from evennia.typeclasses.managers import TypedObjectManager
@ -131,7 +130,7 @@ class HelpEntryManager(TypedObjectManager):
for topic in topics:
topic.help_category = default_category
topic.save()
string = _("Help database moved to category {default_category}").format(
string = "Help database moved to category {default_category}".format(
default_category=default_category
)
logger.log_info(string)

View file

@ -168,7 +168,9 @@ class HelpEntry(SharedMemoryModel):
a named view of 'character-create' would be referenced by this method.
ex.
url(r'characters/create/', ChargenView.as_view(), name='character-create')
::
url(r'characters/create/', ChargenView.as_view(), name='character-create')
If no View has been created and defined in urls.py, returns an
HTML anchor.
@ -183,7 +185,7 @@ class HelpEntry(SharedMemoryModel):
"""
try:
return reverse("%s-create" % slugify(cls._meta.verbose_name))
except:
except Exception:
return "#"
def web_get_detail_url(self):
@ -198,8 +200,9 @@ class HelpEntry(SharedMemoryModel):
a named view of 'character-detail' would be referenced by this method.
ex.
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$',
CharDetailView.as_view(), name='character-detail')
::
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$',
CharDetailView.as_view(), name='character-detail')
If no View has been created and defined in urls.py, returns an
HTML anchor.
@ -217,8 +220,7 @@ class HelpEntry(SharedMemoryModel):
"%s-detail" % slugify(self._meta.verbose_name),
kwargs={"category": slugify(self.db_help_category), "topic": slugify(self.db_key)},
)
except Exception as e:
print(e)
except Exception:
return "#"
def web_get_update_url(self):
@ -233,8 +235,10 @@ class HelpEntry(SharedMemoryModel):
a named view of 'character-update' would be referenced by this method.
ex.
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
CharUpdateView.as_view(), name='character-update')
::
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
CharUpdateView.as_view(), name='character-update')
If no View has been created and defined in urls.py, returns an
HTML anchor.
@ -252,7 +256,7 @@ class HelpEntry(SharedMemoryModel):
"%s-update" % slugify(self._meta.verbose_name),
kwargs={"category": slugify(self.db_help_category), "topic": slugify(self.db_key)},
)
except:
except Exception:
return "#"
def web_get_delete_url(self):
@ -266,8 +270,10 @@ class HelpEntry(SharedMemoryModel):
a named view of 'character-detail' would be referenced by this method.
ex.
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
CharDeleteView.as_view(), name='character-delete')
::
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
CharDeleteView.as_view(), name='character-delete')
If no View has been created and defined in urls.py, returns an
HTML anchor.
@ -285,7 +291,7 @@ class HelpEntry(SharedMemoryModel):
"%s-delete" % slugify(self._meta.verbose_name),
kwargs={"category": slugify(self.db_help_category), "topic": slugify(self.db_key)},
)
except:
except Exception:
return "#"
# Used by Django Sites/Admin

View file

@ -73,7 +73,6 @@ def help_search_with_index(query, candidate_entries, suggestion_maxnum=5, fields
custom_stop_words_filter = stop_word_filter.generate_stop_word_filter(stop_words)
_LUNR_BUILDER_PIPELINE = (trimmer, custom_stop_words_filter, stemmer)
indx = [cnd.search_index_entry for cnd in candidate_entries]
mapping = {indx[ix]["key"]: cand for ix, cand in enumerate(candidate_entries)}

View file

@ -11,81 +11,6 @@ Note that `accessing_obj` and `accessed_obj` can be any object type
with a lock variable/field, so be careful to not expect
a certain object type.
**Appendix: MUX locks**
Below is a list nicked from the MUX help file on the locks available
in standard MUX. Most of these are not relevant to core Evennia since
locks in Evennia are considerably more flexible and can be implemented
on an individual command/typeclass basis rather than as globally
available like the MUX ones. So many of these are not available in
basic Evennia, but could all be implemented easily if needed for the
individual game.
```
MUX Name: Affects: Effect:
----------------------------------------------------------------------
DefaultLock: Exits: controls who may traverse the exit to
its destination.
Evennia: "traverse:<lockfunc()>"
Rooms: controls whether the account sees the
SUCC or FAIL message for the room
following the room description when
looking at the room.
Evennia: Custom typeclass
Accounts/Things: controls who may GET the object.
Evennia: "get:<lockfunc()"
EnterLock: Accounts/Things: controls who may ENTER the object
Evennia:
GetFromLock: All but Exits: controls who may gets things from a
given location.
Evennia:
GiveLock: Accounts/Things: controls who may give the object.
Evennia:
LeaveLock: Accounts/Things: controls who may LEAVE the object.
Evennia:
LinkLock: All but Exits: controls who may link to the location
if the location is LINK_OK (for linking
exits or setting drop-tos) or ABODE (for
setting homes)
Evennia:
MailLock: Accounts: controls who may @mail the account.
Evennia:
OpenLock: All but Exits: controls who may open an exit.
Evennia:
PageLock: Accounts: controls who may page the account.
Evennia: "send:<lockfunc()>"
ParentLock: All: controls who may make @parent links to
the object.
Evennia: Typeclasses and
"puppet:<lockstring()>"
ReceiveLock: Accounts/Things: controls who may give things to the
object.
Evennia:
SpeechLock: All but Exits: controls who may speak in that location
Evennia:
TeloutLock: All but Exits: controls who may teleport out of the
location.
Evennia:
TportLock: Rooms/Things: controls who may teleport there
Evennia:
UseLock: All but Exits: controls who may USE the object, GIVE
the object money and have the PAY
attributes run, have their messages
heard and possibly acted on by LISTEN
and AxHEAR, and invoke $-commands
stored on the object.
Evennia: Commands and Cmdsets.
DropLock: All but rooms: controls who may drop that object.
Evennia:
VisibleLock: All: Controls object visibility when the
object is not dark and the looker
passes the lock. In DARK locations, the
object must also be set LIGHT and the
viewer must pass the VisibleLock.
Evennia: Room typeclass with
Dark/light script
```
"""
@ -112,16 +37,21 @@ def _to_account(accessing_obj):
def true(*args, **kwargs):
"Always returns True."
return True
"""
Always returns True.
"""
return True
def all(*args, **kwargs):
return True
def false(*args, **kwargs):
"Always returns False"
"""
Always returns False
"""
return False
@ -129,6 +59,10 @@ def none(*args, **kwargs):
return False
def superuser(*args, **kwargs):
return False
def self(accessing_obj, accessed_obj, *args, **kwargs):
"""
Check if accessing_obj is the same as accessed_obj
@ -167,7 +101,7 @@ def perm(accessing_obj, accessed_obj, *args, **kwargs):
try:
permission = args[0].lower()
perms_object = accessing_obj.permissions.all()
except (AttributeError, IndexError) as err:
except (AttributeError, IndexError):
return False
gtmode = kwargs.pop("_greater_than", False)
@ -644,17 +578,6 @@ def holds(accessing_obj, accessed_obj, *args, **kwargs):
return False
def superuser(*args, **kwargs):
"""
Only accepts an accesing_obj that is superuser (e.g. user #1)
Since a superuser would not ever reach this check (superusers
bypass the lock entirely), any user who gets this far cannot be a
superuser, hence we just return False. :)
"""
return False
def has_account(accessing_obj, accessed_obj, *args, **kwargs):
"""
Only returns true if accessing_obj has_account is true, that is,

View file

@ -124,6 +124,7 @@ _LOCK_HANDLER = None
class LockException(Exception):
"""
Raised during an error in a lock.
"""
pass
@ -139,6 +140,7 @@ _LOCKFUNCS = {}
def _cache_lockfuncs():
"""
Updates the cache.
"""
global _LOCKFUNCS
_LOCKFUNCS = {}
@ -163,7 +165,7 @@ _RE_OK = re.compile(r"%s|and|or|not")
#
class LockHandler(object):
class LockHandler:
"""
This handler should be attached to all objects implementing
permission checks, under the property 'lockhandler'.
@ -260,16 +262,13 @@ class LockHandler(object):
continue
if access_type in locks:
duplicates += 1
wlist.append(
_(
"LockHandler on %(obj)s: access type '%(access_type)s' changed from '%(source)s' to '%(goal)s' "
% {
"obj": self.obj,
"access_type": access_type,
"source": locks[access_type][2],
"goal": raw_lockstring,
}
)
wlist.append(_(
"LockHandler on {obj}: access type '{access_type}' "
"changed from '{source}' to '{goal}' ".format(
obj=self.obj,
access_type=access_type,
source=locks[access_type][2],
goal=raw_lockstring))
)
locks[access_type] = (evalstring, tuple(lock_funcs), raw_lockstring)
if wlist and WARNING_LOG:
@ -284,12 +283,14 @@ class LockHandler(object):
def _cache_locks(self, storage_lockstring):
"""
Store data
"""
self.locks = self._parse_lockstring(storage_lockstring)
def _save_locks(self):
"""
Store locks to obj
"""
self.obj.lock_storage = ";".join([tup[2] for tup in self.locks.values()])
@ -693,8 +694,7 @@ def check_lockstring(
access_type=access_type,
)
def check_perm(
obj, permission, no_superuser_bypass=False):
def check_perm(obj, permission, no_superuser_bypass=False):
"""
Shortcut for checking if an object has the given `permission`. If the
permission is in `settings.PERMISSION_HIERARCHY`, the check passes

View file

@ -2,7 +2,6 @@
Custom manager for Objects.
"""
import re
from itertools import chain
from django.db.models import Q
from django.conf import settings
from django.db.models.fields import exceptions
@ -154,7 +153,8 @@ class ObjectDBManager(TypedObjectManager):
Args:
attribute_name (str): Attribute key to search for.
attribute_value (any): Attribute value to search for. This can also be database objects.
attribute_value (any): Attribute value to search for. This can also be database
objects.
candidates (list, optional): Candidate objects to limit search to.
typeclasses (list, optional): Python pats to restrict matches with.
@ -591,6 +591,7 @@ class ObjectDBManager(TypedObjectManager):
"""
Clear the db_sessid field of all objects having also the
db_account field set.
"""
self.filter(db_sessid__isnull=False).update(db_sessid=None)

View file

@ -299,6 +299,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
"""
Returns all exits from this object, i.e. all objects at this
location having the property destination != `None`.
"""
return [exi for exi in self.contents if exi.destination]
@ -345,6 +346,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
Returns:
singular (str): The singular form to display.
plural (str): The determined plural form of the key, including the count.
"""
plural_category = "plural_key"
key = kwargs.get("key", self.key)
@ -700,6 +702,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
Keyword Args:
Keyword arguments will be passed to the function for all objects.
"""
contents = self.contents
if exclude:
@ -947,6 +950,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
"""
Destroys all of the exits and any exits pointing to this
object as a destination.
"""
for out_exit in [exi for exi in ObjectDB.objects.get_contents(self) if exi.db_destination]:
out_exit.delete()
@ -957,6 +961,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
"""
Moves all objects (accounts/things) to their home location or
to default home.
"""
# Gather up everything that thinks this is its location.
default_home_id = int(settings.DEFAULT_HOME.lstrip("#"))
@ -979,11 +984,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# If for some reason it's still None...
if not home:
string = "Missing default home, '%s(#%d)' "
string += "now has a null location."
obj.location = None
obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin."))
logger.log_err(string % (obj.name, obj.dbid))
logger.log_err("Missing default home - '{name}(#{dbid})' now "
"has a null location.".format(name=obj.name, dbid=obj.dbid))
return
if obj.has_account:
@ -1539,7 +1543,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
if not source_location and self.location.has_account:
# This was created from nowhere and added to an account's
# inventory; it's probably the result of a create command.
string = "You now have %s in your possession." % self.get_display_name(self.location)
string = _("You now have {name} in your possession.").format(
name=self.get_display_name(self.location))
self.location.msg(string)
return
@ -1547,9 +1552,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
if msg:
string = msg
else:
string = "{object} arrives to {destination} from {origin}."
string = _("{object} arrives to {destination} from {origin}.")
else:
string = "{object} arrives to {destination}."
string = _("{object} arrives to {destination}.")
origin = source_location
destination = self.location
@ -2157,7 +2162,7 @@ class DefaultCharacter(DefaultObject):
key = cls.normalize_name(key)
if not cls.validate_name(key):
errors.append("Invalid character name.")
errors.append(_("Invalid character name."))
return obj, errors
# Set the supplied key as the name of the intended object
@ -2176,7 +2181,7 @@ class DefaultCharacter(DefaultObject):
# Check to make sure account does not have too many chars
if account:
if len(account.characters) >= settings.MAX_NR_CHARACTERS:
errors.append("There are too many characters associated with this account.")
errors.append(_("There are too many characters associated with this account."))
return obj, errors
# Create the Character
@ -2202,10 +2207,10 @@ class DefaultCharacter(DefaultObject):
# If no description is set, set a default description
if description or not obj.db.desc:
obj.db.desc = description if description else "This is a character."
obj.db.desc = description if description else _("This is a character.")
except Exception as e:
errors.append("An error occurred while creating this '%s' object." % key)
errors.append(f"An error occurred while creating object '{key} object.")
logger.log_err(e)
return obj, errors
@ -2274,6 +2279,7 @@ class DefaultCharacter(DefaultObject):
Args:
account (Account): This is the connecting account.
session (Session): Session controlling the connection.
"""
if (
self.location is None
@ -2287,7 +2293,8 @@ class DefaultCharacter(DefaultObject):
self.db.prelogout_location = self.location # save location again to be sure.
else:
account.msg(
"|r%s has no location and no home is set.|n" % self, session=session
_("|r{obj} has no location and no home is set.|n").format(obj=self),
session=session
) # Note to set home.
def at_post_puppet(self, **kwargs):
@ -2305,11 +2312,12 @@ class DefaultCharacter(DefaultObject):
puppeting this Object.
"""
self.msg("\nYou become |c%s|n.\n" % self.name)
self.msg(_("\nYou become |c{name}|n.\n").format(name=self.key))
self.msg((self.at_look(self.location), {"type": "look"}), options=None)
def message(obj, from_obj):
obj.msg("%s has entered the game." % self.get_display_name(obj), from_obj=from_obj)
obj.msg(_("{name} has entered the game.").format(name=self.get_display_name(obj)),
from_obj=from_obj)
self.location.for_contents(message, exclude=[self], from_obj=self)
@ -2332,7 +2340,8 @@ class DefaultCharacter(DefaultObject):
if self.location:
def message(obj, from_obj):
obj.msg("%s has left the game." % self.get_display_name(obj), from_obj=from_obj)
obj.msg(_("{name} has left the game.").format(name=self.get_display_name(obj)),
from_obj=from_obj)
self.location.for_contents(message, exclude=[self], from_obj=self)
self.db.prelogout_location = self.location
@ -2343,6 +2352,7 @@ class DefaultCharacter(DefaultObject):
"""
Returns the idle time of the least idle session in seconds. If
no sessions are connected it returns nothing.
"""
idle = [session.cmd_last_visible for session in self.sessions.all()]
if idle:
@ -2354,6 +2364,7 @@ class DefaultCharacter(DefaultObject):
"""
Returns the maximum connection time of all connected sessions
in seconds. Returns nothing if there are no sessions.
"""
conn = [session.conn_time for session in self.sessions.all()]
if conn:
@ -2447,7 +2458,7 @@ class DefaultRoom(DefaultObject):
# If no description is set, set a default description
if description or not obj.db.desc:
obj.db.desc = description if description else "This is a room."
obj.db.desc = description if description else _("This is a room.")
except Exception as e:
errors.append("An error occurred while creating this '%s' object." % key)
@ -2653,7 +2664,7 @@ class DefaultExit(DefaultObject):
# If no description is set, set a default description
if description or not obj.db.desc:
obj.db.desc = description if description else "This is an exit."
obj.db.desc = description if description else _("This is an exit.")
except Exception as e:
errors.append("An error occurred while creating this '%s' object." % key)
@ -2750,4 +2761,4 @@ class DefaultExit(DefaultObject):
read for an error string instead.
"""
traversing_object.msg("You cannot go there.")
traversing_object.msg(_("You cannot go there."))

View file

@ -51,8 +51,6 @@ def protfunc_callable_protkey(*args, **kwargs):
return prot_value
# this is picked up by FuncParser
FUNCPARSER_CALLABLES = {
"protkey": protfunc_callable_protkey,

View file

@ -7,10 +7,10 @@ Handling storage of prototypes, both database-based ones (DBPrototypes) and thos
import hashlib
import time
from ast import literal_eval
from django.conf import settings
from django.db.models import Q, Subquery
from django.db.models import Q
from django.core.paginator import Paginator
from django.utils.translation import gettext as _
from evennia.scripts.scripts import DefaultScript
from evennia.objects.models import ObjectDB
from evennia.typeclasses.attributes import Attribute
@ -22,10 +22,6 @@ from evennia.utils.utils import (
make_iter,
is_iter,
dbid_to_obj,
callables_from_module,
get_all_typeclasses,
to_str,
dbref,
justify,
class_from_module,
)
@ -84,7 +80,6 @@ def homogenize_prototype(prototype, custom_keys=None):
"""
Homogenize the more free-form prototype supported pre Evennia 0.7 into the stricter form.
Args:
prototype (dict): Prototype.
custom_keys (list, optional): Custom keys which should not be interpreted as attrs, beyond
@ -211,6 +206,7 @@ def load_module_prototypes():
class DbPrototype(DefaultScript):
"""
This stores a single prototype, in an Attribute `prototype`.
"""
def at_script_creation(self):
@ -262,7 +258,7 @@ def save_prototype(prototype):
prototype_key = in_prototype.get("prototype_key")
if not prototype_key:
raise ValidationError("Prototype requires a prototype_key")
raise ValidationError(_("Prototype requires a prototype_key"))
prototype_key = str(prototype_key).lower()
@ -270,7 +266,8 @@ def save_prototype(prototype):
if prototype_key in _MODULE_PROTOTYPES:
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
raise PermissionError(
"{} is a read-only prototype " "(defined as code in {}).".format(prototype_key, mod)
_("{protkey} is a read-only prototype " "(defined as code in {module}).").format(
protkey=prototype_key, module=mod)
)
# make sure meta properties are included with defaults
@ -337,20 +334,21 @@ def delete_prototype(prototype_key, caller=None):
if prototype_key in _MODULE_PROTOTYPES:
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key.lower(), "N/A")
raise PermissionError(
"{} is a read-only prototype " "(defined as code in {}).".format(prototype_key, mod)
_("{protkey} is a read-only prototype " "(defined as code in {module}).").format(
protkey=prototype_key, module=mod)
)
stored_prototype = DbPrototype.objects.filter(db_key__iexact=prototype_key)
if not stored_prototype:
raise PermissionError("Prototype {} was not found.".format(prototype_key))
raise PermissionError(_("Prototype {} was not found.").format(prototype_key))
stored_prototype = stored_prototype[0]
if caller:
if not stored_prototype.access(caller, "edit"):
raise PermissionError(
"{} needs explicit 'edit' permissions to "
"delete prototype {}.".format(caller, prototype_key)
_("{} needs explicit 'edit' permissions to "
"delete prototype {}.").format(caller, prototype_key)
)
stored_prototype.delete()
return True
@ -449,7 +447,11 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators
nmodules = len(module_prototypes)
ndbprots = db_matches.count()
if nmodules + ndbprots != 1:
raise KeyError(f"Found {nmodules + ndbprots} matching prototypes {module_prototypes}.")
raise KeyError(_(
"Found {num} matching prototypes {module_prototypes}.").format(
num=nmodules + ndbprots,
module_prototypes=module_prototypes)
)
if return_iterators:
# trying to get the entire set of prototypes - we must paginate
@ -479,10 +481,14 @@ class PrototypeEvMore(EvMore):
Listing 1000+ prototypes can be very slow. So we customize EvMore to
display an EvTable per paginated page rather than to try creating an
EvTable for the entire dataset and then paginate it.
"""
def __init__(self, caller, *args, session=None, **kwargs):
"""Store some extra properties on the EvMore class"""
"""
Store some extra properties on the EvMore class
"""
self.show_non_use = kwargs.pop("show_non_use", False)
self.show_non_edit = kwargs.pop("show_non_edit", False)
super().__init__(caller, *args, session=session, **kwargs)
@ -493,6 +499,7 @@ class PrototypeEvMore(EvMore):
and we must handle these separately since they cannot be paginated in the same
way. We will build the prototypes so that the db-prototypes come first (they
are likely the most volatile), followed by the mod-prototypes.
"""
dbprot_query, modprot_list = inp
# set the number of entries per page to half the reported height of the screen
@ -514,6 +521,7 @@ class PrototypeEvMore(EvMore):
"""
The listing is separated in db/mod prototypes, so we need to figure out which
one to pick based on the page number. Also, pageno starts from 0.
"""
dbprot_pages, modprot_list = self._data
@ -522,15 +530,16 @@ class PrototypeEvMore(EvMore):
else:
# get the correct slice, adjusted for the db-prototypes
pageno = max(0, pageno - self._npages_db)
return modprot_list[pageno * self.height : pageno * self.height + self.height]
return modprot_list[pageno * self.height: pageno * self.height + self.height]
def page_formatter(self, page):
"""Input is a queryset page from django.Paginator"""
"""
Input is a queryset page from django.Paginator
"""
caller = self._caller
# get use-permissions of readonly attributes (edit is always False)
display_tuples = []
table = EvTable(
"|wKey|n",
"|wSpawn/Edit|n",
@ -599,7 +608,7 @@ def list_prototypes(
dbprot_query, modprot_list = search_prototype(key, tags, return_iterators=True)
if not dbprot_query and not modprot_list:
caller.msg("No prototypes found.", session=session)
caller.msg(_("No prototypes found."), session=session)
return None
# get specific prototype (one value or exception)
@ -650,7 +659,7 @@ def validate_prototype(
protkey = protkey and protkey.lower() or prototype.get("prototype_key", None)
if strict and not bool(protkey):
_flags["errors"].append("Prototype lacks a `prototype_key`.")
_flags["errors"].append(_("Prototype lacks a `prototype_key`."))
protkey = "[UNSET]"
typeclass = prototype.get("typeclass")
@ -659,12 +668,13 @@ def validate_prototype(
if strict and not (typeclass or prototype_parent):
if is_prototype_base:
_flags["errors"].append(
"Prototype {} requires `typeclass` " "or 'prototype_parent'.".format(protkey)
_("Prototype {protkey} requires `typeclass` " "or 'prototype_parent'.").format(
protkey=protkey)
)
else:
_flags["warnings"].append(
"Prototype {} can only be used as a mixin since it lacks "
"a typeclass or a prototype_parent.".format(protkey)
_("Prototype {protkey} can only be used as a mixin since it lacks "
"a typeclass or a prototype_parent.").format(protkey=protkey)
)
if strict and typeclass:
@ -672,9 +682,9 @@ def validate_prototype(
class_from_module(typeclass)
except ImportError as err:
_flags["errors"].append(
"{}: Prototype {} is based on typeclass {}, which could not be imported!".format(
err, protkey, typeclass
)
_("{err}: Prototype {protkey} is based on typeclass {typeclass}, "
"which could not be imported!").format(
err=err, protkey=protkey, typeclass=typeclass)
)
# recursively traverese prototype_parent chain
@ -682,19 +692,22 @@ def validate_prototype(
for protstring in make_iter(prototype_parent):
protstring = protstring.lower()
if protkey is not None and protstring == protkey:
_flags["errors"].append("Prototype {} tries to parent itself.".format(protkey))
_flags["errors"].append(_("Prototype {protkey} tries to parent itself.").format(
protkey=protkey))
protparent = protparents.get(protstring)
if not protparent:
_flags["errors"].append(
"Prototype {}'s prototype_parent '{}' was not found.".format(protkey, protstring)
_("Prototype {protkey}'s prototype_parent '{parent}' was not found.").format(
protkey=protkey, parent=protstring)
)
if id(prototype) in _flags["visited"]:
_flags["errors"].append(
"{} has infinite nesting of prototypes.".format(protkey or prototype)
_("{protkey} has infinite nesting of prototypes.").format(
protkey=protkey or prototype)
)
if _flags["errors"]:
raise RuntimeError("Error: " + "\nError: ".join(_flags["errors"]))
raise RuntimeError(_("Error: ") + _("\nError: ").join(_flags["errors"]))
_flags["visited"].append(id(prototype))
_flags["depth"] += 1
validate_prototype(
@ -709,16 +722,16 @@ def validate_prototype(
# if we get back to the current level without a typeclass it's an error.
if strict and is_prototype_base and _flags["depth"] <= 0 and not _flags["typeclass"]:
_flags["errors"].append(
"Prototype {} has no `typeclass` defined anywhere in its parent\n "
"chain. Add `typeclass`, or a `prototype_parent` pointing to a "
"prototype with a typeclass.".format(protkey)
_("Prototype {protkey} has no `typeclass` defined anywhere in its parent\n "
"chain. Add `typeclass`, or a `prototype_parent` pointing to a "
"prototype with a typeclass.").format(protkey=protkey)
)
if _flags["depth"] <= 0:
if _flags["errors"]:
raise RuntimeError("Error: " + "\nError: ".join(_flags["errors"]))
raise RuntimeError(_("Error: " + "\nError: ").join(_flags["errors"]))
if _flags["warnings"]:
raise RuntimeWarning("Warning: " + "\nWarning: ".join(_flags["warnings"]))
raise RuntimeWarning(_("Warning: " + "\nWarning: ").join(_flags["warnings"]))
# make sure prototype_locks are set to defaults
prototype_locks = [
@ -831,10 +844,10 @@ def prototype_to_str(prototype):
category=category if category else "|wNone|n"
)
out.append(
"{attrkey}{cat_locks} |c=|n {value}".format(
"{attrkey}{cat_locks}{locks} |c=|n {value}".format(
attrkey=attrkey,
cat_locks=cat_locks,
locks=locks if locks else "|wNone|n",
locks=" |w(locks:|n {locks})".format(locks=locks) if locks else "",
value=value,
)
)

View file

@ -40,8 +40,8 @@ Possible keywords are:
supported are 'edit' and 'use'.
prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype
in listings
prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or
a list of parents, for multiple left-to-right inheritance.
prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent
prototype, or a list of parents, for multiple left-to-right inheritance.
prototype: Deprecated. Same meaning as 'parent'.
typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use
@ -138,6 +138,7 @@ import hashlib
import time
from django.conf import settings
from django.utils.translation import gettext as _
import evennia
from evennia.objects.models import ObjectDB
@ -355,8 +356,8 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implici
This is most useful for displaying.
implicit_keep (bool, optional): If set, the resulting diff will assume KEEP unless the new
prototype explicitly change them. That is, if a key exists in `prototype1` and
not in `prototype2`, it will not be REMOVEd but set to KEEP instead. This is particularly
useful for auto-generated prototypes when updating objects.
not in `prototype2`, it will not be REMOVEd but set to KEEP instead. This is
particularly useful for auto-generated prototypes when updating objects.
Returns:
diff (dict): A structure detailing how to convert prototype1 to prototype2. All
@ -469,8 +470,8 @@ def flatten_diff(diff):
out.extend(_get_all_nested_diff_instructions(val))
else:
raise RuntimeError(
"Diff contains non-dicts that are not on the "
"form (old, new, inst): {}".format(diffpart)
_("Diff contains non-dicts that are not on the "
"form (old, new, inst): {diffpart}").format(diffpart)
)
return out
@ -693,11 +694,13 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None,
elif key == "permissions":
if directive == "REPLACE":
obj.permissions.clear()
obj.permissions.batch_add(*(init_spawn_value(perm, str, caller=caller) for perm in val))
obj.permissions.batch_add(*(init_spawn_value(perm, str, caller=caller)
for perm in val))
elif key == "aliases":
if directive == "REPLACE":
obj.aliases.clear()
obj.aliases.batch_add(*(init_spawn_value(alias, str, caller=caller) for alias in val))
obj.aliases.batch_add(*(init_spawn_value(alias, str, caller=caller)
for alias in val))
elif key == "tags":
if directive == "REPLACE":
obj.tags.clear()
@ -923,7 +926,8 @@ def spawn(*prototypes, caller=None, **kwargs):
create_kwargs["db_home"] = init_spawn_value(val, value_to_obj, caller=caller)
else:
try:
create_kwargs["db_home"] = init_spawn_value(settings.DEFAULT_HOME, value_to_obj, caller=caller)
create_kwargs["db_home"] = init_spawn_value(
settings.DEFAULT_HOME, value_to_obj, caller=caller)
except ObjectDB.DoesNotExist:
# settings.DEFAULT_HOME not existing is common for unittests
pass
@ -945,7 +949,8 @@ def spawn(*prototypes, caller=None, **kwargs):
val = prot.pop("tags", [])
tags = []
for (tag, category, *data) in val:
tags.append((init_spawn_value(tag, str, caller=caller), category, data[0] if data else None))
tags.append((init_spawn_value(tag, str, caller=caller), category, data[0]
if data else None))
prototype_key = prototype.get("prototype_key", None)
if prototype_key:

View file

@ -27,11 +27,13 @@ class MonitorHandler(object):
"""
This is a resource singleton that allows for registering
callbacks for when a field or Attribute is updated (saved).
"""
def __init__(self):
"""
Initialize the handler.
"""
self.savekey = "_monitorhandler_save"
self.monitors = defaultdict(lambda: defaultdict(dict))

View file

@ -62,22 +62,28 @@ class TaskHandlerTask:
return TASK_HANDLER.get_deferred(self.task_id)
def pause(self):
"""Pause the callback of a task.
To resume use TaskHandlerTask.unpause
"""
Pause the callback of a task.
To resume use `TaskHandlerTask.unpause`.
"""
d = self.deferred
if d:
d.pause()
def unpause(self):
"""Unpause a task, run the task if it has passed delay time."""
"""
Unpause a task, run the task if it has passed delay time.
"""
d = self.deferred
if d:
d.unpause()
@property
def paused(self):
"""A task attribute to check if the deferred instance of a task has been paused.
"""
A task attribute to check if the deferred instance of a task has been paused.
This exists to mock usage of a twisted deferred object.
@ -93,7 +99,8 @@ class TaskHandlerTask:
return None
def do_task(self):
"""Execute the task (call its callback).
"""
Execute the task (call its callback).
If calling before timedelay, cancel the deferred instance affliated to this task.
Remove the task from the dictionary of current tasks on a successful
callback.
@ -106,7 +113,8 @@ class TaskHandlerTask:
return TASK_HANDLER.do_task(self.task_id)
def call(self):
"""Call the callback of a task.
"""
Call the callback of a task.
Leave the task unaffected otherwise.
This does not use the task's deferred instance.
The only requirement is that the task exist in task handler.
@ -173,7 +181,8 @@ class TaskHandlerTask:
return None
def exists(self):
"""Check if a task exists.
"""
Check if a task exists.
Most task handler methods check for existence for you.
Returns:
@ -183,7 +192,8 @@ class TaskHandlerTask:
return TASK_HANDLER.exists(self.task_id)
def get_id(self):
""" Returns the global id for this task. For use with
"""
Returns the global id for this task. For use with
`evennia.scripts.taskhandler.TASK_HANDLER`.
Returns:
@ -215,7 +225,7 @@ class TaskHandler(object):
self.clock = reactor
# number of seconds before an uncalled canceled task is removed from TaskHandler
self.stale_timeout = 60
self._now = False # used in unit testing to manually set now time
self._now = False # used in unit testing to manually set now time
def load(self):
"""Load from the ServerConfig.
@ -271,7 +281,10 @@ class TaskHandler(object):
return True
def save(self):
"""Save the tasks in ServerConfig."""
"""
Save the tasks in ServerConfig.
"""
for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items():
if task_id in self.to_save:
@ -286,14 +299,12 @@ class TaskHandler(object):
callback = (obj, name)
# Check if callback can be pickled. args and kwargs have been checked
safe_callback = None
self.to_save[task_id] = dbserialize((date, callback, args, kwargs))
ServerConfig.objects.conf("delayed_tasks", self.to_save)
def add(self, timedelay, callback, *args, **kwargs):
"""Add a new task.
"""
Add a new task.
If the persistent kwarg is truthy:
The callback, args and values for kwarg will be serialized. Type
@ -399,7 +410,8 @@ class TaskHandler(object):
return TaskHandlerTask(task_id)
def exists(self, task_id):
"""Check if a task exists.
"""
Check if a task exists.
Most task handler methods check for existence for you.
Args:
@ -415,7 +427,8 @@ class TaskHandler(object):
return False
def active(self, task_id):
"""Check if a task is active (has not been called yet).
"""
Check if a task is active (has not been called yet).
Args:
task_id (int): an existing task ID.
@ -433,7 +446,8 @@ class TaskHandler(object):
return False
def cancel(self, task_id):
"""Stop a task from automatically executing.
"""
Stop a task from automatically executing.
This will not remove the task.
Args:
@ -459,7 +473,8 @@ class TaskHandler(object):
return False
def remove(self, task_id):
"""Remove a task without executing it.
"""
Remove a task without executing it.
Deletes the instance of the task's deferred.
Args:
@ -485,8 +500,8 @@ class TaskHandler(object):
return True
def clear(self, save=True, cancel=True):
"""clear all tasks.
By default tasks are canceled and removed from the database also.
"""
Clear all tasks. By default tasks are canceled and removed from the database as well.
Args:
save=True (bool): Should changes to persistent tasks be saved to database.
@ -508,7 +523,8 @@ class TaskHandler(object):
return True
def call_task(self, task_id):
"""Call the callback of a task.
"""
Call the callback of a task.
Leave the task unaffected otherwise.
This does not use the task's deferred instance.
The only requirement is that the task exist in task handler.
@ -528,7 +544,8 @@ class TaskHandler(object):
return callback(*args, **kwargs)
def do_task(self, task_id):
"""Execute the task (call its callback).
"""
Execute the task (call its callback).
If calling before timedelay cancel the deferred instance affliated to this task.
Remove the task from the dictionary of current tasks on a successful
callback.
@ -573,7 +590,8 @@ class TaskHandler(object):
return None
def create_delays(self):
"""Create the delayed tasks for the persistent tasks.
"""
Create the delayed tasks for the persistent tasks.
This method should be automatically called when Evennia starts.
"""

View file

@ -9,8 +9,6 @@ manager's conf() method.
"""
from django.db import models
from django.urls import reverse
from django.contrib.contenttypes.models import ContentType
from evennia.utils.idmapper.models import WeakSharedMemoryModel
from evennia.utils import logger, utils

View file

@ -93,7 +93,10 @@ def loads(data):
def _get_logger():
"Delay import of logger until absolutely necessary"
"""
Delay import of logger until absolutely necessary
"""
global _LOGGER
if not _LOGGER:
from evennia.utils import logger as _LOGGER
@ -102,7 +105,10 @@ def _get_logger():
@wraps
def catch_traceback(func):
"Helper decorator"
"""
Helper decorator
"""
def decorator(*args, **kwargs):
try:
@ -353,6 +359,7 @@ class AMPMultiConnectionProtocol(amp.AMP):
def dataReceived(self, data):
"""
Handle non-AMP messages, such as HTTP communication.
"""
# print("dataReceived: {}".format(data))
if data[:1] == NUL:
@ -413,6 +420,7 @@ class AMPMultiConnectionProtocol(amp.AMP):
that is irrelevant. If a true connection error happens, the
portal will continuously try to reconnect, showing the problem
that way.
"""
# print("ConnectionLost: {}: {}".format(self, reason))
try:
@ -422,20 +430,20 @@ class AMPMultiConnectionProtocol(amp.AMP):
# Error handling
def errback(self, e, info):
def errback(self, err, info):
"""
Error callback.
Handles errors to avoid dropping connections on server tracebacks.
Args:
e (Failure): Deferred error instance.
err (Failure): Deferred error instance.
info (str): Error string.
"""
e.trap(Exception)
err.trap(Exception)
_get_logger().log_err(
"AMP Error from {info}: {trcbck} {err}".format(
info=info, trcbck=e.getTraceback(), err=e.getErrorMessage()
info=info, trcbck=err.getTraceback(), err=err.getErrorMessage()
)
)

View file

@ -43,7 +43,10 @@ class AMPServerFactory(protocol.ServerFactory):
noisy = False
def logPrefix(self):
"How this is named in logs"
"""
How this is named in logs
"""
return "AMP"
def __init__(self, portal):

View file

@ -335,7 +335,7 @@ class GrapevineClient(WebSocketClientProtocol, Session):
# incoming broadcast from network
payload = data["payload"]
print("channels/broadcast:", payload["channel"], self.channel)
# print("channels/broadcast:", payload["channel"], self.channel)
if str(payload["channel"]) != self.channel:
# only echo from channels this particular bot actually listens to
return

View file

@ -183,6 +183,7 @@ class Portal(object):
Returns:
server_twistd_cmd (list): An instruction for starting the server, to pass to Popen.
"""
server_twistd_cmd = [
"twistd",
@ -196,7 +197,10 @@ class Portal(object):
return server_twistd_cmd
def get_info_dict(self):
"Return the Portal info, for display."
"""
Return the Portal info, for display.
"""
return INFO_DICT
def shutdown(self, _reactor_stopping=False, _stop_server=False):
@ -354,7 +358,8 @@ 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)
@ -390,7 +395,7 @@ if WEBSERVER_ENABLED:
if WEBSOCKET_CLIENT_ENABLED and not websocket_started:
# start websocket client port for the webclient
# we only support one websocket client
from evennia.server.portal import webclient
from evennia.server.portal import webclient # noqa
from autobahn.twisted.websocket import WebSocketServerFactory
w_interface = WEBSOCKET_CLIENT_INTERFACE
@ -417,10 +422,13 @@ if WEBSERVER_ENABLED:
if WEB_PLUGINS_MODULE:
try:
web_root = WEB_PLUGINS_MODULE.at_webproxy_root_creation(web_root)
except Exception as e: # Legacy user has not added an at_webproxy_root_creation function in existing web plugins file
except Exception:
# Legacy user has not added an at_webproxy_root_creation function in existing
# web plugins file
INFO_DICT["errors"] = (
"WARNING: WEB_PLUGINS_MODULE is enabled but at_webproxy_root_creation() not found - "
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf."
"WARNING: WEB_PLUGINS_MODULE is enabled but at_webproxy_root_creation() "
"not found copy 'evennia/game_template/server/conf/web_plugins.py to "
"mygame/server/conf."
)
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
web_root.is_portal = True
@ -435,4 +443,3 @@ for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
# external plugin services to start
if plugin_module:
plugin_module.start_plugin_services(PORTAL)

View file

@ -1,5 +1,6 @@
"""
Sessionhandler for portal sessions
Sessionhandler for portal sessions.
"""
@ -11,6 +12,7 @@ from evennia.server.sessionhandler import SessionHandler
from evennia.server.portal.amp import PCONN, PDISCONN, PCONNSYNC, PDISCONNALL
from evennia.utils.logger import log_trace
from evennia.utils.utils import class_from_module
from django.utils.translation import gettext as _
# module import
_MOD_IMPORT = None
@ -35,6 +37,8 @@ DUMMYSESSION = namedtuple("DummySession", ["sessid"])(0)
# Portal-SessionHandler class
# -------------------------------------------------------------
DOS_PROTECTION_MSG = _("{servername} DoS protection is active. You are queued to connect in {num} seconds ...")
class PortalSessionHandler(SessionHandler):
"""
@ -111,16 +115,12 @@ class PortalSessionHandler(SessionHandler):
_CONNECTION_QUEUE.appendleft(session)
if len(_CONNECTION_QUEUE) > 1:
session.data_out(
text=[
[
"%s DoS protection is active. You are queued to connect in %g seconds ..."
% (
settings.SERVERNAME,
len(_CONNECTION_QUEUE) * _MIN_TIME_BETWEEN_CONNECTS,
)
],
text=(
(DOS_PROTECTION_MSG.format(
servername=settings.SERVERNAME,
num=len(_CONNECTION_QUEUE) * _MIN_TIME_BETWEEN_CONNECTS),),
{},
]
)
)
now = time.time()
if (
@ -220,6 +220,7 @@ class PortalSessionHandler(SessionHandler):
def disconnect_all(self):
"""
Disconnect all sessions, informing the Server.
"""
def _callback(result, sessionhandler):
@ -478,5 +479,4 @@ class PortalSessionHandler(SessionHandler):
_PORTAL_SESSION_HANDLER_CLASS = class_from_module(settings.PORTAL_SESSION_HANDLER_CLASS)
PORTAL_SESSIONS = _PORTAL_SESSION_HANDLER_CLASS()

View file

@ -145,6 +145,7 @@ class RSSBotFactory(object):
def start(self):
"""
Called by portalsessionhandler. Starts the bot.
"""
def errback(fail):

View file

@ -61,24 +61,25 @@ CTRL_D = "\x04"
CTRL_BACKSLASH = "\x1c"
CTRL_L = "\x0c"
_NO_AUTOGEN = """
_NO_AUTOGEN = f"""
Evennia could not generate SSH private- and public keys ({{err}})
Using conch default keys instead.
If this error persists, create the keys manually (using the tools for your OS)
and put them here:
{}
{}
""".format(
_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE
)
{_PRIVATE_KEY_FILE}
{_PUBLIC_KEY_FILE}
"""
_BASE_SESSION_CLASS = class_from_module(settings.BASE_SESSION_CLASS)
# not used atm
class SSHServerFactory(protocol.ServerFactory):
"This is only to name this better in logs"
"""
This is only to name this better in logs
"""
noisy = False
def logPrefix(self):

View file

@ -50,6 +50,7 @@ class SSLProtocol(_TELNET_PROTOCOL_CLASS):
"""
Communication is the same as telnet, except data transfer
is done with encryption.
"""
def __init__(self, *args, **kwargs):
@ -62,6 +63,7 @@ def verify_SSL_key_and_cert(keyfile, certfile):
This function looks for RSA key and certificate in the current
directory. If files ssl.key and ssl.cert does not exist, they
are created.
"""
if not (os.path.exists(keyfile) and os.path.exists(certfile)):
@ -74,10 +76,11 @@ def verify_SSL_key_and_cert(keyfile, certfile):
try:
# create the RSA key and store it.
KEY_LENGTH = 1024
rsaKey = Key(RSA.generate(KEY_LENGTH))
keyString = rsaKey.toString(type="OPENSSH")
file(keyfile, "w+b").write(keyString)
KEY_LENGTH = 2048
rsa_key = Key(RSA.generate(KEY_LENGTH))
key_string = rsa_key.toString(type="OPENSSH")
with open(keyfile, "w+b") as fil:
fil.write(key_string)
except Exception as err:
print(NO_AUTOGEN.format(err=err, keyfile=keyfile))
sys.exit(5)

View file

@ -59,7 +59,10 @@ _BASE_SESSION_CLASS = class_from_module(settings.BASE_SESSION_CLASS)
class TelnetServerFactory(protocol.ServerFactory):
"This is only to name this better in logs"
"""
This exists only to name this better in logs.
"""
noisy = False
def logPrefix(self):
@ -71,6 +74,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
Each player connecting over telnet (ie using most traditional mud
clients) gets a telnet protocol instance assigned to them. All
communication between game and player goes through here.
"""
def __init__(self, *args, **kwargs):
@ -81,6 +85,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
"""
Unused by default, but a good place to put debug printouts
of incoming data.
"""
# print(f"telnet dataReceived: {data}")
try:
@ -145,11 +150,15 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
Client refuses do(linemode). This is common for MUD-specific
clients, but we must ask for the sake of raw telnet. We ignore
this error.
"""
pass
def _send_nop_keepalive(self):
"""Send NOP keepalive unless flag is set"""
"""
Send NOP keepalive unless flag is set
"""
if self.protocol_flags.get("NOPKEEPALIVE"):
self._write(IAC + NOP)
@ -158,7 +167,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
Allow to toggle the NOP keepalive for those sad clients that
can't even handle a NOP instruction. This is turned off by the
protocol_flag NOPKEEPALIVE (settable e.g. by the default
`@option` command).
`option` command).
"""
if self.nop_keep_alive and self.nop_keep_alive.running:
self.nop_keep_alive.stop()
@ -172,6 +182,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
When all have reported, a sync with the server is performed.
The system will force-call this sync after a small time to handle
clients that don't reply to handshakes at all.
"""
if timeout:
if self.handshakes > 0:
@ -186,6 +197,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
def at_login(self):
"""
Called when this session gets authenticated by the server.
"""
pass
@ -321,7 +333,10 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
self.data_in(text=dat + b"\n")
def _write(self, data):
"""hook overloading the one used in plain telnet"""
"""
Hook overloading the one used in plain telnet
"""
data = data.replace(b"\n", b"\r\n").replace(b"\r\r\n", b"\r\n")
super()._write(mccp_compress(self, data))
@ -347,7 +362,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
def disconnect(self, reason=""):
"""
generic hook for the engine to call in order to
Generic hook for the engine to call in order to
disconnect this protocol.
Args:
@ -376,6 +391,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
Keyword Args:
kwargs (any): Options to the protocol
"""
self.sessionhandler.data_out(self, **kwargs)
@ -442,7 +458,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
prompt = mxp_parse(prompt)
prompt = to_bytes(prompt, self)
prompt = prompt.replace(IAC, IAC + IAC).replace(b"\n", b"\r\n")
if not self.protocol_flags.get("NOPROMPTGOAHEAD",
if not self.protocol_flags.get("NOPROMPTGOAHEAD",
self.protocol_flags.get("NOGOAHEAD", True)):
prompt += IAC + GA
self.transport.write(mccp_compress(self, prompt))
@ -488,6 +504,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
def send_default(self, cmdname, *args, **kwargs):
"""
Send other oob data
"""
if not cmdname == "options":
self.oob.data_out(cmdname, *args, **kwargs)

View file

@ -46,32 +46,29 @@ _CERTIFICATE_ISSUER = {
# messages
NO_AUTOGEN = """
NO_AUTOGEN = f"""
Evennia could not auto-generate the SSL private- and public keys ({{err}}).
If this error persists, create them manually (using the tools for your OS). The files
should be placed and named like this:
{}
{}
""".format(
_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE
)
{_PRIVATE_KEY_FILE}
{_PUBLIC_KEY_FILE}
"""
NO_AUTOCERT = """
Evennia's could not auto-generate the SSL certificate ({{err}}).
The private key already exists here:
{}
{_PRIVATE_KEY_FILE}
If this error persists, create the certificate manually (using the private key and
the tools for your OS). The file should be placed and named like this:
{}
""".format(
_PRIVATE_KEY_FILE, _CERTIFICATE_FILE
)
{_CERTIFICATE_FILE}
"""
class SSLProtocol(TelnetProtocol):
"""
Communication is the same as telnet, except data transfer
is done with encryption set up by the portal at start time.
"""
def __init__(self, *args, **kwargs):

View file

@ -44,6 +44,7 @@ _BASE_SESSION_CLASS = class_from_module(settings.BASE_SESSION_CLASS)
class WebSocketClient(WebSocketServerProtocol, _BASE_SESSION_CLASS):
"""
Implements the server-side of the Websocket connection.
"""
# nonce value, used to prevent the webclient from erasing the
@ -155,7 +156,7 @@ class WebSocketClient(WebSocketServerProtocol, _BASE_SESSION_CLASS):
# in case anyone wants to expose this functionality later.
#
# sendClose() under autobahn/websocket/interfaces.py
ret = self.sendClose(CLOSE_NORMAL, reason)
self.sendClose(CLOSE_NORMAL, reason)
def onClose(self, wasClean, code=None, reason=None):
"""

View file

@ -15,6 +15,7 @@ http://localhost:4001/webclient.)
The WebClient resource in this module will
handle these requests and act as a gateway
to sessions connected over the webclient.
"""
import json
import re
@ -27,7 +28,7 @@ from django.utils.functional import Promise
from django.conf import settings
from evennia.utils.ansi import parse_ansi
from evennia.utils import utils
from evennia.utils.utils import to_bytes, to_str
from evennia.utils.utils import to_bytes
from evennia.utils.text2html import parse_html
from evennia.server import session
@ -223,10 +224,13 @@ class AjaxWebClient(resource.Resource):
return jsonify({"msg": host_string, "csessid": csessid})
def mode_keepalive(self, request):
"""
This is called by render_POST when the
client is replying to the keepalive.
Args:
request (Request): Incoming request.
"""
csessid = self.get_client_sessid(request)
self.last_alive[csessid] = (time.time(), False)

View file

@ -174,6 +174,7 @@ class Evennia:
The main Evennia server handler. This object sets up the database and
tracks and interlinks all the twisted network services that make up
evennia.
"""
def __init__(self, application):
@ -243,6 +244,7 @@ class Evennia:
This allows for changing default cmdset locations and default
typeclasses in the settings file and have them auto-update all
already existing objects.
"""
global INFO_DICT
@ -471,7 +473,10 @@ class Evennia:
ServerConfig.objects.conf("runtime", _GAMETIME_MODULE.runtime())
def get_info_dict(self):
"Return the server info, for display."
"""
Return the server info, for display.
"""
return INFO_DICT
# server start/stop hooks
@ -480,6 +485,7 @@ class Evennia:
"""
This is called every time the server starts up, regardless of
how it was shut down.
"""
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_start()
@ -488,6 +494,7 @@ class Evennia:
"""
This is called just before a server is shut down, regardless
of it is fore a reload, reset or shutdown.
"""
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_stop()
@ -495,6 +502,7 @@ class Evennia:
def at_server_reload_start(self):
"""
This is called only when server starts back up after a reload.
"""
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_reload_start()
@ -505,7 +513,7 @@ class Evennia:
after reconnecting.
Args:
mode (str): One of reload, reset or shutdown.
mode (str): One of 'reload', 'reset' or 'shutdown'.
"""
@ -555,6 +563,7 @@ class Evennia:
def at_server_reload_stop(self):
"""
This is called only time the server stops before a reload.
"""
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_reload_stop()
@ -563,6 +572,7 @@ class Evennia:
"""
This is called only when the server starts "cold", i.e. after a
shutdown or a reset.
"""
# We need to do this just in case the server was killed in a way where
# the normal cleanup operations did not have time to run.
@ -590,6 +600,7 @@ class Evennia:
def at_server_cold_stop(self):
"""
This is called only when the server goes down due to a shutdown or reset.
"""
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_cold_stop()

View file

@ -13,7 +13,6 @@ from evennia.comms.models import ChannelDB
from evennia.utils import logger
from evennia.utils.utils import make_iter, lazy_property, class_from_module
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
@ -22,9 +21,6 @@ _SA = object.__setattr__
_ObjectDB = None
_ANSI = None
# i18n
from django.utils.translation import gettext as _
_BASE_SESSION_CLASS = class_from_module(settings.BASE_SESSION_CLASS)
@ -45,7 +41,10 @@ class ServerSession(_BASE_SESSION_CLASS):
"""
def __init__(self):
"""Initiate to avoid AttributeErrors down the line"""
"""
Initiate to avoid AttributeErrors down the line
"""
self.puppet = None
self.account = None
self.cmdset_storage_string = ""
@ -321,7 +320,10 @@ class ServerSession(_BASE_SESSION_CLASS):
self.sessionhandler.data_in(session or self, **kwargs)
def __eq__(self, other):
"""Handle session comparisons"""
"""
Handle session comparisons
"""
try:
return self.address == other.address
except AttributeError:
@ -368,6 +370,7 @@ class ServerSession(_BASE_SESSION_CLASS):
def at_cmdset_get(self, **kwargs):
"""
A dummy hook all objects with cmdsets need to have
"""
pass
@ -414,7 +417,10 @@ class ServerSession(_BASE_SESSION_CLASS):
# @ndb.deleter
def ndb_del(self):
"""Stop accidental deletion."""
"""
Stop accidental deletion.
"""
raise Exception("Cannot delete the ndb object!")
ndb = property(ndb_get, ndb_set, ndb_del)
@ -423,5 +429,8 @@ class ServerSession(_BASE_SESSION_CLASS):
# Mock access method for the session (there is no lock info
# at this stage, so we just present a uniform API)
def access(self, *args, **kwargs):
"""Dummy method to mimic the logged-in API."""
"""
Dummy method to mimic the logged-in API.
"""
return True

View file

@ -18,7 +18,6 @@ from django.conf import settings
from evennia.commands.cmdhandler import CMD_LOGINSTART
from evennia.utils.logger import log_trace
from evennia.utils.utils import (
variable_from_module, class_from_module,
is_iter,
make_iter,
delay,
@ -29,6 +28,7 @@ from evennia.server.portal import amp
from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT
from evennia.server.signals import SIGNAL_ACCOUNT_POST_FIRST_LOGIN, SIGNAL_ACCOUNT_POST_LAST_LOGOUT
from codecs import decode as codecs_decode
from django.utils.translation import gettext as _
_FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED
@ -39,7 +39,7 @@ _ServerConfig = None
_ScriptDB = None
_OOB_HANDLER = None
_ERR_BAD_UTF8 = "Your client sent an incorrect UTF-8 sequence."
_ERR_BAD_UTF8 = _("Your client sent an incorrect UTF-8 sequence.")
class DummySession(object):
@ -48,9 +48,6 @@ class DummySession(object):
DUMMYSESSION = DummySession()
# i18n
from django.utils.translation import gettext as _
_SERVERNAME = settings.SERVERNAME
_MULTISESSION_MODE = settings.MULTISESSION_MODE
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
@ -61,7 +58,6 @@ _MODEL_MAP = None
_FUNCPARSER = None
# input handlers
_INPUT_FUNCS = {}
@ -103,24 +99,36 @@ class SessionHandler(dict):
"""
def __getitem__(self, key):
"Clean out None-sessions automatically."
"""
Clean out None-sessions automatically.
"""
if None in self:
del self[None]
return super().__getitem__(key)
def get(self, key, default=None):
"Clean out None-sessions automatically."
"""
Clean out None-sessions automatically.
"""
if None in self:
del self[None]
return super().get(key, default)
def __setitem__(self, key, value):
"Don't assign None sessions"
"""
Don't assign None sessions"
"""
if key is not None:
super().__setitem__(key, value)
def __contains__(self, key):
"None-keys are not accepted."
"""
None-keys are not accepted.
"""
return False if key is None else super().__contains__(key)
def get_sessions(self, include_unloggedin=False):
@ -158,9 +166,8 @@ class SessionHandler(dict):
Args:
session (Session): The relevant session instance.
kwargs (dict) Each keyword represents a send-instruction, with the keyword itself being the name
of the instruction (like "text"). Suitable values for each
keyword are:
kwargs (dict) Each keyword represents a send-instruction, with the keyword itself being
the name of the instruction (like "text"). Suitable values for each keyword are:
- arg -> [[arg], {}]
- [args] -> [[args], {}]
- {kwargs} -> [[], {kwargs}]
@ -177,7 +184,8 @@ class SessionHandler(dict):
global _FUNCPARSER
if not _FUNCPARSER:
from evennia.utils.funcparser import FuncParser
_FUNCPARSER = FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES, raise_errors=True)
_FUNCPARSER = FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES,
raise_errors=True)
options = kwargs.pop("options", None) or {}
raw = options.get("raw", False)
@ -199,7 +207,10 @@ class SessionHandler(dict):
return data
def _validate(data):
"Helper function to convert data to AMP-safe (picketable) values"
"""
Helper function to convert data to AMP-safe (picketable) values"
"""
if isinstance(data, dict):
newdict = {}
for key, part in data.items():
@ -210,7 +221,8 @@ class SessionHandler(dict):
elif isinstance(data, (str, bytes)):
data = _utf8(data)
if _FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED and not raw and isinstance(self, ServerSessionHandler):
if (_FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED
and not raw and isinstance(self, ServerSessionHandler)):
# only apply funcparser on the outgoing path (sessionhandler->)
# data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
data = _FUNCPARSER.parse(data, strip=strip_inlinefunc, session=session)
@ -261,14 +273,11 @@ class SessionHandler(dict):
class ServerSessionHandler(SessionHandler):
"""
This object holds the stack of sessions active in the game at
any time.
This object holds the stack of sessions active in the game at any time.
A session register with the handler in two steps, first by
registering itself with the connect() method. This indicates an
non-authenticated session. Whenever the session is authenticated
the session together with the related account is sent to the login()
method.
A session register with the handler in two steps, first by registering itself with the connect()
method. This indicates an non-authenticated session. Whenever the session is authenticated the
session together with the related account is sent to the login() method.
"""
@ -468,9 +477,8 @@ class ServerSessionHandler(SessionHandler):
def login(self, session, account, force=False, testmode=False):
"""
Log in the previously unloggedin session and the account we by
now should know is connected to it. After this point we assume
the session to be logged in one way or another.
Log in the previously unloggedin session and the account we by now should know is connected
to it. After this point we assume the session to be logged in one way or another.
Args:
session (Session): The Session to authenticate.
@ -627,7 +635,8 @@ class ServerSessionHandler(SessionHandler):
# mean connecting from the same host would not catch duplicates
sid = id(curr_session)
doublet_sessions = [
sess for sess in self.values() if sess.logged_in and sess.uid == uid and id(sess) != sid
sess for sess in self.values()
if sess.logged_in and sess.uid == uid and id(sess) != sid
]
for session in doublet_sessions:
@ -737,8 +746,8 @@ class ServerSessionHandler(SessionHandler):
puppet (Object): Object puppeted
Returns.
sessions (Session or list): Can be more than one of Object is controlled by
more than one Session (MULTISESSION_MODE > 1).
sessions (Session or list): Can be more than one of Object is controlled by more than
one Session (MULTISESSION_MODE > 1).
"""
sessions = puppet.sessid.get()

View file

@ -2,9 +2,10 @@ from django.core.cache import caches
from collections import deque
from evennia.utils import logger
import time
from django.utils.translation import gettext as _
class Throttle(object):
class Throttle:
"""
Keeps a running count of failed actions per IP address.
@ -17,7 +18,7 @@ class Throttle(object):
caches for automatic key eviction and persistence configurability.
"""
error_msg = "Too many failed attempts; you must wait a few minutes before trying again."
error_msg = _("Too many failed attempts; you must wait a few minutes before trying again.")
def __init__(self, **kwargs):
"""
@ -34,7 +35,7 @@ class Throttle(object):
"""
try:
self.storage = caches['throttle']
except Exception as e:
except Exception:
logger.log_trace("Throttle: Errors encountered; using default cache.")
self.storage = caches['default']
@ -123,7 +124,10 @@ class Throttle(object):
# If this makes it engage, log a single activation event
if not previously_throttled and currently_throttled:
logger.log_sec(f"Throttle Activated: {failmsg} (IP: {ip}, {self.limit} hits in {self.timeout} seconds.)")
logger.log_sec(
f"Throttle Activated: {failmsg} (IP: {ip}, "
f"{self.limit} hits in {self.timeout} seconds.)"
)
self.record_ip(ip)
@ -136,14 +140,15 @@ class Throttle(object):
"""
exists = self.get(ip)
if not exists: return False
if not exists:
return False
cache_key = self.get_cache_key(ip)
self.storage.delete(cache_key)
self.unrecord_ip(ip)
# Return True if NOT exists
return ~bool(self.get(ip))
return not bool(self.get(ip))
def record_ip(self, ip, *args, **kwargs):
"""

View file

@ -10,7 +10,6 @@ which is a non-db version of Attributes.
"""
import re
import fnmatch
import weakref
from collections import defaultdict
@ -117,6 +116,7 @@ class IAttribute:
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.
@ -126,11 +126,12 @@ class InMemoryAttribute(IAttribute):
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.
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
@ -245,8 +246,8 @@ class Attribute(IAttribute, SharedMemoryModel):
lock_storage = property(__lock_storage_get, __lock_storage_set, __lock_storage_del)
# value property (wraps db_value)
# @property
def __value_get(self):
@property
def value(self):
"""
Getter. Allows for `value = self.value`.
We cannot cache here since it makes certain cases (such
@ -255,8 +256,8 @@ class Attribute(IAttribute, SharedMemoryModel):
"""
return from_pickle(self.db_value, db_obj=self)
# @value.setter
def __value_set(self, new_value):
@value.setter
def value(self, new_value):
"""
Setter. Allows for self.value = value. We cannot cache here,
see self.__value_get.
@ -264,14 +265,11 @@ class Attribute(IAttribute, SharedMemoryModel):
self.db_value = to_pickle(new_value)
self.save(update_fields=["db_value"])
# @value.deleter
def __value_del(self):
@value.deleter
def value(self):
"""Deleter. Allows for del attr.value. This removes the entire attribute."""
self.delete()
value = property(__value_get, __value_set, __value_del)
#
# Handlers making use of the Attribute model
#
@ -348,7 +346,7 @@ class IAttributeBackend:
def _get_cache_key(self, key, category):
"""
Fetch cache key.
Args:
key (str): The key of the Attribute being searched for.
@ -529,7 +527,8 @@ class IAttributeBackend:
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.
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)
@ -714,7 +713,8 @@ class IAttributeBackend:
]
)
else:
# have to cast the results to a list or we'll get a RuntimeError for removing from the dict we're iterating
# 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()
@ -735,10 +735,10 @@ class IAttributeBackend:
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.
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.
"""
@ -810,7 +810,8 @@ class InMemoryAttributeBackend(IAttributeBackend):
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.
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.
@ -929,8 +930,9 @@ class AttributeHandler:
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.
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)
@ -1263,6 +1265,7 @@ class DbHolder:
all = property(get_all)
#
# Nick templating
#
@ -1282,7 +1285,7 @@ This happens in two steps:
This will be converted to the following regex:
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
Supported template markers (through fnmatch)
* matches anything (non-greedy) -> .*?
@ -1345,7 +1348,7 @@ def initialize_nick_templates(pattern, replacement, pattern_is_regex=False):
# groups. we need to split out any | - separated parts so we can
# attach the line-break/ending extras all regexes require.
pattern_regex_string = r"|".join(
or_part + r"(?:[\n\r]*?)\Z"
or_part + r"(?:[\n\r]*?)\Z"
for or_part in _RE_OR.split(pattern))
else:

View file

@ -595,13 +595,21 @@ class TypeclassManager(TypedObjectManager):
Search by supplying a string with optional extra search criteria to aid the query.
Args:
query (str): A search criteria that accepts extra search criteria on the
query (str): A search criteria that accepts extra search criteria on the following
forms:
[key|alias|#dbref...]
[tag==<tagstr>[:category]...]
[attr==<key>:<value>:category...]
All three can be combined in the same query, separated by spaces.
following forms: [key|alias|#dbref...] [tag==<tagstr>[:category]...] [attr==<key>:<value>:category...]
" != " != "
Returns:
matches (queryset): A queryset result matching all queries exactly. If wanting to use spaces or
==, != in tags or attributes, enclose them in quotes.
matches (queryset): A queryset result matching all queries exactly. If wanting to use
spaces or ==, != in tags or attributes, enclose them in quotes.
Example:
house = smart_search("key=foo alias=bar tag=house:building tag=magic attr=color:red")
Note:
The flexibility of this method is limited by the input line format. Tag/attribute

View file

@ -69,6 +69,7 @@ _SA = object.__setattr__
def call_at_first_save(sender, instance, created, **kwargs):
"""
Receives a signal just after the object is saved.
"""
if created:
instance.at_first_save()
@ -77,6 +78,7 @@ def call_at_first_save(sender, instance, created, **kwargs):
def remove_attributes_on_delete(sender, instance, **kwargs):
"""
Wipe object's Attributes when it's deleted
"""
instance.db_attributes.all().delete()
@ -98,6 +100,7 @@ class TypeclassBase(SharedMemoryModelBase):
Metaclass which should be set for the root of model proxies
that don't define any new fields, like Object, Script etc. This
is the basis for the typeclassing system.
"""
def __new__(cls, name, bases, attrs):
@ -208,7 +211,8 @@ class TypedObject(SharedMemoryModel):
"typeclass",
max_length=255,
null=True,
help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.",
help_text="this defines what 'type' of entity this is. This variable holds "
"a Python path to a module with a valid Evennia Typeclass.",
db_index=True,
)
# Creation date. This is not changed once the object is created.
@ -217,16 +221,20 @@ class TypedObject(SharedMemoryModel):
db_lock_storage = models.TextField(
"locks",
blank=True,
help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.",
help_text="locks limit access to an entity. A lock is defined as a 'lock string' "
"on the form 'type:lockfunctions', defining what functionality is locked and "
"how to determine access. Not defining a lock means no access is granted.",
)
# many2many relationships
db_attributes = models.ManyToManyField(
Attribute,
help_text="attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).",
help_text="attributes on this object. An attribute can hold any pickle-able "
"python object (see docs for special cases).",
)
db_tags = models.ManyToManyField(
Tag,
help_text="tags on this object. Tags are simple string markers to identify, group and alias objects.",
help_text="tags on this object. Tags are simple string markers to identify, "
"group and alias objects.",
)
# Database manager
@ -701,8 +709,8 @@ class TypedObject(SharedMemoryModel):
# Attribute storage
#
# @property db
def __db_get(self):
@property
def db(self):
"""
Attribute handler wrapper. Allows for the syntax
@ -725,26 +733,24 @@ class TypedObject(SharedMemoryModel):
self._db_holder = DbHolder(self, "attributes")
return self._db_holder
# @db.setter
def __db_set(self, value):
@db.setter
def db(self, value):
"Stop accidentally replacing the db object"
string = "Cannot assign directly to db object! "
string += "Use db.attr=value instead."
raise Exception(string)
# @db.deleter
def __db_del(self):
@db.deleter
def db(self):
"Stop accidental deletion."
raise Exception("Cannot delete the db object!")
db = property(__db_get, __db_set, __db_del)
#
# Non-persistent (ndb) storage
#
# @property ndb
def __ndb_get(self):
@property
def ndb(self):
"""
A non-attr_obj store (ndb: NonDataBase). Everything stored
to this is guaranteed to be cleared when a server is shutdown.
@ -757,20 +763,18 @@ class TypedObject(SharedMemoryModel):
self._ndb_holder = DbHolder(self, "nattrhandler", manager_name="nattributes")
return self._ndb_holder
# @db.setter
def __ndb_set(self, value):
@ndb.setter
def ndb(self, value):
"Stop accidentally replacing the ndb object"
string = "Cannot assign directly to ndb object! "
string += "Use ndb.attr=value instead."
raise Exception(string)
# @db.deleter
def __ndb_del(self):
@ndb.deleter
def ndb(self):
"Stop accidental deletion."
raise Exception("Cannot delete the ndb object!")
ndb = property(__ndb_get, __ndb_set, __ndb_del)
def get_display_name(self, looker, **kwargs):
"""
Displays the name of the object in a viewer-aware manner.
@ -879,7 +883,7 @@ class TypedObject(SharedMemoryModel):
"""
try:
return reverse("%s-create" % slugify(cls._meta.verbose_name))
except:
except Exception:
return "#"
def web_get_detail_url(self):
@ -919,7 +923,7 @@ class TypedObject(SharedMemoryModel):
"%s-detail" % slugify(self._meta.verbose_name),
kwargs={"pk": self.pk, "slug": slugify(self.name)},
)
except:
except Exception:
return "#"
def web_get_puppet_url(self):
@ -931,19 +935,17 @@ class TypedObject(SharedMemoryModel):
str: URI path to object puppet page, if defined.
Examples:
::
```python
Oscar (Character) = '/characters/oscar/1/puppet/'
```
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format 'modelname-action', so in this case
a named view of 'character-puppet' would be referenced by this method.
::
```python
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/puppet/$',
CharPuppetView.as_view(), name='character-puppet')
```
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/puppet/$',
CharPuppetView.as_view(), name='character-puppet')
If no View has been created and defined in urls.py, returns an
HTML anchor.
@ -959,7 +961,7 @@ class TypedObject(SharedMemoryModel):
"%s-puppet" % slugify(self._meta.verbose_name),
kwargs={"pk": self.pk, "slug": slugify(self.name)},
)
except:
except Exception:
return "#"
def web_get_update_url(self):
@ -979,11 +981,10 @@ class TypedObject(SharedMemoryModel):
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format 'modelname-action', so in this case
a named view of 'character-update' would be referenced by this method.
::
```python
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
CharUpdateView.as_view(), name='character-update')
```
If no View has been created and defined in urls.py, returns an
HTML anchor.
@ -999,7 +1000,7 @@ class TypedObject(SharedMemoryModel):
"%s-update" % slugify(self._meta.verbose_name),
kwargs={"pk": self.pk, "slug": slugify(self.name)},
)
except:
except Exception:
return "#"
def web_get_delete_url(self):
@ -1019,11 +1020,10 @@ class TypedObject(SharedMemoryModel):
somewhere in urls.py that follows the format 'modelname-action', so
in this case a named view of 'character-detail' would be referenced
by this method.
::
```python
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
CharDeleteView.as_view(), name='character-delete')
```
If no View has been created and defined in urls.py, returns an HTML
anchor.
@ -1039,7 +1039,7 @@ class TypedObject(SharedMemoryModel):
"%s-delete" % slugify(self._meta.verbose_name),
kwargs={"pk": self.pk, "slug": slugify(self.name)},
)
except:
except Exception:
return "#"
# Used by Django Sites/Admin

View file

@ -125,7 +125,10 @@ class TagHandler(object):
self._cache_complete = False
def _query_all(self):
"Get all tags for this objects"
"""
Get all tags for this object.
"""
query = {
"%s__id" % self._model: self._objid,
"tag__db_model": self._model,
@ -137,7 +140,10 @@ class TagHandler(object):
]
def _fullcache(self):
"Cache all tags of this object"
"""
Cache all tags of this object.
"""
if not _TYPECLASS_AGGRESSIVE_CACHE:
return
tags = self._query_all()
@ -277,6 +283,7 @@ class TagHandler(object):
def reset_cache(self):
"""
Reset the cache from the outside.
"""
self._cache_complete = False
self._cache = {}
@ -483,8 +490,9 @@ class TagHandler(object):
Batch-add tags from a list of tuples.
Args:
*args (tuple or str): Each argument should be a `tagstr` keys or tuple `(keystr, category)` or
`(keystr, category, data)`. It's possible to mix input types.
*args (tuple or str): Each argument should be a `tagstr` keys or tuple
`(keystr, category)` or `(keystr, category, data)`. It's possible to mix input
types.
Notes:
This will generate a mimimal number of self.add calls,

View file

@ -20,8 +20,8 @@ Supported standards:
## Markup
ANSI colors: `r` ed, `g` reen, `y` ellow, `b` lue, `m` agenta, `c` yan, `n` ormal (no color). Capital
letters indicate the 'dark' variant.
ANSI colors: `r` ed, `g` reen, `y` ellow, `b` lue, `m` agenta, `c` yan, `n` ormal (no color).
Capital letters indicate the 'dark' variant.
- `|r` fg bright red
- `|R` fg dark red
@ -337,8 +337,9 @@ class ANSIParser(object):
colval = 16 + (red * 36) + (green * 6) + blue
return "\033[%s8;5;%sm" % (3 + int(background), colval)
# replaced since some clients (like Potato) does not accept codes with leading zeroes, see issue #1024.
# return "\033[%s8;5;%s%s%sm" % (3 + int(background), colval // 100, (colval % 100) // 10, colval%10)
# replaced since some clients (like Potato) does not accept codes with leading zeroes,
# see issue #1024.
# return "\033[%s8;5;%s%s%sm" % (3 + int(background), colval // 100, (colval % 100) // 10, colval%10) # noqa
else:
# xterm256 not supported, convert the rgb value to ansi instead
@ -729,7 +730,8 @@ class ANSIString(str, metaclass=ANSIMeta):
"""
# A compiled Regex for the format mini-language: https://docs.python.org/3/library/string.html#formatspec
# A compiled Regex for the format mini-language:
# https://docs.python.org/3/library/string.html#formatspec
re_format = re.compile(
r"(?i)(?P<just>(?P<fill>.)?(?P<align>\<|\>|\=|\^))?(?P<sign>\+|\-| )?(?P<alt>\#)?"
r"(?P<zero>0)?(?P<width>\d+)?(?P<grouping>\_|\,)?(?:\.(?P<precision>\d+))?"
@ -802,12 +804,14 @@ class ANSIString(str, metaclass=ANSIMeta):
Current features supported: fill, align, width.
Args:
format_spec (str): The format specification passed by f-string or str.format(). This is a string such as
"0<30" which would mean "left justify to 30, filling with zeros". The full specification can be found
at https://docs.python.org/3/library/string.html#formatspec
format_spec (str): The format specification passed by f-string or str.format(). This is
a string such as "0<30" which would mean "left justify to 30, filling with zeros".
The full specification can be found at
https://docs.python.org/3/library/string.html#formatspec
Returns:
ansi_str (str): The formatted ANSIString's .raw() form, for display.
"""
# This calls the compiled regex stored on ANSIString's class to analyze the format spec.
# It returns a dictionary.
@ -1067,7 +1071,7 @@ class ANSIString(str, metaclass=ANSIMeta):
current_index = 0
result = tuple()
for section in parent_result:
result += (self[current_index : current_index + len(section)],)
result += (self[current_index: current_index + len(section)],)
current_index += len(section)
return result
@ -1187,7 +1191,7 @@ class ANSIString(str, metaclass=ANSIMeta):
start = next + bylen
maxsplit -= 1 # NB. if it's already < 0, it stays < 0
res.append(self[start : len(self)])
res.append(self[start: len(self)])
if drop_spaces:
return [part for part in res if part != ""]
return res
@ -1230,7 +1234,7 @@ class ANSIString(str, metaclass=ANSIMeta):
if next < 0:
break
# Get character codes after the index as well.
res.append(self[next + bylen : end])
res.append(self[next + bylen: end])
end = next
maxsplit -= 1 # NB. if it's already < 0, it stays < 0
@ -1284,7 +1288,7 @@ class ANSIString(str, metaclass=ANSIMeta):
ic -= 1
ir2 -= 1
rstripped = rstripped[::-1]
return ANSIString(lstripped + raw[ir1 : ir2 + 1] + rstripped)
return ANSIString(lstripped + raw[ir1: ir2 + 1] + rstripped)
def lstrip(self, chars=None):
"""
@ -1403,7 +1407,7 @@ class ANSIString(str, metaclass=ANSIMeta):
start = None
end = char._char_indexes[0]
prefix = char._raw_string[start:end]
postfix = char._raw_string[end + 1 :]
postfix = char._raw_string[end + 1:]
line = char._clean_string * amount
code_indexes = [i for i in range(0, len(prefix))]
length = len(prefix) + len(line)

View file

@ -284,7 +284,7 @@ class BatchCommandProcessor(object):
try:
path = match.group(1)
return "\n#\n".join(self.parse_file(path))
except IOError as err:
except IOError:
raise IOError("#INSERT {} failed.".format(path))
text = _RE_INSERT.sub(replace_insert, text)

View file

@ -19,7 +19,7 @@ from evennia.utils import logger
SCRIPTDB = None
class Container(object):
class Container:
"""
Base container class. A container is simply a storage object whose
properties can be acquired as a property on it. This is generally
@ -156,9 +156,8 @@ class GlobalScriptContainer(Container):
return new_script
if ((found.interval != interval)
or (found.start_delay != start_delay)
or (found.repeats != repeats)
):
or (found.start_delay != start_delay)
or (found.repeats != repeats)):
# the setup changed
found.start(interval=interval, start_delay=start_delay, repeats=repeats)
if found.desc != desc:

View file

@ -29,7 +29,7 @@ except ImportError:
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import SafeString
from evennia.utils.utils import uses_database, is_iter, to_str, to_bytes
from evennia.utils.utils import uses_database, is_iter, to_bytes
from evennia.utils import logger
__all__ = ("to_pickle", "from_pickle", "do_pickle", "do_unpickle", "dbserialize", "dbunserialize")

View file

@ -42,10 +42,11 @@ survive a reload. See the `EvEditor` class for more details.
import re
from django.conf import settings
from evennia import Command, CmdSet
from evennia import CmdSet
from evennia.utils import is_iter, fill, dedent, logger, justify, to_str, utils
from evennia.utils.ansi import raw
from evennia.commands import cmdhandler
from django.utils.translation import gettext as _
# we use cmdhandler instead of evennia.syscmdkeys to
# avoid some cases of loading before evennia init'd
@ -63,7 +64,7 @@ _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
#
# -------------------------------------------------------------
_HELP_TEXT = """
_HELP_TEXT = _("""
<txt> - any non-command is appended to the end of the buffer.
: <l> - view buffer or only line(s) <l>
:: <l> - raw-view buffer or only line(s) <l>
@ -99,66 +100,66 @@ _HELP_TEXT = """
:fd <l> - de-indent entire buffer or line <l>
:echo - turn echoing of the input on/off (helpful for some clients)
"""
""")
_HELP_LEGEND = """
_HELP_LEGEND = _("""
Legend:
<l> - line number, like '5' or range, like '3:7'.
<w> - a single word, or multiple words with quotes around them.
<txt> - longer string, usually not needing quotes.
"""
""")
_HELP_CODE = """
_HELP_CODE = _("""
:! - Execute code buffer without saving
:< - Decrease the level of automatic indentation for the next lines
:> - Increase the level of automatic indentation for the next lines
:= - Switch automatic indentation on/off
""".lstrip(
"\n"
)
))
_ERROR_LOADFUNC = """
_ERROR_LOADFUNC = _("""
{error}
|rBuffer load function error. Could not load initial data.|n
"""
""")
_ERROR_SAVEFUNC = """
_ERROR_SAVEFUNC = _("""
{error}
|rSave function returned an error. Buffer not saved.|n
"""
""")
_ERROR_NO_SAVEFUNC = "|rNo save function defined. Buffer cannot be saved.|n"
_ERROR_NO_SAVEFUNC = _("|rNo save function defined. Buffer cannot be saved.|n")
_MSG_SAVE_NO_CHANGE = "No changes need saving"
_DEFAULT_NO_QUITFUNC = "Exited editor."
_MSG_SAVE_NO_CHANGE = _("No changes need saving")
_DEFAULT_NO_QUITFUNC = _("Exited editor.")
_ERROR_QUITFUNC = """
_ERROR_QUITFUNC = _("""
{error}
|rQuit function gave an error. Skipping.|n
"""
""")
_ERROR_PERSISTENT_SAVING = """
_ERROR_PERSISTENT_SAVING = _("""
{error}
|rThe editor state could not be saved for persistent mode. Switching
to non-persistent mode (which means the editor session won't survive
an eventual server reload - so save often!)|n
"""
""")
_TRACE_PERSISTENT_SAVING = (
_TRACE_PERSISTENT_SAVING = _(
"EvEditor persistent-mode error. Commonly, this is because one or "
"more of the EvEditor callbacks could not be pickled, for example "
"because it's a class method or is defined inside another function."
)
_MSG_NO_UNDO = "Nothing to undo."
_MSG_NO_REDO = "Nothing to redo."
_MSG_UNDO = "Undid one step."
_MSG_REDO = "Redid one step."
_MSG_NO_UNDO = _("Nothing to undo.")
_MSG_NO_REDO = _("Nothing to redo.")
_MSG_UNDO = _("Undid one step.")
_MSG_REDO = _("Redid one step.")
# -------------------------------------------------------------
#
@ -180,7 +181,10 @@ class CmdSaveYesNo(_COMMAND_DEFAULT_CLASS):
help_cateogory = "LineEditor"
def func(self):
"""Implement the yes/no choice."""
"""
Implement the yes/no choice.
"""
# this is only called from inside the lineeditor
# so caller.ndb._lineditor must be set.
@ -195,7 +199,10 @@ class CmdSaveYesNo(_COMMAND_DEFAULT_CLASS):
class SaveYesNoCmdSet(CmdSet):
"""Stores the yesno question"""
"""
Stores the yesno question
"""
key = "quitsave_yesno"
priority = 150 # override other cmdsets.
@ -331,6 +338,7 @@ class CmdEditorBase(_COMMAND_DEFAULT_CLASS):
def _load_editor(caller):
"""
Load persistent editor from storage.
"""
saved_options = caller.attributes.get("_eveditor_saved")
saved_buffer, saved_undo = caller.attributes.get("_eveditor_buffer_temp", (None, None))
@ -356,6 +364,7 @@ def _load_editor(caller):
class CmdLineInput(CmdEditorBase):
"""
No command match - Inputs line of text into buffer.
"""
key = _CMD_NOMATCH
@ -440,6 +449,7 @@ class CmdEditorGroup(CmdEditorBase):
This command handles all the in-editor :-style commands. Since
each command is small and very limited, this makes for a more
efficient presentation.
"""
caller = self.caller
editor = caller.ndb._eveditor
@ -467,7 +477,7 @@ class CmdEditorGroup(CmdEditorBase):
# Insert single colon alone on a line
editor.update_buffer([":"] if lstart == 0 else linebuffer + [":"])
if echo_mode:
caller.msg("Single ':' added to buffer.")
caller.msg(_("Single ':' added to buffer."))
elif cmd == ":h":
# help entry
editor.display_help()
@ -482,7 +492,7 @@ class CmdEditorGroup(CmdEditorBase):
# quit. If not saved, will ask
if self.editor._unsaved:
caller.cmdset.add(SaveYesNoCmdSet)
caller.msg("Save before quitting? |lcyes|lt[Y]|le/|lcno|ltN|le")
caller.msg(_("Save before quitting?") + " |lcyes|lt[Y]|le/|lcno|ltN|le")
else:
editor.quit()
elif cmd == ":q!":
@ -497,24 +507,24 @@ class CmdEditorGroup(CmdEditorBase):
elif cmd == ":UU":
# reset buffer
editor.update_buffer(editor._pristine_buffer)
caller.msg("Reverted all changes to the buffer back to original state.")
caller.msg(_("Reverted all changes to the buffer back to original state."))
elif cmd == ":dd":
# :dd <l> - delete line <l>
buf = linebuffer[:lstart] + linebuffer[lend:]
editor.update_buffer(buf)
caller.msg("Deleted %s." % self.lstr)
caller.msg(_("Deleted {string}.").format(string= self.lstr))
elif cmd == ":dw":
# :dw <w> - delete word in entire buffer
# :dw <l> <w> delete word only on line(s) <l>
if not self.arg1:
caller.msg("You must give a search word to delete.")
caller.msg(_("You must give a search word to delete."))
else:
if not self.linerange:
lstart = 0
lend = self.cline + 1
caller.msg("Removed %s for lines %i-%i." % (self.arg1, lstart + 1, lend + 1))
caller.msg(_("Removed %s for lines %i-%i.") % (self.arg1, lstart + 1, lend + 1))
else:
caller.msg("Removed %s for %s." % (self.arg1, self.lstr))
caller.msg(_("Removed %s for %s.") % (self.arg1, self.lstr))
sarea = "\n".join(linebuffer[lstart:lend])
sarea = re.sub(r"%s" % self.arg1.strip("'").strip('"'), "", sarea, re.MULTILINE)
buf = linebuffer[:lstart] + sarea.split("\n") + linebuffer[lend:]
@ -529,49 +539,49 @@ class CmdEditorGroup(CmdEditorBase):
editor._indent = 0
if editor._persistent:
caller.attributes.add("_eveditor_indent", 0)
caller.msg("Cleared %i lines from buffer." % self.nlines)
caller.msg(_("Cleared %i lines from buffer.") % self.nlines)
elif cmd == ":y":
# :y <l> - yank line(s) to copy buffer
cbuf = linebuffer[lstart:lend]
editor._copy_buffer = cbuf
caller.msg("%s, %s yanked." % (self.lstr.capitalize(), cbuf))
caller.msg(_("%s, %s yanked.") % (self.lstr.capitalize(), cbuf))
elif cmd == ":x":
# :x <l> - cut line to copy buffer
cbuf = linebuffer[lstart:lend]
editor._copy_buffer = cbuf
buf = linebuffer[:lstart] + linebuffer[lend:]
editor.update_buffer(buf)
caller.msg("%s, %s cut." % (self.lstr.capitalize(), cbuf))
caller.msg(_("%s, %s cut.") % (self.lstr.capitalize(), cbuf))
elif cmd == ":p":
# :p <l> paste line(s) from copy buffer
if not editor._copy_buffer:
caller.msg("Copy buffer is empty.")
caller.msg(_("Copy buffer is empty."))
else:
buf = linebuffer[:lstart] + editor._copy_buffer + linebuffer[lstart:]
editor.update_buffer(buf)
caller.msg("Pasted buffer %s to %s." % (editor._copy_buffer, self.lstr))
caller.msg(_("Pasted buffer %s to %s.") % (editor._copy_buffer, self.lstr))
elif cmd == ":i":
# :i <l> <txt> - insert new line
new_lines = self.args.split("\n")
if not new_lines:
caller.msg("You need to enter a new line and where to insert it.")
caller.msg(_("You need to enter a new line and where to insert it."))
else:
buf = linebuffer[:lstart] + new_lines + linebuffer[lstart:]
editor.update_buffer(buf)
caller.msg("Inserted %i new line(s) at %s." % (len(new_lines), self.lstr))
caller.msg(_("Inserted %i new line(s) at %s.") % (len(new_lines), self.lstr))
elif cmd == ":r":
# :r <l> <txt> - replace lines
new_lines = self.args.split("\n")
if not new_lines:
caller.msg("You need to enter a replacement string.")
caller.msg(_("You need to enter a replacement string."))
else:
buf = linebuffer[:lstart] + new_lines + linebuffer[lend:]
editor.update_buffer(buf)
caller.msg("Replaced %i line(s) at %s." % (len(new_lines), self.lstr))
caller.msg(_("Replaced %i line(s) at %s.") % (len(new_lines), self.lstr))
elif cmd == ":I":
# :I <l> <txt> - insert text at beginning of line(s) <l>
if not self.raw_string and not editor._codefunc:
caller.msg("You need to enter text to insert.")
caller.msg(_("You need to enter text to insert."))
else:
buf = (
linebuffer[:lstart]
@ -579,11 +589,11 @@ class CmdEditorGroup(CmdEditorBase):
+ linebuffer[lend:]
)
editor.update_buffer(buf)
caller.msg("Inserted text at beginning of %s." % self.lstr)
caller.msg(_("Inserted text at beginning of %s.") % self.lstr)
elif cmd == ":A":
# :A <l> <txt> - append text after end of line(s)
if not self.args:
caller.msg("You need to enter text to append.")
caller.msg(_("You need to enter text to append."))
else:
buf = (
linebuffer[:lstart]
@ -591,23 +601,23 @@ class CmdEditorGroup(CmdEditorBase):
+ linebuffer[lend:]
)
editor.update_buffer(buf)
caller.msg("Appended text to end of %s." % self.lstr)
caller.msg(_("Appended text to end of %s.") % self.lstr)
elif cmd == ":s":
# :s <li> <w> <txt> - search and replace words
# in entire buffer or on certain lines
if not self.arg1 or not self.arg2:
caller.msg("You must give a search word and something to replace it with.")
caller.msg(_("You must give a search word and something to replace it with."))
else:
if not self.linerange:
lstart = 0
lend = self.cline + 1
caller.msg(
"Search-replaced %s -> %s for lines %i-%i."
_("Search-replaced %s -> %s for lines %i-%i.")
% (self.arg1, self.arg2, lstart + 1, lend)
)
else:
caller.msg(
"Search-replaced %s -> %s for %s." % (self.arg1, self.arg2, self.lstr)
_("Search-replaced %s -> %s for %s.") % (self.arg1, self.arg2, self.lstr)
)
sarea = "\n".join(linebuffer[lstart:lend])
@ -629,9 +639,9 @@ class CmdEditorGroup(CmdEditorBase):
if not self.linerange:
lstart = 0
lend = self.cline + 1
caller.msg("Flood filled lines %i-%i." % (lstart + 1, lend))
caller.msg(_("Flood filled lines %i-%i.") % (lstart + 1, lend))
else:
caller.msg("Flood filled %s." % self.lstr)
caller.msg(_("Flood filled %s.") % self.lstr)
fbuf = "\n".join(linebuffer[lstart:lend])
fbuf = fill(fbuf, width=width)
buf = linebuffer[:lstart] + fbuf.split("\n") + linebuffer[lend:]
@ -653,16 +663,17 @@ class CmdEditorGroup(CmdEditorBase):
width = _DEFAULT_WIDTH
if self.arg1 and self.arg1.lower() not in align_map:
self.caller.msg(
"Valid justifications are [f]ull (default), [c]enter, [r]right or [l]eft"
_("Valid justifications are")
+ " [f]ull (default), [c]enter, [r]right or [l]eft"
)
return
align = align_map[self.arg1.lower()] if self.arg1 else "f"
if not self.linerange:
lstart = 0
lend = self.cline + 1
self.caller.msg("%s-justified lines %i-%i." % (align_name[align], lstart + 1, lend))
self.caller.msg(_("%s-justified lines %i-%i.") % (align_name[align], lstart + 1, lend))
else:
self.caller.msg("%s-justified %s." % (align_name[align], self.lstr))
self.caller.msg(_("%s-justified %s.") % (align_name[align], self.lstr))
jbuf = "\n".join(linebuffer[lstart:lend])
jbuf = justify(jbuf, width=width, align=align)
buf = linebuffer[:lstart] + jbuf.split("\n") + linebuffer[lend:]
@ -673,9 +684,9 @@ class CmdEditorGroup(CmdEditorBase):
if not self.linerange:
lstart = 0
lend = self.cline + 1
caller.msg("Indented lines %i-%i." % (lstart + 1, lend))
caller.msg(_("Indented lines %i-%i.") % (lstart + 1, lend))
else:
caller.msg("Indented %s." % self.lstr)
caller.msg(_("Indented %s.") % self.lstr)
fbuf = [indent + line for line in linebuffer[lstart:lend]]
buf = linebuffer[:lstart] + fbuf + linebuffer[lend:]
editor.update_buffer(buf)
@ -684,9 +695,9 @@ class CmdEditorGroup(CmdEditorBase):
if not self.linerange:
lstart = 0
lend = self.cline + 1
caller.msg("Removed left margin (dedented) lines %i-%i." % (lstart + 1, lend))
caller.msg(_("Removed left margin (dedented) lines %i-%i.") % (lstart + 1, lend))
else:
caller.msg("Removed left margin (dedented) %s." % self.lstr)
caller.msg(_("Removed left margin (dedented) %s.") % self.lstr)
fbuf = "\n".join(linebuffer[lstart:lend])
fbuf = dedent(fbuf)
buf = linebuffer[:lstart] + fbuf.split("\n") + linebuffer[lend:]
@ -694,45 +705,45 @@ class CmdEditorGroup(CmdEditorBase):
elif cmd == ":echo":
# set echoing on/off
editor._echo_mode = not editor._echo_mode
caller.msg("Echo mode set to %s" % editor._echo_mode)
caller.msg(_("Echo mode set to %s") % editor._echo_mode)
elif cmd == ":!":
if editor._codefunc:
editor._codefunc(caller, editor._buffer)
else:
caller.msg("This command is only available in code editor mode.")
caller.msg(_("This command is only available in code editor mode."))
elif cmd == ":<":
# :<
if editor._codefunc:
editor.decrease_indent()
indent = editor._indent
if indent >= 0:
caller.msg("Decreased indentation: new indentation is {}.".format(indent))
caller.msg(_("Decreased indentation: new indentation is {}.").format(indent))
else:
caller.msg("|rManual indentation is OFF.|n Use := to turn it on.")
caller.msg(_("|rManual indentation is OFF.|n Use := to turn it on."))
else:
caller.msg("This command is only available in code editor mode.")
caller.msg(_("This command is only available in code editor mode."))
elif cmd == ":>":
# :>
if editor._codefunc:
editor.increase_indent()
indent = editor._indent
if indent >= 0:
caller.msg("Increased indentation: new indentation is {}.".format(indent))
caller.msg(_("Increased indentation: new indentation is {}.").format(indent))
else:
caller.msg("|rManual indentation is OFF.|n Use := to turn it on.")
caller.msg(_("|rManual indentation is OFF.|n Use := to turn it on."))
else:
caller.msg("This command is only available in code editor mode.")
caller.msg(_("This command is only available in code editor mode."))
elif cmd == ":=":
# :=
if editor._codefunc:
editor.swap_autoindent()
indent = editor._indent
if indent >= 0:
caller.msg("Auto-indentation turned on.")
caller.msg(_("Auto-indentation turned on."))
else:
caller.msg("Auto-indentation turned off.")
caller.msg(_("Auto-indentation turned off."))
else:
caller.msg("This command is only available in code editor mode.")
caller.msg(_("This command is only available in code editor mode."))
class EvEditorCmdSet(CmdSet):
@ -879,12 +890,13 @@ class EvEditor(object):
def load_buffer(self):
"""
Load the buffer using the load function hook.
"""
try:
self._buffer = self._loadfunc(self._caller)
if not isinstance(self._buffer, str):
self._buffer = to_str(self._buffer)
self._caller.msg("|rNote: input buffer was converted to a string.|n")
self._caller.msg(_("|rNote: input buffer was converted to a string.|n"))
except Exception as e:
from evennia.utils import logger
@ -1021,7 +1033,7 @@ class EvEditor(object):
header = (
"|n"
+ sep * 10
+ "Line Editor [%s]" % self._key
+ _("Line Editor [%s]") % self._key
+ sep * (_DEFAULT_WIDTH - 24 - len(self._key))
)
footer = (
@ -1029,7 +1041,7 @@ class EvEditor(object):
+ sep * 10
+ "[l:%02i w:%03i c:%04i]" % (nlines, nwords, nchars)
+ sep * 12
+ "(:h for help)"
+ _("(:h for help)")
+ sep * (_DEFAULT_WIDTH - 54)
)
if linenums:

View file

@ -415,7 +415,8 @@ class CmdEvMenuNode(Command):
) # don't give the session as a kwarg here, direct to original
raise EvMenuError(err)
# we must do this after the caller with the menu has been correctly identified since it
# can be either Account, Object or Session (in the latter case this info will be superfluous).
# can be either Account, Object or Session (in the latter case this info will be
# superfluous).
caller.ndb._evmenu._session = self.session
# we have a menu, use it.
menu.parse_input(self.raw_string)
@ -619,7 +620,8 @@ class EvMenu:
).intersection(set(kwargs.keys()))
if reserved_clash:
raise RuntimeError(
f"One or more of the EvMenu `**kwargs` ({list(reserved_clash)}) is reserved by EvMenu for internal use."
f"One or more of the EvMenu `**kwargs` ({list(reserved_clash)}) "
"is reserved by EvMenu for internal use."
)
for key, val in kwargs.items():
setattr(self, key, val)
@ -1262,7 +1264,7 @@ class EvMenu:
table.extend([" " for i in range(nrows - nlastcol)])
# build the actual table grid
table = [table[icol * nrows : (icol * nrows) + nrows] for icol in range(0, ncols)]
table = [table[icol * nrows: (icol * nrows) + nrows] for icol in range(0, ncols)]
# adjust the width of each column
for icol in range(len(table)):
@ -1349,6 +1351,7 @@ def list_node(option_generator, select=None, pagesize=10):
def _select_parser(caller, raw_string, **kwargs):
"""
Parse the select action
"""
available_choices = kwargs.get("available_choices", [])
@ -1356,7 +1359,7 @@ def list_node(option_generator, select=None, pagesize=10):
index = int(raw_string.strip()) - 1
selection = available_choices[index]
except Exception:
caller.msg("|rInvalid choice.|n")
caller.msg(_("|rInvalid choice.|n"))
else:
if callable(select):
try:
@ -1388,7 +1391,7 @@ def list_node(option_generator, select=None, pagesize=10):
if option_list:
nall_options = len(option_list)
pages = [
option_list[ind : ind + pagesize] for ind in range(0, nall_options, pagesize)
option_list[ind: ind + pagesize] for ind in range(0, nall_options, pagesize)
]
npages = len(pages)
@ -1413,7 +1416,7 @@ def list_node(option_generator, select=None, pagesize=10):
# allows us to call ourselves over and over, using different kwargs.
options.append(
{
"key": ("|Wcurrent|n", "c"),
"key": (_("|Wcurrent|n"), "c"),
"desc": "|W({}/{})|n".format(page_index + 1, npages),
"goto": (lambda caller: None, {"optionpage_index": page_index}),
}
@ -1421,14 +1424,14 @@ def list_node(option_generator, select=None, pagesize=10):
if page_index > 0:
options.append(
{
"key": ("|wp|Wrevious page|n", "p"),
"key": (_("|wp|Wrevious page|n"), "p"),
"goto": (lambda caller: None, {"optionpage_index": page_index - 1}),
}
)
if page_index < npages - 1:
options.append(
{
"key": ("|wn|Wext page|n", "n"),
"key": (_("|wn|Wext page|n"), "n"),
"goto": (lambda caller: None, {"optionpage_index": page_index + 1}),
}
)
@ -1662,7 +1665,7 @@ class CmdYesNoQuestion(Command):
inp = raw
if inp in ('a', 'abort') and yes_no_question.allow_abort:
caller.msg("Aborted.")
caller.msg(_("Aborted."))
self._clean(caller)
return
@ -1672,7 +1675,6 @@ class CmdYesNoQuestion(Command):
kwargs = yes_no_question.kwargs
kwargs['caller_session'] = self.session
ok = False
if inp in ('yes', 'y'):
yes_no_question.yes_callable(caller, *args, **kwargs)
elif inp in ('no', 'n'):
@ -1684,9 +1686,9 @@ class CmdYesNoQuestion(Command):
# cleanup
self._clean(caller)
except Exception as err:
except Exception:
# make sure to clean up cmdset if something goes wrong
caller.msg("|rError in ask_yes_no. Choice not confirmed (report to admin)|n")
caller.msg(_("|rError in ask_yes_no. Choice not confirmed (report to admin)|n"))
logger.log_trace("Error in ask_yes_no")
self._clean(caller)
raise
@ -1938,7 +1940,7 @@ def parse_menu_template(caller, menu_template, goto_callables=None):
"""
Validate goto-callable kwarg is on correct form.
"""
if not "=" in kwarg:
if "=" not in kwarg:
raise RuntimeError(
f"EvMenu template error: goto-callable '{goto}' has a "
f"non-kwarg argument ({kwarg}). All callables in the "
@ -1955,6 +1957,7 @@ def parse_menu_template(caller, menu_template, goto_callables=None):
def _parse_options(nodename, optiontxt, goto_callables):
"""
Parse option section into option dict.
"""
options = []
optiontxt = optiontxt[0].strip() if optiontxt else ""
@ -2032,6 +2035,7 @@ def parse_menu_template(caller, menu_template, goto_callables=None):
def _parse(caller, menu_template, goto_callables):
"""
Parse the menu string format into a node tree.
"""
nodetree = {}
splits = _RE_NODE.split(menu_template)

View file

@ -43,6 +43,7 @@ from evennia import Command, CmdSet
from evennia.commands import cmdhandler
from evennia.utils.ansi import ANSIString
from evennia.utils.utils import make_iter, inherits_from, justify, dedent
from django.utils.translation import gettext as _
_CMD_NOMATCH = cmdhandler.CMD_NOMATCH
_CMD_NOINPUT = cmdhandler.CMD_NOINPUT
@ -231,7 +232,7 @@ class EvMore(object):
self._justify_kwargs = justify_kwargs
self.exit_on_lastpage = exit_on_lastpage
self.exit_cmd = exit_cmd
self._exit_msg = "Exited |wmore|n pager."
self._exit_msg = _("Exited |wmore|n pager.")
self._kwargs = kwargs
self._data = None
@ -354,8 +355,9 @@ class EvMore(object):
"""
Paginate by slice. This is done with an eye on memory efficiency (usually for
querysets); to avoid fetching all objects at the same time.
"""
return self._data[pageno * self.height : pageno * self.height + self.height]
return self._data[pageno * self.height: pageno * self.height + self.height]
def paginator_django(self, pageno):
"""
@ -433,7 +435,7 @@ class EvMore(object):
lines = text.split("\n")
self._data = [
_LBR.join(lines[i : i + self.height]) for i in range(0, len(lines), self.height)
_LBR.join(lines[i: i + self.height]) for i in range(0, len(lines), self.height)
]
self._npages = len(self._data)
@ -451,13 +453,15 @@ class EvMore(object):
Notes:
If overridden, this method must perform the following actions:
- read and re-store `self._data` (the incoming data set) if needed for pagination to work.
- read and re-store `self._data` (the incoming data set) if needed for pagination to
work.
- set `self._npages` to the total number of pages. Default is 1.
- set `self._paginator` to a callable that will take a page number 1...N and return
the data to display on that page (not any decorations or next/prev buttons). If only
wanting to change the paginator, override `self.paginator` instead.
- set `self._page_formatter` to a callable that will receive the page from `self._paginator`
and format it with one element per line. Default is `str`. Or override `self.page_formatter`
- set `self._page_formatter` to a callable that will receive the page from
`self._paginator` and format it with one element per line. Default is `str`. Or
override `self.page_formatter`
directly instead.
By default, helper methods are called that perform these actions

View file

@ -239,12 +239,12 @@ class ANSITextWrapper(TextWrapper):
del chunks[-1]
while chunks:
l = d_len(chunks[-1])
ln = d_len(chunks[-1])
# Can at least squeeze this chunk onto the current line.
if cur_len + l <= width:
if cur_len + ln <= width:
cur_line.append(chunks.pop())
cur_len += l
cur_len += ln
# Nope, this line is full.
else:
@ -262,10 +262,10 @@ class ANSITextWrapper(TextWrapper):
# Convert current line back to a string and store it in list
# of all lines (return value).
if cur_line:
l = ""
ln = ""
for w in cur_line: # ANSI fix
l += w #
lines.append(indent + l)
ln += w #
lines.append(indent + ln)
return lines
@ -1099,8 +1099,9 @@ class EvTable(object):
height (int, optional): Fixed height of table. Defaults to being unset. Width is
still given precedence. If given, table cells will crop text rather
than expand vertically.
evenwidth (bool, optional): Used with the `width` keyword. Adjusts columns to have as even width as
possible. This often looks best also for mixed-length tables. Default is `False`.
evenwidth (bool, optional): Used with the `width` keyword. Adjusts columns to have as
even width as possible. This often looks best also for mixed-length tables. Default
is `False`.
maxwidth (int, optional): This will set a maximum width
of the table while allowing it to be smaller. Only if it grows wider than this
size will it be resized by expanding horizontally (or crop `height` is given).
@ -1347,7 +1348,8 @@ class EvTable(object):
self.ncols = ncols
self.nrows = nrowmax
# add borders - these add to the width/height, so we must do this before calculating width/height
# add borders - these add to the width/height, so we must do this before calculating
# width/height
self._borders()
# equalize widths within each column
@ -1434,7 +1436,8 @@ class EvTable(object):
except Exception:
raise
# equalize heights for each row (we must do this here, since it may have changed to fit new widths)
# equalize heights for each row (we must do this here, since it may have changed to fit new
# widths)
cheights = [
max(cell.get_height() for cell in (col[iy] for col in self.worktable))
for iy in range(nrowmax)

View file

@ -43,11 +43,9 @@ The `FuncParser` also accepts a direct dict mapping of `{'name': callable, ...}`
---
"""
import re
import dataclasses
import inspect
import random
from functools import partial
from django.conf import settings
from evennia.utils import logger
from evennia.utils.utils import (
@ -234,8 +232,6 @@ class FuncParser:
f"(available: {available})")
return str(parsedfunc)
nargs = len(args)
# build kwargs in the proper priority order
kwargs = {**self.default_kwargs, **kwargs, **reserved_kwargs,
**{'funcparser': self, "raise_errors": raise_errors}}
@ -606,7 +602,7 @@ def funcparser_callable_eval(*args, **kwargs):
- `$py(3 + 4) -> 7`
"""
args, kwargs = safe_convert_to_types(("py", {}) , *args, **kwargs)
args, kwargs = safe_convert_to_types(("py", {}), *args, **kwargs)
return args[0] if args else ''
@ -694,7 +690,7 @@ def funcparser_callable_round(*args, **kwargs):
"""
if not args:
return ''
args, _ = safe_convert_to_types(((float, int), {}) *args, **kwargs)
args, _ = safe_convert_to_types(((float, int), {}), *args, **kwargs)
num, *significant = args
significant = significant[0] if significant else 0
@ -1032,7 +1028,8 @@ def funcparser_callable_search_list(*args, caller=None, access="control", **kwar
return_list=True, **kwargs)
def funcparser_callable_you(*args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs):
def funcparser_callable_you(*args, caller=None, receiver=None, mapping=None, capitalize=False,
**kwargs):
"""
Usage: $you() or $you(key)
@ -1081,10 +1078,12 @@ def funcparser_callable_you(*args, caller=None, receiver=None, mapping=None, cap
capitalize = bool(capitalize)
if caller == receiver:
return "You" if capitalize else "you"
return caller.get_display_name(looker=receiver) if hasattr(caller, "get_display_name") else str(caller)
return (caller.get_display_name(looker=receiver)
if hasattr(caller, "get_display_name") else str(caller))
def funcparser_callable_You(*args, you=None, receiver=None, mapping=None, capitalize=True, **kwargs):
def funcparser_callable_You(*args, you=None, receiver=None, mapping=None, capitalize=True,
**kwargs):
"""
Usage: $You() - capitalizes the 'you' output.

View file

@ -7,7 +7,6 @@ total runtime of the server and the current uptime.
"""
import time
from calendar import monthrange
from datetime import datetime, timedelta
from django.db.utils import OperationalError

View file

@ -63,7 +63,8 @@ class SharedMemoryModelBase(ModelBase):
return super(SharedMemoryModelBase, cls).__call__(*args, **kwargs)
instance_key = cls._get_cache_key(args, kwargs)
# depending on the arguments, we might not be able to infer the PK, so in that case we create a new instance
# depending on the arguments, we might not be able to infer the PK, so in that case we
# create a new instance
if instance_key is None:
return new_instance()
cached_instance = cls.get_cached_instance(instance_key)
@ -154,9 +155,9 @@ class SharedMemoryModelBase(ModelBase):
if isinstance(value, (str, int)):
value = to_str(value)
if value.isdigit() or value.startswith("#"):
# we also allow setting using dbrefs, if so we try to load the matching object.
# (we assume the object is of the same type as the class holding the field, if
# not a custom handler must be used for that field)
# we also allow setting using dbrefs, if so we try to load the matching
# object. (we assume the object is of the same type as the class holding
# the field, if not a custom handler must be used for that field)
dbid = dbref(value, reqhash=False)
if dbid:
model = _GA(cls, "_meta").get_field(fname).model
@ -266,21 +267,24 @@ class SharedMemoryModel(Model, metaclass=SharedMemoryModelBase):
pk = cls._meta.pks[0]
else:
pk = cls._meta.pk
# get the index of the pk in the class fields. this should be calculated *once*, but isn't atm
# get the index of the pk in the class fields. this should be calculated *once*, but isn't
# atm
pk_position = cls._meta.fields.index(pk)
if len(args) > pk_position:
# if it's in the args, we can get it easily by index
result = args[pk_position]
elif pk.attname in kwargs:
# retrieve the pk value. Note that we use attname instead of name, to handle the case where the pk is a
# a ForeignKey.
# retrieve the pk value. Note that we use attname instead of name, to handle the case
# where the pk is a a ForeignKey.
result = kwargs[pk.attname]
elif pk.name != pk.attname and pk.name in kwargs:
# ok we couldn't find the value, but maybe it's a FK and we can find the corresponding object instead
# ok we couldn't find the value, but maybe it's a FK and we can find the corresponding
# object instead
result = kwargs[pk.name]
if result is not None and isinstance(result, Model):
# if the pk value happens to be a model instance (which can happen wich a FK), we'd rather use its own pk as the key
# if the pk value happens to be a model instance (which can happen wich a FK), we'd
# rather use its own pk as the key
result = result._get_pk_val()
return result

View file

@ -16,7 +16,6 @@ log_typemsg(). This is for historical, back-compatible reasons.
import os
import time
import glob
from datetime import datetime
from traceback import format_exc
from twisted.python import log, logfile
@ -47,6 +46,7 @@ def timeformat(when=None):
Returns:
timestring (str): A formatted string of the given time.
"""
when = when if when else time.time()
@ -126,6 +126,7 @@ class WeeklyLogFile(logfile.DailyLogFile):
server.log.2020_01_29
server.log.2020_01_29__1
server.log.2020_01_29__2
"""
suffix = ""
copy_suffix = 0
@ -146,7 +147,10 @@ class WeeklyLogFile(logfile.DailyLogFile):
return suffix
def write(self, data):
"Write data to log file"
"""
Write data to log file
"""
logfile.BaseLogFile.write(self, data)
self.lastDate = max(self.lastDate, self.toDate())
self.size += len(data)
@ -155,6 +159,7 @@ class WeeklyLogFile(logfile.DailyLogFile):
class PortalLogObserver(log.FileLogObserver):
"""
Reformat logging
"""
timeFormat = None
@ -289,6 +294,7 @@ def log_info(infomsg):
Prints any generic debugging/informative info that should appear in the log.
infomsg: (string) The message to be logged.
"""
try:
infomsg = str(infomsg)
@ -307,6 +313,7 @@ def log_dep(depmsg):
Args:
depmsg (str): The deprecation message to log.
"""
try:
depmsg = str(depmsg)
@ -325,6 +332,7 @@ def log_sec(secmsg):
Args:
secmsg (str): The security message to log.
"""
try:
secmsg = str(secmsg)
@ -346,6 +354,7 @@ class EvenniaLogFile(logfile.LogFile):
the LogFile's rotate method in order to append some of the last
lines of the previous log to the start of the new log, in order
to preserve a continuous chat history for channel log files.
"""
# we delay import of settings to keep logger module as free
@ -361,6 +370,7 @@ class EvenniaLogFile(logfile.LogFile):
"""
Rotates our log file and appends some number of lines from
the previous log to the start of the new one.
"""
append_tail = (num_lines_to_append
if num_lines_to_append is not None
@ -377,9 +387,11 @@ class EvenniaLogFile(logfile.LogFile):
"""
Convenience method for accessing our _file attribute's seek method,
which is used in tail_log_function.
Args:
*args: Same args as file.seek
**kwargs: Same kwargs as file.seek
"""
return self._file.seek(*args, **kwargs)
@ -387,12 +399,14 @@ class EvenniaLogFile(logfile.LogFile):
"""
Convenience method for accessing our _file attribute's readlines method,
which is used in tail_log_function.
Args:
*args: same args as file.readlines
**kwargs: same kwargs as file.readlines
Returns:
lines (list): lines from our _file attribute.
"""
return [line.decode("utf-8") for line in self._file.readlines(*args, **kwargs)]
@ -550,7 +564,7 @@ def tail_log_file(filename, offset, nlines, callback=None):
lines_found = filehandle.readlines()
block_count -= 1
# return the right number of lines
lines_found = lines_found[-nlines - offset : -offset if offset else None]
lines_found = lines_found[-nlines - offset: -offset if offset else None]
if callback:
callback(lines_found)
return None

View file

@ -1,5 +1,6 @@
from evennia.utils.utils import string_partial_matching
from evennia.utils.containers import OPTION_CLASSES
from django.utils.translation import gettext as _
_GA = object.__getattribute__
_SA = object.__setattr__
@ -134,7 +135,7 @@ class OptionHandler:
"""
if key not in self.options_dict:
if raise_error:
raise KeyError("Option not found!")
raise KeyError(_("Option not found!"))
return default
# get the options or load/recache it
op_found = self.options.get(key) or self._load_option(key)
@ -155,12 +156,14 @@ class OptionHandler:
"""
if not key:
raise ValueError("Option field blank!")
raise ValueError(_("Option field blank!"))
match = string_partial_matching(list(self.options_dict.keys()), key, ret_index=False)
if not match:
raise ValueError("Option not found!")
raise ValueError(_("Option not found!"))
if len(match) > 1:
raise ValueError(f"Multiple matches: {', '.join(match)}. Please be more specific.")
raise ValueError(_("Multiple matches:")
+ f"{', '.join(match)}. "
+ _("Please be more specific."))
match = match[0]
op = self.get(match, return_obj=True)
op.set(value, **kwargs)

View file

@ -61,8 +61,7 @@ except OperationalError:
from evennia.scripts.models import ScriptDB
from evennia.comms.models import Msg, ChannelDB
from evennia.help.models import HelpEntry
from evennia.typeclasses.tags import Tag
from evennia.typeclasses.tags import Tag # noqa
# -------------------------------------------------------------------
# Search manager-wrappers
@ -243,7 +242,7 @@ def search_script_attribute(
def search_channel_attribute(
key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
return Channel.objects.get_by_attribute(
return ChannelDB.objects.get_by_attribute(
key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs
)

View file

@ -9,7 +9,6 @@ be of use when designing your own game.
import os
import gc
import sys
import copy
import types
import math
import re
@ -35,6 +34,7 @@ from django.utils.translation import gettext as _
from django.apps import apps
from django.core.validators import validate_email as django_validate_email
from django.core.exceptions import ValidationError as DjangoValidationError
from evennia.utils import logger
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
@ -204,7 +204,7 @@ def dedent(text, baseline_index=None, indent=None):
baseline = lines[baseline_index]
spaceremove = len(baseline) - len(baseline.lstrip(" "))
return "\n".join(
line[min(spaceremove, len(line) - len(line.lstrip(" "))) :] for line in lines
line[min(spaceremove, len(line) - len(line.lstrip(" "))):] for line in lines
)
@ -343,7 +343,7 @@ def columnize(string, columns=2, spacing=4, align="l", width=None):
cols = []
istart = 0
for irows in nrows:
cols.append(onecol[istart : istart + irows])
cols.append(onecol[istart: istart + irows])
istart = istart + irows
for col in cols:
if len(col) < height:
@ -1029,8 +1029,6 @@ def uses_database(name="sqlite3"):
return engine == "django.db.backends.%s" % name
def delay(timedelay, callback, *args, **kwargs):
"""
Delay the calling of a callback (function).
@ -1238,8 +1236,8 @@ def check_evennia_dependencies():
except ImportError:
errstring += (
"\n ERROR: IRC is enabled, but twisted.words is not installed. Please install it."
"\n Linux Debian/Ubuntu users should install package 'python-twisted-words', others"
"\n can get it from http://twistedmatrix.com/trac/wiki/TwistedWords."
"\n Linux Debian/Ubuntu users should install package 'python-twisted-words', "
"\n others can get it from http://twistedmatrix.com/trac/wiki/TwistedWords."
)
not_error = False
errstring = errstring.strip()
@ -1911,7 +1909,7 @@ def format_grid(elements, width=78, sep=" ", verbatim_elements=None):
wl = wls[ie]
lrow = len(row)
debug = row.replace(" ", ".")
# debug = row.replace(" ", ".")
if lrow + wl > width:
# this slot extends outside grid, move to next line
@ -1970,8 +1968,6 @@ def format_grid(elements, width=78, sep=" ", verbatim_elements=None):
return _weighted_rows(elements)
def get_evennia_pids():
"""
Get the currently valid PIDs (Process IDs) of the Portal and
@ -2323,7 +2319,7 @@ def get_game_dir_path():
"""
# current working directory, assumed to be somewhere inside gamedir.
for _ in range(10):
for inum in range(10):
gpath = os.getcwd()
if "server" in os.listdir(gpath):
if os.path.isfile(os.path.join("server", "conf", "settings.py")):

View file

@ -22,18 +22,20 @@ def text(entry, option_key="Text", **kwargs):
try:
return str(entry)
except Exception as err:
raise ValueError(f"Input could not be converted to text ({err})")
raise ValueError(_("Input could not be converted to text ({err})").format(err=err))
def color(entry, option_key="Color", **kwargs):
"""
The color should be just a color character, so 'r' if red color is desired.
"""
if not entry:
raise ValueError(f"Nothing entered for a {option_key}!")
raise ValueError(_("Nothing entered for a {option_key}!").format(option_key=option_key))
test_str = strip_ansi(f"|{entry}|n")
if test_str:
raise ValueError(f"'{entry}' is not a valid {option_key}.")
raise ValueError(_("'{entry}' is not a valid {option_key}.").format(
entry=entry, option_key=option_key))
return entry
@ -83,13 +85,17 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs)
entry = f"{split_time[0]} {split_time[1]} {split_time[2]} {split_time[3]}"
else:
raise ValueError(
f"{option_key} must be entered in a 24-hour format such as: {now.strftime('%b %d %H:%M')}"
_("{option_key} must be entered in a 24-hour format such as: {timeformat}").format(
option_key=option_key,
timeformat=now.strftime('%b %d %H:%M'))
)
try:
local = _dt.datetime.strptime(entry, "%b %d %H:%M %Y")
except ValueError:
raise ValueError(
f"{option_key} must be entered in a 24-hour format such as: {now.strftime('%b %d %H:%M')}"
_("{option_key} must be entered in a 24-hour format such as: {timeformat}").format(
option_key=option_key,
timeformat=now.strftime('%b %d %H:%M'))
)
local_tz = from_tz.localize(local)
return local_tz.astimezone(utc)
@ -100,8 +106,9 @@ def duration(entry, option_key="Duration", **kwargs):
Take a string and derive a datetime timedelta from it.
Args:
entry (string): This is a string from user-input. The intended format is, for example: "5d 2w 90s" for
'five days, two weeks, and ninety seconds.' Invalid sections are ignored.
entry (string): This is a string from user-input. The intended format is, for example:
"5d 2w 90s" for 'five days, two weeks, and ninety seconds.' Invalid sections are
ignored.
option_key (str): Name to display this query as.
Returns:
@ -129,7 +136,10 @@ def duration(entry, option_key="Duration", **kwargs):
elif _re.match(r"^[\d]+y$", interval):
days += int(interval.rstrip("y")) * 365
else:
raise ValueError(f"Could not convert section '{interval}' to a {option_key}.")
raise ValueError(
_("Could not convert section '{interval}' to a {option_key}.").format(
interval=interval, option_key=option_key)
)
return _dt.timedelta(days, seconds, 0, 0, minutes, hours, weeks)
@ -137,45 +147,56 @@ def duration(entry, option_key="Duration", **kwargs):
def future(entry, option_key="Future Datetime", from_tz=None, **kwargs):
time = datetime(entry, option_key, from_tz=from_tz)
if time < _dt.datetime.utcnow().replace(tzinfo=_dt.timezone.utc):
raise ValueError(f"That {option_key} is in the past! Must give a Future datetime!")
raise ValueError(_("That {option_key} is in the past! Must give a Future datetime!").format(
option_key=option_key))
return time
def signed_integer(entry, option_key="Signed Integer", **kwargs):
if not entry:
raise ValueError(f"Must enter a whole number for {option_key}!")
raise ValueError(_("Must enter a whole number for {option_key}!").format(
option_key=option_key))
try:
num = int(entry)
except ValueError:
raise ValueError(f"Could not convert '{entry}' to a whole number for {option_key}!")
raise ValueError(_("Could not convert '{entry}' to a whole "
"number for {option_key}!").format(
entry=entry, option_key=option_key))
return num
def positive_integer(entry, option_key="Positive Integer", **kwargs):
num = signed_integer(entry, option_key)
if not num >= 1:
raise ValueError(f"Must enter a whole number greater than 0 for {option_key}!")
raise ValueError(_("Must enter a whole number greater than 0 for {option_key}!").format(
option_key=option_key))
return num
def unsigned_integer(entry, option_key="Unsigned Integer", **kwargs):
num = signed_integer(entry, option_key)
if not num >= 0:
raise ValueError(f"{option_key} must be a whole number greater than or equal to 0!")
raise ValueError(_("{option_key} must be a whole number greater than "
"or equal to 0!").format(
option_key=option_key))
return num
def boolean(entry, option_key="True/False", **kwargs):
"""
Simplest check in computer logic, right? This will take user input to flick the switch on or off
Args:
entry (str): A value such as True, On, Enabled, Disabled, False, 0, or 1.
option_key (str): What kind of Boolean we are setting. What Option is this for?
Returns:
Boolean
"""
error = f"Must enter 0 (false) or 1 (true) for {option_key}. Also accepts True, False, On, Off, Yes, No, Enabled, and Disabled"
error = (_("Must enter a true/false input for {option_key}. Accepts {alternatives}.").format(
option_key=option_key,
alternatives="0/1, True/False, On/Off, Yes/No, Enabled/Disabled"))
if not isinstance(entry, str):
raise ValueError(error)
entry = entry.upper()
@ -196,39 +217,42 @@ def timezone(entry, option_key="Timezone", **kwargs):
Returns:
A PYTZ timezone.
"""
if not entry:
raise ValueError(f"No {option_key} entered!")
raise ValueError(_("No {option_key} entered!").format(option_key=option_key))
found = _partial(list(_TZ_DICT.keys()), entry, ret_index=False)
if len(found) > 1:
raise ValueError(
f"That matched: {', '.join(str(t) for t in found)}. Please be more specific!"
)
_("That matched: {matches}. Please be more specific!").format(
matches=', '.join(str(t) for t in found)))
if found:
return _TZ_DICT[found[0]]
raise ValueError(f"Could not find timezone '{entry}' for {option_key}!")
raise ValueError(_("Could not find timezone '{entry}' for {option_key}!").format(
entry=entry, option_key=option_key))
def email(entry, option_key="Email Address", **kwargs):
if not entry:
raise ValueError("Email address field empty!")
raise ValueError(_("Email address field empty!"))
valid = validate_email_address(entry)
if not valid:
raise ValueError(f"That isn't a valid {option_key}!")
raise ValueError(_("That isn't a valid {option_key}!").format(option_key=option_key))
return entry
def lock(entry, option_key="locks", access_options=None, **kwargs):
entry = entry.strip()
if not entry:
raise ValueError(f"No {option_key} entered to set!")
raise ValueError(_("No {option_key} entered to set!").format(option_key=option_key))
for locksetting in entry.split(";"):
access_type, lockfunc = locksetting.split(":", 1)
if not access_type:
raise ValueError("Must enter an access type!")
raise ValueError(_("Must enter an access type!"))
if access_options:
if access_type not in access_options:
raise ValueError(f"Access type must be one of: {', '.join(access_options)}")
raise ValueError(_("Access type must be one of: {alternatives}").format(
alternatives=', '.join(access_options)))
if not lockfunc:
raise ValueError("Lock func not entered.")
raise ValueError(_("Lock func not entered."))
return entry