Source code for mkname.mod

"""
.. _mod_api:

Modifying Names
===============

Functions for modifying names after they've been generated.


.. _simple_mod:

Simple Mods
-----------
Simple mods only require one parameter: the name to modify. This
makes them a bit limited in what they do, but it's easier to call
them from the command line.

.. autofunction:: mkname.double_vowel
.. autofunction:: mkname.garble
.. autofunction:: mkname.make_scifi
.. autofunction:: mkname.vulcanize


Registration
------------
Simple mods can be registered for use in the `mkname.mods`
registry by using the :class:`mkname.simple_mods` decorator.

.. autoclass:: mkname.simple_mod


Complex Mods
------------
Any mod that requires multiple parameters is a complex mod. These
allow more flexible behavior but cannot be used directly through
the `mkname` command line tool.

.. autofunction:: mkname.add_letters
.. autofunction:: mkname.add_punctuation
.. autofunction:: mkname.compound_names
.. autofunction:: mkname.double_letter
.. autofunction:: mkname.translate_characters

"""
import base64 as b64
from collections.abc import Callable, Mapping, Sequence
from functools import partial

from mkname.constants import *
from mkname.model import SimpleMod
from mkname.utility import roll


# Names that will be imported when using *.
__all__ = [
    # Mods.
    'add_letters',
    'add_punctuation',
    'compound_names',
    'double_letter',
    'double_vowel',
    'garble',
    'make_scifi',
    'translate_characters',
    'vulcanize',

    # Mods registration and registry.
    'mods',
    'simple_mod',
]


# Mod registration.
mods: dict[str, SimpleMod] = {}


