Repository: theskumar/python-usernames (language: python)

Issue:
consider renaming the module to python_usernames
The name of a python-package is used to install it:

`> pip install <package-name>`

The package-name must be unique in pypi,
the associated unique module-name  is the lowercased package-name with dashes replaced by underscores.

`> pip install foo-bar`
`>>> import foo_bar`

so a package named "python-usernames", should use a module named "python_usernames".

Imagine there is another package named "usernames", 
then pip installing your package will remove the code of that package
without complaining.

This is a proper example:
https://github.com/yoavravid/RedditUsernames



Discussion / hints:
Same issue:
> pip install dotenv                 # correct module-name: dotenv
> pip install django-dotenv    # wrong module-name: dotenv
> pip install python-dotenv    # wrong module-name: dotenv



@woutervh I'm onboard with your proposal. I'll have the package names changed. Thanks for creating this issue. 

Fix patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
deleted file mode 100644
index 797fcc6..0000000
--- a/.bumpversion.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-[bumpversion]
-current_version = 0.4.1
-commit = True
-tag = True
-
-[bumpversion:file:setup.py]
diff --git a/.gitignore b/.gitignore
index 4431d1a..ea46ae2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ __pycache__
 .coverage
 .DS_Store
 htmlcov/
+
+.venv/
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..12398d7
--- /dev/null
+++ b/.python-version
@@ -0,0 +1,1 @@
+cpython@3.11.3
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 2cb44c4..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-language: python
-sudo: false
-
-python:
-- '2.7'
-- '3.4'
-- '3.5'
-- '3.6'
-- '3.7'
-- '3.8'
-- pypy
-
-install:
-- pip install python-coveralls
-- pip install -q -r requirements.txt
-- pip install --editable .
-
-before_script: flake8
-
-script:
-- coverage run --source=usernames/ --omit='*tests*' -m py.test tests/ -v --tb=native
-- coverage report
-
-after_script:
-- coveralls
-
-deploy:
-  provider: pypi
-  user: theskumar
-  password:
-    secure: DBGKZgPvE9wUsz0PrsB4ooGTqcOpTSsLbwora1JMX1DVB0t5GhGl66CYhouw3hSm3+ud9jZhxn2EAtVPMKQ4XRbfSAlPqg/FhPo+Gn7CpSHz/ZlFGmQ73rL+uKnJi72rP1s9h6xDCJfRt1eoZRNC6KL+7WW5W37KM7TjdVxxMF58FLEEJCZ4GfKf/Vg35ISLKrUtAEmf4nCoNN2UhfYozZ9Yb+2qNwM5yfPEOqHjjfrGbkTA5hvorTg5lZgbl/1xYGvmXb0ep6Aal4wlHi/rS1o+YR+Q/ctmqZ/FFz0Iq+Ky9OUGVkDFgDz29eiLqlZxOM3ciZbZSI708ltchk3dNeluFgzfHD9l6SrzX5rnUm8oZ8Bqmej5lqxbuRfyp2ZRjRcbQP5LfWdiyZ1V0NzySHnKYn9cj4fUqPZPu8nB4y/765dn39QGyJfoWcA+UIrb/4Itn1/cNJ7wXCz2NeuhJJnZHg2RWvnCiSiwOSVcsf+DyjYA2F/17fEKXrwBAt3HiQHwvVE5CNVwNCFP9HSmZjwZw0nBvQio9tPofS4rgTd//Ot4sWcAQG0MI0R3HnvM/xeGEsdSxoO2RCQx9jDxcvI2G0UmnBoO0SuGfozDKZ758s8URsUvOS3hiCrxHmm3rtWVpkb0gpgD2a6U6VeOKAEQHObA2AM73NWb+DF0heE=
-  on:
-    tags: true
-    repo: theskumar/python-usernames
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 1048fb1..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,22 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2015 Saurabh Kumar
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 0c73842..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,1 +0,0 @@
-include README.rst LICENSE
diff --git a/Makefile b/Makefile
deleted file mode 100644
index a31eae4..0000000
--- a/Makefile
+++ /dev/null
@@ -1,24 +0,0 @@
-.PHONY: clean-pyc clean-build
-
-clean: clean-build clean-pyc
-
-clean-build:
-	rm -fr build/
-	rm -fr dist/
-	rm -fr *.egg-info
-
-clean-pyc:
-	find . -name '*.pyc' -exec rm -f {} +
-	find . -name '*.pyo' -exec rm -f {} +
-	find . -name '*~' -exec rm -f {} +
-
-release: clean sdist
-	twine check dist/*
-	twine upload dist/*
-
-release-test: sdist
-	twine upload --repository-url https://test.pypi.org/legacy/ dist/*
-
-sdist: clean
-	python setup.py sdist bdist_wheel
-	ls -l dist
diff --git a/README.md b/README.md
index 1f8f2cc..5f01f1e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-python-usernames
-================
+# python-usernames
 
 [![Build
 Status](https://travis-ci.org/theskumar/python-usernames.svg?branch=v0.1.0)](https://travis-ci.org/theskumar/python-usernames)
@@ -11,25 +10,22 @@ version](https://badge.fury.io/py/python-usernames.svg)](http://badge.fury.io/py
 Python library to validate usernames suitable for use in public facing
 applications where use can choose login names and sub-domains.
 
-Features
---------
+## Features
 
 -   Provides a default regex validator
 -   Validates against list of [banned
     words](https://github.com/theskumar/python-usernames/blob/master/usernames/reserved_words.py)
     that should not be used as username.
--   Python 2.7, 3.4, 3.5, 3.6, 3.7, 3.8 & pypi
+-   Python 3.8+
 
-Installation
-------------
+## Installation
 
     pip install python-usernames
 
-Usages
-------
+## Usages
 
-``` {.sourceCode .python}
-from usernames import is_safe_username
+```python
+from python_usernames import is_safe_username
 
 >>> is_safe_username("jerk")
 False  # contains one of the banned words
@@ -60,12 +56,12 @@ The default regular expression is as follows:
     (?<![.-])               # no - or . at the end
     $                       # end of string
 
-**Further Reading**
+## Further Reading
 
 -   [Let’s talk about
     usernames](https://www.b-list.org/weblog/2018/feb/11/usernames/)
 
-**Note:**
+## Gotchas
 
 Words like `bigcock12` will validated just fine, only equality against
 the [banned word
@@ -74,7 +70,6 @@ is checked. We don't try to be smart to avoid [Scunthorpe
 problem](https://en.wikipedia.org/wiki/Scunthorpe_problem). If you can
 come up with a algorithm/solution, please create an issue/pr :).
 
-License
--------
+## License
 
 MIT
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..853bcf8
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,67 @@
+[project]
+name = "python-usernames"
+version = "1.0.0"
+description = "Python library to validate usernames suitable for use in public facing applications."
+authors = [
+    { name = "Saurabh Kumar", email = "python-usernames@saurabh-kumar.com" }
+]
+dependencies = []
+readme = "README.md"
+license = "MIT"
+requires-python = ">= 3.8"
+
+keywords = [
+    'security',
+    'username',
+    'validation',
+    'user registration',
+    'safe',
+    'signup',
+    'regex',
+]
+
+classifiers=[
+    # As from https://pypi.org/classifiers/
+    # 'Development Status :: 1 - Planning',
+    # 'Development Status :: 2 - Pre-Alpha',
+    # 'Development Status :: 3 - Alpha',
+    'Development Status :: 4 - Beta',
+    # 'Development Status :: 5 - Production/Stable',
+    # 'Development Status :: 6 - Mature',
+    # 'Development Status :: 7 - Inactive',
+    'Programming Language :: Python',
+    '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',
+    'Intended Audience :: Developers',
+    'Intended Audience :: System Administrators',
+    'License :: OSI Approved :: MIT License',
+    'Operating System :: OS Independent',
+    # 'Topic :: System :: Systems Administration',
+    'Topic :: Utilities',
+    'Topic :: Security',
+    # 'Environment :: Web Environment',
+    # 'Framework :: Django',
+]
+
+[project.urls]
+Documentation = "https://github.com/theskumar/python-usernames/tree/main#readme"
+Source = "https://github.com/theskumar/python-usernames"
+Tracker = "https://github.com/theskumar/python-usernames/issues"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.rye]
+managed = true
+dev-dependencies = [
+    "pytest~=7.3.2",
+    "black~=23.3.0",
+    "ruff~=0.0.272",
+]
+
+[tool.hatch.metadata]
+allow-direct-references = true
diff --git a/requirements-dev.lock b/requirements-dev.lock
new file mode 100644
index 0000000..f85c8e0
--- /dev/null
+++ b/requirements-dev.lock
@@ -0,0 +1,19 @@
+# generated by rye
+# use `rye lock` or `rye sync` to update this lockfile
+#
+# last locked with the following flags:
+#   pre: false
+#   features: []
+#   all-features: false
+
+-e file:.
+black==23.3.0
+click==8.1.3
+iniconfig==2.0.0
+mypy-extensions==1.0.0
+packaging==23.1
+pathspec==0.11.1
+platformdirs==3.5.3
+pluggy==1.0.0
+pytest==7.3.2
+ruff==0.0.272
diff --git a/requirements.lock b/requirements.lock
new file mode 100644
index 0000000..aa6be96
--- /dev/null
+++ b/requirements.lock
@@ -0,0 +1,9 @@
+# generated by rye
+# use `rye lock` or `rye sync` to update this lockfile
+#
+# last locked with the following flags:
+#   pre: false
+#   features: []
+#   all-features: false
+
+-e file:.
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index f43a84d..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-bumpversion
-flake8==2.4.1
-pytest
-twine
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index f654e7a..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,12 +0,0 @@
-[bdist_wheel]
-universal = 1
-
-[flake8]
-max-line-length = 120
-exclude = .tox,.git,venv,docs
-
-[metadata]
-description_file = README.md
-
-[tool:pytest]
-norecursedirs = .svn _build tmp* dist venv* .git
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 9f4fd94..0000000
--- a/setup.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from setuptools import setup
-
-with open('README.md') as readme_file:
-    readme = readme_file.read()
-
-setup(
-    name="python-usernames",
-    description="Python library to validate usernames suitable for use in public facing applications.",
-    long_description=readme,
-    long_description_content_type="text/markdown",
-    version="0.4.1",
-    author="Saurabh Kumar",
-    author_email="me+github@saurabh-kumar.com",
-    url="http://github.com/theskumar/python-usernames",
-
-    packages=[
-        str('usernames'),
-    ],
-    package_dir={'usernames': 'usernames'},
-    include_package_data=True,
-    zip_safe=False,
-    license='MIT',
-    classifiers=[
-        # As from https://pypi.python.org/pypi?%3Aaction=list_classifiers
-        'Development Status :: 1 - Planning',
-        # 'Development Status :: 2 - Pre-Alpha',
-        # 'Development Status :: 3 - Alpha',
-        # 'Development Status :: 4 - Beta',
-        # 'Development Status :: 5 - Production/Stable',
-        # 'Development Status :: 6 - Mature',
-        # 'Development Status :: 7 - Inactive',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 2',
-        # 'Programming Language :: Python :: 2.3',
-        # 'Programming Language :: Python :: 2.4',
-        # 'Programming Language :: Python :: 2.5',
-        'Programming Language :: Python :: 2.6',
-        'Programming Language :: Python :: 2.7',
-        'Programming Language :: Python :: 3',
-        # 'Programming Language :: Python :: 3.0',
-        # 'Programming Language :: Python :: 3.1',
-        # 'Programming Language :: Python :: 3.2',
-        'Programming Language :: Python :: 3.3',
-        'Programming Language :: Python :: 3.4',
-        'Programming Language :: Python :: 3.5',
-        'Programming Language :: Python :: 3.6',
-        'Programming Language :: Python :: 3.7',
-        # 'Programming Language :: Python :: 3.4',
-        'Intended Audience :: Developers',
-        'Intended Audience :: System Administrators',
-        'License :: OSI Approved :: MIT License',
-        'Operating System :: OS Independent',
-        # 'Topic :: System :: Systems Administration',
-        'Topic :: Utilities',
-        # 'Environment :: Web Environment',
-        # 'Framework :: Django',
-    ],
-    keywords=[
-        'username', 'validation', 'registration', 'python', 'safe', 'signup'
-    ],
-)
-
-# (*) Please direct queries to the discussion group, rather than to me directly
-#     Doing so helps ensure your question is helpful to other users.
-#     Queries directly to my email are likely to receive a canned response.
-#
-#     Many thanks for your understanding.
diff --git a/usernames/__init__.py b/src/python_usernames/__init__.py
similarity index 75%
rename from usernames/__init__.py
rename to src/python_usernames/__init__.py
index 388515f..4a7ecac 100644
--- a/usernames/__init__.py
+++ b/src/python_usernames/__init__.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 from .validators import is_safe_username
 
 __all__ = ["is_safe_username"]
diff --git a/src/python_usernames/reserved_words.py b/src/python_usernames/reserved_words.py
new file mode 100644
index 0000000..ccddf9e
--- /dev/null
+++ b/src/python_usernames/reserved_words.py
@@ -0,0 +1,30 @@
+"""
+List of reserved usernames (pre-defined list of special banned and reserved
+keywords in names, such as "root", "www", "admin"). Useful when creating
+public systems, where users can choose a login name or a sub-domain name.
+
+__References:__
+1. http://www.bannedwordlist.com/
+2. http://blog.postbit.com/reserved-username-list.html
+3. https://ldpreload.com/blog/names-to-reserve
+"""
+
+try:
+    import importlib.resources as pkg_resources
+except ImportError:
+    # Try backported to PY<37 `importlib_resources`.
+    import importlib_resources as pkg_resources
+
+def get_reserved_words():
+    content = ''
+    # https://stackoverflow.com/questions/6028000/
+    try:
+        with (pkg_resources.files("python_usernames") / "words.txt").open() as _f:
+            content = _f.read()
+    except AttributeError:
+        # Python < PY3.9, fall back to method deprecated in PY3.11.
+        content = pkg_resources.read_text("python_usernames", 'words.txt')
+
+    return set(content.splitlines())
+
+__all__ = ["get_reserved_words"]
diff --git a/src/python_usernames/validators.py b/src/python_usernames/validators.py
new file mode 100644
index 0000000..7adcb7d
--- /dev/null
+++ b/src/python_usernames/validators.py
@@ -0,0 +1,50 @@
+import re
+
+from .reserved_words import get_reserved_words
+
+
+username_regex = re.compile(
+    r"""
+    ^                       # beginning of string
+    (?!_$)                  # no only _
+    (?![-.])                # no - or . at the beginning
+    (?!.*[_.-]{2})          # no __ or _. or ._ or .. or -- inside
+    [a-zA-Z0-9_.-]+         # allowed characters, atleast one must be present
+    (?<![.-])               # no - or . at the end
+    $                       # end of string
+    """,
+    re.X,
+)
+
+
+def is_safe_username(
+    username: str, whitelist=None, blacklist=None, regex=username_regex, max_length=None
+) -> bool:
+    # check for max length
+    if max_length and len(username) > max_length:
+        return False
+
+    # check against provided regex
+    if not re.match(regex, username):
+        return False
+
+    # ensure the word is not in the blacklist and is not a reserved word
+    if whitelist is None:
+        whitelist = []
+
+    if blacklist is None:
+        blacklist = []
+
+    default_words = get_reserved_words()
+
+    whitelist = set(
+        [each_whitelisted_name.lower() for each_whitelisted_name in whitelist]
+    )
+    blacklist = set(
+        [each_blacklisted_name.lower() for each_blacklisted_name in blacklist]
+    )
+
+    default_words = default_words - whitelist
+    default_words = default_words.union(blacklist)
+
+    return False if username.lower() in default_words else True
diff --git a/usernames/reserved_words.py b/src/python_usernames/words.txt
similarity index 85%
rename from usernames/reserved_words.py
rename to src/python_usernames/words.txt
index 5311d84..eee9930 100644
--- a/usernames/reserved_words.py
+++ b/src/python_usernames/words.txt
@@ -1,17 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-"""
-List of reserved usernames (pre-defined list of special banned and reserved keywords in names,
-such as "root", "www", "admin"). Useful when creating public systems, where users can choose
-a login name or a sub-domain name.
-__References:__
-1. http://www.bannedwordlist.com/
-2. http://blog.postbit.com/reserved-username-list.html
-3. https://ldpreload.com/blog/names-to-reserve
-"""
-
-_d = """
 about
 abuse
 access
@@ -502,12 +488,3 @@
 yourname
 yoursite
 yourusername
-"""
-
-
-
-def get_reserved_wordlist():
-    return set(_d.splitlines())
-
-
-__all__ = ["get_reserved_wordlist"]
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index c1f78bb..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,17 +0,0 @@
-# tox (https://tox.readthedocs.io/) is a tool for running tests
-# in multiple virtualenvs. This configuration file will run the
-# test suite on all supported python versions. To use it, "pip install tox"
-# and then run "tox" from this directory.
-
-[tox]
-envlist = py27, py34, py35, py36, py37
-
-[testenv]
-deps =
-
-commands =
-    pip install python-coveralls
-    pip install -q -r requirements.txt
-    pip install --editable .
-    python tests/test_usernames.py
-
diff --git a/usernames/validators.py b/usernames/validators.py
deleted file mode 100644
index 4a83fe1..0000000
--- a/usernames/validators.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-import re
-
-from .reserved_words import get_reserved_wordlist
-
-
-username_regex = re.compile(
-    r"""
-    ^                       # beginning of string
-    (?!_$)                  # no only _
-    (?![-.])                # no - or . at the beginning
-    (?!.*[_.-]{2})          # no __ or _. or ._ or .. or -- inside
-    [a-zA-Z0-9_.-]+         # allowed characters, atleast one must be present
-    (?<![.-])               # no - or . at the end
-    $                       # end of string
-    """,
-    re.X,
-)
-
-
-def is_safe_username(
-    username, whitelist=[], blacklist=[], regex=username_regex, max_length=None
-):
-    if max_length and len(username) > max_length:
-        return False
-    if not re.match(regex, username):
-        return False
-    wordlist = get_reserved_wordlist()
-    whitelist = set([each_whitelisted_name.lower() for each_whitelisted_name in whitelist])
-    blacklist = set([each_blacklisted_name.lower() for each_blacklisted_name in blacklist])
-    wordlist = wordlist - whitelist
-    wordlist = wordlist.union(blacklist)
-    return False if username.lower() in wordlist else True


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..3461b63
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,34 @@
+name: Test
+
+on: [push]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -r requirements-dev.lock
+
+      - name: Lint with ruff
+        run: |
+          # stop the build if there are Python syntax errors or undefined names
+          ruff --format=github --select=E9,F63,F7,F82 --target-version=py37 .
+          # default set of ruff rules with GitHub Annotations
+          ruff --format=github --target-version=py37 .
+      - name: Test with pytest
+        run: |
+          pytest
diff --git a/tests/test_usernames.py b/tests/test_validators.py
similarity index 96%
rename from tests/test_usernames.py
rename to tests/test_validators.py
index d2f44b2..1e13e7f 100644
--- a/tests/test_usernames.py
+++ b/tests/test_validators.py
@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
-from usernames import is_safe_username
+from python_usernames import is_safe_username
 
 
 def test_max_lenght():