Merge branch 'main' of https://github.com/evennia/evennia into editor_search_traceback

This commit is contained in:
Chiizujin 2024-04-07 22:06:14 +10:00
commit c21c5667c9
25 changed files with 503 additions and 188 deletions

View file

@ -1 +1 @@
4.1.0
4.1.1

View file

@ -16,13 +16,14 @@ import time
import typing
from random import getrandbits
import evennia
from django.conf import settings
from django.contrib.auth import authenticate, password_validation
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.utils import timezone
from django.utils.module_loading import import_string
from django.utils.translation import gettext as _
import evennia
from evennia.accounts.manager import AccountManager
from evennia.accounts.models import AccountDB
from evennia.commands.cmdsethandler import CmdSetHandler
@ -30,17 +31,24 @@ from evennia.comms.models import ChannelDB
from evennia.objects.models import ObjectDB
from evennia.scripts.scripthandler import ScriptHandler
from evennia.server.models import ServerConfig
from evennia.server.signals import (SIGNAL_ACCOUNT_POST_CREATE,
SIGNAL_ACCOUNT_POST_LOGIN_FAIL,
SIGNAL_OBJECT_POST_PUPPET,
SIGNAL_OBJECT_POST_UNPUPPET)
from evennia.server.signals import (
SIGNAL_ACCOUNT_POST_CREATE,
SIGNAL_ACCOUNT_POST_LOGIN_FAIL,
SIGNAL_OBJECT_POST_PUPPET,
SIGNAL_OBJECT_POST_UNPUPPET,
)
from evennia.server.throttle import Throttle
from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler
from evennia.typeclasses.models import TypeclassBase
from evennia.utils import class_from_module, create, logger
from evennia.utils.optionhandler import OptionHandler
from evennia.utils.utils import (is_iter, lazy_property, make_iter, to_str,
variable_from_module)
from evennia.utils.utils import (
is_iter,
lazy_property,
make_iter,
to_str,
variable_from_module,
)
__all__ = ("DefaultAccount", "DefaultGuest")

View file

@ -5,13 +5,13 @@ Building and world design commands
import re
import typing
import evennia
from django.conf import settings
from django.core.paginator import Paginator
from django.db.models import Max, Min, Q
import evennia
from evennia import InterruptCommand
from evennia.commands.cmdhandler import (generate_cmdset_providers,
get_and_merge_cmdsets)
from evennia.commands.cmdhandler import generate_cmdset_providers, get_and_merge_cmdsets
from evennia.locks.lockhandler import LockException
from evennia.objects.models import ObjectDB
from evennia.prototypes import menus as olc_menus
@ -24,10 +24,18 @@ from evennia.utils.dbserialize import deserialize
from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore
from evennia.utils.evtable import EvTable
from evennia.utils.utils import (class_from_module, crop, dbref, display_len,
format_grid, get_all_typeclasses,
inherits_from, interactive, list_to_string,
variable_from_module)
from evennia.utils.utils import (
class_from_module,
crop,
dbref,
display_len,
format_grid,
get_all_typeclasses,
inherits_from,
interactive,
list_to_string,
variable_from_module,
)
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -1397,7 +1405,7 @@ class CmdSetHome(CmdLink):
obj.home = new_home
if old_home:
string = (
f"Home location of {obj} was changed from {old_home}({old_home.dbref} to"
f"Home location of {obj} was changed from {old_home}({old_home.dbref}) to"
f" {new_home}({new_home.dbref})."
)
else:
@ -3274,11 +3282,15 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
string += f"\n |RNo match found for '{searchstring}' in #dbref interval.|n"
else:
result = result[0]
string += (f"\n|g {result.get_display_name(caller)}"
f"{result.get_extra_display_name_info(caller)} - {result.path}|n")
string += (
f"\n|g {result.get_display_name(caller)}"
f"{result.get_extra_display_name_info(caller)} - {result.path}|n"
)
if "loc" in self.switches and not is_account and result.location:
string += (f" (|wlocation|n: |g{result.location.get_display_name(caller)}"
f"{result.get_extra_display_name_info(caller)}|n)")
string += (
f" (|wlocation|n: |g{result.location.get_display_name(caller)}"
f"{result.get_extra_display_name_info(caller)}|n)"
)
else:
# Not an account/dbref search but a wider search; build a queryset.
# Searches for key and aliases

View file

