New Upstream Release - pytest-helpers-namespace

Ready changes

Summary

Merged new upstream version: 2021.12.29 (was: 2021.4.29).

Resulting package

Built on 2023-01-11T14:17 (took 5m7s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-pytest-helpers-namespace

Lintian Result

Diff

diff --git a/.coveragerc b/.coveragerc
index 307c21a..6cb08cb 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -8,6 +8,7 @@ relative_files = True
 omit =
   .nox/*
   setup.py
+  noxfile.py
 
 [report]
 # Regexes for lines to exclude from consideration
@@ -20,24 +21,26 @@ exclude_lines =
 
     # Don't complain if tests don't hit defensive assertion code:
     raise AssertionError
+    raise NotImplemented
     raise NotImplementedError
 
     # Don't complain if non-runnable code isn't run:
     if 0:
     if False:
     if __name__ == .__main__.:
+    if TYPE_CHECKING:
 
 omit =
   .nox/*
   setup.py
-  src/pytest_helpers_namespace/version.py
-  tests/support/coverage/sitecustomize.py
+  noxfile.py
 
 
 ignore_errors = True
 
 [paths]
 source =
-  src/pytest_helpers_namespace
+  src/pytest_helpers_namespace/
+  **/site-packages/pytest_helpers_namespace/
 testsuite =
   tests/
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..a0cebdf
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,14 @@
+# See GitHub's Docs About Code Owners
+# for more info about the CODEOWNERS file
+
+
+# This is a comment.
+# Each line is a file pattern followed by one or more owners.
+
+# These owners will be the default owners for everything in
+# the repo. Unless a later match takes precedence,
+# @and will be requested for
+# review when someone opens a pull request.
+
+# Team Core
+*       @saltstack/team-core
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 9182cce..9cfe300 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -4,41 +4,45 @@ on: [push, pull_request]
 
 jobs:
   Pre-Commit:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v2
     - name: Set up Python
       uses: actions/setup-python@v2
       with:
         python-version: 3.7
-    - id: changed-files
-      name: Get Changed Files
-      uses: dorny/paths-filter@v2
-      with:
-        token: ${{ github.token }}
-        list-files: shell
-        filters: |
-          repo:
-            - added|modified:
-              - '**'
     - name: Set Cache Key
       run: echo "PY=$(python --version --version | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
-    - uses: actions/cache@v2
+    - name: Install System Deps
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y libxml2 libxml2-dev libxslt-dev
+    - uses: actions/cache@v1
       with:
         path: ~/.cache/pre-commit
         key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
-    - name: Check ALL Files On Branch
-      uses: pre-commit/action@v2.0.0
-      if: github.event_name != 'pull_request'
-    - name: Check Changed Files On PR
-      uses: pre-commit/action@v2.0.0
-      if: github.event_name == 'pull_request'
-      with:
-        extra_args: --files ${{ steps.changed-files.outputs.repo_files }}
+    - uses: pre-commit/action@v1.0.1
 
+  Twine-Check:
+    runs-on: ubuntu-latest
+    needs: Pre-Commit
 
-  Docs:
-    runs-on: ubuntu-20.04
+    steps:
+    - uses: actions/checkout@v2
+    - name: Setup Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.8'
+    - name: Install Nox
+      run: |
+        python -m pip install --upgrade pip
+        pip install nox
+    - name: Twine check
+      run: |
+        nox -e twine-check
+
+  PyLint:
+    runs-on: ubuntu-latest
     needs: Pre-Commit
 
     timeout-minutes: 10
@@ -56,10 +60,34 @@ jobs:
         python -m pip install --upgrade pip
         pip install nox
 
-    - name: Set up Python ${{ matrix.python-version }}
+    - name: Install Lint Requirements
+      run: |
+        nox --force-color -e lint --install-only
+
+    - name: Build Docs
+      env:
+        SKIP_REQUIREMENTS_INSTALL: YES
+      run: |
+        nox --force-color -e lint
+
+  Docs:
+    runs-on: ubuntu-latest
+    needs: Pre-Commit
+
+    timeout-minutes: 10
+
+    steps:
+    - uses: actions/checkout@v2
+
+    - name: Set up Python 3.7 For Nox
       uses: actions/setup-python@v2
       with:
-        python-version: ${{ matrix.python-version }}
+        python-version: 3.7
+
+    - name: Install Nox
+      run: |
+        python -m pip install --upgrade pip
+        pip install nox
 
     - name: Install Doc Requirements
       run: |
@@ -72,25 +100,32 @@ jobs:
         nox --force-color -e docs
 
   Linux:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-latest
     needs: Pre-Commit
 
-    timeout-minutes: 20
+    timeout-minutes: 15
 
     strategy:
       fail-fast: false
-      max-parallel: 5
+      max-parallel: 4
       matrix:
         python-version:
-          - 3.5
-          - 3.6
-          - 3.7
-          - 3.8
-          - 3.9
+          - "3.5"
+          - "3.6"
+          - "3.7"
+          - "3.8"
+          - "3.9"
+          - "3.10"
         pytest-version:
           - "~=6.0.0"
           - "~=6.1.0"
-          - ""
+          - "~=6.2.0"
+          - ">=7.0.0rc1"
+        exclude:
+          - {"python-version": "3.5", "pytest-version": "~=6.2.0"}
+          - {"python-version": "3.5", "pytest-version": ">=7.0.0rc1"}
+          - {"python-version": "3.10", "pytest-version": "~=6.0.0"}
+          - {"python-version": "3.10", "pytest-version": "~=6.1.0"}
 
     steps:
     - uses: actions/checkout@v2
@@ -103,84 +138,133 @@ jobs:
     - name: Install Nox
       run: |
         python -m pip install --upgrade pip
-        pip install nox
+        pip install nox 'pytest${{ matrix.pytest-version }}'
 
     - name: Install Test Requirements
       env:
         PYTEST_VERSION_REQUIREMENT: pytest${{ matrix.pytest-version }}
       run: |
-        nox --force-color -e tests-${{ matrix.python-version }} --install-only
+        nox --force-color -e tests-3 --install-only
 
     - name: Test
-      id: run-tests
       env:
         SKIP_REQUIREMENTS_INSTALL: YES
       run: |
-        nox --force-color -e tests-${{ matrix.python-version }} -- -vv tests/
+        nox --force-color -e tests-3 -- -vv tests/
+
+    - name: Gather CodeCov Info
+      if: always()
+      id: codecov-info
+      run: |
+        echo ::set-output name=flag-python-version::$(python -c "import sys; print('Py{}{}'.format(*sys.version_info))")
+        echo ::set-output name=flag-pytest-version::$(python -c "import pytest; print('PyTest{}{}'.format(*pytest.__version__.split('.')))")
+        echo ::set-output name=flag-runner-os::$(python -c "print('${{ runner.os }}'.replace('-latest', ''))")
+        echo ::set-output name=uploader-url::$(python -c "print('https://uploader.codecov.io/latest/codecov-linux')")
+        echo ::set-output name=uploader-name::$(python -c "print('codecov-linux')")
 
     - name: Create CodeCov Flags
       if: always()
-      id: codecov-flags
+      id: codecov
       run: |
-        echo ::set-output name=flags::$(python -c "import sys; print('{},{},pytest${{ matrix.pytest-version || 'latest' }}'.format('${{ runner.os }}'.replace('-latest', ''), 'py{}{}'.format(*sys.version_info)))")
+        echo ::set-output name=flags::$(python -c "print(','.join(['${{ steps.codecov-info.outputs.flag-runner-os }}', '${{ steps.codecov-info.outputs.flag-python-version }}', '${{ steps.codecov-info.outputs.flag-pytest-version }}']))")
+        echo ::set-output name=report-name::$(python -c "print('-'.join(['${{ steps.codecov-info.outputs.flag-runner-os }}', '${{ steps.codecov-info.outputs.flag-python-version }}', '${{ steps.codecov-info.outputs.flag-pytest-version }}']))")
 
-    - name: Upload Helpers Namespace Code Coverage
+    - name: Download Code Coverage Tool
       if: always()
       shell: bash
-      env:
-        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},helpers
-        REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-helpers
-        REPORT_PATH: artifacts/coverage-project.xml
       run: |
-        if [ ! -f codecov.sh ]; then
-          n=0
-          until [ "$n" -ge 5 ]
-          do
-          if curl --max-time 30 -L https://codecov.io/bash --output codecov.sh; then
-              break
-          fi
+        if [ "$(which curl)x" == "x" ]; then
+            echo "Failed to find the 'curl' binary"
+            exit 0
+        fi
+
+        if [ "$(which gpg)x" == "x" ]; then
+            echo "Failed to find the 'gpg' binary"
+            exit 0
+        fi
+
+        if [ "$(which shasum)x" == "x" ]; then
+            echo "Failed to find the 'shasum' binary"
+            exit 0
+        fi
+
+        if [ ! -x codecov-linux ]; then
+            n=0
+            until [ "$n" -ge 5 ]
+            do
+            if curl --max-time 30 -L ${{ steps.codecov-info.outputs.uploader-url }} --output ${{ steps.codecov-info.outputs.uploader-name }}; then
+                break
+            fi
             n=$((n+1))
             sleep 15
-          done
-        fi
-        if [ -f codecov.sh ]; then
-          n=0
-          until [ "$n" -ge 5 ]
-          do
-            if bash codecov.sh -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
+            done
+            n=0
+            until [ "$n" -ge 5 ]
+            do
+            if curl --max-time 30 -L ${{ steps.codecov-info.outputs.uploader-url }}.SHA256SUM --output ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM; then
                 break
             fi
             n=$((n+1))
             sleep 15
-          done
+            done
+            n=0
+            until [ "$n" -ge 5 ]
+            do
+            if curl --max-time 30 -L ${{ steps.codecov-info.outputs.uploader-url }}.SHA256SUM.sig --output ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM.sig; then
+                break
+            fi
+            n=$((n+1))
+            sleep 15
+            done
+            n=0
+            until [ "$n" -ge 5 ]
+            do
+            if curl --max-time 30 -L https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --import; then
+                break
+            fi
+            n=$((n+1))
+            sleep 15
+            done
+            gpg --verify ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM.sig ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM && \
+                shasum -a 256 -c ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM && \
+                chmod +x ${{ steps.codecov-info.outputs.uploader-name }} || exit 0
         fi
 
-    - name: Upload Helpers Namespace Tests Code Coverage
+    - name: Upload Project Code Coverage
       if: always()
       shell: bash
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},helpers
-        REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-tests
-        REPORT_PATH: artifacts/coverage-tests.xml
+        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},src
+        REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-src
+        REPORT_PATH: artifacts/coverage-project.xml
       run: |
-        if [ ! -f codecov.sh ]; then
+        if [ -x ${{ steps.codecov-info.outputs.uploader-name }} ]; then
           n=0
           until [ "$n" -ge 5 ]
           do
-          if curl --max-time 30 -L https://codecov.io/bash --output codecov.sh; then
-              break
-          fi
+            if ./${{ steps.codecov-info.outputs.uploader-name }} -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
+                break
+            fi
             n=$((n+1))
             sleep 15
           done
         fi
-        if [ -f codecov.sh ]; then
+
+    - name: Upload Tests Code Coverage
+      if: always()
+      shell: bash
+      env:
+        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},tests
+        REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-tests
+        REPORT_PATH: artifacts/coverage-tests.xml
+      run: |
+        if [ -x ${{ steps.codecov-info.outputs.uploader-name }} ]; then
           n=0
           until [ "$n" -ge 5 ]
           do
-            if bash codecov.sh -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
+            if ./${{ steps.codecov-info.outputs.uploader-name }} -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
                 break
             fi
             n=$((n+1))
@@ -192,7 +276,7 @@ jobs:
       if: always()
       uses: actions/upload-artifact@main
       with:
-        name: runtests-${{ runner.os }}-py${{ matrix.python-version }}.log
+        name: runtests-${{ steps.codecov.outputs.report-name }}.log
         path: artifacts/runtests-*.log
 
   Windows:
@@ -206,15 +290,17 @@ jobs:
       max-parallel: 5
       matrix:
         python-version:
-          - 3.5
-          - 3.6
-          - 3.7
-          - 3.8
-          - 3.9
+          - "3.6"
+          - "3.7"
+          - "3.8"
+          - "3.9"
+          - "3.10"
         pytest-version:
-          - "~=6.0.0"
-          - "~=6.1.0"
-          - ""
+          - "~=6.2.0"
+          - ">=7.0.0rc1"
+        exclude:
+          - {"python-version": "3.10", "pytest-version": "~=6.0.0"}
+          - {"python-version": "3.10", "pytest-version": "~=6.1.0"}
 
     steps:
     - uses: actions/checkout@v2
@@ -227,56 +313,81 @@ jobs:
     - name: Install Nox
       run: |
         python -m pip install --upgrade pip
-        pip install nox
+        pip install nox 'pytest${{ matrix.pytest-version }}'
 
     - name: Install Test Requirements
+      shell: bash
       env:
         PYTEST_VERSION_REQUIREMENT: pytest${{ matrix.pytest-version }}
-      shell: bash
       run: |
         export PATH="/C/Program Files (x86)/Windows Kits/10/bin/10.0.18362.0/x64;$PATH"
-        nox --force-color -e tests-${{ matrix.python-version }} --install-only
+        nox --force-color -e tests-3 --install-only
 
     - name: Test
-      id: run-tests
       shell: bash
       env:
         SKIP_REQUIREMENTS_INSTALL: YES
       run: |
         export PATH="/C/Program Files (x86)/Windows Kits/10/bin/10.0.18362.0/x64;$PATH"
-        nox --force-color -e tests-${{ matrix.python-version }} -- -vv tests/
+        nox --force-color -e tests-3 -- -vv tests/
+
+    - name: Gather CodeCov Info
+      if: always()
+      id: codecov-info
+      shell: bash
+      run: |
+        echo ::set-output name=flag-python-version::$(python -c "import sys; print('Py{}{}'.format(*sys.version_info))")
+        echo ::set-output name=flag-pytest-version::$(python -c "import pytest; print('PyTest{}{}'.format(*pytest.__version__.split('.')))")
+        echo ::set-output name=flag-runner-os::$(python -c "print('${{ runner.os }}'.replace('-latest', ''))")
+        echo ::set-output name=uploader-url::$(python -c "print('https://uploader.codecov.io/latest/windows/codecov.exe')")
+        echo ::set-output name=uploader-name::$(python -c "print('codecov.exe')")
 
     - name: Create CodeCov Flags
       if: always()
-      id: codecov-flags
+      id: codecov
       run: |
-        echo ::set-output name=flags::$(python -c "import sys; print('{},{},pytest${{ matrix.pytest-version || 'latest' }}'.format('${{ runner.os }}'.replace('-latest', ''), 'py{}{}'.format(*sys.version_info)))")
+        echo ::set-output name=flags::$(python -c "print(','.join(['${{ steps.codecov-info.outputs.flag-runner-os }}', '${{ steps.codecov-info.outputs.flag-python-version }}', '${{ steps.codecov-info.outputs.flag-pytest-version }}']))")
+        echo ::set-output name=report-name::$(python -c "print('-'.join(['${{ steps.codecov-info.outputs.flag-runner-os }}', '${{ steps.codecov-info.outputs.flag-python-version }}', '${{ steps.codecov-info.outputs.flag-pytest-version }}']))")
+
 
