diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 16bbaa3..0000000
--- a/.coveragerc
+++ /dev/null
@@ -1,2 +0,0 @@
-[run]
-source = hamcrest
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..04de374
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,142 @@
+---
+name: CI
+
+on:
+  push:
+    branches: ["main", "master", "ci-testing-*"]
+
+  pull_request:
+    branches: ["main", "master"]
+
+  workflow_dispatch:
+
+jobs:
+  tests:
+    name: "Python ${{ matrix.python-version }} / ${{ matrix.os }}"
+    runs-on: "${{ matrix.os }}"
+    env:
+      USING_COVERAGE: "3.7,3.8,3.9,3.10"
+
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version:
+          - "3.6"
+          - "3.7"
+          - "3.8"
+          - "3.9"
+          - "3.10.0"
+          - "pypy2"
+          - "pypy3"
+        exclude:
+          - os: macos-latest
+            python-version: pypy3
+
+    steps:
+      - uses: "actions/checkout@v2"
+      - uses: "actions/setup-python@v2"
+        with:
+          python-version: "${{ matrix.python-version }}"
+      - name: "Install dependencies"
+        run: |
+          python -VV
+          python -msite
+          python -m pip install --upgrade pip setuptools wheel
+          python -m pip install --upgrade coverage[toml] virtualenv tox tox-gh-actions
+
+      - name: "Run tox targets for ${{ matrix.python-version }}"
+        run: "python -m tox"
+
+      - name: Upload coverage data
+        uses: actions/upload-artifact@v2
+        with:
+          name: coverage-data
+          path: ".coverage.*"
+          if-no-files-found: ignore
+
+  coverage:
+    needs:
+      - tests
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: "3.10"
+
+      - name: Install coverage
+        run: python -m pip install --upgrade coverage[toml]
+
+      - name: Download coverage data
+        uses: actions/download-artifact@v2
+        with:
+          name: coverage-data
+
+      - name: Combine coverage
+        run: python -m coverage combine
+
+        # ignore-errors is so that we don't gag on missing code in .tox environments
+      - name: Generate the HTML report
+        run: python -m coverage html --skip-covered --skip-empty --ignore-errors
+
+      - name: Upload the HTML report
+        uses: actions/upload-artifact@v2
+        with:
+          name: html-report
+          path: htmlcov
+
+        # ignore-errors is so that we don't gag on missing code in .tox environments
+      - name: Enforce the coverage
+        run: python -m coverage report --ignore-errors --fail-under 95
+
+  package:
+    name: "Build & verify package"
+    runs-on: "ubuntu-latest"
+
+    steps:
+      - uses: "actions/checkout@v2"
+      - uses: "actions/setup-python@v1"
+        with:
+          python-version: "3.10"
+
+      - name: Check if we have the publish key
+        env:
+          TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }}
+        if: "${{ env.TEST_PYPI_API_TOKEN != '' }}"
+        run: |
+          echo "DO_PUBLISH=yes" >> $GITHUB_ENV
+
+      - name: "Install pep517 and twine"
+        run: "python -m pip install pep517 twine"
+      - name: "Build package"
+        run: "python -m pep517.build --source --binary ."
+      - name: "List result"
+        run: "ls -l dist"
+      - name: "Check long_description"
+        run: "python -m twine check dist/*"
+      - name: "Publish package to TestPyPI"
+        uses: "pypa/gh-action-pypi-publish@release/v1"
+        if: "${{ env.DO_PUBLISH == 'yes' }}"
+        with:
+          user: "__token__"
+          password: "${{ secrets.TEST_PYPI_API_TOKEN }}"
+          repository_url: "https://test.pypi.org/legacy/"
+          skip_existing: true
+
+  install-dev:
+    strategy:
+      matrix:
+        os: ["ubuntu-latest", "windows-latest", "macos-latest"]
+
+    name: "Verify dev env / ${{ matrix.os }}"
+    runs-on: "${{ matrix.os }}"
+
+    steps:
+      - uses: "actions/checkout@v2"
+      - uses: "actions/setup-python@v2"
+        with:
+          python-version: "3.9"
+      - name: "Install in dev mode"
+        run: "python -m pip install -e .[dev]"
+      - name: "Import package"
+        run: "python -c 'import hamcrest; print(hamcrest.__version__)'"
diff --git a/.gitignore b/.gitignore
index 9f4bb09..ceb92ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,10 @@ PyHamcrest.egg-info/
 build/
 dist/
 .tox
-.coverage
+.coverage*
 .idea/
 *~
 .python-version
 .mypy_cache/
+requirements.txt
+requirements.in
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..5af67ff
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,32 @@
+repos:
+  - repo: meta
+    hooks:
+      - id: check-hooks-apply
+      - id: check-useless-excludes
+
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.0.1
+    hooks:
+      - id: debug-statements
+
+  - repo: https://github.com/asottile/blacken-docs
+    rev: v1.12.0
+    hooks:
+      - id: blacken-docs
+        # args: ["-l100"]
+
+  - repo: https://github.com/PyCQA/flake8
+    rev: 4.0.1
+    hooks:
+      - id: flake8
+        exclude: >-
+          (?x)^(
+            examples/.*\.py$
+            | doc/.*\.py$
+          )
+
+  - repo: https://github.com/psf/black
+    rev: 21.12b0
+    hooks:
+      - id: black
+        # args: ["-l100"]
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..511ae16
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,11 @@
+---
+version: 2
+python:
+  # Keep version in sync with tox.ini (docs and gh-actions).
+  version: 3.7
+
+  install:
+    - method: pip
+      path: .
+      extra_requirements:
+        - docs
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b9dd3b7..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,62 +0,0 @@
-language: python
-dist: xenial
-
-matrix:
-  include:
-    - python: 3.5
-      env:
-        - TOX_ENV=py35
-    - python: 3.6
-      env:
-        - TOX_ENV=py36
-    - python: 3.6
-      env:
-        - TOX_ENV=pypy3.6
-    - python: 3.6
-      env:
-        - TOX_ENV=py36-numpy
-    - python: 3.7
-      sudo: yes
-      env:
-        - TOX_ENV=py37
-    - python: 3.8
-      sudo: yes
-      env:
-        - TOX_ENV=py38
-    - os: osx
-      language: generic
-      python: 3.7
-      env:
-        - TOX_ENV=py37
-    - os: windows
-      language: sh
-      python: 3.7
-      before_install:
-        - choco install python --version=3.7.5
-        - export PATH="/c/Python37:/c/Python37/Scripts:$PATH"
-        - python -m pip install --upgrade pip wheel
-      env:
-        - TOX_ENV=py37
-    - python: 3.6
-      env:
-        - TOX_ENV=docs-py3
-    - python: 3.7
-      env:
-        - TOX_ENV=check-format
-    - python: 3.8
-      env:
-        - TOX_ENV=mypy
-
-before_install:
-  - export EASY_SETUP_URL='http://peak.telecommunity.com/dist/ez_setup.py'
-
-install:
-  - pip install --upgrade pip tox coveralls
-  - python --version
-  - pip --version
-
-script:
-  - tox -e $TOX_ENV
-
-after_success:
-  - coveralls
diff --git a/CHANGES.txt b/CHANGELOG.rst
similarity index 79%
rename from CHANGES.txt
rename to CHANGELOG.rst
index bddf874..b7c9c30 100644
--- a/CHANGES.txt
+++ b/CHANGELOG.rst
@@ -1,12 +1,60 @@
-=== Version 2.0.2 ===
+2.0.3 (2021-12-12)
+------------------
+ 
+  Features ^^^^^^^^
+
+  - * Adds the tests to the sdist. Fixed by #150
+
+`#141 <https://github.com/hamcrest/PyHamcrest/issues/141>`_
+ - * Update the CI to test Python 3.10
+
+`#160 <https://github.com/hamcrest/PyHamcrest/issues/160>`_
+ - * Add pretty string representation for matchers objects
+
+`#170 <https://github.com/hamcrest/PyHamcrest/issues/170>`_
+
+  
+ Bugfixes ^^^^^^^^
+
+  - * Test coverage is now submitted to codecov.io.
+
+    Fixed by #150
+
+`#135 <https://github.com/hamcrest/PyHamcrest/issues/135>`_
+ - Change to the ``has_entry()`` matcher - if exactly one key matches, but the value does not, report only the mismatching
+  value.
+
+  Fixed by #157
+
+`#156 <https://github.com/hamcrest/PyHamcrest/issues/156>`_
+ - * Fix is_() type annotations
+
+`#180 <https://github.com/hamcrest/PyHamcrest/issues/180>`_
+
+  
+ Misc ^^^^
+
+ - `#150 <https://github.com/hamcrest/PyHamcrest/issues/150>`_, `#159 <https://github.com/hamcrest/PyHamcrest/issues/159>`_, `#162 <https://github.com/hamcrest/PyHamcrest/issues/162>`_, `#163 <https://github.com/hamcrest/PyHamcrest/issues/163>`_, `#166 <https://github.com/hamcrest/PyHamcrest/issues/166>`_, `#175 <https://github.com/hamcrest/PyHamcrest/issues/175>`_
+
+  
+   ----
+
+
+Changelog
+=========
+
+Version 2.0.2
+-------------
 
 Various type hint bug fixes.
 
-=== Version 2.0.1 ===
+Version 2.0.1
+-------------
 
 * Make hamcrest package PEP 561 compatible, i.e. supply type hints for external use.
 
-=== Version 2.0.0 ==
+Version 2.0.0
+-------------
 
 Drop formal support for 2.x
 Drop formal support for 3.x < 3.5
@@ -18,7 +66,8 @@ Fix #128 - raises() grows support for additional matchers on exception object.
 * Type fixes.
 * Remove obsolete dependencies.
 
-=== Version 1.10.1 ==
+Version 1.10.1
+--------------
 
 Add support up to Python 3.8
 
@@ -26,36 +75,43 @@ Fix #66 - deprecate contains() in favour of contains_exactly().
 Fix #72 - make has_properties mismatch description less verbose by adding option to AllOf not to include matcher description in its mismatch messages.
 Fix #82 - include exception details in mismatch description.
 
-=== Version 1.9.0 ==
+Version 1.9.0
+-------------
 
 Drop formal support for 2.x < 2.7
 Drop formal support for 3.x < 3.4
 
 Fix #62 - Return result of a deferred call
 
-=== Version 1.8.5 ===
+Version 1.8.5
+-------------
 
 Fix #56 - incorrect handling of () in is_ matcher
 Fix #60 - correct calling API call with args
 
-=== Version 1.8.4 ==
+Version 1.8.4
+-------------
 
 * Fix #54 - Make instance_of work with tuple like isinstance and unittest's assertIsInstance
 
-=== Version 1.8.3 ===
+Version 1.8.3
+-------------
 
 * Fix #52 - bad handling when reporting mismatches for byte arrays in Python 3
 