@ -4,8 +4,9 @@ General Character commands usually available to all characters
import re
import evennia
from django.conf import settings
import evennia
from evennia.typeclasses.attributes import NickTemplateInvalid
from evennia.utils import utils
@ -397,7 +398,7 @@ class NumberedTargetCommand(COMMAND_DEFAULT_CLASS):
"""
super().parse()
self.number = 0
if hasattr(self, "lhs"):
if getattr(self, "lhs", None):
# handle self.lhs but don't require it
count, *args = self.lhs.split(maxsplit=1)
# we only use the first word as a count if it's a number and

View file

@ -134,6 +134,17 @@ class TestGeneral(BaseEvenniaCommandTest):
self.obj2.location = self.char1
self.call(general.CmdGive(), "2 Obj = Char2", "You give two Objs")
def test_numbered_target_command(self):
class CmdTest(general.NumberedTargetCommand):
key = "test"
def func(self):
self.msg(f"Number: {self.number} Args: {self.args}")
self.call(CmdTest(), "", "Number: 0 Args: ")
self.call(CmdTest(), "obj", "Number: 0 Args: obj")
self.call(CmdTest(), "1 obj", "Number: 1 Args: obj")
def test_mux_command(self):
class CmdTest(MuxCommand):
key = "test"

View file

@ -155,6 +155,22 @@ class Component(metaclass=BaseComponent):
"""
return self.host.attributes
@property
def pk(self):
"""
Shortcut property returning the host's primary key.
Returns:
int: The Host's primary key.
Notes:
This is requried to allow AttributeProperties to correctly update `_SaverMutable` data
(like lists) in-place (since the DBField sits on the Component which doesn't itself
have a primary key, this save operation would otherwise fail).
"""
return self.host.pk
@property
def nattributes(self):
"""

View file

@ -6,7 +6,8 @@ This file contains the Descriptors used to set Fields in Components
import typing
from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty
from evennia.typeclasses.attributes import (AttributeProperty,
NAttributeProperty)
if typing.TYPE_CHECKING:
from .components import Component

View file

@ -268,7 +268,7 @@ class TestComponents(EvenniaTest):
def test_mutables_are_not_shared_when_autocreate(self):
self.char1.test_a.my_list.append(1)
self.assertNotEqual(self.char1.test_a.my_list, self.char2.test_a.my_list)
self.assertIsNot(self.char1.test_a.my_list, self.char2.test_a.my_list)
def test_replacing_class_component_slot_with_runtime_component(self):
self.char1.components.add_default("replacement_inherited_test_a")

View file

@ -20,11 +20,11 @@ import uuid
from collections import defaultdict
from django.core import exceptions as django_exceptions
from evennia.prototypes import spawner
from evennia.utils.utils import class_from_module
from .utils import (BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError,
MapParserError)
from .utils import BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, MapParserError
NodeTypeclass = None
ExitTypeclass = None
@ -331,7 +331,8 @@ class MapNode:
raise MapError(
f"Multiple objects found: {NodeTypeclass.objects.filter_xyz(xyz=xyz)}. "
"This may be due to manual creation of XYZRooms at this position. "
"Delete duplicates.", self
"Delete duplicates.",
self,
)
else:
self.log(f" updating existing room (if changed) at xyz={xyz}")

View file

@ -9,6 +9,7 @@ used as stand-alone XYZ-coordinate-aware rooms.
from django.conf import settings
from django.db.models import Q
from evennia.objects.manager import ObjectManager
from evennia.objects.objects import DefaultExit, DefaultRoom
@ -282,7 +283,7 @@ class XYZRoom(DefaultRoom):
def __repr__(self):
x, y, z = self.xyz
return f"<XYZRoom '{self.db_key}', XYZ=({x},{y},{z})>"
return f"<{self.__class__.__name__} '{self.db_key}', XYZ=({x},{y},{z})>"
@property
def xyz(self):
@ -307,8 +308,7 @@ class XYZRoom(DefaultRoom):
def xyzgrid(self):
global GET_XYZGRID
if not GET_XYZGRID:
from evennia.contrib.grid.xyzgrid.xyzgrid import \
get_xyzgrid as GET_XYZGRID
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
return GET_XYZGRID()
@property
@ -532,8 +532,7 @@ class XYZExit(DefaultExit):
def xyzgrid(self):
global GET_XYZGRID
if not GET_XYZGRID:
from evennia.contrib.grid.xyzgrid.xyzgrid import \
get_xyzgrid as GET_XYZGRID
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
return GET_XYZGRID()
@property

View file

@ -4,6 +4,7 @@ EvAdventure character generation.
"""
from django.conf import settings
from evennia.objects.models import ObjectDB
from evennia.prototypes.spawner import spawn
from evennia.utils.create import create_object

