From 7d84ccccdbc87f80b1500e87a05e4713b90ae533 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Mon, 17 Jul 2017 19:05:53 +0200 Subject: [PATCH 01/13] Add the generator contrib to generate pseudo-random strings --- evennia/contrib/generator.py | 332 +++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 evennia/contrib/generator.py diff --git a/evennia/contrib/generator.py b/evennia/contrib/generator.py new file mode 100644 index 0000000000..5bc0395ece --- /dev/null +++ b/evennia/contrib/generator.py @@ -0,0 +1,332 @@ +""" +Pseudo-random generator and registry + +Evennia contribution - Vincent-lg 2017 + +This contrib can be used to generate pseudo-random strings of information +with specific criteria. You could, for instance, use it to generate +phone numbers, license plate numbers, validation codes, non-sensivite +passwords and so on. The strings generated by the generator will be +stored and won't be available again in order to avoid repetition. +Here's a very simple example: + +```python +from evennia.contrib.generator import Generator +# Create a generator for phone numbers +phone_generator = Generator("phone number", r"555-\d{3}-\d{4}") +# Generate a phone number +number = phone_generator.generate() +# `number` will contain something like: "555-981-2207" +# If you call `phone_generator.generate`, it won't give the same anymore. +phone_generator.all() +# Will return a list of all currently-used phone numbers +phone_generator.free("555-981-2207") +# The number can be generated again. +``` + +To use it, you will need to: + +1. Import the `Generator` class from the contrib. +2. Create an instance of this class taking two arguments: + - Tje name of tje gemerator (like "phone number", "license plate"...). + - The regular expression representing the expected results. +3. Use the generator's `all`, `generate` and `free` methods as shown above. + +Some examples of regular expressions you could use: + +- `r"555-\d{3}-\d{4}"`: 555, a dash, 3 digits, another dash, 4 digits. +- `r"[0-9]{3}[A-Z][0-9]{3}"`: 3 digits, a capital letter, 3 digits. +- `r"[A-Za-z0-9]{8,15}"`: between 8 and 15 letters and digits. +- ... + +Behind the scenes, a script is created to store the generated information +for a single generator. The `Generator` object will also read the regular +expression you give to it to see what information is required (letters, +digits, a more restricted class, simple characters...). More complex +regular expressions (with branches for instance) might not be available. + +""" + +from random import choice, randint, seed +import re +import string +import time + +from evennia import DefaultScript, ScriptDB +from evennia.utils.create import create_script + +class RejectedRegex(RuntimeError): + + """The provided regular expression has been rejected. + + More details regarding why this error occurred will be provided in + the message. The usual reason is the provided regular expression is + not specific enough and could lead to inconsistent generating. + + """ + + pass + + +class ExhaustedGenerator(RuntimeError): + + """The generator hasn't any available strings to generate anymore.""" + + pass + + +class GeneratorScript(DefaultScript): + + """ + The global script to hold all generators. + + It will be automatically created the first time `generate` is called + on a Generator object. + + """ + + def at_script_creation(self): + """Hook called when the script is created.""" + self.key = "generator_script" + self.desc = "Global generator script" + self.persistent = True + + # Permanent data to be stored + self.db.generated = {} + + +class Generator(object): + + """ + A generator class to generate pseudo-random strings with a rule. + + The "rule" defining what the generator should provide in terms of + string is given as a regular expression when creating instances of + this class. You can use the `all` method to get all generated strings, + the `generate` method to generate a new string, the `free` method + to remove a generated string, or the `clear` method to remove all + generated strings. + + """ + + script = None + + def __init__(self, name, regex): + """ + Create a new generator. + + Args: + name (str): name of the generator to create. + regex (str): regular expression describing the generator. + + Notes: + `name` should be an explicit name. If you use more than one + generator in your game, be sure to give them different names. + This name will be used to store the generated information + in the global script, and in case of errors. + + The regular expression should describe the generator, what + it should generate: a phone number, a license plate, a password + or something else. Regular expressions allow you to use + pretty advanced criteria, but be aware that some regular + expressions will be rejected if not specific enough. + + Raises: + RejectedRegex: the provided regular expression couldn't be + accepted as a valid generator description. + + """ + self.name = name + self.elements = [] + self.total = 1 + + # Analyze the regex if any + if regex: + self.find_elements(regex) + + def __repr__(self): + return "".format(self.name) + + def all(self): + """ + Return all generated strings for this generator. + + Returns: + strings (list of strr): the list of strings that are already + used. The strings that were generated first come first in the list. + + """ + script = self._get_script() + generated = list(script.db.generated.get(self.name, [])) + return generated + + def generate(self, store=True, keep_trying=True): + """ + Generate a pseudo-random string according to the regular expression. + + Args: + store (bool, optional): store the generated string in the script. + keep_trying (bool, optional): keep on trying if the string already exists. + + Returns: + The newly-generated string. + + Raises: + ExhaustedGenerator: if there's no available string in this generator. + + Note: + Unless asked explicitly, the returned string can't repeat itself. + + """ + script = self._get_script() + generated = script.db.generated.get(self.name) + if generated is None: + script.db.generated[self.name] = [] + generated = script.db.generated[self.name] + + if len(generated) >= self.total: + raise ExhaustedGenerator + + # Generate a pseudo-random string that might be used already + result = "" + for element in self.elements: + number = randint(element["min"], element["max"]) + chars = element["chars"] + for index in range(number): + char = choice(chars) + result += char + + # If the string has already been generated, try again + if result in generated and keep_trying: + # Change the random seed, incrementing it slowly + epoch = time.time() + while result in generated: + epoch += 1 + seed(epoch) + result = self.generate(store=False, keep_trying=False) + + if store: + generated.append(result) + + return result + + def free(self, element): + """ + Removes a generated string from the list of stored strings. + + Args: + element (str): the string to remove from the list of generated strings. + + Note: + The specified string has to be present in the script (so + has to have been generated). It will remove this entry + from the script, so this string could be generated again by + calling the `generate` method. + + """ + script = self._get_script() + generated = script.db.generated.get(self.name, []) + if element not in generated: + raise ValueError("the string {} isn't stored as generated by the generator {}".format( + element, self.name)) + + generated.remove(element) + + def clear(self): + """ + Clear the generator of all generated strings. + + """ + script = self._get_script() + generated = script.db.generated.get(self.name, []) + generated[:] = [] + + def _get_script(self): + """Get or create the script.""" + if type(self).script: + return type(self).script + + try: + script = ScriptDB.objects.get(db_key="generator_script") + except ScriptDB.DoesNotExist: + script = create_script("contrib.generator.GeneratorScript") + + type(self).script = script + return script + + def find_elements(self, regex): + """ + Find the elements described in the regular expression. This will + analyze the provided regular expression and try to find elements. + + Args: + regex (str): the regular expression. + + """ + self.total = 1 + self.elements = [] + tree = re.sre_parse.parse(regex).data + # `tree` contains a list of elements in the regular expression + for element in tree: + # `eleemnt` is also a list, the first element is a string + name = element[0] + desc = {"min": 1, "max": 1} + + # If `.`, break here + if name == "any": + raise RejectedRegex("the . definition is too broad, specify what you need more precisely") + elif name == "at": + # Either the beginning or end, we ignore it + continue + elif name == "min_repeat": + raise RejectedRegex("you have to provide a maximum number of this character class") + elif name == "max_repeat": + desc["min"] = element[1][0] + desc["max"] = element[1][1] + desc["chars"] = self._find_literal(element[1][2][0]) + elif name == "in": + desc["chars"] = self._find_literal(element) + elif name == "literal": + desc["chars"] = self._find_literal(element) + else: + raise RejectedRegex("unhandled regex syntax:: {}".format(repr(name))) + + self.elements.append(desc) + self.total *= len(desc["chars"]) ** desc["max"] + + def _find_literal(self, element): + """Find the literal corresponding to a piece of regular expression.""" + chars = [] + if element[0] == "literal": + chars.append(chr(element[1])) + elif element[0] == "in": + negate = False + if element[1][0][0] == "negate": + negate = True + chars = list(string.ascii_letters + string.digits) + + for part in element[1]: + if part[0] == "negate": + continue + + sublist = self._find_literal(part) + for char in sublist: + if negate: + if char in chars: + chars.remove(char) + else: + chars.append(char) + elif element[0] == "range": + chars = [chr(i) for i in range(element[1][0], element[1][1] + 1)] + elif element[0] == "category": + category = element[1] + if category == "category_digit": + chars = list(string.digits) + elif category == "category_word": + chars = list(string.letters) + else: + raise RejectedRegex("unknown category: {}".format(category)) + else: + raise RejectedRegex("cannot find the literal: {}".format(element[0])) + + return chars From 09aeb7d2e3f186f49198a6747a1a4ca81fdf5047 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Sat, 22 Jul 2017 11:25:55 +0200 Subject: [PATCH 02/13] Update the documentation for the generator contrib --- evennia/contrib/README.md | 6 ++++-- evennia/contrib/generator.py | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index 50329bf0eb..a30b7cb64a 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -30,8 +30,8 @@ things you want from here into your game folder and change them there. multiple descriptions for time and season as well as details. * GenderSub (Griatch 2015) - Simple example (only) of storing gender on a character and access it in an emote with a custom marker. -* In-game Python (Vincent Le Geoff 2017) - Allow trusted builders to script - objects and events using Python from in-game. +* Generator (Vincent Le Goff 2017) - Simple pseudo-random generator of + strings with rules, avoiding repetitions. * Mail (grungies1138 2016) - An in-game mail system for communication. * Menu login (Griatch 2011) - A login system using menus asking for name/password rather than giving them as one command @@ -59,6 +59,8 @@ things you want from here into your game folder and change them there. * EGI_Client (gtaylor 2016) - Client for reporting game status to the Evennia game index (games.evennia.com) +* In-game Python (Vincent Le Goff 2017) - Allow trusted builders to script + objects and events using Python from in-game. * Tutorial examples (Griatch 2011, 2015) - A folder of basic example objects, commands and scripts. * Tutorial world (Griatch 2011, 2015) - A folder containing the diff --git a/evennia/contrib/generator.py b/evennia/contrib/generator.py index 5bc0395ece..84d6401dcf 100644 --- a/evennia/contrib/generator.py +++ b/evennia/contrib/generator.py @@ -1,7 +1,7 @@ """ Pseudo-random generator and registry -Evennia contribution - Vincent-lg 2017 +Evennia contribution - Vincent Le Goff 2017 This contrib can be used to generate pseudo-random strings of information with specific criteria. You could, for instance, use it to generate @@ -14,14 +14,14 @@ Here's a very simple example: from evennia.contrib.generator import Generator # Create a generator for phone numbers phone_generator = Generator("phone number", r"555-\d{3}-\d{4}") -# Generate a phone number +# Generate a phone number (555-XXX-XXXX with X as numbers) number = phone_generator.generate() # `number` will contain something like: "555-981-2207" # If you call `phone_generator.generate`, it won't give the same anymore. phone_generator.all() # Will return a list of all currently-used phone numbers phone_generator.free("555-981-2207") -# The number can be generated again. +# The number can be generated again ``` To use it, you will need to: @@ -107,8 +107,16 @@ class Generator(object): to remove a generated string, or the `clear` method to remove all generated strings. + Bear in mind, however, that while the generated strings will be + stored to avoid repetition, the generator will not concern itself + with how the string is stored on the object you use. You probably + want to create a tag to mark this object. This is outside of the scope + of this class. + """ + # We keep the script as a class variable to optimize querying + # with multiple instandces script = None def __init__(self, name, regex): @@ -142,7 +150,7 @@ class Generator(object): # Analyze the regex if any if regex: - self.find_elements(regex) + self._find_elements(regex) def __repr__(self): return "".format(self.name) @@ -166,7 +174,7 @@ class Generator(object): Args: store (bool, optional): store the generated string in the script. - keep_trying (bool, optional): keep on trying if the string already exists. + keep_trying (bool, optional): keep on trying if the string is already used. Returns: The newly-generated string. @@ -212,7 +220,7 @@ class Generator(object): def free(self, element): """ - Removes a generated string from the list of stored strings. + Remove a generated string from the list of stored strings. Args: element (str): the string to remove from the list of generated strings. @@ -254,7 +262,7 @@ class Generator(object): type(self).script = script return script - def find_elements(self, regex): + def _find_elements(self, regex): """ Find the elements described in the regular expression. This will analyze the provided regular expression and try to find elements. From c17982e11821b9ba92d0478f61827f9cc641d818 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Sat, 22 Jul 2017 11:36:38 +0200 Subject: [PATCH 03/13] Add a test for the generator contrib --- evennia/contrib/tests.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index bbf6d29d43..d89ac6c7b6 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -985,3 +985,24 @@ class TestUnixCommand(CommandTest): lines = ret.splitlines() self.assertTrue(any(l.startswith("usage:") for l in lines)) self.assertTrue(any(l.startswith("dummy: error:") for l in lines)) + + +from evennia.contrib import generator + +SIMPLE_GENERATOR = generator.Generator("simple", "[01]{2}") + +class TestGenerator(EvenniaTest): + + def test_generate(self): + """Generate and fail when exhausted.""" + generated = [] + for i in range(4): + generated.append(SIMPLE_GENERATOR.generate()) + + generated.sort() + self.assertEqual(generated, ["00", "01", "10", "11"]) + + # At this point, we have generated 4 strings. + # We can't generate one more + with self.assertRaises(generator.ExhaustedGenerator): + SIMPLE_GENERATOR.generate() From c324646055cd80bf27e6b9bdc9a3ab559cbabb2a Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Tue, 25 Jul 2017 14:28:54 +0200 Subject: [PATCH 04/13] Update the generator module with new method names --- evennia/contrib/generator.py | 198 +++++++++++++++++------------------ evennia/contrib/tests.py | 4 +- 2 files changed, 101 insertions(+), 101 deletions(-) diff --git a/evennia/contrib/generator.py b/evennia/contrib/generator.py index 84d6401dcf..91cfa77c7b 100644 --- a/evennia/contrib/generator.py +++ b/evennia/contrib/generator.py @@ -15,12 +15,12 @@ from evennia.contrib.generator import Generator # Create a generator for phone numbers phone_generator = Generator("phone number", r"555-\d{3}-\d{4}") # Generate a phone number (555-XXX-XXXX with X as numbers) -number = phone_generator.generate() +number = phone_generator.get() # `number` will contain something like: "555-981-2207" -# If you call `phone_generator.generate`, it won't give the same anymore. +# If you call `phone_generator.get`, it won't give the same anymore. phone_generator.all() # Will return a list of all currently-used phone numbers -phone_generator.free("555-981-2207") +phone_generator.remove("555-981-2207") # The number can be generated again ``` @@ -30,7 +30,7 @@ To use it, you will need to: 2. Create an instance of this class taking two arguments: - Tje name of tje gemerator (like "phone number", "license plate"...). - The regular expression representing the expected results. -3. Use the generator's `all`, `generate` and `free` methods as shown above. +3. Use the generator's `all`, `get` and `remove` methods as shown above. Some examples of regular expressions you could use: @@ -103,7 +103,7 @@ class Generator(object): The "rule" defining what the generator should provide in terms of string is given as a regular expression when creating instances of this class. You can use the `all` method to get all generated strings, - the `generate` method to generate a new string, the `free` method + the `get` method to generate a new string, the `remove` method to remove a generated string, or the `clear` method to remove all generated strings. @@ -155,100 +155,6 @@ class Generator(object): def __repr__(self): return "".format(self.name) - def all(self): - """ - Return all generated strings for this generator. - - Returns: - strings (list of strr): the list of strings that are already - used. The strings that were generated first come first in the list. - - """ - script = self._get_script() - generated = list(script.db.generated.get(self.name, [])) - return generated - - def generate(self, store=True, keep_trying=True): - """ - Generate a pseudo-random string according to the regular expression. - - Args: - store (bool, optional): store the generated string in the script. - keep_trying (bool, optional): keep on trying if the string is already used. - - Returns: - The newly-generated string. - - Raises: - ExhaustedGenerator: if there's no available string in this generator. - - Note: - Unless asked explicitly, the returned string can't repeat itself. - - """ - script = self._get_script() - generated = script.db.generated.get(self.name) - if generated is None: - script.db.generated[self.name] = [] - generated = script.db.generated[self.name] - - if len(generated) >= self.total: - raise ExhaustedGenerator - - # Generate a pseudo-random string that might be used already - result = "" - for element in self.elements: - number = randint(element["min"], element["max"]) - chars = element["chars"] - for index in range(number): - char = choice(chars) - result += char - - # If the string has already been generated, try again - if result in generated and keep_trying: - # Change the random seed, incrementing it slowly - epoch = time.time() - while result in generated: - epoch += 1 - seed(epoch) - result = self.generate(store=False, keep_trying=False) - - if store: - generated.append(result) - - return result - - def free(self, element): - """ - Remove a generated string from the list of stored strings. - - Args: - element (str): the string to remove from the list of generated strings. - - Note: - The specified string has to be present in the script (so - has to have been generated). It will remove this entry - from the script, so this string could be generated again by - calling the `generate` method. - - """ - script = self._get_script() - generated = script.db.generated.get(self.name, []) - if element not in generated: - raise ValueError("the string {} isn't stored as generated by the generator {}".format( - element, self.name)) - - generated.remove(element) - - def clear(self): - """ - Clear the generator of all generated strings. - - """ - script = self._get_script() - generated = script.db.generated.get(self.name, []) - generated[:] = [] - def _get_script(self): """Get or create the script.""" if type(self).script: @@ -338,3 +244,97 @@ class Generator(object): raise RejectedRegex("cannot find the literal: {}".format(element[0])) return chars + + def all(self): + """ + Return all generated strings for this generator. + + Returns: + strings (list of strr): the list of strings that are already + used. The strings that were generated first come first in the list. + + """ + script = self._get_script() + generated = list(script.db.generated.get(self.name, [])) + return generated + + def get(self, store=True, keep_trying=True): + """ + Generate a pseudo-random string according to the regular expression. + + Args: + store (bool, optional): store the generated string in the script. + keep_trying (bool, optional): keep on trying if the string is already used. + + Returns: + The newly-generated string. + + Raises: + ExhaustedGenerator: if there's no available string in this generator. + + Note: + Unless asked explicitly, the returned string can't repeat itself. + + """ + script = self._get_script() + generated = script.db.generated.get(self.name) + if generated is None: + script.db.generated[self.name] = [] + generated = script.db.generated[self.name] + + if len(generated) >= self.total: + raise ExhaustedGenerator + + # Generate a pseudo-random string that might be used already + result = "" + for element in self.elements: + number = randint(element["min"], element["max"]) + chars = element["chars"] + for index in range(number): + char = choice(chars) + result += char + + # If the string has already been generated, try again + if result in generated and keep_trying: + # Change the random seed, incrementing it slowly + epoch = time.time() + while result in generated: + epoch += 1 + seed(epoch) + result = self.get(store=False, keep_trying=False) + + if store: + generated.append(result) + + return result + + def remove(self, element): + """ + Remove a generated string from the list of stored strings. + + Args: + element (str): the string to remove from the list of generated strings. + + Note: + The specified string has to be present in the script (so + has to have been generated). It will remove this entry + from the script, so this string could be generated again by + calling the `get` method. + + """ + script = self._get_script() + generated = script.db.generated.get(self.name, []) + if element not in generated: + raise ValueError("the string {} isn't stored as generated by the generator {}".format( + element, self.name)) + + generated.remove(element) + + def clear(self): + """ + Clear the generator of all generated strings. + + """ + script = self._get_script() + generated = script.db.generated.get(self.name, []) + generated[:] = [] diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index d89ac6c7b6..6add0571a4 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -997,7 +997,7 @@ class TestGenerator(EvenniaTest): """Generate and fail when exhausted.""" generated = [] for i in range(4): - generated.append(SIMPLE_GENERATOR.generate()) + generated.append(SIMPLE_GENERATOR.get()) generated.sort() self.assertEqual(generated, ["00", "01", "10", "11"]) @@ -1005,4 +1005,4 @@ class TestGenerator(EvenniaTest): # At this point, we have generated 4 strings. # We can't generate one more with self.assertRaises(generator.ExhaustedGenerator): - SIMPLE_GENERATOR.generate() + SIMPLE_GENERATOR.get() From 4845be13db1772a10ece5a170f9cfcaad56d3327 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Tue, 25 Jul 2017 15:25:55 +0200 Subject: [PATCH 05/13] Fix a typo in the generator contrib --- evennia/contrib/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/generator.py b/evennia/contrib/generator.py index 91cfa77c7b..a1a48eacae 100644 --- a/evennia/contrib/generator.py +++ b/evennia/contrib/generator.py @@ -28,7 +28,7 @@ To use it, you will need to: 1. Import the `Generator` class from the contrib. 2. Create an instance of this class taking two arguments: - - Tje name of tje gemerator (like "phone number", "license plate"...). + - The name of tje gemerator (like "phone number", "license plate"...). - The regular expression representing the expected results. 3. Use the generator's `all`, `get` and `remove` methods as shown above. From 2437ddccc1b4911f95d13213d5a326238166c39c Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Tue, 25 Jul 2017 22:25:29 +0200 Subject: [PATCH 06/13] Rename the generator contrib into random_string_generator --- evennia/contrib/README.md | 4 ++-- .../contrib/{generator.py => random_string_generator.py} | 4 ++-- evennia/contrib/tests.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) rename evennia/contrib/{generator.py => random_string_generator.py} (98%) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index a30b7cb64a..5515cfd923 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -30,8 +30,6 @@ things you want from here into your game folder and change them there. multiple descriptions for time and season as well as details. * GenderSub (Griatch 2015) - Simple example (only) of storing gender on a character and access it in an emote with a custom marker. -* Generator (Vincent Le Goff 2017) - Simple pseudo-random generator of - strings with rules, avoiding repetitions. * Mail (grungies1138 2016) - An in-game mail system for communication. * Menu login (Griatch 2011) - A login system using menus asking for name/password rather than giving them as one command @@ -40,6 +38,8 @@ things you want from here into your game folder and change them there. * Menu Login (Vincent-lg 2016) - Alternate login system using EvMenu. * Multidescer (Griatch 2016) - Advanced descriptions combined from many separate description components, inspired by MUSH. +* Random_string_generator (Vincent Le Goff 2017) - Simple pseudo-random + gereator of strings with rules, avoiding repetitions. * RPLanguage (Griatch 2015) - Dynamic obfuscation of emotes when speaking unfamiliar languages. Also obfuscates whispers. * RPSystem (Griatch 2015) - Full director-style emoting system diff --git a/evennia/contrib/generator.py b/evennia/contrib/random_string_generator.py similarity index 98% rename from evennia/contrib/generator.py rename to evennia/contrib/random_string_generator.py index a1a48eacae..76198c5729 100644 --- a/evennia/contrib/generator.py +++ b/evennia/contrib/random_string_generator.py @@ -11,7 +11,7 @@ stored and won't be available again in order to avoid repetition. Here's a very simple example: ```python -from evennia.contrib.generator import Generator +from evennia.contrib.random_string_generator import Generator # Create a generator for phone numbers phone_generator = Generator("phone number", r"555-\d{3}-\d{4}") # Generate a phone number (555-XXX-XXXX with X as numbers) @@ -163,7 +163,7 @@ class Generator(object): try: script = ScriptDB.objects.get(db_key="generator_script") except ScriptDB.DoesNotExist: - script = create_script("contrib.generator.GeneratorScript") + script = create_script("contrib.random_string_generator.GeneratorScript") type(self).script = script return script diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 6add0571a4..bc37eb42de 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -987,11 +987,11 @@ class TestUnixCommand(CommandTest): self.assertTrue(any(l.startswith("dummy: error:") for l in lines)) -from evennia.contrib import generator +from evennia.contrib import random_string_generator -SIMPLE_GENERATOR = generator.Generator("simple", "[01]{2}") +SIMPLE_GENERATOR = random_string_generator.Generator("simple", "[01]{2}") -class TestGenerator(EvenniaTest): +class TestRandomStringGenerator(EvenniaTest): def test_generate(self): """Generate and fail when exhausted.""" @@ -1004,5 +1004,5 @@ class TestGenerator(EvenniaTest): # At this point, we have generated 4 strings. # We can't generate one more - with self.assertRaises(generator.ExhaustedGenerator): + with self.assertRaises(random_string_generator.ExhaustedGenerator): SIMPLE_GENERATOR.get() From f6255caadece8c8c44470d79a514108967c453d8 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Tue, 25 Jul 2017 22:40:27 +0200 Subject: [PATCH 07/13] Allow contrib.ingame_python.scripts.EventHandler.get_events to work with typeclasses --- evennia/contrib/ingame_python/scripts.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/ingame_python/scripts.py b/evennia/contrib/ingame_python/scripts.py index 11a1b9e24a..af74853ffb 100644 --- a/evennia/contrib/ingame_python/scripts.py +++ b/evennia/contrib/ingame_python/scripts.py @@ -10,7 +10,7 @@ import traceback from django.conf import settings from evennia import DefaultObject, DefaultScript, ChannelDB, ScriptDB -from evennia import logger +from evennia import logger, ObjectDB from evennia.utils.ansi import raw from evennia.utils.create import create_channel from evennia.utils.dbserialize import dbserialize @@ -101,7 +101,7 @@ class EventHandler(DefaultScript): Return a dictionary of events on this object. Args: - obj (Object): the connected object. + obj (Object or typeclass): the connected object or typeclass. Returns: A dictionary of the object's events. @@ -115,7 +115,11 @@ class EventHandler(DefaultScript): events = {} all_events = self.ndb.events classes = Queue() - classes.put(type(obj)) + if isinstance(obj, ObjectDB): + classes.put(type(obj)) + else: + classes.put(obj) + invalid = [] while not classes.empty(): typeclass = classes.get() From bafd069d970d481e8f4663955bad2d8ed5827ce8 Mon Sep 17 00:00:00 2001 From: Tehom Date: Fri, 28 Jul 2017 01:26:00 -0400 Subject: [PATCH 08/13] Create fallback for default cmdsets that fail to load --- evennia/commands/cmdsethandler.py | 8 ++++++++ evennia/settings_default.py | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index 3f01dbcac2..2042588683 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -351,6 +351,14 @@ class CmdSetHandler(object): elif path: cmdset = self._import_cmdset(path) if cmdset: + if cmdset.key == '_CMDSET_ERROR': + # If a cmdset fails to load, check if we have a fallback path to use + fallback_path = settings.CMDSET_FALLBACKS.get(path, None) + if fallback_path: + cmdset = self._import_cmdset(fallback_path) + # If no cmdset is returned from the fallback, we can't go further + if not cmdset: + continue cmdset.permanent = cmdset.key != '_CMDSET_ERROR' self.cmdset_stack.append(cmdset) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index f94bf5d3ef..cc90972f16 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -348,6 +348,12 @@ CMDSET_CHARACTER = "commands.default_cmdsets.CharacterCmdSet" CMDSET_PLAYER = "commands.default_cmdsets.PlayerCmdSet" # Location to search for cmdsets if full path not given CMDSET_PATHS = ["commands", "evennia", "contribs"] +# Fallbacks for cmdset paths that fail to load. Note that if you change the path for your default cmdsets, +# you will also need to copy CMDSET_FALLBACKS after your change in your settings file for it to detect the change. +CMDSET_FALLBACKS = {CMDSET_CHARACTER: 'evennia.commands.default.cmdset_character.CharacterCmdSet', + CMDSET_PLAYER: 'evennia.commands.default.cmdset_player.PlayerCmdSet', + CMDSET_SESSION: 'evennia.commands.default.cmdset_session.SessionCmdSet', + CMDSET_UNLOGGEDIN: 'evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet'} # Parent class for all default commands. Changing this class will # modify all default commands, so do so carefully. COMMAND_DEFAULT_CLASS = "evennia.commands.default.muxcommand.MuxCommand" From 72a91ebce6b947ab2ab83729a43a0d2640eac2b4 Mon Sep 17 00:00:00 2001 From: Tehom Date: Tue, 8 Aug 2017 20:46:48 -0400 Subject: [PATCH 09/13] Add logging for fallback. --- evennia/commands/cmdsethandler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index 2042588683..3ecbd8bd36 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -355,9 +355,12 @@ class CmdSetHandler(object): # If a cmdset fails to load, check if we have a fallback path to use fallback_path = settings.CMDSET_FALLBACKS.get(path, None) if fallback_path: + logger.log_err("Error encountered for cmdset at path %s. Replacing with: %s" % ( + path, fallback_path)) cmdset = self._import_cmdset(fallback_path) # If no cmdset is returned from the fallback, we can't go further if not cmdset: + logger.log_err("Fallback path '%s' failed to generate a cmdset." % fallback_path) continue cmdset.permanent = cmdset.key != '_CMDSET_ERROR' self.cmdset_stack.append(cmdset) From 32ad83d51c2875719578c5e36243ced3ad378e5f Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Thu, 10 Aug 2017 11:59:04 +0200 Subject: [PATCH 10/13] Fix documentation in the new option of the event handler --- evennia/contrib/ingame_python/scripts.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/evennia/contrib/ingame_python/scripts.py b/evennia/contrib/ingame_python/scripts.py index af74853ffb..cf538a7846 100644 --- a/evennia/contrib/ingame_python/scripts.py +++ b/evennia/contrib/ingame_python/scripts.py @@ -101,24 +101,28 @@ class EventHandler(DefaultScript): Return a dictionary of events on this object. Args: - obj (Object or typeclass): the connected object or typeclass. + obj (Object or typeclass): the connected object or a general typeclass. Returns: A dictionary of the object's events. - Note: + Notes: Events would define what the object can have as callbacks. Note, however, that chained callbacks will not appear in events and are handled separately. + You can also request the events of a typeclass, not a + connected object. This is useful to get the global list + of events for a typeclass that has no object yet. + """ events = {} all_events = self.ndb.events classes = Queue() - if isinstance(obj, ObjectDB): - classes.put(type(obj)) - else: + if isinstance(obj, type): classes.put(obj) + else: + classes.put(type(obj)) invalid = [] while not classes.empty(): From ee0974313c5a1ca810effb485eb27aa34b812944 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Sun, 13 Aug 2017 11:32:22 +0200 Subject: [PATCH 11/13] Fix several issues in the contrib --- evennia/contrib/random_string_generator.py | 43 ++++++++++++---------- evennia/contrib/tests.py | 2 +- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/evennia/contrib/random_string_generator.py b/evennia/contrib/random_string_generator.py index 76198c5729..cdf38d6831 100644 --- a/evennia/contrib/random_string_generator.py +++ b/evennia/contrib/random_string_generator.py @@ -11,14 +11,13 @@ stored and won't be available again in order to avoid repetition. Here's a very simple example: ```python -from evennia.contrib.random_string_generator import Generator +from evennia.contrib.random_string_generator import RandomStringGenerator # Create a generator for phone numbers -phone_generator = Generator("phone number", r"555-\d{3}-\d{4}") +phone_generator = RandomStringGenerator("phone number", r"555-[0-9]{3}-[0-9]{4}") # Generate a phone number (555-XXX-XXXX with X as numbers) number = phone_generator.get() # `number` will contain something like: "555-981-2207" -# If you call `phone_generator.get`, it won't give the same anymore. -phone_generator.all() +# If you call `phone_generator.get`, it won't give the same anymore.phone_generator.all() # Will return a list of all currently-used phone numbers phone_generator.remove("555-981-2207") # The number can be generated again @@ -26,12 +25,14 @@ phone_generator.remove("555-981-2207") To use it, you will need to: -1. Import the `Generator` class from the contrib. +1. Import the `RandomStringGenerator` class from the contrib. 2. Create an instance of this class taking two arguments: - - The name of tje gemerator (like "phone number", "license plate"...). + - The name of the gemerator (like "phone number", "license plate"...). - The regular expression representing the expected results. 3. Use the generator's `all`, `get` and `remove` methods as shown above. +To understand how to read and create regular expressions, you can refer to +[the documentation on the re module](https://docs.python.org/2/library/re.html). Some examples of regular expressions you could use: - `r"555-\d{3}-\d{4}"`: 555, a dash, 3 digits, another dash, 4 digits. @@ -40,10 +41,11 @@ Some examples of regular expressions you could use: - ... Behind the scenes, a script is created to store the generated information -for a single generator. The `Generator` object will also read the regular -expression you give to it to see what information is required (letters, -digits, a more restricted class, simple characters...). More complex -regular expressions (with branches for instance) might not be available. +for a single generator. The `RandomStringGenerator` object will also +read the regular expression you give to it to see what information is +required (letters, digits, a more restricted class, simple characters...)... +More complex regular expressions (with branches for instance) might not be +available. """ @@ -75,13 +77,13 @@ class ExhaustedGenerator(RuntimeError): pass -class GeneratorScript(DefaultScript): +class RandomStringGeneratorScript(DefaultScript): """ The global script to hold all generators. It will be automatically created the first time `generate` is called - on a Generator object. + on a RandomStringGenerator object. """ @@ -95,7 +97,7 @@ class GeneratorScript(DefaultScript): self.db.generated = {} -class Generator(object): +class RandomStringGenerator(object): """ A generator class to generate pseudo-random strings with a rule. @@ -153,7 +155,7 @@ class Generator(object): self._find_elements(regex) def __repr__(self): - return "".format(self.name) + return "".format(self.name) def _get_script(self): """Get or create the script.""" @@ -163,7 +165,7 @@ class Generator(object): try: script = ScriptDB.objects.get(db_key="generator_script") except ScriptDB.DoesNotExist: - script = create_script("contrib.random_string_generator.GeneratorScript") + script = create_script("contrib.random_string_generator.RandomStringGeneratorScript") type(self).script = script return script @@ -258,13 +260,13 @@ class Generator(object): generated = list(script.db.generated.get(self.name, [])) return generated - def get(self, store=True, keep_trying=True): + def get(self, store=True, unique=True): """ Generate a pseudo-random string according to the regular expression. Args: store (bool, optional): store the generated string in the script. - keep_trying (bool, optional): keep on trying if the string is already used. + unique (bool, optional): keep on trying if the string is already used. Returns: The newly-generated string. @@ -295,13 +297,13 @@ class Generator(object): result += char # If the string has already been generated, try again - if result in generated and keep_trying: + if result in generated and unique: # Change the random seed, incrementing it slowly epoch = time.time() while result in generated: epoch += 1 seed(epoch) - result = self.get(store=False, keep_trying=False) + result = self.get(store=False, unique=False) if store: generated.append(result) @@ -315,6 +317,9 @@ class Generator(object): Args: element (str): the string to remove from the list of generated strings. + Raises: + ValueError: the specified value hasn't been generated and is not present. + Note: The specified string has to be present in the script (so has to have been generated). It will remove this entry diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index bc37eb42de..7bc07ff22d 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -989,7 +989,7 @@ class TestUnixCommand(CommandTest): from evennia.contrib import random_string_generator -SIMPLE_GENERATOR = random_string_generator.Generator("simple", "[01]{2}") +SIMPLE_GENERATOR = random_string_generator.RandomStringGenerator("simple", "[01]{2}") class TestRandomStringGenerator(EvenniaTest): From 1030ac6c804ce7cd43f4ee6cf295be6ed053d96c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 13 Aug 2017 14:39:14 +0200 Subject: [PATCH 12/13] Minor text fix in contrib README --- evennia/contrib/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index 5515cfd923..378af26c32 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -38,8 +38,8 @@ things you want from here into your game folder and change them there. * Menu Login (Vincent-lg 2016) - Alternate login system using EvMenu. * Multidescer (Griatch 2016) - Advanced descriptions combined from many separate description components, inspired by MUSH. -* Random_string_generator (Vincent Le Goff 2017) - Simple pseudo-random - gereator of strings with rules, avoiding repetitions. +* Random String Generator (Vincent Le Goff 2017) - Simple pseudo-random + generator of strings with rules, avoiding repetitions. * RPLanguage (Griatch 2015) - Dynamic obfuscation of emotes when speaking unfamiliar languages. Also obfuscates whispers. * RPSystem (Griatch 2015) - Full director-style emoting system From 09b9784c3f703ed7efe1eb36d4f8d4306dd48763 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 13 Aug 2017 17:17:27 +0200 Subject: [PATCH 13/13] Some refactoring of the cmdsethandler updates. Also took the opportunity to add the in-game-error report while I was at it. --- evennia/commands/cmdsethandler.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index 3ecbd8bd36..b52c70d3a8 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -80,6 +80,8 @@ __all__ = ("import_cmdset", "CmdSetHandler") _CACHED_CMDSETS = {} _CMDSET_PATHS = utils.make_iter(settings.CMDSET_PATHS) _IN_GAME_ERRORS = settings.IN_GAME_ERRORS +_CMDSET_FALLBACKS = settings.CMDSET_FALLBACKS + # Output strings @@ -102,6 +104,16 @@ _ERROR_CMDSET_EXCEPTION = _( Compile/Run error when loading cmdset '{path}'.", (Traceback was logged {timestamp})""") +_ERROR_CMDSET_FALLBACK = _( +""" +Error encountered for cmdset at path '{path}'. +Replacing with fallback '{fallback_path}'. +""") + +_ERROR_CMDSET_NO_FALLBACK = _( +"""Fallback path '{fallback_path}' failed to generate a cmdset.""" +) + class _ErrorCmdSet(CmdSet): """ @@ -353,14 +365,19 @@ class CmdSetHandler(object): if cmdset: if cmdset.key == '_CMDSET_ERROR': # If a cmdset fails to load, check if we have a fallback path to use - fallback_path = settings.CMDSET_FALLBACKS.get(path, None) + fallback_path = _CMDSET_FALLBACKS.get(path, None) if fallback_path: - logger.log_err("Error encountered for cmdset at path %s. Replacing with: %s" % ( - path, fallback_path)) + err = _ERROR_CMDSET_FALLBACK.format(path=path, fallback_path=fallback_path) + logger.log_err(err) + if _IN_GAME_ERRORS: + self.obj.msg(err) cmdset = self._import_cmdset(fallback_path) # If no cmdset is returned from the fallback, we can't go further if not cmdset: - logger.log_err("Fallback path '%s' failed to generate a cmdset." % fallback_path) + err = _ERROR_CMDSET_NO_FALLBACK.format(fallback_path=fallback_path) + logger.log_err(err) + if _IN_GAME_ERRORS: + self.obj.msg(err) continue cmdset.permanent = cmdset.key != '_CMDSET_ERROR' self.cmdset_stack.append(cmdset)