5
\$\begingroup\$

I have spent some time with python. I got an idea of creating a random password generator. It generates passwords consisting of random characters. The user can exclude different types of characters (letters, numbers, symbols) from the generated password and can customize the length of the generated password. The code consists of a single module, by the way. I am using python 3.13.0-rc1, if that is necessary. I am new here, so please pardon and mention if there is any mistake. Here is my main.py:

#####################################################

# Name: pwgen
# Author: Muhammad Altaaf <[email protected]>
# Description: A random password generator in your
# toolset.

#####################################################

from __future__ import annotations

import secrets
import string
import sys
import typing
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from dataclasses import dataclass

if typing.TYPE_CHECKING:
    from argparse import Namespace

PROG_NAME = "pwgen"
PROG_DESC = """
██████╗ ██╗    ██╗ ██████╗ ███████╗███╗   ██╗
██╔══██╗██║    ██║██╔════╝ ██╔════╝████╗  ██║
██████╔╝██║ █╗ ██║██║  ███╗█████╗  ██╔██╗ ██║
██╔═══╝ ██║███╗██║██║   ██║██╔══╝  ██║╚██╗██║
██║     ╚███╔███╔╝╚██████╔╝███████╗██║ ╚████║
╚═╝      ╚══╝╚══╝  ╚═════╝ ╚══════╝╚═╝  ╚═══╝

A random password generator in your toolset.
"""

PROG_VERSION = "1.1.0"
PROG_AUTHOR = "Muhammad Altaaf"
PROG_AUTHOR_CONTACT = "[email protected]"
PROG_EPILOG = f"""\
Version {PROG_VERSION}.
Written by {PROG_AUTHOR} <{PROG_AUTHOR_CONTACT}>.
"""


@dataclass(frozen=True)
class _Config:
    letters: bool = True
    digits: bool = True
    punct: bool = True
    length: int = 8

    def __post_init__(self):
        if not (self.letters or self.digits or self.punct):
            print(
                "At least one of the three components (alphabets, numbers or \
                symbols) must be allowed",
                file=sys.stderr,
            )
            sys.exit(1)


def _gen_passwd(config: _Config) -> str:
    """The main password generator."""

    passwd_len = config.length
    # string to get password characters from
    base = ""
    # the password
    passwd = ""

    if config.letters:
        base += string.ascii_letters
    if config.digits:
        base += string.digits
    if config.punct:
        base += string.punctuation

    for _ in range(passwd_len):
        while (char := secrets.choice(base)) == "\\":
            continue
        passwd += char

    return passwd


def parse_opts() -> Namespace:
    """Parse command line options and return Namespace object."""

    o_parser = ArgumentParser(
        prog=PROG_NAME,
        description=PROG_DESC,
        epilog=PROG_EPILOG,
        formatter_class=RawDescriptionHelpFormatter,
    )
    add_opt = o_parser.add_argument

    add_opt(
        "-a",
        "--alphabets",
        action="store_true",
        help="Don't include alphabets in the password. (Default is to include)",
    )
    add_opt(
        "-n",
        "--numbers",
        action="store_true",
        help="Don't include numbers in the password. (Default is to include)",
    )
    add_opt(
        "-p",
        "--punctuation",
        action="store_true",
        help="Don't include symbols in the password. (Default is to include)",
    )
    add_opt("-l",
            "--length",
            type=int,
            help="Length of the password. (Default is 8)",
            )

    return o_parser.parse_args()


def gen_config(cmd_opts: Namespace) -> _Config:
    """Generate config from arguments."""

    incl_letters = not cmd_opts.alphabets
    incl_digits = not cmd_opts.numbers
    incl_punct = not cmd_opts.punctuation
    p_len = cmd_opts.length or 8

    config = _Config(incl_letters, incl_digits, incl_punct, p_len)
    return config


def gen_passwd() -> str:
    """Wrapper function for putting all things together."""

    options = parse_opts()
    config = gen_config(options)
    passwd: str = _gen_passwd(config)

    return passwd


def main():
    passwd = gen_passwd()
    print(f"Password: {passwd}")


if __name__ == "__main__":
    main()
\$\endgroup\$
3
  • 3
    \$\begingroup\$ I encourage you to carefully read codereview.stackexchange.com/questions/292925/… \$\endgroup\$ Commented Aug 30, 2024 at 11:43
  • \$\begingroup\$ Please don't call it pwgen - a well-known unix utility exists with that name, and several other libraries/packages already reuse that name. \$\endgroup\$ Commented Aug 30, 2024 at 23:58
  • \$\begingroup\$ @STerliakov Oh, I am seeing the utility the first time \$\endgroup\$ Commented Aug 31, 2024 at 2:28

1 Answer 1

6
\$\begingroup\$

Indeed, there is a good reference topic so without rehashing what has already been said, a couple remarks though:

Unnecessary variable duplication

In _gen_passwd:

passwd_len = config.length

Just use config.length

Further, the variable concatenation is not really needed.

A more straightforward approach would involve a list comprehension along these lines:

return ''.join(secrets.choice(base) for i in range(config.length))

Parsing arguments

I haved mixed feelings about your _Config dataclass, because it does very little in fact. You check that:

At least one of the three components (alphabets, numbers or symbols) must be allowed

This is something that should preferably be handled with argparse, the module is underutilized. You can set default values for your CLI arguments and even apply a range for the password length for example. Custom validation routines can be added if the built-in filters are not sufficient.

It is possible to have toggle boolean arguments, for example --no-alphabets to negate --alphabets. See here for some tips. Accordingly your code could be adapted like this since you are using Python > 3.9:

add_opt('--alphabets', action=argparse.BooleanOptionalAction, default=True,
        help="Don't include alphabets in the password. (Default is to include)")
add_opt('--numbers', action=argparse.BooleanOptionalAction, default=True,
        help="Don't include numbers in the password. (Default is to include)")
add_opt('--punctuation', action=argparse.BooleanOptionalAction, default=True,
        help="Don't include symbols in the password. (Default is to include)")
add_opt("-l",
        "--length",
        type=int, default=8,
        help="Length of the password. (Default is 8)",
        )

args = o_parser.parse_args()
if not (args.alphabets or args.numbers or args.punctuation):
    o_parser.error("At least one of the three components (alphabets, numbers or symbols) must be allowed")

return args

(Maybe there is an even better way to ensure that at least one of the three parameters is True).

One minor downside is that --no-alphabets and --alphabets are not mutually exclusive on the command line, the last option will prevail but you could still use parser.add_mutually_exclusive_group to improve further. So I believe the dataclass is redundant and not useful here.

\$\endgroup\$
1
  • \$\begingroup\$ I wrote "full" for loop (for _ in...) for readability \$\endgroup\$ Commented Aug 31, 2024 at 2:39

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.