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
This commit is contained in:
kilyabin
2026-03-10 00:42:16 +04:00
parent c11345d576
commit eeca690c0a
13 changed files with 969 additions and 67 deletions

3
.gitignore vendored
View File

@@ -109,6 +109,9 @@ ipython_config.py
# https://pdm.fming.dev/#use-with-ide # https://pdm.fming.dev/#use-with-ide
.pdm.toml .pdm.toml
# uv
uv.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/ __pypackages__/

19
.pre-commit-config.yaml Normal file
View File

@@ -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

310
README.md
View File

@@ -1,31 +1,38 @@
# GenPass # 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. Generate strong, random passwords from the command line with customizable character sets.
--- ---
## Features ## Features
- Specify password length and quantity - **Default behavior**: All character types enabled by default (lowercase, uppercase, digits, symbols)
- Include/exclude: - Specify password length (`-l`) and quantity (`-n`)
- Lowercase letters (`--lower`) - Include/exclude character types:
- Uppercase letters (`--upper`) - `--lower` - Lowercase letters
- Digits (`--digits`) - `--upper` - Uppercase letters
- Symbols (`--symbols`) - `--digits` - Digits
- Optional custom symbol set (`--symbol-set`) - `--symbols` - Symbols
- Ensure at least one character from each selected type (can be disabled with `--no-ensure`) - `--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** - Shell completions for **bash**, **zsh**, and **fish**
- Easy installation via **pipx** or local script
--- ---
## Installation ## Installation
### 1. Via pipx (recommended) ### 1. Via pipx (recommended)
```bash ```bash
pipx install git+https://github.com/kilyabin/GenPass pipx install git+https://github.com/kilyabin/GenPass
```` ```
### 2. Local installation ### 2. Local installation
@@ -33,26 +40,62 @@ Clone the repo and run the install script:
```bash ```bash
git clone https://github.com/kilyabin/GenPass.git git clone https://github.com/kilyabin/GenPass.git
cd genpass cd GenPass
./install.sh ./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 ## 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 ```bash
genpass --lower --upper --digits --symbols genpass --lower --upper --digits --symbols
``` ```
### Length and Quantity
Generate 5 passwords of length 20: Generate 5 passwords of length 20:
```bash ```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: Use a custom symbol set:
@@ -61,10 +104,134 @@ Use a custom symbol set:
genpass --symbols --symbol-set "!@#%&" genpass --symbols --symbol-set "!@#%&"
``` ```
Disable "ensure each type" rule: ### Excluding Ambiguous Characters
Exclude confusing characters like `l`, `1`, `I`, `O`, `0`:
```bash ```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: Restart your shell or source the completion files manually:
**Bash** **Bash**
```bash ```bash
source ~/.local/share/bash-completion/completions/genpass source ~/.local/share/bash-completion/completions/genpass
``` ```
**Zsh** **Zsh**
```bash ```bash
source ~/.local/share/zsh/site-functions/_genpass source ~/.local/share/zsh/site-functions/_genpass
``` ```
**Fish** **Fish**
```fish ```fish
source ~/.local/share/fish/vendor_completions.d/genpass.fish source ~/.local/share/fish/vendor_completions.d/genpass.fish
``` ```
@@ -96,20 +260,111 @@ source ~/.local/share/fish/vendor_completions.d/genpass.fish
## Development ## Development
1. Install in editable mode for development: 1. Install in editable mode:
```bash ```bash
pipx install --editable . 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 ## Contributing
Contributions are welcome! Please fork the repo and submit pull requests. Contributions are welcome! Please follow these guidelines:
Ensure code follows PEP8 style and add shell completion tests if applicable.
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. This project is licensed under the **MIT License** see the [LICENSE](LICENSE) file for details.
``` ---
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history.

View File

@@ -1,7 +1,7 @@
_genpass() { _genpass() {
local cur opts local cur opts
cur="${COMP_WORDS[COMP_CWORD]}" 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") ) COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
} }
complete -F _genpass genpass complete -F _genpass genpass

View File

@@ -1,8 +1,15 @@
complete -c genpass -l length -s l -d "Password length" complete -c genpass -s h -l help -d "Show help message"
complete -c genpass -l count -s n -d "Number of passwords" complete -c genpass -s l -l length -d "Password length" -r
complete -c genpass -l lower -d "Lowercase letters" complete -c genpass -s n -l count -d "Number of passwords" -r
complete -c genpass -l upper -d "Uppercase letters" complete -c genpass -l lower -d "Include lowercase letters"
complete -c genpass -l digits -d "Digits" complete -c genpass -l upper -d "Include uppercase letters"
complete -c genpass -l symbols -d "Symbols" complete -c genpass -l digits -d "Include digits"
complete -c genpass -l symbol-set -d "Custom symbol set" complete -c genpass -l symbols -d "Include symbols"
complete -c genpass -l no-ensure -d "Disable character guarantees" 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"

View File

@@ -1,10 +1,21 @@
#compdef genpass #compdef genpass
_arguments \ _arguments \
'--length[-l]:password length:' \ '-h[show help]' \
'--count[-n]:number of passwords:' \ '--help[show help]' \
'--lower[use lowercase letters]' \ '-l[password length]:length:' \
'--upper[use uppercase letters]' \ '--length[password length]:length:' \
'--digits[use digits]' \ '-n[number of passwords]:count:' \
'--symbols[use symbols]' \ '--count[number of passwords]:count:' \
'--symbol-set[custom symbol set]' \ '--lower[include lowercase letters]' \
'--no-ensure[do not enforce each type]' '--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]'

View File

@@ -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__",
]

8
genpass/__main__.py Normal file
View File

@@ -0,0 +1,8 @@
"""
Allow running genpass as a module: python -m genpass
"""
from genpass.cli import main
if __name__ == "__main__":
main()

View File

@@ -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 argparse
import json
import math
import secrets import secrets
import string import string
import sys 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: if not pools:
raise ValueError("No character sets selected") raise ValueError("No character sets selected")
password = []
password: List[str] = []
# Ensure at least one character from each pool
if ensure_each: if ensure_each:
if length < len(pools): if length < len(pools):
raise ValueError("Password length too small") raise ValueError("Password length too small for ensure_each option")
for pool in pools: for pool in pools:
password.append(secrets.choice(pool)) password.append(secrets.choice(pool))
# Fill remaining length with random characters from all pools
all_chars = "".join(pools) all_chars = "".join(pools)
while len(password) < length: while len(password) < length:
password.append(secrets.choice(all_chars)) password.append(secrets.choice(all_chars))
# Shuffle to randomize positions
secrets.SystemRandom().shuffle(password) secrets.SystemRandom().shuffle(password)
return "".join(password) return "".join(password)
def main():
parser = argparse.ArgumentParser(prog="genpass", description="Secure password generator") def format_output(
parser.add_argument("-l", "--length", type=int, default=16) passwords: List[str],
parser.add_argument("-n", "--count", type=int, default=1) output_format: str,
parser.add_argument("--lower", action="store_true") show_entropy: bool = False,
parser.add_argument("--upper", action="store_true") entropy_value: Optional[float] = None,
parser.add_argument("--digits", action="store_true") ) -> str:
parser.add_argument("--symbols", action="store_true") """
parser.add_argument("--symbol-set", default="!@#$%^&*()-_=+[]{};:,.<>?") Format passwords for output.
parser.add_argument("--no-ensure", action="store_true")
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() args = parser.parse_args()
pools = [] # Load config
if args.lower: pools.append(string.ascii_lowercase) config = load_config()
if args.upper: pools.append(string.ascii_uppercase)
if args.digits: pools.append(string.digits)
if args.symbols: pools.append(args.symbol_set)
if not pools: # Show config if requested
print("Select at least one character set", file=sys.stderr) 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) sys.exit(1)
for _ in range(args.count): # Build character pools
print(generate_password(args.length, pools, ensure_each=not args.no_ensure)) 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__": if __name__ == "__main__":
main() main()

View File

@@ -21,3 +21,4 @@ cp completions/genpass.zsh "$PREFIX/zsh/site-functions/_genpass"
cp completions/genpass.fish "$PREFIX/fish/vendor_completions.d/genpass.fish" cp completions/genpass.fish "$PREFIX/fish/vendor_completions.d/genpass.fish"
echo "[✓] Installation complete. Restart your shell." echo "[✓] Installation complete. Restart your shell."
echo "[✓] Clipboard support: pipx inject genpass pyperclip"

View File

@@ -1,14 +1,103 @@
[project] [project]
name = "genpass" name = "genpass"
version = "1.0.0" version = "2.0.0"
description = "Secure password generator CLI" description = "Secure password generator CLI"
authors = [{ name = "kilyabin" }] authors = [{ name = "kilyabin" }]
readme = "README.md" readme = "README.md"
license = "MIT" license = { text = "MIT" }
requires-python = ">= 3.8" 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] [project.scripts]
genpass = "genpass.cli:main" 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] [tool.setuptools]
packages = ["genpass"] 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
)/
'''

7
requirements-dev.txt Normal file
View File

@@ -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

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
pyperclip>=1.8.0