├── coolname ├── py.typed ├── data │ ├── adjective_first.txt │ ├── size.txt │ ├── prefix.txt │ ├── noun_adjective.txt │ ├── animal_breed.txt │ ├── from2.txt │ ├── animal_legendary.txt │ ├── from_noun_no_mod.txt │ ├── of_noun_no_mod.txt │ ├── adjective_near.txt │ ├── of_modifier.txt │ ├── subj2.txt │ ├── of_noun.txt │ ├── config.json │ ├── adjective.txt │ └── animal.txt ├── __init__.py ├── exceptions.py ├── __main__.py ├── config.py ├── loader.py └── impl.py ├── tests ├── __init__.py ├── common.py ├── import_coolname_and_print_slugs.py ├── test_data.py ├── generate.py ├── test_coolname_env.py ├── measure_performance.py ├── test_impl.py ├── test_loader.py └── test_coolname.py ├── docs ├── history.rst ├── thread-safe.rst ├── index.rst ├── environment-variables.rst ├── classes-and-functions.rst ├── randomization.rst ├── Makefile ├── make.bat ├── conf.py └── customization.rst ├── examples ├── russian │ ├── color.txt │ ├── animal.txt │ └── config.json └── russian_module │ └── __init__.py ├── .coveragerc ├── requirements ├── docs.txt └── development.txt ├── .travis.yml ├── run_coveralls.py ├── setup.cfg ├── MANIFEST.in ├── .gitignore ├── tox.ini ├── LICENSE ├── CONTRIBUTING.rst ├── HISTORY.rst ├── setup.py └── README.rst /coolname/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /examples/russian/color.txt: -------------------------------------------------------------------------------- 1 | # UTF-8 2 | 3 | белая 4 | чёрная 5 | -------------------------------------------------------------------------------- /examples/russian/animal.txt: -------------------------------------------------------------------------------- 1 | # UTF-8 2 | 3 | корова 4 | кошка 5 | собака 6 | -------------------------------------------------------------------------------- /coolname/data/adjective_first.txt: -------------------------------------------------------------------------------- 1 | max_length = 13 2 | 3 | first 4 | new 5 | 6 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | include = coolname/* 3 | omit = coolname/data/__init__.py 4 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | Sphinx==5.3.0 2 | sphinxcontrib-fulltoc==1.2.0 3 | sphinxcontrib-websupport==1.2.4 4 | -------------------------------------------------------------------------------- /examples/russian/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": { 3 | "type": "cartesian", 4 | "lists": ["color", "animal"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | - 3.7 5 | - 3.8 6 | - 3.9 7 | - 3.10 8 | - pypy3 9 | install: pip install tox-travis 10 | script: tox 11 | -------------------------------------------------------------------------------- /requirements/development.txt: -------------------------------------------------------------------------------- 1 | -r docs.txt 2 | 3 | bumpversion==0.6.0 4 | wheel==0.38.1 5 | watchdog==2.1.9 6 | tox==3.26.0 7 | coverage==6.5.0 8 | cryptography==39.0.1 9 | PyYAML==6.0 10 | -------------------------------------------------------------------------------- /run_coveralls.py: -------------------------------------------------------------------------------- 1 | #!/bin/env/python 2 | import os 3 | from subprocess import call 4 | 5 | 6 | if __name__ == '__main__': 7 | if 'TRAVIS' in os.environ: 8 | raise SystemExit(call('coveralls')) 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.2.0 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:setup.py] 8 | 9 | [bumpversion:file:coolname/__init__.py] 10 | 11 | [bumpversion:file:docs/conf.py] 12 | 13 | [wheel] 14 | universal = 1 15 | -------------------------------------------------------------------------------- /docs/thread-safe.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | Is it thread-safe? 3 | ================== 4 | 5 | :mod:`coolname` is thread-safe and virtually stateless. 6 | The only shared state is the global :class:`random.Random` instance, which is also thread-safe. 7 | You can re-seed or even completely override it, see :ref:`randomization`. 8 | -------------------------------------------------------------------------------- /coolname/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.2.0' 2 | 3 | # Hint: set COOLNAME_DATA_DIR and/or COOLNAME_DATA_MODULE 4 | # before `import coolname` to change the default generator. 5 | 6 | from .exceptions import InitializationError 7 | from .impl import generate, generate_slug, get_combinations_count,\ 8 | RandomGenerator, replace_random 9 | -------------------------------------------------------------------------------- /coolname/data/size.txt: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Adjectives related to physical size 3 | # =================================== 4 | 5 | max_length = 13 6 | 7 | big 8 | colossal 9 | enormous 10 | gigantic 11 | great 12 | huge 13 | hulking 14 | humongous 15 | large 16 | little 17 | massive 18 | miniature 19 | petite 20 | portable 21 | small 22 | tiny 23 | towering 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | include coolname/data/*.json 7 | include coolname/data/*.txt 8 | 9 | recursive-include tests * 10 | recursive-include examples * 11 | recursive-exclude * __pycache__ 12 | recursive-exclude * *.py[co] 13 | 14 | recursive-include docs *.rst conf.py Makefile make.bat 15 | -------------------------------------------------------------------------------- /examples/russian_module/__init__.py: -------------------------------------------------------------------------------- 1 | # Config dict MUST be named `config` 2 | config = { 3 | 'all': { 4 | 'type': 'cartesian', 5 | 'lists': ['color', 'animal'] 6 | }, 7 | 'color': { 8 | 'type': 'words', 9 | 'words': ['белая', 'чёрная'] 10 | }, 11 | 'animal': { 12 | 'type': 'words', 13 | 'words': ['кошка', 'собака'] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /coolname/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Do not import anything directly from this module. 3 | """ 4 | 5 | 6 | class InitializationError(Exception): 7 | """Custom exception for all generator initialization errors.""" 8 | pass 9 | 10 | 11 | class ConfigurationError(InitializationError): 12 | """Specific exception for invalid configuration.""" 13 | 14 | def __init__(self, msg): 15 | super(ConfigurationError, self).__init__('Invalid config: {}'.format(msg)) 16 | -------------------------------------------------------------------------------- /coolname/data/prefix.txt: -------------------------------------------------------------------------------- 1 | # =============================================================== 2 | # Words that can be used only immediately before the main subject 3 | # =============================================================== 4 | 5 | max_length = 13 6 | 7 | # Metric prefixes & other size-related prefixes 8 | giga 9 | mega 10 | micro 11 | mini 12 | nano 13 | pygmy 14 | 15 | # Superiority prefixes 16 | super 17 | uber 18 | ultra 19 | 20 | # Misc modifiers 21 | cyber 22 | mutant 23 | ninja 24 | space 25 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. coolname documentation master file, created by 2 | sphinx-quickstart on Sat Apr 23 13:03:36 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../README.rst 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | :hidden: 11 | 12 | Introduction 13 | customization 14 | environment-variables 15 | randomization 16 | classes-and-functions 17 | thread-safe 18 | history 19 | -------------------------------------------------------------------------------- /coolname/data/noun_adjective.txt: -------------------------------------------------------------------------------- 1 | # ========================================================== 2 | # Words that can be used anywhere as adjectives or of-nouns. 3 | # 4 | # Example: 5 | # black dog 6 | # big dog 7 | # big dog of 8 | # 9 | # If you want to contribute: 10 | # 1. Make sure word really fits anywhere. 11 | # 2. Keep lists sorted alphabetically. 12 | # ========================================================== 13 | 14 | max_length = 13 15 | 16 | fancy 17 | magic 18 | rainbow 19 | woodoo 20 | -------------------------------------------------------------------------------- /coolname/data/animal_breed.txt: -------------------------------------------------------------------------------- 1 | # ======================= 2 | # Domestic animal breeds. 3 | # ======================= 4 | 5 | max_length = 13 6 | 7 | # Dogs 8 | beagle 9 | bloodhound 10 | bulldog 11 | bullmastiff 12 | chihuahua 13 | chowchow 14 | collie 15 | corgi 16 | dalmatian 17 | dachshund 18 | doberman 19 | foxhound 20 | husky 21 | labradoodle 22 | labrador 23 | mastiff 24 | malamute 25 | mongrel 26 | poodle 27 | pug 28 | rottweiler 29 | saluki 30 | spaniel 31 | terrier 32 | whippet 33 | 34 | # Horses & Hybrids 35 | mule 36 | mustang 37 | pony 38 | -------------------------------------------------------------------------------- /coolname/data/from2.txt: -------------------------------------------------------------------------------- 1 | # ==================================== 2 | # Two-word phrases to use after "from" 3 | # ==================================== 4 | 5 | max_length = 24 6 | number_of_words = 2 7 | 8 | # Small places 9 | fancy cafe 10 | prestigious college 11 | prestigious university 12 | 13 | # Big places 14 | big city 15 | foreign country 16 | small town 17 | wild west 18 | 19 | # Weird places 20 | ancient ruins 21 | another dimension 22 | another planet 23 | flying circus 24 | secret laboratory 25 | the government 26 | the future 27 | the past 28 | the stars 29 | -------------------------------------------------------------------------------- /coolname/data/animal_legendary.txt: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # Legendary creatures. 3 | # We specify only a few most memorable ones, not diving too deep into mythology. 4 | # If you want to contribute: 5 | # 1. Please do not include human-like species (centaur, werewolf, etc.). 6 | # 2. Make sure word gets 1M+ Google hits. 7 | # 3. Keep lists sorted alphabetically. 8 | # ============================================================================== 9 | 10 | max_length = 13 11 | 12 | basilisk 13 | chupacabra 14 | dragon 15 | griffin 16 | pegasus 17 | unicorn -------------------------------------------------------------------------------- /coolname/data/from_noun_no_mod.txt: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------ 2 | # Nouns that can be used after the "from" without an adjective 3 | # ------------------------------------------------------------ 4 | 5 | # Planets & moons 6 | venus 7 | mars 8 | jupiter 9 | ganymede 10 | saturn 11 | uranus 12 | neptune 13 | pluto 14 | 15 | # Stars 16 | betelgeuse 17 | sirius 18 | vega 19 | 20 | # Fictional places 21 | arcadia 22 | asgard 23 | atlantis 24 | avalon 25 | camelot 26 | eldorado 27 | heaven 28 | hell 29 | hyperborea 30 | lemuria 31 | nibiru 32 | shambhala 33 | tartarus 34 | valhalla 35 | wonderland 36 | -------------------------------------------------------------------------------- /coolname/data/of_noun_no_mod.txt: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------- 2 | # "of-nouns" which are used without modifiers. 3 | # 4 | # Example: 5 | # big dog of 6 | # ------------------------------------------------- 7 | 8 | max_length = 13 9 | 10 | # Science 11 | chemistry 12 | education 13 | experiment 14 | mathematics 15 | psychology 16 | reading 17 | 18 | # Art 19 | cubism 20 | painting 21 | 22 | # Misc 23 | advertising 24 | agreement 25 | climate 26 | competition 27 | effort 28 | emphasis 29 | foundation 30 | judgment 31 | memory 32 | opportunity 33 | perspective 34 | priority 35 | promise 36 | teaching 37 | -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | import os.path as op 2 | import unittest 3 | from unittest import mock 4 | from unittest.mock import patch 5 | 6 | TESTS_DIR = op.dirname(op.abspath(__file__)) 7 | PROJECT_DIR = op.abspath(op.join(TESTS_DIR, '..')) 8 | EXAMPLES_DIR = op.join(PROJECT_DIR, 'examples') 9 | 10 | 11 | class TestCase(unittest.TestCase): 12 | pass 13 | 14 | 15 | class FakeRandom(object): 16 | """Generates 0, 1, 2...""" 17 | 18 | def __init__(self, i=0): 19 | self.i = i 20 | 21 | def randrange(self, stop): 22 | result = (self.i + 1) % stop 23 | self.i += 1 24 | return result 25 | 26 | def seed(self, a): 27 | assert isinstance(a, int) 28 | self.i = a 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | .eggs/ 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | .cache 29 | nosetests.xml 30 | htmlcov 31 | 32 | # Translations 33 | *.mo 34 | 35 | # PyCharm 36 | .idea/ 37 | 38 | # Mr Developer 39 | .mr.developer.cfg 40 | .project 41 | .pydevproject 42 | 43 | # Complexity 44 | output/*.html 45 | output/*/index.html 46 | 47 | # pyenv 48 | .python-version 49 | 50 | # Sphinx 51 | docs/_build 52 | 53 | # Dynamically generated code 54 | coolname/data/__init__.py 55 | -------------------------------------------------------------------------------- /coolname/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | from coolname import generate 5 | 6 | 7 | def parse_args(argv): 8 | parser = argparse.ArgumentParser(description='Generate slug to stdout') 9 | parser.add_argument('pattern', nargs='?', type=int, choices=[2, 3, 4], default=None) 10 | parser.add_argument('-s', '--separator', default='-') 11 | parser.add_argument('-n', '--number', type=int, default=1, help='how many slugs to generate (default: 1)') 12 | return parser.parse_args(argv) 13 | 14 | 15 | def main(): 16 | args = parse_args(sys.argv[1:]) 17 | for _ in range(args.number): 18 | print(args.separator.join(generate(args.pattern))) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() # pragma: nocover 23 | -------------------------------------------------------------------------------- /coolname/config.py: -------------------------------------------------------------------------------- 1 | class _CONF: 2 | """All strings related to config, to avoid hardcoding.""" 3 | 4 | class TYPE: 5 | """Node type in configuration.""" 6 | NESTED = 'nested' 7 | CARTESIAN = 'cartesian' 8 | WORDS = 'words' 9 | PHRASES = 'phrases' 10 | CONST = 'const' 11 | 12 | class FIELD: 13 | """Allowed fields.""" 14 | TYPE = 'type' 15 | LISTS = 'lists' 16 | WORDS = 'words' 17 | PHRASES = 'phrases' 18 | NUMBER_OF_WORDS = 'number_of_words' 19 | VALUE = 'value' 20 | GENERATOR = 'generator' 21 | MAX_LENGTH = 'max_length' 22 | MAX_SLUG_LENGTH = 'max_slug_length' 23 | ENSURE_UNIQUE = 'ensure_unique' 24 | ENSURE_UNIQUE_PREFIX = 'ensure_unique_prefix' 25 | -------------------------------------------------------------------------------- /tests/import_coolname_and_print_slugs.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import itertools 3 | import sys 4 | 5 | 6 | class NotRandom(object): 7 | 8 | def __init__(self): 9 | self.gen = itertools.count() 10 | 11 | def randrange(self, r): 12 | return next(self.gen) % r 13 | 14 | 15 | def _parse_args(argv): 16 | parser = argparse.ArgumentParser(description='Auxiliary script to run coolname in a separate process.') 17 | parser.add_argument('number_of_slugs', type=int, help='Number of slugs to generate') 18 | return parser.parse_args(argv) 19 | 20 | 21 | def main(argv): 22 | args = _parse_args(argv) 23 | import coolname 24 | coolname.replace_random(NotRandom()) 25 | for i in range(args.number_of_slugs): 26 | print(coolname.generate_slug()) 27 | 28 | 29 | if __name__ == '__main__': 30 | main(sys.argv[1:]) 31 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py37, py38, py39, py310, pypy3, flake8, docs 3 | 4 | [testenv] 5 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 6 | setenv = 7 | PYTHONPATH = {toxinidir}:{toxinidir}/coolname 8 | commands = 9 | python setup.py test 10 | 11 | [testenv:py310] 12 | deps = 13 | coveralls 14 | commands = 15 | coverage run --source=coolname setup.py test 16 | python run_coveralls.py 17 | 18 | [testenv:flake8] 19 | deps = 20 | flake8 21 | commands = flake8 coolname 22 | 23 | [flake8] 24 | max-line-length = 119 25 | exclude = coolname/__init__.py,coolname/data/__init__.py,tests/* 26 | 27 | [testenv:docs] 28 | deps = 29 | -r{toxinidir}/requirements/docs.txt 30 | commands = 31 | python setup.py build_sphinx {posargs} 32 | 33 | [travis] 34 | python = 35 | 3.6: py36 36 | 3.7: py37 37 | 3.8: py38 38 | 3.9: py310 39 | 3.10: py310, flake8, docs 40 | pypy3: pypy3 41 | -------------------------------------------------------------------------------- /tests/test_data.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from coolname import RandomGenerator 4 | from coolname.data import config 5 | 6 | class DataTest(TestCase): 7 | 8 | def test_initialization(self): 9 | generator = RandomGenerator(config) 10 | assert generator.generate_slug() 11 | 12 | def test_all_unicode(self): 13 | for name, rule in config.items(): 14 | if rule['type'] == 'words': 15 | assert all(isinstance(x, str) for x in rule['words']) 16 | elif rule['type'] == 'phrases': 17 | assert all(all(isinstance(y, str) for y in x) for x in rule['phrases']) 18 | elif rule['type'] == 'const': 19 | assert isinstance(rule['value'], str) 20 | elif rule['type'] in ('nested', 'cartesian'): 21 | pass 22 | else: 23 | raise AssertionError('Rule {!r} has unexpected type {!r}'.format(name, rule['type'])) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Alexander Lukanin 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY 16 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 19 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | How to contribute 2 | ================= 3 | 4 | Add more adjectives and nouns 5 | ----------------------------- 6 | 7 | The more, the better - as long as names are cool! 8 | 9 | Feel free to open a pull request for ``coolname/data`` files. 10 | Or just send me an email with your suggestions. 11 | 12 | If you find some words too sophisticated / not so cool / just plain wrong, 13 | please let me know. English is my not native language, and I may have some 14 | wrong ideas about words meaning and usage. 15 | 16 | Report bugs 17 | ----------- 18 | 19 | Report bugs at https://github.com/alexanderlukanin13/coolname/issues. 20 | 21 | If you are reporting a bug, please include: 22 | 23 | * Your operating system name and version. 24 | * Python version. 25 | * Any details about your local setup that might be helpful in troubleshooting. 26 | * Detailed steps to reproduce the bug. 27 | 28 | Add new features 29 | ---------------- 30 | 31 | If you want to extend API, please create a feature request at 32 | https://github.com/alexanderlukanin13/coolname/issues 33 | 34 | Pull request guidelines 35 | ----------------------- 36 | 37 | Before you submit a pull request, check that it meets these guidelines: 38 | 39 | 1. The pull request should include tests. 40 | 2. New functions/methods should be reasonably documented. 41 | 3. The pull request should pass all tests at https://travis-ci.org/alexanderlukanin13/coolname/pull_requests. 42 | -------------------------------------------------------------------------------- /docs/environment-variables.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Environment variables 3 | ===================== 4 | 5 | .. py:currentmodule:: coolname 6 | 7 | You can replace the default generator using one or both following variables: 8 | 9 | .. code-block:: bash 10 | 11 | export COOLNAME_DATA_DIR=some/path 12 | export COOLNAME_DATA_MODULE=some.module 13 | 14 | If *any* of these is set and not empty, default generator is not created (saving memory), 15 | and your custom generator is used instead. 16 | 17 | ``COOLNAME_DATA_DIR`` 18 | ===================== 19 | 20 | It must be a valid path (absolute or relative) to the directory with ``config.json`` and ``*.txt`` files. 21 | 22 | ``COOLNAME_DATA_MODULE`` 23 | ======================== 24 | 25 | It must be a valid module name, importable from the current Python environment. 26 | 27 | It must contain a variable named ``config``, which is a dictionary (see :ref:`configuration-rules`). 28 | 29 | Adjust :py:data:`sys.path` (or ``PYTHONPATH``) if your module fails to import. 30 | 31 | Precedence 32 | ========== 33 | 34 | 1. If ``COOLNAME_DATA_DIR`` is defined and not empty, *and the directory exists*, it is used. 35 | 36 | 2. If ``COOLNAME_DATA_MODULE`` is defined and not empty, it is used. 37 | 38 | 3. Otherwise, :py:class:`ImportError` is raised. 39 | 40 | The reason for this order is to support packaging in egg files. 41 | If you don't care about eggs, use only ``COOLNAME_DATA_DIR`` because it's more efficient and easier to maintain. 42 | -------------------------------------------------------------------------------- /tests/generate.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | import time 5 | 6 | PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 7 | if PROJECT_PATH not in sys.path: 8 | sys.path.append(PROJECT_PATH) 9 | 10 | from coolname import generate_slug 11 | 12 | 13 | def main(argv): 14 | if sys.version_info[:2] < (3, 6): 15 | sys.stderr.write('This script requires Python 3.6+\n') 16 | return 1 17 | parser = argparse.ArgumentParser(description='Generate slug to stdout') 18 | parser.add_argument('length', default=None, nargs='?', type=int, help='Number of words') 19 | parser.add_argument('-w', '--word', help='With particular substring') 20 | parser.add_argument('-a', '--attempts', type=int, default=100000, help='Number of attempts before giving up') 21 | parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output (with timing)') 22 | args = parser.parse_args(argv) 23 | generate_slug(args.length) # for more precise timing 24 | if args.word: 25 | words = args.word.split(',') 26 | slug = None 27 | for i in range(0, args.attempts): 28 | start_time = time.perf_counter() 29 | s = generate_slug(args.length) 30 | elapsed_time = time.perf_counter() - start_time 31 | if any(x in s for x in words): 32 | slug = s 33 | break 34 | if slug is None: 35 | print('Failed to generate in {} attempts'.format(args.attempts)) 36 | return 1 37 | else: 38 | start_time = time.perf_counter() 39 | slug = generate_slug(args.length) 40 | elapsed_time = time.perf_counter() - start_time 41 | print(slug) 42 | if args.verbose: 43 | sys.stderr.write('Generated in {:0.06f} seconds\n'.format(elapsed_time)) 44 | return 0 45 | 46 | 47 | if __name__ == '__main__': 48 | sys.exit(main(sys.argv[1:])) 49 | -------------------------------------------------------------------------------- /docs/classes-and-functions.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Classes and functions 3 | ===================== 4 | 5 | .. py:module:: coolname 6 | 7 | Default generator 8 | ================= 9 | 10 | .. py:function:: generate(pattern=None) 11 | 12 | Returns a random sequence as a list of strings. 13 | 14 | :param int pattern: Can be 2, 3 or 4. 15 | :rtype: list of strings 16 | 17 | .. py:function:: generate_slug(pattern=None) 18 | 19 | Same as :func:`generate`, but returns a slug as a string. 20 | 21 | :param int pattern: Can be 2, 3 or 4. 22 | :rtype: str 23 | 24 | .. py:function:: get_combinations_count(pattern=None) 25 | 26 | Returns the number of possible combinations. 27 | 28 | :param int pattern: Can be 2, 3 or 4. 29 | :rtype: int 30 | 31 | .. py:function:: replace_random(random) 32 | 33 | Replaces the random number generator. It doesn't affect custom generators. 34 | 35 | :param random: :class:`random.Random` instance. 36 | 37 | Custom generators 38 | ================= 39 | 40 | .. py:class:: RandomGenerator(config, random=None) 41 | 42 | :param dict config: Custom configuration dictionary. 43 | :param random: :class:`random.Random` instance. If not provided, :func:`random.randrange` will be used. 44 | 45 | .. py:method:: generate(pattern=None) 46 | 47 | Returns a random sequence as a list of strings. 48 | 49 | :param pattern: Not applicable by default. Can be configured. 50 | :rtype: list of strings 51 | 52 | .. py:method:: generate_slug(pattern=None) 53 | 54 | Same as :meth:`generate`, but returns a slug as a string. 55 | 56 | :param pattern: Not applicable by default. Can be configured. 57 | :rtype: str 58 | 59 | .. py:method:: get_combinations_count(pattern=None) 60 | 61 | Returns the number of possible combinations. 62 | 63 | :param pattern: Not applicable by default. Can be configured. 64 | :rtype: int 65 | -------------------------------------------------------------------------------- /coolname/data/adjective_near.txt: -------------------------------------------------------------------------------- 1 | # ====================================================================== 2 | # Adjective that must be the nearest to the noun (color, material, etc.) 3 | # ====================================================================== 4 | 5 | max_length = 13 6 | 7 | almond 8 | amaranth 9 | amigurumi 10 | apricot 11 | artichoke 12 | auburn 13 | azure 14 | banana 15 | beige 16 | black 17 | blond 18 | blue 19 | brown 20 | burgundy 21 | carmine 22 | carrot 23 | celadon 24 | cerise 25 | cerulean 26 | charcoal 27 | cherry 28 | chestnut 29 | chocolate 30 | cinnamon 31 | copper 32 | cream 33 | crimson 34 | cyan 35 | daffodil 36 | dandelion 37 | denim 38 | ebony 39 | eggplant 40 | gray 41 | ginger 42 | green 43 | indigo 44 | infrared 45 | jasmine 46 | khaki 47 | lavender 48 | lilac 49 | mauve 50 | magenta 51 | mahogany 52 | maize 53 | marigold 54 | mustard 55 | ochre 56 | orange 57 | origami 58 | papaya 59 | paper 60 | peach 61 | persimmon 62 | pink 63 | pistachio 64 | pumpkin 65 | purple 66 | raspberry 67 | red 68 | rose 69 | russet 70 | saffron 71 | sage 72 | scarlet 73 | sepia 74 | silver 75 | tan 76 | tangerine 77 | taupe 78 | teal 79 | tomato 80 | turquoise 81 | tuscan 82 | ultramarine 83 | ultraviolet 84 | umber 85 | vanilla 86 | vermilion 87 | violet 88 | viridian 89 | white 90 | wine 91 | wisteria 92 | yellow 93 | 94 | # Stone, metal, etc. 95 | agate 96 | amber 97 | amethyst 98 | aquamarine 99 | asparagus 100 | beryl 101 | brass 102 | bronze 103 | clay 104 | cobalt 105 | coral 106 | cornflower 107 | diamond 108 | emerald 109 | garnet 110 | golden 111 | granite 112 | ivory 113 | jade 114 | jasper 115 | lemon 116 | lime 117 | malachite 118 | maroon 119 | myrtle 120 | nickel 121 | olive 122 | olivine 123 | onyx 124 | opal 125 | orchid 126 | pearl 127 | peridot 128 | platinum 129 | porcelain 130 | quartz 131 | ruby 132 | sandy 133 | sapphire 134 | steel 135 | thistle 136 | topaz 137 | tourmaline 138 | tungsten 139 | xanthic 140 | zircon 141 | -------------------------------------------------------------------------------- /coolname/data/of_modifier.txt: -------------------------------------------------------------------------------- 1 | # ============================================== 2 | # Adjective for "of-noun" 3 | # 4 | # Example: 5 | # black dog of achievement 6 | # 7 | # If you want to contribute, check that the word 8 | # makes sense in following phrases: 9 | # black dog of faith 10 | # black dog of perfection 11 | # black dog of storm 12 | # black dog of coffee 13 | # ============================================== 14 | 15 | max_length = 13 16 | 17 | absolute 18 | abstract 19 | algebraic 20 | amazing 21 | amusing 22 | ancient 23 | angelic 24 | astonishing 25 | authentic 26 | awesome 27 | beautiful 28 | classic 29 | delightful 30 | demonic 31 | eminent 32 | enjoyable 33 | eternal 34 | excellent 35 | exotic 36 | extreme 37 | fabulous 38 | famous 39 | fantastic 40 | fascinating 41 | flawless 42 | fortunate 43 | glorious 44 | great 45 | heavenly 46 | holistic 47 | hypothetical 48 | ideal 49 | illegal 50 | imaginary 51 | immense 52 | imminent 53 | immortal 54 | impossible 55 | impressive 56 | improbable 57 | incredible 58 | inescapable 59 | inevitable 60 | infinite 61 | inspiring 62 | interesting 63 | legal 64 | magic 65 | majestic 66 | major 67 | marvelous 68 | massive 69 | mysterious 70 | nonconcrete 71 | nonstop 72 | luxurious 73 | optimal 74 | original 75 | pastoral 76 | perfect 77 | perpetual 78 | phenomenal 79 | pleasurable 80 | pragmatic 81 | premium 82 | radical 83 | rampant 84 | regular 85 | remarkable 86 | satisfying 87 | serious 88 | scientific 89 | sexy 90 | sheer 91 | simple 92 | silent 93 | spectacular 94 | splendid 95 | stereotyped 96 | stimulating 97 | strange 98 | striking 99 | strongest 100 | sublime 101 | subtle 102 | sudden 103 | terrific 104 | therapeutic 105 | total 106 | ultimate 107 | uncanny 108 | undeniable 109 | unearthly 110 | unexpected 111 | unknown 112 | unmatched 113 | unnatural 114 | unreal 115 | unusual 116 | utter 117 | weird 118 | wonderful 119 | wondrous 120 | 121 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | Release history 4 | =============== 5 | 6 | 2.2.0 (2023-01-09) 7 | ------------------ 8 | 9 | * More dogs, cats and cows! 10 | 11 | 2.1.0 (2022-12-07) 12 | ------------------ 13 | 14 | * Support OpenSSL FIPS by using ``hashlib.md5(..., usedforsecurity=False)`` 15 | 16 | 2.0.0 (2022-10-24) 17 | ------------------ 18 | 19 | * Support for old Python versions (<3.5) is dropped, because it's 2022 20 | 21 | * Command line usage and pipx support. 22 | 23 | * With additional owls and bitterns 24 | 25 | 1.1.0 (2018-08-02) 26 | ------------------ 27 | 28 | * 32-bit Python is supported. 29 | 30 | 1.0.4 (2018-02-17) 31 | ------------------ 32 | 33 | * **Breaking changes:** 34 | 35 | - Renamed :class:`RandomNameGenerator` to :class:`RandomGenerator`. 36 | 37 | - :func:`randomize` was removed, because it was just an alias to :func:`random.seed`. 38 | 39 | * `Phrase lists `_ 40 | give you even more freedom when creating custom generators. 41 | 42 | * You can seed or even replace the underlying :class:`random.Random` instance, see 43 | `Randomization `_. 44 | 45 | * Change the default generator using ``COOLNAME_DATA_DIR`` and ``COOLNAME_DATA_MODULE``. This also saves memory! 46 | 47 | * Total number of combinations = 60 billions. 48 | 49 | 0.2.0 (2016-09-28) 50 | ------------------ 51 | 52 | * More flexible configuration: ``max_length`` and ``max_slug_length`` constraints. 53 | See `documentation `_. 54 | 55 | * Total number of combinations increased from 43 to 51 billions. 56 | 57 | 0.1.1 (2015-12-17) 58 | ------------------ 59 | 60 | * Consistent behavior in Python 2/3: output is always unicode. 61 | 62 | * Provide ``from coolname.loader import load_config`` as a public API for loading custom configuration. 63 | 64 | * More strict configuration validation. 65 | 66 | * Total number of combinations increased from 33 to 43 billions. 67 | 68 | 0.1.0 (2015-11-03) 69 | ------------------ 70 | 71 | * First release on PyPI. 72 | -------------------------------------------------------------------------------- /tests/test_coolname_env.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains tests for COOLNAME_DATA_DIR and COOLNAME_DATA_MODULE. 3 | """ 4 | import os 5 | import os.path as op 6 | import subprocess 7 | import sys 8 | 9 | from .common import PROJECT_DIR, EXAMPLES_DIR 10 | 11 | 12 | RUSSIAN = [ 13 | 'белая-корова', 14 | 'белая-кошка', 15 | 'белая-собака', 16 | 'чёрная-корова', 17 | 'чёрная-кошка', 18 | 'чёрная-собака' 19 | ] 20 | # In module, there are only 4 combinations 21 | RUSSIAN_M = [ 22 | 'белая-кошка', 23 | 'белая-собака', 24 | 'чёрная-кошка', 25 | 'чёрная-собака', 26 | 'белая-кошка', 27 | 'белая-собака' 28 | ] 29 | 30 | 31 | def generate_slugs(number_of_slugs, data_dir=None, data_module=None, path=None, expect_returncode=0): 32 | env = dict(os.environ) 33 | env['PYTHONPATH'] = PROJECT_DIR 34 | if path: 35 | env['PYTHONPATH'] += os.pathsep + path 36 | if data_dir: 37 | env['COOLNAME_DATA_DIR'] = data_dir 38 | if data_module: 39 | env['COOLNAME_DATA_MODULE'] = data_module 40 | process = subprocess.Popen([sys.executable, 'tests/import_coolname_and_print_slugs.py', str(number_of_slugs)], 41 | stdout=subprocess.PIPE, 42 | stderr=subprocess.PIPE, 43 | cwd=PROJECT_DIR, env=env) 44 | process.wait() 45 | assert process.returncode == expect_returncode 46 | output = process.stdout.read().decode('utf8') 47 | if not output: 48 | output = process.stderr.read().decode('utf8') 49 | return [x.strip() for x in output.split('\n') if x.strip()] 50 | 51 | 52 | def test_coolname_env(): 53 | # Only COOLNAME_DATA_DIR 54 | assert generate_slugs(len(RUSSIAN), data_dir=op.join(EXAMPLES_DIR, 'russian')) == RUSSIAN 55 | # Only COOLNAME_DATA_MODULE 56 | assert generate_slugs(len(RUSSIAN_M), data_module='russian_module', path=EXAMPLES_DIR) == RUSSIAN_M 57 | # Both: data dir has more priority than the module 58 | assert generate_slugs(len(RUSSIAN), 59 | data_dir=op.join(EXAMPLES_DIR, 'russian'), 60 | data_module='russian_module', 61 | path=EXAMPLES_DIR) == RUSSIAN 62 | # Both, but data dir does not exist - fall back to module 63 | assert generate_slugs(len(RUSSIAN_M), 64 | data_dir=op.join(EXAMPLES_DIR, 'no_such'), 65 | data_module='russian_module', 66 | path=EXAMPLES_DIR) == RUSSIAN_M 67 | # Only COOLNAME_DATA_DIR, and it is invalid 68 | lines = generate_slugs(1, data_dir='no_such', expect_returncode=1) 69 | assert lines[-1] == 'ImportError: Configure valid COOLNAME_DATA_DIR and/or COOLNAME_DATA_MODULE' 70 | -------------------------------------------------------------------------------- /coolname/data/subj2.txt: -------------------------------------------------------------------------------- 1 | # ============================================================================= 2 | # Two-word animals 3 | # NOTE: don't include phrases that can be generated via regular adjective-noun, 4 | # such as "golden eagle". 5 | # ============================================================================= 6 | 7 | number_of_words = 2 8 | max_length = 22 9 | 10 | # Birds 11 | atlantic puffin 12 | bank swallow 13 | barn owl 14 | barn swallow 15 | barred owl 16 | chimney swift 17 | cliff swallow 18 | eagle owl 19 | emperor goose 20 | fishing owl 21 | harlequin duck 22 | himalayan snowcock 23 | hyacinth macaw 24 | mangrove cuckoo 25 | mute swan 26 | northern cardinal 27 | peregrine falcon 28 | prairie falcon 29 | red cardinal 30 | snow goose 31 | snowy owl 32 | stygian owl 33 | tawny owl 34 | trumpeter swan 35 | tufted puffin 36 | whooper swan 37 | whooping crane 38 | 39 | # Insects 40 | fire ant 41 | 42 | # Dog breeds 43 | akita inu 44 | belgian shepherd 45 | boxer dog 46 | cane corso 47 | great dane 48 | hulky dog 49 | newfoundland dog 50 | pomeranian dog 51 | samoyed dog 52 | shepherd dog 53 | shiba inu 54 | 55 | # Cat breeds 56 | abyssinian cat 57 | birman cat 58 | chartreux cat 59 | korat cat 60 | manx cat 61 | munchkin cat 62 | persian cat 63 | pixiebob cat 64 | ragdoll cat 65 | ragamuffin cat 66 | savannah cat 67 | sphynx cat 68 | 69 | # Domestic cattle (to keep it simple, we just copypaste bull+cow+calf here) 70 | braford cow 71 | braford bull 72 | braford calf 73 | brahman cow 74 | brahman bull 75 | brahman calf 76 | gyr cow 77 | gyr bull 78 | gyr calf 79 | highland cow 80 | highland bull 81 | highland calf 82 | jersey cow 83 | jersey bull 84 | jersey calf 85 | kangayam cow 86 | kangayam bull 87 | kangayam calf 88 | longhorn cow 89 | longhorn bull 90 | longhorn calf 91 | 92 | # Rabbit breeds 93 | angora rabbit 94 | chinchilla rabbit 95 | harlequin rabbit 96 | 97 | # Mammals (wild) 98 | alpine chipmunk 99 | beaked whale 100 | bottlenose dolphin 101 | clouded leopard 102 | eared seal 103 | elephant seal 104 | feral cat 105 | feral dog 106 | feral donkey 107 | feral goat 108 | feral horse 109 | feral pig 110 | fur seal 111 | grizzly bear 112 | harbor porpoise 113 | honey badger 114 | humpback whale 115 | killer whale 116 | mountain deer 117 | mountain goat 118 | mountain lion 119 | olympic marmot 120 | pampas deer 121 | pine marten 122 | polynesian rat 123 | rhesus macaque 124 | river dolphin 125 | sea lion 126 | sea otter 127 | snow leopard 128 | sperm whale 129 | spinner dolphin 130 | vampire bat 131 | 132 | # Reptiles 133 | gila monster 134 | freshwater crocodile 135 | saltwater crocodile 136 | snapping turtle 137 | 138 | # Fictional 139 | walking mushroom 140 | -------------------------------------------------------------------------------- /tests/measure_performance.py: -------------------------------------------------------------------------------- 1 | # Pre-import a lot of standard library stuff 2 | # to get accurate measurement of coolname memory consumption. 3 | import argparse 4 | import hashlib 5 | import itertools 6 | import json 7 | import os 8 | import random 9 | import re 10 | import sys 11 | import time 12 | from timeit import timeit 13 | 14 | 15 | if __name__ == '__main__': 16 | argument_parser = argparse.ArgumentParser( 17 | description='Measure performance of coolname functions') 18 | argument_parser.add_argument('--dump', action='store_true', help='Dump whole tree') 19 | argument_parser.add_argument('--all', 20 | action='store_true', 21 | help='Measure repeat probability') 22 | arguments = argument_parser.parse_args(sys.argv[1:]) 23 | 24 | # Make sure coolname is importable 25 | project_path = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 26 | if project_path not in sys.path: 27 | sys.path.append(project_path) 28 | 29 | # Use psutil to measure memory. 30 | try: 31 | import psutil 32 | except ImportError: 33 | print('Please install psutil') 34 | sys.exit(1) 35 | process = psutil.Process() 36 | rss_base = process.memory_info().rss 37 | vm_base = process.memory_info().vms 38 | 39 | # Import coolname and measure memory growth. 40 | # Give OS some time to correctly register amount of memory allocated. 41 | start_time = time.time() 42 | from coolname import generate, generate_slug, get_combinations_count 43 | print('Loading time: {:.3f}'.format(time.time() - start_time)) 44 | time.sleep(3) 45 | print('RSS growth: {} K'.format((process.memory_info().rss - rss_base) // 1024)) 46 | print('VM growth: {} K'.format((process.memory_info().vms - vm_base) // 1024)) 47 | 48 | # Measure average call time 49 | number = 100000 50 | print('generate() time: {:.6f}'.format(timeit(generate, number=number) / number)) 51 | print('generate_slug() time: {:.6f}'.format(timeit(generate_slug, number=number) / number)) 52 | 53 | # Total combinations count 54 | print('Total combinations: {:,}'.format(get_combinations_count())) 55 | print('Combinations(4): {:,}'.format(get_combinations_count(4))) 56 | print('Combinations(3): {:,}'.format(get_combinations_count(3))) 57 | print('Combinations(2): {:,}'.format(get_combinations_count(2))) 58 | 59 | # Check probability of repeat if we have used 0.1% of total namespace. 60 | # It should be around 0.0001. 61 | if arguments.all: 62 | combinations = get_combinations_count() 63 | items = set({}) 64 | items_count = combinations // 10000 65 | while len(items) < items_count: 66 | items.add(generate_slug()) 67 | repeats = 0 68 | loops = 100000 69 | for i in range(loops): 70 | if generate_slug() in items: 71 | repeats += 1 72 | print('Repeat probability: {:.6f} (with {} names used)'.format(repeats / loops, len(items))) 73 | 74 | # Dump tree 75 | if arguments.dump: 76 | print() 77 | import coolname.impl 78 | print(coolname.impl._default._dump(sys.stdout, object_ids=True)) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re 3 | 4 | try: 5 | from setuptools import setup 6 | from setuptools.command.sdist import sdist 7 | except ImportError: 8 | from distutils.core import setup 9 | from distutils.command.sdist import sdist 10 | 11 | 12 | # All this magic is needed to support packaging in zip/egg. 13 | # Reading files inside zip is problematic, so we compile 14 | # everything into config dict and stuff it into 15 | # coolname/data/__init__.py. 16 | def compile_init_py(): 17 | import codecs 18 | import os 19 | import sys 20 | current_path = os.path.dirname(__file__) 21 | current_path_appended = False 22 | if current_path not in sys.path: 23 | sys.path.append(current_path) 24 | current_path_appended = True 25 | from coolname.loader import load_config 26 | if current_path_appended: 27 | sys.path.remove(current_path) 28 | config_path = os.path.join(current_path, 'coolname', 'data') 29 | config = load_config(config_path) 30 | # Write to data/__init__.py to be used from .egg 31 | with codecs.open(os.path.join(config_path, '__init__.py'), 'w', encoding='utf-8') as file: 32 | file.write(f'''# THIS FILE IS AUTO-GENERATED, DO NOT EDIT 33 | config = {config!r} 34 | ''') 35 | 36 | 37 | def customize(cls): 38 | _old_run = cls.run 39 | def run(self, *args, **kwargs): 40 | compile_init_py() 41 | return _old_run(self, *args, **kwargs) 42 | cls.run = run 43 | return cls 44 | 45 | 46 | with open('README.rst') as readme_file: 47 | readme = readme_file.read() 48 | 49 | with open('HISTORY.rst') as history_file: 50 | history = history_file.read().replace('.. :changelog:', '') 51 | history = history[:history.find('0.2.0')] + ''' 52 | For earlier releases, see `History `_ 53 | ''' 54 | history = re.sub(r':\w+:`(\w+(?:\.\w+)*)`', r'``\1``', history) 55 | 56 | 57 | test_requirements = [] 58 | 59 | setup( 60 | name='coolname', 61 | version='2.2.0', 62 | description="Random name and slug generator", 63 | long_description=readme + '\n\n' + history, 64 | author="Alexander Lukanin", 65 | author_email='alexander.lukanin.13@gmail.com', 66 | url='https://github.com/alexanderlukanin13/coolname', 67 | packages=[ 68 | 'coolname', 69 | 'coolname.data', 70 | ], 71 | package_dir={ 72 | 'coolname': 'coolname' 73 | }, 74 | cmdclass={'sdist': customize(sdist)}, 75 | include_package_data=True, 76 | entry_points={'console_scripts': ['coolname = coolname.__main__:main']}, 77 | license="BSD", 78 | zip_safe=True, 79 | keywords='coolname', 80 | classifiers=[ 81 | 'Development Status :: 5 - Production/Stable', 82 | 'Intended Audience :: Developers', 83 | 'License :: OSI Approved :: BSD License', 84 | 'Natural Language :: English', 85 | 'Programming Language :: Python :: 3', 86 | 'Programming Language :: Python :: 3.6', 87 | 'Programming Language :: Python :: 3.7', 88 | 'Programming Language :: Python :: 3.8', 89 | 'Programming Language :: Python :: 3.9', 90 | 'Programming Language :: Python :: 3.10', 91 | ], 92 | test_suite='tests', 93 | tests_require=test_requirements 94 | ) 95 | -------------------------------------------------------------------------------- /docs/randomization.rst: -------------------------------------------------------------------------------- 1 | .. _randomization: 2 | 3 | .. py:currentmodule:: coolname 4 | 5 | ============= 6 | Randomization 7 | ============= 8 | 9 | Re-seeding 10 | ---------- 11 | 12 | As a source of randomness :mod:`coolname` uses standard :py:mod:`random` module, 13 | specifically :py:func:`random.randrange` function. 14 | 15 | To re-seed the default generator, simply call :py:func:`random.seed`: 16 | 17 | .. code-block:: python 18 | 19 | import os, random 20 | random.seed(os.urandom(128)) 21 | 22 | :mod:`coolname` itself never calls :py:func:`random.seed`. 23 | 24 | Replacing the random number generator 25 | ------------------------------------- 26 | 27 | By default, all instances of :class:`RandomGenerator` share the same random number generator. 28 | 29 | To replace it for a custom generator: 30 | 31 | .. code-block:: python 32 | 33 | from coolname import RandomGenerator 34 | import random, os 35 | seed = os.urandom(128) 36 | generator = RandomGenerator(config, random=random.Random(seed)) 37 | 38 | To replace it for :func:`coolname.generate` and :func:`coolname.generate_slug`: 39 | 40 | .. code-block:: python 41 | 42 | import coolname 43 | import random, os 44 | seed = os.urandom(128) 45 | coolname.replace_random(random.Random(seed)) 46 | 47 | How randomization works 48 | ----------------------- 49 | 50 | In this section we dive into details of how :mod:`coolname` generates random sequences. 51 | 52 | Let's say we have following config: 53 | 54 | .. code-block:: python 55 | 56 | config = { 57 | 'all': { 58 | 'type': 'cartesian', 59 | 'lists': ['price', 'color', 'object'] 60 | }, 61 | # 2 items 62 | 'price': { 63 | 'type': 'words', 64 | 'words': ['cheap', 'expensive'] 65 | }, 66 | # 3 items 67 | 'color': { 68 | 'type': 'words', 69 | 'words': ['black', 'white', 'red'] 70 | }, 71 | # 5 + 6 = 11 items 72 | 'object': { 73 | 'type': 'nested', 74 | 'lists': ['footwear', 'hat'] 75 | }, 76 | # 5 items 77 | 'footwear': { 78 | 'type': 'words', 79 | 'words': ['shoes', 'boots', 'sandals', 'sneakers', 'socks'] 80 | }, 81 | # 6 items 82 | 'hat': { 83 | 'type': 'phrases', 84 | 'phrases': ['top hat', 'fedora', 'beret', 'cricket cap', 'panama', 'sombrero'] 85 | } 86 | } 87 | import coolname 88 | generator = coolname.RandomGenerator(config) 89 | 90 | The overall number of combinations is 2 × 3 × (5 + 6) = 66. 91 | 92 | You can imagine a space of possible combinations as a virtual N-dimensional array. 93 | In this example, it's 3-dimensional, with sides equal to 2, 3 and 11. 94 | 95 | When user calls :meth:`RandomGenerator.generate_slug`, 96 | a random integer is generated via ``randrange(66)``. 97 | Then, the integer is used to pick an element from 3-dimensional array. 98 | 99 | .. table:: Possible combinations 100 | :widths: auto 101 | 102 | ============================= ========================================= 103 | :func:`randrange` returns :func:`generate_slug` returns 104 | ============================= ========================================= 105 | 0 cheap-black-top-hat 106 | 1 cheap-black-fedora 107 | 2 cheap-black-beret 108 | 3 cheap-black-cricket-cap 109 | 4 cheap-black-panama 110 | 5 cheap-black-sombrero 111 | 6 cheap-black-shoes 112 | 7 cheap-black-boots 113 | 8 cheap-black-sandals 114 | 9 cheap-black-sneakers 115 | 10 cheap-black-socks 116 | 11 cheap-white-top-hat 117 | 12 cheap-white-fedora 118 | ... ... 119 | 63 expensive-red-sandals 120 | 64 expensive-red-sneakers 121 | 65 expensive-red-socks 122 | ============================= ========================================= 123 | 124 | .. note:: 125 | Actual order of combinations is an implementation detail, you should not rely on it. 126 | -------------------------------------------------------------------------------- /coolname/data/of_noun.txt: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------- 2 | # Nouns that can be used in the end after the "of". 3 | # 4 | # Example: 5 | # big dog of 6 | # 7 | # If you want to contribute: 8 | # 1. Insert word into the most appropriate list. 9 | # When in doubt, search for synonyms, maybe they are already here. 10 | # If it still doesn't make sense, feel free to contact the package owner. 11 | # 2. Keep lists sorted alphabetically. 12 | # ------------------------------------------------- 13 | 14 | max_length = 13 15 | 16 | # Emotions & Happiness 17 | anger 18 | bliss 19 | contentment 20 | courage 21 | ecstasy 22 | excitement 23 | faith 24 | felicity 25 | fury 26 | gaiety 27 | glee 28 | glory 29 | greatness 30 | inspiration 31 | jest 32 | joy 33 | happiness 34 | holiness 35 | love 36 | merriment 37 | passion 38 | patience 39 | peace 40 | persistence 41 | pleasure 42 | pride 43 | recreation 44 | relaxation 45 | romance 46 | serenity 47 | tranquility 48 | 49 | # Abstract Concepts 50 | apotheosis 51 | chaos 52 | energy 53 | essence 54 | eternity 55 | excellence 56 | experience 57 | freedom 58 | nirvana 59 | order 60 | perfection 61 | spirit 62 | variation 63 | 64 | # Social 65 | acceptance 66 | brotherhood 67 | criticism 68 | culture 69 | discourse 70 | discussion 71 | justice 72 | piety 73 | respect 74 | security 75 | support 76 | tolerance 77 | trust 78 | warranty 79 | 80 | # Money & Power 81 | abundance 82 | admiration 83 | assurance 84 | authority 85 | awe 86 | certainty 87 | control 88 | domination 89 | enterprise 90 | fame 91 | grandeur 92 | influence 93 | luxury 94 | management 95 | opposition 96 | plenty 97 | popularity 98 | prestige 99 | prosperity 100 | reputation 101 | reverence 102 | reward 103 | superiority 104 | triumph 105 | wealth 106 | 107 | # Intelligence and Abilities 108 | acumen 109 | aptitude 110 | art 111 | artistry 112 | competence 113 | efficiency 114 | expertise 115 | finesse 116 | genius 117 | leadership 118 | perception 119 | skill 120 | virtuosity 121 | 122 | # Conversation 123 | argument 124 | debate 125 | 126 | # Strength 127 | action 128 | agility 129 | amplitude 130 | attack 131 | charisma 132 | chivalry 133 | defense 134 | defiance 135 | devotion 136 | dignity 137 | endurance 138 | exercise 139 | force 140 | fortitude 141 | gallantry 142 | health 143 | honor 144 | infinity 145 | inquire 146 | intensity 147 | luck 148 | mastery 149 | might 150 | opportunity 151 | penetration 152 | performance 153 | pluck 154 | potency 155 | protection 156 | prowess 157 | resistance 158 | serendipity 159 | speed 160 | stamina 161 | strength 162 | swiftness 163 | temperance 164 | tenacity 165 | valor 166 | vigor 167 | vitality 168 | will 169 | 170 | # Progress 171 | advance 172 | conversion 173 | correction 174 | development 175 | diversity 176 | elevation 177 | enhancement 178 | enrichment 179 | enthusiasm 180 | focus 181 | fruition 182 | growth 183 | improvement 184 | innovation 185 | modernism 186 | novelty 187 | proficiency 188 | progress 189 | promotion 190 | realization 191 | refinement 192 | renovation 193 | revolution 194 | success 195 | tempering 196 | upgrade 197 | 198 | # Completion 199 | ampleness 200 | completion 201 | satiation 202 | saturation 203 | sufficiency 204 | vastness 205 | wholeness 206 | 207 | # Charm & Beauty 208 | attraction 209 | beauty 210 | bloom 211 | cleaning 212 | courtesy 213 | glamour 214 | elegance 215 | fascination 216 | kindness 217 | joviality 218 | politeness 219 | refinement 220 | symmetry 221 | sympathy 222 | tact 223 | 224 | # Science & art 225 | calibration 226 | drama 227 | economy 228 | engineering 229 | examination 230 | philosophy 231 | poetry 232 | research 233 | science 234 | 235 | # Politics (and this is as far as we want to go) 236 | democracy 237 | election 238 | feminism 239 | 240 | # Pleasurable things 241 | champagne 242 | coffee 243 | cookies 244 | flowers 245 | fragrance 246 | honeydew 247 | music 248 | pizza 249 | 250 | # Weather & sky 251 | aurora 252 | blizzard 253 | current 254 | dew 255 | downpour 256 | drizzle 257 | hail 258 | hurricane 259 | lightning 260 | rain 261 | snow 262 | storm 263 | sunshine 264 | tempest 265 | thunder 266 | tornado 267 | typhoon 268 | weather 269 | wind 270 | whirlwind 271 | 272 | # Misc 273 | abracadabra 274 | adventure 275 | atheism 276 | camouflage 277 | destiny 278 | endeavor 279 | expression 280 | fantasy 281 | fertility 282 | imagination 283 | karma 284 | masquerade 285 | maturity 286 | radiance 287 | shopping 288 | sorcery 289 | unity 290 | witchcraft 291 | wizardry 292 | wonder 293 | youth 294 | 295 | # Participles 296 | purring 297 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Random Name and Slug Generator 3 | ============================== 4 | 5 | |pypi| |build| |coverage| |docs| 6 | 7 | Do you want random human-readable strings? 8 | 9 | .. code-block:: python 10 | 11 | >>> from coolname import generate_slug 12 | >>> generate_slug() 13 | 'big-maize-lori-of-renovation' 14 | >>> generate_slug() 15 | 'tunneling-amaranth-rhino-of-holiness' 16 | >>> generate_slug() 17 | 'soft-cuddly-shrew-of-expertise' 18 | 19 | Features 20 | ======== 21 | 22 | * Generate slugs, ready to use, Django-compatible. 23 | 24 | .. code-block:: python 25 | 26 | >>> from coolname import generate_slug 27 | >>> generate_slug() 28 | 'qualified-agama-of-absolute-kindness' 29 | 30 | * Generate names as sequences and do whatever you want with them. 31 | 32 | .. code-block:: python 33 | 34 | >>> from coolname import generate 35 | >>> generate() 36 | ['beneficial', 'bronze', 'bee', 'of', 'glee'] 37 | >>> ' '.join(generate()) 38 | 'limber transparent toad of luck' 39 | >>> ''.join(x.capitalize() for x in generate()) 40 | 'CalmRefreshingTerrierOfAttraction' 41 | 42 | * Generate names of specific length: 2, 3 or 4 words. 43 | 44 | .. code-block:: python 45 | 46 | >>> generate_slug(2) 47 | 'mottled-crab' 48 | >>> generate_slug(3) 49 | 'fantastic-acoustic-whale' 50 | >>> generate_slug(4) 51 | 'military-diamond-tuatara-of-endeavor' 52 | 53 | *Note: without argument, it returns a random length, but probability of 4‑word name is much higher.* 54 | *Prepositions and articles (of, from, the) are not counted as words.* 55 | 56 | * Use in command line: 57 | 58 | .. code-block:: bash 59 | 60 | $ coolname 61 | prophetic-tireless-bullfrog-of-novelty 62 | $ coolname 3 -n 2 -s '_' 63 | wildebeest_of_original_champagne 64 | ara_of_imminent_luck 65 | 66 | * Over 10\ :sup:`10`\ random names. 67 | 68 | ===== ============== ======================================= 69 | Words Combinations Example 70 | ===== ============== ======================================= 71 | 4 10\ :sup:`10`\ ``talented-enigmatic-bee-of-hurricane`` 72 | 3 10\ :sup:`8`\ ``ambitious-turaco-of-joviality`` 73 | 2 10\ :sup:`5`\ ``prudent-armadillo`` 74 | ===== ============== ======================================= 75 | 76 | .. code-block:: python 77 | 78 | >>> from coolname import get_combinations_count 79 | >>> get_combinations_count(4) 80 | 62620779367 81 | 82 | * Hand-picked vocabulary. ``sexy`` and ``demonic`` are about the most "offensive" words here - 83 | but there is only a pinch of them, for spice. Most words are either neutral, such as ``red``, or positive, 84 | such as ``brave``. And subject is always some animal, bird, fish, or insect - you can't be more neutral than 85 | Mother Nature. 86 | 87 | * `Easy customization `_. Create your own rules! 88 | 89 | .. code-block:: python 90 | 91 | >>> from coolname import RandomGenerator 92 | >>> generator = RandomGenerator({ 93 | ... 'all': { 94 | ... 'type': 'cartesian', 95 | ... 'lists': ['first_name', 'last_name'] 96 | ... }, 97 | ... 'first_name': { 98 | ... 'type': 'words', 99 | ... 'words': ['james', 'john'] 100 | ... }, 101 | ... 'last_name': { 102 | ... 'type': 'words', 103 | ... 'words': ['smith', 'brown'] 104 | ... } 105 | ... }) 106 | >>> generator.generate_slug() 107 | 'james-brown' 108 | 109 | Installation 110 | ============ 111 | 112 | .. code-block:: bash 113 | 114 | pip install coolname 115 | 116 | **coolname** is written in pure Python and has no dependencies. It works on any modern Python version (3.6+), including PyPy. 117 | 118 | 119 | .. |pypi| image:: https://img.shields.io/pypi/v/coolname.svg 120 | :target: https://pypi.python.org/pypi/coolname 121 | :alt: pypi 122 | 123 | .. |build| image:: https://api.travis-ci.org/alexanderlukanin13/coolname.svg?branch=master 124 | :target: https://travis-ci.org/alexanderlukanin13/coolname?branch=master 125 | :alt: build status 126 | 127 | .. |coverage| image:: https://coveralls.io/repos/alexanderlukanin13/coolname/badge.svg?branch=master&service=github 128 | :target: https://coveralls.io/github/alexanderlukanin13/coolname?branch=master 129 | :alt: coverage 130 | 131 | .. |docs| image:: https://img.shields.io/readthedocs/coolname.svg 132 | :target: http://coolname.readthedocs.io/en/latest/ 133 | :alt: documentation 134 | -------------------------------------------------------------------------------- /coolname/data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": { 3 | "comment": "Entry point", 4 | "type": "nested", 5 | "lists": ["2", "3", "4"], 6 | "ensure_unique": true, 7 | "ensure_unique_prefix": 4, 8 | "max_slug_length": 50 9 | }, 10 | "2": { 11 | "comment": "Two words (may also contain prepositions)", 12 | "type": "nested", 13 | "lists": ["an"] 14 | }, 15 | "3": { 16 | "comment": "Three words (may also contain prepositions)", 17 | "type": "nested", 18 | "lists": ["aan", "ano", "anl", "nuo", "as2", "s2o", "s2l", "sl2"] 19 | }, 20 | "4": { 21 | "comment": "Four words (may also contain prepositions)", 22 | "type": "nested", 23 | "lists": ["aano", "aanl", "anuo", "as2o", "s2uo", "as2l", "asl2"] 24 | }, 25 | "an": { 26 | "comment": "adjective-noun", 27 | "type": "cartesian", 28 | "lists": ["adj_any", "subj"] 29 | }, 30 | "aan": { 31 | "comment": "adjective-adjective-noun", 32 | "type": "cartesian", 33 | "lists": ["adj_far", "adj_near", "subj"] 34 | }, 35 | "ano": { 36 | "comment": "adjective-noun-of-noun", 37 | "type": "cartesian", 38 | "lists": ["adj_any", "subj", "of", "of_noun_any"] 39 | }, 40 | "anl": { 41 | "comment": "adjective-noun-from-location", 42 | "type": "cartesian", 43 | "lists": ["adj_any", "subj", "from", "from_noun_no_mod"] 44 | }, 45 | "nuo": { 46 | "comment": "noun-of-adjective-noun", 47 | "type": "cartesian", 48 | "lists": ["subj", "of", "of_modifier", "of_noun"] 49 | }, 50 | "as2": { 51 | "comment": "adjective-2word-subject", 52 | "type": "cartesian", 53 | "lists": ["adj_far", "subj2"] 54 | }, 55 | "s2o": { 56 | "comment": "2word-subject-of-noun", 57 | "type": "cartesian", 58 | "lists": ["subj2", "of", "of_noun_any"] 59 | }, 60 | "s2l": { 61 | "comment": "2word-subject-from-location", 62 | "type": "cartesian", 63 | "lists": ["subj2", "from", "from_noun_no_mod"] 64 | }, 65 | "sl2": { 66 | "comment": "subject-from-some-location", 67 | "type": "cartesian", 68 | "lists": ["subj", "from", "from2"] 69 | }, 70 | "aano": { 71 | "comment": "adjective-adjective-noun-of-noun", 72 | "type": "cartesian", 73 | "lists": ["adj_far", "adj_near", "subj", "of", "of_noun_any"] 74 | }, 75 | "aanl": { 76 | "comment": "adjective-adjective-noun-from-location", 77 | "type": "cartesian", 78 | "lists": ["adj_far", "adj_near", "subj", "from", "from_noun_no_mod"] 79 | }, 80 | "anuo": { 81 | "comment": "adjective-noun-of-adjective-noun", 82 | "type": "cartesian", 83 | "lists": ["adj_any", "subj", "of", "of_modifier", "of_noun"] 84 | }, 85 | "as2o": { 86 | "comment": "adjective-2word-subject-of-noun", 87 | "type": "cartesian", 88 | "lists": ["adj_far", "subj2", "of", "of_noun_any"] 89 | }, 90 | "s2uo": { 91 | "comment": "adjective-2word-subject-of-adjective-noun", 92 | "type": "cartesian", 93 | "lists": ["subj2", "of", "of_modifier", "of_noun"] 94 | }, 95 | "as2l": { 96 | "comment": "adjective-2word-subject-from-location", 97 | "type": "cartesian", 98 | "lists": ["adj_far", "subj2", "from", "from_noun_no_mod"] 99 | }, 100 | "asl2": { 101 | "comment": "adjective-subject-from-some-location", 102 | "type": "cartesian", 103 | "lists": ["adj_any", "subj", "from", "from2"] 104 | }, 105 | "adj_far": { 106 | "comment": "First adjective (with more following)", 107 | "type": "nested", 108 | "lists": [ 109 | "adjective", 110 | "adjective_first", 111 | "noun_adjective", 112 | "size" 113 | ] 114 | }, 115 | "adj_near": { 116 | "comment": "Last adjective (closest to the subject)", 117 | "type": "nested", 118 | "lists": [ 119 | "adjective", 120 | "adjective_near", 121 | "noun_adjective", 122 | "prefix" 123 | ] 124 | }, 125 | "adj_any": { 126 | "comment": "The only adjective (includes everything)", 127 | "type": "nested", 128 | "lists": [ 129 | "adjective", 130 | "adjective_near", 131 | "noun_adjective", 132 | "prefix", 133 | "size" 134 | ] 135 | }, 136 | "subj": { 137 | "comment": "The subject (animal)", 138 | "type": "nested", 139 | "lists": [ 140 | "animal", 141 | "animal_breed", 142 | "animal_legendary" 143 | ] 144 | }, 145 | "of": { 146 | "type": "const", 147 | "value": "of" 148 | }, 149 | "of_noun_any": { 150 | "type": "nested", 151 | "lists": ["of_noun", "of_noun_no_mod"] 152 | }, 153 | "from": { 154 | "type": "const", 155 | "value": "from" 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /coolname/loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides `load_config` function, 3 | which loads configuration from file or directory. 4 | 5 | You will need this only if you are creating 6 | custom instance of RandomGenerator. 7 | """ 8 | 9 | 10 | import codecs 11 | import json 12 | import os 13 | import re 14 | 15 | from .config import _CONF 16 | from .exceptions import InitializationError, ConfigurationError 17 | 18 | 19 | def load_config(path): 20 | """ 21 | Loads configuration from a path. 22 | 23 | Path can be a json file, or a directory containing config.json 24 | and zero or more *.txt files with word lists or phrase lists. 25 | 26 | Returns config dict. 27 | 28 | Raises InitializationError when something is wrong. 29 | """ 30 | path = os.path.abspath(path) 31 | if os.path.isdir(path): 32 | config, wordlists = _load_data(path) 33 | elif os.path.isfile(path): 34 | config = _load_config(path) 35 | wordlists = {} 36 | else: 37 | raise InitializationError('File or directory not found: {0}'.format(path)) 38 | for name, wordlist in wordlists.items(): 39 | if name in config: 40 | raise InitializationError("Conflict: list {!r} is defined both in config " 41 | "and in *.txt file. If it's a {!r} list, " 42 | "you should remove it from config." 43 | .format(name, _CONF.TYPE.WORDS)) 44 | config[name] = wordlist 45 | return config 46 | 47 | 48 | def _load_data(path): 49 | """ 50 | Loads data from a directory. 51 | Returns tuple (config_dict, wordlists). 52 | Raises Exception on failure (e.g. if data is corrupted). 53 | """ 54 | path = os.path.abspath(path) 55 | if not os.path.isdir(path): 56 | raise InitializationError('Directory not found: {0}'.format(path)) 57 | wordlists = {} 58 | for file_name in os.listdir(path): 59 | if os.path.splitext(file_name)[1] != '.txt': 60 | continue 61 | file_path = os.path.join(path, file_name) 62 | name = os.path.splitext(os.path.split(file_path)[1])[0] 63 | try: 64 | with codecs.open(file_path, encoding='utf-8') as file: 65 | wordlists[name] = _load_wordlist(name, file) 66 | except OSError as ex: 67 | raise InitializationError('Failed to read {}: {}'.format(file_path, ex)) 68 | config = _load_config(os.path.join(path, 'config.json')) 69 | return (config, wordlists) 70 | 71 | 72 | def _load_config(config_file_path): 73 | try: 74 | with codecs.open(config_file_path, encoding='utf-8') as file: 75 | return json.load(file) 76 | except OSError as ex: 77 | raise InitializationError('Failed to read config from {}: {}'.format(config_file_path, ex)) 78 | except ValueError as ex: 79 | raise ConfigurationError('Invalid JSON: {}'.format(ex)) 80 | 81 | 82 | # Word must be in English, 1-N letters, lowercase. 83 | _WORD_REGEX = re.compile(r'^[a-z]+$') 84 | _PHRASE_REGEX = re.compile(r'^\w+(?: \w+)*$') 85 | 86 | 87 | # Options are defined using simple notation: 'option = value' 88 | _OPTION_REGEX = re.compile(r'^([a-z_]+)\s*=\s*(\w+)$', re.UNICODE) 89 | _OPTIONS = [ 90 | (_CONF.FIELD.MAX_LENGTH, int), 91 | (_CONF.FIELD.NUMBER_OF_WORDS, int) 92 | ] 93 | 94 | 95 | def _parse_option(line): 96 | """ 97 | Parses option line. 98 | Returns (name, value). 99 | Raises ValueError on invalid syntax or unknown option. 100 | """ 101 | match = _OPTION_REGEX.match(line) 102 | if not match: 103 | raise ValueError('Invalid syntax') 104 | for name, type_ in _OPTIONS: 105 | if name == match.group(1): 106 | return name, type_(match.group(2)) 107 | raise ValueError('Unknown option') 108 | 109 | 110 | def _load_wordlist(name, stream): 111 | """ 112 | Loads list of words or phrases from file. 113 | 114 | Returns "words" or "phrases" dictionary, the same as used in config. 115 | Raises Exception if file is missing or invalid. 116 | """ 117 | items = [] 118 | max_length = None 119 | multiword = False 120 | multiword_start = None 121 | number_of_words = None 122 | for i, line in enumerate(stream, start=1): 123 | line = line.strip() 124 | if not line or line.startswith('#'): 125 | continue 126 | # Is it an option line, e.g. 'max_length = 10'? 127 | if '=' in line: 128 | if items: 129 | raise ConfigurationError('Invalid assignment at list {!r} line {}: {!r} ' 130 | '(options must be defined before words)' 131 | .format(name, i, line)) 132 | try: 133 | option, option_value = _parse_option(line) 134 | except ValueError as ex: 135 | raise ConfigurationError('Invalid assignment at list {!r} line {}: {!r} ' 136 | '({})' 137 | .format(name, i, line, ex)) 138 | if option == _CONF.FIELD.MAX_LENGTH: 139 | max_length = option_value 140 | elif option == _CONF.FIELD.NUMBER_OF_WORDS: 141 | number_of_words = option_value 142 | continue # pragma: no cover 143 | # Parse words 144 | if not multiword and _WORD_REGEX.match(line): 145 | if max_length is not None and len(line) > max_length: 146 | raise ConfigurationError('Word is too long at list {!r} line {}: {!r}' 147 | .format(name, i, line)) 148 | items.append(line) 149 | elif _PHRASE_REGEX.match(line): 150 | if not multiword: 151 | multiword = True 152 | multiword_start = len(items) 153 | phrase = tuple(line.split(' ')) 154 | if number_of_words is not None and len(phrase) != number_of_words: 155 | raise ConfigurationError('Phrase has {} word(s) (while number_of_words={}) ' 156 | 'at list {!r} line {}: {!r}' 157 | .format(len(phrase), number_of_words, name, i, line)) 158 | if max_length is not None and sum(len(x) for x in phrase) > max_length: 159 | raise ConfigurationError('Phrase is too long at list {!r} line {}: {!r}' 160 | .format(name, i, line)) 161 | items.append(phrase) 162 | else: 163 | raise ConfigurationError('Invalid syntax at list {!r} line {}: {!r}' 164 | .format(name, i, line)) 165 | if multiword: 166 | # If in phrase mode, convert everything to tuples 167 | for i in range(0, multiword_start): 168 | items[i] = (items[i], ) 169 | result = { 170 | _CONF.FIELD.TYPE: _CONF.TYPE.PHRASES, 171 | _CONF.FIELD.PHRASES: items 172 | } 173 | if number_of_words is not None: 174 | result[_CONF.FIELD.NUMBER_OF_WORDS] = number_of_words 175 | else: 176 | result = { 177 | _CONF.FIELD.TYPE: _CONF.TYPE.WORDS, 178 | _CONF.FIELD.WORDS: items 179 | } 180 | if max_length is not None: 181 | result[_CONF.FIELD.MAX_LENGTH] = max_length 182 | return result 183 | -------------------------------------------------------------------------------- /tests/test_impl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import io 3 | import unittest 4 | 5 | from coolname import RandomGenerator, InitializationError 6 | from coolname.impl import NestedList, CartesianList, Scalar,\ 7 | WordList, PhraseList, WordAsPhraseWrapper,\ 8 | _create_lists, _to_bytes, _create_default_generator 9 | 10 | from .common import TestCase, patch 11 | 12 | 13 | class TestImplementation(TestCase): 14 | 15 | def test_phrase_list(self): 16 | phrase_list = PhraseList([('black', 'cat'), ('white', 'dog')]) 17 | assert len(phrase_list) == 2 18 | assert phrase_list.multiword 19 | phrase_list[0] == ('black', 'cat') 20 | phrase_list[1] == ('white', 'dog') 21 | assert str(phrase_list) == "PhraseList([('black', 'cat'), ('white', 'dog')], len=2)" 22 | 23 | def test_nested_list(self): 24 | # Note that lists are internally sorted 25 | nested_list = NestedList([[1, 2, 3], 26 | [4, 5], 27 | [6, 7, 8, 9]]) 28 | self.assertEqual(nested_list.length, 9) 29 | self.assertEqual(nested_list[0], 6) 30 | self.assertEqual(nested_list[1], 7) 31 | self.assertEqual(nested_list[2], 8) 32 | self.assertEqual(nested_list[3], 9) 33 | self.assertEqual(nested_list[4], 1) 34 | self.assertEqual(nested_list[5], 2) 35 | self.assertEqual(nested_list[6], 3) 36 | self.assertEqual(nested_list[7], 4) 37 | self.assertEqual(nested_list[8], 5) 38 | 39 | def test_nested_list_out_of_range(self): 40 | nested_list = NestedList([[1, 2, 3], 41 | [4, 5]]) 42 | self.assertEqual(nested_list[4], 5) 43 | with self.assertRaises(IndexError): 44 | nested_list[5] 45 | 46 | def test_carthesian_list(self): 47 | cart_list = CartesianList([[1, 2, 3], [4, 5], [6, 7, 8, 9]]) 48 | self.assertEqual(cart_list.length, 24) 49 | self.assertEqual(cart_list[0], [1, 4, 6]) 50 | self.assertEqual(cart_list[1], [1, 4, 7]) 51 | self.assertEqual(cart_list[4], [1, 5, 6]) 52 | self.assertEqual(cart_list[8], [2, 4, 6]) 53 | self.assertEqual(cart_list[9], [2, 4, 7]) 54 | self.assertEqual(cart_list[23], [3, 5, 9]) 55 | # One more level of nesting 56 | cart_list = NestedList([ 57 | CartesianList([[10, 11], [12, 13]]), 58 | CartesianList([[1, 2, 3], [4, 5], [6, 7, 8, 9]]), 59 | ]) 60 | self.assertEqual(cart_list.length, 28) 61 | self.assertEqual(cart_list[0], [1, 4, 6]) 62 | self.assertEqual(cart_list[23], [3, 5, 9]) 63 | self.assertEqual(cart_list[24], [10, 12]) 64 | self.assertEqual(cart_list[27], [11, 13]) 65 | 66 | def test_phrase_list_squash_optimization(self): 67 | """PhraseLists should be squashed just like WordLists.""" 68 | config = { 69 | 'all': { 70 | 'type': 'cartesian', 71 | 'lists': ['nested1', 'nested2'], 72 | 'ensure_unique': True 73 | }, 74 | 'nested1': { 75 | 'type': 'nested', 76 | 'lists': ['phrases1', 'phrases2'] 77 | }, 78 | 'nested2': { 79 | 'type': 'nested', 80 | 'lists': ['phrases1', 'phrases2'] 81 | }, 82 | 'phrases1': { 83 | 'type': 'phrases', 84 | 'phrases': [['alpha', 'one'], ['beta', 'two']] 85 | }, 86 | 'phrases2': { 87 | 'type': 'phrases', 88 | 'phrases': [['gamma', 'three'], ['delta', 'four'], ['epsilon']] 89 | } 90 | } 91 | generator = RandomGenerator(config) 92 | # Make sure NestedLists are squashed and transformed into PhraseLists 93 | all_list = generator._lists[None] 94 | assert isinstance(all_list._lists[0], PhraseList) 95 | assert isinstance(all_list._lists[1], PhraseList) 96 | assert len(all_list._lists[0]) == 5 97 | assert all_list._lists[0] == all_list._lists[1] 98 | tuples = [ 99 | ('alpha', 'one'), 100 | ('beta', 'two'), 101 | ('gamma', 'three'), 102 | ('delta', 'four'), 103 | ('epsilon', ) 104 | ] 105 | assert all_list._lists[0] == sorted(tuples) 106 | assert 3 <= len(generator.generate()) <= 4 107 | 108 | def test_scalar(self): 109 | self.assertTrue(Scalar(10).random(), 10) 110 | 111 | def test_str(self): 112 | nested_list = NestedList([ 113 | CartesianList([[10, 11], [12, 13]]), 114 | CartesianList([[1, 2, 3], [4, 5], [6, 7, 8, 9]]), 115 | ]) 116 | self.assertEqual(str(nested_list), 'NestedList(2, len=28)') 117 | cart_list = CartesianList([[1, 2, 3], [4, 5], [6, 7, 8, 9]]) 118 | self.assertEqual(str(cart_list), 'CartesianList(3, len=24)') 119 | scalar = Scalar('10') 120 | self.assertEqual(str(scalar), "Scalar(value='10')") 121 | 122 | def test_dump_list(self): 123 | cart_list = NestedList([ 124 | CartesianList([[10, 11], [12, 13]]), 125 | CartesianList([[1, 2, 3], [4, 5], [6, 7, 8, 9]]), 126 | ]) 127 | stream = io.StringIO() 128 | cart_list._dump(stream) 129 | self.assertEqual(stream.getvalue(), 130 | 'NestedList(2, len=28)\n' 131 | ' CartesianList(3, len=24)\n' 132 | ' WordList([1, 2, 3], len=3)\n' 133 | ' WordList([4, 5], len=2)\n' 134 | ' WordList([6, 7, 8, ...], len=4)\n' 135 | ' CartesianList(2, len=4)\n' 136 | ' WordList([10, 11], len=2)\n' 137 | ' WordList([12, 13], len=2)\n') 138 | 139 | def test_dump_generator(self): 140 | config = { 141 | 'all': { 142 | 'type': 'words', 143 | 'words': ['one', 'two', 'three'] 144 | } 145 | } 146 | generator = RandomGenerator(config) 147 | stream = io.StringIO() 148 | generator._dump(stream) 149 | self.assertEqual(stream.getvalue(), 150 | "WordList(['one', 'two', 'three'], len=3)\n") 151 | 152 | def test_create_lists(self): 153 | # For the sake of coverage 154 | with self.assertRaisesRegex(InitializationError, r"Unknown list type: 'wrong'"): 155 | config = { 156 | 'all': {'type': 'wrong'} 157 | } 158 | _create_lists(config, {}, 'all', []) 159 | 160 | def test_encode(self): 161 | # _encode must encode unicode strings 162 | self.assertEqual(_to_bytes('привет'), 163 | 'привет'.encode('utf-8')) 164 | # _encode must return byte strings unchanged 165 | self.assertEqual(_to_bytes('привет'.encode('utf-8')), 166 | 'привет'.encode('utf-8')) 167 | 168 | @patch('os.path.isdir', return_value=False) 169 | def test_import_data_from_init_py(self, *args): 170 | generator = _create_default_generator() 171 | assert isinstance(generator.generate()[0], str) 172 | assert isinstance(generator.generate_slug(), str) 173 | 174 | 175 | def test_WordAsPhraseWrapper(self): 176 | wrapper = WordAsPhraseWrapper(WordList(['one', 'two'])) 177 | assert len(wrapper) == 2 178 | assert wrapper[0] == ('one', ) 179 | assert wrapper[1] == ('two', ) 180 | assert str(wrapper) == "WordAsPhraseWrapper(WordList(['one', 'two'], len=2))" 181 | assert repr(wrapper) == str(wrapper) 182 | 183 | 184 | def test_NestLest_str_repr(self): 185 | nested_list = NestedList([WordList(['one', 'two'])]) 186 | assert str(nested_list) == "NestedList(1, len=2)" 187 | assert repr(nested_list) == str(nested_list) 188 | 189 | 190 | if __name__ == '__main__': 191 | import sys 192 | sys.exit(unittest.main()) 193 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/coolname.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/coolname.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/coolname" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/coolname" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\coolname.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\coolname.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /coolname/data/adjective.txt: -------------------------------------------------------------------------------- 1 | # =================================================================================== 2 | # Adjectives that can be used anywhere (as a first adjective, second adjective, etc.) 3 | # 4 | # If you want to contribute: 5 | # 1. Make sure adjective really fits anywhere. 6 | # Try: 7 | # black dog 8 | # big dog 9 | # Does it sound too awkward? 10 | # 2. Insert word into the most appropriate list. 11 | # When in doubt, search for synonyms, maybe they are already here. 12 | # If it still doesn't make sense, feel free to contact the package owner. 13 | # 3. Keep lists sorted alphabetically. 14 | # =================================================================================== 15 | 16 | max_length = 13 17 | 18 | # Appearance, sound, smell... 19 | acrid 20 | ambrosial 21 | amorphous 22 | armored 23 | aromatic 24 | bald 25 | blazing 26 | boisterous 27 | bouncy 28 | brawny 29 | bulky 30 | camouflaged 31 | caped 32 | chubby 33 | curvy 34 | elastic 35 | ethereal 36 | fat 37 | feathered 38 | fiery 39 | flashy 40 | flat 41 | fluffy 42 | foamy 43 | fragrant 44 | furry 45 | fuzzy 46 | glaring 47 | hairy 48 | heavy 49 | hissing 50 | horned 51 | icy 52 | imaginary 53 | invisible 54 | lean 55 | loud 56 | loutish 57 | lumpy 58 | lush 59 | masked 60 | meaty 61 | messy 62 | misty 63 | nebulous 64 | noisy 65 | nondescript 66 | organic 67 | purring 68 | quiet 69 | quirky 70 | radiant 71 | roaring 72 | ruddy 73 | rustling 74 | screeching 75 | shaggy 76 | shapeless 77 | shiny 78 | silent 79 | silky 80 | singing 81 | skinny 82 | smooth 83 | soft 84 | spicy 85 | spiked 86 | statuesque 87 | sticky 88 | tacky 89 | tall 90 | tangible 91 | tentacled 92 | thick 93 | thundering 94 | venomous 95 | warm 96 | weightless 97 | whispering 98 | winged 99 | wooden 100 | 101 | # Beauty & Charm 102 | adorable 103 | affable 104 | amazing 105 | amiable 106 | attractive 107 | beautiful 108 | calm 109 | charming 110 | cherubic 111 | classic 112 | classy 113 | convivial 114 | cordial 115 | cuddly 116 | curly 117 | cute 118 | debonair 119 | elegant 120 | famous 121 | fresh 122 | friendly 123 | funny 124 | gorgeous 125 | graceful 126 | gregarious 127 | grinning 128 | handsome 129 | hilarious 130 | hot 131 | interesting 132 | kind 133 | laughing 134 | lovely 135 | meek 136 | mellow 137 | merciful 138 | neat 139 | nifty 140 | notorious 141 | poetic 142 | pretty 143 | refined 144 | refreshing 145 | sexy 146 | smiling 147 | sociable 148 | spiffy 149 | stylish 150 | sweet 151 | tactful 152 | whimsical 153 | 154 | # Character & Emotions 155 | abiding 156 | accurate 157 | adamant 158 | adaptable 159 | adventurous 160 | alluring 161 | aloof 162 | ambitious 163 | amusing 164 | annoying 165 | arrogant 166 | aspiring 167 | belligerent 168 | benign 169 | berserk 170 | benevolent 171 | bold 172 | brave 173 | cheerful 174 | chirpy 175 | cocky 176 | congenial 177 | courageous 178 | cryptic 179 | curious 180 | daft 181 | dainty 182 | daring 183 | defiant 184 | delicate 185 | delightful 186 | determined 187 | devout 188 | didactic 189 | diligent 190 | discreet 191 | dramatic 192 | dynamic 193 | eager 194 | eccentric 195 | elated 196 | encouraging 197 | enigmatic 198 | enthusiastic 199 | evasive 200 | faithful 201 | fair 202 | fanatic 203 | fearless 204 | fervent 205 | festive 206 | fierce 207 | fine 208 | free 209 | gabby 210 | garrulous 211 | gay 212 | gentle 213 | glistening 214 | greedy 215 | grumpy 216 | happy 217 | honest 218 | hopeful 219 | hospitable 220 | impetuous 221 | independent 222 | industrious 223 | innocent 224 | intrepid 225 | jolly 226 | jovial 227 | just 228 | lively 229 | loose 230 | loyal 231 | merry 232 | modest 233 | mysterious 234 | nice 235 | obedient 236 | optimistic 237 | orthodox 238 | outgoing 239 | outrageous 240 | overjoyed 241 | passionate 242 | perky 243 | placid 244 | polite 245 | positive 246 | proud 247 | prudent 248 | puzzling 249 | quixotic 250 | quizzical 251 | rebel 252 | resolute 253 | rampant 254 | righteous 255 | romantic 256 | rough 257 | rousing 258 | sassy 259 | satisfied 260 | sly 261 | sincere 262 | snobbish 263 | solemn 264 | spirited 265 | spry 266 | stalwart 267 | stirring 268 | swinging 269 | tasteful 270 | thankful 271 | tidy 272 | tremendous 273 | truthful 274 | unselfish 275 | upbeat 276 | uppish 277 | valiant 278 | vehement 279 | vengeful 280 | vigorous 281 | vivacious 282 | zealous 283 | zippy 284 | 285 | # Intelligence & Abilities 286 | able 287 | adept 288 | analytic 289 | astute 290 | attentive 291 | brainy 292 | busy 293 | calculating 294 | capable 295 | careful 296 | cautious 297 | certain 298 | clever 299 | competent 300 | conscious 301 | cooperative 302 | crafty 303 | crazy 304 | cunning 305 | daffy 306 | devious 307 | discerning 308 | efficient 309 | expert 310 | functional 311 | gifted 312 | helpful 313 | enlightened 314 | idealistic 315 | impartial 316 | industrious 317 | ingenious 318 | inquisitive 319 | intelligent 320 | inventive 321 | judicious 322 | keen 323 | knowing 324 | literate 325 | logical 326 | masterful 327 | mindful 328 | nonchalant 329 | observant 330 | omniscient 331 | poised 332 | practical 333 | pragmatic 334 | proficient 335 | provocative 336 | qualified 337 | radical 338 | rational 339 | realistic 340 | resourceful 341 | savvy 342 | sceptical 343 | sensible 344 | serious 345 | shrewd 346 | skilled 347 | slick 348 | slim 349 | sloppy 350 | smart 351 | sophisticated 352 | stoic 353 | subtle 354 | succinct 355 | talented 356 | thoughtful 357 | tricky 358 | unbiased 359 | uptight 360 | versatile 361 | versed 362 | visionary 363 | wise 364 | witty 365 | 366 | # Strength & Agility 367 | accelerated 368 | active 369 | agile 370 | athletic 371 | dashing 372 | deft 373 | dexterous 374 | energetic 375 | fast 376 | frisky 377 | hasty 378 | hypersonic 379 | meteoric 380 | mighty 381 | muscular 382 | nimble 383 | nippy 384 | powerful 385 | prompt 386 | quick 387 | rapid 388 | resilient 389 | robust 390 | rugged 391 | solid 392 | speedy 393 | steadfast 394 | steady 395 | strong 396 | sturdy 397 | tireless 398 | tough 399 | unyielding 400 | 401 | # Money & Power 402 | rich 403 | wealthy 404 | 405 | # Science 406 | meticulous 407 | precise 408 | rigorous 409 | scrupulous 410 | strict 411 | 412 | # Movement type 413 | airborne 414 | burrowing 415 | crouching 416 | flying 417 | hidden 418 | hopping 419 | jumping 420 | lurking 421 | tunneling 422 | warping 423 | 424 | # Location and Dwelling 425 | aboriginal 426 | amphibian 427 | aquatic 428 | arboreal 429 | polar 430 | terrestrial 431 | urban 432 | 433 | # Awesome 434 | accomplished 435 | astonishing 436 | authentic 437 | awesome 438 | delectable 439 | excellent 440 | exotic 441 | exuberant 442 | fabulous 443 | fantastic 444 | fascinating 445 | flawless 446 | fortunate 447 | funky 448 | godlike 449 | glorious 450 | groovy 451 | honored 452 | illustrious 453 | imposing 454 | important 455 | impressive 456 | incredible 457 | invaluable 458 | kickass 459 | majestic 460 | magnificent 461 | marvellous 462 | monumental 463 | perfect 464 | phenomenal 465 | pompous 466 | precious 467 | premium 468 | private 469 | remarkable 470 | spectacular 471 | splendid 472 | successful 473 | wonderful 474 | wondrous 475 | 476 | # Original 477 | offbeat 478 | original 479 | outstanding 480 | quaint 481 | unique 482 | 483 | # Time 484 | ancient 485 | antique 486 | prehistoric 487 | primitive 488 | 489 | # Misc 490 | abstract 491 | acoustic 492 | angelic 493 | arcane 494 | archetypal 495 | augmented 496 | auspicious 497 | axiomatic 498 | beneficial 499 | bipedal 500 | bizarre 501 | complex 502 | dancing 503 | dangerous 504 | demonic 505 | divergent 506 | economic 507 | electric 508 | elite 509 | eminent 510 | enchanted 511 | esoteric 512 | finicky 513 | fractal 514 | futuristic 515 | gainful 516 | hallowed 517 | heavenly 518 | heretic 519 | holistic 520 | hungry 521 | hypnotic 522 | hysterical 523 | illegal 524 | imperial 525 | imported 526 | impossible 527 | inescapable 528 | juicy 529 | liberal 530 | ludicrous 531 | lyrical 532 | magnetic 533 | manipulative 534 | mature 535 | military 536 | macho 537 | married 538 | melodic 539 | natural 540 | naughty 541 | nocturnal 542 | nostalgic 543 | optimal 544 | pastoral 545 | peculiar 546 | piquant 547 | pristine 548 | prophetic 549 | psychedelic 550 | quantum 551 | rare 552 | real 553 | secret 554 | simple 555 | spectral 556 | spiritual 557 | stereotyped 558 | stimulating 559 | straight 560 | strange 561 | tested 562 | therapeutic 563 | true 564 | ubiquitous 565 | uncovered 566 | unnatural 567 | utopian 568 | vagabond 569 | vague 570 | vegan 571 | victorious 572 | vigilant 573 | voracious 574 | wakeful 575 | wandering 576 | watchful 577 | wild 578 | 579 | # Pseudo-colors 580 | bright 581 | brilliant 582 | colorful 583 | crystal 584 | dark 585 | dazzling 586 | fluorescent 587 | glittering 588 | glossy 589 | gleaming 590 | light 591 | mottled 592 | neon 593 | opalescent 594 | pastel 595 | smoky 596 | sparkling 597 | spotted 598 | striped 599 | translucent 600 | transparent 601 | vivid 602 | -------------------------------------------------------------------------------- /coolname/data/animal.txt: -------------------------------------------------------------------------------- 1 | # ======================================================== 2 | # Wild animals (in the widest sense of the word "animal"). 3 | # Also, some generic terms for domestic animals. 4 | # 5 | # Animals are grouped by scientific classification 6 | # (typically by Class -> Order, but it may vary). 7 | # ======================================================== 8 | 9 | max_length = 13 10 | 11 | # Annelida 12 | earthworm 13 | leech 14 | worm 15 | 16 | # Arthropoda -> Arachnomorpha 17 | scorpion 18 | spider 19 | tarantula 20 | 21 | # Arthropoda -> Crustacea 22 | barnacle 23 | crab 24 | crayfish 25 | lobster 26 | pillbug 27 | prawn 28 | shrimp 29 | 30 | # Arthropoda -> Insecta 31 | ant 32 | bee 33 | beetle 34 | bug 35 | bumblebee 36 | butterfly 37 | caterpillar 38 | cicada 39 | cricket 40 | dragonfly 41 | earwig 42 | firefly 43 | grasshopper 44 | honeybee 45 | hornet 46 | inchworm 47 | ladybug 48 | locust 49 | mantis 50 | mayfly 51 | mosquito 52 | moth 53 | sawfly 54 | silkworm 55 | termite 56 | wasp 57 | woodlouse 58 | 59 | # Arthropoda -> Myriapoda 60 | centipede 61 | millipede 62 | 63 | # Artiodactyla -> Antilocapridae 64 | pronghorn 65 | 66 | # Artiodactyla -> Bovidae 67 | antelope 68 | bison 69 | buffalo 70 | bull 71 | chamois 72 | cow 73 | gazelle 74 | gaur 75 | goat 76 | ibex 77 | impala 78 | kudu 79 | markhor 80 | mouflon 81 | muskox 82 | nyala 83 | oryx 84 | sheep 85 | wildebeest 86 | yak 87 | zebu 88 | 89 | # Artiodactyla -> Camelidae 90 | alpaca 91 | camel 92 | llama 93 | vicugna 94 | 95 | # Artiodactyla -> Cervidae 96 | caribou 97 | chital 98 | deer 99 | elk 100 | moose 101 | pudu 102 | reindeer 103 | sambar 104 | wapiti 105 | 106 | # Artiodactyla -> Cetacea 107 | beluga 108 | dolphin 109 | narwhal 110 | orca 111 | porpoise 112 | whale 113 | 114 | # Artiodactyla -> Equidae 115 | donkey 116 | horse 117 | stallion 118 | zebra 119 | 120 | # Artiodactyla-> Giraffidae 121 | giraffe 122 | okapi 123 | 124 | # Artiodactyla-> Hippopotamidae 125 | hippo 126 | 127 | # Artiodactyla -> Rhinocerotidae 128 | rhino 129 | 130 | # Artiodactyla -> Suidae 131 | boar 132 | hog 133 | pig 134 | swine 135 | warthog 136 | 137 | # Artiodactyla -> Tayassuidae 138 | peccary 139 | 140 | # Aves -> Accipitriformes 141 | buzzard 142 | eagle 143 | goshawk 144 | harrier 145 | hawk 146 | vulture 147 | 148 | # Aves -> Anseriformes 149 | duck 150 | goose 151 | swan 152 | teal 153 | 154 | # Aves 155 | bird 156 | 157 | # Aves -> Apodiformes 158 | hummingbird 159 | swift 160 | 161 | # Aves -> Apterygiformes 162 | kiwi 163 | 164 | # Aves -> Ardeidae 165 | bittern 166 | 167 | # Aves -> Caprimulgiformes 168 | potoo 169 | 170 | # Aves -> Cariamiformes 171 | seriema 172 | 173 | # Aves -> Casuariiformes 174 | cassowary 175 | emu 176 | 177 | # Aves -> Cathartiformes 178 | condor 179 | 180 | # Aves -> Charadriiformes 181 | auk 182 | avocet 183 | guillemot 184 | kittiwake 185 | puffin 186 | seagull 187 | skua 188 | 189 | # Aves -> Ciconiiformes 190 | stork 191 | 192 | # Aves -> Columbiformes 193 | dodo 194 | dove 195 | pigeon 196 | 197 | # Aves -> Coraciiformes 198 | kingfisher 199 | tody 200 | 201 | # Aves -> Cuculiformes 202 | bustard 203 | coua 204 | coucal 205 | cuckoo 206 | koel 207 | malkoha 208 | roadrunner 209 | 210 | # Aves -> Eurypygiformes 211 | kagu 212 | 213 | # Aves -> Falconiformes 214 | caracara 215 | falcon 216 | kestrel 217 | 218 | # Aves -> Galliformes 219 | chachalaca 220 | chicken 221 | curassow 222 | grouse 223 | guan 224 | junglefowl 225 | partridge 226 | peacock 227 | pheasant 228 | quail 229 | rooster 230 | turkey 231 | 232 | # Aves -> Gaviiformes 233 | loon 234 | 235 | # Aves -> Gruiformes 236 | coot 237 | crane 238 | 239 | # Aves -> Musophagiformes 240 | turaco 241 | 242 | # Aves -> Opisthocomiformes 243 | hoatzin 244 | 245 | # Aves -> Passeriformes 246 | bullfinch 247 | crow 248 | jackdaw 249 | jaybird 250 | finch 251 | lyrebird 252 | magpie 253 | myna 254 | nightingale 255 | nuthatch 256 | oriole 257 | oxpecker 258 | raven 259 | robin 260 | rook 261 | skylark 262 | sparrow 263 | starling 264 | swallow 265 | waxbill 266 | wren 267 | 268 | # Aves -> Pelecaniformes 269 | heron 270 | ibis 271 | 272 | # Aves -> Piciformes 273 | jacamar 274 | piculet 275 | toucan 276 | toucanet 277 | woodpecker 278 | 279 | # Aves -> Phoenicopteriformes 280 | flamingo 281 | 282 | # Aves -> Podicipediformes 283 | grebe 284 | 285 | # Aves -> Procellariiformes 286 | albatross 287 | fulmar 288 | petrel 289 | spoonbill 290 | 291 | # Aves -> Psittaciformes 292 | ara 293 | cockatoo 294 | kakapo 295 | lorikeet 296 | macaw 297 | parakeet 298 | parrot 299 | 300 | # Aves -> Sphenisciformes 301 | penguin 302 | 303 | # Aves -> Struthionidae 304 | ostrich 305 | 306 | # Aves -> Strigiformes 307 | boobook 308 | owl 309 | 310 | # Aves -> Suliformes 311 | booby 312 | cormorant 313 | frigatebird 314 | pelican 315 | 316 | # Aves -> Trogoniformes 317 | quetzal 318 | trogon 319 | 320 | # Chordata -> Amphibia 321 | axolotl 322 | bullfrog 323 | frog 324 | newt 325 | salamander 326 | toad 327 | 328 | # Chordata -> Actinopterygii 329 | angelfish 330 | barracuda 331 | carp 332 | catfish 333 | dogfish 334 | goldfish 335 | guppy 336 | eel 337 | flounder 338 | herring 339 | lionfish 340 | mackerel 341 | oarfish 342 | perch 343 | salmon 344 | seahorse 345 | sturgeon 346 | sunfish 347 | tench 348 | trout 349 | tuna 350 | wrasse 351 | 352 | # Chordata -> Chondrichthyes 353 | sawfish 354 | shark 355 | stingray 356 | 357 | # Cnidaria -> Medusozoa 358 | jellyfish 359 | 360 | # Crocodilia 361 | alligator 362 | caiman 363 | crocodile 364 | gharial 365 | 366 | # Echinodermata -> Asterozoa 367 | starfish 368 | 369 | # Echinodermata -> Echinoidea 370 | urchin 371 | 372 | # Erinaceomorpha -> Erinaceidae 373 | hedgehog 374 | 375 | # Mammalia -> Carnivora -> Canidae 376 | coyote 377 | dingo 378 | dog 379 | fennec 380 | fox 381 | hound 382 | jackal 383 | tanuki 384 | wolf 385 | 386 | # Mammalia -> Carnivora -> Felidae 387 | bobcat 388 | caracal 389 | cat 390 | cougar 391 | jaguar 392 | jaguarundi 393 | leopard 394 | lion 395 | lynx 396 | manul 397 | ocelot 398 | panther 399 | puma 400 | serval 401 | smilodon 402 | tiger 403 | wildcat 404 | 405 | # Mammalia -> Carnivora -> Feliformia 406 | aardwolf 407 | binturong 408 | cheetah 409 | civet 410 | fossa 411 | hyena 412 | meerkat 413 | mongoose 414 | 415 | # Mammalia -> Carnivora -> Musteloidea 416 | badger 417 | coati 418 | ermine 419 | ferret 420 | marten 421 | mink 422 | otter 423 | polecat 424 | skunk 425 | stoat 426 | weasel 427 | wolverine 428 | 429 | # Mammalia -> Carnivora -> Pinnipedia 430 | seal 431 | walrus 432 | 433 | # Mammalia -> Carnivora -> Procyonidae 434 | raccoon 435 | ringtail 436 | 437 | # Mammalia -> Carnivora -> Ursidae 438 | bear 439 | panda 440 | 441 | # Mammalia -> Chiroptera 442 | bat 443 | 444 | # Mammalia -> Cingulata 445 | armadillo 446 | 447 | # Mammalia -> Elephantidae 448 | elephant 449 | mammoth 450 | # not actually belongs here... 451 | mastodon 452 | 453 | # Mammalia -> Eutheria 454 | mole 455 | 456 | # Mammalia -> Hyracoidea 457 | hyrax 458 | 459 | # Mammalia -> Marsupialia 460 | bandicoot 461 | bettong 462 | cuscus 463 | kangaroo 464 | koala 465 | numbat 466 | quokka 467 | quoll 468 | wallaby 469 | wombat 470 | 471 | # Mammalia -> Monotremata 472 | echidna 473 | platypus 474 | 475 | # Mammalia -> Perissodactyla 476 | tapir 477 | 478 | # Mammalia -> Pilosa 479 | anteater 480 | sloth 481 | 482 | # Mammalia -> Rodenta 483 | agouti 484 | beaver 485 | capybara 486 | chinchilla 487 | chipmunk 488 | degu 489 | dormouse 490 | gerbil 491 | gopher 492 | groundhog 493 | jackrabbit 494 | jerboa 495 | hamster 496 | hare 497 | lemming 498 | marmot 499 | mouse 500 | muskrat 501 | porcupine 502 | rabbit 503 | rat 504 | squirrel 505 | vole 506 | 507 | # Mammalia -> Primates 508 | ape 509 | baboon 510 | bonobo 511 | capuchin 512 | chimpanzee 513 | galago 514 | gibbon 515 | gorilla 516 | lemur 517 | lori 518 | macaque 519 | mandrill 520 | marmoset 521 | monkey 522 | orangutan 523 | tamarin 524 | tarsier 525 | uakari 526 | 527 | # Mammalia -> Sirenia 528 | dugong 529 | manatee 530 | 531 | # Mammalia -> Soricomorpha 532 | shrew 533 | 534 | # Mammalia -> Tubulidentata 535 | aardwark 536 | 537 | # Mollusca -> Bivalvia 538 | clam 539 | cockle 540 | mussel 541 | oyster 542 | scallop 543 | shellfish 544 | 545 | # Mollusca -> Cephalopoda 546 | ammonite 547 | cuttlefish 548 | nautilus 549 | octopus 550 | squid 551 | 552 | # Mollusca -> Gastropoda 553 | limpet 554 | slug 555 | snail 556 | 557 | # Porifera 558 | sponge 559 | 560 | # Rhynchocephalia 561 | tuatara 562 | 563 | # Squamata 564 | agama 565 | chameleon 566 | dragon 567 | gecko 568 | iguana 569 | lizard 570 | pogona 571 | skink 572 | 573 | # Squamata -> Serpentes 574 | adder 575 | anaconda 576 | asp 577 | boa 578 | cobra 579 | copperhead 580 | mamba 581 | python 582 | rattlesnake 583 | sidewinder 584 | snake 585 | taipan 586 | viper 587 | 588 | # Testudines 589 | tortoise 590 | turtle 591 | 592 | # Dinosaurs 593 | dinosaur 594 | raptor 595 | 596 | # Not quite animals 597 | mushroom -------------------------------------------------------------------------------- /tests/test_loader.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from io import StringIO 3 | import os 4 | import os.path as op 5 | import tempfile 6 | 7 | import unittest 8 | 9 | from coolname import InitializationError 10 | from coolname.loader import _load_wordlist, _load_data 11 | 12 | from .common import patch, TestCase 13 | 14 | 15 | NO_DATA_DIR = op.normpath(op.join('.', 'no_such_dir', 'data')) 16 | 17 | 18 | class LoaderTest(TestCase): 19 | 20 | def test_load_wordlist(self): 21 | s = StringIO('\n'.join([ 22 | 'alpha', 23 | '', # blank line 24 | 'beta', 25 | '# Some comment', 26 | 'gamma', 27 | ])) 28 | wordlist = _load_wordlist('words', s) 29 | self.assertEqual(wordlist, { 30 | 'type': 'words', 31 | 'words': ['alpha', 'beta', 'gamma'] 32 | }) 33 | 34 | def test_load_wordlist_max_length(self): 35 | s = StringIO('\n'.join([ 36 | 'max_length = 11', 37 | 'alpha', 38 | ])) 39 | wordlist = _load_wordlist('words', s) 40 | self.assertEqual(wordlist, { 41 | 'type': 'words', 42 | 'max_length': 11, 43 | 'words': ['alpha'] 44 | }) 45 | 46 | def test_invalid_wordlist(self): 47 | s = StringIO('\n'.join([ 48 | 'alpha', 49 | 'invalid?syntax', 50 | ])) 51 | with self.assertRaisesRegex(InitializationError, 52 | r"Invalid config: Invalid syntax " 53 | r"at list 'words' line 2: u?'invalid\?syntax'"): 54 | _load_wordlist('words', s) 55 | 56 | def test_word_too_long(self): 57 | s = StringIO('\n'.join([ 58 | 'max_length = 11', 59 | 'alpha', 60 | 'augmentation', # line exceeds 11 characters 61 | ])) 62 | with self.assertRaisesRegex(InitializationError, 63 | r"Invalid config: Word is too long " 64 | r"at list 'words' line 3: u?'augmentation'"): 65 | _load_wordlist('words', s) 66 | 67 | def test_load_data_no_dir(self): 68 | path = os.path.join(tempfile.gettempdir(), 'does', 'not', 'exist') 69 | with self.assertRaisesRegex(InitializationError, r'Directory not found: .+exist'): 70 | _load_data(path) 71 | 72 | def test_load_phrases(self): 73 | s = StringIO('\n'.join([ 74 | 'one', 75 | 'two', 76 | 'three', 77 | 'four five', 78 | 'six', 79 | 'seven eight' 80 | ])) 81 | wordlist = _load_wordlist('phrases', s) 82 | self.assertEqual(wordlist, { 83 | 'type': 'phrases', 84 | 'phrases': [ 85 | ('one', ), 86 | ('two', ), 87 | ('three', ), 88 | ('four', 'five'), 89 | ('six',), 90 | ('seven', 'eight') 91 | ] 92 | }) 93 | 94 | def test_phrase_too_long(self): 95 | s = StringIO('\n'.join([ 96 | 'max_length = 9', 97 | 'alpha beta', 98 | 'gamma delta', # 10 characters 99 | ])) 100 | with self.assertRaisesRegex(InitializationError, 101 | r"Invalid config: Phrase is too long " 102 | r"at list 'words' line 3: u?'gamma delta'"): 103 | _load_wordlist('words', s) 104 | 105 | @patch('json.load') 106 | @patch('coolname.loader._load_wordlist') 107 | @patch('codecs.open') 108 | @patch('os.path.isdir') 109 | @patch('os.listdir') 110 | def test_load_data(self, 111 | listdir_mock, isdir_mock, open_mock, 112 | load_wordlist_mock, json_mock): 113 | listdir_mock.return_value = ['one.txt', 'two.txt'] 114 | isdir_mock.return_value = True 115 | lists = iter([['one', 'ichi'], ['two', 'ni']]) 116 | load_wordlist_mock.side_effect = lambda x, y: next(lists) 117 | json_mock.return_value = {'hello': 'world'} 118 | config, wordlists = _load_data(NO_DATA_DIR) 119 | self.assertEqual(config, {'hello': 'world'}) 120 | self.assertEqual(wordlists, { 121 | 'one': ['one', 'ichi'], 122 | 'two': ['two', 'ni'], 123 | }) 124 | 125 | @patch('codecs.open', side_effect=OSError('BOOM!')) 126 | @patch('os.path.isdir', return_value=True) 127 | @patch('os.listdir', return_value=['one.txt', 'two.txt']) 128 | def test_load_data_os_error(self, listdir_mock, isdir_mock, open_mock): 129 | with self.assertRaisesRegex(InitializationError, 130 | r'Failed to read .+one.txt: BOOM!'): 131 | _load_data(NO_DATA_DIR) 132 | 133 | @patch('codecs.open') 134 | @patch('os.path.isdir', return_value=True) 135 | @patch('os.listdir', return_value=['one.txt']) 136 | def test_load_data_failed_to_read_config(self, listdir_mock, isdir_mock, 137 | open_mock): 138 | # First call to open() should pass, 139 | # second call should raise OSError. 140 | class open_then_fail(object): 141 | 142 | def __init__(self): 143 | self.called = False 144 | 145 | def __call__(self, *x, **y): 146 | if self.called: 147 | raise OSError('BOOM!') 148 | self.called = True 149 | return StringIO('word') 150 | 151 | open_mock.side_effect = open_then_fail() 152 | with self.assertRaisesRegex(InitializationError, 153 | r"Failed to read config from " 154 | ".+config\.json: BOOM!"): 155 | _load_data(NO_DATA_DIR) 156 | 157 | @patch('codecs.open', side_effect=lambda *x, **y: StringIO('word')) 158 | @patch('os.path.isdir', return_value=True) 159 | @patch('os.listdir', return_value=['one.txt', 'two.txt']) 160 | def test_load_data_invalid_json(self, *args): 161 | with self.assertRaisesRegex(InitializationError, 162 | r"Invalid config: Invalid JSON: " 163 | r"((?:Expecting value|Unexpected 'w'(?: at)?): line 1 column 1 \(char 0\)|" 164 | r"No JSON object could be decoded)"): 165 | _load_data(NO_DATA_DIR) 166 | 167 | @patch('codecs.open') 168 | @patch('os.path.isdir', return_value=True) 169 | @patch('os.listdir', return_value=['one.txt']) 170 | def test_invalid_options_in_txt(self, mock1, mock2, open_mock): 171 | load_data = partial(_load_data, NO_DATA_DIR) 172 | # Invalid syntax 173 | open_mock.return_value = StringIO('max_length=\n') 174 | with self.assertRaisesRegex(InitializationError, 175 | r"Invalid config: Invalid assignment " 176 | r"at list u?'one' line 1: " 177 | r"u?'max_length=' \(Invalid syntax\)"): 178 | load_data() 179 | 180 | # Unknown option 181 | open_mock.return_value = StringIO('unknown_option=10\n') 182 | with self.assertRaisesRegex(InitializationError, 183 | r"Invalid config: Invalid assignment " 184 | r"at list u?'one' line 1: " 185 | r"u?'unknown_option=10' \(Unknown option\)"): 186 | load_data() 187 | 188 | # max_length is not int 189 | open_mock.return_value = StringIO('max_length=string\n') 190 | with self.assertRaisesRegex(InitializationError, 191 | r"Invalid config: Invalid assignment " 192 | r"at list u?'one' line 1: " 193 | r"u?'max_length=string' \(invalid literal.*\)"): 194 | load_data() 195 | 196 | # max_length after some words are defined 197 | open_mock.return_value = StringIO('something\nmax_length=9\n') 198 | with self.assertRaisesRegex(InitializationError, 199 | r"Invalid config: Invalid assignment " 200 | r"at list u?'one' line 2: " 201 | r"u?'max_length=9' \(options must be defined before words\)"): 202 | load_data() 203 | 204 | 205 | @patch('codecs.open') 206 | @patch('os.path.isdir', return_value=True) 207 | @patch('os.listdir', return_value=['one.txt']) 208 | def test_max_length_in_txt(self, mock1, mock2, open_mock): 209 | # Valid option max_length 210 | open_mock.return_value = StringIO('max_length=5\nabcde\nabcdef\nabc\n') 211 | with self.assertRaisesRegex(InitializationError, 212 | r"Invalid config: Word is too long " 213 | r"at list u?'one' line 3: u?'abcdef'"): 214 | _load_data(NO_DATA_DIR) 215 | 216 | @patch('codecs.open') 217 | @patch('os.path.isdir', return_value=True) 218 | @patch('os.listdir', return_value=['one.txt']) 219 | def test_number_of_words_in_txt(self, mock1, mock2, open_mock): 220 | open_mock.return_value = StringIO('number_of_words=2\none two\nathree four\nfive\nsix\n') 221 | with self.assertRaisesRegex(InitializationError, 222 | r"Invalid config: Phrase has 1 word\(s\) \(while number_of_words=2\) " 223 | r"at list u?'one' line 4: u?'five'"): 224 | _load_data(NO_DATA_DIR) 225 | 226 | 227 | if __name__ == '__main__': 228 | import sys 229 | sys.exit(unittest.main()) 230 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # coolname documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Apr 23 13:03:36 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.intersphinx', 35 | 'sphinxcontrib.fulltoc', 36 | ] 37 | 38 | intersphinx_mapping = {'python': ('https://docs.python.org/3/', None)} 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | #source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = 'coolname' 56 | copyright = '2018, Alexander Lukanin' 57 | author = 'Alexander Lukanin' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = '1.0' 65 | # The full version, including alpha/beta/rc tags. 66 | release = '2.2.0' 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = 'en' 74 | 75 | # There are two options for replacing |today|: either, you set today to some 76 | # non-false value, then it is used: 77 | #today = '' 78 | # Else, today_fmt is used as the format for a strftime call. 79 | #today_fmt = '%B %d, %Y' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | exclude_patterns = ['_build'] 84 | 85 | # The reST default role (used for this markup: `text`) to use for all 86 | # documents. 87 | #default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | #add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | #add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | #show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = 'sphinx' 102 | 103 | # A list of ignored prefixes for module index sorting. 104 | #modindex_common_prefix = [] 105 | 106 | # If true, keep warnings as "system message" paragraphs in the built documents. 107 | #keep_warnings = False 108 | 109 | # If true, `todo` and `todoList` produce output, else they produce nothing. 110 | todo_include_todos = False 111 | 112 | 113 | # -- Options for HTML output ---------------------------------------------- 114 | 115 | # The theme to use for HTML and HTML Help pages. See the documentation for 116 | # a list of builtin themes. 117 | html_theme = 'alabaster' 118 | 119 | # Theme options are theme-specific and customize the look and feel of a theme 120 | # further. For a list of options available for each theme, see the 121 | # documentation. 122 | #html_theme_options = {} 123 | 124 | # Add any paths that contain custom themes here, relative to this directory. 125 | #html_theme_path = [] 126 | 127 | # The name for this set of Sphinx documents. If None, it defaults to 128 | # " v documentation". 129 | #html_title = None 130 | 131 | # A shorter title for the navigation bar. Default is the same as html_title. 132 | #html_short_title = None 133 | 134 | # The name of an image file (relative to this directory) to place at the top 135 | # of the sidebar. 136 | #html_logo = None 137 | 138 | # The name of an image file (within the static path) to use as favicon of the 139 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 140 | # pixels large. 141 | #html_favicon = None 142 | 143 | # Add any paths that contain custom static files (such as style sheets) here, 144 | # relative to this directory. They are copied after the builtin static files, 145 | # so a file named "default.css" will overwrite the builtin "default.css". 146 | html_static_path = ['_static'] 147 | 148 | # Add any extra paths that contain custom files (such as robots.txt or 149 | # .htaccess) here, relative to this directory. These files are copied 150 | # directly to the root of the documentation. 151 | #html_extra_path = [] 152 | 153 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 154 | # using the given strftime format. 155 | #html_last_updated_fmt = '%b %d, %Y' 156 | 157 | # If true, SmartyPants will be used to convert quotes and dashes to 158 | # typographically correct entities. 159 | #html_use_smartypants = True 160 | 161 | # Custom sidebar templates, maps document names to template names. 162 | #html_sidebars = {} 163 | 164 | # Additional templates that should be rendered to pages, maps page names to 165 | # template names. 166 | #html_additional_pages = {} 167 | 168 | # If false, no module index is generated. 169 | #html_domain_indices = True 170 | 171 | # If false, no index is generated. 172 | #html_use_index = True 173 | 174 | # If true, the index is split into individual pages for each letter. 175 | #html_split_index = False 176 | 177 | # If true, links to the reST sources are added to the pages. 178 | #html_show_sourcelink = True 179 | 180 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 181 | #html_show_sphinx = True 182 | 183 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 184 | #html_show_copyright = True 185 | 186 | # If true, an OpenSearch description file will be output, and all pages will 187 | # contain a tag referring to it. The value of this option must be the 188 | # base URL from which the finished HTML is served. 189 | #html_use_opensearch = '' 190 | 191 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 192 | #html_file_suffix = None 193 | 194 | # Language to be used for generating the HTML full-text search index. 195 | # Sphinx supports the following languages: 196 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 197 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 198 | #html_search_language = 'en' 199 | 200 | # A dictionary with options for the search language support, empty by default. 201 | # Now only 'ja' uses this config value 202 | #html_search_options = {'type': 'default'} 203 | 204 | # The name of a javascript file (relative to the configuration directory) that 205 | # implements a search results scorer. If empty, the default will be used. 206 | #html_search_scorer = 'scorer.js' 207 | 208 | # Output file base name for HTML help builder. 209 | htmlhelp_basename = 'coolnamedoc' 210 | 211 | # -- Options for LaTeX output --------------------------------------------- 212 | 213 | latex_elements = { 214 | # The paper size ('letterpaper' or 'a4paper'). 215 | #'papersize': 'letterpaper', 216 | 217 | # The font size ('10pt', '11pt' or '12pt'). 218 | #'pointsize': '10pt', 219 | 220 | # Additional stuff for the LaTeX preamble. 221 | #'preamble': '', 222 | 223 | # Latex figure (float) alignment 224 | #'figure_align': 'htbp', 225 | } 226 | 227 | # Grouping the document tree into LaTeX files. List of tuples 228 | # (source start file, target name, title, 229 | # author, documentclass [howto, manual, or own class]). 230 | latex_documents = [ 231 | (master_doc, 'coolname.tex', 'coolname Documentation', 232 | 'Alexander Lukanin', 'manual'), 233 | ] 234 | 235 | # The name of an image file (relative to this directory) to place at the top of 236 | # the title page. 237 | #latex_logo = None 238 | 239 | # For "manual" documents, if this is true, then toplevel headings are parts, 240 | # not chapters. 241 | #latex_use_parts = False 242 | 243 | # If true, show page references after internal links. 244 | #latex_show_pagerefs = False 245 | 246 | # If true, show URL addresses after external links. 247 | #latex_show_urls = False 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #latex_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #latex_domain_indices = True 254 | 255 | 256 | # -- Options for manual page output --------------------------------------- 257 | 258 | # One entry per manual page. List of tuples 259 | # (source start file, name, description, authors, manual section). 260 | man_pages = [ 261 | (master_doc, 'coolname', 'coolname Documentation', 262 | [author], 1) 263 | ] 264 | 265 | # If true, show URL addresses after external links. 266 | #man_show_urls = False 267 | 268 | 269 | # -- Options for Texinfo output ------------------------------------------- 270 | 271 | # Grouping the document tree into Texinfo files. List of tuples 272 | # (source start file, target name, title, author, 273 | # dir menu entry, description, category) 274 | texinfo_documents = [ 275 | (master_doc, 'coolname', 'coolname Documentation', 276 | author, 'coolname', 'One line description of project.', 277 | 'Miscellaneous'), 278 | ] 279 | 280 | # Documents to append as an appendix to all manuals. 281 | #texinfo_appendices = [] 282 | 283 | # If false, no module index is generated. 284 | #texinfo_domain_indices = True 285 | 286 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 287 | #texinfo_show_urls = 'footnote' 288 | 289 | # If true, do not generate a @detailmenu in the "Top" node's menu. 290 | #texinfo_no_detailmenu = False 291 | -------------------------------------------------------------------------------- /docs/customization.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Custom generators 3 | ================= 4 | 5 | .. py:currentmodule:: coolname 6 | 7 | .. _configuration-rules: 8 | 9 | Configuration rules 10 | =================== 11 | 12 | Configuration is a flat dictionary of rules: 13 | 14 | .. code-block:: python 15 | 16 | { 17 | '': { 18 | 'comment': 'Some info about this rule. Not mandatory.', 19 | 'type': '', 20 | # additional fields, depending on type 21 | }, 22 | ... 23 | } 24 | 25 | ```` is the identifier of rule. Root rule must be named ``'all'`` - that's what you use 26 | when you call :func:`generate` or :func:`generate_slug` without arguments. 27 | 28 | There are five types of configuration rules. 29 | 30 | Words list 31 | ---------- 32 | 33 | A ground-level building block. Chooses a random word from a list, 34 | with equal probability. 35 | 36 | .. code-block:: python 37 | 38 | # This will produce random color 39 | 'color': { 40 | 'type': 'words', 41 | 'words': ['red', 'green', 'yellow'] 42 | }, 43 | # This will produce random taste 44 | 'taste': { 45 | 'type': 'words', 46 | 'words': ['sweet', 'sour'] 47 | }, 48 | # This will produce random fruit 49 | 'fruit': { 50 | 'type': 'words', 51 | 'words': ['apple', 'banana'] 52 | }, 53 | 54 | 55 | Phrases list 56 | ------------ 57 | 58 | Same as words list, but each element is one or more words. 59 | 60 | .. code-block:: python 61 | 62 | # This will produce random color 63 | 'color': { 64 | 'type': 'phrases', 65 | 'words': ['red', 'green', 'navy blue', ['royal', 'purple']] 66 | } 67 | 68 | Phrase can be written as a string (words are separated by space) or as a list of words. 69 | 70 | Nested list 71 | ----------- 72 | 73 | Chooses a random word (or phrase) from any of the child lists. 74 | Probability is proportional to child list length. 75 | 76 | .. code-block:: python 77 | 78 | # This will produce random adjective: color or taste 79 | 'adjective': { 80 | 'type': 'nested', 81 | 'lists': ['color', 'taste'] 82 | }, 83 | 84 | Child lists can be of any type. 85 | 86 | Number of child lists is not limited. 87 | 88 | Length of nested list is the sum of lengths of all child lists. 89 | 90 | Constant 91 | -------- 92 | 93 | It's just a word. Useful for prepositions. 94 | 95 | .. code-block:: python 96 | 97 | 'of': { 98 | 'type': 'const', 99 | 'value': 'of' 100 | }, 101 | 102 | Cartesian list 103 | --------------- 104 | 105 | Cartesian_ list works like a slot machine, and produces a list of length N 106 | by choosing one random word (or phrase) from every child list. 107 | 108 | .. code-block:: python 109 | 110 | # This will produce a random list of 4 words, 111 | # for example: ['my', 'banana', 'is', 'sweet'] 112 | 'all': { 113 | 'type': 'cartesian', 114 | 'lists': ['my', 'fruit', 'is', 'adjective'] 115 | }, 116 | # Additional const definitions 117 | 'is': { 118 | 'type': 'const', 119 | 'value': 'is' 120 | }, 121 | 'my': { 122 | 'type': 'const', 123 | 'value': 'my' 124 | }, 125 | 126 | Length of Cartesian list is the product of lengths of child lists. 127 | 128 | Let's try the config defined above: 129 | 130 | .. code-block:: python 131 | 132 | >>> from coolname import RandomGenerator 133 | >>> generator = RandomGenerator(config) 134 | >>> for i in range(3): 135 | ... print(generator.generate_slug()) 136 | ... 137 | my-banana-is-sweet 138 | my-apple-is-green 139 | my-apple-is-sour 140 | 141 | .. warning:: 142 | You can have many nested lists, but you should never put a Cartesian list inside another Cartesian list. 143 | 144 | .. _Cartesian: https://en.wikipedia.org/wiki/Cartesian_product 145 | 146 | Length limits 147 | ============= 148 | 149 | Number of characters 150 | -------------------- 151 | 152 | There are two limits: 153 | 154 | * ``max_length`` 155 | 156 | This constraint is hard: you can't create :class:`RandomGenerator` instance 157 | if some word (or phrase) in some rule exceeds that rule's limit. 158 | 159 | For example, this will fail: 160 | 161 | .. code-block:: json 162 | 163 | { 164 | "all": { 165 | "type": "words", 166 | "words": ["cat", "tiger", "jaguar"], 167 | "max_length": 5 168 | } 169 | } 170 | 171 | Different word lists and phrase lists can have different limits. 172 | If you don't specify it, there is no limit. 173 | 174 | *Note: when max_length is applied to phrase lists, spaces are not counted. So this will work:* 175 | 176 | .. code-block:: json 177 | 178 | { 179 | "all": { 180 | "type": "phrases", 181 | "phrases": ["big cat"], 182 | "max_length": 6 183 | } 184 | } 185 | 186 | * ``max_slug_length`` 187 | 188 | This constraint is soft: if result is too long, it is silently discarded 189 | and generator rolls the dice again. 190 | This allows you to have longer-than-average words (and phrases) which 191 | still fit nicely with shorter words (and phrases) from other lists. 192 | 193 | Of course, it's better to keep the fraction of "too long" combinations low, 194 | as it affects the performance. In fact, :class:`RandomGenerator` performs 195 | a sanity test upon initialization: if probability of getting "too long" combination 196 | is unacceptable, it will raise an exception. 197 | 198 | For example, this will produce 7 possible combinations, 199 | and 2 combinations (green-square and green-circle) will never appear 200 | because they exceed the max slug length: 201 | 202 | .. code-block:: json 203 | 204 | { 205 | "adjective": { 206 | "type": "words", 207 | "words": ["red", "blue", "green"] 208 | }, 209 | "noun": { 210 | "type": "words", 211 | "words": ["line", "square", "circle"] 212 | }, 213 | "all": { 214 | "type": "cartesian", 215 | "lists": ["adjective", "noun"], 216 | "max_slug_length": 11 217 | } 218 | } 219 | 220 | Both of these limits are optional. Default configuration uses ``max_slug_length = 50`` 221 | according to Django slug length. 222 | 223 | Number of words 224 | --------------- 225 | 226 | Use ``number_of_words`` parameter to enforce particular number of words in a phrase for a given list. 227 | 228 | This constraint is hard: you can't create :class:`RandomGenerator` instance 229 | if some phrase in a given list has a wrong number of words. 230 | 231 | For example, this will fail because the last item has 3 words: 232 | 233 | .. code-block:: json 234 | :emphasize-lines: 8,10 235 | 236 | { 237 | "all": { 238 | "type": "phrases", 239 | "phrases": [ 240 | "washing machine", 241 | "microwave oven", 242 | "vacuum cleaner", 243 | "large hadron collider" 244 | ], 245 | "number_of_words": 2 246 | } 247 | } 248 | 249 | Configuration files 250 | =================== 251 | 252 | Another small example: a pair of (adjective, noun) generated as follows: :: 253 | 254 | (crouching|hidden) (tiger|dragon) 255 | 256 | Of course, you can just feed config dict into :class:`RandomGenerator` constructor: 257 | 258 | .. code-block:: python 259 | 260 | >>> from coolname import RandomGenerator 261 | >>> config = {'all': {'type': 'cartesian', 'lists': ['adjective', 'noun']}, 'adjective': {'type':'words', 'words':['crouching','hidden']}, 'noun': {'type': 'words', 'words': ['tiger', 'dragon']}} 262 | >>> g = RandomGenerator(config) 263 | >>> g.generate_slug() 264 | 'hidden-dragon' 265 | 266 | but it becomes inconvenient as number of words grows. So, :mod:`coolname` can also use a mixed files format: 267 | you can specify rules in JSON file, and encapsulate long word lists into separate plain txt files 268 | (one file per one ``"words"`` rule). 269 | 270 | For our example, we would need three files in a directory: 271 | 272 | **my_config/config.json** 273 | 274 | .. code-block:: json 275 | 276 | { 277 | "all": { 278 | "type": "cartesian", 279 | "lists": ["adjective", "noun"] 280 | } 281 | } 282 | 283 | **my_config/adjective.txt** :: 284 | 285 | crouching 286 | hidden 287 | 288 | **my_config/noun.txt** :: 289 | 290 | dragon 291 | tiger 292 | 293 | *Note: only config.json is mandatory; you can name other files as you want.* 294 | 295 | Use auxiliary function to load config from a directory: 296 | 297 | .. code-block:: python 298 | 299 | >>> from coolname.loader import load_config 300 | >>> config = load_config('./my_config') 301 | 302 | That's all! Now loaded config contains all the same rules and we can create :class:`RandomGenerator` object: 303 | 304 | .. code-block:: python 305 | 306 | >>> config 307 | {'adjective': {'words': ['crouching', 'hidden'], 'type': 'words'}, 'noun': {'words': ['dragon', 'tiger'], 'type': 'words'}, 'all': {'lists': ['adjective', 'noun'], 'type': 'cartesian'}} 308 | >>> g = RandomGenerator(config) 309 | >>> g.generate_slug() 310 | 'hidden-tiger' 311 | 312 | Text file format for words 313 | --------------------------- 314 | 315 | Basic format is simple: :: 316 | 317 | # comment 318 | one 319 | two # inline comment 320 | 321 | # blank lines are OK 322 | three 323 | 324 | You can also specify options like this: :: 325 | 326 | max_length = 13 327 | 328 | Which is equivalent to adding the same option in config dictionary: 329 | 330 | .. code-block:: python 331 | :emphasize-lines: 4 332 | 333 | { 334 | 'type': 'words', 335 | 'words': ['one', 'two', 'three'], 336 | 'max_length': 13 337 | } 338 | 339 | Options should be placed in the beginning of the text file, before the first word. 340 | 341 | Text file format for phrases 342 | ----------------------------- 343 | 344 | For phrases, format is the same as for words. If any line in a file has more than one word, 345 | the whole file is automagically transformed to a ``"phrases"`` list instead of ``"words"``. 346 | 347 | For example, this file: :: 348 | 349 | one 350 | two 351 | 352 | # Here is the phrase 353 | three four 354 | 355 | is translated to the following rule: 356 | 357 | .. code-block:: json 358 | 359 | { 360 | "type": "phrases", 361 | "phrases": [ 362 | ["one"], ["two"], ["three", "four"] 363 | ] 364 | } 365 | 366 | Unicode support 367 | =============== 368 | 369 | Default implementation uses English, but you can create configuration in any language - 370 | just save the config files in UTF-8 encoding. 371 | -------------------------------------------------------------------------------- /tests/test_coolname.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from itertools import cycle 3 | import random 4 | import sys 5 | import unittest 6 | import warnings 7 | 8 | import coolname 9 | from coolname import RandomGenerator, InitializationError 10 | from coolname.exceptions import ConfigurationError 11 | from coolname.loader import load_config 12 | 13 | from .common import patch, TestCase, FakeRandom 14 | 15 | 16 | class TestCoolname(TestCase): 17 | 18 | def test_slug(self): 19 | # Basic test, to check that it doesn't crash. 20 | # Output of default generator is always unicode. 21 | items = coolname.generate() 22 | self.assertIsInstance(items[0], str) 23 | name = coolname.generate_slug() 24 | self.assertIsInstance(name, str) 25 | self.assertGreater(len(name), 10) 26 | self.assertIn('-', name) 27 | 28 | def test_combinations(self): 29 | combinations_2 = 10**5 30 | combinations_3 = 10**8 31 | combinations_4 = 10**10 32 | self.assertGreater(coolname.get_combinations_count(), combinations_4) 33 | self.assertGreater(coolname.get_combinations_count(2), combinations_2) 34 | self.assertGreater(coolname.get_combinations_count(3), combinations_3) 35 | self.assertGreater(coolname.get_combinations_count(4), combinations_4) 36 | self.assertLess(coolname.get_combinations_count(3), 37 | coolname.get_combinations_count()) 38 | self.assertLess(coolname.get_combinations_count(4), 39 | coolname.get_combinations_count()) 40 | self.assertEqual(coolname.get_combinations_count(2) + 41 | coolname.get_combinations_count(3) + 42 | coolname.get_combinations_count(4), 43 | coolname.get_combinations_count()) 44 | 45 | @patch('os.path.isdir', return_value=False) 46 | @patch('os.path.isfile', return_value=False) 47 | def test_create_from_file_not_found(self, *args): 48 | with self.assertRaisesRegex(InitializationError, 49 | r'File or directory not found: .*dummy'): 50 | RandomGenerator(load_config('dummy')) 51 | 52 | @patch('os.path.isdir', return_value=False) 53 | @patch('os.path.isfile', return_value=True) 54 | @patch('coolname.loader._load_config') 55 | def test_create_from_file(self, load_config_mock, *args): 56 | load_config_mock.return_value = { 57 | 'all': { 58 | 'type': 'cartesian', 59 | 'lists': ['number', 'number'] 60 | }, 61 | 'number': { 62 | 'type': 'words', 63 | 'words': [str(x) for x in range(0, 10)] 64 | } 65 | } 66 | generator = RandomGenerator(load_config('dummy')) 67 | with patch.object(generator, '_randrange', return_value=35): 68 | self.assertEqual(generator.generate_slug(), '3-5') 69 | 70 | @patch('os.path.isdir', return_value=True) 71 | @patch('os.path.isfile', return_value=False) 72 | @patch('coolname.loader._load_data') 73 | def test_create_from_directory_conflict(self, load_data_mock, *args): 74 | load_data_mock.return_value = ( 75 | { 76 | 'all': { 77 | 'type': 'cartesian', 78 | 'lists': ['mywords'] 79 | }, 80 | 'mywords': { 81 | 'type': 'words', 82 | 'words': ['this', 'is', 'a', 'conflict'] 83 | } 84 | }, 85 | {'mywords': ['a', 'b']}) 86 | with self.assertRaisesRegex(InitializationError, 87 | r"^Conflict: list 'mywords' is defined both in config " 88 | "and in \*\.txt file. If it's a 'words' list, " 89 | "you should remove it from config\.$"): 90 | RandomGenerator(load_config('dummy')) 91 | 92 | def test_generate_by_pattern(self): 93 | generator = RandomGenerator({ 94 | 'all': { 95 | 'type': 'cartesian', 96 | 'lists': ['size', 'color', 'fruit'], 97 | }, 98 | 'justcolor': { 99 | 'generator': True, 100 | 'type': 'cartesian', 101 | 'lists': ['color', 'fruit'], 102 | }, 103 | 'size': { 104 | 'type': 'words', 105 | 'words': ['small', 'large'] 106 | }, 107 | 'color': { 108 | 'type': 'words', 109 | 'words': ['green', 'yellow'] 110 | }, 111 | 'fruit': { 112 | 'type': 'words', 113 | 'words': ['apple', 'banana'] 114 | }, 115 | }) 116 | with patch.object(generator, '_randrange', return_value=0): 117 | self.assertEqual(generator.generate_slug(), 'small-green-apple') 118 | self.assertEqual(generator.generate_slug('justcolor'), 'green-apple') 119 | 120 | def test_unicode_config(self): 121 | generator = RandomGenerator({ 122 | 'all': { 123 | 'type': 'cartesian', 124 | 'lists': ['прилагательное', 'существительное'] 125 | }, 126 | 'прилагательное': { 127 | 'type': 'words', 128 | 'words': ['белый', 'черный'] 129 | }, 130 | 'существительное': { 131 | 'type': 'words', 132 | 'words': ['круг', 'квадрат'] 133 | } 134 | }) 135 | with patch.object(generator, '_randrange', 136 | side_effect=partial(next, cycle(iter(range(4))))): 137 | self.assertEqual(generator.generate_slug(), 'белый-круг') 138 | self.assertEqual(generator.generate_slug(), 'белый-квадрат') 139 | self.assertEqual(generator.generate_slug(), 'черный-круг') 140 | self.assertEqual(generator.generate(), ['черный', 'квадрат']) 141 | 142 | def test_ensure_unique(self): 143 | # Test without ensure_unique - should yield repeats 144 | config = { 145 | 'all': { 146 | 'type': 'cartesian', 147 | 'lists': ['adjective', 'of', 'noun'], 148 | }, 149 | 'adjective': { 150 | 'type': 'words', 151 | 'words': ['one', 'two'] 152 | }, 153 | 'of': { 154 | 'type': 'const', 155 | 'value': 'of' 156 | }, 157 | 'noun': { 158 | 'type': 'words', 159 | 'words': ['one', 'two'] 160 | } 161 | } 162 | generator = RandomGenerator(config) 163 | with patch.object(generator, '_randrange', 164 | side_effect=partial(next, cycle(iter([0, 1, 2, 3])))): 165 | self.assertEqual(generator.generate_slug(), 'one-of-one') 166 | self.assertEqual(generator.generate_slug(), 'one-of-two') 167 | self.assertEqual(generator.generate_slug(), 'two-of-one') 168 | self.assertEqual(generator.generate_slug(), 'two-of-two') 169 | self.assertEqual(generator.generate_slug(), 'one-of-one') 170 | # Invalid ensure_unique 171 | config['all']['ensure_unique'] = 'qwe' 172 | with self.assertRaisesRegex(ConfigurationError, "Invalid config: Invalid ensure_unique value: expected boolean, got 'qwe'"): 173 | RandomGenerator(config) 174 | # Test with ensure_unique 175 | config['all']['ensure_unique'] = True 176 | with warnings.catch_warnings(record=True) as w: 177 | generator = RandomGenerator(config) 178 | if len(w) > 0: 179 | assert len(w) == 1 180 | assert str(w[0].message) == 'coolname.generate() may be slow because a significant fraction of combinations contain repeating words and ensure_unique is set' 181 | with patch.object(generator, '_randrange', 182 | side_effect=partial(next, cycle(iter([0, 1, 2, 3])))): 183 | self.assertEqual(generator.generate_slug(), 'one-of-two') 184 | self.assertEqual(generator.generate_slug(), 'two-of-one') 185 | self.assertEqual(generator.generate_slug(), 'one-of-two') 186 | self.assertEqual(generator.generate_slug(), 'two-of-one') 187 | 188 | def test_ensure_unique_error(self): 189 | config = { 190 | 'all': {'type': 'cartesian', 'lists': ['one', 'one']}, 191 | 'one': {'type': 'words', 'words': ['one', 'one']} 192 | } 193 | RandomGenerator(config) # this is fine 194 | config['all']['ensure_unique'] = True 195 | with self.assertRaisesRegex(ConfigurationError, r'Invalid config: Impossible to generate with ensure_unique'): 196 | RandomGenerator(config) 197 | 198 | def test_ensure_unique_error_on_list(self): 199 | config = { 200 | 'all': {'type': 'cartesian', 'lists': ['one', 'two']}, 201 | 'bad': {'type': 'cartesian', 'generator': True, 'lists': ['one', 'one']}, 202 | 'one': {'type': 'words', 'words': ['one', 'one']}, 203 | 'two': {'type': 'words', 'words': ['two', 'two']} 204 | } 205 | RandomGenerator(config) # this is fine 206 | config['all']['ensure_unique'] = True 207 | with self.assertRaisesRegex(ConfigurationError, r'Invalid config: Impossible to generate with ensure_unique'): 208 | RandomGenerator(config) 209 | 210 | 211 | def test_ensure_unique_prefix(self): 212 | config = { 213 | 'all': { 214 | 'type': 'cartesian', 215 | 'lists': ['w1', 'w2'], 216 | }, 217 | 'w1': { 218 | 'type': 'words', 219 | 'words': ['brave', 'agile'] 220 | }, 221 | 'w2': { 222 | 'type': 'words', 223 | 'words': ['bravery', 'brass', 'agility', 'age'] 224 | } 225 | } 226 | generator = RandomGenerator(config) 227 | with patch.object(generator, '_randrange', 228 | side_effect=partial(next, cycle(iter(range(8))))): 229 | self.assertEqual(generator.generate_slug(), 'brave-bravery') # This sucks 230 | 231 | # ensure_unique_prefix = 0 is not allowed 232 | config['all']['ensure_unique_prefix'] = 0 233 | with self.assertRaisesRegex(ConfigurationError, 'Invalid config: Invalid ensure_unique_prefix value: expected a positive integer, got 0'): 234 | RandomGenerator(config) 235 | 236 | # Now enable unique prefix 237 | config['all']['ensure_unique_prefix'] = 4 238 | generator = RandomGenerator(config) 239 | with patch.object(generator, '_randrange', 240 | side_effect=partial(next, cycle(iter(range(8))))): 241 | self.assertEqual(generator.generate_slug(), 'brave-brass') 242 | self.assertEqual(generator.generate_slug(), 'brave-agility') 243 | self.assertEqual(generator.generate_slug(), 'brave-age') 244 | self.assertEqual(generator.generate_slug(), 'agile-bravery') 245 | self.assertEqual(generator.generate_slug(), 'agile-brass') 246 | self.assertEqual(generator.generate_slug(), 'agile-age') 247 | self.assertEqual(generator.generate_slug(), 'brave-brass') 248 | 249 | def test_configuration_error(self): 250 | with self.assertRaisesRegex(InitializationError, 251 | "Invalid config: Value at key 'all' is not a dict"): 252 | RandomGenerator({'all': ['wrong']}) 253 | with self.assertRaisesRegex(InitializationError, 254 | "Invalid config: Config at key 'all' has no 'type'"): 255 | RandomGenerator({'all': {'typ': 'wrong'}}) 256 | with self.assertRaisesRegex(InitializationError, 257 | "Invalid config: Config at key 'all' has invalid 'type'"): 258 | RandomGenerator({'all': {'type': 'wrong'}}) 259 | with self.assertRaisesRegex(InitializationError, 260 | "Invalid config: Config at key 'all' has no 'lists'"): 261 | RandomGenerator({'all': {'type': 'nested'}}) 262 | with self.assertRaisesRegex(InitializationError, 263 | "Invalid config: Config at key 'all' has invalid 'lists'"): 264 | RandomGenerator({'all': {'type': 'nested', 'lists': 'wrong'}}) 265 | with self.assertRaisesRegex(InitializationError, 266 | "Invalid config: Config at key 'all' has no 'value'"): 267 | RandomGenerator({'all': {'type': 'const'}}) 268 | with self.assertRaisesRegex(InitializationError, 269 | "Invalid config: Config at key 'all' has invalid 'value'"): 270 | RandomGenerator({'all': {'type': 'const', 'value': 123}}) 271 | with self.assertRaisesRegex(InitializationError, 272 | "Invalid config: Config at key 'all' has no 'words'"): 273 | RandomGenerator({'all': {'type': 'words'}}) 274 | with self.assertRaisesRegex(InitializationError, 275 | "Invalid config: Config at key 'all' has invalid 'words'"): 276 | RandomGenerator({'all': {'type': 'words', 'words': []}}) 277 | with self.assertRaisesRegex(InitializationError, 278 | "Invalid config: Lists are referenced but not defined: one, two"): 279 | RandomGenerator({'all': {'type': 'nested', 'lists': ['one', 'two']}}) 280 | with self.assertRaisesRegex(InitializationError, 281 | "Invalid config: Rule 'all' is recursive: \['all', 'one'\]"): 282 | RandomGenerator({ 283 | 'all': {'type': 'nested', 'lists': ['one']}, 284 | 'one': {'type': 'nested', 'lists': ['all']} 285 | }) 286 | 287 | def test_configuration_error_phrases(self): 288 | with self.assertRaisesRegex(InitializationError, 289 | "Invalid config: Config at key 'all' has no 'phrases'"): 290 | RandomGenerator({'all': {'type': 'phrases', 'words': []}}) 291 | with self.assertRaisesRegex(InitializationError, 292 | "Invalid config: Config at key 'all' has invalid 'phrases'"): 293 | RandomGenerator({'all': {'type': 'phrases', 'phrases': []}}) 294 | generator = RandomGenerator({'all': {'type': 'phrases', 'phrases': ['str is allowed']}}) 295 | assert generator.generate_slug() == 'str-is-allowed' 296 | with self.assertRaisesRegex(InitializationError, 297 | "Invalid config: Config at key 'all' has invalid 'phrases': must be all string/tuple/list"): 298 | RandomGenerator({'all': {'type': 'phrases', 'phrases': [[['too many square brackets']]]}}) 299 | # Number of words 300 | RandomGenerator({ 301 | 'all': { 302 | 'type': 'phrases', 303 | 'number_of_words': 2, 304 | 'phrases': [['one', 'two'], ['three', 'four']]} 305 | }) 306 | with self.assertRaisesRegex(InitializationError, 307 | "Invalid config: Config at key 'all' has invalid phrase 'five' \(1 word\(s\) but number_of_words=2\)"): 308 | RandomGenerator({ 309 | 'all': { 310 | 'type': 'phrases', 311 | 'number_of_words': 2, 312 | 'phrases': [['one', 'two'], ['three', 'four'], ['five']]} 313 | }) 314 | # Max length 315 | RandomGenerator({ 316 | 'all': { 317 | 'type': 'phrases', 318 | 'max_length': 10, 319 | 'phrases': [['black', 'goose'], ['white', 'hare']]} 320 | }) 321 | with self.assertRaisesRegex(InitializationError, 322 | "Invalid config: Config at key 'all' has invalid phrase 'white rabbit' \(longer than 10 characters\)"): 323 | RandomGenerator({ 324 | 'all': { 325 | 'type': 'phrases', 326 | 'max_length': 10, 327 | 'phrases': [['black', 'goose'], ['white', 'rabbit']]} 328 | }) 329 | 330 | def test_max_length(self): 331 | with self.assertRaisesRegex(InitializationError, 332 | "Config at key 'one' has invalid word 'tiger' " 333 | "\(longer than 4 characters\)"): 334 | RandomGenerator({ 335 | 'all': {'type': 'nested', 'lists': ['one']}, 336 | 'one': {'type': 'words', 'max_length': 4, 'words': ['cat', 'lion', 'tiger']} 337 | }) 338 | 339 | def test_max_slug_length_invalid(self): 340 | with self.assertRaisesRegex(InitializationError, 341 | r'Invalid config: Invalid max_slug_length value'): 342 | RandomGenerator({ 343 | 'all': {'type': 'words', 'max_slug_length': 'invalid', 'words': ['one', 'two']}, 344 | }) 345 | 346 | def test_max_slug_length(self): 347 | with warnings.catch_warnings(record=True) as w: 348 | generator = RandomGenerator({ 349 | 'all': {'type': 'cartesian', 'max_slug_length': 9, 'lists': ['one', 'two']}, 350 | 'one': {'type': 'words', 'words': ['big', 'small']}, 351 | 'two': {'type': 'words', 'words': ['cat', 'tiger']}, 352 | }) 353 | if len(w) > 0: 354 | assert len(w) == 1 355 | assert str(w[0].message) == 'coolname.generate() may be slow because a significant fraction of combinations exceed max_slug_length=9' 356 | self.assertEqual(set(generator.generate_slug() for i in range(0, 100)), 357 | set(['big-cat', 'big-tiger', 'small-cat'])) 358 | 359 | def test_max_slug_length_too_small(self): 360 | badlist = [str(i) for i in range(10, 100)] 361 | with self.assertRaisesRegex(InitializationError, 362 | r'Invalid config: Impossible to generate ' 363 | r'with max_slug_length=3'): 364 | RandomGenerator({ 365 | 'all': {'type': 'cartesian', 'max_slug_length': 3, 'lists': ['one', 'two']}, 366 | 'one': {'type': 'words', 'words': badlist}, 367 | 'two': {'type': 'words', 'words': badlist}, 368 | }) 369 | 370 | @patch('warnings.warn') 371 | def test_max_slug_length_warning(self, warn_mock): 372 | RandomGenerator({ 373 | 'all': {'type': 'cartesian', 'max_slug_length': 3, 'lists': ['one', 'two']}, 374 | 'one': {'type': 'words', 'words': ['a']*70 + ['bb']*30}, 375 | 'two': {'type': 'words', 'words': ['c']*70 + ['dd']*30}, 376 | }) 377 | warn_mock.assert_called_with('coolname.generate() may be slow because a significant ' 378 | 'fraction of combinations exceed max_slug_length=3') 379 | 380 | def test_configuration_error_too_deep(self): 381 | config = { 382 | 'all': { 383 | 'type': 'nested', 384 | 'lists': ['list0'] 385 | }, 386 | 'list100': { 387 | 'type': 'words', 388 | 'words': ['too', 'deep', 'for', 'you'], 389 | } 390 | } 391 | for i in range(100): 392 | config['list{}'.format(i)] = {'type': 'nested', 'lists': ['list{}'.format(i+1)]} 393 | with self.assertRaisesRegex(InitializationError, 394 | "Invalid config: Rule 'all' is too deep"): 395 | RandomGenerator(config) 396 | 397 | 398 | @patch('coolname.impl.randrange', side_effect=partial(next, cycle(iter(range(8))))) 399 | def test_configuration_error_cartesian_inside_cartesian(self, mock): 400 | config = { 401 | 'all': { 402 | 'type': 'cartesian', 403 | 'lists': ['word_list', 'cart_list'] 404 | }, 405 | 'word_list': { 406 | 'type': 'words', 407 | 'words': ['word1', 'word2'], 408 | }, 409 | 'cart_list': { 410 | 'type': 'cartesian', 411 | 'lists': ['word_list', 'word_list'], 412 | }, 413 | } 414 | with self.assertRaisesRegex(InitializationError, 415 | r"Invalid config: Cartesian list 'all' contains " 416 | r"another Cartesian list 'cart_list'\. Nested Cartesian lists " 417 | r"are not allowed\."): 418 | RandomGenerator(config) 419 | 420 | def test_mix_phrases_and_words_in_nested_list(self): 421 | config = { 422 | 'all': { 423 | 'type': 'cartesian', 424 | 'lists': ['a', 'nested'] 425 | }, 426 | 'a': { 427 | 'type': 'const', 428 | 'value': 'a' 429 | }, 430 | 'nested': { 431 | 'type': 'nested', 432 | 'lists': ['words', 'phrases'] 433 | }, 434 | 'words': { 435 | 'type': 'words', 436 | 'words': ['one', 'two'] 437 | }, 438 | 'phrases': { 439 | 'type': 'phrases', 440 | 'phrases': [ 441 | 'three four', # Can be space-separated string 442 | ['five', 'six'] # or a list/tuple 443 | ] 444 | } 445 | } 446 | generator = RandomGenerator(config) 447 | random.seed(0) 448 | values = set(generator.generate_slug() for i in range(28)) 449 | self.assertEqual(values, set(['a-one', 'a-two', 'a-three-four', 'a-five-six'])) 450 | 451 | # randrange returns different results in Python 2. We skip this test to avoid updating it every time. 452 | def test_random_default(self): 453 | # NOTE: two slugs in this test must be updated every time you change word lists 454 | 455 | # 1. Re-seed default generator 456 | random.seed(123) 457 | self.assertEqual(random.random(), 0.052363598850944326) 458 | self.assertEqual(coolname.generate_slug(), 'puzzling-jaguar-of-satisfying-advance') 459 | 460 | # 2. Replace default generator 461 | rand = random.Random() 462 | rand.seed(456) 463 | self.assertEqual(rand.random(), 0.7482025358782363) 464 | coolname.replace_random(rand) 465 | self.assertEqual(coolname.generate_slug(), 'excellent-brave-hedgehog-of-opportunity') 466 | 467 | # 3. Custom generator with custom Random 468 | config = { 469 | 'all': { 470 | 'type': 'cartesian', 471 | 'lists': ['digits', 'digits'] 472 | }, 473 | 'digits': { 474 | 'type': 'words', 475 | 'words': list(str(x) for x in range(10)) 476 | } 477 | } 478 | generator = RandomGenerator(config) 479 | generator.random.seed(12) 480 | self.assertEqual(generator.generate_slug(), '6-0') 481 | generator.random = FakeRandom(33) 482 | self.assertEqual(generator.generate_slug(), '3-4') 483 | 484 | @patch.object(sys, 'argv', ['coolname', '3', '-s', '_', '-n', '10']) 485 | def test_command_line(self, *args): 486 | from coolname.__main__ import main 487 | main() # just for the sake of coverage 488 | 489 | 490 | if __name__ == '__main__': 491 | sys.exit(unittest.main()) 492 | -------------------------------------------------------------------------------- /coolname/impl.py: -------------------------------------------------------------------------------- 1 | """ 2 | Do not import anything directly from this module. 3 | """ 4 | from functools import partial 5 | import hashlib 6 | import itertools 7 | import os 8 | import os.path as op 9 | import random 10 | from random import randrange 11 | import re 12 | from typing import List, Union 13 | 14 | from .config import _CONF 15 | from .exceptions import ConfigurationError, InitializationError 16 | 17 | # For new Python versions with (possible) OpenSSL FIPS support, 18 | # we should pass usedforsecurity=False argument to md5(). 19 | try: 20 | hashlib.md5(b'', usedforsecurity=False) # noqa 21 | _md5 = partial(hashlib.md5, usedforsecurity=False) 22 | except TypeError: 23 | _md5 = hashlib.md5 24 | 25 | 26 | class AbstractNestedList: 27 | 28 | def __init__(self, lists): 29 | super().__init__() 30 | self._lists = [WordList(x) if x.__class__ is list else x 31 | for x in lists] 32 | # If this is set to True in a subclass, 33 | # then subclass yields sequences instead of single words. 34 | self.multiword = any(x.multiword for x in self._lists) 35 | 36 | def __str__(self): 37 | return f'{self.__class__.__name__}({len(self._lists)}, len={self.length})' 38 | 39 | def __repr__(self): 40 | return self.__str__() 41 | 42 | def squash(self, hard, cache): 43 | if len(self._lists) == 1: 44 | return self._lists[0].squash(hard, cache) 45 | else: 46 | self._lists = [x.squash(hard, cache) for x in self._lists] 47 | return self 48 | 49 | def _dump(self, stream, indent='', object_ids=False): 50 | stream.write(indent + str(self) + 51 | (f' [id={id(self)}]' if object_ids else '') + 52 | '\n') 53 | indent += ' ' 54 | for sublist in self._lists: 55 | sublist._dump(stream, indent, object_ids=object_ids) # noqa 56 | 57 | 58 | # Convert value to bytes, for hashing 59 | # (used to calculate WordList or PhraseList hash) 60 | def _to_bytes(value): 61 | if isinstance(value, str): 62 | return value.encode('utf-8') 63 | elif isinstance(value, tuple): 64 | return str(value).encode('utf-8') 65 | else: 66 | return value 67 | 68 | 69 | class _BasicList(list, AbstractNestedList): 70 | 71 | def __init__(self, sequence=None): 72 | list.__init__(self, sequence) 73 | AbstractNestedList.__init__(self, []) 74 | self.length = len(self) 75 | self.__hash = None 76 | 77 | def __str__(self): 78 | ls = [repr(x) for x in self[:4]] 79 | if len(ls) == 4: 80 | ls[3] = '...' 81 | return '{}([{}], len={})'.format(self.__class__.__name__, ', '.join(ls), len(self)) 82 | 83 | def __repr__(self): 84 | return self.__str__() 85 | 86 | def squash(self, hard, cache): 87 | return self 88 | 89 | @property 90 | def _hash(self): 91 | if self.__hash: 92 | return self.__hash 93 | md5 = _md5() 94 | md5.update(_to_bytes(str(len(self)))) 95 | for x in self: # noqa 96 | md5.update(_to_bytes(x)) 97 | self.__hash = md5.digest() 98 | return self.__hash 99 | 100 | 101 | class WordList(_BasicList): 102 | """List of single words.""" 103 | 104 | 105 | class PhraseList(_BasicList): 106 | """List of phrases (sequences of one or more words).""" 107 | 108 | def __init__(self, sequence=None): 109 | super().__init__(tuple(_split_phrase(x)) for x in sequence) 110 | self.multiword = True 111 | 112 | 113 | class WordAsPhraseWrapper: 114 | 115 | multiword = True 116 | 117 | def __init__(self, wordlist): 118 | self._list = wordlist 119 | self.length = len(wordlist) 120 | 121 | def __len__(self): 122 | return self.length 123 | 124 | def __getitem__(self, i): 125 | return self._list[i], 126 | 127 | def squash(self, hard, cache): # noqa 128 | return self 129 | 130 | def __str__(self): 131 | return f'{self.__class__.__name__}({self._list})' 132 | 133 | def __repr__(self): 134 | return f'{self.__class__.__name__}({self._list!r})' 135 | 136 | 137 | class NestedList(AbstractNestedList): 138 | 139 | def __init__(self, lists): 140 | super().__init__(lists) 141 | # If user mixes WordList and PhraseList in the same NestedList, 142 | # we need to make sure that __getitem__ always returns tuple. 143 | # For that, we wrap WordList instances. 144 | if any(isinstance(x, WordList) for x in self._lists) and any(x.multiword for x in self._lists): 145 | self._lists = [WordAsPhraseWrapper(x) if isinstance(x, WordList) else x for x in self._lists] 146 | # Fattest lists first (to reduce average __getitem__ time) 147 | self._lists.sort(key=lambda x: -x.length) 148 | self.length = sum(x.length for x in self._lists) 149 | 150 | def __getitem__(self, i): 151 | # Retrieve item from appropriate list 152 | for x in self._lists: 153 | n = x.length 154 | if i < n: 155 | return x[i] 156 | else: 157 | i -= n 158 | raise IndexError('list index out of range') 159 | 160 | def squash(self, hard, cache): 161 | # Cache is used to avoid data duplication. 162 | # If we have 4 branches which finally point to the same list of nouns, 163 | # why not using the same WordList instance for all 4 branches? 164 | # This optimization is also applied to PhraseLists, just in case. 165 | result = super().squash(hard, cache) 166 | if result is self and hard: 167 | for cls in (WordList, PhraseList): 168 | if all(isinstance(x, cls) for x in self._lists): 169 | # Creating combined WordList/PhraseList and then checking cache 170 | # is a little wasteful, but it has no long-term consequences. 171 | # And it's simple! 172 | result = cls(sorted(set(itertools.chain.from_iterable(self._lists)))) 173 | if result._hash in cache: # noqa 174 | result = cache.get(result._hash) # noqa 175 | else: 176 | cache[result._hash] = result # noqa 177 | return result 178 | 179 | 180 | class CartesianList(AbstractNestedList): 181 | 182 | def __init__(self, lists): 183 | super().__init__(lists) 184 | self.length = 1 185 | for x in self._lists: 186 | self.length *= x.length 187 | # Let's say list lengths are 5, 7, 11, 13. 188 | # divs = [7*11*13, 11*13, 13, 1] 189 | divs = [1] 190 | prod = 1 191 | for x in reversed(self._lists[1:]): 192 | prod *= x.length 193 | divs.append(prod) 194 | self._list_divs = tuple(zip(self._lists, reversed(divs))) 195 | self.multiword = True 196 | 197 | def __getitem__(self, i): 198 | result = [] 199 | for sublist, n in self._list_divs: 200 | x = sublist[i // n] 201 | if sublist.multiword: 202 | result.extend(x) 203 | else: 204 | result.append(x) 205 | i %= n 206 | return result 207 | 208 | 209 | class Scalar(AbstractNestedList): 210 | 211 | def __init__(self, value): 212 | super().__init__([]) 213 | self.value = value 214 | self.length = 1 215 | 216 | def __getitem__(self, i): 217 | return self.value 218 | 219 | def __str__(self): 220 | return f'{self.__class__.__name__}(value={self.value!r})' 221 | 222 | def random(self): 223 | return self.value 224 | 225 | 226 | class RandomGenerator: 227 | """ 228 | This class provides random name generation interface. 229 | 230 | Create an instance of this class if you want to create custom 231 | configuration. 232 | If default implementation is enough, just use `generate`, 233 | `generate_slug` and other exported functions. 234 | """ 235 | 236 | def __init__(self, config, rand=None): 237 | self.random = rand # sets _random and _randrange 238 | config = dict(config) 239 | _validate_config(config) 240 | lists = {} 241 | _create_lists(config, lists, 'all', []) 242 | self._lists = {} 243 | for key, listdef in config.items(): 244 | # Other generators independent from 'all' 245 | if listdef.get(_CONF.FIELD.GENERATOR) and key not in lists: 246 | _create_lists(config, lists, key, []) 247 | if key == 'all' or key.isdigit() or listdef.get(_CONF.FIELD.GENERATOR): 248 | if key.isdigit(): 249 | pattern = int(key) 250 | elif key == 'all': 251 | pattern = None 252 | else: 253 | pattern = key 254 | self._lists[pattern] = lists[key] 255 | self._lists[None] = self._lists[None].squash(True, {}) 256 | # Should we avoid duplicates? 257 | try: 258 | ensure_unique = config['all'][_CONF.FIELD.ENSURE_UNIQUE] 259 | if not isinstance(ensure_unique, bool): 260 | raise ValueError(f'expected boolean, got {ensure_unique!r}') 261 | self._ensure_unique = ensure_unique 262 | except KeyError: 263 | self._ensure_unique = False 264 | except ValueError as ex: 265 | raise ConfigurationError(f'Invalid {_CONF.FIELD.ENSURE_UNIQUE} value: {ex}') 266 | # Should we avoid duplicating prefixes? 267 | try: 268 | self._check_prefix = int(config['all'][_CONF.FIELD.ENSURE_UNIQUE_PREFIX]) 269 | if self._check_prefix <= 0: 270 | raise ValueError(f'expected a positive integer, got {self._check_prefix!r}') 271 | except KeyError: 272 | self._check_prefix = None 273 | except ValueError as ex: 274 | raise ConfigurationError(f'Invalid {_CONF.FIELD.ENSURE_UNIQUE_PREFIX} value: {ex}') 275 | # Get max slug length 276 | try: 277 | self._max_slug_length = int(config['all'][_CONF.FIELD.MAX_SLUG_LENGTH]) 278 | except KeyError: 279 | self._max_slug_length = None 280 | except ValueError as ex: 281 | raise ConfigurationError(f'Invalid {_CONF.FIELD.MAX_SLUG_LENGTH} value: {ex}') 282 | # Make sure that generate() does not go into long loop. 283 | # Default generator is a special case, we don't need check. 284 | if (not config['all'].get('__nocheck') and 285 | self._ensure_unique or self._check_prefix or self._max_slug_length): 286 | self._check_not_hanging() 287 | # Fire it up 288 | assert self.generate_slug() 289 | 290 | @property 291 | def random(self): 292 | return self._random 293 | 294 | @random.setter 295 | def random(self, rand): 296 | if rand: 297 | self._random = rand 298 | else: 299 | self._random = random 300 | self._randrange = self._random.randrange 301 | 302 | def generate(self, pattern: Union[None, str, int] = None) -> List[str]: 303 | """ 304 | Generates and returns random name as a list of strings. 305 | """ 306 | lst = self._lists[pattern] 307 | while True: 308 | result = lst[self._randrange(lst.length)] 309 | # 1. Check that there are no duplicates 310 | # 2. Check that there are no duplicate prefixes 311 | # 3. Check max slug length 312 | n = len(result) 313 | if (self._ensure_unique and len(set(result)) != n or 314 | self._check_prefix and len(set(x[:self._check_prefix] for x in result)) != n or 315 | self._max_slug_length and sum(len(x) for x in result) + n - 1 > self._max_slug_length): 316 | continue 317 | return result 318 | 319 | def generate_slug(self, pattern: Union[None, str, int] = None) -> str: 320 | """ 321 | Generates and returns random name as a slug. 322 | """ 323 | return '-'.join(self.generate(pattern)) 324 | 325 | def get_combinations_count(self, pattern: Union[None, str, int] = None) -> int: 326 | """ 327 | Returns total number of unique combinations 328 | for the given pattern. 329 | """ 330 | lst = self._lists[pattern] 331 | return lst.length 332 | 333 | def _dump(self, stream, pattern=None, object_ids=False): 334 | """Dumps current tree into a text stream.""" 335 | return self._lists[pattern]._dump(stream, '', object_ids=object_ids) # noqa 336 | 337 | def _check_not_hanging(self): 338 | """ 339 | Rough check that generate() will not hang or be very slow. 340 | 341 | Raises ConfigurationError if generate() spends too much time in retry loop. 342 | Issues a warning.warn() if there is a risk of slowdown. 343 | """ 344 | # (field_name, predicate, warning_msg, exception_msg) 345 | # predicate(g) is a function that returns True if generated combination g must be rejected, 346 | # see checks in generate() 347 | checks = [] 348 | # ensure_unique can lead to infinite loops for some tiny erroneous configs 349 | if self._ensure_unique: 350 | checks.append(( 351 | _CONF.FIELD.ENSURE_UNIQUE, 352 | self._ensure_unique, 353 | lambda g: len(set(g)) != len(g), 354 | '{generate} may be slow because a significant fraction of combinations contain repeating words and {field_name} is set', # noqa 355 | 'Impossible to generate with {field_name}' 356 | )) 357 | # 358 | # max_slug_length can easily slow down or block generation if set too small 359 | if self._max_slug_length: 360 | checks.append(( 361 | _CONF.FIELD.MAX_SLUG_LENGTH, 362 | self._max_slug_length, 363 | lambda g: sum(len(x) for x in g) + len(g) - 1 > self._max_slug_length, 364 | '{generate} may be slow because a significant fraction of combinations exceed {field_name}={field_value}', # noqa 365 | 'Impossible to generate with {field_name}={field_value}' 366 | )) 367 | # Perform the relevant checks for all generators, starting from 'all' 368 | n = 100 369 | warning_threshold = 20 # fail probability: 0.04 for 2 attempts, 0.008 for 3 attempts, etc. 370 | for lst_id, lst in sorted(self._lists.items(), key=lambda x: '' if x is None else str(x)): 371 | context = {'generate': 'coolname.generate({})'.format('' if lst_id is None else repr(lst_id))} 372 | # For each generator, perform checks 373 | for field_name, field_value, predicate, warning_msg, exception_msg in checks: 374 | context.update({'field_name': field_name, 'field_value': field_value}) 375 | bad_count = 0 376 | for _ in range(n): 377 | if predicate(lst[randrange(lst.length)]): 378 | bad_count += 1 379 | if bad_count >= n: 380 | raise ConfigurationError(exception_msg.format(**context)) 381 | elif bad_count >= warning_threshold: 382 | import warnings 383 | warnings.warn(warning_msg.format(**context)) 384 | 385 | 386 | def _is_str(value): 387 | return value.__class__.__name__ in ('str', 'unicode') 388 | 389 | 390 | # Translate phrases defined as strings to tuples 391 | def _split_phrase(x): 392 | try: 393 | return re.split(r'\s+', x.strip()) 394 | except AttributeError: # Not str 395 | return x 396 | 397 | 398 | def _validate_config(config): 399 | """ 400 | A big and ugly method for config validation. 401 | It would be nice to use cerberus, but we don't 402 | want to introduce dependencies just for that. 403 | """ 404 | try: 405 | referenced_sublists = set() 406 | for key, listdef in list(config.items()): 407 | # Check if section is a list 408 | if not isinstance(listdef, dict): 409 | raise ValueError(f'Value at key {key!r} is not a dict') 410 | # Check if it has correct type 411 | if _CONF.FIELD.TYPE not in listdef: 412 | raise ValueError(f'Config at key {key!r} has no {_CONF.FIELD.TYPE!r}') 413 | # Nested or Cartesian 414 | if listdef[_CONF.FIELD.TYPE] in (_CONF.TYPE.NESTED, _CONF.TYPE.CARTESIAN): 415 | sublists = listdef.get(_CONF.FIELD.LISTS) 416 | if sublists is None: 417 | raise ValueError('Config at key {!r} has no {!r}' 418 | .format(key, _CONF.FIELD.LISTS)) 419 | if (not isinstance(sublists, list) or not sublists or 420 | not all(_is_str(x) for x in sublists)): 421 | raise ValueError('Config at key {!r} has invalid {!r}' 422 | .format(key, _CONF.FIELD.LISTS)) 423 | referenced_sublists.update(sublists) 424 | # Const 425 | elif listdef[_CONF.FIELD.TYPE] == _CONF.TYPE.CONST: 426 | try: 427 | value = listdef[_CONF.FIELD.VALUE] 428 | except KeyError: 429 | raise ValueError('Config at key {!r} has no {!r}' 430 | .format(key, _CONF.FIELD.VALUE)) 431 | if not _is_str(value): 432 | raise ValueError('Config at key {!r} has invalid {!r}' 433 | .format(key, _CONF.FIELD.VALUE)) 434 | # Words 435 | elif listdef[_CONF.FIELD.TYPE] == _CONF.TYPE.WORDS: 436 | try: 437 | words = listdef[_CONF.FIELD.WORDS] 438 | except KeyError: 439 | raise ValueError('Config at key {!r} has no {!r}' 440 | .format(key, _CONF.FIELD.WORDS)) 441 | if not isinstance(words, list) or not words: 442 | raise ValueError('Config at key {!r} has invalid {!r}' 443 | .format(key, _CONF.FIELD.WORDS)) 444 | # Validate word length 445 | try: 446 | max_length = int(listdef[_CONF.FIELD.MAX_LENGTH]) 447 | except KeyError: 448 | max_length = None 449 | if max_length is not None: 450 | for word in words: 451 | if len(word) > max_length: 452 | raise ValueError('Config at key {!r} has invalid word {!r} ' 453 | '(longer than {} characters)' 454 | .format(key, word, max_length)) 455 | # Phrases (sequences of one or more words) 456 | elif listdef[_CONF.FIELD.TYPE] == _CONF.TYPE.PHRASES: 457 | try: 458 | phrases = listdef[_CONF.FIELD.PHRASES] 459 | except KeyError: 460 | raise ValueError('Config at key {!r} has no {!r}' 461 | .format(key, _CONF.FIELD.PHRASES)) 462 | if not isinstance(phrases, list) or not phrases: 463 | raise ValueError('Config at key {!r} has invalid {!r}' 464 | .format(key, _CONF.FIELD.PHRASES)) 465 | # Validate multi-word and max length 466 | try: 467 | number_of_words = int(listdef[_CONF.FIELD.NUMBER_OF_WORDS]) 468 | except KeyError: 469 | number_of_words = None 470 | try: 471 | max_length = int(listdef[_CONF.FIELD.MAX_LENGTH]) 472 | except KeyError: 473 | max_length = None 474 | for phrase in phrases: 475 | phrase = _split_phrase(phrase) # str -> sequence, if necessary 476 | if not isinstance(phrase, (tuple, list)) or not all(isinstance(x, str) for x in phrase): 477 | raise ValueError('Config at key {!r} has invalid {!r}: ' 478 | 'must be all string/tuple/list' 479 | .format(key, _CONF.FIELD.PHRASES)) 480 | if number_of_words is not None and len(phrase) != number_of_words: 481 | raise ValueError('Config at key {!r} has invalid phrase {!r} ' 482 | '({} word(s) but {}={})' 483 | .format(key, ' '.join(phrase), 484 | len(phrase), _CONF.FIELD.NUMBER_OF_WORDS, number_of_words)) 485 | if max_length is not None and sum(len(word) for word in phrase) > max_length: 486 | raise ValueError('Config at key {!r} has invalid phrase {!r} ' 487 | '(longer than {} characters)' 488 | .format(key, ' '.join(phrase), max_length)) 489 | else: 490 | raise ValueError('Config at key {!r} has invalid {!r}' 491 | .format(key, _CONF.FIELD.TYPE)) 492 | # Check that all sublists are defined 493 | diff = referenced_sublists.difference(config.keys()) 494 | if diff: 495 | raise ValueError('Lists are referenced but not defined: {}' 496 | .format(', '.join(sorted(diff)[:10]))) 497 | except (KeyError, ValueError) as ex: 498 | raise ConfigurationError(str(ex)) 499 | 500 | 501 | def _create_lists(config, results, current, stack, inside_cartesian=None): 502 | """ 503 | An ugly recursive method to transform config dict 504 | into a tree of AbstractNestedList. 505 | """ 506 | # Have we done it already? 507 | try: 508 | return results[current] 509 | except KeyError: 510 | pass 511 | # Check recursion depth and detect loops 512 | if current in stack: 513 | raise ConfigurationError('Rule {!r} is recursive: {!r}'.format(stack[0], stack)) 514 | if len(stack) > 99: 515 | raise ConfigurationError('Rule {!r} is too deep'.format(stack[0])) 516 | # Track recursion depth 517 | stack.append(current) 518 | try: 519 | # Check what kind of list we have 520 | listdef = config[current] 521 | list_type = listdef[_CONF.FIELD.TYPE] 522 | # 1. List of words 523 | if list_type == _CONF.TYPE.WORDS: 524 | results[current] = WordList(listdef['words']) 525 | # List of phrases 526 | elif list_type == _CONF.TYPE.PHRASES: 527 | results[current] = PhraseList(listdef['phrases']) 528 | # 2. Simple list of lists 529 | elif list_type == _CONF.TYPE.NESTED: 530 | results[current] = NestedList([_create_lists(config, results, x, stack, 531 | inside_cartesian=inside_cartesian) 532 | for x in listdef[_CONF.FIELD.LISTS]]) 533 | 534 | # 3. Cartesian list of lists 535 | elif list_type == _CONF.TYPE.CARTESIAN: 536 | if inside_cartesian is not None: 537 | raise ConfigurationError("Cartesian list {!r} contains another Cartesian list " 538 | "{!r}. Nested Cartesian lists are not allowed." 539 | .format(inside_cartesian, current)) 540 | results[current] = CartesianList([_create_lists(config, results, x, stack, 541 | inside_cartesian=current) 542 | for x in listdef[_CONF.FIELD.LISTS]]) 543 | # 4. Scalar 544 | elif list_type == _CONF.TYPE.CONST: 545 | results[current] = Scalar(listdef[_CONF.FIELD.VALUE]) 546 | # Unknown type 547 | else: 548 | raise InitializationError("Unknown list type: {!r}".format(list_type)) 549 | # Return the result 550 | return results[current] 551 | finally: 552 | stack.pop() 553 | 554 | 555 | # Default generator is a global object 556 | def _default() -> RandomGenerator: 557 | data_dir = os.getenv('COOLNAME_DATA_DIR') 558 | data_module = os.getenv('COOLNAME_DATA_MODULE') 559 | if not data_dir and not data_module: 560 | data_dir = op.join(op.dirname(op.abspath(__file__)), 'data') 561 | data_module = 'coolname.data' # used when imported from egg; consumes more memory 562 | if data_dir and op.isdir(data_dir): 563 | from coolname.loader import load_config 564 | config = load_config(data_dir) 565 | elif data_module: 566 | import importlib 567 | config = importlib.import_module(data_module).config 568 | else: 569 | raise ImportError('Configure valid COOLNAME_DATA_DIR and/or COOLNAME_DATA_MODULE') 570 | config['all']['__nocheck'] = True 571 | return RandomGenerator(config) 572 | 573 | 574 | # Default generator is a global object 575 | _default = _default() 576 | 577 | # Global functions are actually methods of the default generator. 578 | # (most users don't care about creating generator instances) 579 | generate = _default.generate 580 | generate_slug = _default.generate_slug 581 | get_combinations_count = _default.get_combinations_count 582 | 583 | 584 | def replace_random(rand): 585 | """Replaces random number generator for the default RandomGenerator instance.""" 586 | _default.random = rand 587 | --------------------------------------------------------------------------------