View file

@ -10,10 +10,11 @@ import time
import typing
from collections import defaultdict
import evennia
import inflect
from django.conf import settings
from django.utils.translation import gettext as _
import evennia
from evennia.commands import cmdset
from evennia.commands.cmdsethandler import CmdSetHandler
from evennia.objects.manager import ObjectManager
@ -23,9 +24,17 @@ from evennia.server.signals import SIGNAL_EXIT_TRAVERSED
from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler
from evennia.typeclasses.models import TypeclassBase
from evennia.utils import ansi, create, funcparser, logger, search
from evennia.utils.utils import (class_from_module, compress_whitespace, dbref,
is_iter, iter_to_str, lazy_property,
make_iter, to_str, variable_from_module)
from evennia.utils.utils import (
class_from_module,
compress_whitespace,
dbref,
is_iter,
iter_to_str,
lazy_property,
make_iter,
to_str,
variable_from_module,
)
_INFLECT = inflect.engine()
_MULTISESSION_MODE = settings.MULTISESSION_MODE
@ -1425,7 +1434,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
return [
obj
for obj in obj_list
if obj != looker and (obj.access(looker, "view") and obj.access(looker, "search", default=True))
if obj != looker
and (obj.access(looker, "view") and obj.access(looker, "search", default=True))
]
# name and return_appearance hooks
@ -1563,12 +1573,34 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
Args:
looker (DefaultObject): Object doing the looking.
**kwargs: Arbitrary data for use when overriding.
Keyword Args:
exit_order (iterable of str): The order in which exits should be listed, with
unspecified exits appearing at the end, alphabetically.
Returns:
str: The exits display data.
Examples:
::
For a room with exits in the order 'portal', 'south', 'north', and 'out':
obj.get_display_name(looker, exit_order=('north', 'south'))
-> "Exits: north, south, out, and portal." (markup not shown here)
"""
def _sort_exit_names(names):
exit_order = kwargs.get("exit_order")
if not exit_order:
return names
sort_index = {name: key for key, name in enumerate(exit_order)}
names = sorted(names)
end_pos = len(names) + 1
names.sort(key=lambda name:sort_index.get(name, end_pos))
return names
exits = self.filter_visible(self.contents_get(content_type="exit"), looker, **kwargs)
exit_names = iter_to_str(exi.get_display_name(looker, **kwargs) for exi in exits)
exit_names = (exi.get_display_name(looker, **kwargs) for exi in exits)
exit_names = iter_to_str(_sort_exit_names(exit_names))
return f"|wExits:|n {exit_names}" if exit_names else ""

View file

@ -3,13 +3,10 @@ from unittest import skip
from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
from evennia.objects.models import ObjectDB
from evennia.typeclasses.attributes import AttributeProperty
from evennia.typeclasses.tags import (
AliasProperty,
PermissionProperty,
TagCategoryProperty,
TagProperty,
)
from evennia.typeclasses.tags import (AliasProperty, PermissionProperty,
TagCategoryProperty, TagProperty)
from evennia.utils import create, search
from evennia.utils.ansi import strip_ansi
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
@ -94,6 +91,21 @@ class DefaultObjectTest(BaseEvenniaTest):
all_return_exit = ex1.get_return_exit(return_all=True)
self.assertEqual(len(all_return_exit), 2)
def test_exit_order(self):
DefaultExit.create("south", self.room1, self.room2, account=self.account)
DefaultExit.create("portal", self.room1, self.room2, account=self.account)
DefaultExit.create("north", self.room1, self.room2, account=self.account)
DefaultExit.create("aperture", self.room1, self.room2, account=self.account)
# in creation order
exits = strip_ansi(self.room1.get_display_exits(self.char1))
self.assertEqual(exits, "Exits: out, south, portal, north, and aperture")
# in specified order with unspecified exits alpbabetically on the end
exit_order = ('north', 'south', 'out')
exits = strip_ansi(self.room1.get_display_exits(self.char1, exit_order=exit_order))
self.assertEqual(exits, "Exits: north, south, out, aperture, and portal")
def test_urls(self):
"Make sure objects are returning URLs"
self.assertTrue(self.char1.get_absolute_url())
@ -356,6 +368,10 @@ class TestObjectPropertiesClass(DefaultObject):
attr2 = AttributeProperty(default="attr2", category="attrcategory")
attr3 = AttributeProperty(default="attr3", autocreate=False)
attr4 = SubAttributeProperty(default="attr4")
attr5 = AttributeProperty(default=list, autocreate=False)
attr6 = AttributeProperty(default=[None], autocreate=False)
attr7 = AttributeProperty(default=list)
attr8 = AttributeProperty(default=[None])
cusattr = CustomizedProperty(default=5)
tag1 = TagProperty()
tag2 = TagProperty(category="tagcategory")
@ -541,3 +557,99 @@ class TestProperties(EvenniaTestCase):
obj1.delete()
obj2.delete()
def test_not_create_attribute_with_autocreate_false(self):
"""
Test that AttributeProperty with autocreate=False does not create an attribute in the database.
"""
obj = create.create_object(TestObjectPropertiesClass, key="obj1")
self.assertEqual(obj.attr3, "attr3")
self.assertEqual(obj.attributes.get("attr3"), None)
self.assertEqual(obj.attr5, [])
self.assertEqual(obj.attributes.get("attr5"), None)
obj.delete()
def test_callable_defaults__autocreate_false(self):
"""
Test https://github.com/evennia/evennia/issues/3488, where a callable default value like `list`
would produce an infinitely empty result even when appended to.
"""
obj1 = create.create_object(TestObjectPropertiesClass, key="obj1")
obj2 = create.create_object(TestObjectPropertiesClass, key="obj2")
self.assertEqual(obj1.attr5, [])
obj1.attr5.append(1)
self.assertEqual(obj1.attr5, [1])
# check cross-instance sharing
self.assertEqual(obj2.attr5, [], "cross-instance sharing detected")
def test_mutable_defaults__autocreate_false(self):
"""
Test https://github.com/evennia/evennia/issues/3488, where a mutable default value (like a
list `[]` or `[None]`) would not be updated in the database when appended to.
Note that using a mutable default value is not recommended, as the mutable will share the
same memory space across all instances of the class. This means that if one instance modifiesA
the mutable, all instances will be affected.
"""
obj1 = create.create_object(TestObjectPropertiesClass, key="obj1")
obj2 = create.create_object(TestObjectPropertiesClass, key="obj2")
self.assertEqual(obj1.attr6, [None])
obj1.attr6.append(1)
self.assertEqual(obj1.attr6, [None, 1])
obj1.attr6[1] = 2
self.assertEqual(obj1.attr6, [None, 2])
# check cross-instance sharing
self.assertEqual(obj2.attr6, [None], "cross-instance sharing detected")
obj1.delete()
obj2.delete()
def test_callable_defaults__autocreate_true(self):
"""
Test callables with autocreate=True.
"""
obj1 = create.create_object(TestObjectPropertiesClass, key="obj1")
obj2 = create.create_object(TestObjectPropertiesClass, key="obj1")
self.assertEqual(obj1.attr7, [])
obj1.attr7.append(1)
self.assertEqual(obj1.attr7, [1])
# check cross-instance sharing
self.assertEqual(obj2.attr7, [])
def test_mutable_defaults__autocreate_true(self):
"""
Test mutable defaults with autocreate=True.
"""
obj1 = create.create_object(TestObjectPropertiesClass, key="obj1")
obj2 = create.create_object(TestObjectPropertiesClass, key="obj2")
self.assertEqual(obj1.attr8, [None])
obj1.attr8.append(1)
self.assertEqual(obj1.attr8, [None, 1])
obj1.attr8[1] = 2
self.assertEqual(obj1.attr8, [None, 2])
# check cross-instance sharing
self.assertEqual(obj2.attr8, [None])
obj1.delete()
obj2.delete()

View file

@ -642,6 +642,8 @@ def send_instruction(operation, arguments, callback=None, errback=None):
"""
global AMP_CONNECTION, REACTOR_RUN
# print("launcher: Sending to portal: {} + {}".format(ord(operation), arguments))
if None in (AMP_HOST, AMP_PORT, AMP_INTERFACE):
print(ERROR_AMP_UNCONFIGURED)
sys.exit()