[docs] class simple_mod: """A decorator for registering a simple modifies to `mkname.mods`. :param key: The `dict` key the mod will be registered under. :returns: A :class:`function` object. :rtype: function :usage: You can add a :ref:`simple_mod` you create to the `mkname.mods` registery by using :class:`simple_mod` as a decorator for that function. You can then access that modifier from the `mkname.mods` registry withe its key: .. testsetup:: simple_mod from mkname.mod import mods, simple_mod .. testcode:: simple_mod @simple_mod('spam') def spam(name): return f'SPAM {name} SPAM!' mods['spam']('Graham') == 'SPAM Graham SPAM!' .. testoutput:: True """ def __init__(self, key: str) -> None: self.key = key def __call__(self, fn: SimpleMod) -> SimpleMod: mods[self.key] = fn return fn
# Simple mods.
[docs] @simple_mod('double_vowel') def double_vowel(name: str): """Double a vowel within the name, like what with that popular Star Wars™ franchise the kids are talking about. :param name: The name to modify. :return: A :class:`str` object. :rtype: str :usage: .. testsetup:: double_vowel from mkname import double_vowel import yadr.operator as yop yop.random.seed('spam') .. doctest:: double_vowel >>> name = 'Bacon' >>> double_vowel(name) 'Baacon' """ letters = VOWELS return double_letter(name, letters)
[docs] @simple_mod('garble') def garble(name: str): """Garble some characters in the name by base 64 encoding them. :param name: The name to modify. :return: A :class:`str` object. :rtype: str :usage: .. testsetup:: garble from mkname import garble import yadr.operator as yop yop.random.seed('spam') .. doctest:: garble >>> name = 'Eggs' >>> garble(name) 'Rqggs' """ # Determine which character should be garbled. index = roll(f'1d{len(name)}') - 1 # Use base64 encoding to turn the character in a sequence of # different characters. Base64 only works with bytes. char = bytes(name[index], encoding='utf_8') garbled_bytes = b64.encodebytes(char) garbled = str(garbled_bytes, encoding='utf_8') # Transform characters that are valid in base64 but might # not make sense for this kind of name. garbled = garbled.replace('=', ' ') garbled = garbled.rstrip() # Add the garbled characters back into the name and return. name = _insert_substr(name, garbled, index, replace=True) return name.capitalize()
[docs] @simple_mod('make_scifi') def make_scifi(name: str) -> str: """A simple version of :func:`mkname.mod.add_scifi_letters`. :param name: The name to modify. :return: A :class:`str` object. :rtype: str :usage: .. testsetup:: make_scifi from mkname import make_scifi import yadr.operator as yop yop.random.seed('spam') .. doctest:: make_scifi >>> name = 'Eggs' >>> make_scifi(name) 'Keggs' """ return add_letters(name)
[docs] @simple_mod('vulcanize') def vulcanize(name: str) -> str: """Add prefixes to names that are similar to the prefixes seen in Vulcan characters in the Star Trek™ franchise. :param name: The name to modify. :return: A :class:`str` object. :rtype: str :usage: .. testsetup:: vulcanize from mkname import vulcanize import yadr.operator as yop yop.random.seed('spam') .. doctest:: vulcanize >>> name = 'Bacon' >>> vulcanize(name) "T'Bacon" """ letter = 't' if roll('1d6') > 5: letters = 'd k l m n p s su v'.split() index = roll(f'1d{len(letters)}') - 1 letter = letters[index] letter = letter.title() name = name.title() return f"{letter}'{name}"
# Complex mods.
[docs] def add_letters( name: str, letters: str = SCIFI_LETTERS, vowels: str = VOWELS ) -> str: """Add one of the given letters to a name. :param name: The name to modify. :param letters: The letters to add for the modification. :param vowels: The letters to define as vowels. :return: A :class:`str` object. :rtype: str :usage: .. testsetup:: add_letters from mkname import add_letters import yadr.operator as yop yop.random.seed('spam') .. doctest:: add_letters >>> name = 'Eggs' >>> add_letters(name) 'Keggs' In most cases, the function behaves like the given letters are consonants. While it will replace consonants with the letter, it will often try to put a letter before or after a vowel. This means you can alter the behavior by passing different values to the letters and vowels.: .. doctest:: add_letters >>> # Treat 'e' as a consonant and don't use 'k'. >>> letter = 'qxz' >>> vowels = 'aiou' >>> name = 'Eggs' >>> add_letters(name, letter, vowels) 'Eggz' """ # Determine the letter and where the letter should go in the name. letter_index = roll(f'1d{len(letters)}') - 1 letter = letters[letter_index] choice = roll('1d12') wild = roll('1d20') name = name.casefold() replace = False # On a 1-5, put the letter at the beginning. if choice < 6: index = 0 if name[0] not in vowels: replace = True name = _insert_substr(name, letter, index, replace=replace) # On a 6-10, put the letter at the end. elif choice < 11: index = len(name) if name[-1] not in vowels: replace = True index -= 1 name = _insert_substr(name, letter, index, replace=replace) # On an 11 or 12, replace a random letter in the name. elif wild < 20: index = roll(f'1d{len(name)}') - 1 replace = True name = _insert_substr(name, letter, index, replace=replace) # On an 11 or 12, if wild is 20, replace multiple letters. else: len_roll = f'1d{len(name)}' count = roll(len_roll) indices = [roll(len_roll) - 1 for _ in range(count)] replace = True for index in indices: name = _insert_substr(name, letter, index, replace=replace) name = name.capitalize() return name
[docs] def add_punctuation( name: str, punctuation: Sequence[str] = PUNCTUATION, cap_before: bool = True, cap_after: bool = True, index: int | None = None ) -> str: """Add a punctuation mark to the name. :param name: The name to modify. :param punctuation: (Optional.) The punctuation marks to choose from. Defaults to the default set of punctuation marks in mkname.constants. :param cap_before: (Optional.) Whether the first letter of the substring before the punctuation mark should be capitalized. Defaults to capitalizing. :param cap_after: (Optional.) Whether the first letter after the punctuation mark should be capitalized. Defaults to capitalizing. :param index: (Optional.) Where to insert the punctuation. Defaults to picking an index at random. :return: A :class:`str` object. :rtype: str :usage: .. testsetup:: add_punctuation from mkname import add_punctuation import yadr.operator as yop yop.random.seed('spam123') .. doctest:: add_punctuation >>> # Seed the RNG to make the example predictable. Don't do >>> name = 'eggs' >>> add_punctuation(name) 'E|Ggs' The cap_before and cap_after parameters set whether the substrings before or after the added punctuation should be capitalized. It defaults to capitalizing them both: .. doctest:: add_punctuation >>> name = 'eggs' >>> add_punctuation(name, cap_before=False) 'eg@Gs' >>> >>> yop.random.seed('spam123') >>> name = 'eggs' >>> add_punctuation(name, cap_after=False) 'E|ggs' If you want to specify were the punctuation goes, you can use the index parameter. The punctuation parameter also allows you to specify what punctuation is allowed: .. doctest:: add_punctuation >>> name = 'eggs' >>> punctuation = ':' >>> index = 2 >>> add_punctuation(name, punctuation, index=index) 'Eg:Gs' """ # Select the punctuation mark. len_mark = len(punctuation) mark_index = roll(f'1d{len_mark}') - 1 mark = punctuation[mark_index] # Determine where the mark will go if index is None: positions = len(name) + 1 index = roll(f'1d{positions}') - 1 # Add the mark and return. return _insert_substr(name, mark, index, cap_before, cap_after)
[docs] def compound_names( mod_name: str, root_name: str, consonants: Sequence[str] = CONSONANTS, vowels: Sequence[str] = VOWELS ) -> str: """Construct a new name using the parts of two names. :param names: A list of Name objects to use for constructing the new name. :param consonants: (Optional.) The characters to consider as consonants. :param vowels: (Optional.) The characters to consider as vowels. :return: A :class:`str` object. :rtype: str :usage: .. doctest:: mod >>> # Generate the name. >>> mod_name = 'Spam' >>> base_name = 'Eggs' >>> compound_names(mod_name, base_name) 'Speggs' The function takes into account whether the starting letter of each name is a vowel or a consonant when determining how to create the name. You can affect this by changing which letters it treats as consonants or vowels: .. doctest:: mod >>> # Treat 'e' as a consonant and 'g' as a vowel. >>> consonants = 'bcdfhjklmnpqrstvwxze' >>> vowels = 'aioug' >>> >>> # Generate the name. >>> mod_name = 'Spam' >>> base_name = 'Eggs' >>> compound_names(mod_name, base_name, consonants, vowels) 'Spggs' """ def get_change_index(s: str, letters): """Detect how many of the starting characters are in the given list. """ index = 1 while index < len(s) and s[index] in letters: index += 1 return index name = '' mod_name = mod_name.casefold() root_name = root_name.casefold() # When both names start with consonants, replace the starting # consonants in the root name with the starting consonants of # the mod name. if root_name[0] not in vowels and mod_name[0] not in vowels: index_start = get_change_index(mod_name, consonants) index_end = get_change_index(root_name, consonants) name = mod_name[0:index_start] + root_name[index_end:] # When the root name starts with a vowel but the mod name starts # with a consonant, just add the starting consonants of the mod # name to the start of the root name elif root_name[0] in vowels and mod_name[0] not in vowels: index_start = get_change_index(mod_name, consonants) name = mod_name[0:index_start] + root_name # If both names start with vowels, replace the starting vowels # of the root name with the starting vowels of the mod name. elif root_name[0] in vowels and mod_name[0] in vowels: index_start = get_change_index(mod_name, vowels) index_end = get_change_index(root_name, vowels) name = mod_name[0:index_start] + root_name[index_end:] # If the root name starts with a consonant and the mod name # starts with a vowel, add the starting vowels of the mod name # to the beginning of the root name. elif root_name[0] not in vowels and mod_name[0] in vowels: index_start = get_change_index(mod_name, vowels) name = mod_name[0:index_start] + root_name # This condition shouldn't be possible, so throw an exception # for debugging. else: msg = ('Names must start with either vowels or consonants. ' f'Names started with {mod_name[0]} and {root_name[0]}') raise ValueError(msg) return name.title()
[docs] def double_letter(name: str, letters: Sequence[str] = '') -> str: """Double a letter in the name. :param name: The name to modify. :param letters: (Optional.) The letters allowed to double. This defaults to all letters in the name. :return: A :class:`str` object. :rtype: str :usage: .. testsetup:: double_letter from mkname import double_letter import yadr.operator as yop yop.random.seed('spam12345') .. doctest:: double_letter >>> name = 'Bacon' >>> double_letter(name) 'Baacon' You can limit the numbers that it will double by passing a string of valid letters: .. doctest:: double_letter >>> # The valid letters to double. >>> letters = 'bcn' >>> >>> name = 'Bacon' >>> double_letter(name, letters) 'Baccon' """ if letters and not set(name).intersection(set(letters)): return name if not letters: name_len = len(name) index = roll(f'1d{name_len}') - 1 else: possibilities = [i for i, c in enumerate(name) if c in letters] poss_len = len(possibilities) poss_index = roll(f'1d{poss_len}') - 1 index = possibilities[poss_index] return name[0:index] + name[index] + name[index:]
[docs] def translate_characters( name: str, char_map: Mapping[str, str], casefold: bool = True ) -> str: """Translate characters in the name to different characters. :param name: The name to modify. :param char_map: A translation map for the characters in the name. The keys are the original letters and the values are the characters to change them to. :param casefold: Whether case should be ignored for the transform. :return: A :class:`str` object. :rtype: str :usage: .. doctest:: mod >>> # The translation map is a dict. >>> char_map = {'s': 'e', 'p': 'g', 'm': 's'} >>> >>> name = 'spam' >>> translate_characters(name, char_map) 'egas' """ if casefold: name = name.casefold() char_dict = dict(char_map) trans_map = str.maketrans(char_dict) return name.translate(trans_map)
# Private utility functions. def _insert_substr( text: str, substr: str, index: int, cap_before: bool = False, cap_after: bool = False, replace: bool = False ) -> str: """Insert a substring into the text. :param text: The string to modify. :param substr: The substring to insert into the text. :param index: The location to insert substr into the text. :param cap_before: (Optional.) Whether to capitalize the first letter of the section of text before the insertion. Defaults to `False`. :param cap_after: (Optional.) Whether to capitalize the first letter of the section of text after the insertion. Defaults to `False`. :param replace: (Optional.) Whether to capitalize the first letter of the inserted substring. Defaults to `False`. :returns: A :class:`str` object. :rtype: str """ before = text[0:index] if replace: index += 1 after = text[index:] if cap_before: before = before.title() if cap_after: after = after.title() return f'{before}{substr}{after}'