-=== Version 1.8.2 ===
+Version 1.8.2
+-------------
 
 * [Bug] Fix unicode syntax via u() introduction (puppsman)
 
-=== Version 1.8.1 ===
+Version 1.8.1
+-------------
 
 * Added not_ alias for is_not [Matteo Bertini]
 * Added doc directory to the sdist [Alex Brandt]
 
-=== Version 1.8 ==
+Version 1.8
+-----------
 
 * Supported versions
  - Support for Python 2.5 and Jython 2.5 has been dropped. They may still work, but no promises.
@@ -67,7 +123,8 @@ Fix #60 - correct calling API call with args
  - Support for numpy numeric values in iscloseto (Alexander Beedie)
  - A matcher targeting exceptions and call results (Per Fagrell)
 
-=== Version 1.7 ==
+Version 1.7
+-----------
 
 2 Sep 2013 (Version 1.7.2)
 * Supported versions
@@ -100,7 +157,8 @@ Fix #60 - correct calling API call with args
  - README enhancements by ming13
 
 
-=== Version 1.6 ==
+Version 1.6
+-----------
 
 27 Sep 2011
 (All changes by Chris Rose unless otherwise noted.)
@@ -119,7 +177,8 @@ Fix #60 - correct calling API call with args
  - Rewrote documentation. (Jon Reid)
 
 
-== Version 1.5 ==
+Version 1.5
+-----------
 
 29 Apr 2011
 * Packaging:
@@ -138,7 +197,8 @@ Fix #60 - correct calling API call with args
  None.
 
 
-== Version 1.4 ==
+Version 1.4
+-----------
 
 13 Feb 2011
 * New matchers:
@@ -152,7 +212,8 @@ Fix #60 - correct calling API call with args
  - Consistently use articles to begin descriptions, such as "a sequence containing" instead of "sequence containing".
 
 
-== Version 1.3 ==
+Version 1.3
+-----------
 
 04 Feb 2011
 * PyHamcrest is now compatible with Python 3! To install PyHamcrest on Python 3:
@@ -171,7 +232,8 @@ Fix #60 - correct calling API call with args
   - Improved readability of several matchers.
 
 
-== Version 1.2.1 ==
+Version 1.2.1
+-------------
 
 04 Jan 2011
 * Fixed "assert_that" to describe the diagnosis of the mismatch, not just the
@@ -188,7 +250,8 @@ mismatched value. PyHamcrest will now give even more useful information.
 - Corrected manifest so install works. Thanks to: Jeong-Min Lee
 
 
-== Version 1.1 ==
+Version 1.1
+-----------
 
 28 Dec 2010
 * New matchers:
@@ -198,7 +261,8 @@ mismatched value. PyHamcrest will now give even more useful information.
 * Added Sphinx documentation support.
 
 
-== Version 1.0 ==
+Version 1.0
+-----------
 
 04 Dec 2010
 * First official release
diff --git a/MANIFEST.in b/MANIFEST.in
index 3f81fd2..4448559 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,26 @@
-include CHANGES.txt
-include LICENSE.txt
-include README.md
+include LICENSE.txt *.rst *.md *.toml *.yml *.yaml *.ini
+include requirements*
+graft .github
+
+# Tests
+include tox.ini conftest.py
+recursive-include tests *.py
+recursive-include tests *.yml
+
+# Documentation
+include doc/Makefile doc/docutils.conf
 recursive-include examples *.py