-    - name: Upload Helpers Namespace Code Coverage
+    - name: Download Code Coverage Tool
+      if: always()
+      shell: powershell
+      run: |
+        If (-not(Test-Path -Path ./${{ steps.codecov-info.outputs.uploader-name }})) {
+          [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Ssl3
+          [Net.ServicePointManager]::SecurityProtocol = "Tls, Tls11, Tls12, Ssl3"
+
+          $ProgressPreference = 'SilentlyContinue'
+          Invoke-WebRequest -Uri https://keybase.io/codecovsecurity/pgp_keys.asc -OutFile codecov.asc
+          gpg.exe --import codecov.asc
+
+          Invoke-WebRequest -Uri ${{ steps.codecov-info.outputs.uploader-url }} -Outfile ${{ steps.codecov-info.outputs.uploader-name }}
+          Invoke-WebRequest -Uri ${{ steps.codecov-info.outputs.uploader-url }}.SHA256SUM -Outfile ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM
+          Invoke-WebRequest -Uri ${{ steps.codecov-info.outputs.uploader-url }}.SHA256SUM.sig -Outfile ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM.sig
+
+          gpg.exe --verify ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM.sig ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM
+          If ($(Compare-Object -ReferenceObject  $(($(certUtil -hashfile ${{ steps.codecov-info.outputs.uploader-name }} SHA256)[1], "${{ steps.codecov-info.outputs.uploader-name }}") -join "  ") -DifferenceObject $(Get-Content ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM)).length -eq 0) {
+              echo "SHASUM verified"
+          } Else {
+              exit 0
+          }
+        }
+
+    - name: Upload Project Code Coverage
       if: always()
       shell: bash
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},helpers
-        REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-helpers
+        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},src
+        REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-src
         REPORT_PATH: artifacts/coverage-project.xml
       run: |
-        if [ ! -f codecov.sh ]; then
-          n=0
-          until [ "$n" -ge 5 ]
-          do
-          if curl --max-time 30 -L https://codecov.io/bash --output codecov.sh; then
-              break
-          fi
-            n=$((n+1))
-            sleep 15
-          done
-        fi
-        if [ -f codecov.sh ]; then
+        if [ -x ${{ steps.codecov-info.outputs.uploader-name }} ]; then
           n=0
           until [ "$n" -ge 5 ]
           do
-            if bash codecov.sh -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
+            if ./${{ steps.codecov-info.outputs.uploader-name }} -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
                 break
             fi
             n=$((n+1))
@@ -284,31 +395,20 @@ jobs:
           done
         fi
 
-    - name: Upload Helpers Namespace Tests Code Coverage
+    - name: Upload Tests Code Coverage
       if: always()
       shell: bash
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},helpers
+        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},tests
         REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-tests
         REPORT_PATH: artifacts/coverage-tests.xml
       run: |
-        if [ ! -f codecov.sh ]; then
-          n=0
-          until [ "$n" -ge 5 ]
-          do
-          if curl --max-time 30 -L https://codecov.io/bash --output codecov.sh; then
-              break
-          fi
-            n=$((n+1))
-            sleep 15
-          done
-        fi
-        if [ -f codecov.sh ]; then
+        if [ -x ${{ steps.codecov-info.outputs.uploader-name }} ]; then
           n=0
           until [ "$n" -ge 5 ]
           do
-            if bash codecov.sh -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
+            if ./${{ steps.codecov-info.outputs.uploader-name }} -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
                 break
             fi
             n=$((n+1))
@@ -320,29 +420,31 @@ jobs:
       if: always()
       uses: actions/upload-artifact@main
       with:
-        name: runtests-${{ runner.os }}-py${{ matrix.python-version }}.log
+        name: runtests-${{ steps.codecov.outputs.report-name }}.log
         path: artifacts/runtests-*.log
 
   macOS:
     runs-on: macOS-latest
     needs: Pre-Commit
 
-    timeout-minutes: 60
+    timeout-minutes: 40
 
     strategy:
       fail-fast: false
       max-parallel: 5
       matrix:
         python-version:
-          - 3.5
-          - 3.6
-          - 3.7
-          - 3.8
-          - 3.9
+          - "3.6"
+          - "3.7"
+          - "3.8"
+          - "3.9"
+          - "3.10"
         pytest-version:
-          - "~=6.0.0"
-          - "~=6.1.0"
-          - ""
+          - "~=6.2.0"
+          - ">=7.0.0rc1"
+        exclude:
+          - {"python-version": "3.10", "pytest-version": "~=6.0.0"}
+          - {"python-version": "3.10", "pytest-version": "~=6.1.0"}
 
     steps:
     - uses: actions/checkout@v2
@@ -355,84 +457,133 @@ jobs:
     - name: Install Nox
       run: |
         python -m pip install --upgrade pip
-        pip install nox
+        pip install nox 'pytest${{ matrix.pytest-version }}'
 
     - name: Install Test Requirements
       env:
         PYTEST_VERSION_REQUIREMENT: pytest${{ matrix.pytest-version }}
       run: |
-        nox --force-color -e tests-${{ matrix.python-version }} --install-only
+        nox --force-color -e tests-3 --install-only
 
     - name: Test
-      id: run-tests
       env:
         SKIP_REQUIREMENTS_INSTALL: YES
       run: |
-        nox --force-color -e tests-${{ matrix.python-version }} -- -vv tests/
+        nox --force-color -e tests-3 -- -vv tests/
+
+    - name: Gather CodeCov Info
+      if: always()
+      id: codecov-info
+      run: |
+        echo ::set-output name=flag-python-version::$(python -c "import sys; print('Py{}{}'.format(*sys.version_info))")
+        echo ::set-output name=flag-pytest-version::$(python -c "import pytest; print('PyTest{}{}'.format(*pytest.__version__.split('.')))")
+        echo ::set-output name=flag-runner-os::$(python -c "print('${{ runner.os }}'.replace('-latest', ''))")
+        echo ::set-output name=uploader-url::$(python -c "print('https://uploader.codecov.io/latest/codecov-macos')")
+        echo ::set-output name=uploader-name::$(python -c "print('codecov-macos')")
 
     - name: Create CodeCov Flags
       if: always()
-      id: codecov-flags
+      id: codecov
       run: |
-        echo ::set-output name=flags::$(python -c "import sys; print('{},{},pytest${{ matrix.pytest-version || 'latest' }}'.format('${{ runner.os }}'.replace('-latest', ''), 'py{}{}'.format(*sys.version_info)))")
+        echo ::set-output name=flags::$(python -c "print(','.join(['${{ steps.codecov-info.outputs.flag-runner-os }}', '${{ steps.codecov-info.outputs.flag-python-version }}', '${{ steps.codecov-info.outputs.flag-pytest-version }}']))")
+        echo ::set-output name=report-name::$(python -c "print('-'.join(['${{ steps.codecov-info.outputs.flag-runner-os }}', '${{ steps.codecov-info.outputs.flag-python-version }}', '${{ steps.codecov-info.outputs.flag-pytest-version }}']))")
 
-    - name: Upload Helpers Namespace Code Coverage
+    - name: Download Code Coverage Tool
       if: always()
       shell: bash
-      env:
-        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},helpers
-        REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-helpers
-        REPORT_PATH: artifacts/coverage-project.xml
       run: |
-        if [ ! -f codecov.sh ]; then
-          n=0
-          until [ "$n" -ge 5 ]
-          do
-          if curl --max-time 30 -L https://codecov.io/bash --output codecov.sh; then
-              break
-          fi
+        if [ "$(which curl)x" == "x" ]; then
+            echo "Failed to find the 'curl' binary"
+            exit 0
+        fi
+
+        if [ "$(which gpg)x" == "x" ]; then
+            echo "Failed to find the 'gpg' binary"
+            exit 0
+        fi
+
+        if [ "$(which shasum)x" == "x" ]; then
+            echo "Failed to find the 'shasum' binary"
+            exit 0
+        fi
+
+        if [ ! -x codecov-linux ]; then
+            n=0
+            until [ "$n" -ge 5 ]
+            do
+            if curl --max-time 30 -L ${{ steps.codecov-info.outputs.uploader-url }} --output ${{ steps.codecov-info.outputs.uploader-name }}; then
+                break
+            fi
             n=$((n+1))
             sleep 15
-          done
-        fi
-        if [ -f codecov.sh ]; then
-          n=0
-          until [ "$n" -ge 5 ]
-          do
-            if bash codecov.sh -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
+            done
+            n=0
+            until [ "$n" -ge 5 ]
+            do
+            if curl --max-time 30 -L ${{ steps.codecov-info.outputs.uploader-url }}.SHA256SUM --output ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM; then
                 break
             fi
             n=$((n+1))
             sleep 15
-          done
+            done
+            n=0
+            until [ "$n" -ge 5 ]
+            do
+            if curl --max-time 30 -L ${{ steps.codecov-info.outputs.uploader-url }}.SHA256SUM.sig --output ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM.sig; then
+                break
+            fi
+            n=$((n+1))
+            sleep 15
+            done
+            n=0
+            until [ "$n" -ge 5 ]
+            do
+            if curl --max-time 30 -L https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --import; then
+                break
+            fi
+            n=$((n+1))
+            sleep 15
+            done
+            gpg --verify ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM.sig ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM && \
+                shasum -a 256 -c ${{ steps.codecov-info.outputs.uploader-name }}.SHA256SUM && \
+                chmod +x ${{ steps.codecov-info.outputs.uploader-name }} || exit 0
         fi
 
-    - name: Upload Helpers Namespace Tests Code Coverage
+    - name: Upload Project Code Coverage
       if: always()
       shell: bash
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},helpers
-        REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-tests
-        REPORT_PATH: artifacts/coverage-tests.xml
+        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},src
+        REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-src
+        REPORT_PATH: artifacts/coverage-project.xml
       run: |
-        if [ ! -f codecov.sh ]; then
+        if [ -x ${{ steps.codecov-info.outputs.uploader-name }} ]; then
           n=0
           until [ "$n" -ge 5 ]
           do
-          if curl --max-time 30 -L https://codecov.io/bash --output codecov.sh; then
-              break
-          fi
+            if ./${{ steps.codecov-info.outputs.uploader-name }} -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
+                break
+            fi
             n=$((n+1))
             sleep 15
           done
         fi
-        if [ -f codecov.sh ]; then
+
+    - name: Upload Tests Code Coverage
+      if: always()
+      shell: bash
+      env:
+        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+        REPORT_FLAGS: ${{ steps.codecov-flags.outputs.flags }},tests
+        REPORT_NAME: ${{ runner.os }}-Py${{ matrix.python-version }}-tests
+        REPORT_PATH: artifacts/coverage-tests.xml
+      run: |
+        if [ -x ${{ steps.codecov-info.outputs.uploader-name }} ]; then
           n=0
           until [ "$n" -ge 5 ]
           do
-            if bash codecov.sh -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
+            if ./${{ steps.codecov-info.outputs.uploader-name }} -R $(pwd) -n "${REPORT_NAME}" -f "${REPORT_PATH}" -F "${REPORT_FLAGS}"; then
                 break
             fi
             n=$((n+1))
@@ -444,5 +595,5 @@ jobs:
       if: always()
       uses: actions/upload-artifact@main
       with:
-        name: runtests-${{ runner.os }}-py${{ matrix.python-version }}.log
+        name: runtests-${{ steps.codecov.outputs.report-name }}.log
         path: artifacts/runtests-*.log
diff --git a/.gitignore b/.gitignore
index 03212b5..1547f3b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,3 +70,7 @@ target/
 .lvimrc
 
 artifacts/
+
+.vim/
+
+src/pytest_helpers_namespace/version.py
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 10a5db3..3cf0832 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,18 +1,22 @@
 ---
-minimum_pre_commit_version: 1.15.2
+minimum_pre_commit_version: 2.9.2
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v2.1.0
+    rev: v4.0.1
     hooks:
       - id: check-merge-conflict  # Check for files that contain merge conflict strings.
-      - id: trailing-whitespace   # Trims trailing whitespace.
+      - id: trailing-whitespace
         args: [--markdown-linebreak-ext=md]
       - id: mixed-line-ending     # Replaces or checks mixed line ending.
         args: [--fix=lf]
-      - id: end-of-file-fixer     # Makes sure files end in a newline and only a newline.
-      - id: check-merge-conflict  # Check for files that contain merge conflict strings.
-      - id: check-ast             # Simply check whether files parse as valid python.
+      - id: end-of-file-fixer
+      - id: fix-encoding-pragma
+        args: [--remove]
+      - id: check-yaml
+      - id: debug-statements
+        language_version: python3
 
+  # ----- Local Hooks ----------------------------------------------------------------------------------------------->
   - repo: local
     hooks:
       - id: sort-pylint-spelling-words
@@ -21,55 +25,109 @@ repos:
         language: system
         files: ^\.pylint-spelling-words$
 
+      - id: check-changelog-entries
+        name: Check Changelog Entries
+        entry: python .pre-commit-hooks/check-changelog-entries.py
+        language: system
+
+  - repo: local
+    hooks:
+      - id: check-copyright-headers
+        name: Check python modules for appropriate copyright headers
+        files: ^.*\.py$
+        entry: python .pre-commit-hooks/copyright-headers.py
+        language: system
+  # <---- Local Hooks ------------------------------------------------------------------------------------------------
+
+  # ----- Formatting ------------------------------------------------------------------------------------------------>
   - repo: https://github.com/asottile/pyupgrade
-    rev: v2.10.0
+    rev: v2.29.0
     hooks:
       - id: pyupgrade
         name: Rewrite Code to be Py3.5+
-        args: [--py3-plus]
-
-  - repo: https://github.com/hakancelik96/unimport
-    rev: "31cc123640880e385159c719d2f12b5cf8586495"
-    hooks:
-      - id: unimport
-        name: Remove unused imports
-        args: [--remove]
-        #exclude: ^(docs/.*\.py|src/pytest_helpers_namespace/factories/(cli|daemons)/__init__\.py)$
-        exclude: ^docs/.*\.py$
-
+        args: [
+          --py3-plus
+        ]
+        files: ^((setup|noxfile)|(src|tests)/.*)\.py$
+        exclude: src/pytest_helpers_namespace/version.py
 
   - repo: https://github.com/asottile/reorder_python_imports
-    rev: v2.4.0
+    rev: v2.6.0
     hooks:
       - id: reorder-python-imports
-        args: [
-          --py3-plus,
-        ]
+        args:
+          - --py3-plus
+          - --application-directories=.:src
         exclude: src/pytest_helpers_namespace/version.py
 
   - repo: https://github.com/psf/black
-    rev: 21.4b2
+    rev: 21.10b0
     hooks:
       - id: black
         args: [-l 100]
         exclude: src/pytest_helpers_namespace/version.py
 
   - repo: https://github.com/asottile/blacken-docs
-    rev: v1.7.0
+    rev: v1.11.0
     hooks:
       - id: blacken-docs
-        args: [--skip-errors]
-        files: ^docs/.*\.rst
-        additional_dependencies: [black==21.4b2]
+#        args: [--skip-errors]
+        files: ^((docs/.*|README)\.rst|src/pytest_helpers_namespace/.*\.py)$
+        additional_dependencies: [black==21.10b0]
+  # <---- Formatting -------------------------------------------------------------------------------------------------
 
-  - repo: https://github.com/pre-commit/mirrors-pylint
-    rev: v2.4.4
+  # ----- Security -------------------------------------------------------------------------------------------------->
+  - repo: https://github.com/PyCQA/bandit
+    rev: "1.7.0"
     hooks:
-      - id: pylint
-        name: PyLint
-        args: [--output-format=parseable, --rcfile=.pylintrc]
+      - id: bandit
+        alias: bandit-salt
+        name: Run bandit against the code base
+        args: [--silent, -lll, --skip, B701]
+        files: ^(?!tests/).*\.py$
         exclude: src/pytest_helpers_namespace/version.py
+  - repo: https://github.com/PyCQA/bandit
+    rev: "1.7.0"
+    hooks:
+      - id: bandit
+        alias: bandit-tests
+        name: Run bandit against the test suite
+        args: [--silent, -lll, --skip, B701]
+        files: ^tests/.*
+  # <---- Security ---------------------------------------------------------------------------------------------------
+
+  # ----- Code Analysis --------------------------------------------------------------------------------------------->
+  - repo: https://github.com/pycqa/flake8
+    rev: '4.0.1'
+    hooks:
+      - id: flake8
+        exclude: ^(src/pytest_helpers_namespace/version\.py|\.pre-commit-hooks/.*\.py)$
+        additional_dependencies:
+        - flake8-mypy-fork
+        - flake8-docstrings
+        - flake8-typing-imports
+
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v0.930
+    hooks:
+      - id: mypy
+        name: Run mypy against source
+        files: ^src/.*\.py$
+        args: [--strict]
+        additional_dependencies:
+          - types-attrs
+          - types-setuptools
+          - pydantic
+
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v0.930
+    hooks:
+      - id: mypy
+        name: Run mypy against tests
+        files: ^tests/.*\.py$
+        args: []
         additional_dependencies:
