Repository: sarugaku/requirementslib (language: python)

Issue:
AST parser fails on Python 3.9
These tests fail with Python 3.9.2 while they pass with 3.8.8:

```
[   32s] ============================= test session starts ==============================
[   32s] platform linux -- Python 3.9.2, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
[   32s] rootdir: /home/abuild/rpmbuild/BUILD/requirementslib-1.5.16, configfile: setup.cfg
[   32s] plugins: hypothesis-6.8.1, timeout-1.4.2
[   32s] timeout: 300.0s
[   32s] timeout method: signal
[   32s] timeout func_only: False
[   32s] collected 189 items / 20 deselected / 169 selected
[   32s] 
[   32s] tests/unit/test_exceptions.py ......                                     [  3%]
[   34s] tests/unit/test_lockfile.py ...                                          [  5%]
[   34s] tests/unit/test_markers.py .........................................     [ 29%]
[   42s] tests/unit/test_metadata.py ..                                           [ 30%]
[   42s] tests/unit/test_pipfile.py ........                                      [ 35%]
[   42s] tests/unit/test_project.py ....................                          [ 47%]
[   46s] tests/unit/test_requirements.py ........................................ [ 71%]
[   46s] .....                                                                    [ 73%]
[   46s] tests/unit/test_setup_info.py .......F...F..                             [ 82%]
[   49s] tests/unit/test_uri.py ..                                                [ 83%]
[   49s] tests/unit/test_utils.py ............................                    [100%]
[   49s] 
[   49s] =================================== FAILURES ===================================
[   49s] _________________ test_ast_parser_handles_repeated_assignments _________________
[   49s] 
[   49s] setup_py_dir = PosixPath('/home/abuild/rpmbuild/BUILD/requirementslib-1.5.16/tests/fixtures/setup_py')
[   49s] 
[   49s]     def test_ast_parser_handles_repeated_assignments(setup_py_dir):
[   49s]         target = setup_py_dir.joinpath(
[   49s]             "package_with_repeated_assignments/setup.py"
[   49s]         ).as_posix()
[   49s]         parsed = ast_parse_setup_py(target)
[   49s]         analyzer = ast_parse_file(target)
[   49s]         assert parsed["name"] == "test_package_with_repeated_assignments"
[   49s] >       assert isinstance(parsed["version"], str) is False
[   49s] E       AssertionError: assert True is False
[   49s] E        +  where True = isinstance('os.environ', str)
[   49s] 
[   49s] tests/unit/test_setup_info.py:269: AssertionError
```

https://github.com/sarugaku/requirementslib/blob/644198471362e5e7be7a32d2a9fdadd538391aef/tests/fixtures/setup_py/package_with_repeated_assignments/setup.py#L6

```
[   49s] _ test_ast_parser_handles_dependency_on_env_vars[env_vars1-expected_install_requires1] _
[   49s] 
[   49s] env_vars = {'READTHEDOCS': '1'}
[   49s] expected_install_requires = ['sphinx', 'sphinx-argparse']
[   49s] setup_py_dir = PosixPath('/home/abuild/rpmbuild/BUILD/requirementslib-1.5.16/tests/fixtures/setup_py')
[   49s] 
[   49s]     @pytest.mark.parametrize(
[   49s]         "env_vars, expected_install_requires",
[   49s]         [
[   49s]             ({"NOTHING": "1"}, []),
[   49s]             ({"READTHEDOCS": "1"}, ["sphinx", "sphinx-argparse"]),
[   49s]         ],
[   49s]     )
[   49s]     def test_ast_parser_handles_dependency_on_env_vars(
[   49s]         env_vars, expected_install_requires, setup_py_dir
[   49s]     ):
[   49s]         @contextlib.contextmanager
[   49s]         def modified_environ(**update):
[   49s]             env = os.environ
[   49s]             try:
[   49s]                 env.update(update)
[   49s]                 yield
[   49s]             finally:
[   49s]                 [env.pop(k) for k in update]
[   49s]     
[   49s]         with modified_environ(**env_vars):
[   49s]             parsed = ast_parse_setup_py(
[   49s]                 setup_py_dir.joinpath(
[   49s]                     "package_with_dependence_on_env_vars/setup.py"
[   49s]                 ).as_posix()
[   49s]             )
[   49s] >           assert list(sorted(parsed["install_requires"])) == list(
[   49s]                 sorted(expected_install_requires)
[   49s]             )
[   49s] E           AssertionError: assert ['docs'] == ['sphinx', 'sphinx-argparse']
[   49s] E             At index 0 diff: 'docs' != 'sphinx'
[   49s] E             Right contains one more item: 'sphinx-argparse'
[   49s] E             Use -v to get the full diff
[   49s] 
[   49s] tests/unit/test_setup_info.py:327: AssertionError
```

https://github.com/sarugaku/requirementslib/blob/644198471362e5e7be7a32d2a9fdadd538391aef/tests/fixtures/setup_py/package_with_dependence_on_env_vars/setup.py#L14








Discussion / hints:

Fix patch:
diff --git a/.azure-pipelines/templates/build-package.yml b/.azure-pipelines/templates/build-package.yml
deleted file mode 100644
index c03ca1a6..00000000
--- a/.azure-pipelines/templates/build-package.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-steps:
-  - task: UsePythonVersion@0
-    inputs:
-      versionSpec: "3.7"
-    displayName: Use Python 3.7
-
-  - script: python -m pip install pip wheel pipenv --upgrade
-    displayName: Install Pipenv
-
-  - script: pipenv install --dev
-    displayName: Create Development Environment
-
-  - script: |
-      pipenv run inv clean
-      pipenv run inv build
-    displayName: Build Package
-
-  - script: |
-      pipenv run pip install twine readme-renderer[md]
-      pipenv run twine check dist/*
-    displayName: check package build
-
-  - task: CopyFiles@2
-    inputs:
-      sourceFolder: dist/
-      contents: "**"
-      targetFolder: $(Build.ArtifactStagingDirectory)
-    displayName: Copy Artifacts
-
-  - task: PublishBuildArtifacts@1
-    inputs:
-      pathToPublish: $(Build.ArtifactStagingDirectory)
-      artifactName: PackageArtifacts
-    displayName: Publish Artifacts
diff --git a/.azure-pipelines/templates/install-dependencies.yml b/.azure-pipelines/templates/install-dependencies.yml
deleted file mode 100644
index 7307f023..00000000
--- a/.azure-pipelines/templates/install-dependencies.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-steps:
-  - script: |
-      python -m pip install --upgrade pip setuptools wheel pipenv
-      pipenv --python $(python.version) install --dev
-    displayName: Install Dependencies
diff --git a/.azure-pipelines/templates/run-coverage.yml b/.azure-pipelines/templates/run-coverage.yml
deleted file mode 100644
index ded74772..00000000
--- a/.azure-pipelines/templates/run-coverage.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-steps:
-  - script: pipenv run pip install codacy-coverage codecov
-    displayName: 'Install Coverage Reporters'
-
-  - script: |
-        pipenv run coverage combine
-        pipenv run coverage xml -o coverage/coverage.xml
-        pipenv run coverage html -d coverage
-    displayName: 'Build Coverage Report'
-  - script: |
-      pipenv run python-codacy-coverage -r coverage/coverage.xml
-      pipenv run codecov --file coverage/coverage.xml
-    env:
-      CODACY_PROJECT_TOKEN: $(CODACY_PROJECT_TOKEN)
-      CODECOV_TOKEN: $(CODECOV_TOKEN)
-    displayName: 'Send Coverage Report'
-  - task: PublishCodeCoverageResults@1
-    displayName: 'Publish code coverage from coverage.xml'
-    inputs:
-      codeCoverageTool: Cobertura
-      summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml'
-      reportDirectory: '$(System.DefaultWorkingDirectory)/**/coverage'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1503f7cc..6601201d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,32 +29,44 @@ on:
       - ".editorconfig"
 
 jobs:
+  pre-commit:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+      - uses: pre-commit/action@v2.0.3
   test:
     name: ${{matrix.os}} / ${{ matrix.python-version }}
     runs-on: ${{ matrix.os }}-latest
     strategy:
       fail-fast: false
       matrix:
-        python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
-        os: [MacOS, Ubuntu, Windows]
+        python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]
+        os: [macos, ubuntu, windows]
 
     steps:
-      - uses: actions/checkout@v1
+      - uses: actions/checkout@v2
 
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v1
+        uses: actions/setup-python@v2
         with:
           python-version: ${{ matrix.python-version }}
 
-      - name: Get python path
-        id: python-path
+      - name: Get pip cache dir
+        id: pip-cache
         run: |
-          echo ::set-output name=path::$(python -c "import sys; print(sys.executable)")
+          echo "::set-output name=dir::$(pip cache dir)"
 
-      - name: Install latest pip, setuptools, wheel
-        run: |
-          python -m pip install --upgrade coveralls pip setuptools wheel --upgrade-strategy=eager
-          python -m pip install --upgrade --pre pipenv
+      - name: pip cache
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.pip-cache.outputs.dir }}
+          key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}
+          restore-keys: |
+            ${{ runner.os }}-pip-
+
+      - name: Install Dev Dependencies
+        run: python -m pip install --upgrade .[dev]
 
       - name: Run tests
         env:
@@ -67,12 +79,8 @@ jobs:
           COVERALLS_SERVICE_NAME: github
           COVERALLS_SERVICE_JOB_ID: ${{ github.run_id }}
           COVERALLS_PARALLEL: true
-          PIPENV_NOSPIN: "1"
           CI: "1"
         run: |
           git submodule sync
           git submodule update --init --recursive
-          python -m pipenv install --dev --python=${{ steps.python-path.outputs.path }}
-          pipenv run coverage run -p -m pytest -ra tests/
-          pipenv run coverage combine
-          coveralls
+          nox -s coverage
diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml
index e1f5b25b..d0625087 100644
--- a/.github/workflows/pypi_upload.yml
+++ b/.github/workflows/pypi_upload.yml
@@ -4,7 +4,7 @@ on:
   push:
     # Sequence of patterns matched against refs/tags
     tags:
-      - v?[0-9]+.[0-9]+.[0-9]+  # add .* to allow dev releases
+      - v?[0-9]+.[0-9]+.[0-9]+ # add .* to allow dev releases
 
 jobs:
   build:
@@ -14,50 +14,49 @@ jobs:
       CI: "1"
 
     steps:
-    - name: Checkout code
-      uses: actions/checkout@v1
+      - name: Checkout code
+        uses: actions/checkout@v2
 
-    - uses: webfactory/ssh-agent@v0.1.1
-      with:
-        ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
+      - name: Create Release
+        id: create_release
+        uses: actions/create-release@v1.0.0
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          tag_name: ${{ github.ref }}
+          release_name: Release ${{ github.ref }}
+          draft: false
+          prerelease: false
 
-    - name: Create Release
-      id: create_release
-      uses: actions/create-release@v1.0.0
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      with:
-        tag_name: ${{ github.ref }}
-        release_name: Release ${{ github.ref }}
-        draft: false
-        prerelease: false
+      - name: Set up Python 3.9
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.9
 
-    - name: Set up Python 3.7
-      uses: actions/setup-python@v1
-      with:
-        python-version: 3.7
+      - name: Install nox
+        run: |
+          python -m pip install --upgrade .[dev]
+      - name: Build wheels
+        run: |
+          nox -s package
+      # to upload to test pypi, pass repository_url: https://test.pypi.org/legacy/ and use secrets.TEST_PYPI_TOKEN
+      - name: Publish a Python distribution to PyPI
+        uses: pypa/gh-action-pypi-publish@master
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_TOKEN }}
+          packages_dir: dist/
+      # git push https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git HEAD:master
+      # we need to use a deploy key for this to get around branch protection as the default token fails
+      - name: Pre-bump
+        run: |
+          git config --local user.name 'Github Action'
+          git config --local user.email action@github.com
+          nox -s bump_version
+          git commit -am 'pre-bump'
 
-    - name: Install latest tools for build
-      run: |
-        python -m pip install --upgrade pip setuptools wheel pipenv invoke
-        python -m pipenv install --dev --pre
-        python -m pipenv run pip install invoke
-    - name: Build wheels
-      run: |
-        python -m pipenv run inv build
-    # to upload to test pypi, pass repository_url: https://test.pypi.org/legacy/ and use secrets.TEST_PYPI_TOKEN
-    - name: Publish a Python distribution to PyPI
-      uses: pypa/gh-action-pypi-publish@master
-      with:
-        user: __token__
-        password: ${{ secrets.PYPI_TOKEN }}
-        packages_dir: dist/
-    # git push https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git HEAD:master
-    # we need to use a deploy key for this to get around branch protection as the default token fails
-    - name: Pre-bump
-      run: |
-        git config --local user.name 'Github Action'
-        git config --local user.email action@github.com
-        python -m pipenv run inv bump-version -t dev
-        git commit -am 'pre-bump'
-        git push git@github.com:${{ github.repository }}.git HEAD:master
+      - name: Push changes
+        uses: ad-m/github-push-action@master
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          branch: main
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 698f3dae..32b871d6 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,27 +1,23 @@
 repos:
   - repo: https://github.com/psf/black
-    rev: 19.10b0
+    rev: 21.8b0
     hooks:
       - id: black
-        language_version: python3.6
-  #- repo: https://github.com/pre-commit/pre-commit-hooks
-    #rev: v2.0.0
-    #hooks:
-      #- id: flake8
 
-  - repo: https://github.com/asottile/seed-isort-config
-    rev: v2.1.1
+  - repo: https://github.com/PyCQA/flake8
+    rev: 3.9.2
     hooks:
-      - id: seed-isort-config
-        args: [
-          --application-directories=src/requirementslib,
-          --settings-path=./,
-        ]
+      - id: flake8
+        additional_dependencies:
+          - flake8-bugbear
 
-  - repo: https://github.com/timothycrosley/isort
-    rev: 4.3.21
+  - repo: https://github.com/PyCQA/isort
+    rev: 5.9.3
     hooks:
       - id: isort
+        args:
+          - --settings-file
+          - pyproject.toml
 
   - repo: https://github.com/myint/docformatter
     rev: v1.3.1
diff --git a/LICENSE b/LICENSE
index a6b8c96a..21aaa475 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright 2019 Dan Ryan.
+Copyright 2019-2021 Dan Ryan and Frost Ming.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index c67810ef..00000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,89 +0,0 @@
-# Python package
-# Create and test a Python package on multiple Python versions.
-# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
-# https://docs.microsoft.com/azure/devops/pipelines/languages/python
-
-trigger:
-  branches:
-    include:
-      - master
-  paths:
-    exclude:
-      - "appveyor.yml"
-      - "*.md"
-      - "*.rst"
-      - "*.txt"
-      - ".github/*"
-      - "LICENSE"
-      - "news/*"
-      - "docs/*"
-
-jobs:
-  - job: TestLinux
-    pool:
-      vmImage: ubuntu-latest
-    strategy:
-      matrix:
-        Python27:
-          python.version: "2.7"
-        Python35:
-          python.version: "3.5"
-        Python36:
-          python.version: "3.6"
-        Python37:
-          python.version: "3.7"
-        Python38:
-          python.version: "3.8"
-      maxParallel: 4
-    variables:
-      - group: CI
-    steps:
-      - template: .azure-pipelines/templates/run-tests.yml
-
-  - job: TestWindows
-    pool:
-      vmImage: windows-latest
-    strategy:
-      matrix:
-        Python27:
-          python.version: "2.7"
-        Python35:
-          python.version: "3.5"
-        Python36:
-          python.version: "3.6"
-        Python37:
-          python.version: "3.7"
-        Python38:
-          python.version: "3.8"
-      maxParallel: 4
-    steps:
-      - template: .azure-pipelines/templates/run-tests.yml
-
-  - job: TestMacOS
-    pool:
-      vmImage: macOS-latest
-    strategy:
-      matrix:
-        Python27:
-          python.version: "2.7"
-        Python35:
-          python.version: "3.5"
-        Python36:
-          python.version: "3.6"
-        Python37:
-          python.version: "3.7"
-        Python38:
-          python.version: "3.8"
-      maxParallel: 4
-    steps:
-      - template: .azure-pipelines/templates/run-tests.yml
-
-  - job: BuildPackage
-    dependsOn:
-      - TestLinux
-      - TestWindows
-      - TestMacOS
-    pool:
-      vmImage: ubuntu-latest
-    steps:
-      - template: .azure-pipelines/templates/build-package.yml
diff --git a/docs/conf.py b/docs/conf.py
index 8b3f91e3..6925fc1c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -8,13 +8,15 @@
 
 # -- Path setup --------------------------------------------------------------
 
+import codecs
+
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 #
 import os
 import re
-import codecs
+
 # import sys
 # sys.path.insert(0, os.path.abspath('.'))
 
@@ -24,32 +26,30 @@
 def read(*parts):
     # intentionally *not* adding an encoding option to open, See:
     #   https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
-    with codecs.open(os.path.join(root, *parts), 'r') as fp:
+    with codecs.open(os.path.join(root, *parts), "r") as fp:
         return fp.read()
 
 
 def find_version(*file_paths):
     version_file = read(*file_paths)
-    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
-                              version_file, re.M)
+    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
     if version_match:
         return version_match.group(1)
-    return '0.0.0'
+    return "0.0.0"
 
 
-release = find_version('src', 'requirementslib', '__init__.py')
-version = '.'.join(release.split('.')[:2])
+release = find_version("src", "requirementslib", "__init__.py")
+version = ".".join(release.split(".")[:2])
 
 # This is for debugging in case the version is wrong
 print(version)
 
-today_fmt = '%B %d, %Y'
+today_fmt = "%B %d, %Y"
 # -- Project information -----------------------------------------------------
 
-project = 'RequirementsLib'
-copyright = '2018, Dan Ryan <dan@danryan.co>'
-author = 'Dan Ryan <dan@danryan.co>'
-
+project = "RequirementsLib"
+copyright = "2018, Dan Ryan <dan@danryan.co>"
+author = "Dan Ryan <dan@danryan.co>"
 
 
 # -- General configuration ---------------------------------------------------
@@ -62,69 +62,66 @@ def find_version(*file_paths):
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    'sphinx.ext.autodoc',
-    'sphinx.ext.viewcode',
-    'sphinx.ext.todo',
-    'sphinx.ext.intersphinx',
-    'sphinx.ext.autosummary'
+    "sphinx.ext.autodoc",
+    "sphinx.ext.viewcode",
+    "sphinx.ext.todo",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.autosummary",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix(es) of source filenames.
 # You can specify multiple suffix as a list of string:
 #
 # source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
 #
 # This is also used if you do content translation via gettext catalogs.
 # Usually you set "language" from the command line for these cases.
-language = 'en'
+language = "en"
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path .
-exclude_patterns = ['_build', '_man', 'Thumbs.db', '.DS_Store']
+exclude_patterns = ["_build", "_man", "Thumbs.db", ".DS_Store"]
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
-html_theme = 'sphinx_rtd_theme'
+pygments_style = "sphinx"
+html_theme = "sphinx_rtd_theme"
 autosummary_generate = True
 
 extlinks = {
-    'issue': ('https://github.com/sarugaku/requirementslib/issues/%s', '#'),
-    'pull': ('https://github.com/sarugaku/requirementslib/pull/%s', 'PR #'),
+    "issue": ("https://github.com/sarugaku/requirementslib/issues/%s", "#"),
+    "pull": ("https://github.com/sarugaku/requirementslib/pull/%s", "PR #"),
 }
 html_theme_options = {
-    'display_version': True,
-    'prev_next_buttons_location': 'bottom',
-    'style_external_links': True,
-    'vcs_pageview_mode': '',
+    "display_version": True,
+    "prev_next_buttons_location": "bottom",
+    "style_external_links": True,
+    "vcs_pageview_mode": "",
     # Toc options
-    'collapse_navigation': True,
-    'sticky_navigation': True,
-    'navigation_depth': 4,
-    'includehidden': True,
-    'titles_only': False
+    "collapse_navigation": True,
+    "sticky_navigation": True,
+    "navigation_depth": 4,
+    "includehidden": True,
+    "titles_only": False,
 }
 
-html_last_updated_fmt = '%b %d, %Y'
+html_last_updated_fmt = "%b %d, %Y"
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
 smart_quotes = False
 
-html_sidebars = {
-    '**': ['localtoc.html', 'relations.html'],
-    'index': ['localtoc.html']
-}
+html_sidebars = {"**": ["localtoc.html", "relations.html"], "index": ["localtoc.html"]}
 # -- Options for HTML output -------------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
@@ -140,7 +137,7 @@ def find_version(*file_paths):
 # 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,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
 
 # Custom sidebar templates, must be a dictionary that maps document names
 # to template names.
@@ -156,7 +153,7 @@ def find_version(*file_paths):
 # -- Options for HTMLHelp output ---------------------------------------------
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'RequirementsLibdoc'
+htmlhelp_basename = "RequirementsLibdoc"
 
 
 # -- Options for LaTeX output ------------------------------------------------
@@ -165,15 +162,12 @@ def find_version(*file_paths):
     # The paper size ('letterpaper' or 'a4paper').
     #
     # 'papersize': 'letterpaper',
-
     # The font size ('10pt', '11pt' or '12pt').
     #
     # 'pointsize': '10pt',
-
     # Additional stuff for the LaTeX preamble.
     #
     # 'preamble': '',
-
     # Latex figure (float) alignment
     #
     # 'figure_align': 'htbp',
@@ -183,8 +177,13 @@ def find_version(*file_paths):
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-    (master_doc, 'RequirementsLib.tex', 'RequirementsLib Documentation',
-     'Dan Ryan \\textless{}dan@danryan.co\\textgreater{}', 'manual'),
+    (
+        master_doc,
+        "RequirementsLib.tex",
+        "RequirementsLib Documentation",
+        "Dan Ryan \\textless{}dan@danryan.co\\textgreater{}",
+        "manual",
+    ),
 ]
 
 
@@ -193,8 +192,7 @@ def find_version(*file_paths):
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    (master_doc, 'requirementslib', 'RequirementsLib Documentation',
-     [author], 1)
+    (master_doc, "requirementslib", "RequirementsLib Documentation", [author], 1)
 ]
 
 
@@ -204,11 +202,17 @@ def find_version(*file_paths):
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-    (master_doc, 'RequirementsLib', 'RequirementsLib Documentation',
-     author, 'RequirementsLib', 'One line description of project.',
-     'Miscellaneous'),
+    (
+        master_doc,
+        "RequirementsLib",
+        "RequirementsLib Documentation",
+        author,
+        "RequirementsLib",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
 ]
 
 
 # -- Extension configuration -------------------------------------------------
-intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
+intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
diff --git a/news/288.bugfix.rst b/news/288.bugfix.rst
new file mode 100644
index 00000000..4e8e4356
--- /dev/null
+++ b/news/288.bugfix.rst
@@ -0,0 +1,1 @@
+Fix an AST parse error on Python 3.9+.
diff --git a/news/296.bugfix.rst b/news/296.bugfix.rst
new file mode 100644
index 00000000..03e783a5
--- /dev/null
+++ b/news/296.bugfix.rst
@@ -0,0 +1,1 @@
+Replace ``chardet`` with ``charset_normalizer``.
diff --git a/news/301.removal.rst b/news/301.removal.rst
new file mode 100644
index 00000000..b882f035
--- /dev/null
+++ b/news/301.removal.rst
@@ -0,0 +1,1 @@
+Officially drop support for Python 2.7 and 3.5.
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 00000000..3675119d
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,65 @@
+import subprocess
+from pathlib import Path
+
+import nox
+import parver
+
+BASE_PATH = Path(__file__).resolve().parent
+PACKAGE_ROOT = BASE_PATH / "src/requirementslib"
+INIT_PY = PACKAGE_ROOT / "__init__.py"
+
+
+@nox.session
+def tests(session: nox.Session):
+    session.install("-e", ".[tests]")
+    session.run("pytest", "-ra", "tests")
+
+
+@nox.session
+def coverage(session: nox.Session):
+    session.install(".[tests]", "coveralls")
+    session.run("pytest", "--cov=requirementslib", "-ra", "tests")
+    session.run("coveralls")
+
+
+@nox.session
+def docs(session: nox.Session):
+    session.install(".[docs]")
+    session.run("sphinx-build", "-b", "html", "docs", "docs/build/html")
+
+
+@nox.session
+def package(session: nox.Session):
+    session.install("build", "twine")
+    session.run("pyproject-build")
+    session.run("twine", "check", "dist/*")
+
+
+def _current_version() -> parver.Version:
+    cmd = ["git", "describe", "--tags", "--abbrev=0"]
+    ver = subprocess.check_output(cmd).decode("utf-8").strip()
+    return parver.Version.parse(ver)
+
+
+def _prebump(version: parver.Version) -> parver.Version:
+    next_version = version.bump_release(index=2).bump_dev()
+    print(f"[bump] {version} -> {next_version}")
+    return next_version
+
+
+def _write_version(v):
+    lines = []
+    with INIT_PY.open() as f:
+        for line in f:
+            if line.startswith("__version__ = "):
+                line = f"__version__ = {repr(str(v))}\n".replace("'", '"')
+            lines.append(line)
+    with INIT_PY.open("w", newline="\n") as f:
+        f.write("".join(lines))
+
+
+@nox.session
+def bump_version(session: nox.Session):
+    new_version = _prebump(_current_version())
+    _write_version(new_version)
+    return new_version
diff --git a/pyproject.toml b/pyproject.toml
index dfe805c2..06326dbd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,6 @@ requires = ["setuptools>=40.8.0", "wheel>=0.33.0"]
 
 [tool.black]
 line-length = 90