View file

@ -197,8 +197,6 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
if process and not _is_windows():
# avoid zombie-process on Unix/BSD
process.wait()
# unset the reset-mode flag on the portal
self.factory.portal.server_restart_mode = None
return
def wait_for_disconnect(self, callback, *args, **kwargs):
@ -232,11 +230,18 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
"""
if mode == "reload":
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRELOAD)
self.send_AdminPortal2Server(
amp.DUMMYSESSION, operation=amp.SRELOAD, server_restart_mode=mode
)
elif mode == "reset":
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRESET)
self.send_AdminPortal2Server(
amp.DUMMYSESSION, operation=amp.SRESET, server_restart_mode=mode
)
elif mode == "shutdown":
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SSHUTD)
self.send_AdminPortal2Server(
amp.DUMMYSESSION, operation=amp.SSHUTD, server_restart_mode=mode
)
# store the mode for use once server comes back up again
self.factory.portal.server_restart_mode = mode
# sending amp data
@ -326,7 +331,6 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
_, server_connected, _, _, _, _ = self.get_status()
# logger.log_msg("Evennia Launcher->Portal operation %s:%s received" % (ord(operation), arguments))
# logger.log_msg("operation == amp.SSTART: {}: {}".format(operation == amp.SSTART, amp.loads(arguments)))
if operation == amp.SSTART: # portal start #15
@ -405,11 +409,11 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
sessid, kwargs = self.data_in(packed_data)
# logger.log_msg("Evennia Server->Portal admin data %s:%s received" % (sessid, kwargs))
operation = kwargs.pop("operation")
portal_sessionhandler = evennia.PORTAL_SESSION_HANDLER
# logger.log_msg(f"Evennia Server->Portal admin data operation {ord(operation)}")
if operation == amp.SLOGIN: # server_session_login
# a session has authenticated; sync it.
session = portal_sessionhandler.get(sessid)
@ -427,22 +431,28 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason"))
elif operation == amp.SRELOAD: # server reload
# set up callback to restart server once it has disconnected
self.factory.server_connection.wait_for_disconnect(
self.start_server, self.factory.portal.server_twistd_cmd
)
# tell server to reload
self.stop_server(mode="reload")
elif operation == amp.SRESET: # server reset
# set up callback to restart server once it has disconnected
self.factory.server_connection.wait_for_disconnect(
self.start_server, self.factory.portal.server_twistd_cmd
)
# tell server to reset
self.stop_server(mode="reset")
elif operation == amp.SSHUTD: # server-only shutdown
self.stop_server(mode="shutdown")
elif operation == amp.PSHUTD: # full server+server shutdown
# set up callback to shut down portal once server has disconnected
self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown)
# tell server to shut down
self.stop_server(mode="shutdown")
elif operation == amp.PSYNC: # portal sync
@ -451,6 +461,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
self.factory.portal.server_process_id = kwargs.get("spid", None)
# this defaults to 'shutdown' or whatever value set in server_stop
server_restart_mode = self.factory.portal.server_restart_mode
# print("Server has connected. Sending session data to Server ... mode: {}".format(server_restart_mode))
sessdata = evennia.PORTAL_SESSION_HANDLER.get_all_sync_data()
self.send_AdminPortal2Server(
@ -461,6 +472,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
portal_start_time=self.factory.portal.start_time,
)
evennia.PORTAL_SESSION_HANDLER.at_server_connection()
self.factory.portal.server_restart_mode = None
if self.factory.server_connection:
# this is an indication the server has successfully connected, so
@ -480,7 +492,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
)
# set a flag in case we are about to shut down soon
self.factory.server_restart_mode = True
self.factory.server_restart_mode = "shutdown"
elif operation == amp.SCONN: # server_force_connection (for irc/etc)
portal_sessionhandler.server_connect(**kwargs)

View file

@ -12,11 +12,11 @@ which is a non-db version of Attributes.
import fnmatch
import re
from collections import defaultdict
from copy import copy
from django.conf import settings
from django.db import models
from django.utils.encoding import smart_str
from evennia.locks.lockhandler import LockHandler
from evennia.utils.dbserialize import from_pickle, to_pickle
from evennia.utils.idmapper.models import SharedMemoryModel
@ -166,6 +166,7 @@ class AttributeProperty:
"""
attrhandler_name = "attributes"
cached_default_name_template = "_property_attribute_default_{key}"
def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=True):
"""
@ -207,21 +208,6 @@ class AttributeProperty:
self._autocreate = autocreate
self._key = ""
@property
def _default(self):
"""
Tries returning a new instance of default if callable.
"""
if callable(self.__default):
return self.__default()
return self.__default
@_default.setter
def _default(self, value):
self.__default = value
def __set_name__(self, cls, name):
"""
Called when descriptor is first assigned to the class. It is called with
@ -230,17 +216,35 @@ class AttributeProperty:
"""
self._key = name
def _get_and_cache_default(self, instance):
"""
Get and cache the default value for this attribute. We make sure to convert any mutables
into _Saver* equivalent classes here and cache the result on the instance's AttributeHandler.
"""
attrhandler = getattr(instance, self.attrhandler_name)
value = getattr(attrhandler, self.cached_default_name_template.format(key=self._key), None)
if not value:
if callable(self._default):
value = self._default()
else:
value = copy(self._default)
value = from_pickle(value, db_obj=instance)
setattr(attrhandler, self.cached_default_name_template.format(key=self._key), value)
return value
def __get__(self, instance, owner):
"""
Called when the attrkey is retrieved from the instance.
"""
value = self._default
value = self._get_and_cache_default(instance)
try:
value = self.at_get(
getattr(instance, self.attrhandler_name).get(
key=self._key,
default=self._default,
default=value,
category=self._category,
strattr=self._strattr,
raise_exception=self._autocreate,
@ -250,7 +254,7 @@ class AttributeProperty:
except AttributeError:
if self._autocreate:
# attribute didn't exist and autocreate is set
self.__set__(instance, self._default)
self.__set__(instance, value)
else:
raise
return value

View file

@ -98,7 +98,7 @@ _HELP_TEXT = _(
:s <l> <w> <txt> - search/replace word or regex <w> in buffer or on line <l>
:j <l> <w> - justify buffer or line <l>. <w> is f, c, l or r. Default f (full)
:f <l> - flood-fill entire buffer or line <l>: Equivalent to :j left
:f <l> - flood-fill entire buffer or line <l>. Equivalent to :j <l> l
:fi <l> - indent entire buffer or line <l>
:fd <l> - de-indent entire buffer or line <l>
@ -351,6 +351,35 @@ class CmdEditorBase(_COMMAND_DEFAULT_CLASS):
self.arg1 = arg1
self.arg2 = arg2
def insert_raw_string_into_buffer(self):
"""
Insert a line into the buffer. Used by both CmdLineInput and CmdEditorGroup.
"""
caller = self.caller
editor = caller.ndb._eveditor
buf = editor.get_buffer()
# add a line of text to buffer
line = self.raw_string.strip("\r\n")
if editor._codefunc and editor._indent >= 0:
# if automatic indentation is active, add spaces
line = editor.deduce_indent(line, buf)
buf = line if not buf else buf + "\n%s" % line
self.editor.update_buffer(buf)
if self.editor._echo_mode:
# need to do it here or we will be off one line
cline = len(self.editor.get_buffer().split("\n"))
if editor._codefunc:
# display the current level of identation
indent = editor._indent
if indent < 0:
indent = "off"
self.caller.msg("|b%02i|||n (|g%s|n) %s" % (cline, indent, raw(line)))
else:
self.caller.msg("|b%02i|||n %s" % (cline, raw(line)))
def _load_editor(caller):
"""
@ -394,29 +423,7 @@ class CmdLineInput(CmdEditorBase):
If the editor handles code, it might add automatic
indentation.
"""
caller = self.caller
editor = caller.ndb._eveditor
buf = editor.get_buffer()
# add a line of text to buffer
line = self.raw_string.strip("\r\n")
if editor._codefunc and editor._indent >= 0:
# if automatic indentation is active, add spaces
line = editor.deduce_indent(line, buf)
buf = line if not buf else buf + "\n%s" % line
self.editor.update_buffer(buf)
if self.editor._echo_mode:
# need to do it here or we will be off one line
cline = len(self.editor.get_buffer().split("\n"))
if editor._codefunc:
# display the current level of identation
indent = editor._indent
if indent < 0:
indent = "off"
self.caller.msg("|b%02i|||n (|g%s|n) %s" % (cline, indent, raw(line)))
else:
self.caller.msg("|b%02i|||n %s" % (cline, raw(line)))
self.insert_raw_string_into_buffer()
class CmdEditorGroup(CmdEditorBase):
@ -806,6 +813,9 @@ class CmdEditorGroup(CmdEditorBase):
caller.msg(_("Auto-indentation turned off."))
else:
caller.msg(_("This command is only available in code editor mode."))
else:
# no match - insert as line in buffer
self.insert_raw_string_into_buffer()
class EvEditorCmdSet(CmdSet):