-          - saltpylint
-          - pyenchant
-          - salt>=3001
+          - types-attrs
+          - types-setuptools
+          - pydantic
+  # <---- Code Analysis ----------------------------------------------------------------------------------------------
diff --git a/.pre-commit-hooks/check-changelog-entries.py b/.pre-commit-hooks/check-changelog-entries.py
new file mode 100755
index 0000000..876c813
--- /dev/null
+++ b/.pre-commit-hooks/check-changelog-entries.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+# Copyright 2021 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+#
+# pylint: disable=invalid-name,missing-module-docstring,missing-function-docstring
+import argparse
+import pathlib
+import re
+import sys
+
+CODE_ROOT = pathlib.Path(__file__).resolve().parent.parent
+CHANGELOG_ENTRIES_PATH = CODE_ROOT / "changelog"
+CHANGELOG_LIKE_RE = re.compile(r"([\d]+)\.([a-z]+)(\.rst)?$")
+CHANGELOG_EXTENSIONS = (
+    "breaking",
+    "deprecation",
+    "feature",
+    "improvement",
+    "bugfix",
+    "doc",
+    "trivial",
+)
+CHANGELOG_ENTRY_REREX = r"^[\d]+\.({})\.rst$".format("|".join(CHANGELOG_EXTENSIONS))
+CHANGELOG_ENTRY_RE = re.compile(CHANGELOG_ENTRY_REREX)
+
+
+def check_changelog_entries(files):
+
+    exitcode = 0
+    for entry in files:
+        path = pathlib.Path(entry).resolve()
+        # Is it under changelog/
+        try:
+            path.relative_to(CHANGELOG_ENTRIES_PATH)
+            if path.name in (".gitignore", "_template.rst", __name__):
+                # These files should be ignored
+                continue
+            # Is it named properly
+            if not CHANGELOG_ENTRY_RE.match(path.name):
+                # Does it end in .rst
+                if path.suffix != ".rst":
+                    exitcode = 1
+                    print(
+                        "The changelog entry '{}' should have '.rst' as it's file extension".format(
+                            path.relative_to(CODE_ROOT),
+                        ),
+                        file=sys.stderr,
+                        flush=True,
+                    )
+                    continue
+                print(
+                    "The changelog entry '{}' should have one of the following extensions: {}.".format(
+                        path.relative_to(CODE_ROOT),
+                        ", ".join(repr(ext) for ext in CHANGELOG_EXTENSIONS),
+                    ),
+                    file=sys.stderr,
+                    flush=True,
+                )
+                exitcode = 1
+                continue
+            check_changelog_entry_contents(path)
+        except ValueError:
+            # Not under changelog/, carry on checking
+            # Is it a changelog entry
+            if CHANGELOG_ENTRY_RE.match(path.name):
+                # So, this IS a changelog entry, but it's misplaced....
+                exitcode = 1
+                print(
+                    "The changelog entry '{}' should be placed under '{}/', not '{}'".format(
+                        path.relative_to(CODE_ROOT),
+                        CHANGELOG_ENTRIES_PATH.relative_to(CODE_ROOT),
+                        path.relative_to(CODE_ROOT).parent,
+                    ),
+                    file=sys.stderr,
+                    flush=True,
+                )
+                continue
+            elif CHANGELOG_LIKE_RE.match(path.name) and not CHANGELOG_ENTRY_RE.match(path.name):
+                # Does it look like a changelog entry
+                print(
+                    "The changelog entry '{}' should have one of the following extensions: {}.".format(
+                        path.relative_to(CODE_ROOT),
+                        ", ".join(repr(ext) for ext in CHANGELOG_EXTENSIONS),
+                    ),
+                    file=sys.stderr,
+                    flush=True,
+                )
+                exitcode = 1
+                continue
+
+            elif not CHANGELOG_LIKE_RE.match(path.name) and not CHANGELOG_ENTRY_RE.match(path.name):
+                # Does not look like, and it's not a changelog entry
+                continue
+            # Does it end in .rst
+            if path.suffix != ".rst":
+                exitcode = 1
+                print(
+                    "The changelog entry '{}' should have '.rst' as it's file extension".format(
+                        path.relative_to(CODE_ROOT),
+                    ),
+                    file=sys.stderr,
+                    flush=True,
+                )
+    return exitcode
+
+
+def check_changelog_entry_contents(entry):
+    contents = entry.read_text().splitlines()
+    if len(contents) > 1:
+        # More than one line.
+        # If the second line starts with '*' it's a bullet list and we need to add an
+        # empty line before it.
+        if contents[1].strip().startswith("*"):
+            contents.insert(1, "")
+    entry.write_text("{}\n".format("\n".join(contents)))
+
+
+def main(argv):
+    parser = argparse.ArgumentParser(prog=__name__)
+    parser.add_argument("files", nargs="+")
+
+    options = parser.parse_args(argv)
+    return check_changelog_entries(options.files)
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
diff --git a/.pre-commit-hooks/copyright-headers.py b/.pre-commit-hooks/copyright-headers.py
new file mode 100644
index 0000000..5e853f3
--- /dev/null
+++ b/.pre-commit-hooks/copyright-headers.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+# Copyright 2021 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+#
+# pylint: disable=invalid-name,missing-module-docstring,missing-function-docstring
+import argparse
+import pathlib
+import re
+import sys
+from datetime import datetime
+
+CODE_ROOT = pathlib.Path(__file__).resolve().parent.parent
+SPDX_HEADER = "# SPDX-License-Identifier: Apache-2.0"
+COPYRIGHT_HEADER = "# Copyright {year} VMware, Inc."
+COPYRIGHT_REGEX = re.compile(
+    r"# Copyright (?:(?P<start_year>[0-9]{4})(?:-(?P<cur_year>[0-9]{4}))?) VMware, Inc\."
+)
+SPDX_REGEX = re.compile(r"# SPDX-License-Identifier:.*")
+
+
+def check_copyright(files):
+    for file in files:
+        contents = file.read_text()
+        if not contents.strip():
+            # Don't add headers to empty files
+            continue
+        original_contents = contents
+        try:
+            if not COPYRIGHT_REGEX.search(contents):
+                contents = inject_copyright_header(contents)
+                if contents != original_contents:
+                    print(f"Added the copyright header to {file}")
+            else:
+                contents = update_copyright_header(contents)
+                if contents != original_contents:
+                    print(f"Updated the copyright header on {file}")
+            if not SPDX_REGEX.search(contents):
+                contents = inject_spdx_header(contents)
+                if contents != original_contents:
+                    print(f"Added the SPDX header to {file}")
+        finally:
+            if original_contents != contents:
+                file.write_text(contents)
+
+
+def inject_copyright_header(contents):
+    lines = contents.splitlines()
+    shebang_found = False
+    for idx, line in enumerate(lines[:]):
+        if idx == 0 and line.startswith("#!"):
+            shebang_found = True
+            continue
+        if shebang_found and line.strip():
+            shebang_found = False
+            lines.insert(idx, "")
+            idx += 1
+        lines.insert(idx, COPYRIGHT_HEADER.format(year=datetime.today().year))
+        break
+    return "\n".join(lines)
+
+
+def update_copyright_header(contents):
+    lines = contents.splitlines()
+    for idx, line in enumerate(lines[:]):
+        match = COPYRIGHT_REGEX.match(line)
+        if match:
+            this_year = str(datetime.today().year)
+            initial_year = match.group("start_year").strip()
+            if initial_year == this_year:
+                return contents
+            lines[idx] = COPYRIGHT_HEADER.format(year=f"{initial_year}-{this_year}")
+            break
+    return "\n".join(lines)
+
+
+def inject_spdx_header(contents):
+    lines = contents.splitlines()
+    for idx, line in enumerate(lines[:]):
+        if COPYRIGHT_REGEX.match(line):
+            lines.insert(idx + 1, SPDX_HEADER)
+            next_line = lines[idx + 2].strip()
+            if next_line and not next_line.startswith('"""'):
+                # If the next line is not empty, insert an empty comment
+                lines.insert(idx + 2, "#")
+            break
+    return "\n".join(lines)
+
+
+def main(argv):
+    parser = argparse.ArgumentParser(prog=__name__)
+    parser.add_argument("files", nargs="+", type=pathlib.Path)
+
+    options = parser.parse_args(argv)
+    return check_copyright(options.files)
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
diff --git a/.pre-commit-hooks/sort-pylint-spelling-words.py b/.pre-commit-hooks/sort-pylint-spelling-words.py
index fce892c..6a9c902 100755
--- a/.pre-commit-hooks/sort-pylint-spelling-words.py
+++ b/.pre-commit-hooks/sort-pylint-spelling-words.py
@@ -1,4 +1,6 @@
 #!/usr/bin/env python
+# Copyright 2021 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
 # pylint: skip-file
 import pathlib
 
@@ -9,7 +11,7 @@ PYLINT_SPELLING_WORDS = REPO_ROOT / ".pylint-spelling-words"
 def sort():
     in_contents = PYLINT_SPELLING_WORDS.read_text()
     out_contents = ""
-    out_contents += "\n".join(sorted([line.lower() for line in in_contents.splitlines()]))
+    out_contents += "\n".join(sorted({line.lower() for line in in_contents.splitlines()}))
     out_contents += "\n"
     if in_contents != out_contents:
         PYLINT_SPELLING_WORDS.write_text(out_contents)
diff --git a/.pylint-spelling-words b/.pylint-spelling-words
index 7168513..c991c73 100644
--- a/.pylint-spelling-words
+++ b/.pylint-spelling-words
@@ -1,13 +1,16 @@
 abspath
 autodoc
+changelog
 config
 confvals
 css
 favicon
 favicons
+funcwrapper
 ico
 intersphinx
 intl
+linkcheck
 minify
 namespace
 namespaces
@@ -21,6 +24,8 @@ repo
 rst
 sitecustomize
 sitevars
+str
 sys
 toc
 virtualenvs
+vmware
diff --git a/.pylintrc b/.pylintrc
index 38f331a..fc4410e 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -19,10 +19,7 @@ persistent=yes
 
 # List of plugins (as comma separated values of python modules names) to load,
 # usually to register additional checkers.
-load-plugins=saltpylint.pep8,
-  saltpylint.strings,
-  saltpylint.fileperms,
-  saltpylint.smartup,
+load-plugins=
 
 # Use multiple processes to speed up Pylint.
 jobs=1
@@ -116,7 +113,8 @@ disable=R,
   import-outside-toplevel,
   wrong-import-position,
   wrong-import-order,
