├── .editorconfig ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .python-version ├── README.md ├── pyproject.toml ├── requirements-dev.lock ├── requirements.lock ├── src └── python_usernames │ ├── __about__.py │ ├── __init__.py │ ├── reserved_words.py │ ├── validators.py │ └── words.txt └── tests ├── __init__.py └── test_validators.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # see: http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.yml] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | pypi-publish: 9 | name: upload release to PyPI 10 | 11 | runs-on: ubuntu-latest 12 | # Specifying a GitHub environment is optional, but strongly encouraged 13 | environment: release 14 | permissions: 15 | # IMPORTANT: this permission is mandatory for trusted publishing 16 | id-token: write 17 | 18 | steps: 19 | # retrieve your distributions here 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set up Python 3.11 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: "3.11" 26 | 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install hatch 31 | 32 | - name: Build package distributions 33 | run: | 34 | hatch build 35 | 36 | - name: Publish package distributions to PyPI 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.8", "3.9", "3.10", "3.11"] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements-dev.lock 25 | 26 | - name: Lint with ruff 27 | run: | 28 | # stop the build if there are Python syntax errors or undefined names 29 | ruff --format=github --select=E9,F63,F7,F82 --target-version=py37 . 30 | # default set of ruff rules with GitHub Annotations 31 | ruff --format=github --target-version=py37 . 32 | - name: Test with pytest 33 | run: | 34 | pytest 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | .cache/ 4 | build/ 5 | dist/ 6 | .env 7 | __pycache__ 8 | .coverage 9 | .DS_Store 10 | htmlcov/ 11 | 12 | .venv/ 13 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | cpython@3.11.3 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-usernames 2 | 3 | [![Build 4 | Status](https://travis-ci.org/theskumar/python-usernames.svg?branch=v0.1.0)](https://travis-ci.org/theskumar/python-usernames) 5 | [![Coverage 6 | Status](https://coveralls.io/repos/theskumar/python-usernames/badge.svg?branch=master&service=github)](https://coveralls.io/github/theskumar/python-usernames?branch=master) 7 | [![PyPI 8 | version](https://badge.fury.io/py/python-usernames.svg)](http://badge.fury.io/py/python-usernames) 9 | 10 | Python library to validate usernames suitable for use in public facing 11 | applications where use can choose login names and sub-domains. 12 | 13 | ## Features 14 | 15 | - Provides a default regex validator 16 | - Validates against list of [banned 17 | words](https://github.com/theskumar/python-usernames/blob/master/usernames/reserved_words.py) 18 | that should not be used as username. 19 | - Python 3.8+ 20 | 21 | ## Installation 22 | 23 | pip install python-usernames 24 | 25 | ## Usages 26 | 27 | ```python 28 | from python_usernames import is_safe_username 29 | 30 | >>> is_safe_username("jerk") 31 | False # contains one of the banned words 32 | 33 | >>> is_safe_username("handsome!") 34 | False # contains non-url friendly `!` 35 | ``` 36 | 37 | **is\_safe\_username** takes the following optional arguments: 38 | 39 | - `whitelist`: a case insensitive list of words that should be 40 | considered as always safe. Default: `[]` 41 | - `blacklist`: a case insensitive list of words that should be 42 | considered as unsafe. Default: `[]` 43 | - `max_length`: specify the maximun character a username can have. 44 | Default: `None` 45 | 46 | - `regex`: regular expression string that must pass before the banned 47 | : words is checked. 48 | 49 | The default regular expression is as follows: 50 | 51 | ^ # beginning of string 52 | (?!_$) # no only _ 53 | (?![-.]) # no - or . at the beginning 54 | (?!.*[_.-]{2}) # no __ or _. or ._ or .. or -- inside 55 | [a-zA-Z0-9_.-]+ # allowed characters, atleast one must be present 56 | (?= 3.8" 11 | dynamic = ["version"] 12 | 13 | keywords = [ 14 | 'security', 15 | 'username', 16 | 'validation', 17 | 'user registration', 18 | 'safe', 19 | 'signup', 20 | 'regex', 21 | ] 22 | 23 | classifiers=[ 24 | # As from https://pypi.org/classifiers/ 25 | # 'Development Status :: 1 - Planning', 26 | # 'Development Status :: 2 - Pre-Alpha', 27 | # 'Development Status :: 3 - Alpha', 28 | 'Development Status :: 4 - Beta', 29 | # 'Development Status :: 5 - Production/Stable', 30 | # 'Development Status :: 6 - Mature', 31 | # 'Development Status :: 7 - Inactive', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3.8', 34 | 'Programming Language :: Python :: 3.9', 35 | 'Programming Language :: Python :: 3.10', 36 | 'Programming Language :: Python :: 3.11', 37 | 'Programming Language :: Python :: 3.12', 38 | 'Intended Audience :: Developers', 39 | 'Intended Audience :: System Administrators', 40 | 'License :: OSI Approved :: MIT License', 41 | 'Operating System :: OS Independent', 42 | # 'Topic :: System :: Systems Administration', 43 | 'Topic :: Utilities', 44 | 'Topic :: Security', 45 | # 'Environment :: Web Environment', 46 | # 'Framework :: Django', 47 | ] 48 | 49 | [project.urls] 50 | Documentation = "https://github.com/theskumar/python-usernames/tree/main#readme" 51 | Source = "https://github.com/theskumar/python-usernames" 52 | Tracker = "https://github.com/theskumar/python-usernames/issues" 53 | 54 | [build-system] 55 | requires = ["hatchling"] 56 | build-backend = "hatchling.build" 57 | 58 | [tool.rye] 59 | managed = true 60 | dev-dependencies = [ 61 | "pytest~=7.3.2", 62 | "black~=23.3.0", 63 | "ruff~=0.0.272", 64 | "hatch~=1.7.0", 65 | ] 66 | 67 | [tool.hatch.metadata] 68 | allow-direct-references = true 69 | 70 | [tool.hatch.build.targets.sdist] 71 | include = [ 72 | "/src", 73 | ] 74 | 75 | [tool.hatch.build.targets.wheel] 76 | packages = ["src/python_usernames"] 77 | 78 | [tool.hatch.version] 79 | path = "src/python_usernames/__about__.py" 80 | -------------------------------------------------------------------------------- /requirements-dev.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | 9 | -e file:. 10 | anyio==3.7.0 11 | black==23.3.0 12 | certifi==2023.5.7 13 | click==8.1.3 14 | distlib==0.3.6 15 | editables==0.3 16 | filelock==3.12.2 17 | h11==0.14.0 18 | hatch==1.7.0 19 | hatchling==1.18.0 20 | httpcore==0.17.2 21 | httpx==0.24.1 22 | hyperlink==21.0.0 23 | idna==3.4 24 | importlib-metadata==6.6.0 25 | iniconfig==2.0.0 26 | jaraco-classes==3.2.3 27 | keyring==23.13.1 28 | markdown-it-py==3.0.0 29 | mdurl==0.1.2 30 | more-itertools==9.1.0 31 | mypy-extensions==1.0.0 32 | packaging==23.1 33 | pathspec==0.11.1 34 | pexpect==4.8.0 35 | platformdirs==3.5.3 36 | pluggy==1.0.0 37 | ptyprocess==0.7.0 38 | pygments==2.15.1 39 | pyperclip==1.8.2 40 | pytest==7.3.2 41 | rich==13.4.2 42 | ruff==0.0.272 43 | shellingham==1.5.0.post1 44 | sniffio==1.3.0 45 | tomli-w==1.0.0 46 | tomlkit==0.11.8 47 | trove-classifiers==2023.5.24 48 | userpath==1.8.0 49 | virtualenv==20.23.1 50 | zipp==3.15.0 51 | -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | 9 | -e file:. 10 | -------------------------------------------------------------------------------- /src/python_usernames/__about__.py: -------------------------------------------------------------------------------- 1 | VERSION = "1.0.0" 2 | -------------------------------------------------------------------------------- /src/python_usernames/__init__.py: -------------------------------------------------------------------------------- 1 | from .validators import is_safe_username 2 | 3 | __version__ = "0.4.1" 4 | 5 | __all__ = ["is_safe_username"] 6 | -------------------------------------------------------------------------------- /src/python_usernames/reserved_words.py: -------------------------------------------------------------------------------- 1 | """ 2 | List of reserved usernames (pre-defined list of special banned and reserved 3 | keywords in names, such as "root", "www", "admin"). Useful when creating 4 | public systems, where users can choose a login name or a sub-domain name. 5 | 6 | __References:__ 7 | 1. http://www.bannedwordlist.com/ 8 | 2. http://blog.postbit.com/reserved-username-list.html 9 | 3. https://ldpreload.com/blog/names-to-reserve 10 | """ 11 | 12 | try: 13 | import importlib.resources as pkg_resources 14 | except ImportError: 15 | # Try backported to PY<37 `importlib_resources`. 16 | import importlib_resources as pkg_resources 17 | 18 | 19 | def get_reserved_words(): 20 | content = "" 21 | # https://stackoverflow.com/questions/6028000/ 22 | try: 23 | with (pkg_resources.files("python_usernames") / "words.txt").open() as _f: 24 | content = _f.read() 25 | except AttributeError: 26 | # Python < PY3.9, fall back to method deprecated in PY3.11. 27 | content = pkg_resources.read_text("python_usernames", "words.txt") 28 | 29 | return set(content.splitlines()) 30 | 31 | 32 | __all__ = ["get_reserved_words"] 33 | -------------------------------------------------------------------------------- /src/python_usernames/validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .reserved_words import get_reserved_words 4 | 5 | 6 | username_regex = re.compile( 7 | r""" 8 | ^ # beginning of string 9 | (?!_$) # no only _ 10 | (?![-.]) # no - or . at the beginning 11 | (?!.*[_.-]{2}) # no __ or _. or ._ or .. or -- inside 12 | [a-zA-Z0-9_.-]+ # allowed characters, atleast one must be present 13 | (? bool: 23 | # check for max length 24 | if max_length and len(username) > max_length: 25 | return False 26 | 27 | # check against provided regex 28 | if not re.match(regex, username): 29 | return False 30 | 31 | # ensure the word is not in the blacklist and is not a reserved word 32 | if whitelist is None: 33 | whitelist = [] 34 | 35 | if blacklist is None: 36 | blacklist = [] 37 | 38 | default_words = get_reserved_words() 39 | 40 | whitelist = set( 41 | [each_whitelisted_name.lower() for each_whitelisted_name in whitelist] 42 | ) 43 | blacklist = set( 44 | [each_blacklisted_name.lower() for each_blacklisted_name in blacklist] 45 | ) 46 | 47 | default_words = default_words - whitelist 48 | default_words = default_words.union(blacklist) 49 | 50 | return False if username.lower() in default_words else True 51 | -------------------------------------------------------------------------------- /src/python_usernames/words.txt: -------------------------------------------------------------------------------- 1 | about 2 | abuse 3 | access 4 | account 5 | accounts 6 | add 7 | address 8 | adm 9 | admin 10 | administration 11 | administrator 12 | ads 13 | adult 14 | advertising 15 | affiliate 16 | affiliates 17 | ajax 18 | anal 19 | analytics 20 | android 21 | anon 22 | anonymous 23 | anus 24 | apache 25 | api 26 | app 27 | apps 28 | archive 29 | arse 30 | ass 31 | atom 32 | audio 33 | auth 34 | authentication 35 | authority 36 | autoconfig 37 | avatar 38 | backup 39 | balls 40 | ballsack 41 | banned 42 | banner 43 | banners 44 | bastard 45 | biatch 46 | billing 47 | bin 48 | bitch 49 | blocked 50 | blog 51 | blogs 52 | bloody 53 | blowjob 54 | board 55 | bollock 56 | bollok 57 | boner 58 | boob 59 | bot 60 | bots 61 | broadcasthost 62 | bugger 63 | bum 64 | business 65 | butt 66 | buttplug 67 | ca 68 | cache 69 | cadastro 70 | calendar 71 | campaign 72 | careers 73 | ceo 74 | cert 75 | cgi 76 | chat 77 | client 78 | cliente 79 | clitoris 80 | cock 81 | code 82 | comercial 83 | commerce 84 | compare 85 | compras 86 | config 87 | connect 88 | console 89 | contact 90 | contest 91 | coon 92 | copyright 93 | crap 94 | create 95 | crt 96 | css 97 | cunt 98 | daemon 99 | damn 100 | dashboard 101 | data 102 | db 103 | delete 104 | demo 105 | design 106 | designer 107 | dev 108 | devel 109 | devs 110 | dick 111 | dildo 112 | dir 113 | directory 114 | dns 115 | doc 116 | docs 117 | domain 118 | download 119 | downloads 120 | dyke 121 | ecommerce 122 | edit 123 | editor 124 | email 125 | end 126 | errors 127 | events 128 | example 129 | exploit 130 | fag 131 | faq 132 | faqs 133 | favorite 134 | feck 135 | feed 136 | feedback 137 | felching 138 | fellate 139 | fellatio 140 | file 141 | files 142 | firewall 143 | flange 144 | flog 145 | follow 146 | forum 147 | forums 148 | free 149 | ftp 150 | fuck 151 | fucker 152 | fudge 153 | fudgepacker 154 | gadget 155 | gadgets 156 | games 157 | god 158 | goddamn 159 | group 160 | groups 161 | guarantee 162 | guest 163 | guests 164 | hacker 165 | hell 166 | help 167 | home 168 | homepage 169 | homo 170 | host 171 | hosting 172 | hostmaster 173 | hostname 174 | hpg 175 | hr 176 | html 177 | http 178 | httpd 179 | https 180 | illegal 181 | image 182 | images 183 | imap 184 | img 185 | index 186 | indice 187 | info 188 | information 189 | inquiry 190 | intranet 191 | intro 192 | introduction 193 | invite 194 | invoice 195 | invoices 196 | ipad 197 | iphone 198 | irc 199 | is 200 | isatap 201 | it 202 | java 203 | javascript 204 | jerk 205 | jizz 206 | job 207 | jobs 208 | js 209 | knob 210 | knobend 211 | knowledgebase 212 | labia 213 | licence 214 | license 215 | licensing 216 | links 217 | list 218 | list-request 219 | lists 220 | live 221 | lmao 222 | lmfao 223 | localdomain 224 | localhost 225 | log 226 | login 227 | logout 228 | logs 229 | magazine 230 | mail 231 | mail1 232 | mail2 233 | mail3 234 | mail4 235 | mail5 236 | mailer 237 | mailer-daemon 238 | mailing 239 | maintainer 240 | manager 241 | marketing 242 | master 243 | me 244 | media 245 | message 246 | messenger 247 | microblog 248 | microblogs 249 | mine 250 | mis 251 | mob 252 | mobile 253 | mod 254 | moderator 255 | moderators 256 | mods 257 | motherfucker 258 | movie 259 | movies 260 | mp3 261 | msg 262 | msn 263 | muff 264 | music 265 | musicas 266 | mx 267 | my 268 | mysql 269 | name 270 | named 271 | net 272 | network 273 | new 274 | news 275 | newsletter 276 | nginx 277 | nick 278 | nickname 279 | nigga 280 | nigger 281 | no-reply 282 | noaccess 283 | nobody 284 | noc 285 | noreply 286 | notes 287 | noticias 288 | ns 289 | ns1 290 | ns2 291 | ns3 292 | ns4 293 | ntp 294 | null 295 | offensive 296 | old 297 | omg 298 | online 299 | operator 300 | operators 301 | order 302 | orders 303 | packer 304 | page 305 | pager 306 | pages 307 | panel 308 | passwd 309 | password 310 | pay 311 | payment 312 | payments 313 | penis 314 | perl 315 | photo 316 | photoalbum 317 | photos 318 | php 319 | pic 320 | pics 321 | piss 322 | plugin 323 | plugins 324 | policies 325 | policy 326 | poop 327 | pop 328 | pop3 329 | post 330 | postfix 331 | postmaster 332 | posts 333 | pr 334 | pricing 335 | prick 336 | printer 337 | privacy 338 | profile 339 | project 340 | projects 341 | promo 342 | promotion 343 | proxy 344 | pub 345 | pube 346 | public 347 | purchase 348 | pussy 349 | python 350 | queer 351 | qwerty 352 | random 353 | redir 354 | refund 355 | refunds 356 | register 357 | registration 358 | return 359 | returns 360 | reviews 361 | root 362 | rss 363 | ruby 364 | sale 365 | sales 366 | sample 367 | samples 368 | scammer 369 | script 370 | scripts 371 | scrotum 372 | search 373 | secure 374 | security 375 | send 376 | sendmail 377 | service 378 | services 379 | setting 380 | settings 381 | setup 382 | sex 383 | sh1t 384 | shit 385 | shop 386 | signin 387 | signup 388 | site 389 | sitemap 390 | sites 391 | slut 392 | smegma 393 | smtp 394 | soporte 395 | spammer 396 | spunk 397 | sql 398 | squid 399 | ssh 400 | ssl 401 | ssladmin 402 | ssladministrator 403 | sslwebmaster 404 | stage 405 | staging 406 | start 407 | stat 408 | static 409 | stats 410 | status 411 | store 412 | stores 413 | stream 414 | subdomain 415 | subscribe 416 | sudo 417 | superuser 418 | suporte 419 | support 420 | survey 421 | suspended 422 | sync 423 | sys 424 | sysadmin 425 | syslog 426 | sysop 427 | system 428 | tablet 429 | tablets 430 | talk 431 | task 432 | tasks 433 | tech 434 | telnet 435 | terminal 436 | terms 437 | test 438 | test1 439 | test2 440 | test3 441 | teste 442 | testimonials 443 | tests 444 | testuser 445 | theme 446 | themes 447 | tit 448 | tmp 449 | todo 450 | tools 451 | tosser 452 | tour 453 | turd 454 | tv 455 | twat 456 | update 457 | upload 458 | url 459 | usage 460 | usenet 461 | user 462 | userdb 463 | username 464 | users 465 | usuario 466 | uucp 467 | vagina 468 | vendas 469 | video 470 | videos 471 | visitor 472 | vulgarity 473 | wank 474 | web 475 | webmail 476 | webmaster 477 | website 478 | websites 479 | wheel 480 | whore 481 | win 482 | workshop 483 | wpad 484 | wtf 485 | ww 486 | wws 487 | www 488 | www1 489 | www2 490 | www3 491 | www4 492 | www5 493 | www6 494 | www7 495 | wwws 496 | wwww 497 | xpg 498 | xxx 499 | you 500 | yourdomain 501 | yourname 502 | yoursite 503 | yourusername 504 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theskumar/python-usernames/8784b15ac5a39984f4556a353658ba2c7b5cdb3b/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_validators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from python_usernames import is_safe_username 4 | 5 | 6 | def test_max_lenght(): 7 | assert is_safe_username("u" * 10, max_length=10) 8 | assert not is_safe_username("u" * 11, max_length=10) 9 | 10 | 11 | def test_blacklist(): 12 | assert not is_safe_username("helo", blacklist=["helo"]) 13 | assert not is_safe_username("helo", blacklist=["Helo"]) 14 | 15 | 16 | def test_whitelist(): 17 | assert not is_safe_username("he..lo", whitelist=["he..lo"]) 18 | assert is_safe_username("fuck", whitelist=["fuck"]) 19 | assert is_safe_username("fuck", whitelist=["Fuck"]) 20 | 21 | 22 | def test_usernames(): 23 | unsafe_words = [ 24 | "!", 25 | "#", 26 | "", 27 | "()", 28 | "-", 29 | "-hello", 30 | ".", 31 | ".hello", 32 | "_", 33 | "a@!/", 34 | "fuck", 35 | "hel--lo", 36 | "hel-.lo", 37 | "hel..lo", 38 | "hel__lo", 39 | "hello-", 40 | "hello.", 41 | "sex", 42 | "\\", 43 | "\\\\", 44 | "--1", 45 | "!@#$%^&*()`~", 46 | "`⁄€‹›fifl‡°·‚—±", 47 | "⅛⅜⅝⅞", 48 | "😍", 49 | "👩🏽", 50 | "👾 🙇 💁 🙅 🙆 🙋 🙎 🙍 ", 51 | "🐵 🙈 🙉 🙊", 52 | "❤️ 💔 💌 💕 💞 💓 💗 💖 💘 💝 💟 💜 💛 💚 💙", 53 | "✋🏿 💪🏿 👐🏿 🙌🏿 👏🏿 🙏🏿", 54 | "🚾 🆒 🆓 🆕 🆖 🆗 🆙 🏧", 55 | "0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟", 56 | "123", 57 | "١٢٣", 58 | "ثم نفس سقطت وبالتحديد،, جزيرتي باستخدام أن دنو. إذ هنا؟ الستار وتنصيب كان. أهّل ايطاليا، بريطانيا-فرنسا قد أخذ. سليمان، إتفاقية بين ما, يذكر الحدود أي بعد, معاملة بولندا، الإطلاق عل إيو.", # noqa 59 | "בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ", 60 | "הָיְתָהtestالصفحات التّحول", 61 | "﷽", 62 | "ﷺ", 63 | " ", 64 | "𝐓𝐡𝐞", 65 | "⒯⒣⒠", 66 | "Powerلُلُصّبُلُلصّبُررًॣॣhॣॣ冗", 67 | ] 68 | 69 | safe_words = [ 70 | "a" "10101", 71 | "1he-llo", 72 | "_hello", 73 | "he-llo", 74 | "he.llo_", 75 | "hello", 76 | "hello_", 77 | "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", 78 | ] 79 | 80 | for w in unsafe_words: 81 | assert not is_safe_username(w) 82 | 83 | for w in safe_words: 84 | assert is_safe_username(w) 85 | --------------------------------------------------------------------------------