6
\$\begingroup\$

I work for a service desk and change A LOT of passwords. Most the time the end user is a complete fool that doesn't understand the requirements for the password they want to use. So I created a program that will generate random passwords based on a randomness level (1 - 3).

Here's how the levels work:

  1. Reads the passwords from a file that you made, these are the most readable passwords
  2. Runs through a gibberish function and returns somewhat word like passwords complete with uppercase, lowercase, integer, and special character
  3. Completely random string, no pattern to it, has uppercase, lowercase, integers, and special characters.

What I would like to know is if there is anything I can do better inside of this program, along with a way to make more readable strings output from the gibberish function.

import optparse
import string
import random
import itertools
import os
from random import randint


init_cons = (set(string.ascii_lowercase) - set("aeiou")
             - set("qxc")
             | {"bl", "br", "cl", "cr", "dr", "fl", "fr", "gl", "gr", "pl", "pr", "sk", "sl", "sm", "sn", "sp", "st",
                "str", "sw", "tr"}
             )
final_cons = (set(string.ascii_uppercase) - set("aeiou")
              - set("qxcsj")
              | {"ct", "ft", "mp", "nd", "ng", "nk", "nt",
                 "pt", "sk", "sp", "ss", "st"}
              )
vowels = "aeiou"
SYLLABLES = map(''.join, itertools.product(init_cons,
                                           vowels,
                                           final_cons))


opts = optparse.OptionParser()
opts.add_option("-l", "--level", metavar="RANDOMNESS LEVEL",
                dest="level", help="How random do you want the passwords [1-3]")
opts.add_option("-a", "--add", metavar="PASS TO ADD",
                dest="add", help="Add a password to the password file")
opts.add_option("--test-demo", action="store_true", dest="test",
                help="Run the program 5 times in each level")
(options, args) = opts.parse_args()


def define_randomness_level(level):
    """ Return how random you the strings are going to be
    :type level: String the level you're on with your randomness
    >>> from gen import define_randomness_level
    >>>
    >>> print(define_randomness_level(2))
    >>> 2
    """
    if level == "1":
        return 1
    elif level == "2":
        return 2
    elif level == "3":
        return 3
    else:
        return None


def random_strings(size=randint(8, 15), spec_chars=random.choice("?!@*^"), chars=string.ascii_lowercase + string.digits
                   + string.ascii_uppercase):
    """ Return a random string between 8 and 15 characters long.
    :type size: Integers, the length of the string returned
    :type spec_chars: String of special characters that is randomly grabbed from
    :type chars: String of random characters
    >>> from gen import random_strings
    >>>
    >>> print(random_strings())
    >>> hfYpvO4YCzHuCW^
    """
    return ''.join(random.choice(chars) for _ in xrange(size)) + spec_chars


def read_pass_from_file(filename):
    """ Read from the file if you want actual readable password strings
    :type filename: I shouldn't have to tell you what this is..
    >>> from gen import read_pass_from_file
    >>>
    >>> print(read_pass_from_file("pass_list.txt"))
    >>> Cocoa!55
    """
    with open(filename, "r") as data:
        arr = data.read().splitlines()
        return random.choice(arr)


def gibberish(count):
    """ Somewhat readable stuff that almost makes sense.
    :type count: Integer, how many somewhat words you want smooshed together
    >>> from gen import gibberish
    >>>
    >>> print(gibberish(2))
    >>> druJdiE!9
    """
    return ''.join(random.sample(SYLLABLES, count)) + random.choice("?!@*^") + random.choice(string.digits)


def add_to_file(word):
    """ Add a password to the password file
    :type word: String, your cool password you want added to the file
    >>> from gen import add_to_file
    >>>
    >>> add_to_file("this1SAt3st!")
    >>> NOTHING! BECAUSE IT'S IN THE FILE!
    """
    with open("pass_list.txt", "r") as words:
        data = words.read()
        open("pass_list.txt", "w").write(data + "\n{}".format(word))


def test_levels(amount):
    """ Test each level of randomness because @boardrider thought it was confusing
    :type amount: Integer, run each level this many times
    >>> from gen import test_levels
    >>>
    >>> test_levels(5)
    >>> Running with level: 1
    >>> Scha3fer!07
    >>> ...
    """
    command_list = ["python gen.py -l 1", "python gen.py -l 2", "python gen.py -l 3"]
    while amount != 0:
        amount -= 1
        for command in command_list:
            arr = command.split(" ")
            print("Running with level: {}".format(arr[3]))
            os.system(command)