-recursive-include doc *
+recursive-include doc *.png
+recursive-include doc *.svg
+recursive-include doc *.py
+recursive-include doc *.rst
+prune doc/_build
+
+# remove some of the random source
+prune docker
+exclude release.sh
+
+# Just to keep check-manifest happy; on releases those files are gone.
+# Last rule wins!
+exclude changelog.d/*.rst
+include changelog.d/towncrier_template.rst
diff --git a/README.rst b/README.rst
index 7bae39e..9be6c1b 100644
--- a/README.rst
+++ b/README.rst
@@ -1,30 +1,15 @@
 PyHamcrest
 ==========
 
-| |docs| |travis| |coveralls| |landscape| |scrutinizer|
-| |version| |downloads| |wheel| |supported-versions| |supported-implementations|
-| |GitHub forks| |GitHub stars| |GitHub watchers| |GitHub contributors| |Lines of Code|
-| |GitHub issues| |GitHub issues-closed| |GitHub pull-requests| |GitHub pull-requests closed|
+| |docs| |status| |version| |downloads|
 
-.. |docs| image:: https://readthedocs.org/projects/pyhamcrest/badge/
-    :target: https://pyhamcrest.readthedocs.org/
+.. |docs| image:: https://readthedocs.org/projects/pyhamcrest/badge/?version=latest
+    :target: https://pyhamcrest.readthedocs.io/en/latest/?badge=latest
     :alt: Documentation Status
 
-.. |travis| image:: http://img.shields.io/travis/hamcrest/PyHamcrest/master.svg
-    :alt: Travis-CI Build Status
-    :target: https://travis-ci.org/hamcrest/PyHamcrest
-
-.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/hamcrest/PyHamcrest?branch=master&svg=true
-    :alt: AppVeyor Build Status
-    :target: https://ci.appveyor.com/project/hamcrest/PyHamcrest
-
-.. |coveralls| image:: http://img.shields.io/coveralls/hamcrest/PyHamcrest/master.svg?style=flat
-    :alt: Coverage Status
-    :target: https://coveralls.io/r/hamcrest/PyHamcrest
-
-.. |landscape| image:: https://landscape.io/github/hamcrest/PyHamcrest/master/landscape.svg?style=flat
-    :target: https://landscape.io/github/hamcrest/PyHamcrest/master
-    :alt: Code Quality Status
+.. |status| image:: https://github.com/hamcrest/PyHamcrest/workflows/CI/badge.svg
+    :alt: CI Build Status
+    :target: https://github.com/hamcrest/PyHamcrest/actions?query=workflow%3ACI
 
 .. |version| image:: http://img.shields.io/pypi/v/PyHamcrest.svg?style=flat
     :alt: PyPI Package latest release
@@ -34,58 +19,6 @@ PyHamcrest
     :alt: PyPI Package monthly downloads
     :target: https://pypi.python.org/pypi/PyHamcrest
 
-.. |wheel| image:: https://pypip.in/wheel/PyHamcrest/badge.svg?style=flat
-    :alt: PyPI Wheel
-    :target: https://pypi.python.org/pypi/PyHamcrest
-
-.. |supported-versions| image:: https://pypip.in/py_versions/PyHamcrest/badge.svg?style=flat
-    :alt: Supported versions
-    :target: https://pypi.python.org/pypi/PyHamcrest
-
-.. |GitHub forks| image:: https://img.shields.io/github/forks/hamcrest/PyHamcrest.svg?label=Fork&logo=github
-    :alt: GitHub forks
-    :target: https://github.com/hamcrest/PyHamcrest/network/members
-
-.. |GitHub stars| image:: https://img.shields.io/github/stars/hamcrest/PyHamcrest.svg?label=Star&logo=github
-    :alt: GitHub stars
-    :target: https://github.com/hamcrest/PyHamcrest/stargazers/
-
-.. |GitHub watchers| image:: https://img.shields.io/github/watchers/hamcrest/PyHamcrest.svg?label=Watch&logo=github
-    :alt: GitHub watchers
-    :target: https://github.com/hamcrest/PyHamcrest/watchers/
-
-.. |GitHub contributors| image:: https://img.shields.io/github/contributors/hamcrest/PyHamcrest.svg?logo=github
-    :alt: GitHub contributors
-    :target: https://github.com/hamcrest/PyHamcrest/graphs/contributors/
-
-.. |GitHub issues| image:: https://img.shields.io/github/issues/hamcrest/PyHamcrest.svg?logo=github
-    :alt: GitHub issues
-    :target: https://github.com/hamcrest/PyHamcrest/issues/
-
-.. |GitHub issues-closed| image:: https://img.shields.io/github/issues-closed/hamcrest/PyHamcrest.svg?logo=github
-    :alt: GitHub issues-closed
-    :target: https://github.com/hamcrest/PyHamcrest/issues?q=is%3Aissue+is%3Aclosed
-
-.. |GitHub pull-requests| image:: https://img.shields.io/github/issues-pr/hamcrest/PyHamcrest.svg?logo=github
-    :alt: GitHub pull-requests
-    :target: https://github.com/hamcrest/PyHamcrest/pulls
-
-.. |GitHub pull-requests closed| image:: https://img.shields.io/github/issues-pr-closed/hamcrest/PyHamcrest.svg?logo=github
-    :alt: GitHub pull-requests closed
-    :target: https://github.com/hamcrest/PyHamcrest/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aclosed
-
-.. |Lines of Code| image:: https://tokei.rs/b1/github/hamcrest/PyHamcrest
-    :alt: Lines of Code
-    :target: https://github.com/hamcrest/PyHamcrest
-
-.. |supported-implementations| image:: https://pypip.in/implementation/PyHamcrest/badge.svg?style=flat
-    :alt: Supported implementations
-    :target: https://pypi.python.org/pypi/PyHamcrest
-
-.. |scrutinizer| image:: https://img.shields.io/scrutinizer/g/hamcrest/PyHamcrest/master.svg?style=flat
-    :alt: Scrtinizer Status
-    :target: https://scrutinizer-ci.com/g/hamcrest/PyHamcrest/
-
 
 Introduction
 ============
@@ -125,13 +58,15 @@ the standard set of matchers:
  from hamcrest import *
  import unittest
 
+
  class BiscuitTest(unittest.TestCase):
      def testEquals(self):
-         theBiscuit = Biscuit('Ginger')
-         myBiscuit = Biscuit('Ginger')
+         theBiscuit = Biscuit("Ginger")
+         myBiscuit = Biscuit("Ginger")
          assert_that(theBiscuit, equal_to(myBiscuit))
 
- if __name__ == '__main__':
+
+ if __name__ == "__main__":
      unittest.main()
 
 The ``assert_that`` function is a stylized sentence for making a test
@@ -146,14 +81,14 @@ for the tested value in the assertion:
 
 .. code:: python
 
- assert_that(theBiscuit.getChocolateChipCount(), equal_to(10), 'chocolate chips')
- assert_that(theBiscuit.getHazelnutCount(), equal_to(3), 'hazelnuts')
+ assert_that(theBiscuit.getChocolateChipCount(), equal_to(10), "chocolate chips")
+ assert_that(theBiscuit.getHazelnutCount(), equal_to(3), "hazelnuts")
 
 As a convenience, assert_that can also be used to verify a boolean condition:
 
 .. code:: python
 
- assert_that(theBiscuit.isCooked(), 'cooked')
+ assert_that(theBiscuit.isCooked(), "cooked")
 
 This is equivalent to the ``assert_`` method of unittest.TestCase, but because
 it's a standalone function, it offers greater flexibility in test writing.
@@ -282,21 +217,30 @@ And here's the implementation:
  from hamcrest.core.base_matcher import BaseMatcher
  from hamcrest.core.helpers.hasmethod import hasmethod
 
- class IsGivenDayOfWeek(BaseMatcher):
 
+ class IsGivenDayOfWeek(BaseMatcher):
      def __init__(self, day):
          self.day = day  # Monday is 0, Sunday is 6
 
      def _matches(self, item):
-         if not hasmethod(item, 'weekday'):
+         if not hasmethod(item, "weekday"):
              return False
          return item.weekday() == self.day
 
      def describe_to(self, description):
-         day_as_string = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
-                          'Friday', 'Saturday', 'Sunday']
-         description.append_text('calendar date falling on ')    \
-                    .append_text(day_as_string[self.day])
+         day_as_string = [
+             "Monday",
+             "Tuesday",
+             "Wednesday",
+             "Thursday",
+             "Friday",
+             "Saturday",
+             "Sunday",
+         ]
+         description.append_text("calendar date falling on ").append_text(
+             day_as_string[self.day]
+         )
+
 
  def on_a_saturday():
      return IsGivenDayOfWeek(5)
@@ -326,12 +270,14 @@ could use it in our test by importing the factory function ``on_a_saturday``:
  import unittest
  from isgivendayofweek import on_a_saturday
 
+
  class DateTest(unittest.TestCase):
      def testDateIsOnASaturday(self):
          d = datetime.date(2008, 4, 26)
          assert_that(d, is_(on_a_saturday()))
 
- if __name__ == '__main__':
+
+ if __name__ == "__main__":
      unittest.main()
 
 Even though the ``on_a_saturday`` function creates a new matcher each time it
diff --git a/changelog.d/towncrier_template.rst b/changelog.d/towncrier_template.rst
new file mode 100644
index 0000000..3125265
--- /dev/null
+++ b/changelog.d/towncrier_template.rst
@@ -0,0 +1,20 @@
+{% for section, _ in sections.items() %} {% set underline = underlines[0] %}{% if section %}{{section}} {{ underline * section|length }}{% set underline = underlines[1] %}
+
+{% endif %}
+
+{% if sections[section] %} {% for category, val in definitions.items() if category in sections[section]%} {{ definitions[category]['name'] }} {{ underline * definitions[category]['name']|length }}
+
+{% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category].items() %} - {{ text }}
+
+{{ values|join(',n ') }}
+{% endfor %}
+
+{% else %} - {{ sections[section][category]['']|join(', ') }}
+
+{% endif %} {% if sections[section][category]|length == 0 %} No significant changes.
+
+{% else %} {% endif %}
+
+{% endfor %} {% else %} No significant changes.
+
+{% endif %} {% endfor %} ----
diff --git a/debian/changelog b/debian/changelog
index 48c5669..0e0b7c1 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+pyhamcrest (2.0.3-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 10 Mar 2022 05:13:49 -0000
+
 pyhamcrest (2.0.2-2) unstable; urgency=medium
 
   * Team upload.
diff --git a/devel-requirements.txt b/devel-requirements.txt
deleted file mode 100644
index 6fee5e8..0000000
--- a/devel-requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-pytest>=2.6
-Sphinx>=1.2.2
diff --git a/doc/conf.py b/doc/conf.py
index e9266d4..0a6b43d 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -12,118 +12,119 @@
 # serve to show the default.
 
 import sys, os
-import six
-import sphinx_rtd_theme
+import alabaster
 
 # 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.
-sys.path.insert(0, os.path.abspath('..'))
+sys.path.insert(0, os.path.abspath("../src"))
 
 from hamcrest import __version__
 
 # -- General configuration -----------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+# needs_sphinx = '1.0'
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
+extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "alabaster"]
 
-autodoc_default_options = {'members': None, 'show-inheritance': None}
-intersphinx_mapping = {'python': ('http://docs.python.org/2.6', None)}
+autodoc_default_options = {"members": None, "show-inheritance": None}
+autodoc_typehints = "description"
+intersphinx_mapping = {"python": ("http://docs.python.org/3", None)}
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = six.u('PyHamcrest')
-copyright = six.u('2020, hamcrest.org')
+project = "PyHamcrest"
+copyright = "2020, hamcrest.org"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
-# The short X.Y version.
-version = __version__
 # The full version, including alpha/beta/rc tags.
-release = __version__
+version = __version__
+
+# The short X.Y version.
+release = ".".join(version.split(".")[:2])
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
-#language = None
+# language = None
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ''
 # Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
-exclude_patterns = ['_build']
+exclude_patterns = ["_build"]
 
 # The reST default role (used for this markup: `text`) to use for all documents.
-default_role = 'py:obj'
+default_role = "py:obj"
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
 add_function_parentheses = False
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 
 # -- Options for HTML output ---------------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-html_theme = 'sphinx_rtd_theme'
+html_theme = "alabaster"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+# html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
-html_theme_path = [ sphinx_rtd_theme.get_html_theme_path() ]
+# html_theme_path = []
+html_theme_path = [alabaster.get_path()]
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-#html_title = None
+# html_title = None
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-#html_favicon = None
+# html_favicon = None
 
 # 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,
@@ -132,101 +133,100 @@ html_static_path = []
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-#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.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
 
 # If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
 
 # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
 
 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
 
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
 
 # This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'PyHamcrestdoc'
+htmlhelp_basename = "PyHamcrestdoc"
 
 
 # -- Options for LaTeX output --------------------------------------------------
 
 # The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
+# latex_paper_size = 'letter'
 
 # The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
+# latex_font_size = '10pt'
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-  ('index', 'PyHamcrest.tex', six.u('PyHamcrest Documentation'),
-   six.u('hamcrest.org'), 'manual'),
+    ("index", "PyHamcrest.tex", "PyHamcrest Documentation", "hamcrest.org"),
+    "manual",
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
 
 # If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
 
 # Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
+# latex_preamble = ''
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
 
 # PyHamcrest customization: Don't skip BaseMatcher's _matches method
 def skip_member(app, what, name, obj, skip, options):
-    if skip and str(obj).find('BaseMatcher._matches') >= 0:
+    if skip and str(obj).find("BaseMatcher._matches") >= 0:
         return False
     return skip
 
+
 def setup(app):
-    app.connect('autodoc-skip-member', skip_member)
+    app.connect("autodoc-skip-member", skip_member)
+
 
 # -- Options for manual page output --------------------------------------------
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [
-    ('index', 'pyhamcrest', six.u('PyHamcrest Documentation'),
-     [six.u('hamcrest.org')], 1)
-]
+man_pages = [("index", "pyhamcrest", "PyHamcrest Documentation", ["hamcrest.org"], 1)]
diff --git a/examples/CustomDateMatcher.py b/examples/CustomDateMatcher.py
index 3fc498e..171de6e 100644
--- a/examples/CustomDateMatcher.py
+++ b/examples/CustomDateMatcher.py
@@ -1,5 +1,6 @@
 import sys
-sys.path.append('..')
+
+sys.path.append("..")
 
 from hamcrest.core.base_matcher import BaseMatcher
 from hamcrest.core.helpers.hasmethod import hasmethod
@@ -17,16 +18,22 @@ class IsGivenDayOfWeek(BaseMatcher):
 
     def _matches(self, item):
         """Test whether item matches."""
-        if not hasmethod(item, 'weekday'):
+        if not hasmethod(item, "weekday"):
             return False
         return item.weekday() == self.day
 
     def describe_to(self, description):
         """Describe the matcher."""
-        day_as_string = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
-                         'Friday', 'Saturday', 'Sunday']
-        description.append_text('calendar date falling on ')    \
-                   .append_text(day_as_string[self.day])
+        day_as_string = [
+            "Monday",
+            "Tuesday",
+            "Wednesday",
+            "Thursday",
+            "Friday",
+            "Saturday",
+            "Sunday",
+        ]
+        description.append_text("calendar date falling on ").append_text(day_as_string[self.day])
 
 
 def on_a_saturday():
@@ -37,19 +44,19 @@ def on_a_saturday():
 class SampleTest(unittest.TestCase):
     def testDateIsOnASaturday(self):
         """Example of successful match."""
-        d = datetime.date(2008, 04, 26)
+        d = datetime.date(2008, 4, 26)
         assert_that(d, is_(on_a_saturday()))
 
     def testFailsWithMismatchedDate(self):
         """Example of what happens with date that doesn't match."""
-        d = datetime.date(2008, 04, 06)
+        d = datetime.date(2008, 4, 6)
         assert_that(d, is_(on_a_saturday()))
 
     def testFailsWithNonDate(self):
         """Example of what happens with object that isn't a date."""
-        d = 'oops'
+        d = "oops"
         assert_that(d, is_(on_a_saturday()))
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
diff --git a/examples/ExampleWithAssertThat.py b/examples/ExampleWithAssertThat.py
index af960a2..12982de 100644
--- a/examples/ExampleWithAssertThat.py
+++ b/examples/ExampleWithAssertThat.py
@@ -1,5 +1,6 @@
 import sys
-sys.path.append('..')
+
+sys.path.append("..")
 
 from hamcrest import *
 import unittest
@@ -7,16 +8,16 @@ import unittest
 
 class ExampleWithAssertThat(unittest.TestCase):
     def testUsingAssertThat(self):
-        assert_that('xx', is_('xx'))
-        assert_that('yy', is_not('xx'))
-        assert_that('i like cheese', contains_string('cheese'))
+        assert_that("xx", is_("xx"))
+        assert_that("yy", is_not("xx"))
+        assert_that("i like cheese", contains_string("cheese"))
 
     def testCanAlsoSupplyDescriptiveReason(self):
-        assert_that('xx', is_('xx'), 'description')
+        assert_that("xx", is_("xx"), "description")
 
     def testCanAlsoAssertPlainBooleans(self):
-        assert_that(True, 'This had better not fail')
+        assert_that(True, "This had better not fail")
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..859a8e2
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,57 @@
+[build-system]
+requires = ["setuptools>=40.6.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+
+[tool.coverage.run]
+parallel = true
+branch = true
+source = ["hamcrest"]
+
+[tool.coverage.paths]
+source = ["src", ".tox/*/site-packages"]
+
+[tool.coverage.report]
+show_missing = true
+skip_covered = true
+exclude_lines = [
+    # a more strict default pragma
+    "\\# pragma: no cover\\b",
+
+    # allow defensive code
+    "^\\s*raise AssertionError\\b",
+    "^\\s*raise NotImplementedError\\b",
+    "^\\s*return NotImplemented\\b",
+    "^\\s*raise$",
+
+    # typing-related code
+    "^if (False|TYPE_CHECKING):",
+    ": \\.\\.\\.(\\s*#.*)?$",
+    "^ +\\.\\.\\.$",
+    "-> ['\"]?NoReturn['\"]?:",
+]
+[tool.black]
+line_length = 100
+
+[tool.interrogate]
+verbose = 2
+fail-under = 100
+whitelist-regex = ["test_.*"]
+
+
+[tool.isort]
+profile = "hamcrests"
+
+known_first_party = "hamcrest"
+known_third_party = ["hypothesis", "pytest", "setuptools", "six"]
+
+
+[tool.towncrier]
+package = "hamcrest"
+package_dir = "src"
+filename = "CHANGELOG.rst"
+template = "changelog.d/towncrier_template.rst"
+issue_format = "`#{issue} <https://github.com/hamcrest/PyHamcrest/issues/{issue}>`_"
+directory = "changelog.d"
+title_format = "{version} ({project_date})"
+underlines = ["-", "^"]
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 8b13789..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 7d320b9..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-[egg_info]
-;tag_build = .dev
-;tag_svn_revision = true
diff --git a/setup.py b/setup.py
index 98377d5..b68895b 100755
--- a/setup.py
+++ b/setup.py
@@ -17,14 +17,43 @@ def read(fname):
 # On Python 3, we can't "from hamcrest import __version__" (get ImportError),
 # so we extract the variable assignment and execute it ourselves.
 fh = open("src/hamcrest/__init__.py")