-target-version = ['py27', 'py38']
 include = '\.pyi?$'
 exclude = '''
 /(
@@ -27,7 +26,6 @@ package = "requirementslib"
 package_dir = "src"
 filename = "CHANGELOG.rst"
 directory = "news/"
-title_format = "{version} ({project_date})"
 template = "tasks/CHANGELOG.rst.jinja2"
 issue_format = '`#{issue} <https://github.com/sarugaku/requirementslib/issues/{issue}>`_'
 
@@ -60,3 +58,41 @@ issue_format = '`#{issue} <https://github.com/sarugaku/requirementslib/issues/{i
   directory = "removal"
   name = "Removals and Deprecations"
   showcontent = true
+
+[tool.isort]
+src_paths = ["src", "tests"]
+skip_glob = ["tests/artifacts/*", "tests/fixtures/*"]
+profile = "black"
+atomic = true
+line_length = 90
+filter_files = true
+known_third_party = [
+  "attr",
+  "cached_property",
+  "chardet",
+  "dateutil",
+  "distlib",
+  "environ",
+  "hypothesis",
+  "invoke",
+  "orderedmultidict",
+  "packaging",
+  "parver",
+  "pep517",
+  "pip_shims",
+  "platformdirs",
+  "plette",
+  "pyparsing",
+  "pytest",
+  "requests",
+  "setuptools",
+  "six",
+  "tomlkit",
+  "towncrier",
+  "urllib3",
+  "vistir"
+]
+known_first_party = [
+  "requirementslib",
+  "tests"
+]
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 6ffa9dca..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-sphinx
--e .[dev,tests,typing]
-sphinx-rtd-theme
-ptpython
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index cb0ee7b3..9a3ee030 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,6 +4,8 @@ description = A tool for converting between pip-style and pipfile requirements.
 url = https://github.com/sarugaku/requirementslib
 author = Dan Ryan
 author_email = dan@danryan.co
+maintainer = Frost Ming
+maintainer_email = mianghong@gmail.com
 long_description = file: README.rst
 long_description_content_type = text/x-rst
 license = MIT
@@ -23,13 +25,12 @@ classifier =
     Intended Audience :: Developers
     License :: OSI Approved :: MIT License
     Programming Language :: Python
-    Programming Language :: Python :: 2
-    Programming Language :: Python :: 2.7
     Programming Language :: Python :: 3
-    Programming Language :: Python :: 3.5
     Programming Language :: Python :: 3.6
     Programming Language :: Python :: 3.7
     Programming Language :: Python :: 3.8
+    Programming Language :: Python :: 3.9
+    Programming Language :: Python :: 3.10
     Programming Language :: Python :: Implementation :: CPython
     Programming Language :: Python :: Implementation :: PyPy
     Topic :: Software Development :: Build Tools
@@ -38,79 +39,48 @@ classifier =
 
 [options]
 zip_safe = true
-python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*
-setup_requires =
-    invoke
-    vistir
-    setuptools>=40.8
-    wheel
-    parver
+python_requires = >=3.6
 install_requires =
     attrs>=19.2
     cached_property
-    contextlib2;python_version=="2.7"
     distlib>=0.2.8
     orderedmultidict
     packaging>=19.0
     pep517>=0.5.0
-    pip-shims>=0.5.2
+    pip-shims>=0.6.0
     platformdirs
     plette[validation]
     python-dateutil
     requests
-    scandir;python_version<"3.5"
     setuptools>=40.8
-    six>=1.11.0
     tomlkit>=0.5.3
     vistir>=0.3.1
 
 [options.extras_require]
-dev =
-    black;python_version>="3.6"
-    vulture
-    flake8
-    flake8-bugbear;python_version>="3.5"
-    rope
-    isort
-    invoke
-    twine
-    pre-commit
-    lxml
-    towncrier
-    parver
 tests =
-    mock
     pytest
-    twine
     readme-renderer[md]
     pytest-xdist
     pytest-cov
     pytest-timeout
     coverage
     hypothesis
-typing =
-    typing;python_version<"3.5"
-    mypy;python_version>="3.4"
-    mypy-extensions;python_version>="3.4"
-    mypytools;python_version>="3.4"
-    monkeytype;python_version>="3.6"
-    pytype;python_version>="3.4"
-    typed-ast;python_version>="3.4"
-
+docs =
+    sphinx
+    sphinx_rtd_theme
+dev =
+    nox
+    twine
+    parver
+    towncrier
 
 [bdist_wheel]
 universal = 1
 
 [tool:pytest]
-strict = true
-plugins = flake8 timeout
 addopts = -ra --timeout 300
 testpaths = tests/
 norecursedirs = .* build dist news tasks docs tests/artifacts
-flake8-ignore =
-    docs/source/* ALL
-    tests/*.py ALL
-    setup.py ALL
 filterwarnings =
     ignore::DeprecationWarning
     ignore::PendingDeprecationWarning
@@ -122,25 +92,13 @@ markers =
     to_line
     serial
 
-[isort]
-atomic = true
-not_skip = __init__.py
-line_length = 90
-multi_line_output = 3
-known_third_party = attr,cached_property,chardet,dateutil,distlib,environ,hypothesis,invoke,orderedmultidict,packaging,parver,pep517,pip_shims,platformdirs,plette,pyparsing,pytest,requests,setuptools,six,tomlkit,towncrier,urllib3,vistir
-known_first_party = requirementslib,tests
-combine_as_imports=True
-include_trailing_comma = True
-use_parentheses=True
-force_grid_wrap=0
-
 [flake8]
 max-line-length = 90
 select = C,E,F,W,B,B950
 #select = E,W,F
 ignore =
     # The default ignore list:
-    D203,F401,E123,E203,W503,E501,E402
+    D203,F401,E123,E203,W503,E501,E402,E731,C901,B950
     #E121,E123,E126,E226,E24,E704,
     # Our additions:
     # E123: closing bracket does not match indentation of opening bracket’s line
@@ -193,7 +151,7 @@ output = .typeshed
 pythonpath =
     ./src/requirementslib:
 # Python version (major.minor) of the target code.
-python_version = 2.7
+python_version = 3.6
 # Comma separated list of error names to ignore.
 disable =
     pyi-error
@@ -211,5 +169,4 @@ requires = ["setuptools>=40.8.0", "wheel>=0.33.0"]
 ignore_missing_imports=true
 follow_imports=skip
 html_report=mypyhtml
-python_version=2.7
 show_error_context=true
diff --git a/setup.py b/setup.py
index ce633658..253d8ff8 100644
--- a/setup.py
+++ b/setup.py
@@ -1,12 +1,7 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-import codecs
 import os
 import re
-import sys
-
-from setuptools import setup, find_packages, Command
 
+from setuptools import find_packages, setup
 
 here = os.path.abspath(os.path.dirname(__file__))
 
@@ -14,7 +9,7 @@
 def read(*parts):
     # intentionally *not* adding an encoding option to open, See:
     #   https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
-    with codecs.open(os.path.join(here, *parts), "r") as fp:
+    with open(os.path.join(here, *parts), encoding="utf-8") as fp:
         return fp.read()
 
 
@@ -26,49 +21,13 @@ def find_version(*file_paths):
     raise RuntimeError("Unable to find version string.")
 
 
-if sys.argv[-1] == "publish":
-    os.system("python setup.py sdist bdist_wheel upload")
-    sys.exit()
-
-
-class UploadCommand(Command):
-    """Support setup.py publish."""
-
-    description = "Build and publish the package."
-    user_options = []
-
-    @staticmethod
-    def status(s):
-        """Prints things in bold."""
-        print("\033[1m{0}\033[0m".format(s))
-
-    def initialize_options(self):
-        pass
-
-    def finalize_options(self):
-        pass
-
-    def run(self):
-        # self.status('Building Source distribution…')
-        # os.system('{0} setup.py sdist'.format(sys.executable))
-        self.status("Uploading the package to PyPi via Twine...")
-        os.system("twine upload dist/*")
-        self.status("Pushing git tags…")
-        os.system(
-            "git tag v{0}".format(find_version("src", "requirementslib", "__init__.py"))
-        )
-        os.system("git push origin --tags")
-        sys.exit()
-
-
 setup(
     name="requirementslib",
     version=find_version("src", "requirementslib", "__init__.py"),
-    package_dir={'': 'src'},
+    package_dir={"": "src"},
     packages=find_packages(where="src", exclude=["docs*", "tests*", "tasks*"]),
-
     # I don't know how to specify an empty key in setup.cfg.
     package_data={
-        '': ['LICENSE*', 'README*'],
+        "": ["LICENSE*", "README*"],
     },
 )
diff --git a/src/requirementslib/__init__.py b/src/requirementslib/__init__.py
index 4ce96b38..9643c43d 100644
--- a/src/requirementslib/__init__.py
+++ b/src/requirementslib/__init__.py
@@ -4,8 +4,6 @@
 import logging
 import warnings
 
-from vistir.compat import ResourceWarning
-
 from .models.lockfile import Lockfile
 from .models.pipfile import Pipfile
 from .models.requirements import Requirement
diff --git a/src/requirementslib/environment.py b/src/requirementslib/environment.py
index 3dd25837..69d9d513 100644
--- a/src/requirementslib/environment.py
+++ b/src/requirementslib/environment.py
@@ -1,7 +1,8 @@
 # -*- coding=utf-8 -*-
-from __future__ import print_function, absolute_import
+from __future__ import absolute_import, print_function
 
 import os
+
 from platformdirs import user_cache_dir
 
 
@@ -13,5 +14,7 @@ def is_type_checking():
     return TYPE_CHECKING
 
 
-REQUIREMENTSLIB_CACHE_DIR = os.getenv("REQUIREMENTSLIB_CACHE_DIR", user_cache_dir("pipenv"))
+REQUIREMENTSLIB_CACHE_DIR = os.getenv(
+    "REQUIREMENTSLIB_CACHE_DIR", user_cache_dir("pipenv")
+)
 MYPY_RUNNING = os.environ.get("MYPY_RUNNING", is_type_checking())
diff --git a/src/requirementslib/exceptions.py b/src/requirementslib/exceptions.py
index d11dbce9..eacef437 100644
--- a/src/requirementslib/exceptions.py
+++ b/src/requirementslib/exceptions.py
@@ -5,20 +5,6 @@
 import os
 import sys
 
-import six
-from vistir.compat import FileNotFoundError
-
-if six.PY2:
-
-    class FileExistsError(OSError):
-        def __init__(self, *args, **kwargs):
-            self.errno = errno.EEXIST
-            super(FileExistsError, self).__init__(*args, **kwargs)
-
-
-else:
-    from six.moves.builtins import FileExistsError
-
 
 class RequirementError(Exception):
     pass
@@ -44,7 +30,7 @@ def __init__(self, path, *args, **kwargs):
         if not backup_path and args:
             args = reversed(args)
             backup_path = args.pop()
-            if not isinstance(backup_path, six.string_types) or not os.path.exists(
+            if not isinstance(backup_path, str) or not os.path.exists(
                 os.path.abspath(os.path.dirname(backup_path))
             ):
                 args.append(backup_path)
diff --git a/src/requirementslib/models/cache.py b/src/requirementslib/models/cache.py
index 28065c71..6da15d57 100644
--- a/src/requirementslib/models/cache.py
+++ b/src/requirementslib/models/cache.py
@@ -6,18 +6,15 @@
 import hashlib
 import json
 import os
+import pathlib
 import sys
 
 import vistir
-
-from platformdirs import user_cache_dir
-from pip_shims.shims import FAVORITE_HASH, SafeFileCache
 from packaging.requirements import Requirement
+from pip_shims.shims import FAVORITE_HASH, SafeFileCache
+from platformdirs import user_cache_dir
 
-from .utils import as_tuple, key_from_req, lookup_table, get_pinned_version
-
-from ..exceptions import FileExistsError
-
+from .utils import as_tuple, get_pinned_version, key_from_req, lookup_table
 
 CACHE_DIR = os.environ.get("PIPENV_CACHE_DIR", user_cache_dir("pipenv"))
 
@@ -29,64 +26,64 @@ def __init__(self, path):
 
     def __str__(self):
         lines = [
-            'The dependency cache seems to have been corrupted.',
-            'Inspect, or delete, the following file:',
-            '  {}'.format(self.path),
+            "The dependency cache seems to have been corrupted.",
+            "Inspect, or delete, the following file:",
+            "  {}".format(self.path),
         ]
         return os.linesep.join(lines)
 
 
 def read_cache_file(cache_file_path):
-    with open(cache_file_path, 'r') as cache_file:
+    with open(cache_file_path, "r") as cache_file:
         try:
             doc = json.load(cache_file)
         except ValueError:
             raise CorruptCacheError(cache_file_path)
 
         # Check version and load the contents
-        assert doc['__format__'] == 1, 'Unknown cache file format'
-        return doc['dependencies']
+        assert doc["__format__"] == 1, "Unknown cache file format"
+        return doc["dependencies"]
 
 
 class DependencyCache(object):
-    """
-    Creates a new persistent dependency cache for the current Python version.
-    The cache file is written to the appropriate user cache dir for the
-    current platform, i.e.
+    """Creates a new persistent dependency cache for the current Python
+    version. The cache file is written to the appropriate user cache dir for
+    the current platform, i.e.
 
         ~/.cache/pip-tools/depcache-pyX.Y.json
 
     Where X.Y indicates the Python version.
     """
+
     def __init__(self, cache_dir=None):
         if cache_dir is None:
             cache_dir = CACHE_DIR
-        if not vistir.compat.Path(CACHE_DIR).absolute().is_dir():
+        if not pathlib.Path(CACHE_DIR).absolute().is_dir():
             try:
                 vistir.path.mkdir_p(os.path.abspath(cache_dir))
-            except (FileExistsError, OSError):
+            except OSError:
                 pass
 
-        py_version = '.'.join(str(digit) for digit in sys.version_info[:2])
-        cache_filename = 'depcache-py{}.json'.format(py_version)
+        py_version = ".".join(str(digit) for digit in sys.version_info[:2])
+        cache_filename = "depcache-py{}.json".format(py_version)
 
         self._cache_file = os.path.join(cache_dir, cache_filename)
         self._cache = None
 
     @property
     def cache(self):
-        """
-        The dictionary that is the actual in-memory cache.  This property
-        lazily loads the cache from disk.
+        """The dictionary that is the actual in-memory cache.
+
+        This property lazily loads the cache from disk.
         """
         if self._cache is None:
             self.read_cache()
         return self._cache
 
     def as_cache_key(self, ireq):
-        """
-        Given a requirement, return its cache key. This behavior is a little weird in order to allow backwards
-        compatibility with cache files. For a requirement without extras, this will return, for example:
+        """Given a requirement, return its cache key. This behavior is a little
+        weird in order to allow backwards compatibility with cache files. For a
+        requirement without extras, this will return, for example:
 
         ("ipython", "2.1.0")
 
@@ -112,10 +109,10 @@ def read_cache(self):
     def write_cache(self):
         """Writes the cache to disk as JSON."""
         doc = {
-            '__format__': 1,
-            'dependencies': self._cache,
+            "__format__": 1,
+            "dependencies": self._cache,
         }
-        with open(self._cache_file, 'w') as f:
+        with open(self._cache_file, "w") as f:
             json.dump(doc, f, sort_keys=True)
 
     def clear(self):
@@ -149,20 +146,20 @@ def get(self, ireq, default=None):
         return self.cache.get(pkgname, {}).get(pkgversion_and_extras, default)
 
     def reverse_dependencies(self, ireqs):
-        """
-        Returns a lookup table of reverse dependencies for all the given ireqs.
+        """Returns a lookup table of reverse dependencies for all the given
+        ireqs.
 
         Since this is all static, it only works if the dependency cache
-        contains the complete data, otherwise you end up with a partial view.
-        This is typically no problem if you use this function after the entire
-        dependency tree is resolved.
+        contains the complete data, otherwise you end up with a partial
+        view. This is typically no problem if you use this function
+        after the entire dependency tree is resolved.
         """
         ireqs_as_cache_values = [self.as_cache_key(ireq) for ireq in ireqs]
         return self._reverse_dependencies(ireqs_as_cache_values)
 
     def _reverse_dependencies(self, cache_keys):
-        """
-        Returns a lookup table of reverse dependencies for all the given cache keys.
+        """Returns a lookup table of reverse dependencies for all the given
+        cache keys.
 
         Example input:
 
@@ -177,35 +174,39 @@ def _reverse_dependencies(self, cache_keys):
              'flake8': [],
              'mccabe': ['flake8'],
              'pyflakes': ['flake8']}
-
         """
         # First, collect all the dependencies into a sequence of (parent, child) tuples, like [('flake8', 'pep8'),
         # ('flake8', 'mccabe'), ...]
-        return lookup_table((key_from_req(Requirement(dep_name)), name)
-                            for name, version_and_extras in cache_keys
-                            for dep_name in self.cache[name][version_and_extras])
+        return lookup_table(
+            (key_from_req(Requirement(dep_name)), name)
+            for name, version_and_extras in cache_keys
+            for dep_name in self.cache[name][version_and_extras]
+        )
 
 
 class HashCache(SafeFileCache):
     """Caches hashes of PyPI artifacts so we do not need to re-download them.
 
-    Hashes are only cached when the URL appears to contain a hash in it and the
-    cache key includes the hash value returned from the server). This ought to
-    avoid ssues where the location on the server changes.
+    Hashes are only cached when the URL appears to contain a hash in it
+    and the cache key includes the hash value returned from the server).
+    This ought to avoid ssues where the location on the server changes.
     """
+
     def __init__(self, *args, **kwargs):
         session = kwargs.pop("session", None)
         if not session:
             import requests
+
             session = requests.session()
             atexit.register(session.close)
-        cache_dir = kwargs.pop('cache_dir', CACHE_DIR)
+        cache_dir = kwargs.pop("cache_dir", CACHE_DIR)
         self.session = session
-        kwargs.setdefault('directory', os.path.join(cache_dir, 'hash-cache'))
+        kwargs.setdefault("directory", os.path.join(cache_dir, "hash-cache"))
         super(HashCache, self).__init__(*args, **kwargs)
 
     def get_hash(self, location):
         from pip_shims import VcsSupport
+
         # if there is no location hash (i.e., md5 / sha256 / etc) we on't want to store it
         hash_value = None
         vcs = VcsSupport()
@@ -216,13 +217,17 @@ def get_hash(self, location):
         can_hash = new_location.hash
         if can_hash:
             # hash url WITH fragment
-            hash_value = self._get_file_hash(new_location.url) if not new_location.url.startswith("ssh") else None
+            hash_value = (
+                self._get_file_hash(new_location.url)
+                if not new_location.url.startswith("ssh")
+                else None
+            )
         if not hash_value:
             hash_value = self._get_file_hash(new_location)
-            hash_value = hash_value.encode('utf8')
+            hash_value = hash_value.encode("utf8")
         if can_hash:
             self.set(new_location.url, hash_value)
-        return hash_value.decode('utf8')
+        return hash_value.decode("utf8")
 
     def _get_file_hash(self, location):
         h = hashlib.new(FAVORITE_HASH)
@@ -242,6 +247,7 @@ class _JSONCache(object):
 
     Where X.Y indicates the Python version.
     """
+
     filename_format = None
 
     def __init__(self, cache_dir=CACHE_DIR):
@@ -287,21 +293,19 @@ def as_cache_key(self, ireq):
         return name, "{}{}".format(version, extras_string)
 
     def read_cache(self):
-        """Reads the cached contents into memory.
-        """
+        """Reads the cached contents into memory."""
         if os.path.exists(self._cache_file):
             self._cache = read_cache_file(self._cache_file)
         else:
             self._cache = {}
 
     def write_cache(self):
-        """Writes the cache to disk as JSON.
-        """
+        """Writes the cache to disk as JSON."""
         doc = {
-            '__format__': 1,
-            'dependencies': self._cache,
+            "__format__": 1,
+            "dependencies": self._cache,
         }
-        with open(self._cache_file, 'w') as f:
+        with open(self._cache_file, "w") as f:
             json.dump(doc, f, sort_keys=True)
 
     def clear(self):
@@ -336,6 +340,6 @@ def get(self, ireq, default=None):
 
 
 class RequiresPythonCache(_JSONCache):
-    """Cache a candidate's Requires-Python information.
-    """
+    """Cache a candidate's Requires-Python information."""
+
     filename_format = "pyreqcache-py{python_version}.json"
diff --git a/src/requirementslib/models/dependencies.py b/src/requirementslib/models/dependencies.py
index 2608479a..21cdd3b6 100644
--- a/src/requirementslib/models/dependencies.py
+++ b/src/requirementslib/models/dependencies.py
@@ -5,6 +5,8 @@
 import copy
 import functools
 import os
+from contextlib import ExitStack
+from json import JSONDecodeError
 
 import attr
 import packaging.markers
@@ -12,7 +14,7 @@
 import pip_shims.shims
 import requests
 from packaging.utils import canonicalize_name
-from vistir.compat import JSONDecodeError, fs_str
+from vistir.compat import fs_str
 from vistir.contextmanagers import cd, temp_environ
 from vistir.path import create_tracked_tempdir
 
@@ -32,32 +34,28 @@
     version_from_ireq,
 )
 
-try:
-    from contextlib import ExitStack
-except ImportError:
-    from contextlib2 import ExitStack
-
 if MYPY_RUNNING:
     from typing import (
         Any,
         Dict,
-        List,
         Generator,
+        List,
         Optional,
-        Union,
+        Set,
+        Text,
         Tuple,
         TypeVar,
-        Text,
-        Set,
+        Union,
     )
+
+    from packaging.markers import Marker
+    from packaging.requirements import Requirement as PackagingRequirement
     from pip_shims.shims import (
-        InstallRequirement,
+        Command,
         InstallationCandidate,
+        InstallRequirement,
         PackageFinder,
-        Command,
     )
-    from packaging.requirements import Requirement as PackagingRequirement
-    from packaging.markers import Marker
 
     TRequirement = TypeVar("TRequirement")
     RequirementType = TypeVar(
@@ -88,8 +86,8 @@ def _get_filtered_versions(ireq, versions, prereleases):
 
 def find_all_matches(finder, ireq, pre=False):
     # type: (PackageFinder, InstallRequirement, bool) -> List[InstallationCandidate]
-    """Find all matching dependencies using the supplied finder and the
-    given ireq.
+    """Find all matching dependencies using the supplied finder and the given
+    ireq.
 
     :param finder: A package finder for discovering matching candidates.
     :type finder: :class:`~pip._internal.index.PackageFinder`
@@ -130,7 +128,8 @@ class AbstractDependency(object):
 
     @property
     def version_set(self):
-        """Return the set of versions for the candidates in this abstract dependency.
+        """Return the set of versions for the candidates in this abstract
+        dependency.
 
         :return: A set of matching versions
         :rtype: set(str)
@@ -141,8 +140,8 @@ def version_set(self):
         return set(packaging.version.parse(version_from_ireq(c)) for c in self.candidates)
 
     def compatible_versions(self, other):
-        """Find compatible version numbers between this abstract
-        dependency and another one.
+        """Find compatible version numbers between this abstract dependency and
+        another one.
 
         :param other: An abstract dependency to compare with.
         :type other: :class:`~requirementslib.models.dependency.AbstractDependency`
@@ -230,8 +229,9 @@ def get_deps(self, candidate):
 
     @classmethod
     def from_requirement(cls, requirement, parent=None):
-        """Creates a new :class:`~requirementslib.models.dependency.AbstractDependency`
-        from a :class:`~requirementslib.models.requirements.Requirement` object.
+        """Creates a new
+        :class:`~requirementslib.models.dependency.AbstractDependency` from a
+        :class:`~requirementslib.models.requirements.Requirement` object.
 
         This class is used to find all candidates matching a given set of specifiers
         and a given requirement.
@@ -358,7 +358,8 @@ def get_dependencies(ireq, sources=None, parent=None):
 
 def get_dependencies_from_wheel_cache(ireq):
     # type: (pip_shims.shims.InstallRequirement) -> Optional[Set[pip_shims.shims.InstallRequirement]]
-    """Retrieves dependencies for the given install requirement from the wheel cache.
+    """Retrieves dependencies for the given install requirement from the wheel
+    cache.
 
     :param ireq: A single InstallRequirement
     :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
@@ -384,7 +385,8 @@ def _marker_contains_extra(ireq):
 
 
 def get_dependencies_from_json(ireq):
-    """Retrieves dependencies for the given install requirement from the json api.
+    """Retrieves dependencies for the given install requirement from the json
+    api.
 
     :param ireq: A single InstallRequirement
     :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
@@ -433,7 +435,8 @@ def gen(ireq):
 
 
 def get_dependencies_from_cache(ireq):
-    """Retrieves dependencies for the given install requirement from the dependency cache.
+    """Retrieves dependencies for the given install requirement from the
+    dependency cache.
 
     :param ireq: A single InstallRequirement
     :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
@@ -474,7 +477,8 @@ def is_python(section):
 
 
 def get_dependencies_from_index(dep, sources=None, pip_options=None, wheel_cache=None):
-    """Retrieves dependencies for the given install requirement from the pip resolver.
+    """Retrieves dependencies for the given install requirement from the pip
+    resolver.
 
     :param dep: A single InstallRequirement
     :type dep: :class:`~pip._internal.req.req_install.InstallRequirement`
@@ -506,8 +510,8 @@ def get_dependencies_from_index(dep, sources=None, pip_options=None, wheel_cache
     return requirements
 
 
-def get_pip_options(args=[], sources=None, pip_command=None):
-    """Build a pip command from a list of sources
+def get_pip_options(args=None, sources=None, pip_command=None):
+    """Build a pip command from a list of sources.
 
     :param args: positional arguments passed through to the pip parser
     :param sources: A list of pipfile-formatted sources, defaults to None
@@ -523,7 +527,7 @@ def get_pip_options(args=[], sources=None, pip_command=None):
     if not sources:
         sources = [{"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True}]
     _ensure_dir(CACHE_DIR)
-    pip_args = args
+    pip_args = args or []
     pip_args = prepare_pip_source_args(sources, pip_args)
     pip_options, _ = pip_command.parser.parse_args(pip_args)
     pip_options.cache_dir = CACHE_DIR
@@ -532,7 +536,7 @@ def get_pip_options(args=[], sources=None, pip_command=None):
 
 def get_finder(sources=None, pip_command=None, pip_options=None):
     # type: (List[Dict[S, Union[S, bool]]], Optional[Command], Any) -> PackageFinder
-    """Get a package finder for looking up candidates to install
+    """Get a package finder for looking up candidates to install.
 
     :param sources: A list of pipfile-formatted sources, defaults to None
     :param sources: list[dict], optional
@@ -567,7 +571,8 @@ def start_resolver(finder=None, session=None, wheel_cache=None):
     :param :class:`~requests.Session` session: A session instance
     :param :class:`~pip._internal.cache.WheelCache` wheel_cache: A pip WheelCache instance
     :return: A 3-tuple of finder, preparer, resolver
-    :rtype: (:class:`~pip._internal.operations.prepare.RequirementPreparer`, :class:`~pip._internal.resolve.Resolver`)
+    :rtype: (:class:`~pip._internal.operations.prepare.RequirementPreparer`,
+             :class:`~pip._internal.resolve.Resolver`)
     """
 
     pip_command = get_pip_command()
diff --git a/src/requirementslib/models/lockfile.py b/src/requirementslib/models/lockfile.py
index 3eabc504..ce41c702 100644
--- a/src/requirementslib/models/lockfile.py
+++ b/src/requirementslib/models/lockfile.py
@@ -4,11 +4,11 @@
 import copy
 import itertools
 import os
+from json import JSONDecodeError
+from pathlib import Path
 
 import attr
 import plette.lockfiles
-import six
-from vistir.compat import FileNotFoundError, JSONDecodeError, Path
 
 from ..exceptions import LockfileCorruptException, MissingParameter, PipfileNotFound
 from ..utils import is_editable, is_vcs, merge_items
@@ -16,11 +16,11 @@
 from .requirements import Requirement
 from .utils import optional_instance_of
 
-DEFAULT_NEWLINES = six.text_type("\n")
+DEFAULT_NEWLINES = "\n"
 
 
 def preferred_newlines(f):
-    if isinstance(f.newlines, six.text_type):
+    if isinstance(f.newlines, str):
         return f.newlines
     return DEFAULT_NEWLINES
 
@@ -36,7 +36,7 @@ class Lockfile(object):
     _dev_requirements = attr.ib(default=attr.Factory(list), type=list)
     projectfile = attr.ib(validator=is_projectfile, type=ProjectFile)
     _lockfile = attr.ib(validator=is_lockfile, type=plette.lockfiles.Lockfile)
-    newlines = attr.ib(default=DEFAULT_NEWLINES, type=six.text_type)
+    newlines = attr.ib(default=DEFAULT_NEWLINES, type=str)
 
     @path.default
     def _get_path(self):
@@ -118,7 +118,8 @@ def get_deps(self, dev=False, only=True):
 
     @classmethod
     def read_projectfile(cls, path):
-        """Read the specified project file and provide an interface for writing/updating.
+        """Read the specified project file and provide an interface for
+        writing/updating.
 
         :param str path: Path to the target file.
         :return: A project file with the model and location for interaction
@@ -261,7 +262,8 @@ def default(self):
         return self._lockfile.default
 
     def get_requirements(self, dev=True, only=False):
-        """Produces a generator which generates requirements from the desired section.
+        """Produces a generator which generates requirements from the desired
+        section.
 
         :param bool dev: Indicates whether to use dev requirements, defaults to False
         :return: Requirements from the relevant the relevant pipfile
@@ -297,7 +299,7 @@ def write(self):
         self.projectfile.write()
 
     def as_requirements(self, include_hashes=False, dev=False):
-        """Returns a list of requirements in pip-style format"""
+        """Returns a list of requirements in pip-style format."""
         lines = []
         section = self.dev_requirements if dev else self.requirements
         for req in section:
diff --git a/src/requirementslib/models/markers.py b/src/requirementslib/models/markers.py
index 8593326b..6149558d 100644
--- a/src/requirementslib/models/markers.py
+++ b/src/requirementslib/models/markers.py
@@ -2,25 +2,22 @@
 import itertools
 import operator
 import re
+from collections.abc import Mapping, Set
+from functools import lru_cache, reduce
 
 import attr
 import distlib.markers
 import packaging.version
-import six
 from packaging.markers import InvalidMarker, Marker
 from packaging.specifiers import Specifier, SpecifierSet
-from vistir.compat import Mapping, Set, lru_cache
 from vistir.misc import dedup
 
 from ..environment import MYPY_RUNNING
 from ..exceptions import RequirementError
 from .utils import filter_none, validate_markers
 
-from six.moves import reduce  # isort:skip
-
-
 if MYPY_RUNNING:
-    from typing import Optional, List, Type, Any, Tuple, Union, AnyStr, Text, Iterator
+    from typing import Any, AnyStr, Iterator, List, Optional, Text, Tuple, Type, Union
 
     STRING_TYPE = Union[str, bytes, Text]
 
@@ -130,7 +127,7 @@ def _tuplize_version(version):
 @lru_cache(maxsize=1024)
 def _format_version(version):
     # type: (Tuple[int, ...]) -> STRING_TYPE
-    if not isinstance(version, six.string_types):
+    if not isinstance(version, str):
         return ".".join(str(i) for i in version)
     return version
 
@@ -212,7 +209,8 @@ def _group_by_op(specs):
 # TODO: rename this to something meaningful
 def normalize_specifier_set(specs):
     # type: (Union[str, SpecifierSet]) -> Optional[Set[Specifier]]
-    """Given a specifier set, a string, or an iterable, normalize the specifiers
+    """Given a specifier set, a string, or an iterable, normalize the
+    specifiers.
 
     .. note:: This function exists largely to deal with ``pyzmq`` which handles
         the ``requires_python`` specifier incorrectly, using ``3.7*`` rather than
@@ -228,7 +226,7 @@ def normalize_specifier_set(specs):
     if isinstance(specs, set):
         return specs
     # when we aren't dealing with a string at all, we can normalize this as usual
-    elif not isinstance(specs, six.string_types):
+    elif not isinstance(specs, str):
         return {_format_pyspec(spec) for spec in specs}
     spec_list = []
     for spec in specs.split(","):
@@ -368,10 +366,11 @@ def _strip_pyversion(elements):
 def _strip_marker_elem(elem_name, elements):
     """Remove the supplied element from the marker.
 
-    This is not a comprehensive implementation, but relies on an important
-    characteristic of metadata generation: The element's operand is always
-    associated with an "and" operator. This means that we can simply remove the
-    operand and the "and" operator associated with it.
+    This is not a comprehensive implementation, but relies on an
+    important characteristic of metadata generation: The element's
+    operand is always associated with an "and" operator. This means that
+    we can simply remove the operand and the "and" operator associated
+    with it.
     """
 
     extra_indexes = []
@@ -425,8 +424,8 @@ def get_without_extra(marker):
 def get_without_pyversion(marker):
     """Built a new marker without the `python_version` part.
 
-    This could return `None` if the `python_version` section is the only section in the
-    marker.
+    This could return `None` if the `python_version` section is the only
+    section in the marker.
     """
 
     return _get_stripped_marker(marker, _strip_pyversion)
@@ -444,7 +443,7 @@ def _markers_collect_extras(markers, collection):
 def _markers_collect_pyversions(markers, collection):
     local_collection = []
     marker_format_str = "{0}"
-    for i, el in enumerate(reversed(markers)):
+    for el in reversed(markers):
         if isinstance(el, tuple) and el[0].value == "python_version":
             new_marker = str(gen_marker(el))
             local_collection.append(marker_format_str.format(new_marker))
@@ -490,8 +489,7 @@ def get_contained_extras(marker):
 
 @lru_cache(maxsize=1024)
 def get_contained_pyversions(marker):
-    """Collect all `python_version` operands from a marker.
-    """
+    """Collect all `python_version` operands from a marker."""
 
     collection = []
     if not marker:
@@ -522,8 +520,7 @@ def get_contained_pyversions(marker):
 
 @lru_cache(maxsize=128)
 def contains_extra(marker):
-    """Check whehter a marker contains an "extra == ..." operand.
-    """
+    """Check whehter a marker contains an "extra == ..." operand."""
     if not marker:
         return False
     marker = _ensure_marker(marker)
@@ -532,8 +529,7 @@ def contains_extra(marker):
 
 @lru_cache(maxsize=128)
 def contains_pyversion(marker):
-    """Check whether a marker contains a python_version operand.
-    """
+    """Check whether a marker contains a python_version operand."""
 
     if not marker:
         return False
@@ -543,8 +539,8 @@ def contains_pyversion(marker):
 
 def _split_specifierset_str(specset_str, prefix="=="):
     # type: (str, str) -> Set[Specifier]
-    """
-    Take a specifierset string and split it into a list to join for specifier sets
+    """Take a specifierset string and split it into a list to join for
+    specifier sets.
 
     :param str specset_str: A string containing python versions, often comma separated
     :param str prefix: A prefix to use when generating the specifier set
@@ -564,8 +560,7 @@ def _split_specifierset_str(specset_str, prefix="=="):
 
 
 def _get_specifiers_from_markers(marker_item):
-    """
-    Given a marker item, get specifiers from the version marker
+    """Given a marker item, get specifiers from the version marker.
 
     :param :class:`~packaging.markers.Marker` marker_sequence: A marker describing a version constraint
     :return: A set of specifiers corresponding to the marker constraint
diff --git a/src/requirementslib/models/metadata.py b/src/requirementslib/models/metadata.py
index b45b1f02..28bc4b73 100644
--- a/src/requirementslib/models/metadata.py
+++ b/src/requirementslib/models/metadata.py
@@ -8,6 +8,8 @@
 import os
 import zipfile
 from collections import defaultdict
+from functools import reduce
+from typing import Sequence
 
 import attr
 import dateutil.parser
@@ -15,7 +17,6 @@
 import distlib.wheel
 import packaging.version
 import requests
-import six
 import vistir
 from packaging.markers import Marker
 from packaging.requirements import Requirement as PackagingRequirement
@@ -35,12 +36,6 @@
 from .requirements import Requirement
 from .utils import filter_dict, get_pinned_version, is_pinned_requirement
 
-# fmt: off
-from six.moves import Sequence  # type: ignore  # isort:skip
-from six.moves import reduce  # type: ignore # isort:skip
-# fmt: on # isort:skip
-
-
 ch = logging.StreamHandler()
 formatter = logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
 ch.setFormatter(formatter)
@@ -64,7 +59,9 @@
         TypeVar,
         Union,
     )
+
     from attr import Attribute  # noqa
+
     from .setup_info import SetupInfo
 
     TAttrsClass = TypeVar("TAttrsClass")
@@ -318,7 +315,9 @@ def from_requirement(cls, req, parent=None):
         return cls(
             name=req.name,
             specifier=req.specifier,
-            extras=tuple(sorted(set(req.extras))) if req.extras is not None else req.extras,
+            extras=tuple(sorted(set(req.extras)))
+            if req.extras is not None
+            else req.extras,
             requirement=req,
             from_extras=from_extras,
             python_version=python_version,
@@ -364,7 +363,9 @@ def from_info(cls, info):
         return cls(
             name=info.name,
             specifier=req.specifier,
-            extras=tuple(sorted(set(req.extras))) if req.extras is not None else req.extras,
+            extras=tuple(sorted(set(req.extras)))
+            if req.extras is not None
+            else req.extras,
             requirement=req,
             from_extras=None,
             python_version=SpecifierSet(requires_python_str),
@@ -625,15 +626,15 @@ def create(cls, release_dict, name=None):
         # type: (TReleaseUrlDict, Optional[str]) -> "ReleaseUrl"
         valid_digest_keys = set("{0}_digest".format(k) for k in VALID_ALGORITHMS.keys())
         digest_keys = set(release_dict.keys()) & valid_digest_keys
-        creation_kwargs = {}  # type: Dict[str, Union[bool, int, str, Digest, TDigestDict]]
-        creation_kwargs = {
-            k: v for k, v in release_dict.items() if k not in digest_keys
-        }
+        creation_kwargs = (
+            {}
+        )  # type: Dict[str, Union[bool, int, str, Digest, TDigestDict]]
+        creation_kwargs = {k: v for k, v in release_dict.items() if k not in digest_keys}
         if name is not None:
             creation_kwargs["name"] = name
         for k in digest_keys:
             digest = release_dict[k]
-            if not isinstance(digest, six.string_types):
+            if not isinstance(digest, str):
                 raise TypeError("Digests must be strings, got {!r}".format(digest))
             creation_kwargs[k] = Digest.create(k.replace("_digest", ""), digest)
         release_url = cls(**filter_dict(creation_kwargs))  # type: ignore
@@ -827,7 +828,8 @@ def get_releases_from_package(releases, name=None):
 @attr.s(frozen=True)
 class ReleaseCollection(object):
     releases = attr.ib(
-        factory=list, converter=instance_check_converter(list, get_releases_from_package),  # type: ignore
+        factory=list,
+        converter=instance_check_converter(list, get_releases_from_package),  # type: ignore
     )  # type: List[Release]
 
     def __iter__(self):
@@ -894,7 +896,7 @@ def convert_releases_to_collection(releases, name=None):
 
 def split_keywords(value):
     # type: (Union[str, List]) -> List[str]
-    if value and isinstance(value, six.string_types):
+    if value and isinstance(value, str):
         return value.split(",")
     elif isinstance(value, list):
         return value
@@ -1027,7 +1029,9 @@ class Package(object):
 
     @urls.default
     def _get_urls_collection(self):
-        return functools.partial(convert_release_urls_to_collection, urls=[], name=self.name)
+        return functools.partial(
+            convert_release_urls_to_collection, urls=[], name=self.name
+        )
 
     @property
     def name(self):
@@ -1173,7 +1177,9 @@ def get_latest_lockfile(self):
             try:
                 version = next(iter(specset.filter((r.version for r in sorted_releases))))
             except StopIteration:
-                logger.info("No version of {0} matches specifier: {1}".format(sub_dep, specset))
+                logger.info(
+                    "No version of {0} matches specifier: {1}".format(sub_dep, specset)
+                )
                 logger.info(
                     "Available versions: {0}".format(
                         " ".join([r.version for r in sorted_releases])
diff --git a/src/requirementslib/models/pipfile.py b/src/requirementslib/models/pipfile.py
index 9c0aea4e..a1816cd0 100644
--- a/src/requirementslib/models/pipfile.py
+++ b/src/requirementslib/models/pipfile.py
@@ -5,13 +5,12 @@
 import copy
 import itertools
 import os
-import sys
+from pathlib import Path
 
 import attr
 import plette.models.base
 import plette.pipfiles
 import tomlkit
-from vistir.compat import FileNotFoundError, Path
 
 from ..environment import MYPY_RUNNING
 from ..exceptions import RequirementError
@@ -21,7 +20,7 @@
 from .utils import get_url_name, optional_instance_of, tomlkit_value_to_python
 
 if MYPY_RUNNING:
-    from typing import Union, Any, Dict, Iterable, Mapping, List, Text
+    from typing import Any, Dict, Iterable, List, Mapping, Text, Union
 
     package_type = Dict[Text, Dict[Text, Union[List[Text], Text]]]
     source_type = Dict[Text, Union[Text, bool]]
@@ -68,8 +67,8 @@ def validate(cls, data):
     @classmethod
     def ensure_package_sections(cls, data):
         # type: (tomlkit.toml_document.TOMLDocument[Text, Any]) -> tomlkit.toml_document.TOMLDocument[Text, Any]
-        """
-        Ensure that all pipfile package sections are present in the given toml document
+        """Ensure that all pipfile package sections are present in the given
+        toml document.
 
         :param :class:`~tomlkit.toml_document.TOMLDocument` data: The toml document to
             ensure package sections are present on
@@ -256,7 +255,8 @@ def allow_prereleases(self):
     @classmethod
     def read_projectfile(cls, path):
         # type: (Text) -> ProjectFile
-        """Read the specified project file and provide an interface for writing/updating.
+        """Read the specified project file and provide an interface for
+        writing/updating.
 
         :param Text path: Path to the target file.
         :return: A project file with the model and location for interaction
@@ -268,8 +268,7 @@ def read_projectfile(cls, path):
     @classmethod
     def load_projectfile(cls, path, create=False):
         # type: (Text, bool) -> ProjectFile
-        """
-        Given a path, load or create the necessary pipfile.
+        """Given a path, load or create the necessary pipfile.
 
         :param Text path: Path to the project root or pipfile
         :param bool create: Whether to create the pipfile if not found, defaults to True
@@ -294,8 +293,7 @@ def load_projectfile(cls, path, create=False):
     @classmethod
     def load(cls, path, create=False):
         # type: (Text, bool) -> Pipfile
-        """
-        Given a path, load or create the necessary pipfile.
+        """Given a path, load or create the necessary pipfile.
 
         :param Text path: Path to the project root or pipfile
         :param bool create: Whether to create the pipfile if not found, defaults to True
@@ -378,7 +376,6 @@ def build_requires(self):
     @property
     def build_backend(self):
         # type: () -> Text
-        pyproject = self.path.parent.joinpath("pyproject.toml")
         if not self.build_system:
             self._read_pyproject()
         return self.build_system.get("build-backend", None)
diff --git a/src/requirementslib/models/project.py b/src/requirementslib/models/project.py
index 7c1b0e81..8e1af95d 100644
--- a/src/requirementslib/models/project.py
+++ b/src/requirementslib/models/project.py
@@ -11,9 +11,7 @@
 import packaging.utils
 import plette
 import plette.models
-import six
 import tomlkit
-from vistir.compat import FileNotFoundError
 
 SectionDifference = collections.namedtuple("SectionDifference", ["inthis", "inthat"])
 FileDifference = collections.namedtuple("FileDifference", ["default", "develop"])
@@ -39,15 +37,14 @@ def _are_pipfile_entries_equal(a, b):
 
 
 def preferred_newlines(f):
-    if isinstance(f.newlines, six.text_type):
+    if isinstance(f.newlines, str):
         return f.newlines
     return DEFAULT_NEWLINES
 
 
 @attr.s
 class ProjectFile(object):
-    """A file in the Pipfile project.
-    """
+    """A file in the Pipfile project."""
 
     location = attr.ib()
     line_ending = attr.ib()
@@ -74,7 +71,7 @@ def write(self):
             self.model.dump(f)
 
     def dumps(self):
-        strio = six.StringIO()
+        strio = io.StringIO()
         self.model.dump(strio)
         return strio.getvalue()
 
diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py
index 6831a7c8..89fae113 100644
--- a/src/requirementslib/models/requirements.py
+++ b/src/requirementslib/models/requirements.py
@@ -8,10 +8,13 @@
 import sys
 from contextlib import contextmanager
 from distutils.sysconfig import get_python_lib
+from functools import lru_cache
+from pathlib import Path
+from urllib import parse as urllib_parse
+from urllib.parse import unquote
 
 import attr
 import pip_shims
-import six
 from cached_property import cached_property
 from packaging.markers import Marker
 from packaging.requirements import Requirement as PackagingRequirement
@@ -22,9 +25,6 @@
     SpecifierSet,
 )
 from packaging.utils import canonicalize_name
-from six.moves.urllib import parse as urllib_parse
-from six.moves.urllib.parse import unquote
-from vistir.compat import FileNotFoundError, Path, lru_cache
 from vistir.contextmanagers import temp_path
 from vistir.misc import dedup
 from vistir.path import (
@@ -47,9 +47,7 @@
     is_vcs,
     strip_ssh_from_git_uri,
 )
-from .markers import (
-    normalize_marker_str,
-)
+from .markers import normalize_marker_str
 from .setup_info import (
     SetupInfo,
     _prepare_wheel_building_kwargs,
@@ -87,34 +85,36 @@
 
 if MYPY_RUNNING:
     from typing import (
-        Optional,
-        TypeVar,
-        List,
-        Dict,
-        Union,
         Any,
-        Tuple,
+        AnyStr,
+        Dict,
+        FrozenSet,
+        Generator,
+        List,
+        Optional,
         Sequence,
         Set,
-        AnyStr,
         Text,
-        Generator,
-        FrozenSet,
+        Tuple,
+        TypeVar,
+        Union,
     )
+
     from pip_shims.shims import (
-        Link,
+        InstallationCandidate,
         InstallRequirement,
+        Link,
         PackageFinder,
-        InstallationCandidate,
     )
 
     RequirementType = TypeVar(
         "RequirementType", covariant=True, bound=PackagingRequirement
     )
     F = TypeVar("F", "FileRequirement", "VCSRequirement", covariant=True)
-    from six.moves.urllib.parse import SplitResult
-    from .vcs import VCSRepository
+    from urllib.parse import SplitResult
+
     from .dependencies import AbstractDependency
+    from .vcs import VCSRepository
 
     NON_STRING_ITERABLE = Union[List, Set, Tuple]
     STRING_TYPE = Union[str, bytes, Text]
@@ -446,7 +446,7 @@ def specifiers(self):
     def specifiers(self, specifiers):
         # type: (Union[Text, str, SpecifierSet]) -> None
         if not isinstance(specifiers, SpecifierSet):
-            if isinstance(specifiers, six.string_types):
+            if isinstance(specifiers, str):
                 specifiers = SpecifierSet(specifiers)
             else:
                 raise TypeError("Must pass a string or a SpecifierSet")
@@ -1004,7 +1004,7 @@ def _parse_name_from_path(self):
             parsed_setup_py = self.parsed_setup_py
             if parsed_setup_py:
                 name = parsed_setup_py.get("name", "")
-                if name and isinstance(name, six.string_types):
+                if name and isinstance(name, str):
                     return name
         return None
 
@@ -1374,7 +1374,7 @@ def from_pipfile(cls, name, pipfile):
         creation_args["version"] = version  # type: ignore
         req = init_requirement("{0}{1}".format(name, version))
         if req and extras and req.extras and isinstance(req.extras, tuple):
-            if isinstance(extras, six.string_types):
+            if isinstance(extras, str):
                 req.extras = (extras) + tuple(["{0}".format(xtra) for xtra in req.extras])
             elif isinstance(extras, (tuple, list)):
                 req.extras += tuple(extras)
@@ -1648,7 +1648,7 @@ def get_uri(self):
         elif (
             getattr(self, "req", None)
             and self.req is not None
-            and getattr(self.req, "url")
+            and getattr(self.req, "url", None)
         ):
             return self.req.url
         elif self.link is not None:
@@ -1713,7 +1713,7 @@ def is_local(self):
             elif (
                 getattr(self, "req", None)
                 and self.req is not None
-                and (getattr(self.req, "url") and self.req.url is not None)
+                and (getattr(self.req, "url", None) and self.req.url is not None)
             ):
                 uri = self.req.url
             if uri and is_file_url(uri):
@@ -1765,7 +1765,7 @@ def from_pipfile(cls, name, pipfile):
         uri = pipfile.get("uri")
         fil = pipfile.get("file")
         path = pipfile.get("path")
-        if path and isinstance(path, six.string_types):
+        if path and isinstance(path, str):
             if isinstance(path, Path) and not path.is_absolute():
                 path = get_converted_relative_path(path.as_posix())
             elif not os.path.isabs(path):
@@ -1790,7 +1790,7 @@ def from_pipfile(cls, name, pipfile):
         if not uri:
             uri = pip_shims.shims.path_to_url(path)
         link_info = None  # type: Optional[LinkInfo]
-        if uri and isinstance(uri, six.string_types):
+        if uri and isinstance(uri, str):
             link_info = cls.get_link_from_line(uri)
         else:
             raise ValueError(
@@ -1826,7 +1826,7 @@ def from_pipfile(cls, name, pipfile):
             else:
                 if link:
                     line = link.url
-                elif uri and isinstance(uri, six.string_types):
+                elif uri and isinstance(uri, str):
                     line = uri
                 else:
                     raise ValueError(
@@ -2259,7 +2259,7 @@ def from_pipfile(cls, name, pipfile):
             if key in VCS_LIST and key in pipfile_keys:
                 creation_args["vcs"] = key
                 target = pipfile[key]
-                if isinstance(target, six.string_types):
+                if isinstance(target, str):
                     drive, path = os.path.splitdrive(target)
                     if (
                         not drive
@@ -2305,9 +2305,7 @@ def line_part(self):
             self._parsed_line.is_direct_url and self._parsed_line.line_with_prefix
         ):
             return self._parsed_line.line_with_prefix
-        elif getattr(self, "_base_line", None) and (
-            isinstance(self._base_line, six.string_types)
-        ):
+        elif getattr(self, "_base_line", None) and (isinstance(self._base_line, str)):
             base = self._base_line
         else:
             base = getattr(self, "link", self.get_link()).url
@@ -2439,7 +2437,7 @@ def add_hashes(self, hashes):
         new_hashes = set()  # type: Set[STRING_TYPE]
         if self.hashes is not None:
             new_hashes |= set(self.hashes)
-        if isinstance(hashes, six.string_types):
+        if isinstance(hashes, str):
             new_hashes.add(hashes)
         else:
             new_hashes |= set(hashes)
@@ -2462,7 +2460,7 @@ def get_hashes_as_pip(self, as_list=False):
     def hashes_as_pip(self):
         # type: () -> STRING_TYPE
         hashes = self.get_hashes_as_pip()
-        assert isinstance(hashes, six.string_types)
+        assert isinstance(hashes, str)
         return hashes
 
     @property
@@ -2972,8 +2970,8 @@ def get_abstract_dependencies(self, sources=None):
 
         from .dependencies import (
             AbstractDependency,
-            get_dependencies,
             get_abstract_dependencies,
+            get_dependencies,
         )
 
         if not self.abstract_dep:
@@ -3003,7 +3001,7 @@ def find_all_matches(self, sources=None, finder=None):
         :rtype: list[ :class:`~pip._internal.index.InstallationCandidate` ]
         """
 
-        from .dependencies import get_finder, find_all_matches
+        from .dependencies import find_all_matches, get_finder
 
         if not finder:
             _, finder = get_finder(sources=sources)
diff --git a/src/requirementslib/models/resolvers.py b/src/requirementslib/models/resolvers.py
index 43590523..8c360c45 100644
--- a/src/requirementslib/models/resolvers.py
+++ b/src/requirementslib/models/resolvers.py
@@ -2,7 +2,6 @@
 from contextlib import contextmanager
 
 import attr
-import six
 from pip_shims.shims import Wheel
 
 from .cache import HashCache
@@ -62,8 +61,8 @@ def resolution(self):
         return list(self.pinned_deps.values())
 
     def add_abstract_dep(self, dep):
-        """Add an abstract dependency by either creating a new entry or
-        merging with an old one.
+        """Add an abstract dependency by either creating a new entry or merging
+        with an old one.
 
         :param dep: An abstract dependency to add
         :type dep: :class:`~requirementslib.models.dependency.AbstractDependency`
@@ -85,10 +84,12 @@ def add_abstract_dep(self, dep):
             self.dep_dict[dep.name] = dep
 
     def pin_deps(self):
-        """Pins the current abstract dependencies and adds them to the history dict.
+        """Pins the current abstract dependencies and adds them to the history
+        dict.
 
-        Adds any new dependencies to the abstract dependencies already present by
-        merging them together to form new, compatible abstract dependencies.
+        Adds any new dependencies to the abstract dependencies already
+        present by merging them together to form new, compatible
+        abstract dependencies.
         """
 
         for name in list(self.dep_dict.keys()):
@@ -122,7 +123,8 @@ def pin_deps(self):
                     break
 
     def resolve(self, root_nodes, max_rounds=20):
-        """Resolves dependencies using a backtracking resolver and multiple endpoints.
+        """Resolves dependencies using a backtracking resolver and multiple
+        endpoints.
 
         Note: this resolver caches aggressively.
         Runs for *max_rounds* or until any two pinning rounds yield the same outcome.
@@ -141,11 +143,11 @@ def resolve(self, root_nodes, max_rounds=20):
 
         # Coerce input into AbstractDependency instances.
         # We accept str, Requirement, and AbstractDependency as input.
-        from .dependencies import AbstractDependency
         from ..utils import log
+        from .dependencies import AbstractDependency
 
         for dep in root_nodes:
-            if isinstance(dep, six.string_types):
+            if isinstance(dep, str):
                 dep = AbstractDependency.from_string(dep)
             elif not isinstance(dep, AbstractDependency):
                 dep = AbstractDependency.from_requirement(dep)
@@ -227,11 +229,11 @@ def get_hashes_for_one(self, ireq):
 
     @contextmanager
     def allow_all_wheels(self):
-        """
-        Monkey patches pip.Wheel to allow wheels from all platforms and Python versions.
+        """Monkey patches pip.Wheel to allow wheels from all platforms and
+        Python versions.
 
-        This also saves the candidate cache and set a new one, or else the results from the
-        previous non-patched calls will interfere.
+        This also saves the candidate cache and set a new one, or else
+        the results from the previous non-patched calls will interfere.
         """
 
         def _wheel_supported(self, tags=None):
diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py
index 18579c02..d30ac64a 100644
--- a/src/requirementslib/models/setup_info.py
+++ b/src/requirementslib/models/setup_info.py
@@ -3,6 +3,7 @@
 
 import ast
 import atexit
+import configparser
 import contextlib
 import importlib
 import io
@@ -10,23 +11,22 @@
 import os
 import shutil
 import sys
-from functools import partial
+from collections.abc import Iterable, Mapping
+from functools import lru_cache, partial
+from pathlib import Path
+from urllib.parse import urlparse, urlunparse
+from weakref import finalize
 
 import attr
-import chardet
 import packaging.specifiers
 import packaging.utils
 import packaging.version
 import pep517.envbuild
 import pep517.wrappers
-import six
-from platformdirs import user_cache_dir
 from distlib.wheel import Wheel
 from packaging.markers import Marker
 from pip_shims.utils import call_function_with_correct_args
-from six.moves import configparser
-from six.moves.urllib.parse import urlparse, urlunparse
-from vistir.compat import FileNotFoundError, Iterable, Mapping, Path, finalize, lru_cache
+from platformdirs import user_cache_dir
 from vistir.contextmanagers import cd, temp_path
 from vistir.misc import run
 from vistir.path import create_tracked_tempdir, ensure_mkdir_p, mkdir_p, rmtree
@@ -49,7 +49,7 @@
     pkg_resources_requirements = None
 
 try:
-    from setuptools.dist import distutils, Distribution
+    from setuptools.dist import Distribution, distutils
 except ImportError:
     import distutils
     from distutils.core import Distribution
@@ -68,28 +68,25 @@
 if MYPY_RUNNING:
     from typing import (
         Any,
+        AnyStr,
         Callable,
         Dict,
-        List,
         Generator,
+        List,
         Optional,
-        Union,
+        Sequence,
+        Set,
+        Text,
         Tuple,
         TypeVar,
-        Text,
-        Set,
-        AnyStr,
-        Sequence,
+        Union,
     )
+
     import requests
-    from pip_shims.shims import InstallRequirement, PackageFinder
-    from pkg_resources import (
-        PathMetadata,
-        DistInfoDistribution,
-        EggInfoDistribution,
-        Requirement as PkgResourcesRequirement,
-    )
     from packaging.requirements import Requirement as PackagingRequirement
+    from pip_shims.shims import InstallRequirement, PackageFinder
+    from pkg_resources import DistInfoDistribution, EggInfoDistribution, PathMetadata
+    from pkg_resources import Requirement as PkgResourcesRequirement
 
     TRequirement = TypeVar("TRequirement")
     RequirementType = TypeVar(
@@ -152,7 +149,8 @@ def pip_install(self, reqs):
 class HookCaller(pep517.wrappers.Pep517HookCaller):
     def __init__(self, source_dir, build_backend, backend_path=None):
         super(pep517.wrappers.Pep517HookCaller, self).__init__(
-            source_dir, build_backend, backend_path=backend_path)
+            source_dir, build_backend, backend_path=backend_path
+        )
         self.source_dir = os.path.abspath(source_dir)
         self.build_backend = build_backend
         self._subprocess_runner = pep517_subprocess_runner
@@ -195,7 +193,7 @@ def parse_special_directives(setup_entry, package_dir=None):
                 return str(rv)
             module = importlib.import_module(resource)
             rv = getattr(module, attribute)
-            if not isinstance(rv, six.string_types):
+            if not isinstance(rv, str):
                 rv = str(rv)
     return rv
 
@@ -212,7 +210,7 @@ def make_base_requirements(reqs):
             req, pkg_resources_requirements.Requirement
         ):
             requirements.add(BaseRequirement.from_req(req))
-        elif req and isinstance(req, six.string_types) and not req.startswith("#"):
+        elif req and isinstance(req, str) and not req.startswith("#"):
             requirements.add(BaseRequirement.from_string(req))
     return requirements
 
@@ -314,11 +312,7 @@ def parse_setup_cfg(
         },
     }
     parser = configparser.ConfigParser(default_opts)
-    if six.PY2:
-        buff = io.BytesIO(setup_cfg_contents)
-        parser.readfp(buff)
-    else:
-        parser.read_string(setup_cfg_contents)
+    parser.read_string(setup_cfg_contents)
     results = {}
     package_dir = get_package_dir_from_setupcfg(parser, base_dir=base_dir)
     name, version = get_name_and_version_from_setupcfg(parser, package_dir)
@@ -407,7 +401,7 @@ def ensure_reqs(reqs):
     for req in reqs:
         if not req:
             continue
-        if isinstance(req, six.string_types):
+        if isinstance(req, str):
             req = pkg_resources.Requirement.parse("{0}".format(str(req)))
         # req = strip_extras_markers_from_requirement(req)
         new_reqs.append(req)
@@ -589,7 +583,7 @@ def get_extra_name_from_marker(marker):
 
 def get_metadata_from_wheel(wheel_path):
     # type: (S) -> Dict[Any, Any]
-    if not isinstance(wheel_path, six.string_types):
+    if not isinstance(wheel_path, str):
         raise TypeError("Expected string instance, received {0!r}".format(wheel_path))
     try:
         dist = Wheel(wheel_path)
@@ -845,7 +839,7 @@ def unparse_Attribute(self, item):
         if attr_name and not self.recurse:
             name = attr_name
         elif name and attr_attr:
-            if isinstance(name, six.string_types):
+            if isinstance(name, str):
                 unparsed = ".".join([item for item in (name, attr_attr) if item])
             else:
                 unparsed = item
@@ -973,7 +967,7 @@ def parse_function_names(self, should_retry=True, function_map=None):
                         retries.append((k, v))
                     continue
                 else:
-                    if isinstance(fn, six.string_types):
+                    if isinstance(fn, str):
                         _, _, fn_name = fn.rpartition(".")
             if fn_name:
                 self.resolved_function_names[fn_name] = ast_unparse(v, analyzer=self)
@@ -982,9 +976,7 @@ def parse_function_names(self, should_retry=True, function_map=None):
     def parse_functions(self):
         retries = self.parse_function_names(function_map=self.function_map)
         if retries:
-            failures = self.parse_function_names(
-                should_retry=False, function_map=dict(retries)
-            )
+            self.parse_function_names(should_retry=False, function_map=dict(retries))
         return self.resolved_function_names
 
     def parse_setup_function(self):
@@ -1041,7 +1033,7 @@ def ast_unparse(item, initial_mapping=False, analyzer=None, recurse=True):  # no
     elif isinstance(item, ast.Subscript):
         unparsed = unparse(item.value)
         if not initial_mapping:
-            if isinstance(item.slice, ast.Index):
+            if isinstance(item.slice, (ast.Index, ast.Constant)):
                 try:
                     unparsed = unparsed[unparse(item.slice.value)]
                 except (KeyError, TypeError):
@@ -1083,7 +1075,7 @@ def ast_unparse(item, initial_mapping=False, analyzer=None, recurse=True):  # no
                         unparsed = items.get(item.id, item.id)
         else:
             unparsed = item
-    elif six.PY3 and isinstance(item, ast.NameConstant):
+    elif isinstance(item, ast.NameConstant):
         unparsed = item.value
     elif any(isinstance(item, k) for k in AST_COMPARATORS.keys()):
         unparsed = AST_COMPARATORS[type(item)]
@@ -1138,7 +1130,7 @@ def ast_unparse(item, initial_mapping=False, analyzer=None, recurse=True):  # no
         else:
             name = unparse(attr_name) if attr_name is not None else attr_attr
         if name and attr_attr:
-            if not initial_mapping and isinstance(name, six.string_types):
+            if not initial_mapping and isinstance(name, str):
                 unparsed = ".".join([item for item in (name, attr_attr) if item])
             else:
                 unparsed = item
@@ -1199,13 +1191,13 @@ def ast_unparse(item, initial_mapping=False, analyzer=None, recurse=True):  # no
                 unparsed[k] = unparse(v)
     elif isinstance(item, (list, tuple)):
         unparsed = type(item)([unparse(el) for el in item])
-    elif isinstance(item, six.string_types):
+    elif isinstance(item, str):
         unparsed = item
     return unparsed
 
 
 def ast_parse_attribute_from_file(path, attribute):
-    # type: (S) -> Any
+    # type: (S, S) -> Any
     analyzer = ast_parse_file(path)
     target_value = None
     for k, v in analyzer.assignments.items():
@@ -1214,7 +1206,7 @@ def ast_parse_attribute_from_file(path, attribute):
             name = k.id
         elif isinstance(k, ast.Attribute):
             fn = ast_unparse(k)
-            if isinstance(fn, six.string_types):
+            if isinstance(fn, str):
                 _, _, name = fn.rpartition(".")
         if name == attribute:
             target_value = ast_unparse(v, analyzer=analyzer)
@@ -1229,11 +1221,13 @@ def ast_parse_file(path):
     try:
         tree = ast.parse(read_source(path))
     except SyntaxError:
+        import charset_normalizer  # noqa: F811
+
         # The source may be encoded strangely, e.g. azure-storage
         # which has a setup.py encoded with utf-8-sig
         with open(path, "rb") as fh:
             contents = fh.read()
-        encoding = chardet.detect(contents)["encoding"]
+        encoding = charset_normalizer.detect(contents)["encoding"]
         tree = ast.parse(contents.decode(encoding))
     ast_analyzer = Analyzer()
     ast_analyzer.visit(tree)
@@ -1289,26 +1283,16 @@ def run_setup(script_path, egg_base=None):
         script_name = os.path.basename(script_path)
         g = {"__file__": script_name, "__name__": "__main__"}
         sys.path.insert(0, target_cwd)
-        local_dict = {}
-        if sys.version_info < (3, 5):
-            save_argv = sys.argv
-        else:
-            save_argv = sys.argv.copy()
+
+        save_argv = sys.argv.copy()
         try:
             global _setup_distribution, _setup_stop_after
             _setup_stop_after = "run"
             sys.argv[0] = script_name
             sys.argv[1:] = args
             with open(script_name, "rb") as f:
-                contents = f.read()
-                if six.PY3:
-                    contents.replace(br"\r\n", br"\n")
-                else:
-                    contents.replace(r"\r\n", r"\n")
-                if sys.version_info < (3, 5):
-                    exec(contents, g, local_dict)
-                else:
-                    exec(contents, g)
+                contents = f.read().replace(br"\r\n", br"\n")
+                exec(contents, g)
         # We couldn't import everything needed to run setup
         except Exception:
             python = os.environ.get("PIP_PYTHON_PATH", sys.executable)
@@ -1481,7 +1465,7 @@ def egg_base(self):
 
     def update_from_dict(self, metadata):
         name = metadata.get("name", self.name)
-        if isinstance(name, six.string_types):
+        if isinstance(name, str):
             self.name = self.name if self.name else name
         version = metadata.get("version", None)
         if version:
@@ -1546,8 +1530,6 @@ def parse_setup_cfg(self):
             try:
                 parsed = setuptools_parse_setup_cfg(self.setup_cfg.as_posix())
             except Exception:
-                if six.PY2:
-                    contents = self.setup_cfg.read_bytes()
                 parsed = parse_setup_cfg(contents, base_dir)
             if not parsed:
                 return {}
@@ -1617,7 +1599,7 @@ def build_wheel(self):
                     ['"{0}"'.format(r) for r in self.build_requires]
                 )
             self.pyproject.write_text(
-                six.text_type(
+                str(
                     """
 [build-system]
 requires = [{0}]
@@ -1650,7 +1632,7 @@ def build_sdist(self):
                     ['"{0}"'.format(r) for r in self.build_requires]
                 )
             self.pyproject.write_text(
-                six.text_type(
+                str(
                     """
 [build-system]
 requires = [{0}]
@@ -1895,8 +1877,7 @@ def from_ireq(cls, ireq, subdir=None, finder=None, session=None):
             cmd = pip_shims.shims.InstallCommand()
             options, _ = cmd.parser.parse_args([])
             session = cmd._build_session(options)
-            finder = cmd._build_package_finder(options, session)
-        tempdir_manager = stack.enter_context(pip_shims.shims.global_tempdir_manager())
+        stack.enter_context(pip_shims.shims.global_tempdir_manager())
         vcs, uri = split_vcs_method_from_uri(ireq.link.url_without_fragment)
         parsed = urlparse(uri)
         if "file" in parsed.scheme:
diff --git a/src/requirementslib/models/url.py b/src/requirementslib/models/url.py
index 8f5d8488..106d6d4c 100644
--- a/src/requirementslib/models/url.py
+++ b/src/requirementslib/models/url.py
@@ -1,11 +1,13 @@
 # -*- coding=utf-8 -*-
 from __future__ import absolute_import, print_function
 
+from urllib.parse import quote
+from urllib.parse import unquote as url_unquote
+from urllib.parse import unquote_plus
+
 import attr
 import pip_shims.shims
 from orderedmultidict import omdict
-from six.moves.urllib.parse import quote, unquote_plus, unquote as url_unquote
-from urllib3 import util as urllib3_util
 from urllib3.util import parse_url as urllib3_parse
 from urllib3.util.url import Url
 
@@ -14,9 +16,9 @@
 from .utils import extras_to_string, parse_extras
 
 if MYPY_RUNNING:
-    from typing import Dict, List, Optional, Text, Tuple, TypeVar, Union
+    from typing import Dict, Optional, Text, Tuple, TypeVar, Union
+
     from pip_shims.shims import Link
-    from vistir.compat import Path
 
     _T = TypeVar("_T")
     STRING_TYPE = Union[bytes, str, Text]
diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py
index 59900c68..c0f4b239 100644
--- a/src/requirementslib/models/utils.py
+++ b/src/requirementslib/models/utils.py
@@ -7,9 +7,10 @@
 import string
 import sys
 from collections import defaultdict
+from functools import lru_cache
 from itertools import chain, groupby
+from pathlib import Path
 
-import six
 import tomlkit
 from attr import validators
 from packaging.markers import InvalidMarker, Marker, Op, Value, Variable
@@ -20,7 +21,6 @@
 from tomlkit.items import AoT, Array, Bool, InlineTable, Item, String, Table
 from urllib3 import util as urllib3_util
 from urllib3.util import parse_url as urllib3_parse
-from vistir.compat import lru_cache
 from vistir.misc import dedup
 from vistir.path import is_valid_url
 
@@ -28,33 +28,31 @@
 from ..utils import SCHEME_LIST, VCS_LIST, is_star
 
 if MYPY_RUNNING:
-    from attr import _ValidatorType  # noqa
-    from packaging.requirements import Requirement as PackagingRequirement
-    from pip_shims.shims import Link
-    from pkg_resources import Requirement as PkgResourcesRequirement
-    from pkg_resources.extern.packaging.markers import (
-        Op as PkgResourcesOp,
-        Variable as PkgResourcesVariable,
-        Value as PkgResourcesValue,
-        Marker as PkgResourcesMarker,
-    )
+    from typing import Iterable  # noqa
     from typing import (
-        Union,
-        Optional,
-        List,
-        Set,
         Any,
-        TypeVar,
-        Tuple,
-        Sequence,
-        Dict,
-        Text,
         AnyStr,
+        Dict,
+        List,
         Match,
-        Iterable,  # noqa
+        Optional,
+        Sequence,
+        Set,
+        Text,
+        Tuple,
+        TypeVar,
+        Union,
     )
+
+    from attr import _ValidatorType  # noqa
+    from packaging.markers import Marker as PkgResourcesMarker
+    from packaging.markers import Op as PkgResourcesOp
+    from packaging.markers import Value as PkgResourcesValue
+    from packaging.markers import Variable as PkgResourcesVariable
+    from packaging.requirements import Requirement as PackagingRequirement
+    from pip_shims.shims import Link
+    from pkg_resources import Requirement as PkgResourcesRequirement
     from urllib3.util.url import Url
-    from vistir.compat import Path
 
     _T = TypeVar("_T")
     TMarker = Union[Marker, PkgResourcesMarker]
@@ -115,9 +113,9 @@ def optional_instance_of(cls):
 def create_link(link):
     # type: (AnyStr) -> Link
 
-    if not isinstance(link, six.string_types):
+    if not isinstance(link, str):
         raise TypeError("must provide a string to instantiate a new link")
-    from pip_shims.shims import Link
+    from pip_shims.shims import Link  # noqa: F811
 
     return Link(link)
 
@@ -178,14 +176,13 @@ def tomlkit_dict_to_python(toml_dict):
 
 def get_url_name(url):
     # type: (AnyStr) -> AnyStr
-    """
-    Given a url, derive an appropriate name to use in a pipfile.
+    """Given a url, derive an appropriate name to use in a pipfile.
 
     :param str url: A url to derive a string from
     :returns: The name of the corresponding pipfile entry
     :rtype: Text
     """
-    if not isinstance(url, six.string_types):
+    if not isinstance(url, str):
         raise TypeError("Expected a string, got {0!r}".format(url))
     return urllib3_util.parse_url(url).host
 
@@ -193,7 +190,7 @@ def get_url_name(url):
 def init_requirement(name):
     # type: (AnyStr) -> TRequirement
 
-    if not isinstance(name, six.string_types):
+    if not isinstance(name, str):
         raise TypeError("must supply a name to generate a requirement")
     from pkg_resources import Requirement
 
@@ -207,13 +204,13 @@ def init_requirement(name):
 
 def extras_to_string(extras):
     # type: (Iterable[S]) -> S
-    """Turn a list of extras into a string
+    """Turn a list of extras into a string.
 
     :param List[str]] extras: a list of extras to format
     :return: A string of extras
     :rtype: str
     """
-    if isinstance(extras, six.string_types):
+    if isinstance(extras, str):
         if extras.startswith("["):
             return extras
         else:
@@ -225,7 +222,7 @@ def extras_to_string(extras):
 
 def parse_extras(extras_str):
     # type: (AnyStr) -> List[AnyStr]
-    """Turn a string of extras into a parsed extras list
+    """Turn a string of extras into a parsed extras list.
 
     :param str extras_str: An extras string
     :return: A sorted list of extras
@@ -240,7 +237,7 @@ def parse_extras(extras_str):
 
 def specs_to_string(specs):
     # type: (List[Union[STRING_TYPE, Specifier]]) -> AnyStr
-    """Turn a list of specifier tuples into a string
+    """Turn a list of specifier tuples into a string.
 
     :param List[Union[Specifier, str]] specs: a list of specifiers to format
     :return: A string of specifiers
@@ -248,7 +245,7 @@ def specs_to_string(specs):
     """
 
     if specs:
-        if isinstance(specs, six.string_types):
+        if isinstance(specs, str):
             return specs
         try:
             extras = ",".join(["".join(spec) for spec in specs])
@@ -288,8 +285,7 @@ def build_vcs_uri(
 
 def _get_parsed_url(url):
     # type: (S) -> Url
-    """
-    This is a stand-in function for `urllib3.util.parse_url`
+    """This is a stand-in function for `urllib3.util.parse_url`
 
     The orignal function doesn't handle special characters very well, this simply splits
     out the authentication section, creates the parsed url, then puts the authentication
@@ -311,7 +307,7 @@ def _get_parsed_url(url):
 
 def convert_direct_url_to_url(direct_url):
     # type: (AnyStr) -> AnyStr
-    """Converts direct URLs to standard, link-style URLs
+    """Converts direct URLs to standard, link-style URLs.
 
     Given a direct url as defined by *PEP 508*, convert to a :class:`~pip_shims.shims.Link`
     compatible URL by moving the name and extras into an **egg_fragment**.
@@ -352,8 +348,7 @@ def convert_direct_url_to_url(direct_url):
 
 def convert_url_to_direct_url(url, name=None):
     # type: (AnyStr, Optional[AnyStr]) -> AnyStr
-    """
-    Converts normal link-style URLs to direct urls.
+    """Converts normal link-style URLs to direct urls.
 
     Given a :class:`~pip_shims.shims.Link` compatible URL, convert to a direct url as
     defined by *PEP 508* by extracting the name and extras from the **egg_fragment**.
@@ -366,7 +361,7 @@ def convert_url_to_direct_url(url, name=None):
     :raises ValueError: Raised when the URL can't be parsed or a name can't be found.
     :raises TypeError: When a non-string input is provided.
     """
-    if not isinstance(url, six.string_types):
+    if not isinstance(url, str):
         raise TypeError(
             "Expected a string to convert to a direct url, got {0!r}".format(url)
         )
@@ -410,15 +405,14 @@ def get_version(pipfile_entry):
             return ""
         return pipfile_entry.get("version", "").strip().lstrip("(").rstrip(")")
 
-    if isinstance(pipfile_entry, six.string_types):
+    if isinstance(pipfile_entry, str):
         return pipfile_entry.strip().lstrip("(").rstrip(")")
     return ""
 
 
 def strip_extras_markers_from_requirement(req):
     # type: (TRequirement) -> TRequirement
-    """
-    Strips extras markers from requirement instances.
+    """Strips extras markers from requirement instances.
 
     Given a :class:`~packaging.requirements.Requirement` instance with markers defining
     *extra == 'name'*, strip out the extras from the markers and return the cleaned
@@ -485,9 +479,8 @@ def get_default_pyproject_backend():
 
 def get_pyproject(path):
     # type: (Union[STRING_TYPE, Path]) -> Optional[Tuple[List[STRING_TYPE], STRING_TYPE]]
-    """
-    Given a base path, look for the corresponding ``pyproject.toml`` file and return its
-    build_requires and build_backend.
+    """Given a base path, look for the corresponding ``pyproject.toml`` file
+    and return its build_requires and build_backend.
 
     :param AnyStr path: The root path of the project, should be a directory (will be truncated)
     :return: A 2 tuple of build requirements and the build backend
@@ -495,7 +488,6 @@ def get_pyproject(path):
     """
     if not path:
         return
-    from vistir.compat import Path
 
     if not isinstance(path, Path):
         path = Path(path)
@@ -510,7 +502,7 @@ def get_pyproject(path):
         backend = get_default_pyproject_backend()
     else:
         pyproject_data = {}
-        with io.open(pp_toml.as_posix(), encoding="utf-8") as fh:
+        with open(pp_toml.as_posix(), encoding="utf-8") as fh:
             pyproject_data = tomlkit.loads(fh.read())
         build_system = pyproject_data.get("build-system", None)
         if build_system is None:
@@ -530,7 +522,7 @@ def get_pyproject(path):
 
 def split_markers_from_line(line):
     # type: (AnyStr) -> Tuple[AnyStr, Optional[AnyStr]]
-    """Split markers from a dependency"""
+    """Split markers from a dependency."""
     quote_chars = ["'", '"']
     line_quote = next(
         iter(quote for quote in quote_chars if line.startswith(quote)), None
@@ -562,15 +554,14 @@ def split_vcs_method_from_uri(uri):
 
 def split_ref_from_uri(uri):
     # type: (AnyStr) -> Tuple[AnyStr, Optional[AnyStr]]
-    """
-    Given a path or URI, check for a ref and split it from the path if it is present,
-    returning a tuple of the original input and the ref or None.
+    """Given a path or URI, check for a ref and split it from the path if it is
+    present, returning a tuple of the original input and the ref or None.
 
     :param AnyStr uri: The path or URI to split
     :returns: A 2-tuple of the path or URI and the ref
     :rtype: Tuple[AnyStr, Optional[AnyStr]]
     """
-    if not isinstance(uri, six.string_types):
+    if not isinstance(uri, str):
         raise TypeError("Expected a string, received {0!r}".format(uri))
     parsed = _get_parsed_url(uri)
     path = parsed.path if parsed.path else ""
@@ -738,8 +729,7 @@ def get_pinned_version(ireq):
 
 
 def is_pinned_requirement(ireq):
-    """
-    Returns whether an InstallRequirement is a "pinned" requirement.
+    """Returns whether an InstallRequirement is a "pinned" requirement.
 
     An InstallRequirement is considered pinned if:
 
@@ -763,9 +753,8 @@ def is_pinned_requirement(ireq):
 
 
 def as_tuple(ireq):
-    """
-    Pulls out the (name: str, version:str, extras:(str)) tuple from the pinned InstallRequirement.
-    """
+    """Pulls out the (name: str, version:str, extras:(str)) tuple from the
+    pinned InstallRequirement."""
 
     if not is_pinned_requirement(ireq):
         raise TypeError("Expected a pinned InstallRequirement, got {}".format(ireq))
@@ -777,24 +766,19 @@ def as_tuple(ireq):
 
 
 def full_groupby(iterable, key=None):
-    """
-    Like groupby(), but sorts the input on the group key first.
-    """
+    """Like groupby(), but sorts the input on the group key first."""
 
     return groupby(sorted(iterable, key=key), key=key)
 
 
 def flat_map(fn, collection):
-    """
-    Map a function over a collection and flatten the result by one-level
-    """
+    """Map a function over a collection and flatten the result by one-level."""
 
     return chain.from_iterable(map(fn, collection))
 
 
 def lookup_table(values, key=None, keyval=None, unique=False, use_lists=False):
-    """
-    Builds a dict-based lookup table (index) elegantly.
+    """Builds a dict-based lookup table (index) elegantly.
 
     Supports building normal and unique lookup tables.  For example:
 
@@ -855,7 +839,7 @@ def lookup_table(values, key=None, keyval=None, unique=False, use_lists=False):
 
 
 def name_from_req(req):
-    """Get the name of the requirement"""
+    """Get the name of the requirement."""
     if hasattr(req, "project_name"):
         # from pkg_resources, such as installed dists for pip-sync
         return req.project_name
@@ -867,8 +851,7 @@ def name_from_req(req):
 def make_install_requirement(
     name, version=None, extras=None, markers=None, constraint=False
 ):
-    """
-    Generates an :class:`~pip._internal.req.req_install.InstallRequirement`.
+    """Generates an :class:`~pip._internal.req.req_install.InstallRequirement`.
 
     Create an InstallRequirement from the supplied metadata.
 
@@ -903,7 +886,8 @@ def make_install_requirement(
 
 
 def version_from_ireq(ireq):
-    """version_from_ireq Extract the version from a supplied :class:`~pip._internal.req.req_install.InstallRequirement`
+    """version_from_ireq Extract the version from a supplied
+    :class:`~pip._internal.req.req_install.InstallRequirement`
 
     :param ireq: An InstallRequirement
     :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
@@ -924,7 +908,8 @@ def _get_requires_python(candidate):
 
 
 def clean_requires_python(candidates):
-    """Get a cleaned list of all the candidates with valid specifiers in the `requires_python` attributes."""
+    """Get a cleaned list of all the candidates with valid specifiers in the
+    `requires_python` attributes."""
     all_candidates = []
     sys_version = ".".join(map(str, sys.version_info[:3]))
     from packaging.version import parse as parse_version
@@ -985,25 +970,24 @@ def normalize_name(pkg):
     :rtype: AnyStr
     """
 
-    assert isinstance(pkg, six.string_types)
+    assert isinstance(pkg, str)
     return pkg.replace("_", "-").lower()
 
 
 def get_name_variants(pkg):
     # type: (STRING_TYPE) -> Set[STRING_TYPE]
-    """
-    Given a packager name, get the variants of its name for both the canonicalized
-    and "safe" forms.
+    """Given a packager name, get the variants of its name for both the
+    canonicalized and "safe" forms.
 
     :param AnyStr pkg: The package to lookup
     :returns: A list of names.
     :rtype: Set
     """
 
-    if not isinstance(pkg, six.string_types):
+    if not isinstance(pkg, str):
         raise TypeError("must provide a string to derive package names")
-    from pkg_resources import safe_name
     from packaging.utils import canonicalize_name
+    from pkg_resources import safe_name
 
     pkg = pkg.lower()
     names = {safe_name(pkg), canonicalize_name(pkg), pkg.replace("-", "_")}
@@ -1012,29 +996,26 @@ def get_name_variants(pkg):
 
 def read_source(path, encoding="utf-8"):
     # type: (S, S) -> S
-    """
-    Read a source file and get the contents with proper encoding for Python 2/3.
+    """Read a source file and get the contents with proper encoding for Python
+    2/3.
 
     :param AnyStr path: the file path
     :param AnyStr encoding: the encoding that defaults to UTF-8
     :returns: The contents of the source file
     :rtype: AnyStr
     """
-    if six.PY3:
-        with open(path, "r", encoding=encoding) as fp:
-            return fp.read()
-    else:
-        with open(path, "r") as fp:
-            return fp.read()
+    with open(path, "r", encoding=encoding) as fp:
+        return fp.read()
 
 
 def expand_env_variables(line):
     # type: (AnyStr) -> AnyStr
     """Expand the env vars in a line following pip's standard.
-    https://pip.pypa.io/en/stable/reference/pip_install/#id10
+    https://pip.pypa.io/en/stable/reference/pip_install/#id10.
 
-    Matches environment variable-style values in '${MY_VARIABLE_1}' with the
-    variable name consisting of only uppercase letters, digits or the '_'
+    Matches environment variable-style values in '${MY_VARIABLE_1}' with
+    the variable name consisting of only uppercase letters, digits or
+    the '_'
     """
 
     def replace_with_env(match):
diff --git a/src/requirementslib/models/vcs.py b/src/requirementslib/models/vcs.py
index d4a2f160..18adb085 100644
--- a/src/requirementslib/models/vcs.py
+++ b/src/requirementslib/models/vcs.py
@@ -7,7 +7,6 @@
 
 import attr
 import pip_shims
-import six
 
 from ..environment import MYPY_RUNNING
 from .url import URI
@@ -124,13 +123,11 @@ def monkeypatch_pip(cls):
             # set the default to not write stdout, the first option sets this value
             new_defaults = [False] + list(run_command_defaults)[1:]
             new_defaults = tuple(new_defaults)
-        if six.PY3:
-            try:
-                pip_vcs.VersionControl.run_command.__defaults__ = new_defaults
-            except AttributeError:
-                pip_vcs.VersionControl.run_command.__func__.__defaults__ = new_defaults
-        else:
+        try:
+            pip_vcs.VersionControl.run_command.__defaults__ = new_defaults
+        except AttributeError:
             pip_vcs.VersionControl.run_command.__func__.__defaults__ = new_defaults
+
         sys.modules[target_module] = pip_vcs
         cls.DEFAULT_RUN_ARGS = new_defaults
         return new_defaults
diff --git a/src/requirementslib/utils.py b/src/requirementslib/utils.py
index b9db5d16..4023d014 100644
--- a/src/requirementslib/utils.py
+++ b/src/requirementslib/utils.py
@@ -4,37 +4,20 @@
 import logging
 import os
 import sys
+from collections.abc import ItemsView, Mapping, Sequence, Set
+from pathlib import Path
+from urllib.parse import urlparse, urlsplit, urlunparse
 
 import pip_shims.shims
-import six
-import six.moves
 import tomlkit
 import vistir
-from six.moves.urllib.parse import urlparse, urlsplit, urlunparse
-from vistir.compat import Path, fs_decode
+from vistir.compat import fs_decode
 from vistir.path import ensure_mkdir_p, is_valid_url
 
 from .environment import MYPY_RUNNING
 
-# fmt: off
-six.add_move(  # type: ignore
-    six.MovedAttribute("Mapping", "collections", "collections.abc")  # type: ignore
-)  # noqa  # isort:skip
-six.add_move(  # type: ignore
-    six.MovedAttribute("Sequence", "collections", "collections.abc")  # type: ignore
-)  # noqa  # isort:skip
-six.add_move(  # type: ignore
-    six.MovedAttribute("Set", "collections", "collections.abc")  # type: ignore
-)  # noqa  # isort:skip
-six.add_move(  # type: ignore
-    six.MovedAttribute("ItemsView", "collections", "collections.abc")  # type: ignore
-)  # noqa
-from six.moves import ItemsView, Mapping, Sequence, Set  # type: ignore  # noqa  # isort:skip
-# fmt: on
-
-
 if MYPY_RUNNING:
-    from typing import Dict, Any, Optional, Union, Tuple, List, Iterable, Text, TypeVar
+    from typing import Any, Dict, Iterable, List, Optional, Text, Tuple, TypeVar, Union
 
     STRING_TYPE = Union[bytes, str, Text]
     S = TypeVar("S", bytes, str, Text)
@@ -104,8 +87,8 @@ def is_installable_dir(path):
 
 def strip_ssh_from_git_uri(uri):
     # type: (S) -> S
-    """Return git+ssh:// formatted URI to git+git@ format"""
-    if isinstance(uri, six.string_types):
+    """Return git+ssh:// formatted URI to git+git@ format."""
+    if isinstance(uri, str):
         if "git+ssh://" in uri:
             parsed = urlparse(uri)
             # split the path on the first separating / so we can put the first segment
@@ -121,8 +104,8 @@ def strip_ssh_from_git_uri(uri):
 
 def add_ssh_scheme_to_git_uri(uri):
     # type: (S) -> S
-    """Cleans VCS uris from pip format"""
-    if isinstance(uri, six.string_types):
+    """Cleans VCS uris from pip format."""
+    if isinstance(uri, str):
         # Add scheme for parsing purposes, this is also what pip does
         if uri.startswith("git+") and "://" not in uri:
             uri = uri.replace("git+", "git+ssh://", 1)
@@ -140,7 +123,7 @@ def is_vcs(pipfile_entry):
     if isinstance(pipfile_entry, Mapping):
         return any(key for key in pipfile_entry.keys() if key in VCS_LIST)
 
-    elif isinstance(pipfile_entry, six.string_types):
+    elif isinstance(pipfile_entry, str):
         if not is_valid_url(pipfile_entry) and pipfile_entry.startswith("git+"):
             pipfile_entry = add_ssh_scheme_to_git_uri(pipfile_entry)
 
@@ -153,21 +136,21 @@ def is_editable(pipfile_entry):
     # type: (PipfileType) -> bool
     if isinstance(pipfile_entry, Mapping):
         return pipfile_entry.get("editable", False) is True
-    if isinstance(pipfile_entry, six.string_types):
+    if isinstance(pipfile_entry, str):
         return pipfile_entry.startswith("-e ")
     return False
 
 
 def is_star(val):
     # type: (PipfileType) -> bool
-    return (isinstance(val, six.string_types) and val == "*") or (
+    return (isinstance(val, str) and val == "*") or (
         isinstance(val, Mapping) and val.get("version", "") == "*"
     )
 
 
 def convert_entry_to_path(path):
     # type: (Dict[S, Union[S, bool, Tuple[S], List[S]]]) -> S
-    """Convert a pipfile entry to a string"""
+    """Convert a pipfile entry to a string."""
 
     if not isinstance(path, Mapping):
         raise TypeError("expecting a mapping, received {0!r}".format(path))
@@ -187,7 +170,7 @@ def convert_entry_to_path(path):
 
 def is_installable_file(path):
     # type: (PipfileType) -> bool
-    """Determine if a path can potentially be installed"""
+    """Determine if a path can potentially be installed."""
     from packaging import specifiers
 
     if isinstance(path, Mapping):
@@ -211,7 +194,7 @@ def is_installable_file(path):
         or (len(parsed.scheme) == 1 and os.name == "nt")
     )
     if parsed.scheme and parsed.scheme == "file":
-        path = vistir.compat.fs_decode(vistir.path.url_to_path(path))
+        path = fs_decode(vistir.path.url_to_path(path))
     normalized_path = vistir.path.normalize_path(path)
     if is_local and not os.path.exists(normalized_path):
         return False
@@ -230,9 +213,10 @@ def is_installable_file(path):
 
 
 def get_dist_metadata(dist):
-    import pkg_resources
     from email.parser import FeedParser
 
+    import pkg_resources
+
     if isinstance(dist, pkg_resources.DistInfoDistribution) and dist.has_metadata(
         "METADATA"
     ):
@@ -343,10 +327,8 @@ def _ensure_dir(path):
 
 
 class PathAccessError(KeyError, IndexError, TypeError):
-    """An amalgamation of KeyError, IndexError, and TypeError,
-    representing what can occur when looking up a path in a nested
-    object.
-    """
+    """An amalgamation of KeyError, IndexError, and TypeError, representing
+    what can occur when looking up a path in a nested object."""
 
     def __init__(self, exc, seg, path):
         self.exc = exc
@@ -368,6 +350,7 @@ def __str__(self):
 def get_path(root, path, default=_UNSET):
     """Retrieve a value from a nested object via a tuple representing the
     lookup path.
+
     >>> root = {'a': {'b': {'c': [[1], [2], [3]]}}}
     >>> get_path(root, ('a', 'b', 'c', 2, 0))
     3
@@ -391,7 +374,7 @@ def get_path(root, path, default=_UNSET):
        default: The value to be returned should any
           ``PathAccessError`` exceptions be raised.
     """
-    if isinstance(path, six.string_types):
+    if isinstance(path, str):
         path = path.split(".")
     cur = root
     try:
@@ -426,8 +409,12 @@ def default_visit(path, key, value):
 
 # Modified from https://github.com/mahmoud/boltons/blob/master/boltons/iterutils.py
 def dict_path_enter(path, key, value):
-    if isinstance(value, six.string_types):
+    if isinstance(value, str):
         return value, False
+    elif isinstance(value, (tomlkit.items.Table, tomlkit.items.InlineTable)):
+        return value.__class__(
+            tomlkit.container.Container(), value.trivia, False
+        ), ItemsView(value)
     elif isinstance(value, (Mapping, dict)):
         return value.__class__(), ItemsView(value)
     elif isinstance(value, tomlkit.items.Array):
@@ -479,16 +466,16 @@ def dict_path_exit(path, key, old_parent, new_parent, new_items):
 def remap(
     root, visit=default_visit, enter=dict_path_enter, exit=dict_path_exit, **kwargs
 ):
-    """The remap ("recursive map") function is used to traverse and
-    transform nested structures. Lists, tuples, sets, and dictionaries
-    are just a few of the data structures nested into heterogenous
-    tree-like structures that are so common in programming.
-    Unfortunately, Python's built-in ways to manipulate collections
-    are almost all flat. List comprehensions may be fast and succinct,
-    but they do not recurse, making it tedious to apply quick changes
-    or complex transforms to real-world data.
-    remap goes where list comprehensions cannot.
-    Here's an example of removing all Nones from some data:
+    """The remap ("recursive map") function is used to traverse and transform
+    nested structures. Lists, tuples, sets, and dictionaries are just a few of
+    the data structures nested into heterogenous tree-like structures that are
+    so common in programming. Unfortunately, Python's built-in ways to
+    manipulate collections are almost all flat. List comprehensions may be fast
+    and succinct, but they do not recurse, making it tedious to apply quick
+    changes or complex transforms to real-world data. remap goes where list
+    comprehensions cannot. Here's an example of removing all Nones from some
+    data:
+
     >>> from pprint import pprint
     >>> reviews = {'Star Trek': {'TNG': 10, 'DS9': 8.5, 'ENT': None},
     ...            'Babylon 5': 6, 'Dr. Who': None}
@@ -644,7 +631,7 @@ def remerge_enter(path, key, value):
 
         try:
             cur_val = get_path(ret, path + (key,))
-        except KeyError as ke:
+        except KeyError:
             pass
         else:
             new_parent = cur_val
diff --git a/tasks/CHANGELOG.rst.jinja2 b/tasks/CHANGELOG.rst.jinja2
index 8aff2057..7f6c3b6f 100644
--- a/tasks/CHANGELOG.rst.jinja2
+++ b/tasks/CHANGELOG.rst.jinja2
@@ -1,3 +1,14 @@
+{% 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 %}
 {% set underline = "-" %}
 {% if section %}
diff --git a/tasks/__init__.py b/tasks/__init__.py
index de6e8686..9f0dae73 100644
--- a/tasks/__init__.py
+++ b/tasks/__init__.py
@@ -60,9 +60,8 @@ def typecheck(ctx):
 
 @invoke.task()
 def clean(ctx):
-    """Clean previously built package artifacts.
-    """
-    ctx.run(f"python setup.py clean")
+    """Clean previously built package artifacts."""
+    ctx.run("python setup.py clean")
     dist = ROOT.joinpath("dist")
     build = ROOT.joinpath("build")
     print(f"[clean] Removing {dist} and {build}")
@@ -75,7 +74,6 @@ def clean(ctx):
 def _read_version():
     out = subprocess.check_output(["git", "tag"], encoding="ascii")
     versions = [line.strip() for line in out.splitlines() if line]
-    _unparsed = [v for v in versions if v.startswith("v")]
     try:
         version = max(parver.Version.parse(v.lstrip("v")).normalize() for v in versions)
     except ValueError:
@@ -95,8 +93,7 @@ def _write_version(v):
 
 
 def _render_log():
-    """Totally tap into Towncrier internals to get an in-memory result.
-    """
+    """Totally tap into Towncrier internals to get an in-memory result."""
     config = load_config(ROOT)
     definitions = config["types"]
     fragments, fragment_filenames = find_fragments(
@@ -232,16 +229,15 @@ def release(ctx, version=None, type_="patch", yes=False, dry_run=False):
 
 @invoke.task(pre=[clean])
 def full_release(ctx, type_, repo, prebump=PREBUMP, yes=False):
-    """Make a new release.
-    """
+    """Make a new release."""
     if prebump not in REL_TYPES:
         raise ValueError(f"{type_} not in {REL_TYPES}")
     prebump = REL_TYPES.index(prebump)
-    version = bump_version(ctx, type_, log=not dry_run)
+    version = bump_version(ctx, type_, log=True)
     # Needs to happen before Towncrier deletes fragment files.
     tag_release(version, yes=yes)
 
-    ctx.run(f"python setup.py sdist bdist_wheel")
+    ctx.run("python setup.py sdist bdist_wheel")
 
     dist_pattern = f'{PACKAGE_NAME.replace("-", "[-_]")}-*'
     artifacts = list(ROOT.joinpath("dist").glob(dist_pattern))
@@ -304,7 +300,7 @@ def log(task, message, level=LogLevel.INFO):
 
 @invoke.task
 def profile(ctx, filepath, calltree=False):
-    """ Run and profile a given Python script.
+    """Run and profile a given Python script.
 
     :param str filepath: The filepath of the script to profile
     """
diff --git a/tasks/news.py b/tasks/news.py
deleted file mode 100644
index 3e85eeba..00000000
--- a/tasks/news.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# -*- coding=utf-8 -*-
-import invoke
-import tomlkit
-import uuid
-from pathlib import Path
-
-
-def _get_git_root(ctx):
-    return Path(ctx.run('git rev-parse --show-toplevel', hide=True).stdout.strip())
-
-
-def _get_news_dir(ctx):
-    return _get_git_root(ctx) / 'news'
-
-
-def get_toml_file(ctx):
-    return _get_git_root(ctx) / 'pyproject.toml'
-
-
-def get_random():
-    return str(uuid.uuid4())[:8]
-
-
-@invoke.task
-def add(ctx, description, type_='feature', issue=None):
-    with get_toml_file(ctx).open() as f:
-        tf = tomlkit.parse(f.read_text())
-    allowed_types = tf.get('tool', {}).get('towncrier', {}).get('type', [])
-    if allowed_types:
-        allowed_types = [t.get('directory') for t in allowed_types]
-    if not allowed_types or type_ not in allowed_types:
-        print("[news.add] Invalid type or no types found: {0}".format(type_))
-        print("Valid news types: {0}".format(' '.join(allowed_types)))
-        return
-    target_dir = _get_news_dir(ctx)
-    existing_files = [f.suffix.split('-')[0] for f in target_dir.glob('{0}.*'.format(type_)) if f.suffix.split('-')[0].isdigit()]
-    if not existing_files and not issue:
-        nextfile = target_dir / '1-{0}.{1}'.format(get_random(), type_)
-    elif issue:
-        nextfile = target_dir / '{0}.{1}'.format(issue, type_)
-    else:
-        nextfile = '{0}-{1}.{2}'.format(int(sorted(existing_files).pop()) + 1, get_random(), type_)
-        nextfile = target_dir / nextfile
-    print("[news.add] Adding newsfile {0} => [{1}] {2}".format(nextfile.name, type_.upper(), description))
-    nextfile.write_text(description)
-
-
-@invoke.task
-def generate(ctx, draft=False):
-    print("[news.generate] Generating NEWS")
-
-    args = []
-    if draft:
-        args.append("--draft")
-
-    ctx.run("towncrier {}".format(" ".join(args)))
diff --git a/tasks/release.py b/tasks/release.py
deleted file mode 100644
index f18614fb..00000000
--- a/tasks/release.py
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-import invoke
-from parver import Version
-import re
-import sys
-from .vendoring import mkdir_p, drop_dir, remove_all, _get_git_root
-TASK_NAME = 'RELEASE'
-
-
-def find_version(version_path):
-    version_file = version_path.read_text()
-    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
-                              version_file, re.M)
-    if version_match:
-        return version_match.group(1)
-    raise RuntimeError("Unable to find version string.")
-
-
-def get_version_file(ctx):
-    version_path = _get_git_root(ctx) / 'src' / 'requirementslib' / '__init__.py'
-    return version_path
-
-
-def get_version(ctx):
-    version = find_version(get_version_file(ctx))
-    return version
-
-
-def log(msg):
-    print('[release] %s' % msg)
-
-
-def get_dist_dir(ctx):
-    return _get_git_root(ctx) / 'dist'
-
-
-def get_build_dir(ctx):
-    return _get_git_root(ctx) / 'build'
-
-
-def drop_dist_dirs(ctx):
-    log('Dropping Dist dir...')
-    drop_dir(get_dist_dir(ctx))
-    log('Dropping build dir...')
-    drop_dir(get_build_dir(ctx))
-
-
-@invoke.task
-def build_dists(ctx, drop_existing=True):
-    if drop_existing:
-        drop_dist_dirs(ctx)
-    log('Building sdist using %s ....' % sys.executable)
-    ctx.run('%s setup.py sdist' % sys.executable)
-    log('Building wheel using %s ....' % sys.executable)
-    ctx.run('%s setup.py bdist_wheel' % sys.executable)
-
-
-@invoke.task(build_dists)
-def upload_dists(ctx, build=False):
-    if build:
-        build_dists(ctx)
-    log('Uploading distributions to pypi...')
-    ctx.run('twine upload dist/*')
-
-
-@invoke.task
-def generate_changelog(ctx, commit=False):
-    log('Generating changelog...')
-    ctx.run('towncrier')
-    if commit:
-        log('Committing...')
-        ctx.run('git add .')
-        ctx.run('git commit -m "Update changelog."')
-
-
-@invoke.task
-def tag_version(ctx, push=False):
-    version = get_version(ctx)
-    log('Tagging revision: v%s' % version)
-    ctx.run('git tag v%s' % version)
-    if push:
-        log('Pushing tags...')
-        ctx.run('git push --tags')
-
-
-@invoke.task
-def bump_version(ctx, dry_run=False, major=False, minor=False, micro=True, dev=False, pre=False, post=False, tag=None, clear=False, commit=False,):
-    _current_version = get_version(ctx)
-    current_version = Version.parse(_current_version)
-    new_version = current_version
-    if pre and not tag and not clear:
-        print('Using "pre" requires a corresponding tag.')
-        return
-    if pre and dev and not clear:
-        print("Pre and dev cannot be used together.")
-        return
-    if not dev and not pre and not post:
-        new_version = new_version.clear(pre=True, dev=True, post=True)
-    elif dev:
-        new_version = new_version.bump_dev()
-    elif pre:
-        new_version = new_version.bump_pre(tag=tag)
-    if major:
-        new_version = new_version.bump_release(0)
-    elif minor:
-        new_version = new_version.bump_release(1)
-    elif micro:
-        new_version = new_version.bump_release(2)
-    elif post:
-        new_version = new_version.bump_post("post").bump_post("post")
-    if clear:
-        new_version = new_version.clear(dev=True, pre=True, post=True)
-    log('Updating version to %s' % new_version.normalize())
-    version_file = get_version_file(ctx)
-    file_contents = version_file.read_text()
-    log('Found current version: %s' % _current_version)
-    if dry_run:
-        log('Would update to: %s' % new_version.normalize())
-    else:
-        log('Updating to: %s' % new_version.normalize())
-        version_file.write_text(file_contents.replace(_current_version, str(new_version.normalize())))
-        if commit:
-            ctx.run('git add {0}'.format(get_version_file(ctx)))
-            log('Committing...')
-            ctx.run('git commit -s -m "Bumped version."')
-
-
-@invoke.task
-def build_docs(ctx):
-    _current_version = get_version(ctx)
-    minor = [str(i) for i in Version.parse(_current_version).release[:2]]
-    docs_folder = (_get_git_root(ctx) / 'docs').as_posix()
-    if not docs_folder.endswith('/'):
-        docs_folder = '{0}/'.format(docs_folder)
-    args = ["--ext-autodoc", "--ext-viewcode", "-o", docs_folder]
-    args.extend(["-A", "'Dan Ryan <dan@danryan.co>'"])
-    args.extend(["-R", _current_version])
-    args.extend(["-V", ".".join(minor)])
-    args.extend(["-e", "-M", "-F", "src/requirementslib"])
-    log("Building docs...")
-    ctx.run("sphinx-apidoc {0}".format(" ".join(args)))
diff --git a/tasks/vendoring/__init__.py b/tasks/vendoring/__init__.py
deleted file mode 100644
index 4b9a58bf..00000000
--- a/tasks/vendoring/__init__.py
+++ /dev/null
@@ -1,404 +0,0 @@
-# -*- coding=utf-8 -*-
-""""Vendoring script, python 3.5 needed"""
-# Taken from pip
-# see https://github.com/pypa/pip/blob/95bcf8c5f6394298035a7332c441868f3b0169f4/tasks/vendoring/__init__.py
-from pathlib import Path
-import tarfile
-import zipfile
-import os
-import re
-import shutil
-import invoke
-import requests
-
-TASK_NAME = 'update'
-
-LIBRARY_DIRNAMES = {
-    'requirements-parser': 'requirements',
-    'backports.shutil_get_terminal_size': 'backports/shutil_get_terminal_size',
-    'backports.weakref': 'backports/weakref',
-    'shutil_backports': 'backports/shutil_get_terminal_size',
-    'python-dotenv': 'dotenv',
-    'pip-tools': 'piptools',
-    'setuptools': 'pkg_resources',
-    'msgpack-python': 'msgpack',
-    'attrs': 'attr',
-}
-
-# from time to time, remove the no longer needed ones
-HARDCODED_LICENSE_URLS = {
-}
-
-FILE_WHITE_LIST = (
-    'Makefile',
-    'vendor.txt',
-    'requirements.txt',
-    '_vendor.txt',
-    'patched.txt',
-    '__init__.py',
-    'README.rst',
-    'README.md',
-)
-
-LIBRARY_RENAMES = {
-    'pip': 'pipenv.patched.notpip'
-}
-
-
-def mkdir_p(newdir):
-    """works the way a good mkdir should :)
-        - already exists, silently complete
-        - regular file in the way, raise an exception
-        - parent directory(ies) does not exist, make them as well
-        From: http://code.activestate.com/recipes/82465-a-friendly-mkdir/
-    """
-    if os.path.isdir(newdir):
-        pass
-    elif os.path.isfile(newdir):
-        raise OSError(
-            "a file with the same name as the desired dir, '{0}', already exists.".format(
-                newdir
-            )
-        )
-
-    else:
-        head, tail = os.path.split(newdir)
-        if head and not os.path.isdir(head):
-            mkdir_p(head)
-        if tail:
-            os.mkdir(newdir)
-
-
-def drop_dir(path):
-    if path.exists() and path.is_dir():
-        shutil.rmtree(str(path))
-
-
-def remove_all(paths):
-    for path in paths:
-        if path.is_dir():
-            drop_dir(path)
-        else:
-            path.unlink()
-
-
-def log(msg):
-    print('[vendoring.%s] %s' % (TASK_NAME, msg))
-
-
-def _get_git_root(ctx):
-    return Path(ctx.run('git rev-parse --show-toplevel', hide=True).stdout.strip())
-
-
-def _get_vendor_dir(ctx):
-    return _get_git_root(ctx) / 'src' / 'requirementslib' / '_vendor'
-
-
-def _find_requirements_file(ctx, vendor_dir):
-    options = ['{0}.txt'.format(vendor_dir.name), 'requirements.txt', 'vendor.txt', 'patched.txt']
-    return next((r for r in options if (vendor_dir / r).exists()), None)
-
-
-def clean_vendor(ctx, vendor_dir):
-    # Old _vendor cleanup
-    remove_all(vendor_dir.glob('*.pyc'))
-    log('Cleaning %s' % vendor_dir)
-    for item in vendor_dir.iterdir():
-        if item.is_dir():
-            shutil.rmtree(str(item))
-        elif "LICENSE" in item.name or "COPYING" in item.name:
-            continue
-        elif item.name not in FILE_WHITE_LIST:
-            item.unlink()
-        else:
-            log('Skipping %s' % item)
-
-
-def detect_vendored_libs(vendor_dir):
-    retval = []
-    for item in vendor_dir.iterdir():
-        if item.is_dir():
-            retval.append(item.name)
-        elif "LICENSE" in item.name or "COPYING" in item.name:
-            continue
-        elif item.name.endswith(".pyi"):
-            continue
-        elif item.name not in FILE_WHITE_LIST:
-            retval.append(item.name[:-3])
-    return retval
-
-
-def rewrite_imports(package_dir, vendored_libs, vendor_dir):
-    for item in package_dir.iterdir():
-        if item.is_dir():
-            rewrite_imports(item, vendored_libs, vendor_dir)
-        elif item.name.endswith('.py'):
-            rewrite_file_imports(item, vendored_libs, vendor_dir)
-
-
-def rewrite_file_imports(item, vendored_libs, vendor_dir):
-    """Rewrite 'import xxx' and 'from xxx import' for vendored_libs"""
-    text = item.read_text(encoding='utf-8')
-    renames = LIBRARY_RENAMES
-    for k in LIBRARY_RENAMES.keys():
-        if k not in vendored_libs:
-            vendored_libs.append(k)
-    for lib in vendored_libs:
-        to_lib = lib
-        if lib in renames:
-            to_lib = renames[lib]
-        text = re.sub(
-            r'([\n\s]*)import %s([\n\s\.]+)' % lib,
-            r'\1import %s\2' % to_lib,
-            text,
-        )
-        text = re.sub(
-            r'([\n\s]*)from %s([\s\.])+' % lib,
-            r'\1from %s\2' % to_lib,
-            text,
-        )
-        text = re.sub(
-            r"(\n\s*)__import__\('%s([\s'\.])+" % lib,
-            r"\1__import__('%s\2" % to_lib,
-            text,
-        )
-    item.write_text(text, encoding='utf-8')
-
-
-def apply_patch(ctx, patch_file_path):
-    log('Applying patch %s' % patch_file_path.name)
-    ctx.run('git apply --ignore-whitespace --verbose %s' % patch_file_path)
-
-
-def rename_if_needed(ctx, vendor_dir, item):
-    new_path = None
-    if item.name in LIBRARY_RENAMES or item.name in LIBRARY_DIRNAMES:
-        new_name = LIBRARY_RENAMES.get(item.name, LIBRARY_DIRNAMES.get(item.name))
-        new_path = item.parent / new_name
-        log('Renaming %s => %s' % (item.name, new_path))
-        # handle existing directories
-        try:
-            item.rename(str(new_path))
-        except OSError:
-            for child in item.iterdir():
-                child.rename(str(new_path / child.name))
-
-
-def write_backport_imports(ctx, vendor_dir):
-    backport_dir = vendor_dir / 'backports'
-    if not backport_dir.exists():
-        return
-    backport_init = backport_dir / '__init__.py'
-    backport_libs = detect_vendored_libs(backport_dir)
-    init_py_lines = backport_init.read_text().splitlines()
-    for lib in backport_libs:
-        lib_line = 'from . import {0}'.format(lib)
-        if lib_line not in init_py_lines:
-            log('Adding backport %s to __init__.py exports' % lib)
-            init_py_lines.append(lib_line)
-    backport_init.write_text('\n'.join(init_py_lines) + '\n')
-
-
-def vendor(ctx, vendor_dir, rewrite=True):
-    log('Reinstalling vendored libraries')
-    options = ['{0}.txt'.format(vendor_dir.name), 'requirements.txt', 'vendor.txt', 'patched.txt']
-    requirements_file = next((r for r in options if (vendor_dir / r).exists()), None)
-    if not requirements_file:
-        raise FileNotFoundError("No vendor file found!")
-    # We use --no-deps because we want to ensure that all of our dependencies
-    # are added to vendor.txt, this includes all dependencies recursively up
-    # the chain.
-    ctx.run(
-        'pip install -t {0} -r {0}/{1} --no-compile --no-deps'.format(
-            str(vendor_dir),
-            requirements_file,
-        )
-    )
-    remove_all(vendor_dir.glob('*.dist-info'))
-    remove_all(vendor_dir.glob('*.egg-info'))
-
-    # Cleanup setuptools unneeded parts
-    drop_dir(vendor_dir / 'bin')
-    drop_dir(vendor_dir / 'tests')
-
-    # Detect the vendored packages/modules
-    vendored_libs = detect_vendored_libs(_get_vendor_dir(ctx))
-    log("Detected vendored libraries: %s" % ", ".join(vendored_libs))
-
-    # Apply pre-patches
-    log("Applying pre-patches...")
-    patch_dir = Path(__file__).parent / 'patches'
-
-    # Global import rewrites
-    log('Renaming specified libs...')
-    for item in vendor_dir.iterdir():
-        if item.is_dir():
-            if rewrite:
-                log('Rewriting imports for %s...' % item)
-                rewrite_imports(item, vendored_libs, vendor_dir)
-            rename_if_needed(ctx, vendor_dir, item)
-        elif item.name not in FILE_WHITE_LIST:
-            if rewrite:
-                rewrite_file_imports(item, vendored_libs, vendor_dir)
-    write_backport_imports(ctx, vendor_dir)
-    log('Applying post-patches...')
-    patches = patch_dir.glob('*.patch')
-    for patch in patches:
-        apply_patch(ctx, patch)
-
-
-@invoke.task
-def rewrite_all_imports(ctx):
-    vendor_dir = _get_vendor_dir(ctx)
-    log('Using vendor dir: %s' % vendor_dir)
-    vendored_libs = detect_vendored_libs(vendor_dir)
-    log("Detected vendored libraries: %s" % ", ".join(vendored_libs))
-    log("Rewriting all imports related to vendored libs")
-    for item in vendor_dir.iterdir():
-        if item.is_dir():
-            rewrite_imports(item, vendored_libs)
-        elif item.name not in FILE_WHITE_LIST:
-            rewrite_file_imports(item, vendored_libs)
-
-
-@invoke.task
-def download_licenses(ctx, vendor_dir, requirements_file='vendor.txt'):
-    log('Downloading licenses')
-    if not vendor_dir:
-        vendor_dir = _get_vendor_dir(ctx)
-    requirements_file = _find_requirements_file(ctx, vendor_dir)
-    tmp_dir = vendor_dir / '__tmp__'
-    ctx.run(
-        'pip download -r {0}/{1} --no-binary :all: --no-deps -d {2}'.format(
-            str(vendor_dir),
-            requirements_file,
-            str(tmp_dir),
-        )
-    )
-    for sdist in tmp_dir.iterdir():
-        extract_license(vendor_dir, sdist)
-    drop_dir(tmp_dir)
-
-
-def extract_license(vendor_dir, sdist):
-    if sdist.stem.endswith('.tar'):
-        ext = sdist.suffix[1:]
-        with tarfile.open(sdist, mode='r:{}'.format(ext)) as tar:
-            found = find_and_extract_license(vendor_dir, tar, tar.getmembers())
-    elif sdist.suffix == '.zip':
-        with zipfile.ZipFile(sdist) as zip:
-            found = find_and_extract_license(vendor_dir, zip, zip.infolist())
-    else:
-        raise NotImplementedError('new sdist type!')
-
-    if not found:
-        log('License not found in {}, will download'.format(sdist.name))
-        license_fallback(vendor_dir, sdist.name)
-
-
-def find_and_extract_license(vendor_dir, tar, members):
-    found = False
-    for member in members:
-        try:
-            name = member.name
-        except AttributeError:  # zipfile
-            name = member.filename
-        if 'LICENSE' in name or 'COPYING' in name:
-            if '/test' in name:
-                # some testing licenses in hml5lib and distlib
-                log('Ignoring {}'.format(name))
-                continue
-            found = True
-            extract_license_member(vendor_dir, tar, member, name)
-    return found
-
-
-def license_fallback(vendor_dir, sdist_name):
-    """Hardcoded license URLs. Check when updating if those are still needed"""
-    libname = libname_from_dir(sdist_name)
-    if libname not in HARDCODED_LICENSE_URLS:
-        raise ValueError('No hardcoded URL for {} license'.format(libname))
-
-    url = HARDCODED_LICENSE_URLS[libname]
-    _, _, name = url.rpartition('/')
-    dest = license_destination(vendor_dir, libname, name)
-    r = requests.get(url, allow_redirects=True)
-    log('Downloading {}'.format(url))
-    r.raise_for_status()
-    dest.write_bytes(r.content)
-
-
-def libname_from_dir(dirname):
-    """Reconstruct the library name without it's version"""
-    parts = []
-    for part in dirname.split('-'):
-        if part[0].isdigit():
-            break
-        parts.append(part)
-    return '-'.join(parts)
-
-
-def license_destination(vendor_dir, libname, filename):
-    """Given the (reconstructed) library name, find appropriate destination"""
-    normal = vendor_dir / libname
-    if normal.is_dir():
-        return normal / filename
-    lowercase = vendor_dir / libname.lower()
-    if lowercase.is_dir():
-        return lowercase / filename
-    # Short circuit all logic if we are renaming the whole library
-    if libname in LIBRARY_RENAMES:
-        return vendor_dir / LIBRARY_RENAMES[libname] / filename
-    if libname in LIBRARY_DIRNAMES:
-        override = vendor_dir / LIBRARY_DIRNAMES[libname]
-        if not override.exists() and override.parent.exists():
-            # for flattened subdeps, specifically backports/weakref.py
-            return (
-                vendor_dir / override.parent
-            ) / '{0}.{1}'.format(override.name, filename)
-        return vendor_dir / LIBRARY_DIRNAMES[libname] / filename
-    # fallback to libname.LICENSE (used for nondirs)
-    return vendor_dir / '{}.{}'.format(libname, filename)
-
-
-def extract_license_member(vendor_dir, tar, member, name):
-    mpath = Path(name)  # relative path inside the sdist
-    dirname = list(mpath.parents)[-2].name  # -1 is .
-    libname = libname_from_dir(dirname)
-    dest = license_destination(vendor_dir, libname, mpath.name)
-    log('Extracting {} into {}'.format(name, dest))
-    try:
-        fileobj = tar.extractfile(member)
-        dest.write_bytes(fileobj.read())
-    except AttributeError:  # zipfile
-        dest.write_bytes(tar.read(member))
-
-
-@invoke.task()
-def generate_patch(ctx, package_path, patch_description, base='HEAD'):
-    vendor_dir = _get_vendor_dir(ctx)
-    pkg = vendor_dir / package_path
-    if not pkg.exists():
-        raise ValueError('example usage: generate-patch pew some-description')
-    if patch_description:
-        patch_fn = '{0}-{1}.patch'.format(pkg.parts[1], patch_description)
-    else:
-        patch_fn = '{0}.patch'.format(pkg.parts[1])
-    command = 'git diff {base} -p {root} > {out}'.format(
-        base=base,
-        root=Path('src').joinpath('requirementslib', '_vendor').as_posix(),
-        out=Path(__file__).parent.joinpath('patches', patch_fn).as_posix(),
-    )
-    with ctx.cd(str(_get_git_root(ctx))):
-        log(command)
-        ctx.run(command)
-
-
-@invoke.task(name=TASK_NAME)
-def main(ctx):
-    vendor_dir = _get_vendor_dir(ctx)
-    log('Using vendor dir: %s' % vendor_dir)
-    clean_vendor(ctx, vendor_dir)
-    vendor(ctx, vendor_dir)
-    download_licenses(ctx, vendor_dir)
-    log('Revendoring complete')
diff --git a/tasks/vendoring/patches/pipfile.patch b/tasks/vendoring/patches/pipfile.patch
deleted file mode 100644
index d1f305b2..00000000
--- a/tasks/vendoring/patches/pipfile.patch
+++ /dev/null
@@ -1,125 +0,0 @@
-diff --git a/src/requirementslib/_vendor/pipfile/api.py b/src/requirementslib/_vendor/pipfile/api.py
-index 8a2a6a3..18a1ea2 100644
---- a/src/requirementslib/_vendor/pipfile/api.py
-+++ b/src/requirementslib/_vendor/pipfile/api.py
-@@ -4,10 +4,18 @@ import codecs
- import json
- import hashlib
- import platform
-+import six
- import sys
- import os
- 
- 
-+DEFAULT_SOURCE = {
-+    u'url': u'https://pypi.org/simple',
-+    u'verify_ssl': True,
-+    u'name': u'pypi',
-+}
-+
-+
- def format_full_version(info):
-     version = '{0.major}.{0.minor}.{0.micro}'.format(info)
-     kind = info.releaselevel
-@@ -26,7 +34,7 @@ def walk_up(bottom):
-     # get files in current dir
-     try:
-         names = os.listdir(bottom)
--    except Exception as e:
-+    except Exception:
-         return
- 
-     dirs, nondirs = [], []
-@@ -62,14 +70,33 @@ class PipfileParser(object):
-     def __repr__(self):
-         return '<PipfileParser path={0!r}'.format(self.filename)
- 
--    def parse(self):
-+    def inject_environment_variables(self, d):
-+        """
-+        Recursively injects environment variables into TOML values
-+        """
-+
-+        if not d:
-+            return d
-+        if isinstance(d, six.string_types):
-+            return os.path.expandvars(d)
-+        for k, v in d.items():
-+            if isinstance(v, six.string_types):
-+                d[k] = os.path.expandvars(v)
-+            elif isinstance(v, dict):
-+                d[k] = self.inject_environment_variables(v)
-+            elif isinstance(v, list):
-+                d[k] = [self.inject_environment_variables(e) for e in v]
-+
-+        return d
-+
-+    def parse(self, inject_env=True):
-         # Open the Pipfile.
-         with open(self.filename) as f:
-             content = f.read()
- 
-         # Load the default configuration.
-         default_config = {
--            u'source': [{u'url': u'https://pypi.python.org/simple', u'verify_ssl': True}],
-+            u'source': [DEFAULT_SOURCE],
-             u'packages': {},
-             u'requires': {},
-             u'dev-packages': {}
-@@ -78,8 +105,16 @@ class PipfileParser(object):
-         config = {}
-         config.update(default_config)
- 
--        # Load the Pipfile's configuration.
--        config.update(toml.loads(content))
-+        # Deserialize the TOML, and parse for Environment Variables
-+        parsed = toml.loads(content)
-+
-+        if inject_env:
-+            injected_toml = self.inject_environment_variables(parsed)
-+
-+            # Load the Pipfile's configuration.
-+            config.update(injected_toml)
-+        else:
-+            config.update(parsed)
- 
-         # Structure the data for output.
-         data = {
-@@ -119,11 +154,11 @@ class Pipfile(object):
-         raise RuntimeError('No Pipfile found!')
- 
-     @classmethod
--    def load(klass, filename):
-+    def load(klass, filename, inject_env=True):
-         """Load a Pipfile from a given filename."""
-         p = PipfileParser(filename=filename)
-         pipfile = klass(filename=filename)
--        pipfile.data = p.parse()
-+        pipfile.data = p.parse(inject_env=inject_env)
-         return pipfile
- 
-     @property
-@@ -142,7 +177,7 @@ class Pipfile(object):
-         """Returns a JSON representation of the Pipfile."""
-         data = self.data
-         data['_meta']['hash'] = {"sha256": self.hash}
--        # return _json.dumps(data)
-+        data['_meta']['pipfile-spec'] = 6
-         return json.dumps(data, indent=4, separators=(',', ': '))
- 
-     def assert_requirements(self):
-@@ -184,7 +219,7 @@ class Pipfile(object):
-                     raise AssertionError('Specifier {!r} does not match {!r}.'.format(marker, specifier))
- 
- 
--def load(pipfile_path=None):
-+def load(pipfile_path=None, inject_env=True):
-     """Loads a pipfile from a given path.
-     If none is provided, one will try to be found.
-     """
-@@ -192,4 +227,4 @@ def load(pipfile_path=None):
-     if pipfile_path is None:
-         pipfile_path = Pipfile.find()
- 
--    return Pipfile.load(filename=pipfile_path)
-+    return Pipfile.load(filename=pipfile_path, inject_env=inject_env)


Test patch:
diff --git a/.azure-pipelines/templates/run-tests.yml b/.azure-pipelines/templates/run-tests.yml
deleted file mode 100644
index 469d3ac4..00000000
--- a/.azure-pipelines/templates/run-tests.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-steps:
-- task: UsePythonVersion@0
-  inputs:
-    versionSpec: $(python.version)
-  displayName: Use Python $(python.version)
-
-- script: git submodule sync && git submodule update --init --recursive
-  displayName: 'update submodules'
-
-- template: install-dependencies.yml
-
-- script: pipenv run coverage run --parallel -m pytest -ra --junitxml=junit/test-results.xml tests/
-  displayName: 'Run Pytest'
-
-- task: PublishTestResults@2
-  inputs:
-    testResultsFiles: '**/test-results.xml'
-    testRunTitle: '$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)'
-  condition: succeededOrFailed()
-  displayName: 'Publish Test Results'
diff --git a/tests/artifacts/environ_config/environ_config/setup.py b/tests/artifacts/environ_config/environ_config/setup.py
index e21117de..566c8bc2 100644
--- a/tests/artifacts/environ_config/environ_config/setup.py
+++ b/tests/artifacts/environ_config/environ_config/setup.py
@@ -27,9 +27,7 @@
     "Topic :: Software Development :: Libraries :: Python Modules",
 ]
 INSTALL_REQUIRES = ["attrs>=17.4.0"]
-EXTRAS_REQUIRE = {
-    "tests": ["pytest", "coverage"]
-}
+EXTRAS_REQUIRE = {"tests": ["pytest", "coverage"]}
 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"]
 
 ###############################################################################
@@ -38,9 +36,10 @@
 
 
 def read(*parts):
-    """
-    Build an absolute path from *parts* and and return the contents of the
-    resulting file.  Assume UTF-8 encoding.
+    """Build an absolute path from *parts* and and return the contents of the
+    resulting file.
+
+    Assume UTF-8 encoding.
     """
     with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f:
         return f.read()
@@ -59,12 +58,9 @@ def read(*parts):
 
 
 def find_meta(meta):
-    """
-    Extract __*meta*__ from META_FILE.
-    """
+    """Extract __*meta*__ from META_FILE."""
     meta_match = re.search(
-        r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta),
-        META_FILE, re.M
+        r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE, re.M
     )
     if meta_match:
         return meta_match.group(1)
@@ -74,14 +70,16 @@ def find_meta(meta):
 VERSION = find_meta("version")
 URI = find_meta("uri")
 LONG = (
-    read("README.rst") + "\n\n" +
-    "Release Information\n" +
-    "===================\n\n" +
-    re.search("(\\d+.\\d.\\d \\(.*?\\)\n.*?)(\n\n\n----\n)",
-              read("CHANGELOG.rst"), re.S).group(1) +
-    "\n\n`Full changelog " +
-    "<{uri}en/stable/changelog.html>`_.\n\n".format(uri=URI) +
-    read("AUTHORS.rst")
+    read("README.rst")
+    + "\n\n"
+    + "Release Information\n"
+    + "===================\n\n"
+    + re.search(
+        "(\\d+.\\d.\\d \\(.*?\\)\n.*?)(\n\n\n----\n)", read("CHANGELOG.rst"), re.S
+    ).group(1)
+    + "\n\n`Full changelog "
+    + "<{uri}en/stable/changelog.html>`_.\n\n".format(uri=URI)
+    + read("AUTHORS.rst")
 )
 
 
diff --git a/tests/artifacts/environ_config/environ_config/src/environ/_environ_config.py b/tests/artifacts/environ_config/environ_config/src/environ/_environ_config.py
index 4b1924a3..12c115d6 100644
--- a/tests/artifacts/environ_config/environ_config/src/environ/_environ_config.py
+++ b/tests/artifacts/environ_config/environ_config/src/environ/_environ_config.py
@@ -51,9 +51,7 @@ def var(default=RAISE, converter=None, name=None, validator=None):
 
 
 def _env_to_bool(val):
-    """
-    Convert *val* to a bool if it's not a bool in the first place.
-    """
+    """Convert *val* to a bool if it's not a bool in the first place."""
     if isinstance(val, bool):
         return val
     val = val.strip().lower()
@@ -72,10 +70,7 @@ def bool_var(default=RAISE, name=None):
 
 
 def group(cls):
-    return attr.ib(
-        default=None,
-        metadata={CNF_KEY: _ConfigEntry(None, None, cls, True)}
-    )
+    return attr.ib(default=None, metadata={CNF_KEY: _ConfigEntry(None, None, cls, True)})
 
 
 def to_config(config_cls, environ=os.environ):
@@ -112,8 +107,10 @@ def _to_config(config_cls, default_get, environ, prefix):
             val = get(environ, a.metadata, prefix, a.name)
         else:
             val = _to_config(
-                ce.sub_cls, default_get, environ,
-                prefix + ((a.name if prefix else a.name),)
+                ce.sub_cls,
+                default_get,
+                environ,
+                prefix + ((a.name if prefix else a.name),),
             )
 
         vals[a.name] = val
diff --git a/tests/artifacts/environ_config/environ_config/src/environ/exceptions.py b/tests/artifacts/environ_config/environ_config/src/environ/exceptions.py
index 7a9a60a8..cc887011 100644
--- a/tests/artifacts/environ_config/environ_config/src/environ/exceptions.py
+++ b/tests/artifacts/environ_config/environ_config/src/environ/exceptions.py
@@ -1,6 +1,4 @@
-"""
-Exceptions raised by environ_config.
-"""
+"""Exceptions raised by environ_config."""
 
 from __future__ import absolute_import, division, print_function
 
diff --git a/tests/artifacts/environ_config/environ_config/src/environ/secrets.py b/tests/artifacts/environ_config/environ_config/src/environ/secrets.py
index c6a9973c..4dc99627 100644
--- a/tests/artifacts/environ_config/environ_config/src/environ/secrets.py
+++ b/tests/artifacts/environ_config/environ_config/src/environ/secrets.py
@@ -11,9 +11,9 @@
 
 
 try:
-    from configparser import RawConfigParser, NoOptionError
+    from configparser import NoOptionError, RawConfigParser
 except ImportError:
-    from ConfigParser import RawConfigParser, NoOptionError
+    from ConfigParser import NoOptionError, RawConfigParser
 
 
 log = logging.getLogger(__name__)
@@ -32,9 +32,9 @@ def from_path(cls, path, section="secrets"):
 
     @classmethod
     def from_path_in_env(cls, env_name, default=None, section="secrets"):
-        """
-        Get and load path only when actually loading config.  Useful in tests
-        for setting up an environment.
+        """Get and load path only when actually loading config.
+
+        Useful in tests for setting up an environment.
         """
         return cls(section, None, env_name, default)
 
@@ -55,9 +55,7 @@ def _get(self, environ, metadata, prefix, name):
         # Delayed loading.
         if self._cfg is None and self._env_name is not None:
             log.debug("looking for env var '%s'." % (self._env_name,))
-            self._cfg = _load_ini(
-                environ.get(self._env_name, self._env_default)
-            )
+            self._cfg = _load_ini(environ.get(self._env_name, self._env_default))
 
         ce = metadata[CNF_KEY]
         ic = metadata[CNF_INI_SECRET_KEY]
@@ -68,7 +66,13 @@ def _get(self, environ, metadata, prefix, name):
         else:
             var = "_".join((prefix + (name,)))
         try:
-            log.debug("looking for '%s' in section '%s'." % (var, section,))
+            log.debug(
+                "looking for '%s' in section '%s'."
+                % (
+                    var,
+                    section,
+                )
+            )
             return _SecretStr(self._cfg.get(section, var))
         except NoOptionError:
             if ce.default is not RAISE:
@@ -78,9 +82,9 @@ def _get(self, environ, metadata, prefix, name):
 
 @attr.s
 class VaultEnvSecrets(object):
-    """
-    Almost identical to regular env vars except that it has its own prefix.
-    """
+    """Almost identical to regular env vars except that it has its own
+    prefix."""
+
     vault_prefix = attr.ib()
 
     def secret(self, default=RAISE, converter=None, name=None):
@@ -100,9 +104,7 @@ def _get(self, environ, metadata, prefix, name):
                 vp = self.vault_prefix(environ)
             else:
                 vp = self.vault_prefix
-            var = "_".join(
-                ((vp,) + prefix + (name,))
-            ).upper()
+            var = "_".join(((vp,) + prefix + (name,))).upper()
 
         log.debug("looking for env var '%s'." % (var,))
         val = environ.get(var, ce.default)
@@ -112,9 +114,8 @@ def _get(self, environ, metadata, prefix, name):
 
 
 class _SecretStr(str):
-    """
-    String that censors its __repr__ if called from an attrs repr.
-    """
+    """String that censors its __repr__ if called from an attrs repr."""
+
     def __repr__(self):
         f = sys._getframe(2)
 
@@ -133,9 +134,7 @@ class _INIConfig(object):
 
 
 def _load_ini(path):
-    """
-    Load an INI file from *path*.
-    """
+    """Load an INI file from *path*."""
     cfg = RawConfigParser()
     with codecs.open(path, mode="r", encoding="utf-8") as f:
         try:
diff --git a/tests/artifacts/environ_config/environ_config/tests/test_environ_config.py b/tests/artifacts/environ_config/environ_config/tests/test_environ_config.py
index be62719e..6bcf66e4 100644
--- a/tests/artifacts/environ_config/environ_config/tests/test_environ_config.py
+++ b/tests/artifacts/environ_config/environ_config/tests/test_environ_config.py
@@ -8,9 +8,8 @@
 
 @environ.config(prefix="XYZ")
 class Nested(object):
-    """
-    A nested configuration example.
-    """
+    """A nested configuration example."""
+
     @environ.config
     class Sub(object):
         y = environ.var()
@@ -21,9 +20,8 @@ class Sub(object):
 
 class TestEnvironConfig(object):
     def test_empty(self):
-        """
-        Empty config is accepted.
-        """
+        """Empty config is accepted."""
+
         @environ.config
         class Empty(object):
             pass
@@ -33,25 +31,26 @@ class Empty(object):
         assert "Empty()" == repr(cfg)
 
     def test_flat(self):
-        """
-        Flat config is extracted.
-        """
+        """Flat config is extracted."""
+
         @environ.config(prefix="APP")
         class Flat(object):
             x = environ.var()
             y = environ.var()
 
-        cfg = environ.to_config(Flat, environ={
-            "APP_X": "foo",
-            "APP_Y": "bar",
-        })
+        cfg = environ.to_config(
+            Flat,
+            environ={
+                "APP_X": "foo",
+                "APP_Y": "bar",
+            },
+        )
 
         assert Flat(x="foo", y="bar") == cfg
 
     def test_nested(self):
-        """
-        Nested config is extracted, prefix and vault_prefix are propagated.
-        """
+        """Nested config is extracted, prefix and vault_prefix are
+        propagated."""
         env = {
             "APP_X": "nope",
             "XYZ_X": "foo",
@@ -62,9 +61,9 @@ def test_nested(self):
         assert Nested(x="foo", sub=Nested.Sub(y="bar")) == cfg
 
     def test_missing(self):
-        """
-        If a var is missing, a human-readable MissingEnvValueError is raised.
-        """
+        """If a var is missing, a human-readable MissingEnvValueError is
+        raised."""
+
         @environ.config
         class Mandatory(object):
             x = environ.var()
@@ -75,56 +74,62 @@ class Mandatory(object):
         assert ("APP_X",) == e.value.args
 
     def test_default(self):
-        """
-        Default values are used iff the vars are missing.
-        """
+        """Default values are used iff the vars are missing."""
+
         @environ.config
         class Defaults(object):
             x = environ.var("foo")
             y = environ.var("qux")
 
-        cfg = environ.to_config(Defaults, environ={
-            "APP_Y": "bar",
-        })
+        cfg = environ.to_config(
+            Defaults,
+            environ={
+                "APP_Y": "bar",
+            },
+        )
 
         assert Defaults(x="foo", y="bar") == cfg
 
     @pytest.mark.parametrize("prefix", [None, ""])
     def test_no_prefix(self, prefix):
-        """
-        If prefix is None or "", don't add a leading _ when adding namespaces.
-        """
+        """If prefix is None or "", don't add a leading _ when adding
+        namespaces."""
+
         @environ.config(prefix=prefix)
         class Cfg(object):
             x = environ.var()
 
-        cfg = environ.to_config(Cfg, environ={
-            "X": "foo",
-        })
+        cfg = environ.to_config(
+            Cfg,
+            environ={
+                "X": "foo",
+            },
+        )
 
         assert Cfg("foo") == cfg
 
     def test_overwrite(self):
-        """
-        The env variable name can be overwritten.
-        """
+        """The env variable name can be overwritten."""
+
         @environ.config
         class Cfg(object):
             x = environ.var(name="LANG")
             y = environ.var()
 
-        cfg = environ.to_config(Cfg, environ={
-            "APP_X": "nope",
-            "LANG": "foo",
-            "APP_Y": "bar",
-        })
+        cfg = environ.to_config(
+            Cfg,
+            environ={
+                "APP_X": "nope",
+                "LANG": "foo",
+                "APP_Y": "bar",
+            },
+        )
 
         assert Cfg("foo", "bar") == cfg
 
     def test_no_prefixes(self):
-        """
-        If no prefixes are wished, nothing is prepended.
-        """
+        """If no prefixes are wished, nothing is prepended."""
+
         @environ.config(prefix=None)
         class Cfg(object):
             @environ.config
@@ -135,50 +140,56 @@ class Sub(object):
             y = environ.var()
             sub = environ.group(Sub)
 
-        cfg = environ.to_config(Cfg, environ={
-            "X": "x",
-            "Y": "y",
-            "SUB_Y": "sub_y",
-        })
+        cfg = environ.to_config(
+            Cfg,
+            environ={
+                "X": "x",
+                "Y": "y",
+                "SUB_Y": "sub_y",
+            },
+        )
 
         assert Cfg("x", "y", Cfg.Sub("sub_y")) == cfg
 
-    @pytest.mark.parametrize("val", [
-        "1", "trUe", "yEs  "
-    ])
+    @pytest.mark.parametrize("val", ["1", "trUe", "yEs  "])
     def test_bool_var(self, val):
-        """
-        Truthy strings are converted to True, everything else to False.
+        """Truthy strings are converted to True, everything else to False.
 
         Defaults can be passed as bools.
         """
+
         @environ.config
         class Cfg(object):
             t = environ.bool_var()
             f = environ.bool_var()
             d = environ.bool_var(True)
 
-        cfg = environ.to_config(Cfg, environ={
-            "APP_T": val,
-            "APP_F": "nope",
-        })
+        cfg = environ.to_config(
+            Cfg,
+            environ={
+                "APP_T": val,
+                "APP_F": "nope",
+            },
+        )
 
         assert cfg.t is True
         assert cfg.f is False
         assert cfg.d is True
 
     def test_tolerates_attribs(self):
-        """
-        Classes are allowed to have plain attr.ibs for e.g.
-        __attrs_post_init__.
-        """
+        """Classes are allowed to have plain attr.ibs for e.g.
+        __attrs_post_init__."""
+
         @environ.config
         class Cfg(object):
             e = environ.var()
             x = attr.ib(default=42)
 
-        cfg = environ.to_config(Cfg, environ={
-            "APP_E": "e",
-        })
+        cfg = environ.to_config(
+            Cfg,
+            environ={
+                "APP_E": "e",
+            },
+        )
 
         assert Cfg("e", 42) == cfg
diff --git a/tests/artifacts/environ_config/environ_config/tests/test_secrets.py b/tests/artifacts/environ_config/environ_config/tests/test_secrets.py
index cb459095..1e8e32b9 100644
--- a/tests/artifacts/environ_config/environ_config/tests/test_secrets.py
+++ b/tests/artifacts/environ_config/environ_config/tests/test_secrets.py
@@ -11,17 +11,14 @@
 
 class TestSecretStr:
     def test_secret_str_no_repr(self):
-        """
-        Outside of reprs, _SecretStr behaves normally.
-        """
+        """Outside of reprs, _SecretStr behaves normally."""
         s = _SecretStr("abc")
 
         assert "'abc'" == repr(s)
 
     def test_secret_str_censors(self):
-        """
-        _SecretStr censors it's __repr__ if its called from another __repr__.
-        """
+        """_SecretStr censors it's __repr__ if its called from another
+        __repr__."""
         s = _SecretStr("abc")
 
         @attr.s
@@ -34,7 +31,8 @@ class Cfg(object):
 @pytest.fixture
 def ini_file(tmpdir):
     f = tmpdir.join("foo.ini")
-    f.write("""\
+    f.write(
+        """\
 [secrets]
 password = foobar
 db_password = nested!
@@ -42,7 +40,8 @@ def ini_file(tmpdir):
 password = bar%foo
 [yet_another_section]
 secret = qux
-""")
+"""
+    )
     return f
 
 
@@ -53,9 +52,8 @@ def ini(ini_file):
 
 class TestIniSecret(object):
     def test_missing_default_raises(self, ini):
-        """
-        Missing values without a default raise an MissingSecretError.
-        """
+        """Missing values without a default raise an MissingSecretError."""
+
         @environ.config
         class Cfg(object):
             pw = ini.secret()
@@ -64,9 +62,8 @@ class Cfg(object):
             environ.to_config(Cfg, {})
 
     def test_default(self, ini):
-        """
-        Defaults are used iff the key is missing.
-        """
+        """Defaults are used iff the key is missing."""
+
         @environ.config
         class Cfg(object):
             password = ini.secret(default="not used")
@@ -77,9 +74,8 @@ class Cfg(object):
         assert Cfg("foobar", "used!") == cfg
 
     def test_name_overwrite(self, ini):
-        """
-        Passsing a specific key name is respected.
-        """
+        """Passsing a specific key name is respected."""
+
         @environ.config
         class Cfg(object):
             pw = ini.secret(name="password")
@@ -89,9 +85,7 @@ class Cfg(object):
         assert _SecretStr("foobar") == cfg.pw
 
     def test_overwrite_sections(self, ini):
-        """
-        The source section can be overwritten.
-        """
+        """The source section can be overwritten."""
         ini.section = "yet_another_section"
 
         @environ.config
@@ -104,9 +98,8 @@ class Cfg(object):
         assert _SecretStr("bar%foo") == cfg.password
 
     def test_nested(self, ini):
-        """
-        Prefix building works.
-        """
+        """Prefix building works."""
+
         @environ.config
         class Cfg(object):
             @environ.config
@@ -120,19 +113,15 @@ class DB(object):
         assert _SecretStr("nested!") == cfg.db.password
 
     def test_from_path_in_env_delayed(self, ini_file):
-        """
-        `from_path_in_env` prepares for loading but doesn't load until
-        `to_config` runs.
-        """
+        """`from_path_in_env` prepares for loading but doesn't load until
+        `to_config` runs."""
         secret = INISecrets.from_path_in_env("APP_SECRETS_INI").secret
 
         @environ.config
         class Cfg(object):
             password = secret()
 
-        cfg = environ.to_config(
-            Cfg, {"APP_SECRETS_INI": str(ini_file)}
-        )
+        cfg = environ.to_config(Cfg, {"APP_SECRETS_INI": str(ini_file)})
 
         assert "foobar" == cfg.password
 
@@ -144,9 +133,8 @@ def vault():
 
 class TestVaultEnvSecrets(object):
     def test_returns_secret_str(self, vault):
-        """
-        The returned strings are `_SecretStr`.
-        """
+        """The returned strings are `_SecretStr`."""
+
         @environ.config
         class Cfg(object):
             x = vault.secret()
@@ -157,24 +145,25 @@ class Cfg(object):
         assert "foo" == cfg.x
 
     def test_overwrite_name(self, vault):
-        """
-        The variable name can be overwritten.
-        """
+        """The variable name can be overwritten."""
+
         @environ.config
         class Cfg(object):
             password = vault.secret(name="not_password")
 
-        cfg = environ.to_config(Cfg, {
-            "SECRET_PASSWORD": "wrong",
-            "not_password": "correct",
-        })
+        cfg = environ.to_config(
+            Cfg,
+            {
+                "SECRET_PASSWORD": "wrong",
+                "not_password": "correct",
+            },
+        )
 
         assert "correct" == cfg.password
 
     def test_missing_raises_missing_secret(self, vault):
-        """
-        Missing values without a default raise an MissingSecretError.
-        """
+        """Missing values without a default raise an MissingSecretError."""
+
         @environ.config
         class Cfg(object):
             pw = vault.secret()
@@ -183,9 +172,7 @@ class Cfg(object):
             environ.to_config(Cfg, {})
 
     def test_prefix_callable(self):
-        """
-        vault_prefix can also be a callable that is called on each entry.
-        """
+        """vault_prefix can also be a callable that is called on each entry."""
         fake_environ = {"ABC_PW": "foo"}
 
         def extract(env):
diff --git a/tests/conftest.py b/tests/conftest.py
index 3131014a..5735f4c6 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,6 +5,7 @@
 import io
 import json
 import os
+import pathlib
 import random
 import shutil
 import warnings
@@ -15,7 +16,7 @@
 import requests
 import vistir
 
-CURRENT_FILE = vistir.compat.Path(__file__).absolute()
+CURRENT_FILE = pathlib.Path(__file__).absolute()
 
 
 def check_for_mercurial():
@@ -88,7 +89,7 @@ def pytest_runtest_setup(item):
 
 @pytest.fixture
 def pathlib_tmpdir(tmpdir):
-    yield vistir.compat.Path(str(tmpdir))
+    yield pathlib.Path(str(tmpdir))
     try:
         tmpdir.remove(ignore_errors=True)
     except Exception:
diff --git a/Pipfile b/tests/fixtures/lockfile/Pipfile
similarity index 100%
rename from Pipfile
rename to tests/fixtures/lockfile/Pipfile
diff --git a/Pipfile.lock b/tests/fixtures/lockfile/Pipfile.lock
similarity index 99%
rename from Pipfile.lock
rename to tests/fixtures/lockfile/Pipfile.lock
index 1c497235..e41859c9 100644
--- a/Pipfile.lock
+++ b/tests/fixtures/lockfile/Pipfile.lock
@@ -798,9 +798,7 @@
             "version": "==1.5.0.1"
         },
         "plette": {
-            "extras": [
-                "validation"
-            ],
+            "extras": ["validation"],
             "hashes": [
                 "sha256:46402c03e36d6eadddad2a5125990e322dd74f98160c8f2dcd832b2291858a26",
                 "sha256:d6c9b96981b347bddd333910b753b6091a2c1eb2ef85bb373b4a67c9d91dca16"
@@ -958,9 +956,7 @@
             "version": "==5.3.1"
         },
         "readme-renderer": {
-            "extras": [
-                "md"
-            ],
+            "extras": ["md"],
             "hashes": [
                 "sha256:cbe9db71defedd2428a1589cdc545f9bd98e59297449f69d721ef8f1cfced68d",
                 "sha256:cc4957a803106e820d05d14f71033092537a22daa4f406dfbdd61177e0936376"
@@ -1008,15 +1004,6 @@
             ],
             "version": "==0.9.1"
         },
-        "requirementslib": {
-            "editable": true,
-            "extras": [
-                "dev",
-                "tests",
-                "typing"
-            ],
-            "path": "."
-        },
         "retype": {
             "hashes": [
                 "sha256:7d033b115f66e5327dea0a3fd7c9a3dbfa53841575daf27ce2ce409956d901d4",
diff --git a/tests/fixtures/setup_cfg/package_with_multiple_extras/setup.py b/tests/fixtures/setup_cfg/package_with_multiple_extras/setup.py
index bdae619f..037ad54e 100644
--- a/tests/fixtures/setup_cfg/package_with_multiple_extras/setup.py
+++ b/tests/fixtures/setup_cfg/package_with_multiple_extras/setup.py
@@ -2,6 +2,7 @@
 
 from setuptools import find_packages, setup
 
+
 thisdir = os.path.abspath(os.path.dirname(__file__))
 version = "1.0.0"
 
diff --git a/tests/fixtures/setup_cfg/package_with_special_directives/bug/__init__.py b/tests/fixtures/setup_cfg/package_with_special_directives/bug/__init__.py
index b794fd40..3dc1f76b 100644
--- a/tests/fixtures/setup_cfg/package_with_special_directives/bug/__init__.py
+++ b/tests/fixtures/setup_cfg/package_with_special_directives/bug/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '0.1.0'
+__version__ = "0.1.0"
diff --git a/tests/fixtures/setup_cfg/package_with_special_directives/setup.py b/tests/fixtures/setup_cfg/package_with_special_directives/setup.py
index 60684932..b024da80 100644
--- a/tests/fixtures/setup_cfg/package_with_special_directives/setup.py
+++ b/tests/fixtures/setup_cfg/package_with_special_directives/setup.py
@@ -1,3 +1,4 @@
 from setuptools import setup
 
+
 setup()
diff --git a/tests/fixtures/setup_py/package_using_fully_qualified_setuptools/setup.py b/tests/fixtures/setup_py/package_using_fully_qualified_setuptools/setup.py
index e388b00d..bb075640 100644
--- a/tests/fixtures/setup_py/package_using_fully_qualified_setuptools/setup.py
+++ b/tests/fixtures/setup_py/package_using_fully_qualified_setuptools/setup.py
@@ -2,6 +2,7 @@
 
 import setuptools
 
+
 thisdir = os.path.abspath(os.path.dirname(__file__))
 version = "1.0.0"
 
diff --git a/tests/fixtures/setup_py/package_with_annoted_assignments/setup.py b/tests/fixtures/setup_py/package_with_annoted_assignments/setup.py
index aab93ccf..2cf3e41c 100644
--- a/tests/fixtures/setup_py/package_with_annoted_assignments/setup.py
+++ b/tests/fixtures/setup_py/package_with_annoted_assignments/setup.py
@@ -1,8 +1,10 @@
 import os
+
 from typing import Dict, List
 
 from setuptools import find_packages, setup
 
+
 extras_require: Dict[str, List[str]] = {
     "docs": ["sphinx", "sphinx-argparse"],
 }
diff --git a/tests/fixtures/setup_py/package_with_conditional_install_requires/setup.py b/tests/fixtures/setup_py/package_with_conditional_install_requires/setup.py
index 77a26da6..fd09c3d9 100644
--- a/tests/fixtures/setup_py/package_with_conditional_install_requires/setup.py
+++ b/tests/fixtures/setup_py/package_with_conditional_install_requires/setup.py
@@ -2,6 +2,7 @@
 
 from setuptools import setup
 
+
 try:
     from non_existant_pkg import cmdclass
 except ImportError:
diff --git a/tests/fixtures/setup_py/package_with_dependence_on_env_vars/setup.py b/tests/fixtures/setup_py/package_with_dependence_on_env_vars/setup.py
index 33e8ea60..7d4be8aa 100644
--- a/tests/fixtures/setup_py/package_with_dependence_on_env_vars/setup.py
+++ b/tests/fixtures/setup_py/package_with_dependence_on_env_vars/setup.py
@@ -2,6 +2,7 @@
 
 from setuptools import find_packages, setup
 
+
 extras_require = {
     "docs": ["sphinx", "sphinx-argparse"],
 }
diff --git a/tests/fixtures/setup_py/package_with_extras_as_variable/setup.py b/tests/fixtures/setup_py/package_with_extras_as_variable/setup.py
index 2b7e8233..8b8b8ec2 100644
--- a/tests/fixtures/setup_py/package_with_extras_as_variable/setup.py
+++ b/tests/fixtures/setup_py/package_with_extras_as_variable/setup.py
@@ -2,14 +2,17 @@
 
 from setuptools import find_packages, setup
 
+
 thisdir = os.path.abspath(os.path.dirname(__file__))
 version = "1.0.0"
 
 testing_extras = ["coverage"] + ["flaky"]
 
+
 def print_with_bin_ops():
     print("=" * 10)
 
+
 setup(
     name="test_package",
     version=version,
diff --git a/tests/fixtures/setup_py/package_with_multiple_extras/setup.py b/tests/fixtures/setup_py/package_with_multiple_extras/setup.py
index 702bb07d..87cd982e 100644
--- a/tests/fixtures/setup_py/package_with_multiple_extras/setup.py
+++ b/tests/fixtures/setup_py/package_with_multiple_extras/setup.py
@@ -2,6 +2,7 @@
 
 from setuptools import find_packages, setup
 
+
 thisdir = os.path.abspath(os.path.dirname(__file__))
 version = "1.0.0"
 
diff --git a/tests/fixtures/setup_py/package_with_one_extra/setup.py b/tests/fixtures/setup_py/package_with_one_extra/setup.py
index 21d53011..01bada69 100644
--- a/tests/fixtures/setup_py/package_with_one_extra/setup.py
+++ b/tests/fixtures/setup_py/package_with_one_extra/setup.py
@@ -2,6 +2,7 @@
 
 from setuptools import find_packages, setup
 
+
 thisdir = os.path.abspath(os.path.dirname(__file__))
 version = "1.0.0"
 
diff --git a/tests/fixtures/setup_py/package_with_repeated_assignments/setup.py b/tests/fixtures/setup_py/package_with_repeated_assignments/setup.py
index d979fdd1..11b09456 100644
--- a/tests/fixtures/setup_py/package_with_repeated_assignments/setup.py
+++ b/tests/fixtures/setup_py/package_with_repeated_assignments/setup.py
@@ -2,6 +2,7 @@
 
 from setuptools import find_packages, setup
 
+
 thisdir = os.path.abspath(os.path.dirname(__file__))
 version = os.environ["PACKAGE_VERSION"]
 
@@ -18,7 +19,9 @@ def my_function(other_list):
     version=version,
     description="Test package with repeated assignments and version from environment",
     long_description="This is a package",
-    install_requires=["six",],
+    install_requires=[
+        "six",
+    ],
     package_dir={"": "src"},
     packages=find_packages("src"),
     include_package_data=True,
diff --git a/tests/fixtures/setup_py/package_with_setup_from_dict/setup.py b/tests/fixtures/setup_py/package_with_setup_from_dict/setup.py
index 72a9bc6d..b1f6c848 100644
--- a/tests/fixtures/setup_py/package_with_setup_from_dict/setup.py
+++ b/tests/fixtures/setup_py/package_with_setup_from_dict/setup.py
@@ -1,5 +1,6 @@
 from setuptools import find_packages, setup
 
+
 SETUP = {
     "name": "test package",
     "version": "1.0.0",
diff --git a/tests/unit/strategies.py b/tests/unit/strategies.py
index 16da5eaa..07e9e01c 100644
--- a/tests/unit/strategies.py
+++ b/tests/unit/strategies.py
@@ -9,8 +9,10 @@
 
 import six
 import vistir
-from hypothesis import assume, strategies as st
-from hypothesis.provisional import URL_SAFE_CHARACTERS, domains, urls as url_strategy
+from hypothesis import assume
+from hypothesis import strategies as st
+from hypothesis.provisional import URL_SAFE_CHARACTERS, domains
+from hypothesis.provisional import urls as url_strategy
 from packaging.markers import MARKER_OP, VARIABLE
 from packaging.specifiers import Specifier
 from packaging.version import parse as parse_version
@@ -87,9 +89,7 @@ def valid_names():
 
 
 def urls():
-    """
-    Strategy for generating urls.
-    """
+    """Strategy for generating urls."""
 
     def url_encode(s):
         return "".join(c if c in URL_SAFE_CHARACTERS else "%%%02X" % ord(c) for c in s)
@@ -448,8 +448,10 @@ def repository_url(draw, elements=random_repositories()):
     repo_dict.update(
         {"ref": ref_str, "extras": extras_str, "subdir": subdir_str, "pkg_name": pkg_name}
     )
-    line = "{scheme}{base_url}{pathsep}{user}/{repo}.git{ref}#egg={pkg_name}{subdir}".format(
-        **repo_dict
+    line = (
+        "{scheme}{base_url}{pathsep}{user}/{repo}.git{ref}#egg={pkg_name}{subdir}".format(
+            **repo_dict
+        )
     )
     return line
 
diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py
index 17a95f92..1bc2e3d8 100644
--- a/tests/unit/test_exceptions.py
+++ b/tests/unit/test_exceptions.py
@@ -3,7 +3,6 @@
 
 from requirementslib.exceptions import (
     FileCorruptException,
-    FileExistsError,
     LockfileCorruptException,
     MissingParameter,
     PipfileCorruptException,
diff --git a/tests/unit/test_lockfile.py b/tests/unit/test_lockfile.py
index 39719c50..2d8ec0b1 100644
--- a/tests/unit/test_lockfile.py
+++ b/tests/unit/test_lockfile.py
@@ -12,10 +12,10 @@
 from requirementslib.models.requirements import Requirement
 
 
-def test_lockfile(tmpdir):
+def test_lockfile(tmpdir, fixture_dir):
     with temp_environ():
         os.environ["PIPENV_CACHE_DIR"] = tmpdir.strpath
-        lockfile = Lockfile.create(".")
+        lockfile = Lockfile.create(fixture_dir / "lockfile")
 
         requires = lockfile.as_requirements(dev=True)
         assert any(req.startswith("attrs") for req in requires)
@@ -102,9 +102,6 @@ def test_lockfile_requirements(pathlib_tmpdir):
     loaded = Lockfile.load(lockfile.as_posix())
     dump_to = pathlib_tmpdir.joinpath("new_lockfile")
     dump_to.mkdir()
-    from_data = Lockfile.from_data(
-        dump_to.as_posix(), json.loads(lockfile.read_text()), meta_from_project=False
-    )
     assert isinstance(loaded.dev_requirements[0], Requirement)
     assert isinstance(loaded.dev_requirements_list[0], dict)
     with cd(pathlib_tmpdir.as_posix()):
diff --git a/tests/unit/test_markers.py b/tests/unit/test_markers.py
index 43bbecf9..608f6873 100644
--- a/tests/unit/test_markers.py
+++ b/tests/unit/test_markers.py
@@ -30,7 +30,7 @@ def test_format_version(version_tuple, version_str):
         (Specifier("<=3.6"), Specifier("<3.7")),
         (">2.6", Specifier(">=2.7")),
         (Specifier(">2.6"), Specifier(">=2.7")),
-        (">1.1", Specifier(">=1.2"))
+        (">1.1", Specifier(">=1.2")),
     ],
 )
 def test_format_pyspec(specifier, rounded_specifier):
diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py
index 13dd7402..b903b67b 100644
--- a/tests/unit/test_metadata.py
+++ b/tests/unit/test_metadata.py
@@ -9,7 +9,12 @@
 
 
 @pytest.mark.parametrize(
-    "package_json", [{"name": "celery"}, {"name": "llvmlite"},], indirect=True,
+    "package_json",
+    [
+        {"name": "celery"},
+        {"name": "llvmlite"},
+    ],
+    indirect=True,
 )
 def test_metadata(monkeypatch_wheel_download, package_json):
     package = requirementslib.models.metadata.Package.from_json(package_json)
@@ -61,5 +66,3 @@ def test_metadata(monkeypatch_wheel_download, package_json):
             "vine==1.3.0",
             "zstandard",
         ]
-
-
diff --git a/tests/unit/test_pipfile.py b/tests/unit/test_pipfile.py
index 1f3cbf99..9741a24f 100644
--- a/tests/unit/test_pipfile.py
+++ b/tests/unit/test_pipfile.py
@@ -1,8 +1,8 @@
 # -*- coding: utf-8 -*-
 import itertools
+from pathlib import Path
 
 import pytest
-from vistir.compat import FileNotFoundError, Path
 
 from requirementslib.exceptions import RequirementError
 from requirementslib.models.lockfile import Lockfile
diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py
index 9a3dc8e1..e1913774 100644
--- a/tests/unit/test_project.py
+++ b/tests/unit/test_project.py
@@ -1,8 +1,8 @@
 # -*- coding: utf-8 -*-
+import pathlib
 from itertools import tee
 
 import pytest
-import vistir.compat
 
 import requirementslib.models.project
 
@@ -47,7 +47,7 @@ def test_project_file_works_if_file_exists_but_is_empty(pathlib_tmpdir):
 
 def test_dir_with_empty_pipfile_file_raises_exception(pathlib_tmpdir):
     project = None
-    with pytest.raises(vistir.compat.FileNotFoundError):
+    with pytest.raises(FileNotFoundError):
         project = requirementslib.models.project.Project(root=pathlib_tmpdir.as_posix())
     assert project is None
 
@@ -66,10 +66,10 @@ def test_dir_with_pipfile_creates_project(pathlib_tmpdir):
     pipfile.write_text(u"")
     project = requirementslib.models.project.Project(root=pathlib_tmpdir.as_posix())
     assert project.pipfile is not None
-    assert vistir.compat.Path(project.pipfile_location).as_posix() == pipfile.as_posix()
+    assert pathlib.Path(project.pipfile_location).as_posix() == pipfile.as_posix()
     assert project.lockfile is None
     assert (
-        vistir.compat.Path(project.lockfile_location).as_posix()
+        pathlib.Path(project.lockfile_location).as_posix()
         == pathlib_tmpdir.joinpath("Pipfile.lock").as_posix()
     )
     project.add_line_to_pipfile("requests[security]", False)
diff --git a/tests/unit/test_requirements.py b/tests/unit/test_requirements.py
index e00baf41..965d03ee 100644
--- a/tests/unit/test_requirements.py
+++ b/tests/unit/test_requirements.py
@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
 import os
+from pathlib import Path
 
 import pip_shims.shims
 import pytest
-from hypothesis import given, settings, strategies as st
-from vistir.compat import Path
+from hypothesis import given, settings
+from hypothesis import strategies as st
 from vistir.contextmanagers import temp_environ
 
 from requirementslib.exceptions import RequirementError
diff --git a/tests/unit/test_setup_info.py b/tests/unit/test_setup_info.py
index 4a3d96d3..86156ef0 100644
--- a/tests/unit/test_setup_info.py
+++ b/tests/unit/test_setup_info.py
@@ -2,11 +2,11 @@
 import ast
 import contextlib
 import os
+import pathlib
 import shutil
 import sys
 
 import pytest
-import six
 import vistir
 
 from requirementslib.models.requirements import Requirement
@@ -85,7 +85,6 @@ def find_metadata(path):
         return pth
 
     assert not find_metadata(base_dir)
-    assert not find_metadata(os.path.join(base_dir, "reqlib-metadata"))
     assert not find_metadata(os.path.join(base_dir, "src", "reqlib-metadata"))
     assert r.req.setup_info and os.path.isdir(r.req.setup_info.egg_base)
     setup_info = r.req.setup_info
@@ -106,8 +105,7 @@ def test_without_extras(pathlib_tmpdir):
     assert setup_dir.is_dir()
     setup_py = setup_dir.joinpath("setup.py")
     setup_py.write_text(
-        six.ensure_text(
-            """
+        """
 # -*- coding: utf-8 -*-
 from setuptools import setup
 
@@ -120,7 +118,6 @@ def test_without_extras(pathlib_tmpdir):
     }
 )
     """.strip()
-        )
     )
     setup_dict = None
     with vistir.contextmanagers.cd(setup_dir.as_posix()):
@@ -231,8 +228,6 @@ def test_ast_parser_handles_binops(setup_py_dir):
         "python-dateutil",
         "requests",
     ]
-    if six.PY2:
-        expected.append("futures")
     assert list(sorted(parsed["install_requires"])) == list(sorted(expected))
     assert analyzer.parse_setup_function() == parsed
 
@@ -266,7 +261,7 @@ def test_ast_parser_handles_repeated_assignments(setup_py_dir):
     parsed = ast_parse_setup_py(target)
     analyzer = ast_parse_file(target)
     assert parsed["name"] == "test_package_with_repeated_assignments"
-    assert isinstance(parsed["version"], str) is False
+    assert not isinstance(parsed["version"], str)
     assert parsed["install_requires"] == ["six"]
     analyzer_parsed = analyzer.parse_setup_function()
     # the versions in this instance are AST objects as they come from
@@ -279,10 +274,7 @@ def test_ast_parser_handles_repeated_assignments(setup_py_dir):
 
 def test_setup_cfg_parser(setup_cfg_dir):
     setup_path = setup_cfg_dir / "package_with_multiple_extras/setup.cfg"
-    if six.PY2:
-        contents = setup_path.read_bytes()
-    else:
-        contents = setup_path.read_text()
+    contents = setup_path.read_text()
     result = parse_setup_cfg(contents, setup_path.parent.as_posix())
     assert result["version"] == "0.5.0"
     assert result["name"] == "test_package"
@@ -290,10 +282,7 @@ def test_setup_cfg_parser(setup_cfg_dir):
 
 def test_parse_setup_cfg_with_special_directives(setup_cfg_dir):
     setup_path = setup_cfg_dir / "package_with_special_directives/setup.cfg"
-    if six.PY2:
-        contents = setup_path.read_bytes()
-    else:
-        contents = setup_path.read_text()
+    contents = setup_path.read_text()
     result = parse_setup_cfg(contents, setup_path.parent.as_posix())
     assert result["version"] == "0.1.0"
     assert result["name"] == "bug-test"
@@ -324,9 +313,7 @@ def modified_environ(**update):
                 "package_with_dependence_on_env_vars/setup.py"
             ).as_posix()
         )
-        assert list(sorted(parsed["install_requires"])) == list(
-            sorted(expected_install_requires)
-        )
+        assert sorted(parsed["install_requires"]) == sorted(expected_install_requires)
 
 
 def test_ast_parser_handles_exceptions(artifact_dir):
diff --git a/tests/unit/test_uri.py b/tests/unit/test_uri.py
index dbbc7f79..7a987129 100644
--- a/tests/unit/test_uri.py
+++ b/tests/unit/test_uri.py
@@ -3,9 +3,7 @@
 
 import os
 
-from hypothesis import assume, given, strategies as st
-from six.moves.urllib_parse import quote_plus, urlsplit, urlunsplit
-from vistir.compat import Path
+from hypothesis import assume, given
 
 from requirementslib.models.url import URI
 
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 35f434de..9fb4ce78 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -1,8 +1,9 @@
 # -*- coding=utf-8 -*-
 import os
+from pathlib import Path
+
 import pip_shims.shims
 import pytest
-from vistir.compat import Path
 from vistir.contextmanagers import temp_environ
 
 from requirementslib import utils as base_utils