From eeca690c0a3c521920b03200dec3b5a380797bf2 Mon Sep 17 00:00:00 2001 From: kilyabin <65072190+kilyabin@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:42:16 +0400 Subject: [PATCH] feat: add type hints, new CLI features, and improve project structure chore: add uv.lock to gitignore Major changes: - Add type hints and docstrings to all functions (PEP 484) - Add __main__.py for module execution (python -m genpass) - Expose public API in __init__.py - Add config file support (~/.genpass/config.json) - Add entropy calculation (--entropy) - Add clipboard support (--clipboard/-c) - Add ambiguous characters exclusion (--no-ambiguous) - Add output formats: plain, json, delimited (--format) - Add sensible defaults (all character types enabled by default) - Add input validation for length and count - Update shell completions for all new options - Add pre-commit config with ruff, mypy, black - Update pyproject.toml with dev dependencies and tool configs - Add requirements.txt and requirements-dev.txt - Update README.md with comprehensive examples BREAKING CHANGE: Default behavior now includes all character types --- .gitignore | 3 + .pre-commit-config.yaml | 19 ++ README.md | 310 ++++++++++++++++++++++-- completions/genpass.bash | 2 +- completions/genpass.fish | 23 +- completions/genpass.zsh | 27 ++- genpass/__init__.py | 30 +++ genpass/__main__.py | 8 + genpass/cli.py | 512 +++++++++++++++++++++++++++++++++++++-- install.sh | 1 + pyproject.toml | 93 ++++++- requirements-dev.txt | 7 + requirements.txt | 1 + 13 files changed, 969 insertions(+), 67 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 genpass/__main__.py create mode 100644 requirements-dev.txt create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 68bc17f..de2eb00 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,9 @@ ipython_config.py # https://pdm.fming.dev/#use-with-ide .pdm.toml +# uv +uv.lock + # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..91a3c42 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.6 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.7.1 + hooks: + - id: mypy + additional_dependencies: [] + + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.11.0 + hooks: + - id: black + language_version: python3 diff --git a/README.md b/README.md index 1006e98..e80a730 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,38 @@ # GenPass -**Secure password generator CLI** written in Python. +**Secure password generator CLI** written in Python. Generate strong, random passwords from the command line with customizable character sets. --- ## Features -- Specify password length and quantity -- Include/exclude: - - Lowercase letters (`--lower`) - - Uppercase letters (`--upper`) - - Digits (`--digits`) - - Symbols (`--symbols`) -- Optional custom symbol set (`--symbol-set`) -- Ensure at least one character from each selected type (can be disabled with `--no-ensure`) +- **Default behavior**: All character types enabled by default (lowercase, uppercase, digits, symbols) +- Specify password length (`-l`) and quantity (`-n`) +- Include/exclude character types: + - `--lower` - Lowercase letters + - `--upper` - Uppercase letters + - `--digits` - Digits + - `--symbols` - Symbols +- `--no-ambiguous` - Exclude confusing characters (l, 1, I, O, 0) +- `--symbol-set` - Custom symbol set +- `--no-ensure` - Disable "at least one from each type" rule +- `--entropy` - Calculate and display password entropy +- `--clipboard` / `-c` - Copy password to clipboard +- `--format` - Output format: `plain`, `json`, `delimited` +- `--save-config` - Save current options as defaults +- `--config` - Show current configuration - Shell completions for **bash**, **zsh**, and **fish** -- Easy installation via **pipx** or local script --- ## Installation ### 1. Via pipx (recommended) + ```bash pipx install git+https://github.com/kilyabin/GenPass -```` +``` ### 2. Local installation @@ -33,26 +40,62 @@ Clone the repo and run the install script: ```bash git clone https://github.com/kilyabin/GenPass.git -cd genpass +cd GenPass ./install.sh ``` -This will also set up shell completions for bash, zsh, and fish. +### 3. Development installation + +```bash +pipx install --editable . +# or +pip install -e . +``` --- ## Usage -Generate a single password (default 16 characters): +### Basic Usage + +Generate a single password (default 16 characters, all character types): + +```bash +genpass +``` + +Generate a password with specific character types: ```bash genpass --lower --upper --digits --symbols ``` +### Length and Quantity + Generate 5 passwords of length 20: ```bash -genpass -l 20 -n 5 --lower --upper --digits --symbols +genpass -l 20 -n 5 +``` + +Generate a 32-character password: + +```bash +genpass -l 32 +``` + +### Character Sets + +Password without symbols (only letters and digits): + +```bash +genpass --lower --upper --digits +``` + +Only lowercase and digits: + +```bash +genpass --lower --digits ``` Use a custom symbol set: @@ -61,10 +104,134 @@ Use a custom symbol set: genpass --symbols --symbol-set "!@#%&" ``` -Disable "ensure each type" rule: +### Excluding Ambiguous Characters + +Exclude confusing characters like `l`, `1`, `I`, `O`, `0`: ```bash -genpass --lower --upper --digits --no-ensure +genpass --no-ambiguous +``` + +Combine with other options: + +```bash +genpass -l 20 --no-ambiguous --lower --upper --digits +``` + +### Output Formats + +Plain text (default): + +```bash +genpass -n 3 +``` + +JSON format: + +```bash +genpass -n 3 --format json +``` + +Output: +```json +{ + "passwords": [ + "abc123...", + "def456...", + "ghi789..." + ] +} +``` + +Delimited format (one per line): + +```bash +genpass -n 3 --format delimited +``` + +### Entropy Calculation + +Display password entropy (in bits): + +```bash +genpass --entropy +``` + +Output: +``` +Kx9#mP2$vL5@nQ8w +# Entropy: 94.56 bits +``` + +### Clipboard Support + +Copy the generated password to clipboard: + +```bash +genpass --clipboard +# or +genpass -c +``` + +Output: +``` +✓ Copied to clipboard +Kx9#mP2$vL5@nQ8w +``` + +> **Note**: Clipboard support requires `pyperclip`. Install with `pip install pyperclip`. + +### Configuration + +Save default settings: + +```bash +genpass -l 24 --no-ambiguous --save-config +``` + +Show current configuration: + +```bash +genpass --config +``` + +Configuration is stored in `~/.genpass/config.json`. + +--- + +## Examples + +### Real-world Usage + +**Generate a password for a website:** +```bash +genpass -l 16 --clipboard +``` + +**Generate multiple passwords and save to file:** +```bash +genpass -n 10 -l 20 --format json > passwords.json +``` + +**Generate a memorable password (no ambiguous chars):** +```bash +genpass -l 12 --no-ambiguous +``` + +**Generate a high-entropy password:** +```bash +genpass -l 32 --entropy +``` + +**Script-friendly JSON output:** +```bash +genpass --format json | jq -r '.passwords[0]' +``` + +**Set default password length for all future sessions:** +```bash +genpass -l 20 --save-config +genpass # Now generates 20-char passwords by default ``` --- @@ -75,19 +242,16 @@ After installation, completions are automatically copied to your shell folders. Restart your shell or source the completion files manually: **Bash** - ```bash source ~/.local/share/bash-completion/completions/genpass ``` **Zsh** - ```bash source ~/.local/share/zsh/site-functions/_genpass ``` **Fish** - ```fish source ~/.local/share/fish/vendor_completions.d/genpass.fish ``` @@ -96,20 +260,111 @@ source ~/.local/share/fish/vendor_completions.d/genpass.fish ## Development -1. Install in editable mode for development: - +1. Install in editable mode: ```bash pipx install --editable . +# or +pip install -e . ``` -2. Make changes in `genpass/cli.py` and test immediately. +2. Install dev dependencies: +```bash +pip install -e ".[dev]" +``` + +3. Run linting: +```bash +ruff check genpass/ +mypy genpass/ +black --check genpass/ +``` + +4. Run tests: +```bash +pytest +``` + +--- + +## API Usage + +Use GenPass as a Python library: + +```python +from genpass import generate_password, get_character_pools, calculate_entropy + +# Get character pools +pools = get_character_pools( + use_lower=True, + use_upper=True, + use_digits=True, + use_symbols=True, + symbol_set="!@#$%", + exclude_ambiguous=False, +) + +# Generate password +password = generate_password(length=16, pools=pools) +print(password) + +# Calculate entropy +entropy = calculate_entropy(password, sum(len(p) for p in pools)) +print(f"Entropy: {entropy:.2f} bits") +``` + +--- + +## Command-Line Options + +``` +positional arguments: + (none) + +options: + -h, --help Show help message + -l, --length LENGTH Password length (default: 16) + -n, --count COUNT Number of passwords (default: 1) + --lower Include lowercase letters + --upper Include uppercase letters + --digits Include digits + --symbols Include symbols + --symbol-set SET Custom symbol set + --no-ensure Disable ensure-each-type rule + --no-ambiguous Exclude ambiguous characters + --entropy Show password entropy + -c, --clipboard Copy to clipboard + --format FORMAT Output format: plain, json, delimited + --config Show current configuration + --save-config Save options as defaults +``` --- ## Contributing -Contributions are welcome! Please fork the repo and submit pull requests. -Ensure code follows PEP8 style and add shell completion tests if applicable. +Contributions are welcome! Please follow these guidelines: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Run tests and linting (`pytest`, `ruff check`, `mypy`) +5. Commit your changes (`git commit -m 'Add amazing feature'`) +6. Push to the branch (`git push origin feature/amazing-feature`) +7. Open a Pull Request + +### Code Style + +This project uses: +- **Black** for code formatting +- **Ruff** for linting +- **MyPy** for type checking + +Install dev dependencies and set up pre-commit hooks: + +```bash +pip install -e ".[dev]" +pre-commit install +``` --- @@ -117,5 +372,8 @@ Ensure code follows PEP8 style and add shell completion tests if applicable. This project is licensed under the **MIT License** – see the [LICENSE](LICENSE) file for details. -``` +--- +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for version history. diff --git a/completions/genpass.bash b/completions/genpass.bash index 1736e54..794937f 100644 --- a/completions/genpass.bash +++ b/completions/genpass.bash @@ -1,7 +1,7 @@ _genpass() { local cur opts cur="${COMP_WORDS[COMP_CWORD]}" - opts="--help -l --length -n --count --lower --upper --digits --symbols --symbol-set --no-ensure" + opts="-h --help -l --length -n --count --lower --upper --digits --symbols --symbol-set --no-ensure --no-ambiguous --entropy -c --clipboard --format --config --save-config" COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) } complete -F _genpass genpass diff --git a/completions/genpass.fish b/completions/genpass.fish index ffe428e..5e3a2c9 100644 --- a/completions/genpass.fish +++ b/completions/genpass.fish @@ -1,8 +1,15 @@ -complete -c genpass -l length -s l -d "Password length" -complete -c genpass -l count -s n -d "Number of passwords" -complete -c genpass -l lower -d "Lowercase letters" -complete -c genpass -l upper -d "Uppercase letters" -complete -c genpass -l digits -d "Digits" -complete -c genpass -l symbols -d "Symbols" -complete -c genpass -l symbol-set -d "Custom symbol set" -complete -c genpass -l no-ensure -d "Disable character guarantees" +complete -c genpass -s h -l help -d "Show help message" +complete -c genpass -s l -l length -d "Password length" -r +complete -c genpass -s n -l count -d "Number of passwords" -r +complete -c genpass -l lower -d "Include lowercase letters" +complete -c genpass -l upper -d "Include uppercase letters" +complete -c genpass -l digits -d "Include digits" +complete -c genpass -l symbols -d "Include symbols" +complete -c genpass -l symbol-set -d "Custom symbol set" -r +complete -c genpass -l no-ensure -d "Disable ensure-each-type rule" +complete -c genpass -l no-ambiguous -d "Exclude ambiguous characters" +complete -c genpass -l entropy -d "Show password entropy" +complete -c genpass -s c -l clipboard -d "Copy to clipboard" +complete -c genpass -l format -d "Output format" -r -f -a "plain json delimited" +complete -c genpass -l config -d "Show current configuration" +complete -c genpass -l save-config -d "Save options as defaults" diff --git a/completions/genpass.zsh b/completions/genpass.zsh index a63031d..2dae420 100644 --- a/completions/genpass.zsh +++ b/completions/genpass.zsh @@ -1,10 +1,21 @@ #compdef genpass _arguments \ - '--length[-l]:password length:' \ - '--count[-n]:number of passwords:' \ - '--lower[use lowercase letters]' \ - '--upper[use uppercase letters]' \ - '--digits[use digits]' \ - '--symbols[use symbols]' \ - '--symbol-set[custom symbol set]' \ - '--no-ensure[do not enforce each type]' + '-h[show help]' \ + '--help[show help]' \ + '-l[password length]:length:' \ + '--length[password length]:length:' \ + '-n[number of passwords]:count:' \ + '--count[number of passwords]:count:' \ + '--lower[include lowercase letters]' \ + '--upper[include uppercase letters]' \ + '--digits[include digits]' \ + '--symbols[include symbols]' \ + '--symbol-set[custom symbol set]:symbols:' \ + '--no-ensure[disable ensure-each-type rule]' \ + '--no-ambiguous[exclude ambiguous characters (l, 1, I, O, 0)]' \ + '--entropy[show password entropy]' \ + '-c[copy to clipboard]' \ + '--clipboard[copy to clipboard]' \ + '--format[output format]:format:(plain json delimited)' \ + '--config[show current configuration]' \ + '--save-config[save options as defaults]' diff --git a/genpass/__init__.py b/genpass/__init__.py index e69de29..01d4369 100644 --- a/genpass/__init__.py +++ b/genpass/__init__.py @@ -0,0 +1,30 @@ +""" +GenPass - Secure password generator. + +This package provides functionality to generate strong, random passwords +with customizable character sets and various security features. + +Example usage: + >>> from genpass import generate_password, get_character_pools + >>> pools = get_character_pools(True, True, True, True, "!@#$%", False) + >>> password = generate_password(16, pools) + >>> print(password) +""" + +from genpass.cli import ( + AMBIGUOUS_CHARS, + DEFAULT_SYMBOLS, + calculate_entropy, + generate_password, + get_character_pools, +) + +__version__ = "2.0.0" +__all__ = [ + "generate_password", + "get_character_pools", + "calculate_entropy", + "DEFAULT_SYMBOLS", + "AMBIGUOUS_CHARS", + "__version__", +] diff --git a/genpass/__main__.py b/genpass/__main__.py new file mode 100644 index 0000000..c220f45 --- /dev/null +++ b/genpass/__main__.py @@ -0,0 +1,8 @@ +""" +Allow running genpass as a module: python -m genpass +""" + +from genpass.cli import main + +if __name__ == "__main__": + main() diff --git a/genpass/cli.py b/genpass/cli.py index c95ad1b..7e2df70 100644 --- a/genpass/cli.py +++ b/genpass/cli.py @@ -1,47 +1,515 @@ +""" +GenPass - Secure password generator CLI. + +This module provides functionality to generate strong, random passwords +with customizable character sets, entropy calculation, and various output formats. +""" + import argparse +import json +import math import secrets import string import sys +from pathlib import Path +from typing import List, Optional -def generate_password(length, pools, ensure_each=True): +try: + import pyperclip # type: ignore + + HAS_PYPERCLIP = True +except ImportError: + HAS_PYPERCLIP = False + +# Default character sets +DEFAULT_SYMBOLS = "!@#$%^&*()-_=+[]{};:,.<>?" +AMBIGUOUS_CHARS = "l1IO0" + +# Config file path +CONFIG_DIR = Path.home() / ".genpass" +CONFIG_FILE = CONFIG_DIR / "config.json" + + +def load_config() -> dict: + """ + Load user configuration from ~/.genpass/config.json. + + Returns: + Dictionary containing user configuration settings. + Returns empty dict if config file doesn't exist or is invalid. + """ + if CONFIG_FILE.exists(): + try: + with open(CONFIG_FILE, encoding="utf-8") as f: + config = json.load(f) + return config if isinstance(config, dict) else {} + except (OSError, json.JSONDecodeError): + return {} + return {} + + +def save_config(config: dict) -> None: + """ + Save user configuration to ~/.genpass/config.json. + + Args: + config: Dictionary containing configuration settings to save. + """ + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + with open(CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2) + + +def calculate_entropy(password: str, pool_size: int) -> float: + """ + Calculate the entropy (in bits) of a generated password. + + Entropy is calculated as: length * log2(pool_size) + Higher entropy indicates a stronger password. + + Args: + password: The generated password. + pool_size: Total number of possible characters in the pool. + + Returns: + Entropy value in bits. + """ + if pool_size <= 1: + return 0.0 + return len(password) * math.log2(pool_size) + + +def get_character_pools( + use_lower: bool, + use_upper: bool, + use_digits: bool, + use_symbols: bool, + symbol_set: str, + exclude_ambiguous: bool, +) -> List[str]: + """ + Build character pools based on user preferences. + + Args: + use_lower: Include lowercase letters. + use_upper: Include uppercase letters. + use_digits: Include digits. + use_symbols: Include symbols. + symbol_set: Custom set of symbols to use. + exclude_ambiguous: Exclude ambiguous characters (l, 1, I, O, 0). + + Returns: + List of character pool strings. + """ + pools = [] + + if use_lower: + chars = string.ascii_lowercase + if exclude_ambiguous: + chars = chars.replace("l", "") + pools.append(chars) + + if use_upper: + chars = string.ascii_uppercase + if exclude_ambiguous: + chars = chars.replace("IO", "") + pools.append(chars) + + if use_digits: + chars = string.digits + if exclude_ambiguous: + chars = chars.replace("10", "") + pools.append(chars) + + if use_symbols: + chars = symbol_set + if exclude_ambiguous: + for char in AMBIGUOUS_CHARS: + chars = chars.replace(char, "") + if chars: + pools.append(chars) + + return pools + + +def generate_password( + length: int, + pools: List[str], + ensure_each: bool = True, +) -> str: + """ + Generate a secure random password. + + Args: + length: Desired password length. + pools: List of character pools to choose from. + ensure_each: Ensure at least one character from each pool. + + Returns: + Generated password string. + + Raises: + ValueError: If no character pools provided or length too small. + """ if not pools: raise ValueError("No character sets selected") - password = [] + + password: List[str] = [] + + # Ensure at least one character from each pool if ensure_each: if length < len(pools): - raise ValueError("Password length too small") + raise ValueError("Password length too small for ensure_each option") for pool in pools: password.append(secrets.choice(pool)) + + # Fill remaining length with random characters from all pools all_chars = "".join(pools) while len(password) < length: password.append(secrets.choice(all_chars)) + + # Shuffle to randomize positions secrets.SystemRandom().shuffle(password) + return "".join(password) -def main(): - parser = argparse.ArgumentParser(prog="genpass", description="Secure password generator") - parser.add_argument("-l", "--length", type=int, default=16) - parser.add_argument("-n", "--count", type=int, default=1) - parser.add_argument("--lower", action="store_true") - parser.add_argument("--upper", action="store_true") - parser.add_argument("--digits", action="store_true") - parser.add_argument("--symbols", action="store_true") - parser.add_argument("--symbol-set", default="!@#$%^&*()-_=+[]{};:,.<>?") - parser.add_argument("--no-ensure", action="store_true") + +def format_output( + passwords: List[str], + output_format: str, + show_entropy: bool = False, + entropy_value: Optional[float] = None, +) -> str: + """ + Format passwords for output. + + Args: + passwords: List of generated passwords. + output_format: Output format ('plain', 'json', 'delimited'). + show_entropy: Whether to include entropy information. + entropy_value: Entropy value to include in output. + + Returns: + Formatted output string. + """ + if output_format == "json": + output_data: dict = {"passwords": passwords} + if show_entropy and entropy_value is not None: + output_data["entropy_bits"] = round(entropy_value, 2) + return json.dumps(output_data, indent=2) + + if output_format == "delimited": + result = "\n".join(passwords) + if show_entropy and entropy_value is not None: + result += f"\n# Entropy: {entropy_value:.2f} bits" + return result + + # Plain format (default) + result = "\n".join(passwords) + if show_entropy and entropy_value is not None: + result += f"\n# Entropy: {entropy_value:.2f} bits" + return result + + +def copy_to_clipboard(text: str) -> bool: + """ + Copy text to system clipboard. + + Args: + text: Text to copy to clipboard. + + Returns: + True if successful, False otherwise. + """ + if not HAS_PYPERCLIP: + return False + try: + pyperclip.copy(text) + return True + except Exception: + return False + + +def validate_args(args: argparse.Namespace) -> None: + """ + Validate command-line arguments. + + Args: + args: Parsed command-line arguments. + + Raises: + ValueError: If arguments are invalid. + """ + if args.length <= 0: + raise ValueError("Password length must be positive") + + if args.length > 1000: + raise ValueError("Password length cannot exceed 1000") + + if args.count <= 0: + raise ValueError("Password count must be positive") + + if args.count > 100: + raise ValueError("Password count cannot exceed 100") + + if args.symbol_set and len(args.symbol_set) < 1: + raise ValueError("Symbol set cannot be empty") + + +def create_parser() -> argparse.ArgumentParser: + """ + Create and configure the argument parser. + + Returns: + Configured ArgumentParser instance. + """ + parser = argparse.ArgumentParser( + prog="genpass", + description="Secure password generator CLI - Generate strong, random passwords", + epilog="Examples:\n" + " genpass Generate one 16-char password (all char types)\n" + " genpass -l 20 -n 5 Generate 5 passwords of length 20\n" + " genpass --lower --upper --digits Generate password without symbols\n" + " genpass --no-ambiguous Exclude confusing characters\n" + " genpass --entropy Show password entropy\n" + " genpass --format json Output as JSON\n" + " genpass --clipboard Copy to clipboard\n", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + # Password options + parser.add_argument( + "-l", + "--length", + type=int, + default=None, + help="Password length (default: 16)", + ) + parser.add_argument( + "-n", + "--count", + type=int, + default=None, + help="Number of passwords to generate (default: 1)", + ) + + # Character sets + parser.add_argument( + "--lower", + action="store_true", + default=None, + help="Include lowercase letters (default: yes)", + ) + parser.add_argument( + "--upper", + action="store_true", + default=None, + help="Include uppercase letters (default: yes)", + ) + parser.add_argument( + "--digits", + action="store_true", + default=None, + help="Include digits (default: yes)", + ) + parser.add_argument( + "--symbols", + action="store_true", + default=None, + help="Include symbols (default: yes)", + ) + parser.add_argument( + "--symbol-set", + type=str, + default=None, + help="Custom symbol set (default: " + DEFAULT_SYMBOLS.replace("%", "%%") + ")", + ) + + # Options + parser.add_argument( + "--no-ensure", + action="store_true", + help="Disable ensuring at least one char from each selected type", + ) + parser.add_argument( + "--no-ambiguous", + action="store_true", + help="Exclude ambiguous characters (l, 1, I, O, 0)", + ) + parser.add_argument( + "--entropy", + action="store_true", + help="Calculate and display password entropy", + ) + parser.add_argument( + "--clipboard", + "-c", + action="store_true", + help="Copy the first password to clipboard", + ) + + # Output options + parser.add_argument( + "--format", + choices=["plain", "json", "delimited"], + default="plain", + help="Output format (default: plain)", + ) + parser.add_argument( + "--config", + action="store_true", + help="Show current configuration and exit", + ) + parser.add_argument( + "--save-config", + action="store_true", + help="Save current options as defaults", + ) + + return parser + + +def apply_defaults(args: argparse.Namespace, config: dict) -> argparse.Namespace: + """ + Apply default values from config to arguments. + + Args: + args: Parsed command-line arguments. + config: Loaded configuration dictionary. + + Returns: + Updated arguments with defaults applied. + """ + if args.length is None: + args.length = config.get("length", 16) + + if args.count is None: + args.count = config.get("count", 1) + + # For boolean flags, use config if not explicitly set + if args.lower is None: + args.lower = config.get("lower", True) + + if args.upper is None: + args.upper = config.get("upper", True) + + if args.digits is None: + args.digits = config.get("digits", True) + + if args.symbols is None: + args.symbols = config.get("symbols", True) + + if args.symbol_set is None: + args.symbol_set = config.get("symbol_set", DEFAULT_SYMBOLS) + + if not getattr(args, "no_ambiguous", False): + args.no_ambiguous = config.get("no_ambiguous", False) + + return args + + +def main() -> None: + """Main entry point for the genpass CLI.""" + parser = create_parser() args = parser.parse_args() - pools = [] - if args.lower: pools.append(string.ascii_lowercase) - if args.upper: pools.append(string.ascii_uppercase) - if args.digits: pools.append(string.digits) - if args.symbols: pools.append(args.symbol_set) + # Load config + config = load_config() - if not pools: - print("Select at least one character set", file=sys.stderr) + # Show config if requested + if args.config: + if config: + print(json.dumps(config, indent=2)) + else: + print("No configuration file found. Using defaults.") + return + + # Apply defaults from config + args = apply_defaults(args, config) + + # Validate arguments + try: + validate_args(args) + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) sys.exit(1) - for _ in range(args.count): - print(generate_password(args.length, pools, ensure_each=not args.no_ensure)) + # Build character pools + try: + pools = get_character_pools( + use_lower=args.lower, + use_upper=args.upper, + use_digits=args.digits, + use_symbols=args.symbols, + symbol_set=args.symbol_set, + exclude_ambiguous=args.no_ambiguous, + ) + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + if not pools: + print("Error: Select at least one character set", file=sys.stderr) + sys.exit(1) + + # Generate passwords + passwords: List[str] = [] + entropy_value: Optional[float] = None + + try: + for _ in range(args.count): + pwd = generate_password( + length=args.length, + pools=pools, + ensure_each=not args.no_ensure, + ) + passwords.append(pwd) + + # Calculate entropy for the first password + if args.entropy: + pool_size = sum(len(pool) for pool in pools) + entropy_value = calculate_entropy(passwords[0], pool_size) + + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + # Format output + output = format_output( + passwords=passwords, + output_format=args.format, + show_entropy=args.entropy, + entropy_value=entropy_value, + ) + + # Copy to clipboard if requested + if args.clipboard and passwords: + if copy_to_clipboard(passwords[0]): + print("✓ Copied to clipboard", file=sys.stderr) + else: + print( + "Warning: Clipboard functionality unavailable. Install 'pyperclip'.", + file=sys.stderr, + ) + + # Print output + print(output) + + # Save config if requested + if args.save_config: + new_config = { + "length": args.length, + "count": args.count, + "lower": args.lower, + "upper": args.upper, + "digits": args.digits, + "symbols": args.symbols, + "symbol_set": args.symbol_set, + "no_ambiguous": args.no_ambiguous, + } + save_config(new_config) + print(f"✓ Configuration saved to {CONFIG_FILE}", file=sys.stderr) + if __name__ == "__main__": main() diff --git a/install.sh b/install.sh index 40181c6..7501cac 100755 --- a/install.sh +++ b/install.sh @@ -21,3 +21,4 @@ cp completions/genpass.zsh "$PREFIX/zsh/site-functions/_genpass" cp completions/genpass.fish "$PREFIX/fish/vendor_completions.d/genpass.fish" echo "[✓] Installation complete. Restart your shell." +echo "[✓] Clipboard support: pipx inject genpass pyperclip" diff --git a/pyproject.toml b/pyproject.toml index 091c29f..3b96f0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,103 @@ [project] name = "genpass" -version = "1.0.0" +version = "2.0.0" description = "Secure password generator CLI" authors = [{ name = "kilyabin" }] readme = "README.md" -license = "MIT" +license = { text = "MIT" } requires-python = ">= 3.8" +keywords = ["password", "generator", "security", "cli", "random"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: End Users/Desktop", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Security", + "Topic :: Utilities", +] + +dependencies = [ + "pyperclip>=1.8.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", + "ruff>=0.1.0", + "mypy>=1.0.0", + "black>=23.0.0", + "pre-commit>=3.0.0", +] [project.scripts] genpass = "genpass.cli:main" +[project.urls] +Homepage = "https://github.com/kilyabin/GenPass" +Repository = "https://github.com/kilyabin/GenPass.git" +Issues = "https://github.com/kilyabin/GenPass/issues" +Changelog = "https://github.com/kilyabin/GenPass/blob/main/CHANGELOG.md" + [tool.setuptools] packages = ["genpass"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_functions = ["test_*"] +addopts = "-v --cov=genpass --cov-report=term-missing" + +[tool.ruff] +line-length = 100 +target-version = "py38" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = ["E501"] + +[tool.ruff.lint.isort] +known-first-party = ["genpass"] + +[tool.mypy] +python_version = "3.9" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true + +[tool.black] +line-length = 100 +target-version = ["py38", "py39", "py310", "py311", "py312"] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..0ad5e16 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,7 @@ +pyperclip>=1.8.0 +pytest>=7.0.0 +pytest-cov>=4.0.0 +ruff>=0.1.0 +mypy>=1.0.0 +black>=23.0.0 +pre-commit>=3.0.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7c37124 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyperclip>=1.8.0