+
+# this will be overridden
+__version__ = None
 try:
     for line in fh:
         if re.match("__version__.*", line):
             exec(line)
+
 finally:
     if fh:
         fh.close()
 
+assert __version__ is not None
+
+REQUIREMENTS_DOCS = ["sphinx~=3.0", "alabaster~=0.7"]
+TESTS_BASIC = [
+    "pytest>=5.0",
+    "pytest-sugar",
+    "pytest-xdist",
+    "coverage[toml]",
+    # No point on Pypy thanks to https://github.com/python/typed_ast/issues/111
+    "pytest-mypy-plugins; platform_python_implementation != 'PyPy'",
+    "types-mock",
+]
+TESTS_NUMPY = ["numpy"]
+DEV_TOOLS = [
+    "towncrier",
+    "twine",
+    "pytest-mypy",
+    "flake8",
+    "black",
+    "tox",
+    "tox-pyenv",
+    "tox-asdf",
+]
+
+
 params = dict(
     name="PyHamcrest",
     version=__version__,  # flake8:noqa
@@ -42,8 +71,15 @@ params = dict(
     package_data={"hamcrest": ["py.typed"]},
     provides=["hamcrest"],
     long_description=read("README.rst"),
+    long_description_content_type="text/x-rst",
     python_requires=">=3.5",
     install_requires=[],
+    extras_require={
+        "docs": REQUIREMENTS_DOCS,
+        "tests": TESTS_BASIC,
+        "tests-numpy": TESTS_BASIC + TESTS_NUMPY,
+        "dev": REQUIREMENTS_DOCS + TESTS_BASIC + DEV_TOOLS,
+    },
     classifiers=[
         "Development Status :: 5 - Production/Stable",
         "Environment :: Console",
@@ -52,7 +88,6 @@ params = dict(
         "Natural Language :: English",
         "Operating System :: OS Independent",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.5",
         "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
diff --git a/src/hamcrest/__init__.py b/src/hamcrest/__init__.py
index 7a36257..b307591 100644
--- a/src/hamcrest/__init__.py
+++ b/src/hamcrest/__init__.py
@@ -1,7 +1,12 @@
 from hamcrest.core import *
 from hamcrest.library import *
+from hamcrest import core, library
 
-__version__ = "2.0.2"
+__version__ = "2.0.3"
 __author__ = "Chris Rose"
 __copyright__ = "Copyright 2020 hamcrest.org"
 __license__ = "BSD, see License.txt"
+
+__all__ = []
+__all__.extend(core.__all__)
+__all__.extend(library.__all__)
diff --git a/src/hamcrest/core/__init__.py b/src/hamcrest/core/__init__.py
index d71b14b..a578f36 100644
--- a/src/hamcrest/core/__init__.py
+++ b/src/hamcrest/core/__init__.py
@@ -4,3 +4,21 @@ from hamcrest.core.core import *
 __author__ = "Jon Reid"
 __copyright__ = "Copyright 2011 hamcrest.org"
 __license__ = "BSD, see License.txt"
+
+__all__ = [
+    "assert_that",
+    "all_of",
+    "any_of",
+    "anything",
+    "calling",
+    "described_as",
+    "equal_to",
+    "instance_of",
+    "is_",
+    "is_not",
+    "none",
+    "not_",
+    "not_none",
+    "raises",
+    "same_instance",
+]
diff --git a/src/hamcrest/core/base_matcher.py b/src/hamcrest/core/base_matcher.py
index c1c01ea..7edc994 100644
--- a/src/hamcrest/core/base_matcher.py
+++ b/src/hamcrest/core/base_matcher.py
@@ -1,3 +1,4 @@
+from textwrap import shorten
 from typing import Optional, TypeVar
 
 from hamcrest.core.description import Description
@@ -25,6 +26,12 @@ class BaseMatcher(Matcher[T]):
     def __str__(self) -> str:
         return tostring(self)
 
+    def __repr__(self) -> str:
+        """Returns matcher string representation."""
+        return "<{0}({1})>".format(
+            self.__class__.__name__, shorten(tostring(self), 60, placeholder="...")
+        )
+
     def _matches(self, item: T) -> bool:
         raise NotImplementedError("_matches")
 
diff --git a/src/hamcrest/core/core/__init__.py b/src/hamcrest/core/core/__init__.py
index 6f452c1..3bb1120 100644
--- a/src/hamcrest/core/core/__init__.py
+++ b/src/hamcrest/core/core/__init__.py
@@ -15,3 +15,20 @@ from hamcrest.core.core.raises import calling, raises
 __author__ = "Jon Reid"
 __copyright__ = "Copyright 2011 hamcrest.org"
 __license__ = "BSD, see License.txt"
+
+__all__ = [
+    "all_of",
+    "any_of",
+    "anything",
+    "calling",
+    "described_as",
+    "equal_to",
+    "instance_of",
+    "is_",
+    "is_not",
+    "none",
+    "not_",
+    "not_none",
+    "raises",
+    "same_instance",
+]
diff --git a/src/hamcrest/core/core/is_.py b/src/hamcrest/core/core/is_.py
index 6b1b541..dafe41c 100644
--- a/src/hamcrest/core/core/is_.py
+++ b/src/hamcrest/core/core/is_.py
@@ -1,4 +1,4 @@
-from typing import Optional, Type, TypeVar, Union, overload
+from typing import Optional, Type, TypeVar, overload, Any
 
 from hamcrest.core.base_matcher import BaseMatcher
 from hamcrest.core.description import Description
@@ -46,12 +46,17 @@ def _wrap_value_or_type(x):
 
 
 @overload
-def is_(x: Type) -> Matcher[object]:
+def is_(x: Type) -> Matcher[Any]:
     ...
 
 
 @overload
-def is_(x: Union[Matcher[T], T]) -> Matcher[T]:
+def is_(x: Matcher[T]) -> Matcher[T]:
+    ...
+
+
+@overload
+def is_(x: T) -> Matcher[T]:
     ...
 
 
diff --git a/src/hamcrest/core/core/isanything.py b/src/hamcrest/core/core/isanything.py
index 5fa4550..bed86d8 100644
--- a/src/hamcrest/core/core/isanything.py
+++ b/src/hamcrest/core/core/isanything.py
@@ -11,7 +11,7 @@ __license__ = "BSD, see License.txt"
 
 class IsAnything(BaseMatcher[Any]):
     def __init__(self, description: Optional[str]) -> None:
-        self.description = description or "ANYTHING"  # type: str
+        self.description: str = description or "ANYTHING"
 
     def _matches(self, item: Any) -> bool:
         return True
diff --git a/src/hamcrest/core/core/raises.py b/src/hamcrest/core/core/raises.py
index afb7fd9..0fcd6fe 100644
--- a/src/hamcrest/core/core/raises.py
+++ b/src/hamcrest/core/core/raises.py
@@ -22,8 +22,8 @@ class Raises(BaseMatcher[Callable[..., Any]]):
         self.pattern = pattern
         self.matcher = matching
         self.expected = expected
-        self.actual = None  # type: Optional[BaseException]
-        self.function = None  # type: Optional[Callable[..., Any]]
+        self.actual: Optional[BaseException] = None
+        self.function: Optional[Callable[..., Any]] = None
 
     def _matches(self, function: Callable[..., Any]) -> bool:
         if not callable(function):
@@ -115,8 +115,8 @@ def raises(exception: Type[Exception], pattern=None, matching=None) -> Matcher[C
 class DeferredCallable(object):
     def __init__(self, func: Callable[..., Any]):
         self.func = func
-        self.args = tuple()  # type: Tuple[Any, ...]
-        self.kwargs = {}  # type: Mapping[str, Any]
+        self.args: Tuple[Any, ...] = tuple()
+        self.kwargs: Mapping[str, Any] = {}
 
     def __call__(self):
         self.func(*self.args, **self.kwargs)
diff --git a/src/hamcrest/core/description.py b/src/hamcrest/core/description.py
index 2d36ba4..6f22414 100644
--- a/src/hamcrest/core/description.py
+++ b/src/hamcrest/core/description.py
@@ -1,4 +1,4 @@
-from typing import Any, Iterable, Sequence
+from typing import Any, Iterable
 
 __author__ = "Jon Reid"
 __copyright__ = "Copyright 2011 hamcrest.org"
diff --git a/src/hamcrest/core/helpers/ismock.py b/src/hamcrest/core/helpers/ismock.py
index 26b561b..56f24f4 100644
--- a/src/hamcrest/core/helpers/ismock.py
+++ b/src/hamcrest/core/helpers/ismock.py
@@ -1,6 +1,6 @@
 from typing import Any, List, Type
 
-MOCKTYPES = []  # type: List[Type]
+MOCKTYPES: List[Type] = []
 try:
     from mock import Mock
 
diff --git a/src/hamcrest/library/collection/__init__.py b/src/hamcrest/library/collection/__init__.py
index d06b818..bf07a66 100644
--- a/src/hamcrest/library/collection/__init__.py
+++ b/src/hamcrest/library/collection/__init__.py
@@ -13,3 +13,18 @@ from .issequence_onlycontaining import only_contains
 __author__ = "Chris Rose"
 __copyright__ = "Copyright 2013 hamcrest.org"
 __license__ = "BSD, see License.txt"
+
+__all__ = [
+    "contains",
+    "contains_exactly",
+    "contains_inanyorder",
+    "empty",
+    "has_entries",
+    "has_entry",
+    "has_item",
+    "has_items",
+    "has_key",
+    "has_value",
+    "is_in",
+    "only_contains",
+]
diff --git a/src/hamcrest/library/collection/isdict_containing.py b/src/hamcrest/library/collection/isdict_containing.py
index b93ef36..6ddce47 100644
--- a/src/hamcrest/library/collection/isdict_containing.py
+++ b/src/hamcrest/library/collection/isdict_containing.py
@@ -1,4 +1,4 @@
-from typing import Hashable, Mapping, TypeVar, Union
+from typing import Hashable, Mapping, MutableMapping, TypeVar, Union
 
 from hamcrest.core.base_matcher import BaseMatcher
 from hamcrest.core.description import Description
@@ -32,6 +32,34 @@ class IsDictContaining(BaseMatcher[Mapping[K, V]]):
             self.key_matcher
         ).append_text(": ").append_description_of(self.value_matcher).append_text("]")
 
+    def describe_mismatch(self, item: Mapping[K, V], mismatch_description: Description) -> None:
+        key_matches = self._matching_keys(item)
+        if len(key_matches) == 1:
+            key, value = key_matches.popitem()
+            mismatch_description.append_text("value for ").append_description_of(key).append_text(
+                " "
+            )
+            self.value_matcher.describe_mismatch(value, mismatch_description)
+        else:
+            super().describe_mismatch(item, mismatch_description)
+
+    def describe_match(self, item: Mapping[K, V], match_description: Description) -> None:
+        key_matches = self._matching_keys(item)
+        if len(key_matches) == 1:
+            key, value = key_matches.popitem()
+            match_description.append_text("value for ").append_description_of(key).append_text(" ")
+            self.value_matcher.describe_mismatch(value, match_description)
+        else:
+            super().describe_match(item, match_description)
+
+    def _matching_keys(self, item):
+        key_matches: MutableMapping[K, V] = {}
+        if hasmethod(item, "items"):
+            for key, value in item.items():
+                if self.key_matcher.matches(key):
+                    key_matches[key] = value
+        return key_matches
+
 
 def has_entry(
     key_match: Union[K, Matcher[K]], value_match: Union[V, Matcher[V]]
diff --git a/src/hamcrest/library/collection/isdict_containingentries.py b/src/hamcrest/library/collection/isdict_containingentries.py
index 68a28bc..490affa 100644
--- a/src/hamcrest/library/collection/isdict_containingentries.py
+++ b/src/hamcrest/library/collection/isdict_containingentries.py
@@ -30,7 +30,7 @@ class IsDictContainingEntries(BaseMatcher[Mapping[K, V]]):
         for key, value_matcher in self.value_matchers:
 
             try:
-                if not key in item:
+                if key not in item:
                     if mismatch_description:
                         mismatch_description.append_text("no ").append_description_of(
                             key
diff --git a/src/hamcrest/library/collection/issequence_containing.py b/src/hamcrest/library/collection/issequence_containing.py
index 2d7bf5c..f615dcc 100644
--- a/src/hamcrest/library/collection/issequence_containing.py
+++ b/src/hamcrest/library/collection/issequence_containing.py
@@ -39,7 +39,7 @@ class IsSequenceContaining(BaseMatcher[Sequence[T]]):
 class IsSequenceContainingEvery(BaseMatcher[Sequence[T]]):
     def __init__(self, *element_matchers: Matcher[T]) -> None:
         delegates = [cast(Matcher[Sequence[T]], has_item(e)) for e in element_matchers]
-        self.matcher = all_of(*delegates)  # type: Matcher[Sequence[T]]
+        self.matcher: Matcher[Sequence[T]] = all_of(*delegates)
 
     def _matches(self, item: Sequence[T]) -> bool:
         try:
diff --git a/src/hamcrest/library/number/iscloseto.py b/src/hamcrest/library/number/iscloseto.py
index 69ff025..579eeb8 100644
--- a/src/hamcrest/library/number/iscloseto.py
+++ b/src/hamcrest/library/number/iscloseto.py
@@ -14,8 +14,7 @@ Number = Union[float, Decimal]  # Argh, https://github.com/python/mypy/issues/31
 
 
 def isnumeric(value: Any) -> bool:
-    """Confirm that 'value' can be treated numerically; duck-test accordingly
-    """
+    """Confirm that 'value' can be treated numerically; duck-test accordingly"""
     if isinstance(value, (float, complex, int)):
         return True
 
@@ -24,7 +23,7 @@ def isnumeric(value: Any) -> bool:
         return True
     except ArithmeticError:
         return True
-    except:
+    except Exception:
         return False
 
 
diff --git a/tests/hamcrest_unit_test/base_description_test.py b/tests/hamcrest_unit_test/base_description_test.py
index cdd4e15..c536770 100644
--- a/tests/hamcrest_unit_test/base_description_test.py
+++ b/tests/hamcrest_unit_test/base_description_test.py
@@ -1,4 +1,5 @@
 # coding: utf-8
+import platform
 from unittest.mock import sentinel
 
 import pytest
@@ -40,7 +41,14 @@ def test_append_text_delegates(desc):
         (Described(), "described"),
         ("unicode-py3", "'unicode-py3'"),
         (b"bytes-py3", "<b'bytes-py3'>"),
-        ("\U0001F4A9", "'{0}'".format("\U0001F4A9")),
+        pytest.param(
+            "\U0001F4A9",
+            "'{0}'".format("\U0001F4A9"),
+            marks=pytest.mark.skipif(
+                platform.python_implementation() == "PyPy",
+                reason="Inexplicable failure on PyPy. Not super important, I hope!",
+            ),
+        ),
     ),
 )
 def test_append_description_types(desc, described, appended):
diff --git a/tests/hamcrest_unit_test/base_matcher_test.py b/tests/hamcrest_unit_test/base_matcher_test.py
index 9e492fc..5ad2a61 100644
--- a/tests/hamcrest_unit_test/base_matcher_test.py
+++ b/tests/hamcrest_unit_test/base_matcher_test.py
@@ -5,8 +5,8 @@ if __name__ == "__main__":
 
 import unittest
 
-from hamcrest.core.base_matcher import *
-from hamcrest_unit_test.matcher_test import *
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest_unit_test.matcher_test import assert_match_description, assert_mismatch_description
 
 __author__ = "Jon Reid"
 __copyright__ = "Copyright 2011 hamcrest.org"
@@ -37,6 +37,19 @@ class BaseMatcherTest(unittest.TestCase):
     def testMatchDescriptionShouldDescribeItem(self):
         assert_match_description("was <99>", PassingBaseMatcher(), 99)
 
+    def testMatcherReprShouldDescribeMatcher(self):
+        assert repr(FailingBaseMatcher()) == "<FailingBaseMatcher(SOME DESCRIPTION)>"
+
+    def testMatcherReprShouldTruncateLongDescription(self):
+        class LongDescriptionMatcher(BaseMatcher):
+            def describe_to(self, description):
+                description.append_text("1234 " * 13)
+
+        assert (
+            repr(LongDescriptionMatcher())
+            == "<LongDescriptionMatcher(1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234...)>"
+        )
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/hamcrest_unit_test/collection/is_empty_test.py b/tests/hamcrest_unit_test/collection/is_empty_test.py
index 791f27d..84f8eb8 100644
--- a/tests/hamcrest_unit_test/collection/is_empty_test.py
+++ b/tests/hamcrest_unit_test/collection/is_empty_test.py
@@ -1,4 +1,4 @@
-from hamcrest.library.collection.is_empty import *
+from hamcrest.library.collection.is_empty import empty
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Chris Rose"
diff --git a/tests/hamcrest_unit_test/collection/isdict_containing_test.py b/tests/hamcrest_unit_test/collection/isdict_containing_test.py
index f9ec69a..b073787 100644
--- a/tests/hamcrest_unit_test/collection/isdict_containing_test.py
+++ b/tests/hamcrest_unit_test/collection/isdict_containing_test.py
@@ -1,8 +1,13 @@
 import unittest
 
+from hamcrest import starts_with
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.library.collection.isdict_containing import *
-from hamcrest_unit_test.matcher_test import MatcherTest
+from hamcrest.library.collection.isdict_containing import has_entry
+from hamcrest_unit_test.matcher_test import (
+    MatcherTest,
+    assert_match_description,
+    assert_mismatch_description,
+)
 
 from .quasidict import QuasiDictionary
 
@@ -40,6 +45,18 @@ class IsDictContainingTest(MatcherTest):
     def testDescribeMismatch(self):
         self.assert_describe_mismatch("was 'bad'", has_entry("a", 1), "bad")
 
+    def test_describe_single_matching_key_mismatching_value(self):
+        assert_mismatch_description("value for 'a' was <2>", has_entry("a", 1), {"a": 2})
+        assert_mismatch_description(
+            "value for 'aa' was <2>", has_entry(starts_with("a"), 1), {"aa": 2}
+        )
+        assert_mismatch_description(
+            "was <{'ab': 2, 'ac': 3}>", has_entry(starts_with("a"), 1), {"ab": 2, "ac": 3}
+        )
+
+    def test_describe_match(self):
+        assert_match_description("value for 'a' was <1>", has_entry("a", 1), {"a": 1, "b": 2})
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/hamcrest_unit_test/collection/isdict_containingentries_test.py b/tests/hamcrest_unit_test/collection/isdict_containingentries_test.py
index 4c46390..da2e799 100644
--- a/tests/hamcrest_unit_test/collection/isdict_containingentries_test.py
+++ b/tests/hamcrest_unit_test/collection/isdict_containingentries_test.py
@@ -7,7 +7,7 @@ if __name__ == "__main__":
 import unittest
 
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.library.collection.isdict_containingentries import *
+from hamcrest.library.collection.isdict_containingentries import has_entries
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/collection/isdict_containingkey_test.py b/tests/hamcrest_unit_test/collection/isdict_containingkey_test.py
index 79e6a1f..04fee89 100644
--- a/tests/hamcrest_unit_test/collection/isdict_containingkey_test.py
+++ b/tests/hamcrest_unit_test/collection/isdict_containingkey_test.py
@@ -1,7 +1,7 @@
 import unittest
 
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.library.collection.isdict_containingkey import *
+from hamcrest.library.collection.isdict_containingkey import has_key
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 from .quasidict import QuasiDictionary
diff --git a/tests/hamcrest_unit_test/collection/isdict_containingvalue_test.py b/tests/hamcrest_unit_test/collection/isdict_containingvalue_test.py
index 1ef34fc..68d56c7 100644
--- a/tests/hamcrest_unit_test/collection/isdict_containingvalue_test.py
+++ b/tests/hamcrest_unit_test/collection/isdict_containingvalue_test.py
@@ -1,7 +1,7 @@
 import unittest
 
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.library.collection.isdict_containingvalue import *
+from hamcrest.library.collection.isdict_containingvalue import has_value
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 from .quasidict import QuasiDictionary
diff --git a/tests/hamcrest_unit_test/collection/isin_test.py b/tests/hamcrest_unit_test/collection/isin_test.py
index 83414d1..9acd43f 100644
--- a/tests/hamcrest_unit_test/collection/isin_test.py
+++ b/tests/hamcrest_unit_test/collection/isin_test.py
@@ -1,6 +1,6 @@
 import unittest
 
-from hamcrest.library.collection.isin import *
+from hamcrest.library.collection.isin import is_in
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 from .sequencemixin import GeneratorForm, SequenceForm
diff --git a/tests/hamcrest_unit_test/collection/issequence_containing_test.py b/tests/hamcrest_unit_test/collection/issequence_containing_test.py
index 83e0b38..10bfa4e 100644
--- a/tests/hamcrest_unit_test/collection/issequence_containing_test.py
+++ b/tests/hamcrest_unit_test/collection/issequence_containing_test.py
@@ -1,7 +1,7 @@
 import unittest
 
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.library.collection.issequence_containing import *
+from hamcrest.library.collection.issequence_containing import has_item, has_items
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 from .quasisequence import QuasiSequence
diff --git a/tests/hamcrest_unit_test/collection/issequence_containinginanyorder_test.py b/tests/hamcrest_unit_test/collection/issequence_containinginanyorder_test.py
index 041b0c5..5c21fbb 100644
--- a/tests/hamcrest_unit_test/collection/issequence_containinginanyorder_test.py
+++ b/tests/hamcrest_unit_test/collection/issequence_containinginanyorder_test.py
@@ -1,7 +1,7 @@
 import unittest
 
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.library.collection.issequence_containinginanyorder import *
+from hamcrest.library.collection.issequence_containinginanyorder import contains_inanyorder
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 from .quasisequence import QuasiSequence
diff --git a/tests/hamcrest_unit_test/collection/issequence_containinginorder_test.py b/tests/hamcrest_unit_test/collection/issequence_containinginorder_test.py
index 5b1fe63..e0db596 100644
--- a/tests/hamcrest_unit_test/collection/issequence_containinginorder_test.py
+++ b/tests/hamcrest_unit_test/collection/issequence_containinginorder_test.py
@@ -1,7 +1,7 @@
 import unittest
 
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.library.collection.issequence_containinginorder import *
+from hamcrest.library.collection.issequence_containinginorder import contains, contains_exactly
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 from .quasisequence import QuasiSequence
diff --git a/tests/hamcrest_unit_test/collection/issequence_onlycontaining_test.py b/tests/hamcrest_unit_test/collection/issequence_onlycontaining_test.py
index f8f1d22..7e6e555 100644
--- a/tests/hamcrest_unit_test/collection/issequence_onlycontaining_test.py
+++ b/tests/hamcrest_unit_test/collection/issequence_onlycontaining_test.py
@@ -1,7 +1,7 @@
 import unittest
 
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.library.collection.issequence_onlycontaining import *
+from hamcrest.library.collection.issequence_onlycontaining import only_contains
 from hamcrest.library.number.ordering_comparison import less_than
 from hamcrest_unit_test.matcher_test import MatcherTest
 
diff --git a/tests/hamcrest_unit_test/core/allof_test.py b/tests/hamcrest_unit_test/core/allof_test.py
index 72ca50e..2efd079 100644
--- a/tests/hamcrest_unit_test/core/allof_test.py
+++ b/tests/hamcrest_unit_test/core/allof_test.py
@@ -6,7 +6,7 @@ if __name__ == "__main__":
 
 import unittest
 
-from hamcrest.core.core.allof import *
+from hamcrest.core.core.allof import AllOf, all_of
 from hamcrest.core.core.isequal import equal_to
 from hamcrest_unit_test.matcher_test import MatcherTest
 
diff --git a/tests/hamcrest_unit_test/core/anyof_test.py b/tests/hamcrest_unit_test/core/anyof_test.py
index 0dfa9d0..a6141f7 100644
--- a/tests/hamcrest_unit_test/core/anyof_test.py
+++ b/tests/hamcrest_unit_test/core/anyof_test.py
@@ -6,7 +6,7 @@ if __name__ == "__main__":
 
 import unittest
 
-from hamcrest.core.core.anyof import *
+from hamcrest.core.core.anyof import any_of
 from hamcrest.core.core.isequal import equal_to
 from hamcrest_unit_test.matcher_test import MatcherTest
 
diff --git a/tests/hamcrest_unit_test/core/described_as_test.py b/tests/hamcrest_unit_test/core/described_as_test.py
index e20c359..f9552b7 100644
--- a/tests/hamcrest_unit_test/core/described_as_test.py
+++ b/tests/hamcrest_unit_test/core/described_as_test.py
@@ -1,6 +1,6 @@
 import unittest
 
-from hamcrest.core.core.described_as import *
+from hamcrest.core.core.described_as import described_as
 from hamcrest.core.core.isanything import anything
 from hamcrest_unit_test.matcher_test import MatcherTest
 
diff --git a/tests/hamcrest_unit_test/core/is_test.py b/tests/hamcrest_unit_test/core/is_test.py
index 02d4145..05b115d 100644
--- a/tests/hamcrest_unit_test/core/is_test.py
+++ b/tests/hamcrest_unit_test/core/is_test.py
@@ -1,6 +1,6 @@
 import unittest
 
-from hamcrest.core.core.is_ import *
+from hamcrest.core.core.is_ import is_
 from hamcrest.core.core.isequal import equal_to
 from hamcrest_unit_test.matcher_test import MatcherTest
 
diff --git a/tests/hamcrest_unit_test/core/isanything_test.py b/tests/hamcrest_unit_test/core/isanything_test.py
index bfab0d9..1f49a57 100644
--- a/tests/hamcrest_unit_test/core/isanything_test.py
+++ b/tests/hamcrest_unit_test/core/isanything_test.py
@@ -6,7 +6,7 @@ if __name__ == "__main__":
 
 import unittest
 
-from hamcrest.core.core.isanything import *
+from hamcrest.core.core.isanything import anything
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/core/isequal_test.py b/tests/hamcrest_unit_test/core/isequal_test.py
index 78945cd..6e6fe1a 100644
--- a/tests/hamcrest_unit_test/core/isequal_test.py
+++ b/tests/hamcrest_unit_test/core/isequal_test.py
@@ -6,7 +6,7 @@ if __name__ == "__main__":
 
 import unittest
 
-from hamcrest.core.core.isequal import *
+from hamcrest.core.core.isequal import equal_to
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/core/isinstanceof_test.py b/tests/hamcrest_unit_test/core/isinstanceof_test.py
index cc4a18f..c022e7c 100644
--- a/tests/hamcrest_unit_test/core/isinstanceof_test.py
+++ b/tests/hamcrest_unit_test/core/isinstanceof_test.py
@@ -1,7 +1,7 @@
 import sys
 import unittest
 
-from hamcrest.core.core.isinstanceof import *
+from hamcrest.core.core.isinstanceof import instance_of
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 if __name__ == "__main__":
diff --git a/tests/hamcrest_unit_test/core/isnone_test.py b/tests/hamcrest_unit_test/core/isnone_test.py
index 15f38e2..f00a8ce 100644
--- a/tests/hamcrest_unit_test/core/isnone_test.py
+++ b/tests/hamcrest_unit_test/core/isnone_test.py
@@ -6,7 +6,7 @@ if __name__ == "__main__":
 
 import unittest
 
-from hamcrest.core.core.isnone import *
+from hamcrest.core.core.isnone import none, not_none
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/core/isnot_test.py b/tests/hamcrest_unit_test/core/isnot_test.py
index 0b4e3fe..c433f3c 100644
--- a/tests/hamcrest_unit_test/core/isnot_test.py
+++ b/tests/hamcrest_unit_test/core/isnot_test.py
@@ -7,7 +7,7 @@ if __name__ == "__main__":
 import unittest
 
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.core.core.isnot import *
+from hamcrest.core.core.isnot import is_not
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/core/issame_test.py b/tests/hamcrest_unit_test/core/issame_test.py
index 9620bc2..02e759d 100644
--- a/tests/hamcrest_unit_test/core/issame_test.py
+++ b/tests/hamcrest_unit_test/core/issame_test.py
@@ -7,7 +7,7 @@ if __name__ == "__main__":
 import re
 import unittest
 
-from hamcrest.core.core.issame import *
+from hamcrest.core.core.issame import same_instance
 from hamcrest.core.string_description import StringDescription
 from hamcrest_unit_test.matcher_test import MatcherTest
 
diff --git a/tests/hamcrest_unit_test/core/raises_test.py b/tests/hamcrest_unit_test/core/raises_test.py
index 519b7a2..5f99fbd 100644
--- a/tests/hamcrest_unit_test/core/raises_test.py
+++ b/tests/hamcrest_unit_test/core/raises_test.py
@@ -94,7 +94,7 @@ class RaisesTest(MatcherTest):
 
         self.assert_matches(
             "Regex",
-            raises(AssertionError, "([\d, ]+)"),
+            raises(AssertionError, r"([\d, ]+)"),
             calling(raise_exception).with_args(3, 1, 4),
         )
 
diff --git a/tests/hamcrest_unit_test/integration/match_equality_test.py b/tests/hamcrest_unit_test/integration/match_equality_test.py
index cc88be0..eb95883 100644
--- a/tests/hamcrest_unit_test/integration/match_equality_test.py
+++ b/tests/hamcrest_unit_test/integration/match_equality_test.py
@@ -7,7 +7,7 @@ if __name__ == "__main__":
 import unittest
 
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.library.integration.match_equality import *
+from hamcrest.library.integration.match_equality import match_equality, tostring
 
 __author__ = "Chris Rose"
 __copyright__ = "Copyright 2011 hamcrest.org"
diff --git a/tests/hamcrest_unit_test/number/iscloseto_test.py b/tests/hamcrest_unit_test/number/iscloseto_test.py
index 8c05431..f83964f 100644
--- a/tests/hamcrest_unit_test/number/iscloseto_test.py
+++ b/tests/hamcrest_unit_test/number/iscloseto_test.py
@@ -1,6 +1,6 @@
 import unittest
 
-from hamcrest.library.number.iscloseto import *
+from hamcrest.library.number.iscloseto import Decimal, close_to, isnumeric
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/number/ordering_comparison_test.py b/tests/hamcrest_unit_test/number/ordering_comparison_test.py
index 6943d16..76143df 100644
--- a/tests/hamcrest_unit_test/number/ordering_comparison_test.py
+++ b/tests/hamcrest_unit_test/number/ordering_comparison_test.py
@@ -7,7 +7,12 @@ if __name__ == "__main__":
 import unittest
 from datetime import date
 
-from hamcrest.library.number.ordering_comparison import *
+from hamcrest.library.number.ordering_comparison import (
+    greater_than,
+    greater_than_or_equal_to,
+    less_than,
+    less_than_or_equal_to,
+)
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/object/haslength_test.py b/tests/hamcrest_unit_test/object/haslength_test.py
index 427ead5..49b2934 100644
--- a/tests/hamcrest_unit_test/object/haslength_test.py
+++ b/tests/hamcrest_unit_test/object/haslength_test.py
@@ -8,7 +8,7 @@ import unittest
 
 from hamcrest.core.core.isequal import equal_to
 from hamcrest.library.number.ordering_comparison import greater_than
-from hamcrest.library.object.haslength import *
+from hamcrest.library.object.haslength import has_length
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/object/hasproperty_test.py b/tests/hamcrest_unit_test/object/hasproperty_test.py
index 2b94f5b..4a25b76 100644
--- a/tests/hamcrest_unit_test/object/hasproperty_test.py
+++ b/tests/hamcrest_unit_test/object/hasproperty_test.py
@@ -7,7 +7,7 @@ if __name__ == "__main__":
 import unittest
 
 from hamcrest import greater_than
-from hamcrest.library.object.hasproperty import *
+from hamcrest.library.object.hasproperty import has_properties, has_property
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Chris Rose"
diff --git a/tests/hamcrest_unit_test/object/hasstring_test.py b/tests/hamcrest_unit_test/object/hasstring_test.py
index b510b67..e896461 100644
--- a/tests/hamcrest_unit_test/object/hasstring_test.py
+++ b/tests/hamcrest_unit_test/object/hasstring_test.py
@@ -7,7 +7,7 @@ if __name__ == "__main__":
 import unittest
 
 from hamcrest.core.core.isequal import equal_to
-from hamcrest.library.object.hasstring import *
+from hamcrest.library.object.hasstring import has_string
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/string_description_test.py b/tests/hamcrest_unit_test/string_description_test.py
index 27c8809..2e09225 100644
--- a/tests/hamcrest_unit_test/string_description_test.py
+++ b/tests/hamcrest_unit_test/string_description_test.py
@@ -3,7 +3,7 @@ import unittest
 
 import pytest
 from hamcrest.core.selfdescribing import SelfDescribing
-from hamcrest.core.string_description import *
+from hamcrest.core.string_description import StringDescription
 
 __author__ = "Jon Reid"
 __copyright__ = "Copyright 2011 hamcrest.org"
diff --git a/tests/hamcrest_unit_test/text/isequal_ignoring_whitespace_test.py b/tests/hamcrest_unit_test/text/isequal_ignoring_whitespace_test.py
index 494b11a..e7c729d 100644
--- a/tests/hamcrest_unit_test/text/isequal_ignoring_whitespace_test.py
+++ b/tests/hamcrest_unit_test/text/isequal_ignoring_whitespace_test.py
@@ -1,6 +1,6 @@
 import unittest
 
-from hamcrest.library.text.isequal_ignoring_whitespace import *
+from hamcrest.library.text.isequal_ignoring_whitespace import equal_to_ignoring_whitespace
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/text/stringcontains_test.py b/tests/hamcrest_unit_test/text/stringcontains_test.py
index 7f78190..2f7587d 100644
--- a/tests/hamcrest_unit_test/text/stringcontains_test.py
+++ b/tests/hamcrest_unit_test/text/stringcontains_test.py
@@ -1,6 +1,12 @@
 import pytest
 from hamcrest.library.text.stringcontains import contains_string
-from hamcrest_unit_test.matcher_test import *
+from hamcrest_unit_test.matcher_test import (
+    assert_description,
+    assert_does_not_match,
+    assert_matches,
+    assert_mismatch_description,
+    assert_no_mismatch_description,
+)
 
 __author__ = "Jon Reid"
 __copyright__ = "Copyright 2011 hamcrest.org"
@@ -59,4 +65,6 @@ def test_mismatch_description(matcher, text):
 
 
 if __name__ == "__main__":
+    import unittest
+
     unittest.main()
diff --git a/tests/hamcrest_unit_test/text/stringcontainsinorder_test.py b/tests/hamcrest_unit_test/text/stringcontainsinorder_test.py
index 178f58e..c11669c 100644
--- a/tests/hamcrest_unit_test/text/stringcontainsinorder_test.py
+++ b/tests/hamcrest_unit_test/text/stringcontainsinorder_test.py
@@ -1,57 +1,48 @@
-if __name__ == "__main__":
-    import sys
-
-    sys.path.insert(0, "..")
-    sys.path.insert(0, "../..")
-
-import unittest
-
-from hamcrest.core.string_description import StringDescription
-from hamcrest.library.text import string_contains_in_order
-from hamcrest_unit_test.matcher_test import MatcherTest
-
-__author__ = "Romilly Cocking"
-__copyright__ = "Copyright 2011 hamcrest.org"
-__license__ = "BSD, see License.txt"
-
-
-matcher = string_contains_in_order("string one", "string two", "string three")
-
-
-class StringContainsInOrderTest(MatcherTest):
-    def testMatchesIfOrderIsCorrect(self):
-        self.assert_matches(
-            "correct order", matcher, "string one then string two followed by string three"
-        )
-
-    def testDoesNotMatchIfOrderIsIncorrect(self):
-        self.assert_does_not_match(
-            "incorrect order", matcher, "string two then string one followed by string three"
-        )
-
-    def testDoesNotMatchIfExpectedSubstringsAreMissing(self):
-        self.assert_does_not_match("missing string one", matcher, "string two then string three")
-        self.assert_does_not_match("missing string two", matcher, "string one then string three")
-        self.assert_does_not_match("missing string three", matcher, "string one then string two")
-
-    def testMatcherCreationRequiresString(self):
-        self.assertRaises(TypeError, string_contains_in_order, 3)
-
-    def testFailsIfMatchingAgainstNonString(self):
-        self.assert_does_not_match("non-string", matcher, object())
-
-    def testHasAReadableDescription(self):
-        self.assert_description(
-            "a string containing 'string one', 'string two', 'string three' in order", matcher
-        )
-
-    def testSuccessfulMatchDoesNotGenerateMismatchDescription(self):
-        self.assert_no_mismatch_description(
-            matcher, "string one then string two followed by string three"
-        )
-
-    def testMismatchDescription(self):
-        self.assert_mismatch_description("was 'bad'", matcher, "bad")
-
-    def testDescribeMismatch(self):
-        self.assert_describe_mismatch("was 'bad'", matcher, "bad")
+from hamcrest.library.text import string_contains_in_order
+from hamcrest_unit_test.matcher_test import MatcherTest
+
+__author__ = "Romilly Cocking"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+matcher = string_contains_in_order("string one", "string two", "string three")
+
+
+class StringContainsInOrderTest(MatcherTest):
+    def testMatchesIfOrderIsCorrect(self):
+        self.assert_matches(
+            "correct order", matcher, "string one then string two followed by string three"
+        )
+
+    def testDoesNotMatchIfOrderIsIncorrect(self):
+        self.assert_does_not_match(
+            "incorrect order", matcher, "string two then string one followed by string three"
+        )
+
+    def testDoesNotMatchIfExpectedSubstringsAreMissing(self):
+        self.assert_does_not_match("missing string one", matcher, "string two then string three")
+        self.assert_does_not_match("missing string two", matcher, "string one then string three")
+        self.assert_does_not_match("missing string three", matcher, "string one then string two")
+
+    def testMatcherCreationRequiresString(self):
+        self.assertRaises(TypeError, string_contains_in_order, 3)
+
+    def testFailsIfMatchingAgainstNonString(self):
+        self.assert_does_not_match("non-string", matcher, object())
+
+    def testHasAReadableDescription(self):
+        self.assert_description(
+            "a string containing 'string one', 'string two', 'string three' in order", matcher
+        )
+
+    def testSuccessfulMatchDoesNotGenerateMismatchDescription(self):
+        self.assert_no_mismatch_description(
+            matcher, "string one then string two followed by string three"
+        )
+
+    def testMismatchDescription(self):
+        self.assert_mismatch_description("was 'bad'", matcher, "bad")
+
+    def testDescribeMismatch(self):
+        self.assert_describe_mismatch("was 'bad'", matcher, "bad")
diff --git a/tests/hamcrest_unit_test/text/stringendswith_test.py b/tests/hamcrest_unit_test/text/stringendswith_test.py
index a3c66c7..013ff17 100644
--- a/tests/hamcrest_unit_test/text/stringendswith_test.py
+++ b/tests/hamcrest_unit_test/text/stringendswith_test.py
@@ -1,6 +1,6 @@
 import unittest
 
-from hamcrest.library.text.stringendswith import *
+from hamcrest.library.text.stringendswith import ends_with
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/hamcrest_unit_test/text/stringmatches_test.py b/tests/hamcrest_unit_test/text/stringmatches_test.py
index b461673..d10f1cb 100644
--- a/tests/hamcrest_unit_test/text/stringmatches_test.py
+++ b/tests/hamcrest_unit_test/text/stringmatches_test.py
@@ -4,9 +4,10 @@ if __name__ == "__main__":
     sys.path.insert(0, "..")
     sys.path.insert(0, "../..")
 
+import re
 import unittest
 
-from hamcrest.library.text.stringmatches import *
+from hamcrest.library.text.stringmatches import matches_regexp
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Chris Rose"
diff --git a/tests/hamcrest_unit_test/text/stringstartswith_test.py b/tests/hamcrest_unit_test/text/stringstartswith_test.py
index a97b9b7..8ac58f0 100644
--- a/tests/hamcrest_unit_test/text/stringstartswith_test.py
+++ b/tests/hamcrest_unit_test/text/stringstartswith_test.py
@@ -1,6 +1,6 @@
 import unittest
 
-from hamcrest.library.text.stringstartswith import *
+from hamcrest.library.text.stringstartswith import starts_with
 from hamcrest_unit_test.matcher_test import MatcherTest
 
 __author__ = "Jon Reid"
diff --git a/tests/object_import.py b/tests/object_import.py
index 5dc0954..e57c5a4 100644
--- a/tests/object_import.py
+++ b/tests/object_import.py
@@ -3,7 +3,6 @@ try:
     class MyTest(object):
         pass
 
-
 except TypeError:
     print("Object class defined at {0}".format(getattr(object, "__file__", "NOWHERE")))
     raise
diff --git a/tests/type-hinting/core/core/test_is.yml b/tests/type-hinting/core/core/test_is.yml
new file mode 100644
index 0000000..2f601da
--- /dev/null
+++ b/tests/type-hinting/core/core/test_is.yml
@@ -0,0 +1,12 @@
+- case: is
+  # pypy + mypy doesn't work. See https://foss.heptapod.net/pypy/pypy/-/issues/3526
+  skip: platform.python_implementation() == "PyPy"
+  main: |
+    from hamcrest import assert_that, is_, empty
+    from typing import Any, Sequence
+
+    a: Sequence[Any] = []
+    b = 99
+
+    assert_that(a, is_(empty()))
+    assert_that(b, is_(empty()))  # E: Cannot infer type argument 1 of "assert_that"
diff --git a/tests/type-hinting/core/test_assert_that.yml b/tests/type-hinting/core/test_assert_that.yml
new file mode 100644
index 0000000..897a0ec
--- /dev/null
+++ b/tests/type-hinting/core/test_assert_that.yml
@@ -0,0 +1,11 @@
+- case: assert_that
+  # pypy + mypy doesn't work. See https://foss.heptapod.net/pypy/pypy/-/issues/3526
+  skip: platform.python_implementation() == "PyPy"
+  main: |
+    from hamcrest import assert_that, instance_of, starts_with
+
+    assert_that("string", starts_with("str"))
+    assert_that("str", instance_of(str))
+    assert_that(99, starts_with("str"))
+  out: |
+    main:5: error: Cannot infer type argument 1 of "assert_that"
diff --git a/tests/type-hinting/library/collection/test_empty.yml b/tests/type-hinting/library/collection/test_empty.yml
new file mode 100644
index 0000000..5264802
--- /dev/null
+++ b/tests/type-hinting/library/collection/test_empty.yml
@@ -0,0 +1,8 @@
+- case: empty
+  # pypy + mypy doesn't work. See https://foss.heptapod.net/pypy/pypy/-/issues/3526
+  skip: platform.python_implementation() == "PyPy"
+  main: |
+    from hamcrest import assert_that, is_, empty
+
+    assert_that([], empty())
+    assert_that(99, empty())  # E: Cannot infer type argument 1 of "assert_that"
\ No newline at end of file
diff --git a/tests/type-hinting/library/text/test_equal_to_ignoring_case.yml b/tests/type-hinting/library/text/test_equal_to_ignoring_case.yml
new file mode 100644
index 0000000..0853707
--- /dev/null
+++ b/tests/type-hinting/library/text/test_equal_to_ignoring_case.yml
@@ -0,0 +1,12 @@
+- case: equal_to_ignoring_case
+  # pypy + mypy doesn't work. See https://foss.heptapod.net/pypy/pypy/-/issues/3526
+  skip: platform.python_implementation() == "PyPy"
+  main: |
+    from hamcrest import equal_to_ignoring_case
+
+    reveal_type(equal_to_ignoring_case(""))
+    equal_to_ignoring_case("")
+    equal_to_ignoring_case(99)
+  out: |
+    main:3: note: Revealed type is "hamcrest.core.matcher.Matcher[builtins.str]"
+    main:5: error: Argument 1 to "equal_to_ignoring_case" has incompatible type "int"; expected "str"
diff --git a/tests/type-hinting/test.yml b/tests/type-hinting/test.yml
deleted file mode 100644
index 0abd968..0000000
--- a/tests/type-hinting/test.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-- case: equal_to_ignoring_case
-  main: |
-    from hamcrest.library.text.isequal_ignoring_case import equal_to_ignoring_case
-
-    reveal_type(equal_to_ignoring_case(""))  # N: Revealed type is 'builtins.str*'
diff --git a/tox.ini b/tox.ini
index 341732d..037665a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,109 +1,170 @@
+[pytest]
+addopts = -ra
+testpaths = tests
+xfail_strict = true
+filterwarnings =
+    once::Warning
+    ignore:::pympler[.*]
+looponfailroots =
+    src
+    tests
+
+# Keep docs in sync with docs env and .readthedocs.yml.
+[gh-actions]
+python =
+    3.6: py36, py36-numpy
+    3.7: py37, py37-numpy
+    3.8: py38, py38-numpy
+    3.9: py39, py39-numpy, lint, manifest, typing, changelog, docs
+    3.10: py310
+    pypy-2: pypy2
+    pypy-3: pypy3
+
+
 [tox]
-envlist = py35,py36,py37,py38,pypy2.7,pypy3.6,docs-py3
-# Jython is not testable, but there's no reason it should not work.
+envlist = typing,lint,py36{,-numpy},py37{,-numpy},py38{,-numpy},py39{,-numpy},py310{,-numpy},pypy{,-numpy},pypy3{,-numpy},manifest,docs,pypi-description,changelog,coverage-report
+isolated_build = True
+
 
 [testenv]
-commands  = {envbindir}/py.test []
-            {envpython} tests/object_import.py
-deps      = pytest~=5.0
-            pytest-cov~=2.0
+# Prevent random setuptools/pip breakages like
+# https://github.com/pypa/setuptools/issues/1042 from breaking our builds.
+setenv =
+    VIRTUALENV_NO_DOWNLOAD=1
+extras = {env:TOX_AP_TEST_EXTRAS:tests}
+commands = python -m pytest {posargs}
+
 
-[testenv:jython]
-deps     = pytest 
-commands = {envbindir}/jython tests/alltests.py []
-           {envpython} tests/object_import.py
+[testenv:py27]
+extras = {env:TOX_AP_TEST_EXTRAS:tests}
+commands = coverage run -m pytest {posargs}
 
 [testenv:py36-numpy]
-basepython = python3.6
-deps      = {[testenv]deps}
-            numpy
-setenv    =
-            PYTHONHASHSEED = 4
-
-[testenv:docs-py3]
-basepython = python3.6
-deps       = sphinx
-             sphinx_rtd_theme
-changedir  = {toxinidir}/doc
-commands   = sphinx-build -W -b html -d {envtmpdir}/doctrees .  {envtmpdir}/html
-
-[testenv:format]
-basepython = python3
-skip_install = true
-deps =
-    black~=19.10b0
-    isort~=4.0
-commands =
-    isort {toxinidir}/setup.py
-    isort -rc {toxinidir}/src/
-    isort -rc {toxinidir}/tests/
-    black -l100 -tpy35 src/ tests/ setup.py
+extras = tests-numpy
+commands = python -m pytest {posargs}
+
+[testenv:py37-numpy]
+extras = tests-numpy
+commands = python -m pytest {posargs}
+
+[testenv:py38-numpy]
+extras = tests-numpy
+commands = python -m pytest {posargs}
+
+[testenv:py39-numpy]
+extras = tests-numpy
+commands = python -m pytest {posargs}
 
-[testenv:check-format]
-basepython = python3
+[testenv:py37]
+# Python 3.6+ has a number of compile-time warnings on invalid string escapes.
+# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run.
+install_command = pip install --no-compile {opts} {packages}
+setenv =
+    PYTHONWARNINGS=d
+extras = {env:TOX_AP_TEST_EXTRAS:tests}
+commands = coverage run -m pytest {posargs}
+
+
+[testenv:py38]
+# Python 3.6+ has a number of compile-time warnings on invalid string escapes.
+# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run.
+basepython = python3.8
+install_command = pip install --no-compile {opts} {packages}
+setenv =
+    PYTHONWARNINGS=d
+extras = {env:TOX_AP_TEST_EXTRAS:tests}
+commands = coverage run -m pytest {posargs}
+
+
+[testenv:py39]
+# Python 3.6+ has a number of compile-time warnings on invalid string escapes.
+# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run.
+basepython = python3.9
+install_command = pip install --no-compile {opts} {packages}
+setenv =
+    PYTHONWARNINGS=d
+extras = {env:TOX_AP_TEST_EXTRAS:tests}
+commands = coverage run -m pytest {posargs}
+
+[testenv:py310]
+# Python 3.6+ has a number of compile-time warnings on invalid string escapes.
+# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run.
+basepython = python3.10
+install_command = pip install --no-compile {opts} {packages}
+setenv =
+    PYTHONWARNINGS=d
+extras = {env:TOX_AP_TEST_EXTRAS:tests}
+commands = coverage run -m pytest {posargs}
+
+[testenv:coverage-report]
+basepython = python3.9
 skip_install = true
-deps = {[testenv:format]deps}
+deps = coverage[toml]>=5.0.2
 commands =
-    isort --check-only {toxinidir}/setup.py
-    isort --check-only -rc {toxinidir}/src/
-    isort --check-only -rc {toxinidir}/tests/
-    black --check -l100 -tpy35 src/ tests/ setup.py
-
-[tool:isort]
-multi_line_output=3
-include_trailing_comma=True
-force_grid_wrap=0
-use_parentheses=True
-line_length=100
-
-[testenv:flake8]
-basepython = python3
+    coverage combine
+    coverage report
+
+
+[testenv:lint]
+basepython = python3.9
 skip_install = true
 deps =
-    flake8~=3.0
-    flake8-bugbear~=18.0
-    flake8-comprehensions~=1.0
-    flake8-mutable~=1.0
-    mccabe~=0.6
-    flake8-blind-except~=0.1
-    flake8-builtins~=1.0
-    flake8-pep3101~=1.0
-    flake8-print~=3.0
-    flake8-string-format~=0.2
-    flake8-logging-format~=0.5
+    pre-commit
+passenv = HOMEPATH  # needed on Windows
+commands =
+    pre-commit run --all-files
+
 
+[testenv:docs]
+# Keep basepython in sync with gh-actions and .readthedocs.yml.
+basepython = python3.9
+extras = docs
 commands =
-    flake8 src/ tests/ setup.py
+    sphinx-build -n -T -b html -d {envtmpdir}/doctrees doc doc/_build/html
 
-[flake8]
-max-complexity = 5
-max-line-length = 100
-show-source = True
-enable-extensions = M,B,C,T,P
-ignore = C812,W503,P103,E1,E2,E3,E5
-statistics = True
 
-[testenv:mypy]
-basepython = python3.8
+[testenv:manifest]
+basepython = python3.9
+deps = check-manifest
 skip_install = true
-deps =
-    mypy~=0.6
-commands =
-    mypy src/ tests/ --ignore-missing-imports {posargs}
+commands = check-manifest
 
-[testenv:test-hinting]
-basepython = python3
+
+[testenv:pypi-description]
+basepython = python3.9
 skip_install = true
 deps =
-    pytest-mypy-plugins~=1.0
+    twine
+    pip >= 18.0.0
 commands =
-    pytest --mypy-ini-file=tox.ini tests/type-hinting/ {posargs}
+    pip wheel -w {envtmpdir}/build --no-deps .
+    twine check {envtmpdir}/build/*
+
 
-[testenv:pyre]
-basepython = python3.7
+[testenv:changelog]
+basepython = python3.9
+deps = towncrier
 skip_install = true
+commands = towncrier --draft
+
+
+[testenv:typing]
+basepython = python3.9
 deps =
-    pyre-check
+    mypy
+    types-mock
 commands =
-    pyre --source-directory src/ check {posargs}
-    pyre --source-directory tests/ --search-path src/ check {posargs}
+    mypy src/
+
+
+
+[flake8]
+max-complexity = 15
+max-line-length = 100
+show-source = True
+enable-extensions = M,B,C,T,P
+ignore = C812,W503,P103,E1,E2,E3,E5
+statistics = True
+per-file-ignores =
+    src/*/__init__.py:F401,F403,F405
+    src/hamcrest/__init__.py:F401,F403