def console_main(opt):
    """ Main section of the program, decide where to send what command
    :type opt: Dict, given as an option
    """
    if define_randomness_level(opt) == 1:
        return read_pass_from_file("pass_list.txt")
    elif define_randomness_level(opt) == 2:
        return gibberish(2)
    elif define_randomness_level(opt) == 3:
        return random_strings()
    else:
        raise NotImplementedError("Randomness level of {} is not implemented yet".format(options.level))


if __name__ == '__main__':
    # To test run with the --test-demo flag
    if options.level:
        print(console_main(options.level))
    elif options.add:
        add_to_file(options.add)
    elif options.test:
        test_levels(5)

Example of usage:

C:\bin\python\gen_pass.py>python gen.py --test-demo
Running with level: 1
MSHa!899
Running with level: 2
proIclamp@0
Running with level: 3
DkrYnuADyI!
Running with level: 1
Scha3fer!07
Running with level: 2
hespproE@4
Running with level: 3
wm4m2rVfjh!
Running with level: 1
American!Dad!07
Running with level: 2
taSreL!8
Running with level: 3
TcR5ozXofmCUlY8^
Running with level: 1
American!Dad!07
Running with level: 2
peVcloO@6
Running with level: 3
9ZktisBY?
Running with level: 1
grAnd1k!
Running with level: 2
floskneD!7
Running with level: 3
3jbHmth4j0P@
None
\$\endgroup\$
  • 5
    \$\begingroup\$ @boardrider why do you think the code is complicated? For me it looks like a good question. It has all it needs and it's not complicated at all. And if it looks that way to you, write an answer and tell the OP what he can do to improve it \$\endgroup\$ – Grajdeanu Alex. Nov 1 '16 at 18:16
  • \$\begingroup\$ @boardrider What if in the example I ran it it x amount if times to output level 1, 2 and 3? \$\endgroup\$ – Pyth0nicPenguin Nov 1 '16 at 18:17
  • \$\begingroup\$ I'll add a flag that will run a test suite and edit the question so you can run it easily. \$\endgroup\$ – Pyth0nicPenguin Nov 1 '16 at 18:21
  • \$\begingroup\$ @boardrider Flag has been added see edits \$\endgroup\$ – Pyth0nicPenguin Nov 1 '16 at 18:50
7
\$\begingroup\$

CLI

Did you read the deprecation notice on the optparse module? Use argparse instead, it will help you simplify the code.

  1. Convert to integers and limit available choices to 1, 2 and 3:

    parser = argparse.ArgumentParser(description='TODO')
    parser.add_argument("-l", "--level", metavar="RANDOMNESS LEVEL", type=int, choices=range(1,4),
                        dest="level", help="How random do you want the passwords [1-3]")
    parser.add_argument("-a", "--add", metavar="PASS TO ADD",
                        dest="add", help="Add a password to the password file")
    parser.add_argument("--test-demo", action="store_true", dest="test",
                        help="Run the program 5 times in each level")
    args = parser.parse_args()
    
  2. Disallow specifying two options at once, using groups:

    parser = argparse.ArgumentParser(description='TODO')
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("-l", "--level", metavar="RANDOMNESS LEVEL", type=int, choices=range(1,4),
                       dest="level", help="How random do you want the passwords [1-3]")
    group.add_argument("-a", "--add", metavar="PASS TO ADD",
                       dest="add", help="Add a password to the password file")
    group.add_argument("--test-demo", action="store_true", dest="test",
                       help="Run the program 5 times in each level")
    args = parser.parse_args()
    

    or subcommands:

    parser = argparse.ArgumentParser(description='TODO')
    commands = parser.add_subparser(metavar='command', dest='command')
    generate = commands.add_parser('generate', help='create a new password')
    generate.add_argument('level', type=int, choices=[1, 2, 3],
                          help="How random do you want the passwords [1-3]")
    store = commands.add_parser('store', help='Add a password to the password file')
    store.add_argument('password', help='A password to store in the file')
    commands.add_parser('demo', help="Run the program 5 times in each level")
    args = parser.parse_args()
    if args.command is None:
        parser.error('command is required')
    

