def test_text(self): """Test to parse one argument as text.""" # Try to enter a single word args = CommandArgs() args.add_argument("text", dest="simple") namespace = args.parse("something") self.assertEqual(namespace.simple, "something") # Try to enter several words args = CommandArgs() args.add_argument("text") namespace = args.parse("something else") self.assertEqual(namespace.text, "something else")
def test_options(self): """Test options arguments.""" # Try to enter a single option args = CommandArgs() options = args.add_argument("options") options.add_option("t", "title", dest="title") namespace = args.parse("title=ok") self.assertEqual(namespace.title, "ok") # Try again, but with two words in the title namespace = args.parse("title=a title") self.assertEqual(namespace.title, "a title") # Try short options namespace = args.parse("t=ok") self.assertEqual(namespace.title, "ok") # Try again, but with two words in the title namespace = args.parse("t=a title") self.assertEqual(namespace.title, "a title") # Try with several options args = CommandArgs() options = args.add_argument("options") options.add_option("t", "title", optional=False, dest="title") options.add_option("d", "description", dest="description") namespace = args.parse("title=ok d=a description") self.assertEqual(namespace.title, "ok") self.assertEqual(namespace.description, "a description") # Try again, but with two words in the title namespace = args.parse("title=a title description=something") self.assertEqual(namespace.title, "a title") self.assertEqual(namespace.description, "something") # Try short options namespace = args.parse("description=well t=ok") self.assertEqual(namespace.title, "ok") self.assertEqual(namespace.description, "well") # Try again, but with two words in the title namespace = args.parse("t=a title description=hi") self.assertEqual(namespace.title, "a title") self.assertEqual(namespace.description, "hi") # Test with word argument args = CommandArgs() args.add_argument("word") options = args.add_argument("options") options.add_option("t", "title", dest="title") options.add_option("d", "description", dest="description") namespace = args.parse("and d=something else title=ok") self.assertEqual(namespace.word, "and") self.assertEqual(namespace.title, "ok") self.assertEqual(namespace.description, "something else") # Test mandatory and optional options args = CommandArgs() options = args.add_argument("options") options.add_option("t", "title", default="nothing", dest="title") options.add_option("d", "description", dest="description") namespace = args.parse("d=a description") self.assertEqual(namespace.title, "nothing") self.assertEqual(namespace.description, "a description")
def test_word(self): """Test one or several words in arguments.""" # Try to enter a single word args = CommandArgs() args.add_argument("word", dest="simple") namespace = args.parse("something") self.assertEqual(namespace.simple, "something") # Try to enter several words args = CommandArgs() args.add_argument("word", dest="first") args.add_argument("word", dest="second") namespace = args.parse("something else") self.assertEqual(namespace.first, "something") self.assertEqual(namespace.second, "else")
def test_number(self): """Test a number argument.""" args = CommandArgs() args.add_argument("number") namespace = args.parse("38") self.assertEqual(namespace.number, 38) # Try an invalid number args = CommandArgs() number = args.add_argument("number") result = args.parse("no") self.assertIsInstance(result, ArgumentError) self.assertEqual(str(result), number.msg_invalid_number.format(number="no")) # Try with an optional number args = CommandArgs() args.add_argument("number", optional=True, default=1) args.add_argument("text") namespace = args.parse("2 red apples") self.assertEqual(namespace.number, 2) self.assertEqual(namespace.text, "red apples") namespace = args.parse("red apple") self.assertEqual(namespace.number, 1) self.assertEqual(namespace.text, "red apple") # Try with words and an optional number args = CommandArgs() args.add_argument("number", dest="left", optional=True, default=1) args.add_argument("word") args.add_argument("number", dest="right") namespace = args.parse("2 times 3") self.assertEqual(namespace.left, 2) self.assertEqual(namespace.word, "times") self.assertEqual(namespace.right, 3) namespace = args.parse("neg 5") self.assertEqual(namespace.left, 1) self.assertEqual(namespace.word, "neg") self.assertEqual(namespace.right, 5)
class Command: """ Base class for commands. Static commands are mostly defined and dynamically imported from the file system, from the 'command' directory and its sub-directories. A static command can have a name, inferred from the class name if not provided. It can also have aliases, a command category and basic command permissions. It's help entry is inferred from the command's docstring. Here's a very basic example. Assuming you want to add the 'look' command, just create a file with the '.py' extension in the command package (it can be in a sub-package, for the sake of organization). ```python from command import Command, CommandArgs class Look(Command): # 'look' (lowercase class name) will be the name ''' Look command, to look around you. Syntax: look [object] This command is used to look around you or look a specific object more in details. ''' # If you're not happy with the default name, just provide a # class variable 'name' and set the command name to it. You can # define the category in the 'category' class variable as a # string, and the command permissions, still as a string, in the # 'permissions' class variable. If they're not defined, # these last two values will be searched in parent packages # following a capitalized convention (see below). args = CommandArgs() args.add_argument("object", optional=True) async def run(self, args): '''The command is executed.''' await self.msg("Ho, look!") ``` If the `category` and/or `permissions` class variable aren't provided, they're automatically searched in the module and parent packages. A `CATEGORY` variable is searched in this module and, if not found, in parent packages recursively. Similarly, a `PERMISSIONS` variable is searched in the module and parent packages. This system allows to easily organize commands. For instance, assuming you want to create commands that only the administrators should use: you could create a directory called 'admin' inside of the 'command' package. In this new directory, add an `__init__.py` file. Inside write the following: ```python CATEGORY = "Admin commands" PERMISSIONS = "admin" ``` Then you can create a file per command as usual. For instance, create a file 'services.py' that contains the following code: ```python from command import Command, CommandArgs class Services(Command): '''Command services, to look for active services in the game.''' # name will be 'services' # ... ``` Because we haven't defined a `category` class variable, a `CATEGORY` top-level variable will be searched in the module itself. Since we haven't defined one, it will be searched in the parent package (that is, in our 'command/admin/__init.py' file). In this file is our `CATEGORY` variable, so the 'services' command will have its category set to 'Admin commands'. The same process applies for permissions. Notice that you can always override this behavior should the need arise, but it makes organization much easier. """ #name = "command name" #category = "command category" #permissions = "command permissions" args = CommandArgs() seps = " " alias = () def __init__(self, character=None, sep=None, arguments=""): self.character = character self.sep = sep self.arguments = arguments @property def session(self): return self.character and self.character.session or None @property def db(self): """Return the attribute handler for command storage.""" if (handler := getattr(self, "cached_db_handler", None)): return handler from data.handlers import AttributeHandler if self.character is None: raise ValueError( "the character is not set, can't access attributes") handler = AttributeHandler(self.character) handler.subset = f"cmd.{self.layer}.{self.name}" self.cached_db_handler = handler return handler
def new_parser(self): """Simply return an empty command argument parser.""" return CommandArgs()
class Command: """ Base class for commands. Static commands are mostly defined and dynamically imported from the file system, from the 'command' directory and its sub-directories. A static command can have a name, inferred from the class name if not provided. It can also have aliases, a command category and basic command permissions. It's help entry is inferred from the command's docstring. Here's a very basic example. Assuming you want to add the 'look' command, just create a file with the '.py' extension in the command package (it can be in a sub-package, for the sake of organization). ```python from command import Command, CommandArgs class Look(Command): # 'look' (lowercase class name) will be the name ''' Look command, to look around you. Syntax: look [object] This command is used to look around you or look a specific object more in details. ''' # If you're not happy with the default name, just provide a # class variable 'name' and set the command name to it. You can # define the category in the 'category' class variable as a # string, and the command permissions, still as a string, in the # 'permissions' class variable. If they're not defined, # these last two values will be searched in parent packages # following a capitalized convention (see below). args = CommandArgs() args.add_argument("object", optional=True) async def call(self, args): '''The command is executed.''' await self.msg("Ho, look!") ``` If the `category` and/or `permissions` class variable aren't provided, they're automatically searched in the module and parent packages. A `CATEGORY` variable is searched in this module and, if not found, in parent packages recursively. Similarly, a `PERMISSIONS` variable is searched in the module and parent packages. This system allows to easily organize commands. For instance, assuming you want to create commands that only the administrators should use: you could create a directory called 'admin' inside of the 'command' package. In this new directory, add an `__init__.py` file. Inside write the following: ```python CATEGORY = "Admin commands" PERMISSIONS = "admin" ``` Then you can create a file per command as usual. For instance, create a file 'services.py' that contains the following code: ```python from command import Command, CommandArgs class Services(Command): '''Command services, to look for active services in the game.''' # name will be 'services' # ... ``` Becasue we haven't defined a `category` class variable, a `CATEGORY` top-level variable will be searched in the module itself. Since we haven't defined one, it will be searched in the parent package (that is, in our 'command/admin/__init.py' file). In this file is our `CATEGORY` variable, so the 'services' command will have its category set to 'Admin commands'. The same process applies for permissions. Notice that you can always override this behavior should the need arise, but it makes organization much easier. """ #name = "command name" #category = "command category" #permissions = "command permissions" args = CommandArgs() seps = " " def __init__(self, character=None, sep=None, arguments=""): self.character = character self.sep = sep self.arguments = arguments @property def session(self): return self.character and self.character.session or None @classmethod def can_run(cls, character) -> bool: """ Can the command be run by the specified character? Args: character (Character): the character to run this command. By default, check the command's permissions. You can, however, override this method in individual commands to perform other checks. Returns: can_run (bool): whether this character can run this command. """ if cls.permissions: return character.permissions.has(cls.permissions) return True def parse(self): """Parse the command, returning the namespace or an error.""" return type(self).args.parse(self.arguments) async def run(self, args): """ Run the command with the provided arguments. The command arguments are first parsed through the command argument parser (see `command/args.py`), so that this method is only called if arguments are parsed correctly. Args: args (namespace): The correctly parsed arguments. """ await self.msg(f"Great! You reached the dcommand {self.name}.") async def msg(self, text: str): """ Send the message to the character running the command. Args: text (str): text to send to the character. """ await self.character.msg(text) @classmethod def extrapolate(cls, path: Path): """ Extrapolate name, category and permissions if not set. This will entail looking into parent modules if needed. Args: path (pathlib.Path): path leading to the command. """ # Try to find the command name if not hasattr(cls, "name"): cls.name = cls.__name__.lower() # Try to find the command category if not hasattr(cls, "category"): cls.category = cls._explore_for(path, "CATEGORY", default="General") # Look for the command permissions if not hasattr(cls, "permissions"): cls.permissions = cls._explore_for(path, "PERMISSIONS", default="") return cls @staticmethod def _explore_for(path: Path, name: str, default: Any): """Explore for the given variable name.""" pypath = ".".join(path.parts)[:-3] module = import_module(pypath) value = getattr(module, name, NOT_SET) if value is not NOT_SET: return value # Import parent directories rindex = 1 while rindex < len(path.parts) - 1: pypath = ".".join(path.parts[:-rindex]) module = import_module(pypath) value = getattr(module, name, NOT_SET) if value is not NOT_SET: return value rindex += 1 return default