-  missing-whitespace-after-comma
+  missing-whitespace-after-comma,
+  consider-using-f-string
 
 # Disabled:
 # R* [refactoring suggestions & reports]
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..9931e8b
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,94 @@
+.. _changelog:
+
+=========
+Changelog
+=========
+
+Versions follow `Calendar Versioning <https://calver.org/>`_
+(`<year>.<month>.<day>`).
+
+.. towncrier-draft-entries::
+
+.. towncrier release notes start
+
+
+2021.12.29
+==========
+
+Improvements
+------------
+
+- `#11 <https://github.com/saltstack/pytest-helpers-namespace/issues/11>`_: Plugin is now fully typed.
+
+
+v2021.3.24
+==========
+
+* Switched project to a ``src`` layout.
+* Switched project to a declarative setuptools approach
+* Added support to check if a helper has been registered
+* Pytest >= 6.1.1 is now required
+
+v2019.1.8
+=========
+
+* Patch PyTest before any ``conftest.py`` file is processed.
+
+v2019.1.7
+=========
+
+* Support PyTest >= 4.1
+
+v2019.1.6.post1
+===============
+
+* No changes were made besides locking to PyTest < 4.0
+
+v2019.1.6
+=========
+
+* No changes were made besides locking to PyTest < 4.1
+
+v2017.11.11
+===========
+
+* Allow passing a string to the register function which will be the helper name
+
+v2016.7.10
+==========
+
+* `#4`_: Allow a registered function to contibue to behave as a regular function.
+
+v2016.4.15
+==========
+
+* `#3`_: Hide the ``FuncWrapper`` traceback in pytest failures. Thanks Logan Glickfield(`@lsglick`_)
+
+v2016.4.5
+=========
+
+* Use a wrapper class instead of adding an attribute to a function.
+
+v2016.4.3
+=========
+
+* `#1`_: Provide proper errors when helper functions or namespaces are being
+  overridden.
+
+v2016.3.2
+==========
+
+* First working release
+
+.. _`cookiecutter-pytest-plugin`: https://github.com/pytest-dev/cookiecutter-pytest-plugin
+.. _`file an issue`: https://github.com/saltstack/pytest-helpers-namespace/issues
+.. _`pytest`: https://github.com/pytest-dev/pytest
+.. _`nox`: https://nox.thea.codes/en/stable/
+.. _`pip`: https://pypi.python.org/pypi/pip/
+.. _`PyPI`: https://pypi.python.org/pypi
+
+.. _`#1`: https://github.com/saltstack/pytest-helpers-namespace/issues/1
+.. _`#3`: https://github.com/saltstack/pytest-helpers-namespace/pull/3
+.. _`#4`: https://github.com/saltstack/pytest-helpers-namespace/issues/4
+
+.. _`@lsglick`: https://github.com/lsglick
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..0a0233f
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,127 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in
+pytest-system-statistics project and our community a harassment-free experience
+for everyone, regardless of age, body size, visible or invisible disability,
+ethnicity, sex characteristics, gender identity and expression, level of
+experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+  and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+  overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+  advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+  address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at oss-coc@@vmware.com.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior,  harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..008a15c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,76 @@
+# Contributing to pytest-helpers-namespace
+
+The pytest-helpers-namespace project team welcomes contributions from the community. If you wish to contribute
+code and you have not signed our [Contributor License Agreement](https://cla.vmware.com/cla/1/preview), our
+bot will update the issue when you open a Pull Request.
+For any questions about the CLA process, please refer to our  [FAQ](https://cla.vmware.com/faq).
+
+## Contribution Flow
+
+This is a rough outline of what a contributor's workflow looks like:
+
+- Create a topic branch from where you want to base your work
+- Make commits of logical units
+- Make sure your commit messages are in the proper format (see below)
+- Push your changes to a topic branch in your fork of the repository
+- Submit a pull request
+
+Example:
+
+``` shell
+git remote add upstream https://github.com/saltstack/pytest-helpers-namespace.git
+git checkout -b my-new-feature master
+git commit -a
+git push origin my-new-feature
+```
+
+### Staying In Sync With Upstream
+
+When your branch gets out of sync with the pytest-helpers-namespace/master branch, use the following to update:
+
+``` shell
+git checkout my-new-feature
+git fetch -a
+git pull --rebase upstream master
+git push --force-with-lease origin my-new-feature
+```
+
+### Updating pull requests
+
+If your PR fails to pass CI or needs changes based on code review, you'll most likely want to squash these changes into
+existing commits.
+
+If your pull request contains a single commit or your changes are related to the most recent commit, you can simply
+amend the commit.
+
+``` shell
+git add .
+git commit --amend
+git push --force-with-lease origin my-new-feature
+```
+
+If you need to squash changes into an earlier commit, you can use:
+
+``` shell
+git add .
+git commit --fixup <commit>
+git rebase -i --autosquash master
+git push --force-with-lease origin my-new-feature
+```
+
+Be sure to add a comment to the PR indicating your new changes are ready to review, as GitHub does not generate a
+notification when you git push.
+
+### Code Style
+
+### Formatting Commit Messages
+
+We follow the conventions on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/).
+
+Be sure to include any related GitHub issue references in the commit message.  See
+[GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) for referencing issues
+and commits.
+
+## Reporting Bugs and Creating Issues
+
+When opening a new issue, try to roughly follow the commit message format conventions above.
diff --git a/PKG-INFO b/PKG-INFO
index 7c38c57..5900135 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: pytest-helpers-namespace
-Version: 2021.4.29
+Version: 2021.12.29
 Summary: Pytest Helpers Namespace Plugin
 Home-page: https://github.com/saltstack/pytest-helpers-namespace
 Author: Pedro Algarvio
@@ -37,6 +37,9 @@ Description: .. image:: https://github.com/saltstack/pytest-helpers-namespace/ac
             :alt: Supported implementations
             :target: https://pypi.python.org/pypi/pytest-helpers-namespace
         
+        ..
+           include-starts-here
+        
         
         Pytest Helpers Namespace
         ========================
@@ -78,11 +81,12 @@ Description: .. image:: https://github.com/saltstack/pytest-helpers-namespace/ac
         
            import pytest
         
+        
            @pytest.helpers.register
            def foo(bar):
-               '''
+               """
                this dumb helper function will just return what you pass to it
-               '''
+               """
                return bar
         
         
@@ -101,15 +105,16 @@ Description: .. image:: https://github.com/saltstack/pytest-helpers-namespace/ac
         
         .. code-block:: python
         
-           pytest_plugins = ['helpers_namespace']
+           pytest_plugins = ["helpers_namespace"]
         
            import pytest
         
+        
            @pytest.helpers.can.haz.register
            def foo(bar):
-               '''
+               """
                this dumb helper function will just return what you pass to it
-               '''
+               """
                return bar
         
         
@@ -124,86 +129,6 @@ Description: .. image:: https://github.com/saltstack/pytest-helpers-namespace/ac
         You can even pass a name to the register function and that will be the helper function name.
         
         
-        Contributing
-        ------------
-        Contributions are very welcome. Tests can be run with `nox`_, please ensure
-        the coverage at least stays the same before you submit a pull request.
-        
-        License
-        -------
-        
-        Distributed under the terms of the `Apache Software License 2.0`_ license,
-        "pytest-helpers-namespace" is free and open source software.
-        
-        
-        Issues
-        ------
-        
-        If you encounter any problems, please `file an issue`_ along with a detailed
-        description.
-        
-        Changelog
-        ---------
-        
-        v2021.3.24
-        ~~~~~~~~~~
-        
-        * Switched project to a ``src`` layout.
-        * Switched project to a declarative setuptools approach
-        * Added support to check if a helper has been registered
-        * Pytest >= 6.1.1 is now required
-        
-        v2019.1.8
-        ~~~~~~~~~
-        
-        * Patch PyTest before any ``conftest.py`` file is processed.
-        
-        v2019.1.7
-        ~~~~~~~~~
-        
-        * Support PyTest >= 4.1
-        
-        v2019.1.6.post1
-        ~~~~~~~~~~~~~~~
-        
-        * No changes were made besides locking to PyTest < 4.0
-        
-        v2019.1.6
-        ~~~~~~~~~
-        
-        * No changes were made besides locking to PyTest < 4.1
-        
-        v2017.11.11
-        ~~~~~~~~~~~
-        
-        * Allow passing a string to the register function which will be the helper name
-        
-        v2016.7.10
-        ~~~~~~~~~~
-        
-        * Allow a registered function to contibue to behave as a regular function. `#4`_.
-        
-        v2016.4.15
-        ~~~~~~~~~~
-        
-        * Hide the ``FuncWrapper`` traceback in pytest failures. `#3`_. Thanks Logan Glickfield(`@lsglick`_)
-        
-        v2016.4.5
-        ~~~~~~~~~
-        
-        * Use a wrapper class instead of adding an attribute to a function.
-        
-        v2016.4.3
-        ~~~~~~~~~
-        
-        * Provide proper errors when helper functions or namespaces are being
-          overridden. `#1`_
-        
-        v2016.3.2
-        ~~~~~~~~~~
-        
-        * First working release
-        
         ----
         
         This `Pytest`_ plugin was generated with `Cookiecutter`_ along with
@@ -211,19 +136,18 @@ Description: .. image:: https://github.com/saltstack/pytest-helpers-namespace/ac
         
         .. _`Cookiecutter`: https://github.com/audreyr/cookiecutter
         .. _`@hackebrot`: https://github.com/hackebrot
-        .. _`Apache Software License 2.0`: http://www.apache.org/licenses/LICENSE-2.0
         .. _`cookiecutter-pytest-plugin`: https://github.com/pytest-dev/cookiecutter-pytest-plugin
-        .. _`file an issue`: https://github.com/saltstack/pytest-helpers-namespace/issues
         .. _`pytest`: https://github.com/pytest-dev/pytest
-        .. _`nox`: https://nox.thea.codes/en/stable/
         .. _`pip`: https://pypi.python.org/pypi/pip/
         .. _`PyPI`: https://pypi.python.org/pypi
         
-        .. _`#1`: https://github.com/saltstack/pytest-helpers-namespace/issues/1
-        .. _`#3`: https://github.com/saltstack/pytest-helpers-namespace/pull/3
-        .. _`#4`: https://github.com/saltstack/pytest-helpers-namespace/issues/4
+        ..
+           include-ends-here
+        
+        Documentation
+        =============
         
-        .. _`@lsglick`: https://github.com/lsglick
+        The full documentation can be seen `here <https://pytest-helpers-namespace.readthedocs.io>`_.
         
 Platform: unix
 Platform: linux
@@ -239,10 +163,13 @@ Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: Apache Software License
-Requires-Python: >=3.5
+Requires-Python: >=3.5.6
+Description-Content-Type: text/x-rst
 Provides-Extra: docs
 Provides-Extra: lint
 Provides-Extra: tests
+Provides-Extra: changelog
diff --git a/README.rst b/README.rst
index 79c6bc4..93557dc 100644
--- a/README.rst
+++ b/README.rst
@@ -26,6 +26,9 @@
     :alt: Supported implementations
     :target: https://pypi.python.org/pypi/pytest-helpers-namespace
 
+..
+   include-starts-here
+
 
 Pytest Helpers Namespace
 ========================
@@ -67,11 +70,12 @@ Consider the following ``conftest.py`` file:
 
    import pytest
 
+
    @pytest.helpers.register
    def foo(bar):
-       '''
+       """
        this dumb helper function will just return what you pass to it
-       '''
+       """
        return bar
 
 
@@ -90,15 +94,16 @@ You can even nest namespaces. Consider the following ``conftest.py`` file:
 
 .. code-block:: python
 
-   pytest_plugins = ['helpers_namespace']
+   pytest_plugins = ["helpers_namespace"]
 
    import pytest
 
+
    @pytest.helpers.can.haz.register
    def foo(bar):
-       '''
+       """
        this dumb helper function will just return what you pass to it
-       '''
+       """
        return bar
 
 
@@ -113,86 +118,6 @@ And now consider the following test case:
 You can even pass a name to the register function and that will be the helper function name.
 
 
-Contributing
-------------
-Contributions are very welcome. Tests can be run with `nox`_, please ensure
-the coverage at least stays the same before you submit a pull request.
-
-License
--------
-
-Distributed under the terms of the `Apache Software License 2.0`_ license,
-"pytest-helpers-namespace" is free and open source software.
-
-
-Issues
-------
-
-If you encounter any problems, please `file an issue`_ along with a detailed
-description.
-
-Changelog
----------
-
-v2021.3.24
-~~~~~~~~~~
-
-* Switched project to a ``src`` layout.
-* Switched project to a declarative setuptools approach
-* Added support to check if a helper has been registered
-* Pytest >= 6.1.1 is now required
-
-v2019.1.8
-~~~~~~~~~
-
-* Patch PyTest before any ``conftest.py`` file is processed.
-
-v2019.1.7
-~~~~~~~~~
-
-* Support PyTest >= 4.1
-
-v2019.1.6.post1
-~~~~~~~~~~~~~~~
-
-* No changes were made besides locking to PyTest < 4.0
-
-v2019.1.6
-~~~~~~~~~
-
-* No changes were made besides locking to PyTest < 4.1
-
-v2017.11.11
-~~~~~~~~~~~
-
-* Allow passing a string to the register function which will be the helper name
-
-v2016.7.10
-~~~~~~~~~~
-
-* Allow a registered function to contibue to behave as a regular function. `#4`_.
-
-v2016.4.15
-~~~~~~~~~~
-
-* Hide the ``FuncWrapper`` traceback in pytest failures. `#3`_. Thanks Logan Glickfield(`@lsglick`_)
-
-v2016.4.5
-~~~~~~~~~
-
-* Use a wrapper class instead of adding an attribute to a function.
-
-v2016.4.3
-~~~~~~~~~
-
-* Provide proper errors when helper functions or namespaces are being
-  overridden. `#1`_
-
-v2016.3.2
-~~~~~~~~~~
-
-* First working release
-
 ----
 
 This `Pytest`_ plugin was generated with `Cookiecutter`_ along with
@@ -200,16 +125,15 @@ This `Pytest`_ plugin was generated with `Cookiecutter`_ along with
 
 .. _`Cookiecutter`: https://github.com/audreyr/cookiecutter
 .. _`@hackebrot`: https://github.com/hackebrot
-.. _`Apache Software License 2.0`: http://www.apache.org/licenses/LICENSE-2.0
 .. _`cookiecutter-pytest-plugin`: https://github.com/pytest-dev/cookiecutter-pytest-plugin
-.. _`file an issue`: https://github.com/saltstack/pytest-helpers-namespace/issues
 .. _`pytest`: https://github.com/pytest-dev/pytest
-.. _`nox`: https://nox.thea.codes/en/stable/
 .. _`pip`: https://pypi.python.org/pypi/pip/
 .. _`PyPI`: https://pypi.python.org/pypi
 
-.. _`#1`: https://github.com/saltstack/pytest-helpers-namespace/issues/1
-.. _`#3`: https://github.com/saltstack/pytest-helpers-namespace/pull/3
-.. _`#4`: https://github.com/saltstack/pytest-helpers-namespace/issues/4
+..
+   include-ends-here
+
+Documentation
+=============
 
-.. _`@lsglick`: https://github.com/lsglick
+The full documentation can be seen `here <https://pytest-helpers-namespace.readthedocs.io>`_.
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index 1d3ea23..0000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-# What Python version is installed where:
-# http://www.appveyor.com/docs/installed-software#python
-
-environment:
-  matrix:
-    - PYTHON: "C:\\Python27"
-      TOX_ENV: "py27"
-
-    - PYTHON: "C:\\Python35"
-      TOX_ENV: "py35"
-
-    - PYTHON: "C:\\Python36"
-      TOX_ENV: "py36"
-
-    - PYTHON: "C:\\Python35"
-      TOX_ENV: "py35"
-
-
-init:
-  - "%PYTHON%/python -V"
-  - "%PYTHON%/python -c \"import struct;print( 8 * struct.calcsize(\'P\'))\""
-
-install:
-  - "%PYTHON%/Scripts/easy_install -U pip"
-  - "%PYTHON%/Scripts/pip install tox"
-  - "%PYTHON%/Scripts/pip install wheel"
-  - "%PYTHON%/Scripts/pip install coverage codecov pytest-cov"
-
-build: false  # Not a C# project, build stuff at the test step instead.
-
-test_script:
-  - "%PYTHON%/Scripts/tox -e %TOX_ENV%"
-
-after_test:
-  - "%PYTHON%/python setup.py bdist_wheel"
-  - ps: "ls dist"
-
-artifacts:
-  - path: dist\*
-
-on_success:
-  - "%PYTHON%/Scripts/coverage report"
diff --git a/changelog/_template.rst b/changelog/_template.rst
new file mode 100644
index 0000000..f8733a0
--- /dev/null
+++ b/changelog/_template.rst
@@ -0,0 +1,48 @@
+{% macro issue_link(value) -%}
+`{{ value }} <https://github.com/saltstack/pytest-helpers-namespace/issues/{{ value[1:] }}>`_
+{%- endmacro %}
+
+{% if top_line %}
+{{ top_line }}
+{{ top_underline * ((top_line)|length)}}
+{% elif versiondata.name %}
+{{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }})
+{{ top_underline * ((versiondata.name + versiondata.version + versiondata.date)|length + 4)}}
+{% else %}
+{{ versiondata.version }} ({{ versiondata.date }})
+{{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}}
+{% endif %}
+{% for section, _ in sections.items() %}
+{% set underline = underlines[0] %}{% if section %}{{section}}
+{{ underline * section|length }}{% set underline = underlines[1] %}
+
+{% endif %}
+
+{% if sections[section] %}
+{% for category, val in definitions.items() if category in sections[section]%}
+{{ definitions[category]['name'] }}
+{{ underline * definitions[category]['name']|length }}
+
+{% if definitions[category]['showcontent'] %}
+{% for text, values in sections[section][category].items() %}
+{% set issue_joiner = joiner(', ') -%}
+- {% for value in values|sort %}{{ issue_joiner() }}{{ issue_link(value) }}{% endfor %}: {{ text }}
+{% endfor %}
+
+{% else %}
+- {{ sections[section][category]['']|join(', ') }}
+
+{% endif %}
+{% if sections[section][category]|length == 0 %}
+No significant changes.
+
+{% else %}
+{% endif %}
+
+{% endfor %}
+{% else %}
+No significant changes.
+
+
+{% endif %}
+{% endfor %}
diff --git a/debian/changelog b/debian/changelog
index ae15d9b..19f1119 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,9 @@
-pytest-helpers-namespace (2021.4.29-2) UNRELEASED; urgency=medium
+pytest-helpers-namespace (2021.12.29-1) UNRELEASED; urgency=medium
 
   * Update standards version to 4.6.2, no changes needed.
+  * New upstream release.
 
- -- Debian Janitor <janitor@jelmer.uk>  Wed, 11 Jan 2023 10:23:52 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 11 Jan 2023 14:13:03 -0000
 
 pytest-helpers-namespace (2021.4.29-1) unstable; urgency=medium
 
diff --git a/debian/patches/Drop-setuptools-declarative-requirements.patch b/debian/patches/Drop-setuptools-declarative-requirements.patch
index d0aed49..5ebc85f 100644
--- a/debian/patches/Drop-setuptools-declarative-requirements.patch
+++ b/debian/patches/Drop-setuptools-declarative-requirements.patch
@@ -15,11 +15,11 @@ Signed-off-by: Benjamin Drung <benjamin.drung@ionos.com>
  setup.cfg | 1 -
  1 file changed, 1 deletion(-)
 
-diff --git a/setup.cfg b/setup.cfg
-index a5c667d..f4a9875 100644
---- a/setup.cfg
-+++ b/setup.cfg
-@@ -35,7 +35,6 @@ python_requires = >= 3.5
+Index: pytest-helpers-namespace.git/setup.cfg
+===================================================================
+--- pytest-helpers-namespace.git.orig/setup.cfg
++++ pytest-helpers-namespace.git/setup.cfg
+@@ -37,7 +37,6 @@ python_requires = >= 3.5.6
  setup_requires = 
  	setuptools>=50.3.2
  	setuptools_scm[toml]>=3.4
@@ -27,6 +27,3 @@ index a5c667d..f4a9875 100644
  
  [options.packages.find]
  where = src
--- 
-2.27.0
-
diff --git a/docs/Makefile b/docs/Makefile
index 2d87ff4..d4bb2cb 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -1,192 +1,20 @@
-# Makefile for Sphinx documentation
+# Minimal makefile for Sphinx documentation
 #
 
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-PAPER         =
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
 BUILDDIR      = _build
 
-# User-friendly check for sphinx-build
-ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
-$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
-endif
-
-# Internal variables.
-PAPEROPT_a4     = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-# the i18n builder cannot share the environment and doctrees with the others
-I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
-
+# Put it first so that "make" without argument is like "make help".
 help:
-	@echo "Please use \`make <target>' where <target> is one of"
-	@echo "  html       to make standalone HTML files"
-	@echo "  dirhtml    to make HTML files named index.html in directories"
-	@echo "  singlehtml to make a single large HTML file"
-	@echo "  pickle     to make pickle files"
-	@echo "  json       to make JSON files"
-	@echo "  htmlhelp   to make HTML files and a HTML help project"
-	@echo "  qthelp     to make HTML files and a qthelp project"
-	@echo "  applehelp  to make an Apple Help Book"
-	@echo "  devhelp    to make HTML files and a Devhelp project"
-	@echo "  epub       to make an epub"
-	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
-	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
-	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
-	@echo "  text       to make text files"
-	@echo "  man        to make manual pages"
-	@echo "  texinfo    to make Texinfo files"
-	@echo "  info       to make Texinfo files and run them through makeinfo"
-	@echo "  gettext    to make PO message catalogs"
-	@echo "  changes    to make an overview of all changed/added/deprecated items"
-	@echo "  xml        to make Docutils-native XML files"
-	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
-	@echo "  linkcheck  to check all external links for integrity"
-	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
-	@echo "  coverage   to run coverage check of the documentation (if enabled)"
-
-clean:
-	rm -rf $(BUILDDIR)/*
-
-html:
-	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml:
-	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml:
-	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
-	@echo
-	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle:
-	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
-	@echo
-	@echo "Build finished; now you can process the pickle files."
-
-json:
-	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
-	@echo
-	@echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
-	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
-	@echo
-	@echo "Build finished; now you can run HTML Help Workshop with the" \
-	      ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
-	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
-	@echo
-	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
-	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
-	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pytest-cookiecutterplugin_name.qhcp"
-	@echo "To view the help file:"
-	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pytest-cookiecutterplugin_name.qhc"
-
-applehelp:
-	$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
-	@echo
-	@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
-	@echo "N.B. You won't be able to view it unless you put it in" \
-	      "~/Library/Documentation/Help or install it in your application" \
-	      "bundle."
-
-devhelp:
-	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
-	@echo
-	@echo "Build finished."
-	@echo "To view the help file:"
-	@echo "# mkdir -p $$HOME/.local/share/devhelp/pytest-cookiecutterplugin_name"
-	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pytest-cookiecutterplugin_name"
-	@echo "# devhelp"
-
-epub:
-	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
-	@echo
-	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo
-	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
-	@echo "Run \`make' in that directory to run these through (pdf)latex" \
-	      "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through pdflatex..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-latexpdfja:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through platex and dvipdfmx..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
-	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
-	@echo
-	@echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
-	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
-	@echo
-	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-texinfo:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo
-	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
-	@echo "Run \`make' in that directory to run these through makeinfo" \
-	      "(use \`make info' here to do that automatically)."
-
-info:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo "Running Texinfo files through makeinfo..."
-	make -C $(BUILDDIR)/texinfo info
-	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-gettext:
-	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
-	@echo
-	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-changes:
-	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
-	@echo
-	@echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
-	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
-	@echo
-	@echo "Link check complete; look for any errors in the above output " \
-	      "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
-	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
-	@echo "Testing of doctests in the sources finished, look at the " \
-	      "results in $(BUILDDIR)/doctest/output.txt."
-
-coverage:
-	$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
-	@echo "Testing of coverage in the sources finished, look at the " \
-	      "results in $(BUILDDIR)/coverage/python.txt."
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 
-xml:
-	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
-	@echo
-	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+.PHONY: help Makefile
 
-pseudoxml:
-	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
-	@echo
-	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/_static/css/inline-include.css b/docs/_static/css/inline-include.css
new file mode 100644
index 0000000..8a7d434
--- /dev/null
+++ b/docs/_static/css/inline-include.css
@@ -0,0 +1,15 @@
+.literal-block-wrapper, article div[class^="highlight-default"] {
+  margin-top: 0.4em;
+}
+
+.literal-block-wrapper .code-block-caption {
+  text-align: left;
+}
+
+.literal-block-wrapper .code-block-caption .caption-text {
+  padding: 0.5em 0.7em;
+  border: .1em solid var(--color-code-background);
+  border-radius: 0.6rem 0.6rem 0 0;
+  background-color: var(--color-code-background);
+  font-family: var(--font-stack--monospace);
+}
diff --git a/docs/_static/img/SaltProject_Logomark_teal.png b/docs/_static/img/SaltProject_Logomark_teal.png
new file mode 100644
index 0000000..70a0d9a
Binary files /dev/null and b/docs/_static/img/SaltProject_Logomark_teal.png differ
diff --git a/docs/_static/img/SaltProject_altlogo_teal.png b/docs/_static/img/SaltProject_altlogo_teal.png
new file mode 100644
index 0000000..53f3998
Binary files /dev/null and b/docs/_static/img/SaltProject_altlogo_teal.png differ
diff --git a/docs/all.rst b/docs/all.rst
new file mode 100644
index 0000000..681fed1
--- /dev/null
+++ b/docs/all.rst
@@ -0,0 +1,10 @@
+.. _all the states/modules:
+
+Complete List of pytest-helpers-namespace
+=========================================
+
+
+.. toctree::
+   :maxdepth: 2
+
+   ref/modules.rst
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 0000000..565b052
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1 @@
+.. include:: ../CHANGELOG.rst
diff --git a/docs/conf.py b/docs/conf.py
index b98d46d..08cc1a1 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,3 +1,6 @@
+# Copyright 2021 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+#
 # Configuration file for the Sphinx documentation builder.
 #
 # This file only contains a selection of the most common options. For a full
@@ -13,16 +16,30 @@ import os
 import pathlib
 import sys
 
-import sphinx_material_saltstack
+try:
+    from importlib_metadata import distribution
+except ImportError:
+    from importlib.metadata import distribution
+
 
-docs_basepath = pathlib.Path(__file__).resolve()
+try:
+    DOCS_BASEPATH = pathlib.Path(__file__).resolve().parent
+except NameError:
+    # sphinx-intl and six execute some code which will raise this NameError
+    # assume we're in the doc/ directory
+    DOCS_BASEPATH = pathlib.Path(".").resolve().parent
 
-additional_paths = (docs_basepath / "_ext", docs_basepath.parent.parent / "src")
+REPO_ROOT = DOCS_BASEPATH.parent
+
+addtl_paths = (
+    os.path.join(os.pardir, "src"),  # pytest-helpers-namespace itself (for autodoc)
+    "_ext",  # custom Sphinx extensions
+)
 
-for path in additional_paths:
-    sys.path.insert(0, str(path))
+for addtl_path in addtl_paths:
+    sys.path.insert(0, os.path.abspath(os.path.join(DOCS_BASEPATH, addtl_path)))
 
-import pytest_helpers_namespace
+dist = distribution("pytest-helpers-namespace")
 
 
 # -- Project information -----------------------------------------------------
@@ -31,16 +48,16 @@ if this_year == 2020:
     copyright_year = 2020
 else:
     copyright_year = f"2020 - {this_year}"
-project = "PyTest Helpers Namespace"
-copyright = f"{copyright_year}, SaltStack, Inc."
-author = "SaltStack, Inc."
+project = dist.metadata["Summary"]
+author = dist.metadata["Author"]
+copyright = f"{copyright_year}, {author}"  # pylint: disable=redefined-builtin
 
 # The full version, including alpha/beta/rc tags
-release = pytest_helpers_namespace.__version__
+release = dist.version
 
 
 # Variables to pass into the docs from sitevars.rst for rst substitution
-with open("sitevars.rst") as site_vars_file:
+with open("sitevars.rst", encoding="utf-8") as site_vars_file:
     site_vars = site_vars_file.read().splitlines()
 
 rst_prolog = """
@@ -55,7 +72,6 @@ rst_prolog = """
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    "sphinx_material_saltstack",
     "sphinx.ext.autodoc",
     "sphinx.ext.autosummary",
     "sphinx.ext.napoleon",
@@ -63,7 +79,9 @@ extensions = [
     "sphinx.ext.viewcode",
     "sphinx.ext.todo",
     "sphinx.ext.coverage",
+    "sphinx_copybutton",
     "sphinxcontrib.spelling",
+    "sphinxcontrib.towncrier",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
@@ -85,37 +103,16 @@ exclude_patterns = [
 ]
 
 autosummary_generate = True
+modindex_common_prefix = ["pytest_helpers_namespace."]
+master_doc = "contents"
 
 # -- Options for HTML output -------------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-html_theme = "sphinx_material_saltstack"
-html_theme_path = sphinx_material_saltstack.html_theme_path()
-html_context = sphinx_material_saltstack.get_html_context()
-html_sidebars = {"**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"]}
-html_theme_options = {
-    # Set the name of the project to appear in the navigation.
-    "nav_title": "PyTest Helpers Namespace",
-    # Set you GA account ID to enable tracking
-    # "google_analytics_account": "",
-    # Set the repo location to get a badge with stats (only if public repo)
-    "repo_url": "https://github.com/saltstack/pytest-helpers-namespace",
-    "repo_name": "pytest-helpers-namespace",
-    "repo_type": "github",
-    # Visible levels of the global TOC; -1 means unlimited
-    "globaltoc_depth": 1,
-    # If False, expand all TOC entries
-    "globaltoc_collapse": False,
-    # If True, show hidden TOC entries
-    "globaltoc_includehidden": True,
-    # hide tabs?
-    "master_doc": False,
-    # Minify for smaller HTML/CSS assets
-    "html_minify": True,
-    "css_minify": True,
-}
+html_theme = "furo"
+html_title = project
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
@@ -124,25 +121,13 @@ html_static_path = ["_static"]
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-html_logo = os.path.join(
-    html_theme_path[0],
-    "sphinx_material_saltstack",
-    "static",
-    "images",
-    "saltstack-logo.png",
-)
+html_logo = ""
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large. Favicons can be up to at least 228x228. PNG
 # format is supported as well, not just .ico'
-html_favicon = os.path.join(
-    html_theme_path[0],
-    "sphinx_material_saltstack",
-    "static",
-    "images",
-    "favicon.png",
-)
+html_favicon = ""
 
 # Sphinx Napoleon Config
 napoleon_google_docstring = True
@@ -160,7 +145,7 @@ napoleon_use_rtype = True
 # ----- Intersphinx Config ---------------------------------------------------------------------------------------->
 intersphinx_mapping = {
     "python": ("https://docs.python.org/3", None),
-    "pytest": ("https://pytest.readthedocs.io/en/stable", None),
+    "pytest": ("https://docs.pytest.org/en/stable", None),
 }
 # <---- Intersphinx Config -----------------------------------------------------------------------------------------
 
@@ -169,6 +154,15 @@ autodoc_default_options = {"member-order": "bysource"}
 autodoc_mock_imports = []
 # <---- Autodoc Config -----------------------------------------------------------------------------------------------
 
+# ----- Towncrier Draft Release ------------------------------------------------------------------------------------->
+# Options: draft/sphinx-version/sphinx-release
+towncrier_draft_autoversion_mode = "draft"
+towncrier_draft_include_empty = True
+towncrier_draft_working_directory = REPO_ROOT
+# Not yet supported:
+# towncrier_draft_config_path = 'pyproject.toml'  # relative to cwd
+# <---- Towncrier Draft Release --------------------------------------------------------------------------------------
+
 
 def setup(app):
     app.add_crossref_type(
@@ -176,7 +170,7 @@ def setup(app):
         rolename="fixture",
         indextemplate="pair: %s; fixture",
     )
-    # Allow linking to pytest confvals.
+    # Allow linking to pytest's confvals.
     app.add_object_type(
         "confval",
         "pytest-confval",
diff --git a/docs/contents.rst b/docs/contents.rst
new file mode 100644
index 0000000..8506bc5
--- /dev/null
+++ b/docs/contents.rst
@@ -0,0 +1,13 @@
+.. _table-of-contents:
+
+=================
+Table Of Contents
+=================
+
+
+.. toctree::
+   :maxdepth: 3
+
+   ref/pytest_helpers_namespace
+   changelog
+   GitHub Repository <https://github.com/saltstack/pytest-helpers-namespace>
diff --git a/docs/index.rst b/docs/index.rst
index e3fedb7..4d30e3e 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,16 +1,35 @@
-Pytest Helpers Namespace
-========================
+:orphan:
 
-.. include::  ../README.rst
-   :start-after: ========================
+.. _about:
 
+.. include:: ../README.rst
+   :start-after: include-starts-here
+   :end-before: include-ends-here
 
-Contents:
+Documentation
+=============
 
-.. toctree::
-   :maxdepth: 2
+Please see :ref:`Contents <table-of-contents>` for full documentation, including installation and tutorials.
+
+Bugs/Requests
+=============
+
+Please use the `GitHub issue tracker`_ to submit bugs or request features.
+
+
+Changelog
+=========
 
+Consult the :ref:`Changelog <changelog>` page for fixes and enhancements of each version.
+
+
+.. _GitHub issue tracker: https://github.com/saltstack/pytest-helpers-namespace/issues
+
+.. toctree::
+  :maxdepth: 2
+  :caption: Contents:
 
+  all.rst
 
 Indices and tables
 ==================
diff --git a/docs/make.bat b/docs/make.bat
index 28beda1..922152e 100644
--- a/docs/make.bat
+++ b/docs/make.bat
@@ -1,62 +1,18 @@
 @ECHO OFF
 
+pushd %~dp0
+
 REM Command file for Sphinx documentation
 
 if "%SPHINXBUILD%" == "" (
 	set SPHINXBUILD=sphinx-build
 )
+set SOURCEDIR=.
 set BUILDDIR=_build
-set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
-set I18NSPHINXOPTS=%SPHINXOPTS% .
-if NOT "%PAPER%" == "" (
-	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
-	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
-)
 
 if "%1" == "" goto help
 
-if "%1" == "help" (
-	:help
-	echo.Please use `make ^<target^>` where ^<target^> is one of
-	echo.  html       to make standalone HTML files
-	echo.  dirhtml    to make HTML files named index.html in directories
-	echo.  singlehtml to make a single large HTML file
-	echo.  pickle     to make pickle files
-	echo.  json       to make JSON files
-	echo.  htmlhelp   to make HTML files and a HTML help project
-	echo.  qthelp     to make HTML files and a qthelp project
-	echo.  devhelp    to make HTML files and a Devhelp project
-	echo.  epub       to make an epub
-	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
-	echo.  text       to make text files
-	echo.  man        to make manual pages
-	echo.  texinfo    to make Texinfo files
-	echo.  gettext    to make PO message catalogs
-	echo.  changes    to make an overview over all changed/added/deprecated items
-	echo.  xml        to make Docutils-native XML files
-	echo.  pseudoxml  to make pseudoxml-XML files for display purposes
-	echo.  linkcheck  to check all external links for integrity
-	echo.  doctest    to run all doctests embedded in the documentation if enabled
-	echo.  coverage   to run coverage check of the documentation if enabled
-	goto end
-)
-
-if "%1" == "clean" (
-	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
-	del /q /s %BUILDDIR%\*
-	goto end
-)
-
-
-REM Check if sphinx-build is available and fallback to Python version if any
-%SPHINXBUILD% 2> nul
-if errorlevel 9009 goto sphinx_python
-goto sphinx_ok
-
-:sphinx_python
-
-set SPHINXBUILD=python -m sphinx.__init__
-%SPHINXBUILD% 2> nul
+%SPHINXBUILD% >NUL 2>NUL
 if errorlevel 9009 (
 	echo.
 	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
@@ -69,195 +25,11 @@ if errorlevel 9009 (
 	exit /b 1
 )
 
-:sphinx_ok
-
-
-if "%1" == "html" (
-	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
-	goto end
-)
-
-if "%1" == "dirhtml" (
-	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
-	goto end
-)
-
-if "%1" == "singlehtml" (
-	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
-	goto end
-)
-
-if "%1" == "pickle" (
-	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can process the pickle files.
-	goto end
-)
-
-if "%1" == "json" (
-	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can process the JSON files.
-	goto end
-)
-
-if "%1" == "htmlhelp" (
-	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can run HTML Help Workshop with the ^
-.hhp project file in %BUILDDIR%/htmlhelp.
-	goto end
-)
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
 
-if "%1" == "qthelp" (
-	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can run "qcollectiongenerator" with the ^
-.qhcp project file in %BUILDDIR%/qthelp, like this:
-	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pytest-cookiecutterplugin_name.qhcp
-	echo.To view the help file:
-	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pytest-cookiecutterplugin_name.ghc
-	goto end
-)
-
-if "%1" == "devhelp" (
-	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished.
-	goto end
-)
-
-if "%1" == "epub" (
-	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The epub file is in %BUILDDIR%/epub.
-	goto end
-)
-
-if "%1" == "latex" (
-	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
-	goto end
-)
-
-if "%1" == "latexpdf" (
-	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
-	cd %BUILDDIR%/latex
-	make all-pdf
-	cd %~dp0
-	echo.
-	echo.Build finished; the PDF files are in %BUILDDIR%/latex.
-	goto end
-)
-
-if "%1" == "latexpdfja" (
-	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
-	cd %BUILDDIR%/latex
-	make all-pdf-ja
-	cd %~dp0
-	echo.
-	echo.Build finished; the PDF files are in %BUILDDIR%/latex.
-	goto end
-)
-
-if "%1" == "text" (
-	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The text files are in %BUILDDIR%/text.
-	goto end
-)
-
-if "%1" == "man" (
-	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The manual pages are in %BUILDDIR%/man.
-	goto end
-)
-
-if "%1" == "texinfo" (
-	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
-	goto end
-)
-
-if "%1" == "gettext" (
-	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
-	goto end
-)
-
-if "%1" == "changes" (
-	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.The overview file is in %BUILDDIR%/changes.
-	goto end
-)
-
-if "%1" == "linkcheck" (
-	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Link check complete; look for any errors in the above output ^
-or in %BUILDDIR%/linkcheck/output.txt.
-	goto end
-)
-
-if "%1" == "doctest" (
-	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Testing of doctests in the sources finished, look at the ^
-results in %BUILDDIR%/doctest/output.txt.
-	goto end
-)
-
-if "%1" == "coverage" (
-	%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Testing of coverage in the sources finished, look at the ^
-results in %BUILDDIR%/coverage/python.txt.
-	goto end
-)
-
-if "%1" == "xml" (
-	%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The XML files are in %BUILDDIR%/xml.
-	goto end
-)
-
-if "%1" == "pseudoxml" (
-	%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
-	goto end
-)
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
 
 :end
+popd
diff --git a/docs/ref/modules.rst b/docs/ref/modules.rst
new file mode 100644
index 0000000..219de5a
--- /dev/null
+++ b/docs/ref/modules.rst
@@ -0,0 +1,7 @@
+pytest_helpers_namespace
+========================
+
+.. toctree::
+   :maxdepth: 4
+
+   pytest_helpers_namespace
diff --git a/docs/ref/pytest_helpers_namespace.rst b/docs/ref/pytest_helpers_namespace.rst
new file mode 100644
index 0000000..e9fb430
--- /dev/null
+++ b/docs/ref/pytest_helpers_namespace.rst
@@ -0,0 +1,26 @@
+pytest\_helpers\_namespace package
+==================================
+
+.. automodule:: pytest_helpers_namespace
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Submodules
+----------
+
+pytest\_helpers\_namespace.plugin module
+----------------------------------------
+
+.. automodule:: pytest_helpers_namespace.plugin
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pytest\_helpers\_namespace.version module
+-----------------------------------------
+
+.. automodule:: pytest_helpers_namespace.version
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/noxfile.py b/noxfile.py
index fb8dc14..43e4ca6 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -1,3 +1,7 @@
+# Copyright 2021 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+#
+# pylint: disable=import-error,protected-access,line-too-long
 import datetime
 import json
 import os
@@ -11,23 +15,19 @@ from nox.command import CommandFailed
 
 
 COVERAGE_VERSION_REQUIREMENT = "coverage==5.5"
+PYTEST_VERSION_REQUIREMENT = os.environ.get("PYTEST_VERSION_REQUIREMENT") or None
 IS_WINDOWS = sys.platform.lower().startswith("win")
 IS_DARWIN = sys.platform.lower().startswith("darwin")
 
 if IS_WINDOWS:
-    COVERAGE_FAIL_UNDER_PERCENT = 70
+    COVERAGE_FAIL_UNDER_PERCENT = 96
 elif IS_DARWIN:
-    COVERAGE_FAIL_UNDER_PERCENT = 75
+    COVERAGE_FAIL_UNDER_PERCENT = 96
 else:
-    COVERAGE_FAIL_UNDER_PERCENT = 80
+    COVERAGE_FAIL_UNDER_PERCENT = 96
 
 # Be verbose when running under a CI context
-PIP_INSTALL_SILENT = (
-    os.environ.get("JENKINS_URL")
-    or os.environ.get("CI")
-    or os.environ.get("DRONE")
-    or os.environ.get("GITHUB_ACTIONS")
-) is None
+PIP_INSTALL_SILENT = (os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS")) is None
 CI_RUN = PIP_INSTALL_SILENT is False
 SKIP_REQUIREMENTS_INSTALL = "SKIP_REQUIREMENTS_INSTALL" in os.environ
 EXTRA_REQUIREMENTS_INSTALL = os.environ.get("EXTRA_REQUIREMENTS_INSTALL")
@@ -37,7 +37,6 @@ REPO_ROOT = pathlib.Path(__file__).resolve().parent
 # Change current directory to REPO_ROOT
 os.chdir(str(REPO_ROOT))
 
-SITECUSTOMIZE_DIR = str(REPO_ROOT / "tests" / "support" / "coverage")
 ARTIFACTS_DIR = REPO_ROOT / "artifacts"
 # Make sure the artifacts directory exists
 ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
@@ -56,6 +55,24 @@ nox.options.reuse_existing_virtualenvs = True
 nox.options.error_on_missing_interpreters = False
 
 
+def pytest_version(session):
+    try:
+        return session._runner._pytest_version_info
+    except AttributeError:
+        session_pytest_version = session_run_always(
+            session,
+            "python",
+            "-c",
+            'import sys, pkg_resources; sys.stdout.write("{}".format(pkg_resources.get_distribution("pytest").version))',
+            silent=True,
+            log=False,
+        )
+        session._runner._pytest_version_info = tuple(
+            int(part) for part in session_pytest_version.split(".") if part.isdigit()
+        )
+    return session._runner._pytest_version_info
+
+
 def session_run_always(session, *command, **kwargs):
     try:
         # Guess we weren't the only ones wanting this
@@ -73,23 +90,26 @@ def session_run_always(session, *command, **kwargs):
             session._runner.global_config.install_only = old_install_only_value
 
 
-@nox.session(python=("3", "3.5", "3.6", "3.7", "3.8", "3.9"))
+@nox.session(python=("3", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10"))
 def tests(session):
     """
-    Run tests
+    Run tests.
     """
     env = {}
     if SKIP_REQUIREMENTS_INSTALL is False:
         # Always have the wheel package installed
-        session.install("wheel", silent=PIP_INSTALL_SILENT)
-        session.install(COVERAGE_VERSION_REQUIREMENT, silent=PIP_INSTALL_SILENT)
-        pytest_version_requirement = os.environ.get("PYTEST_VERSION_REQUIREMENT") or None
+        session.install("--progress-bar=off", "wheel", silent=PIP_INSTALL_SILENT)
+        session.install(
+            "--progress-bar=off", COVERAGE_VERSION_REQUIREMENT, silent=PIP_INSTALL_SILENT
+        )
+        pytest_version_requirement = PYTEST_VERSION_REQUIREMENT
         if pytest_version_requirement:
             if not pytest_version_requirement.startswith("pytest"):
                 pytest_version_requirement = "pytest{}".format(pytest_version_requirement)
-            session.install(pytest_version_requirement, silent=PIP_INSTALL_SILENT)
-        session.install("-e", ".", silent=PIP_INSTALL_SILENT)
-        session.install("-r", os.path.join("requirements", "tests.txt"), silent=PIP_INSTALL_SILENT)
+            session.install(
+                "--progress-bar=off", pytest_version_requirement, silent=PIP_INSTALL_SILENT
+            )
+        session.install("--progress-bar=off", "-e", ".[tests]", silent=PIP_INSTALL_SILENT)
 
         if EXTRA_REQUIREMENTS_INSTALL:
             session.log(
@@ -103,25 +123,15 @@ def tests(session):
 
     session.run("coverage", "erase")
 
-    python_path_env_var = os.environ.get("PYTHONPATH") or None
-    if python_path_env_var is None:
-        python_path_env_var = SITECUSTOMIZE_DIR
-    else:
-        python_path_entries = python_path_env_var.split(os.pathsep)
-        if SITECUSTOMIZE_DIR in python_path_entries:
-            python_path_entries.remove(SITECUSTOMIZE_DIR)
-        python_path_entries.insert(0, SITECUSTOMIZE_DIR)
-        python_path_env_var = os.pathsep.join(python_path_entries)
-
-    env = {
-        # The updated python path so that sitecustomize is importable
-        "PYTHONPATH": python_path_env_var,
-        # The full path to the .coverage data file. Makes sure we always write
-        # them to the same directory
-        "COVERAGE_FILE": str(COVERAGE_REPORT_DB),
-        # Instruct sub processes to also run under coverage
-        "COVERAGE_PROCESS_START": str(REPO_ROOT / ".coveragerc"),
-    }
+    env.update(
+        {
+            # The full path to the .coverage data file. Makes sure we always write
+            # them to the same directory
+            "COVERAGE_FILE": str(COVERAGE_REPORT_DB),
+            # Instruct sub processes to also run under coverage
+            "COVERAGE_PROCESS_START": str(REPO_ROOT / ".coveragerc"),
+        }
+    )
 
     args = [
         "--rootdir",
@@ -131,9 +141,12 @@ def tests(session):
         "--show-capture=no",
         "--junitxml={}".format(JUNIT_REPORT),
         "--showlocals",
+        "--strict-markers",
         "-ra",
         "-s",
     ]
+    if pytest_version(session) > (6, 2):
+        args.append("--lsof")
     if session._runner.global_config.forcecolor:
         args.append("--color=yes")
     if not session.posargs:
@@ -144,54 +157,51 @@ def tests(session):
                 args.remove("--color=yes")
             args.append(arg)
 
-    session.run("coverage", "run", "-m", "pytest", *args, env=env)
-
-    # Always combine and generate the XML coverage report
-    try:
-        session.run("coverage", "combine")
-    except CommandFailed:
-        # Sometimes some of the coverage files are corrupt which would
-        # trigger a CommandFailed exception
-        pass
-    # Generate report for project code coverage
-    session.run(
-        "coverage",
-        "xml",
-        "-o",
-        str(COVERAGE_REPORT_PROJECT),
-        "--omit=tests/*",
-        "--include=src/pytest_helpers_namespace/*",
-    )
-    # Generate report for tests code coverage
-    session.run(
-        "coverage",
-        "xml",
-        "-o",
-        str(COVERAGE_REPORT_TESTS),
-        "--omit=src/pytest_helpers_namespace/*",
-        "--include=tests/*",
-    )
     try:
-        cmdline = [
-            "coverage",
-            "report",
-            "--show-missing",
-            "--include=src/pytest_helpers_namespace/*,tests/*",
-            "--fail-under={}".format(COVERAGE_FAIL_UNDER_PERCENT),
-        ]
-        session.run(*cmdline)
+        session.run("coverage", "run", "-m", "pytest", *args, env=env)
     finally:
-        if COVERAGE_REPORT_DB.exists():
-            shutil.copyfile(str(COVERAGE_REPORT_DB), str(ARTIFACTS_DIR / ".coverage"))
+        # Always combine and generate the XML coverage report
+        try:
+            session.run("coverage", "combine")
+        except CommandFailed:
+            # Sometimes some of the coverage files are corrupt which would
+            # trigger a CommandFailed exception
+            pass
+        # Generate report for project code coverage
+        session.run(
+            "coverage",
+            "xml",
+            "-o",
+            str(COVERAGE_REPORT_PROJECT),
+            "--omit=tests/*",
+            "--include=src/pytest_helpers_namespace/*",
+        )
+        # Generate report for tests code coverage
+        session.run(
+            "coverage",
+            "xml",
+            "-o",
+            str(COVERAGE_REPORT_TESTS),
+            "--omit=src/pytest_helpers_namespace/*",
+            "--include=tests/*",
+        )
+        try:
+            cmdline = [
+                "coverage",
+                "report",
+                "--show-missing",
+                "--include=src/pytest_helpers_namespace/*,tests/*",
+            ]
+            session.run(*cmdline)
+            if pytest_version(session) >= (6, 2):
+                cmdline.append("--fail-under={}".format(COVERAGE_FAIL_UNDER_PERCENT))
+        finally:
+            if COVERAGE_REPORT_DB.exists():
+                shutil.copyfile(str(COVERAGE_REPORT_DB), str(ARTIFACTS_DIR / ".coverage"))
 
 
 def _lint(session, rcfile, flags, paths):
-    session.install(
-        "--progress-bar=off",
-        "-r",
-        os.path.join("requirements", "lint.txt"),
-        silent=PIP_INSTALL_SILENT,
-    )
+    session.install("--progress-bar=off", "-e", ".[lint]", silent=PIP_INSTALL_SILENT)
     session.run("pylint", "--version")
     pylint_report_path = os.environ.get("PYLINT_REPORT")
 
@@ -209,7 +219,7 @@ def _lint(session, rcfile, flags, paths):
             sys.stdout.flush()
             if pylint_report_path:
                 # Write report
-                with open(pylint_report_path, "w") as wfh:
+                with open(pylint_report_path, "w", encoding="utf-8") as wfh:
                     wfh.write(contents)
                 session.log("Report file written to %r", pylint_report_path)
         stdout.close()
@@ -253,21 +263,16 @@ def lint_tests(session):
 @nox.session(python="3")
 def docs(session):
     """
-    Build Docs
+    Build Docs.
     """
-    session.install(
-        "--progress-bar=off",
-        "-r",
-        os.path.join("requirements", "docs.txt"),
-        silent=PIP_INSTALL_SILENT,
-    )
+    session.install("--progress-bar=off", "-e", ".[docs]", silent=PIP_INSTALL_SILENT)
     os.chdir("docs/")
     session.run("make", "clean", external=True)
-    session.run("make", "linkcheck", "SPHINXOPTS=-W", external=True)
+    # session.run("make", "linkcheck", "SPHINXOPTS=-W", external=True)
     session.run("make", "coverage", "SPHINXOPTS=-W", external=True)
     docs_coverage_file = os.path.join("_build", "html", "python.txt")
     if os.path.exists(docs_coverage_file):
-        with open(docs_coverage_file) as rfh:
+        with open(docs_coverage_file, encoding="utf-8") as rfh:
             contents = rfh.readlines()[2:]
             if contents:
                 session.error("\n" + "".join(contents))
@@ -275,17 +280,23 @@ def docs(session):
     os.chdir("..")
 
 
+@nox.session(name="docs-dev", python="3")
+def docs_dev(session):
+    """
+    Build Docs.
+    """
+    session.install("--progress-bar=off", "-e", ".[docs]", silent=PIP_INSTALL_SILENT)
+    os.chdir("docs/")
+    session.run("make", "html", "SPHINXOPTS=-W", external=True, env={"LOCAL_DEV_BUILD": "1"})
+    os.chdir("..")
+
+
 @nox.session(name="docs-crosslink-info", python="3")
 def docs_crosslink_info(session):
     """
-    Report intersphinx cross links information
+    Report intersphinx cross links information.
     """
-    session.install(
-        "--progress-bar=off",
-        "-r",
-        os.path.join("requirements", "docs.txt"),
-        silent=PIP_INSTALL_SILENT,
-    )
+    session.install("--progress-bar=off", "-e", ".[docs]", silent=PIP_INSTALL_SILENT)
     os.chdir("docs/")
     intersphinx_mapping = json.loads(
         session.run(
@@ -319,15 +330,92 @@ def docs_crosslink_info(session):
 @nox.session(name="gen-api-docs", python="3")
 def gen_api_docs(session):
     """
-    Generate API Docs
+    Generate API Docs.
     """
-    session.install(
-        "--progress-bar=off",
-        "-r",
-        os.path.join("requirements", "docs.txt"),
-        silent=PIP_INSTALL_SILENT,
-    )
-    shutil.rmtree("docs/ref")
+    session.install("--progress-bar=off", "-e", ".[docs]", silent=PIP_INSTALL_SILENT)
+    shutil.rmtree("docs/ref", ignore_errors=True)
     session.run(
         "sphinx-apidoc", "--module-first", "-o", "docs/ref/", "src/pytest_helpers_namespace/"
     )
+
+
+@nox.session(name="twine-check", python="3")
+def twine_check(session):
+    """
+    Run ``twine-check`` against the source distribution package.
+    """
+    session.install("--progress-bar=off", "twine", silent=PIP_INSTALL_SILENT)
+    session.run(
+        "python",
+        "setup.py",
+        "sdist",
+        silent=True,
+        log=False,
+    )
+    session.run("twine", "check", "dist/*")
+
+
+@nox.session(name="changelog", python="3")
+@nox.parametrize("draft", [False, True])
+def changelog(session, draft):
+    """
+    Generate changelog.
+    """
+    session.install("--progress-bar=off", "-e", ".[changelog]", silent=PIP_INSTALL_SILENT)
+    version = session.run(
+        "python",
+        "setup.py",
+        "--version",
+        silent=True,
+        log=False,
+        stderr=None,
+    )
+
+    town_cmd = ["towncrier", "build", "--version={}".format(version.strip())]
+    if draft:
+        town_cmd.append("--draft")
+    session.run(*town_cmd)
+
+
+@nox.session(name="release")
+def release(session):
+    """
+    Create a release tag.
+    """
+    if not session.posargs:
+        session.error(
+            "Forgot to pass the version to release? For example `nox -e release -- 1.1.0`"
+        )
+    if len(session.posargs) > 1:
+        session.error(
+            "Only one argument is supported by the `release` nox session. "
+            "For example `nox -e release -- 1.1.0`"
+        )
+    version = session.posargs[0]
+    try:
+        session.log("Generating temporary %s tag", version)
+        session.run("git", "tag", "-as", version, "-m", "Release {}".format(version), external=True)
+        changelog(session, draft=False)
+    except CommandFailed:
+        session.error("Failed to generate the temporary tag")
+    # session.notify("changelog(draft=False)")
+    try:
+        session.log("Generating the release changelog")
+        session.run(
+            "git",
+            "commit",
+            "-a",
+            "-m",
+            "Generate Changelog for version {}".format(version),
+            external=True,
+        )
+    except CommandFailed:
+        session.error("Failed to generate the release changelog")
+    try:
+        session.log("Overwriting temporary %s tag", version)
+        session.run(
+            "git", "tag", "-fas", version, "-m", "Release {}".format(version), external=True
+        )
+    except CommandFailed:
+        session.error("Failed to overwrite the temporary tag")
+    session.warn("Don't forget to push the newly created tag")
diff --git a/pyproject.toml b/pyproject.toml
index be0791f..ee96ddb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,4 +4,46 @@ build-backend = "setuptools.build_meta"
 
 [tool.setuptools_scm]
 write_to = "src/pytest_helpers_namespace/version.py"
-write_to_template = "__version__ = \"{version}\""
+write_to_template = "# pylint: skip-file\n\n__version__ = \"{version}\"\n"
+
+[tool.towncrier]
+package = "pytest_helpers_namespace"
+filename = "CHANGELOG.rst"
+directory = "changelog/"
+title_format = "{version}"
+template = "changelog/_template.rst"
+
+  [[tool.towncrier.type]]
+  directory = "breaking"
+  name = "Breaking Changes"
+  showcontent = true
+
+  [[tool.towncrier.type]]
+  directory = "deprecation"
+  name = "Deprecations"
+  showcontent = true
+
+  [[tool.towncrier.type]]
+  directory = "feature"
+  name = "Features"
+  showcontent = true
+
+  [[tool.towncrier.type]]
+  directory = "improvement"
+  name = "Improvements"
+  showcontent = true
+
+  [[tool.towncrier.type]]
+  directory = "bugfix"
+  name = "Bug Fixes"
+  showcontent = true
+
+  [[tool.towncrier.type]]
+  directory = "doc"
+  name = "Improved Documentation"
+  showcontent = true
+
+  [[tool.towncrier.type]]
+  directory = "trivial"
+  name = "Trivial/Internal Changes"
+  showcontent = true
diff --git a/requirements/changelog.txt b/requirements/changelog.txt
new file mode 100644
index 0000000..fe1c872
--- /dev/null
+++ b/requirements/changelog.txt
@@ -0,0 +1 @@
+towncrier==21.9.0rc1
diff --git a/requirements/docs.txt b/requirements/docs.txt
index e2c4f44..5d3aa3d 100644
--- a/requirements/docs.txt
+++ b/requirements/docs.txt
@@ -1,6 +1,9 @@
 -r base.txt
 -r tests.txt
+furo
 sphinx
-sphinx-material-saltstack
+sphinx-copybutton
 sphinx-prompt
 sphinxcontrib-spelling
+towncrier==21.3.0
+sphinxcontrib-towncrier >= 0.2.0a0
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 41a062b..8b68639 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -1,7 +1,6 @@
 -r base.txt
 -r tests.txt
-pylint==2.4.4
-saltpylint==2019.6.7
+pylint==2.12.2
 pyenchant
 black; python_version >= '3.7'
 reorder-python-imports; python_version >= '3.7'
diff --git a/setup.cfg b/setup.cfg
index 2985a7e..0b04f97 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,6 +2,7 @@
 name = pytest-helpers-namespace
 description = Pytest Helpers Namespace Plugin
 long_description = file: README.rst
+long_description_content_type = text/x-rst
 author = Pedro Algarvio
 author_email = pedro@algarvio.me
 url = https://github.com/saltstack/pytest-helpers-namespace
@@ -20,6 +21,7 @@ classifiers =
 	Programming Language :: Python :: 3.7
 	Programming Language :: Python :: 3.8
 	Programming Language :: Python :: 3.9
+	Programming Language :: Python :: 3.10
 	Development Status :: 5 - Production/Stable
 	Intended Audience :: Developers
 	License :: OSI Approved :: Apache Software License
@@ -31,7 +33,7 @@ include_package_data = True
 package_dir = 
 	=src
 packages = find:
-python_requires = >= 3.5
+python_requires = >= 3.5.6
 setup_requires = 
 	setuptools>=50.3.2
 	setuptools_scm[toml]>=3.4
@@ -48,6 +50,7 @@ extras_require =
 	docs = requirements/docs.txt
 	lint = requirements/lint.txt
 	tests = requirements/tests.txt
+	changelog = requirements/changelog.txt
 
 [options.entry_points]
 pytest11 = 
@@ -56,6 +59,53 @@ pytest11 =
 [bdist_wheel]
 universal = false
 
+[sdist]
+owner = root
+group = root
+
+[flake8]
+max-line-length = 120
+exclude = 
+	.git,
+	.nox,
+	__pycache__,
+	src/pytest_helpers_namespace/version.py,
+	build,
+	dist,
+	docs/conf.py,
+	setup.py,
+	.pre-commit-hooks
+per-file-ignores = 
+	__init__.py: F401
+	noxfile.py: D100,D102,D103,D107,D212,E501
+	src/pytestsysstats/plugin.py: W503
+	tests/*.py: D100,D103
+ignore = 
+	D104,
+	D107,
+	D212,
+	D200,
+builtins = 
+	__salt__
+	__opts__
+	__salt_system_encoding__
+docstring-convention = google
+
+[mypy]
+python_version = 3.7
+mypy_path = src
+ignore_missing_imports = True
+no_implicit_optional = True
+show_error_codes = True
+strict_equality = True
+warn_redundant_casts = True
+warn_return_any = True
+warn_unused_configs = True
+warn_unused_ignores = True
+disallow_any_generics = True
+check_untyped_defs = True
+no_implicit_reexport = True
+
 [egg_info]
 tag_build = 
 tag_date = 0
diff --git a/setup.py b/setup.py
index 8d30120..cbc50f7 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,7 @@
 #!/usr/bin/env python
+# Copyright 2021 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+#
 import setuptools
 
 if __name__ == "__main__":
diff --git a/src/pytest_helpers_namespace.egg-info/PKG-INFO b/src/pytest_helpers_namespace.egg-info/PKG-INFO
index 7c38c57..5900135 100644
--- a/src/pytest_helpers_namespace.egg-info/PKG-INFO
+++ b/src/pytest_helpers_namespace.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: pytest-helpers-namespace
-Version: 2021.4.29
+Version: 2021.12.29
 Summary: Pytest Helpers Namespace Plugin
 Home-page: https://github.com/saltstack/pytest-helpers-namespace
 Author: Pedro Algarvio
@@ -37,6 +37,9 @@ Description: .. image:: https://github.com/saltstack/pytest-helpers-namespace/ac
             :alt: Supported implementations
             :target: https://pypi.python.org/pypi/pytest-helpers-namespace
         
+        ..
+           include-starts-here
+        
         
         Pytest Helpers Namespace
         ========================
@@ -78,11 +81,12 @@ Description: .. image:: https://github.com/saltstack/pytest-helpers-namespace/ac
         
            import pytest
         
+        
            @pytest.helpers.register
            def foo(bar):
-               '''
+               """
                this dumb helper function will just return what you pass to it
-               '''
+               """
                return bar
         
         
@@ -101,15 +105,16 @@ Description: .. image:: https://github.com/saltstack/pytest-helpers-namespace/ac
         
         .. code-block:: python
         
-           pytest_plugins = ['helpers_namespace']
+           pytest_plugins = ["helpers_namespace"]
         
            import pytest
         
+        
            @pytest.helpers.can.haz.register
            def foo(bar):
-               '''
+               """
                this dumb helper function will just return what you pass to it
-               '''
+               """
                return bar
         
         
@@ -124,86 +129,6 @@ Description: .. image:: https://github.com/saltstack/pytest-helpers-namespace/ac
         You can even pass a name to the register function and that will be the helper function name.
         
         
-        Contributing
-        ------------
-        Contributions are very welcome. Tests can be run with `nox`_, please ensure
-        the coverage at least stays the same before you submit a pull request.
-        
-        License
-        -------
-        
-        Distributed under the terms of the `Apache Software License 2.0`_ license,
-        "pytest-helpers-namespace" is free and open source software.
-        
-        
-        Issues
-        ------
-        
-        If you encounter any problems, please `file an issue`_ along with a detailed
-        description.
-        
-        Changelog
-        ---------
-        
-        v2021.3.24
-        ~~~~~~~~~~
-        
-        * Switched project to a ``src`` layout.
-        * Switched project to a declarative setuptools approach
-        * Added support to check if a helper has been registered
-        * Pytest >= 6.1.1 is now required
-        
-        v2019.1.8
-        ~~~~~~~~~
-        
-        * Patch PyTest before any ``conftest.py`` file is processed.
-        
-        v2019.1.7
-        ~~~~~~~~~
-        
-        * Support PyTest >= 4.1
-        
-        v2019.1.6.post1
-        ~~~~~~~~~~~~~~~
-        
-        * No changes were made besides locking to PyTest < 4.0
-        
-        v2019.1.6
-        ~~~~~~~~~
-        
-        * No changes were made besides locking to PyTest < 4.1
-        
-        v2017.11.11
-        ~~~~~~~~~~~
-        
-        * Allow passing a string to the register function which will be the helper name
-        
-        v2016.7.10
-        ~~~~~~~~~~
-        
-        * Allow a registered function to contibue to behave as a regular function. `#4`_.
-        
-        v2016.4.15
-        ~~~~~~~~~~
-        
-        * Hide the ``FuncWrapper`` traceback in pytest failures. `#3`_. Thanks Logan Glickfield(`@lsglick`_)
-        
-        v2016.4.5
-        ~~~~~~~~~
-        
-        * Use a wrapper class instead of adding an attribute to a function.
-        
-        v2016.4.3
-        ~~~~~~~~~
-        
-        * Provide proper errors when helper functions or namespaces are being
-          overridden. `#1`_
-        
-        v2016.3.2
-        ~~~~~~~~~~
-        
-        * First working release
-        
         ----
         
         This `Pytest`_ plugin was generated with `Cookiecutter`_ along with
@@ -211,19 +136,18 @@ Description: .. image:: https://github.com/saltstack/pytest-helpers-namespace/ac
         
         .. _`Cookiecutter`: https://github.com/audreyr/cookiecutter
         .. _`@hackebrot`: https://github.com/hackebrot
-        .. _`Apache Software License 2.0`: http://www.apache.org/licenses/LICENSE-2.0
         .. _`cookiecutter-pytest-plugin`: https://github.com/pytest-dev/cookiecutter-pytest-plugin
-        .. _`file an issue`: https://github.com/saltstack/pytest-helpers-namespace/issues
         .. _`pytest`: https://github.com/pytest-dev/pytest
-        .. _`nox`: https://nox.thea.codes/en/stable/
         .. _`pip`: https://pypi.python.org/pypi/pip/
         .. _`PyPI`: https://pypi.python.org/pypi
         
-        .. _`#1`: https://github.com/saltstack/pytest-helpers-namespace/issues/1
-        .. _`#3`: https://github.com/saltstack/pytest-helpers-namespace/pull/3
-        .. _`#4`: https://github.com/saltstack/pytest-helpers-namespace/issues/4
+        ..
+           include-ends-here
+        
+        Documentation
+        =============
         
-        .. _`@lsglick`: https://github.com/lsglick
+        The full documentation can be seen `here <https://pytest-helpers-namespace.readthedocs.io>`_.
         
 Platform: unix
 Platform: linux
@@ -239,10 +163,13 @@ Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: Apache Software License
-Requires-Python: >=3.5
+Requires-Python: >=3.5.6
+Description-Content-Type: text/x-rst
 Provides-Extra: docs
 Provides-Extra: lint
 Provides-Extra: tests
+Provides-Extra: changelog
diff --git a/src/pytest_helpers_namespace.egg-info/SOURCES.txt b/src/pytest_helpers_namespace.egg-info/SOURCES.txt
index bfd5697..001c95c 100644
--- a/src/pytest_helpers_namespace.egg-info/SOURCES.txt
+++ b/src/pytest_helpers_namespace.egg-info/SOURCES.txt
@@ -4,27 +4,43 @@
 .pylint-spelling-words
 .pylintrc
 AUTHORS.rst
+CHANGELOG.rst
+CODE_OF_CONDUCT.md
+CONTRIBUTING.md
 LICENSE
 README.rst
-appveyor.yml
 noxfile.py
 pyproject.toml
 setup.cfg
 setup.py
+.github/CODEOWNERS
 .github/workflows/testing.yml
+.pre-commit-hooks/check-changelog-entries.py
+.pre-commit-hooks/copyright-headers.py
 .pre-commit-hooks/sort-pylint-spelling-words.py
+changelog/_template.rst
 docs/Makefile
+docs/all.rst
+docs/changelog.rst
 docs/conf.py
+docs/contents.rst
 docs/index.rst
 docs/make.bat
 docs/sitevars.rst
 docs/_static/.gitkeep
+docs/_static/css/inline-include.css
+docs/_static/img/SaltProject_Logomark_teal.png
+docs/_static/img/SaltProject_altlogo_teal.png
+docs/ref/modules.rst
+docs/ref/pytest_helpers_namespace.rst
 requirements/base.txt
+requirements/changelog.txt
 requirements/docs.txt
 requirements/lint.txt
 requirements/tests.txt
 src/pytest_helpers_namespace/__init__.py
 src/pytest_helpers_namespace/plugin.py
+src/pytest_helpers_namespace/py.typed
 src/pytest_helpers_namespace/version.py
 src/pytest_helpers_namespace.egg-info/PKG-INFO
 src/pytest_helpers_namespace.egg-info/SOURCES.txt
diff --git a/src/pytest_helpers_namespace.egg-info/requires.txt b/src/pytest_helpers_namespace.egg-info/requires.txt
index a178fc0..73e12c0 100644
--- a/src/pytest_helpers_namespace.egg-info/requires.txt
+++ b/src/pytest_helpers_namespace.egg-info/requires.txt
@@ -1,14 +1,19 @@
 pytest>=6.0.0
 
+[changelog]
+towncrier==21.9.0rc1
+
 [docs]
+furo
 sphinx
-sphinx-material-saltstack
+sphinx-copybutton
 sphinx-prompt
 sphinxcontrib-spelling
+towncrier==21.3.0
+sphinxcontrib-towncrier>=0.2.0a0
 
 [lint]
-pylint==2.4.4
-saltpylint==2019.6.7
+pylint==2.12.2
 pyenchant
 
 [lint:python_version >= "3.7"]
diff --git a/src/pytest_helpers_namespace/__init__.py b/src/pytest_helpers_namespace/__init__.py
index a3a83f6..14607bf 100644
--- a/src/pytest_helpers_namespace/__init__.py
+++ b/src/pytest_helpers_namespace/__init__.py
@@ -1,3 +1,6 @@
+# Copyright 2021 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+#
 # pylint: disable=missing-module-docstring
 import pathlib
 
diff --git a/src/pytest_helpers_namespace/plugin.py b/src/pytest_helpers_namespace/plugin.py
index 90f2bfe..375a5d9 100644
--- a/src/pytest_helpers_namespace/plugin.py
+++ b/src/pytest_helpers_namespace/plugin.py
@@ -1,14 +1,28 @@
+# Copyright 2021 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
 """
-pytest_helpers_namespace.plugin
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Pytest Helpers Namespace Plugin
+Pytest Helpers Namespace Plugin.
 """
 from functools import partial
 from functools import wraps
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
 
 import pytest
 
+if TYPE_CHECKING:
+    from typing import Dict
+
+    # pylint: disable=import-error,unused-import,no-name-in-module
+    from _pytest.main import Session
+
+    # pylint: enable=import-error,unused-import,no-name-in-module
+
 try:  # pragma: no cover
     import importlib.metadata
 
@@ -24,13 +38,22 @@ except ImportError:  # pragma: no cover
         PYTEST_61 = pkg_resources.get_distribution("pytest").version >= "6.1.0"
 
 
+F = TypeVar("F", bound=Callable[..., Any])
+
+
 class FuncWrapper:
-    def __init__(self, func):
+    """
+    Wrapper class for helper functions and namespaces.
+    """
+
+    def __init__(self, func: F):
         self.func = func
 
     @staticmethod
-    def register(func):
+    def register(func: F) -> F:
         """
+        Register a helper function.
+
         This function will just raise a RuntimeError in case a function
         registration, which also sets a nested namespace, tries to override
         a known helper function with that nested namespace.
@@ -40,12 +63,13 @@ class FuncWrapper:
         we will raise the exception below.
         """
         raise RuntimeError(
-            "A namespace is already registered under the name: {}".format(func.__name__)
+            "Helper functions cannot be used to register new helper functions. "
+            "Register and use a namespace for that."
         )
 
-    def __call__(self, *args, **kwargs):
+    def __call__(self, *args: Any, **kwargs: Any) -> Any:
         """
-        This wrapper will just call the actual helper function
+        This wrapper will just call the actual helper function.
         """
         __tracebackhide__ = True
         return self.func(*args, **kwargs)
@@ -53,20 +77,20 @@ class FuncWrapper:
 
 class HelpersRegistry:
     """
-    Helper functions registrar which supports namespaces
+    Helper functions registrar which supports namespaces.
     """
 
     __slots__ = ("_registry",)
 
-    def __init__(self):
-        self._registry = {}
+    def __init__(self) -> None:
+        self._registry = {}  # type: "Dict[str, Union[FuncWrapper, HelpersRegistry]]"
 
-    def register(self, func, name=None):
+    def register(self, func: Union[F, str], name: Optional[str] = None) -> F:
         """
-        Register's a new function as a helper
+        Register's a new function as a helper.
         """
         if isinstance(func, str):
-            return partial(self.register, name=func)
+            return cast(F, partial(self.register, name=func))
 
         if name is None:
             name = func.__name__
@@ -77,41 +101,69 @@ class HelpersRegistry:
         self._registry[name] = wraps(func)(FuncWrapper(func))
         return func
 
-    def __getattribute__(self, name):
+    def __getattribute__(self, name: str) -> Any:
+        """
+        Return an attribute from the registry or register a new namespace.
+        """
         if name in ("__class__", "_registry", "register"):
             return object.__getattribute__(self, name)
         return self._registry.setdefault(name, self.__class__())
 
-    def __repr__(self):
+    def __repr__(self) -> str:
+        """
+        Return a string representation of the class.
+        """
         return "{} {!r}>".format(self.__class__.__name__, self._registry)
 
-    def __call__(self, *_, **__):
-        raise RuntimeError("The helper being called was not registred")
+    def __call__(self, *_: Any, **__: Any) -> Any:
+        """
+        Show a warning when calling an unregistered helper function.
+        """
+        raise RuntimeError("The helper being called was not registered")
 
-    def __contains__(self, key):
+    def __contains__(self, key: str) -> bool:
+        """
+        Check for the presence of a helper name in the registry.
+        """
         return key in self._registry
 
     if PYTEST_61 is False:  # pragma: no cover
 
-        def __fspath__(self):
+        def __fspath__(self) -> str:
+            """
+            Compatibility method against newer Pytest versions.
+            """
             # Compatibility with PyTest 6.0.x
             return __file__
 
 
-def pytest_load_initial_conftests(*_):
+def pytest_load_initial_conftests(*_: Any) -> None:
+    """
+    Hook into pytest to inject our custom ``helpers`` registry.
+    """
     try:
         pytest.helpers  # pragma: no cover
     except AttributeError:
         pytest.helpers = HelpersRegistry()
 
 
-@pytest.hookimpl(trylast=True)
-def pytest_sessionstart(session):
+@pytest.hookimpl(trylast=True)  # type: ignore[misc]
+def pytest_sessionstart(session: "Session") -> None:
+    """
+    Register our plugin with pytest.
+    """
     session.config.pluginmanager.register(pytest.helpers, "helpers-namespace")
 
 
-def pytest_unconfigure():  # pragma: no cover
+def pytest_unconfigure() -> None:  # pragma: no cover
+    """
+    Delete our custom ``helpers`` registry from the ``pytest`` module namespace.
+    """
     try:
         delattr(pytest, "helpers")
     except AttributeError:
         pass
+
+
+if TYPE_CHECKING:
+    setattr(pytest, "helpers", HelpersRegistry())
diff --git a/src/pytest_helpers_namespace/py.typed b/src/pytest_helpers_namespace/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/src/pytest_helpers_namespace/version.py b/src/pytest_helpers_namespace/version.py
index 8755f94..42cbdd6 100644
--- a/src/pytest_helpers_namespace/version.py
+++ b/src/pytest_helpers_namespace/version.py
@@ -1 +1,3 @@
-__version__ = "2021.4.29"
\ No newline at end of file
+# pylint: skip-file
+
+__version__ = "2021.12.29"
diff --git a/tests/conftest.py b/tests/conftest.py
index 383fa44..e93bc7b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,2 +1,40 @@
-# pragma: no cover
-pytest_plugins = "pytester"
+# Copyright 2021 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+#
+import logging
+
+import pytest
+
+try:  # pragma: no cover
+    import importlib.metadata
+
+    pkg_version = importlib.metadata.version
+except ImportError:  # pragma: no cover
+    try:
+        import importlib_metadata
+
+        pkg_version = importlib_metadata.version
+    except ImportError:  # pragma: no cover
+        import pkg_resources
+
+        def pkg_version(package):
+            return pkg_resources.get_distribution(package).version
+
+
+log = logging.getLogger(__name__)
+
+
+def pkg_version_info(package):
+    """
+    Return a version info tuple for the given package.
+    """
+    return tuple(int(part) for part in pkg_version(package).split(".") if part.isdigit())
+
+
+if pkg_version_info("pytest") >= (6, 2):
+    pytest_plugins = ["pytester"]
+else:  # pragma: no cover
+
+    @pytest.fixture
+    def pytester():
+        pytest.skip("The pytester fixture is not available in Pytest < 6.2.0")
diff --git a/tests/test_helpers_namespace.py b/tests/test_helpers_namespace.py
index 27f4743..a47f05d 100644
--- a/tests/test_helpers_namespace.py
+++ b/tests/test_helpers_namespace.py
@@ -1,7 +1,10 @@
+# Copyright 2021 VMware, Inc.
+# SPDX-License-Identifier: Apache-2.0
+#
 import pytest
 
 
-@pytest.fixture
+@pytest.fixture(autouse=True)
 def reset_helpers_namespace(request):
     try:
         yield
@@ -10,8 +13,8 @@ def reset_helpers_namespace(request):
         plugin._registry.clear()
 
 
-def test_namespace(testdir):
-    testdir.makeconftest(
+def test_namespace(pytester):
+    pytester.makeconftest(
         """
         import pytest
 
@@ -21,7 +24,7 @@ def test_namespace(testdir):
         """
     )
 
-    testdir.makepyfile(
+    pytester.makepyfile(
         """
         import pytest
 
@@ -31,7 +34,7 @@ def test_namespace(testdir):
     """
     )
 
-    result = testdir.runpytest_subprocess("-s")
+    result = pytester.runpytest("-s")
 
     # fnmatch_lines does an assertion internally
     result.stdout.fnmatch_lines(
@@ -44,8 +47,8 @@ def test_namespace(testdir):
     assert result.ret == 0
 
 
-def test_nested_namespace(testdir):
-    testdir.makeconftest(
+def test_nested_namespace(pytester):
+    pytester.makeconftest(
         """
         import pytest
 
@@ -55,7 +58,7 @@ def test_nested_namespace(testdir):
         """
     )
 
-    testdir.makepyfile(
+    pytester.makepyfile(
         """
         import pytest
 
@@ -65,7 +68,7 @@ def test_nested_namespace(testdir):
     """
     )
 
-    result = testdir.runpytest_subprocess("-s")
+    result = pytester.runpytest("-s")
 
     # fnmatch_lines does an assertion internally
     result.stdout.fnmatch_lines(
@@ -78,20 +81,21 @@ def test_nested_namespace(testdir):
     assert result.ret == 0
 
 
-def test_unregistered_namespace(testdir):
-    testdir.makepyfile(
+def test_unregistered_namespace(pytester):
+    pytester.makepyfile(
         """
         import pytest
 
         def test_helpers():
             with pytest.raises(RuntimeError) as exc:
                 assert pytest.helpers.foo(True) is True
-            assert 'The helper being called was not registred' in str(exc)
+            strexc = str(exc)
+            assert 'The helper being called was not registered' in strexc
             print('PASSED')
     """
     )
 
-    result = testdir.runpytest_subprocess("-s")
+    result = pytester.runpytest("-s")
 
     # fnmatch_lines does an assertion internally
     result.stdout.fnmatch_lines(
@@ -104,8 +108,8 @@ def test_unregistered_namespace(testdir):
     assert result.ret == 0
 
 
-def test_namespace_override(testdir):
-    testdir.makeconftest(
+def test_namespace_override(pytester):
+    pytester.makeconftest(
         """
         import pytest
 
@@ -118,19 +122,19 @@ def test_namespace_override(testdir):
             return bar
         """
     )
-    testdir.makepyfile(
+    pytester.makepyfile(
         """
         import pytest
 
         def test_helpers():
             with pytest.raises(RuntimeError) as exc:
                 assert pytest.helpers.foo(True) is True
-            assert 'The helper being called was not registred' in str(exc)
+            assert 'The helper being called was not registered' in str(exc)
             print('PASSED')
     """
     )
 
-    result = testdir.runpytest_subprocess("-s")
+    result = pytester.runpytest("-s")
 
     # fnmatch_lines does an assertion internally
     result.stderr.fnmatch_lines(
@@ -141,8 +145,8 @@ def test_namespace_override(testdir):
     assert result.ret != 0
 
 
-def test_namespace_override_2(testdir):
-    testdir.makeconftest(
+def test_helper_override(pytester):
+    pytester.makeconftest(
         """
         import pytest
 
@@ -150,93 +154,80 @@ def test_namespace_override_2(testdir):
         def foo(bar):
             return bar
 
-        @pytest.helpers.foo.register
-        def bar(bar):
+        @pytest.helpers.register
+        def foo(bar):
             return bar
         """
     )
-    testdir.makepyfile(
+    pytester.makepyfile(
         """
         import pytest
 
         def test_helpers():
             with pytest.raises(RuntimeError) as exc:
                 assert pytest.helpers.foo(True) is True
-            assert 'The helper being called was not registred' in str(exc)
+            assert 'The helper being called was not registered' in str(exc)
             print('PASSED')
     """
     )
 
-    result = testdir.runpytest_subprocess("-s")
+    result = pytester.runpytest("-s")
 
     # fnmatch_lines does an assertion internally
     result.stderr.fnmatch_lines(
-        ["*RuntimeError: A namespace is already registered under the name: bar"]
+        ["*RuntimeError: A helper function is already registered under the name: foo"]
     )
 
     # make sure that that we get a '0' exit code for the test suite
     assert result.ret != 0
 
 
-def test_helper_override(testdir):
-    testdir.makeconftest(
+def test_helper_as_regular_function(pytester):
+    pytester.makepyfile(
         """
         import pytest
 
         @pytest.helpers.register
-        def foo(bar):
-            return bar
-
-        @pytest.helpers.register
-        def foo(bar):
-            return bar
-        """
-    )
-    testdir.makepyfile(
-        """
-        import pytest
+        def foo2():
+            return 'bar'
 
         def test_helpers():
-            with pytest.raises(RuntimeError) as exc:
-                assert pytest.helpers.foo(True) is True
-            assert 'The helper being called was not registred' in str(exc)
-            print('PASSED')
-    """
+            assert pytest.helpers.foo2() == 'bar'
+            assert foo2() == 'bar'
+        """
     )
 
-    result = testdir.runpytest_subprocess("-s")
+    result = pytester.runpytest("-svv", "--log-cli-level=debug")
 
     # fnmatch_lines does an assertion internally
-    result.stderr.fnmatch_lines(
-        ["*RuntimeError: A helper function is already registered under the name: foo"]
-    )
+    result.stdout.fnmatch_lines(["test_helper_as_regular_function.py::test_helpers PASSED"])
 
     # make sure that that we get a '0' exit code for the test suite
-    assert result.ret != 0
+    assert result.ret == 0
 
 
-def test_helper_as_regular_function(testdir):
-    testdir.makepyfile(
+def test_helper_with_custom_name(pytester):
+    pytester.makepyfile(
         """
         import pytest
 
-        @pytest.helpers.register
+        @pytest.helpers.register('jump')
         def foo():
             return 'bar'
 
         def test_helpers():
-            assert pytest.helpers.foo() == 'bar'
+            assert pytest.helpers.jump() == 'bar'
             assert foo() == 'bar'
             print('PASSED')
     """
     )
 
-    result = testdir.runpytest_subprocess("-s")
+    result = pytester.runpytest("-s")
 
     # fnmatch_lines does an assertion internally
     result.stdout.fnmatch_lines(
         [
-            "test_helper_as_regular_function.py PASSED",
+            "test_helper_with_custom_name.py PASSED",
         ]
     )
 
@@ -244,42 +235,64 @@ def test_helper_as_regular_function(testdir):
     assert result.ret == 0
 
 
-def test_helper_with_custom_name(testdir):
-    testdir.makepyfile(
+def test_helper_contains_method(pytester):
+    pytester.makeconftest(
         """
         import pytest
 
-        @pytest.helpers.register('jump')
-        def foo():
-            return 'bar'
+        assert "bar" not in pytest.helpers
+
+        @pytest.helpers.register
+        def bar():
+            return True
+        """
+    )
+    pytester.makepyfile(
+        """
+        import pytest
+
+        def test_it():
+            assert "bar" in pytest.helpers
+            assert pytest.helpers.bar() is True
+        """
+    )
+
+    result = pytester.runpytest("-vv")
+    result.assert_outcomes(passed=1)
+
+
+def test_call_register_on_helper_function(pytester):
+    pytester.makeconftest(
+        """
+        import pytest
+
+        @pytest.helpers.register
+        def foo(bar):
+            return bar
+
+        @pytest.helpers.foo.register
+        def blah(blah):
+            return bar
+        """
+    )
+    pytester.makepyfile(
+        """
+        import pytest
 
         def test_helpers():
-            assert pytest.helpers.jump() == 'bar'
-            assert foo() == 'bar'
-            print('PASSED')
+            with pytest.raises(RuntimeError) as exc:
+                assert pytest.helpers.foo(True) is True
     """
     )
 
-    result = testdir.runpytest_subprocess("-s")
+    result = pytester.runpytest("-s")
 
     # fnmatch_lines does an assertion internally
-    result.stdout.fnmatch_lines(
+    result.stderr.fnmatch_lines(
         [
-            "test_helper_with_custom_name.py PASSED",
+            "*RuntimeError: Helper functions cannot be used to register new helper functions. "
+            "Register and use a namespace for that.*",
         ]
     )
-
     # make sure that that we get a '0' exit code for the test suite
-    assert result.ret == 0
-
-
-@pytest.mark.usefixtures("reset_helpers_namespace")
-def test_helper_contains_method():
-    assert "bar" not in pytest.helpers
-
-    @pytest.helpers.register
-    def bar():
-        return True
-
-    assert "bar" in pytest.helpers
-    assert pytest.helpers.bar() is True
+    assert result.ret != 0

More details

Full run details