I like the last version much as it helps separate the logic and the associated values:

import argparse


def command_line_parser():
    parser = argparse.ArgumentParser(description='TODO')
    commands = parser.add_subparser(metavar='command', dest='command')

    generate = commands.add_parser('generate', help='create a new password')
    generate.add_argument('level', type=int, choices=[1, 2, 3],
                          help="How random do you want the passwords [1-3]")

    store = commands.add_parser('store', help='Add a password to the password file')
    store.add_argument('password', help='A password to store in the file')

    commands.add_parser('demo', help="Run the program 5 times in each level")

    args = parser.parse_args()
    if args.command is None:
        parser.error('command is required')

    return args


...


if __name__ == '__main__':
    options = command_line_parser()
    if options.command == 'generate':
        print(console_main(options.level))
    elif options.command == 'store':
        add_to_file(options.password)
    elif options.command == 'demo':
        test_levels(5)

Files

Your add_to_file function append the provided word into the file. There is a special mode to the open function exactly for that purpose:

def add_to_file(word):
    with open("pass_list.txt", "a") as words:
        print(word, file=words)

You also happen to use the plain filename in this function and take it as a parameter in read_pass_from_file. I would rather define the default filename as a constant that will be used in each function:

PASSWORD_FILE = 'pass_list.txt'


def add_to_file(word):
    with open(PASSWORD_FILE, "a") as words:
        print(word, file=words)


def read_pass_from_file():
    with open(PASSWORD_FILE, "r") as password:
        data = password.readlines()
    return random.choice(data).rstrip('\n')

You may also note the use of .readlines() instead of .read().splitlines() that required twice as much as memory. The drawback being that, contrary to splitlines, readlines will keep the newline at the end of each string, so we need to clean it.

Lastly, I would use an absolute path for the password file rather than a file in the current working directory. It let you easily launch it from an other folder or even make it executable and put it in your PATH:

import os.path


PASSWORD_FILE = os.path.join(
    os.path.dirname(os.path.abspath(__file__)),
    'pass_list.txt')

This way, pass_list.txt will always be a file in the same folder than your script.

Default values

As you don't allow access to these through the command line, you may want to define default values for each of your "main" functions. You already had them for random_strings, we just modified the logic for read_pass_from_file, so there is only gibberish missing. Doing so will allow you to simplify console_main since it now receive integers directly:

def gibberish(count=2):
    ....


def console_main(randomness_level):
    function = [
        read_pass_from_file,
        gibberish,
        random_strings,
    ][randomness_level-1]
    return function()

Even without doing so, you should have called define_randomness_level(opt) only once and stored the result in a variable, as there is no reason to recompute it at each if.

Demo

There is absolutely no need to reload the file for each demo as you know that, in the end, console_main will be called. So call it yourself, it will save some resources:

def test_levels(amount):
    for _ in xrange(amount):
        for command in xrange(1, 4):
            print("Running with level: {}".format(command))
            console_main(command)
\$\endgroup\$
  • \$\begingroup\$ I used optparse because I'm using 2.7. I don't understand your take on console_main, how would that work? Like an array of functions and depending on the level given it will choose which function to use? \$\endgroup\$ – Pyth0nicPenguin Nov 2 '16 at 11:11
  • 1
    \$\begingroup\$ @Pyth0nicPenguin And lukily, as the documentation says, argparse is "New in version 2.7.". For console_main, it's exactly that, you put references to the function into a list, pick the correct one by its index and call it right after. \$\endgroup\$ – 409_Conflict Nov 2 '16 at 11:14
  • \$\begingroup\$ That's pretty cool. I didn't know you could do that. \$\endgroup\$ – Pyth0nicPenguin Nov 2 '16 at 11:17
  • \$\begingroup\$ The only problem I have with that is that it does create an array, so that would make index number 1, 0. \$\endgroup\$ – Pyth0nicPenguin Nov 2 '16 at 11:39
  • \$\begingroup\$ @Pyth0nicPenguin Yes, I edited in randomness_level - 1 after your first comment. \$\endgroup\$ – 409_Conflict Nov 2 '16 at 11:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.