From 42e8dd620ad75ef50e6d05a11fbf3fa562871383 Mon Sep 17 00:00:00 2001 From: Artur Barseghyan Date: Fri, 4 Oct 2024 23:58:49 +0200 Subject: [PATCH] Feature/add password (#194) * Add password --- CHANGELOG.rst | 6 +++ Makefile | 2 +- README.rst | 1 + fake.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 +- 5 files changed, 108 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 04cc2b7..743ebc9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,12 @@ are used for versioning (schema follows below): 0.3.4 to 0.4). - All backwards incompatible changes are mentioned in this document. +0.10.1 +------ +2024-10-05 + +- Added ``password`` provider. + 0.10 ---- 2024-09-27 diff --git a/Makefile b/Makefile index 082f8ef..28b90e7 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Update version ONLY here -VERSION := 0.10 +VERSION := 0.10.1 SHELL := /bin/bash # Makefile for project VENV := ~/.virtualenvs/fake.py/bin/activate diff --git a/README.rst b/README.rst index ee796a8..c1cec20 100644 --- a/README.rst +++ b/README.rst @@ -156,6 +156,7 @@ Random texts from fake import FAKER + FAKER.password() # str FAKER.paragraph() # str FAKER.paragraphs() # list[str] FAKER.sentence() # str diff --git a/fake.py b/fake.py index 8e4b834..d110cda 100644 --- a/fake.py +++ b/fake.py @@ -14,6 +14,7 @@ import os import random import re +import secrets import string import subprocess import tarfile @@ -65,7 +66,7 @@ from uuid import UUID __title__ = "fake.py" -__version__ = "0.10" +__version__ = "0.10.1" __author__ = "Artur Barseghyan " __copyright__ = "2023-2024 Artur Barseghyan" __license__ = "MIT" @@ -1824,6 +1825,42 @@ def pystr(self, nb_chars: int = 20) -> str: """Generate a random string.""" return "".join(random.choices(string.ascii_letters, k=nb_chars)) + @provider(tags=("Text",)) + def password( + self, + length: int = 10, + min_lower: int = 1, + min_upper: int = 1, + min_digits: int = 3, + min_special: int = 0, + ): + if length < min_lower + min_upper + min_digits + min_special: + raise ValueError("Length is too short for the given constraints.") + + rng = secrets.SystemRandom() + + password_chars = ( + [rng.choice(string.ascii_lowercase) for _ in range(min_lower)] + + [rng.choice(string.ascii_uppercase) for _ in range(min_upper)] + + [rng.choice(string.digits) for _ in range(min_digits)] + + [rng.choice(string.punctuation) for _ in range(min_special)] + ) + + remaining_length = length - ( + min_lower + min_upper + min_digits + min_special + ) + if remaining_length > 0: + all_chars = ( + string.ascii_letters + string.digits + string.punctuation + ) + password_chars += [ + rng.choice(all_chars) for _ in range(remaining_length) + ] + + rng.shuffle(password_chars) + + return "".join(password_chars) + @provider(tags=("Python",)) def pyfloat( self, @@ -5615,6 +5652,67 @@ def test_pystr(self) -> None: # Check if all characters are from the valid set self.assertTrue(all(c in valid_characters for c in val)) + def test_password(self): + """Test password.""" + with self.subTest("That has the correct length."): + lengths = [10, 12, 15, 20] + for length in lengths: + with self.subTest(length=length): + pwd = self.faker.password(length=length, min_digits=3) + self.assertEqual( + len(pwd), + length, + f"Password length should be {length}", + ) + with self.subTest("Test contains at least 1 lowercase letter."): + pwd = self.faker.password() + self.assertTrue( + any(c.islower() for c in pwd), + "Password must contain at least 1 lowercase letter.", + ) + with self.subTest("Test contains at least 1 uppercase letter."): + pwd = self.faker.password() + self.assertTrue( + any(c.isupper() for c in pwd), + "Password must contain at least 1 uppercase letter.", + ) + with self.subTest("Test contains at least the min number of digits."): + min_digits = 3 + pwd = self.faker.password(min_digits=min_digits) + digit_count = sum(c.isdigit() for c in pwd) + self.assertGreaterEqual( + digit_count, + min_digits, + f"Password must contain at least {min_digits} digits.", + ) + with self.subTest("Test generator with custom constraints."): + length = 15 + min_digits = 5 + pwd = self.faker.password(length=length, min_digits=min_digits) + self.assertEqual(len(pwd), length, "Password length mismatch.") + self.assertTrue( + any(c.islower() for c in pwd), + "Password must contain at least 1 lowercase letter.", + ) + self.assertTrue( + any(c.isupper() for c in pwd), + "Password must contain at least 1 uppercase letter.", + ) + digit_count = sum(c.isdigit() for c in pwd) + self.assertGreaterEqual( + digit_count, + min_digits, + f"Password must contain at least {min_digits} digits.", + ) + with self.subTest("Test raises ValueError when length too short."): + with self.assertRaises(ValueError): + self.faker.password( + length=4, min_digits=3 + ) # 2 required characters + 3 digits > 4 + with self.subTest("Test multiple generated passwords are unique."): + passwords = set(self.faker.password() for _ in range(25)) + self.assertEqual(len(passwords), 25, "Passwords should be unique.") + def test_pyfloat(self) -> None: ranges = [ (None, None, 0.0, 10.0), diff --git a/pyproject.toml b/pyproject.toml index 35181c9..7fb513c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "fake.py" description = "Minimalistic, standalone alternative fake data generator with no dependencies." readme = "README.rst" -version = "0.10" +version = "0.10.1" dependencies = [] authors = [ {name = "Artur Barseghyan", email = "artur.barseghyan@gmail.com"},