Repository: magmax/python-inquirer

Issue:
Fix coverage showup
- Coverage is not being updated since 2019.
- Coverage does not take into account if test code is being executed.

Discussion:

Patch:
diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index e7d592e..0000000
--- a/.coveragerc
+++ /dev/null
@@ -1,2 +0,0 @@
-[run]
-omit = */tests*
diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt
new file mode 100644
index 0000000..6198f84
--- /dev/null
+++ b/.github/workflows/constraints.txt
@@ -0,0 +1,5 @@
+pip==21.3.1
+nox==2021.10.1
+nox-poetry==0.9.0
+poetry==1.1.12
+virtualenv==20.10.0
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6d39325..2068064 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,30 +1,73 @@
-# This workflow will upload a Python Package using Twine when a release is created
-# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
+---
 name: Release
 
 on:
-  release:
-    types: [created]
+  push:
+    branches:
+      - main
+      - master
 
 jobs:
-  deploy:
-
+  release:
+    name: Release
     runs-on: ubuntu-latest
-
     steps:
-    - uses: actions/checkout@v2
-    - name: Set up Python
-      uses: actions/setup-python@v2
-      with:
-        python-version: '3.x'
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install setuptools wheel twine
-    - name: Build and publish
-      env:
-        TWINE_USERNAME: __token__
-        TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
-      run: |
-        python setup.py sdist bdist_wheel
-        twine upload dist/*
+      - name: Check out the repository
+        uses: actions/checkout@v2.4.0
+        with:
+          fetch-depth: 2
+
+      - name: Set up Python
+        uses: actions/setup-python@v2.3.1
+        with:
+          python-version: "3.10"
+
+      - name: Upgrade pip
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt pip
+          pip --version
+
+      - name: Install Poetry
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt poetry
+          poetry --version
+
+      - name: Check if there is a parent commit
+        id: check-parent-commit
+        run: |
+          echo "::set-output name=sha::$(git rev-parse --verify --quiet HEAD^)"
+
+      - name: Detect and tag new version
+        id: check-version
+        if: steps.check-parent-commit.outputs.sha
+        uses: salsify/action-detect-and-tag-new-version@v2.0.1
+        with:
+          version-command: |
+            bash -o pipefail -c "poetry version | awk '{ print \$2 }'"
+
+      - name: Bump version for developmental release
+        if: "! steps.check-version.outputs.tag"
+        run: |
+          poetry version patch &&
+          version=$(poetry version | awk '{ print $2 }') &&
+          poetry version $version.dev.$(date +%s)
+
+      - name: Build package
+        run: |
+          poetry build --ansi
+
+
+      - name: Publish package on PyPI
+        if: steps.check-version.outputs.tag
+        uses: pypa/gh-action-pypi-publish@v1.4.2
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_API_TOKEN }}
+
+      - name: Publish the release notes
+        uses: release-drafter/release-drafter@v5.15.0
+        with:
+          publish: ${{ steps.check-version.outputs.tag != '' }}
+          tag: ${{ steps.check-version.outputs.tag }}
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 380478e..ff63456 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,14 +1,11 @@
 *~
-\#*
-\.\#*
 *.pyc
 *.egg-info/
 venv/
-.coverage*
-coverage.xml
-dist/
-build/
-runtests.py
+/.coverage
+/.coverage.*
+/.nox/
+/dist/
 .cache
 *.swp
-.ropeproject/
+__pycache__/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f32216e..b5db20f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,6 +16,11 @@ repos:
     rev: 6de8252c035844f1e679f509b5f37340b44d5c39
     hooks:
     - id: flake8
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.1.0
+    hooks:
+    - id: check-yaml
+    - id: trailing-whitespace
   - repo: https://github.com/asottile/pyupgrade
     rev: v2.29.1
     hooks:
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 67a5dc8..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,9 +0,0 @@
-include changes.rst
-include LICENSE
-include Makefile
-include README.rst
-include requirements.txt
-include setup.cfg
-
-recursive-include inquirer/ *
-recursive-include tests/ *
diff --git a/README.rst b/README.rst
index cde106d..8e604be 100644
--- a/README.rst
+++ b/README.rst
@@ -1,5 +1,5 @@
 ====================  =================================================================================
-Tests                 |Tests| |coveralls|
+Tests                 |Tests| |Codecov|
 --------------------  ---------------------------------------------------------------------------------
 Downloads             |pip dm| |pip dw| |pip dd|
 --------------------  ---------------------------------------------------------------------------------
@@ -28,7 +28,7 @@ Installation
 ============
 
 .. code:: console
-    
+
     pip install inquirer
 
 
@@ -46,7 +46,7 @@ Text
 .. code:: python
 
   import re
-  
+
   import inquirer
   questions = [
     inquirer.Text('name', message="What's your name"),
@@ -156,9 +156,9 @@ Licensed under `the MIT license`_.
 .. |Tests| image:: https://github.com/magmax/python-inquirer/workflows/Tests/badge.svg
   :target: https://github.com/magmax/python-inquirer/actions?workflow=Tests
   :alt: Tests
-.. |coveralls| image:: https://coveralls.io/repos/magmax/python-inquirer/badge.png
-  :target: `Coveralls`_
-  :alt: Coveralls results_
+.. |Codecov| image:: https://codecov.io/gh/magmax/python-inquirer/branch/master/graph/badge.svg
+  :target: https://app.codecov.io/gh/magmax/python-inquirer
+  :alt: Codecov
 .. |pip version| image:: https://img.shields.io/pypi/v/inquirer.svg
     :target: https://pypi.python.org/pypi/inquirer
     :alt: Latest PyPI version
@@ -201,7 +201,6 @@ Licensed under `the MIT license`_.
   :alt: Example of Checkbox Question
 
 .. _Inquirer.js: https://github.com/SBoudrias/Inquirer.js
-.. _Coveralls: https://coveralls.io/r/magmax/python-inquirer
 .. _examples/: https://github.com/magmax/python-inquirer/tree/master/examples
 .. _`download the python-inquirer code from GitHub`: https://github.com/magmax/python-inquirer
 .. _`download the wheel from Pypi`: https://pypi.python.org/pypi/inquirer
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..4f6dd27
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,10 @@
+
+comment: false
+coverage:
+  status:
+    project:
+      default:
+        target: "90"
+    patch:
+      default:
+        target: "90"
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 0000000..1719504
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,217 @@
+"""Nox sessions."""
+import os
+import shlex
+import shutil
+import sys
+from pathlib import Path
+from textwrap import dedent
+
+import nox
+
+
+try:
+    from nox_poetry import Session
+    from nox_poetry import session
+except ImportError:
+    message = f"""\
+    Nox failed to import the 'nox-poetry' package.
+
+    Please install it using the following command:
+
+    {sys.executable} -m pip install nox-poetry"""
+    raise SystemExit(dedent(message)) from None
+
+
+package = "inquirer"
+python_versions = ["3.10", "3.9", "3.8", "3.7"]
+nox.needs_version = ">= 2021.6.6"
+nox.options.sessions = (
+    "pre-commit",
+    "tests",
+)
+
+
+def activate_virtualenv_in_precommit_hooks(session: Session) -> None:
+    """Activate virtualenv in hooks installed by pre-commit.
+
+    This function patches git hooks installed by pre-commit to activate the
+    session's virtual environment. This allows pre-commit to locate hooks in
+    that environment when invoked from git.
+
+    Args:
+        session: The Session object.
+    """
+    assert session.bin is not None  # noqa: S101
+
+    # Only patch hooks containing a reference to this session's bindir. Support
+    # quoting rules for Python and bash, but strip the outermost quotes so we
+    # can detect paths within the bindir, like <bindir>/python.
+    bindirs = [
+        bindir[1:-1] if bindir[0] in "'\"" else bindir for bindir in (repr(session.bin), shlex.quote(session.bin))
+    ]
+
+    virtualenv = session.env.get("VIRTUAL_ENV")
+    if virtualenv is None:
+        return
+
+    headers = {
+        # pre-commit < 2.16.0
+        "python": f"""\
+            import os
+            os.environ["VIRTUAL_ENV"] = {virtualenv!r}
+            os.environ["PATH"] = os.pathsep.join((
+                {session.bin!r},
+                os.environ.get("PATH", ""),
+            ))
+            """,
+        # pre-commit >= 2.16.0
+        "bash": f"""\
+            VIRTUAL_ENV={shlex.quote(virtualenv)}
+            PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH"
+            """,
+    }
+
+    hookdir = Path(".git") / "hooks"
+    if not hookdir.is_dir():
+        return
+
+    for hook in hookdir.iterdir():
+        if hook.name.endswith(".sample") or not hook.is_file():
+            continue
+
+        if not hook.read_bytes().startswith(b"#!"):
+            continue
+
+        text = hook.read_text()
+
+        if not any(Path("A") == Path("a") and bindir.lower() in text.lower() or bindir in text for bindir in bindirs):
+            continue
+
+        lines = text.splitlines()
+
+        for executable, header in headers.items():
+            if executable in lines[0].lower():
+                lines.insert(1, dedent(header))
+                hook.write_text("\n".join(lines))
+                break
+
+
+@session(name="pre-commit", python=python_versions[0])
+def precommit(session: Session) -> None:
+    """Lint using pre-commit."""
+    args = session.posargs or ["run", "--all-files", "--show-diff-on-failure"]
+    session.install(
+        "black",
+        "darglint",
+        "flake8",
+        "flake8-bandit",
+        "flake8-bugbear",
+        "flake8-docstrings",
+        "flake8-rst-docstrings",
+        "isort",
+        "pep8-naming",
+        "pre-commit",
+        "pre-commit-hooks",
+        "pyupgrade",
+    )
+    session.run("pre-commit", *args)
+    if args and args[0] == "install":
+        activate_virtualenv_in_precommit_hooks(session)
+
+
+@session(python=python_versions[0])
+def safety(session: Session) -> None:
+    """Scan dependencies for insecure packages."""
+    requirements = session.poetry.export_requirements()
+    session.install("safety")
+    session.run("safety", "check", "--full-report", f"--file={requirements}")
+
+
+@session(python=python_versions)
+def mypy(session: Session) -> None:
+    """Type-check using mypy."""
+    args = session.posargs or ["src", "tests", "docs/conf.py"]
+    session.install(".")
+    session.install("mypy", "pytest")
+    session.run("mypy", *args)
+    if not session.posargs:
+        session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py")
+
+
+@session(python=python_versions)
+def tests(session: Session) -> None:
+    """Run the test suite."""
+    session.install(".")
+    session.install("coverage[toml]", "pexpect", "pytest", "pygments")
+    try:
+        session.run("coverage", "run", "--parallel", "-m", "pytest", *session.posargs)
+    finally:
+        if session.interactive:
+            session.notify("coverage", posargs=[])
+
+
+@session(python=python_versions[0])
+def coverage(session: Session) -> None:
+    """Produce the coverage report."""
+    args = session.posargs or ["report"]
+
+    session.install("coverage[toml]")
+
+    if not session.posargs and any(Path().glob(".coverage.*")):
+        session.run("coverage", "combine")
+
+    session.run("coverage", *args)
+
+
+@session(python=python_versions)
+def typeguard(session: Session) -> None:
+    """Runtime type checking using Typeguard."""
+    session.install(".")
+    session.install("pytest", "typeguard", "pygments")
+    session.run("pytest", f"--typeguard-packages={package}", *session.posargs)
+
+
+@session(python=python_versions)
+def xdoctest(session: Session) -> None:
+    """Run examples with xdoctest."""
+    if session.posargs:
+        args = [package, *session.posargs]
+    else:
+        args = [f"--modname={package}", "--command=all"]
+        if "FORCE_COLOR" in os.environ:
+            args.append("--colored=1")
+
+    session.install(".")
+    session.install("xdoctest[colors]")
+    session.run("python", "-m", "xdoctest", *args)
+
+
+@session(name="docs-build", python=python_versions[0])
+def docs_build(session: Session) -> None:
+    """Build the documentation."""
+    args = session.posargs or ["docs", "docs/_build"]
+    if not session.posargs and "FORCE_COLOR" in os.environ:
+        args.insert(0, "--color")
+
+    session.install(".")
+    session.install("sphinx", "sphinx-click", "furo")
+
+    build_dir = Path("docs", "_build")
+    if build_dir.exists():
+        shutil.rmtree(build_dir)
+
+    session.run("sphinx-build", *args)
+
+
+@session(python=python_versions[0])
+def docs(session: Session) -> None:
+    """Build and serve the documentation with live reloading on file changes."""
+    args = session.posargs or ["--open-browser", "docs", "docs/_build"]
+    session.install(".")
+    session.install("sphinx", "sphinx-autobuild", "sphinx-click", "furo")
+
+    build_dir = Path("docs", "_build")
+    if build_dir.exists():
+        shutil.rmtree(build_dir)
+
+    session.run("sphinx-autobuild", *args)
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..a8af246
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,128 @@
+[[package]]
+name = "ansicon"
+version = "1.89.0"
+description = "Python wrapper for loading Jason Hood's ANSICON"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "blessed"
+version = "1.19.0"
+description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities."
+category = "main"
+optional = false
+python-versions = ">=2.7"
+
+[package.dependencies]
+jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""}
+six = ">=1.9.0"
+wcwidth = ">=0.1.4"
+
+[[package]]
+name = "jinxed"
+version = "1.1.0"
+description = "Jinxed Terminal Library"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+ansicon = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "pexpect"
+version = "4.8.0"
+description = "Pexpect allows easy control of interactive console applications."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+ptyprocess = ">=0.5"
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+description = "Run a subprocess in a pseudo terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "python-editor"
+version = "1.0.4"
+description = "Programmatically open an editor, capture the result."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "readchar"
+version = "3.0.4"
+description = "Utilities to read single characters and key-strokes"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "wcwidth"
+version = "0.2.5"
+description = "Measures the displayed width of unicode strings in a terminal"
+category = "main"
+optional = false
+python-versions = "*"
+
+[metadata]
+lock-version = "1.1"
+python-versions = ">=3.7"
+content-hash = "2b644ac763c76fcccda8b7a64b51abad9c3f3858c00d5a8d7579763e71c5ac51"
+
+[metadata.files]
+ansicon = [
+    {file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"},
+    {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"},
+]
+blessed = [
+    {file = "blessed-1.19.0-py2.py3-none-any.whl", hash = "sha256:1f2d462631b2b6d2d4c3c65b54ef79ad87a6ca2dd55255df2f8d739fcc8a1ddb"},
+    {file = "blessed-1.19.0.tar.gz", hash = "sha256:4db0f94e5761aea330b528e84a250027ffe996b5a94bf03e502600c9a5ad7a61"},
+]
+jinxed = [
+    {file = "jinxed-1.1.0-py2.py3-none-any.whl", hash = "sha256:6a61ccf963c16aa885304f27e6e5693783676897cea0c7f223270c8b8e78baf8"},
+    {file = "jinxed-1.1.0.tar.gz", hash = "sha256:d8f1731f134e9e6b04d95095845ae6c10eb15cb223a5f0cabdea87d4a279c305"},
+]
+pexpect = [
+    {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
+    {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
+]
+ptyprocess = [
+    {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
+    {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
+]
+python-editor = [
+    {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"},
+    {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"},
+    {file = "python_editor-1.0.4-py2.7.egg", hash = "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"},
+    {file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"},
+    {file = "python_editor-1.0.4-py3.5.egg", hash = "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77"},
+]
+readchar = [
+    {file = "readchar-3.0.4-py3-none-any.whl", hash = "sha256:ebe7b51edae808101ea54b63468e8bea87e0c246301a0100f7777219eb2eb844"},
+    {file = "readchar-3.0.4.tar.gz", hash = "sha256:3ce642ade5b61efee273b3c4bf55c77a84398178842627c74fcb796e1666ab13"},
+]
+six = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+wcwidth = [
+    {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
+    {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..94116e4
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,44 @@
+[tool.poetry]
+name = "inquirer"
+version = "3.0.0"
+description = "Collection of common interactive command line user interfaces, based on Inquirer.js"
+authors = ["Miguel Ángel García <miguelangel.garcia@gmail.com>"]
+license = "MIT"
+readme = "README.rst"
+homepage = "https://github.com/magmax/python-inquirer"
+repository = "https://github.com/magmax/python-inquirer"
+documentation = "https://github.com/magmax/python-inquirer"
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Environment :: Console",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: OS Independent",
+    "Topic :: Software Development :: User Interfaces",
+    "Topic :: Software Development :: Libraries :: Application Frameworks",
+]
+
+[tool.poetry.dependencies]
+python = ">=3.7"
+blessed = ">=1.19.0"
+readchar = ">=2.0.1"
+python-editor = ">=1.0.4"
+
+[tool.poetry.dev-dependencies]
+pexpect = ">=4.8.0"
+
+[tool.coverage.paths]
+source = ["src", "*/site-packages"]
+tests = ["tests", "*/tests"]
+
+[tool.coverage.run]
+branch = true
+source = ["inquirer", "tests"]
+
+[tool.coverage.report]
+show_missing = true
+fail_under = 90
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index c9fd125..0000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,12 +0,0 @@
--r requirements.txt
-
-flake8==3.8.4
-pexpect==4.8.0
-
-pytest==6.2.5
-pytest-cov==2.12.1
-mock==4.0.3
-
-python-coveralls==2.9.3
-
-wheel >= 0.36.2
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 3694158..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-blessed==1.19.0
-readchar==2.0.1
-python-editor==1.0.4
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index f25c4c0..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[tool:pytest]
-norecursedirs = .git venv build dist
-
-[bdist_wheel]
-universal=1
diff --git a/setup.py b/setup.py
deleted file mode 100644
index b92e90f..0000000
--- a/setup.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import sys
-import os
-from setuptools import setup, find_packages
-from setuptools.command.test import test as TestCommand
-
-version = "3.0.0"
-github_ref = os.getenv("GITHUB_REF")
-if github_ref and github_ref.startswith("refs/tags"):
-    version = github_ref[10:]
-
-
-def read_description():
-    try:
-        with open("README.rst", encoding="utf8") as fd:
-            return fd.read()
-    except TypeError:
-        with open("README.rst") as fd:
-            return fd.read()
-
-
-def read_requirements():
-    try:
-        with open("requirements.txt", encoding="utf8") as fd:
-            return fd.read()
-    except TypeError:
-        with open("requirements.txt") as fd:
-            return fd.read()
-
-
-class PyTest(TestCommand):
-    user_options = [
-        ("pytest-args=", "a", "Arguments to pass to py.test"),
-    ]
-
-    def initialize_options(self):
-        TestCommand.initialize_options(self)
-        self.pytest_args = []
-
-    def finalize_options(self):
-        TestCommand.finalize_options(self)
-        self.test_args = []
-        self.test_suite = True
-
-    def run_tests(self):
-        # import here, cause outside the eggs aren't loaded
-        import pytest
-
-        errno = pytest.main(self.pytest_args or ["--cov-report=term-missing"])
-        sys.exit(errno)
-
-
-setup(
-    name="inquirer",
-    version=version,
-    description=("Collection of common interactive command line user interfaces," " based on Inquirer.js"),
-    long_description=read_description(),
-    cmdclass={"test": PyTest},
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Environment :: Console",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-        "Topic :: Software Development :: User Interfaces",
-        "Topic :: Software Development :: " "Libraries :: Application Frameworks",
-    ],
-    keywords="color terminal",
-    author="Miguel Ángel García",
-    author_email="miguelangel.garcia@gmail.com",
-    url="https://github.com/magmax/python-inquirer",
-    license="MIT",
-    packages=find_packages(exclude=["tests"]),
-    include_package_data=True,
-    zip_safe=False,
-    install_requires=read_requirements().splitlines(),
-)
diff --git a/inquirer/__init__.py b/src/inquirer/__init__.py
similarity index 73%
rename from inquirer/__init__.py
rename to src/inquirer/__init__.py
index 3155243..ccc14e0 100644
--- a/inquirer/__init__.py
+++ b/src/inquirer/__init__.py
@@ -1,5 +1,5 @@
-from .prompt import prompt
-from .questions import (
+from inquirer.prompt import prompt
+from inquirer.questions import (
     Text,
     Editor,
     Password,
@@ -11,7 +11,7 @@ from .questions import (
     load_from_json,
     load_from_list,
 )
-from .shortcuts import text, editor, password, confirm, list_input, checkbox
+from inquirer.shortcuts import text, editor, password, confirm, list_input, checkbox
 
 __all__ = [
     "prompt",


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d0368aa..7abac91 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,38 +1,137 @@
-# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
-# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+---
 name: Tests
 
 on:
-  push:
-    branches: [ master ]
-  pull_request:
-    branches: [ master ]
+  - push
+  - pull_request
 
 jobs:
-  build:
-
+  tests:
+    name: ${{ matrix.session }} ${{ matrix.python }} / ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
     strategy:
+      fail-fast: false
       matrix:
-        platform: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ["3.7", "3.8", "3.9", "3.10"]
-    runs-on: ${{ matrix.platform }}
+        include:
+          - { python: "3.10", os: "ubuntu-latest", session: "pre-commit" }
+          - { python: "3.10", os: "ubuntu-latest", session: "tests" }
+          - { python: "3.9", os: "ubuntu-latest", session: "tests" }
+          - { python: "3.8", os: "ubuntu-latest", session: "tests" }
+          - { python: "3.7", os: "ubuntu-latest", session: "tests" }
+          - { python: "3.10", os: "windows-latest", session: "tests" }
+          - { python: "3.10", os: "macos-latest", session: "tests" }
+
+    env:
+      NOXSESSION: ${{ matrix.session }}
+      FORCE_COLOR: "1"
+      PRE_COMMIT_COLOR: "always"
 
     steps:
-    - uses: actions/checkout@v2.4.0
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2.3.1
-      with:
-        python-version: ${{ matrix.python-version }}
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        python -m pip install -r requirements-dev.txt
-    - name: Lint with flake8
-      run: |
-        # stop the build if there are Python syntax errors or undefined names
-        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
-        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
-        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
-    - name: Test with pytest
-      run: |
-        pytest
+      - name: Check out the repository
+        uses: actions/checkout@v2.4.0
+
+      - name: Set up Python ${{ matrix.python }}
+        uses: actions/setup-python@v2.3.1
+        with:
+          python-version: ${{ matrix.python }}
+
+      - name: Upgrade pip
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt pip
+          pip --version
+
+      - name: Upgrade pip in virtual environments
+        shell: python
+        run: |
+          import os
+          import pip
+          with open(os.environ["GITHUB_ENV"], mode="a") as io:
+              print(f"VIRTUALENV_PIP={pip.__version__}", file=io)
+
+      - name: Install Poetry
+        run: |
+          pipx install --pip-args=--constraint=.github/workflows/constraints.txt poetry
+          poetry --version
+
+      - name: Install Nox
+        run: |
+          pipx install --pip-args=--constraint=.github/workflows/constraints.txt nox
+          pipx inject --pip-args=--constraint=.github/workflows/constraints.txt nox nox-poetry
+          nox --version
+
+      - name: Compute pre-commit cache key
+        if: matrix.session == 'pre-commit'
+        id: pre-commit-cache
+        shell: python
+        run: |
+          import hashlib
+          import sys
+          python = "py{}.{}".format(*sys.version_info[:2])
+          payload = sys.version.encode() + sys.executable.encode()
+          digest = hashlib.sha256(payload).hexdigest()
+          result = "${{ runner.os }}-{}-{}-pre-commit".format(python, digest[:8])
+          print("::set-output name=result::{}".format(result))
+
+      - name: Restore pre-commit cache
+        uses: actions/cache@v2.1.7
+        if: matrix.session == 'pre-commit'
+        with:
+          path: ~/.cache/pre-commit
+          key: ${{ steps.pre-commit-cache.outputs.result }}-${{ hashFiles('.pre-commit-config.yaml') }}
+          restore-keys: |
+            ${{ steps.pre-commit-cache.outputs.result }}-
+
+      - name: Run Nox
+        run: |
+          nox --force-color --python=${{ matrix.python }}
+
+      - name: Upload coverage data
+        if: always() && matrix.session == 'tests'
+        uses: "actions/upload-artifact@v2.3.1"
+        with:
+          name: coverage-data
+          path: ".coverage.*"
+
+  coverage:
+    runs-on: ubuntu-latest
+    needs: tests
+    steps:
+      - name: Check out the repository
+        uses: actions/checkout@v2.4.0
+
+      - name: Set up Python
+        uses: actions/setup-python@v2.3.1
+        with:
+          python-version: "3.10"
+
+      - name: Upgrade pip
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt pip
+          pip --version
+
+      - name: Install Poetry
+        run: |
+          pipx install --pip-args=--constraint=.github/workflows/constraints.txt poetry
+          poetry --version
+
+      - name: Install Nox
+        run: |
+          pipx install --pip-args=--constraint=.github/workflows/constraints.txt nox
+          pipx inject --pip-args=--constraint=.github/workflows/constraints.txt nox nox-poetry
+          nox --version
+
+      - name: Download coverage data
+        uses: actions/download-artifact@v2.1.0
+        with:
+          name: coverage-data
+
+      - name: Combine coverage data and display human readable report
+        run: |
+          nox --force-color --session=coverage
+
+      - name: Create coverage report
+        run: |
+          nox --force-color --session=coverage -- xml
+
+      - name: Upload coverage report
+        uses: codecov/codecov-action@v2.1.0
diff --git a/inquirer/errors.py b/src/inquirer/errors.py
similarity index 100%
rename from inquirer/errors.py
rename to src/inquirer/errors.py
diff --git a/inquirer/events.py b/src/inquirer/events.py
similarity index 100%
rename from inquirer/events.py
rename to src/inquirer/events.py
diff --git a/inquirer/prompt.py b/src/inquirer/prompt.py
similarity index 100%
rename from inquirer/prompt.py
rename to src/inquirer/prompt.py
diff --git a/inquirer/questions.py b/src/inquirer/questions.py
similarity index 100%
rename from inquirer/questions.py
rename to src/inquirer/questions.py
diff --git a/inquirer/render/__init__.py b/src/inquirer/render/__init__.py
similarity index 100%
rename from inquirer/render/__init__.py
rename to src/inquirer/render/__init__.py
diff --git a/inquirer/render/console/__init__.py b/src/inquirer/render/console/__init__.py
similarity index 100%
rename from inquirer/render/console/__init__.py
rename to src/inquirer/render/console/__init__.py
diff --git a/inquirer/render/console/_checkbox.py b/src/inquirer/render/console/_checkbox.py
similarity index 100%
rename from inquirer/render/console/_checkbox.py
rename to src/inquirer/render/console/_checkbox.py
diff --git a/inquirer/render/console/_confirm.py b/src/inquirer/render/console/_confirm.py
similarity index 100%
rename from inquirer/render/console/_confirm.py
rename to src/inquirer/render/console/_confirm.py
diff --git a/inquirer/render/console/_editor.py b/src/inquirer/render/console/_editor.py
similarity index 100%
rename from inquirer/render/console/_editor.py
rename to src/inquirer/render/console/_editor.py
diff --git a/inquirer/render/console/_list.py b/src/inquirer/render/console/_list.py
similarity index 100%
rename from inquirer/render/console/_list.py
rename to src/inquirer/render/console/_list.py
diff --git a/inquirer/render/console/_password.py b/src/inquirer/render/console/_password.py
similarity index 100%
rename from inquirer/render/console/_password.py
rename to src/inquirer/render/console/_password.py
diff --git a/inquirer/render/console/_path.py b/src/inquirer/render/console/_path.py
similarity index 100%
rename from inquirer/render/console/_path.py
rename to src/inquirer/render/console/_path.py
diff --git a/inquirer/render/console/_text.py b/src/inquirer/render/console/_text.py
similarity index 100%
rename from inquirer/render/console/_text.py
rename to src/inquirer/render/console/_text.py
diff --git a/inquirer/render/console/base.py b/src/inquirer/render/console/base.py
similarity index 100%
rename from inquirer/render/console/base.py
rename to src/inquirer/render/console/base.py
diff --git a/inquirer/shortcuts.py b/src/inquirer/shortcuts.py
similarity index 100%
rename from inquirer/shortcuts.py
rename to src/inquirer/shortcuts.py
diff --git a/inquirer/themes.py b/src/inquirer/themes.py
similarity index 100%
rename from inquirer/themes.py
rename to src/inquirer/themes.py
diff --git a/tests/unit/test_question.py b/tests/unit/test_question.py
index 9a2a2db..476d2af 100644
--- a/tests/unit/test_question.py
+++ b/tests/unit/test_question.py
@@ -260,14 +260,13 @@ class TestPathQuestion(unittest.TestCase):
 
         do_test(None, False)
 
-        if os.environ.get("TRAVIS_PYTHON_VERSION") != "pypy3":
-            # Path component must not be longer then 255 bytes
-            do_test("a" * 256, False)
-            do_test(os.path.abspath("/asdf/" + "a" * 256), False)
-            do_test("{}/{}".format("a" * 255, "b" * 255), True)
-
-            # Path component must not contains null bytes
-            do_test("some/path/with/{}byte".format(b"\x00".decode("utf-8")), False)
+        # Path component must not be longer then 255 bytes
+        do_test("a" * 256, False)
+        do_test(os.path.abspath("/asdf/" + "a" * 256), False)
+        do_test("{}/{}".format("a" * 255, "b" * 255), True)
+
+        # Path component must not contains null bytes
+        do_test("some/path/with/{}byte".format(b"\x00".decode("utf-8")), False)
 
     def test_path_type_validation_no_existence_check(self):
         def do_test(path_type, path, result=True):