diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0d20b64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..1215eb6
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,55 @@
+repos:
+-   repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.4.0
+    hooks:
+    -   id: trailing-whitespace
+    -   id: end-of-file-fixer
+    -   id: check-yaml
+    -   id: debug-statements
+    -   id: double-quote-string-fixer
+    -   id: name-tests-test
+    -   id: requirements-txt-fixer
+-   repo: https://github.com/asottile/reorder_python_imports
+    rev: v3.9.0
+    hooks:
+    -   id: reorder-python-imports
+        args: [--py37-plus, --add-import, 'from __future__ import annotations']
+-   repo: https://github.com/asottile/add-trailing-comma
+    rev: v2.4.0
+    hooks:
+    -   id: add-trailing-comma
+        args: [--py36-plus]
+-   repo: https://github.com/asottile/pyupgrade
+    rev: v3.3.1
+    hooks:
+    -   id: pyupgrade
+        args: [--py37-plus]
+-   repo: https://github.com/pre-commit/mirrors-autopep8
+    rev: v2.0.1
+    hooks:
+    -   id: autopep8
+-   repo: https://github.com/PyCQA/flake8
+    rev: 6.0.0
+    hooks:
+    -   id: flake8
+-   repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v0.991
+    hooks:
+    -   id: mypy
+        additional_dependencies: [types-all]
+-   repo: local
+    hooks:
+    -   id: generate-readme
+        name: generate readme
+        entry: ./generate-readme
+        language: python
+        additional_dependencies: [pyyaml]
+        files: ^(\.pre-commit-hooks.yaml|generate-readme)$
+        pass_filenames: false
+    -   id: run-tests
+        name: run tests
+        entry: pytest tests
+        language: python
+        additional_dependencies: [pre-commit, pytest]
+        always_run: true
+        pass_filenames: false
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..dc9dc20
--- /dev/null
+++ b/.pre-commit-hooks.yaml
@@ -0,0 +1,79 @@
+-   id: python-check-blanket-noqa
+    name: check blanket noqa
+    description: 'Enforce that `noqa` annotations always occur with specific codes. Sample annotations: `# noqa: F401`, `# noqa: F401,W203`'
+    entry: '(?i)# noqa(?!: )'
+    language: pygrep
+    types: [python]
+-   id: python-check-blanket-type-ignore
+    name: check blanket type ignore
+    description: 'Enforce that `# type: ignore` annotations always occur with specific codes. Sample annotations: `# type: ignore[attr-defined]`, `# type: ignore[attr-defined, name-defined]`'
+    entry: '# type:? *ignore(?!\[|\w)'
+    language: pygrep
+    types: [python]
+-   id: python-check-mock-methods
+    name: check for not-real mock methods
+    description: >-
+        Prevent common mistakes of `assert mck.not_called()`, `assert mck.called_once_with(...)`
+        and `mck.assert_called`.
+    language: pygrep
+    entry: >
+        (?x)(
+            assert .*\.(
+                not_called|
+                called_
+            )|
+            # ''.join(rf'(?<!\b{s})' for s in dir(mock) if s.endswith('Mock')))
+            (?<!\bAsyncMock)(?<!\bMagicMock)(?<!\bMock)(?<!\bNonCallableMagicMock)(?<!\bNonCallableMock)(?<!\bPropertyMock)
+            \.assert_(
+                any_call|
+                called|
+                called_once|
+                called_once_with|
+                called_with|
+                has_calls|
+                not_called
+            )($|[^(\w])
+        )
+    types: [python]
+-   id: python-no-eval
+    name: check for eval()
+    description: 'A quick check for the `eval()` built-in function'
+    entry: '\beval\('
+    language: pygrep
+    types: [python]
+-   id: python-no-log-warn
+    name: use logger.warning(
+    description: 'A quick check for the deprecated `.warn()` method of python loggers'
+    entry: '(?<!warnings)\.warn\('
+    language: pygrep
+    types: [python]
+-   id: python-use-type-annotations
+    name: type annotations not comments
+    description: 'Enforce that python3.6+ type annotations are used instead of type comments'
+    entry: '# type(?!: *ignore([^a-zA-Z0-9]|$))'
+    language: pygrep
+    types: [python]
+-   id: rst-backticks
+    name: rst ``code`` is two backticks
+    description: 'Detect common mistake of using single backticks when writing rst'
+    entry: '^(?!    ).*(^| )`[^`]+`([^_]|$)'
+    language: pygrep
+    types: [rst]
+-   id: rst-directive-colons
+    name: rst directives end with two colons
+    description: 'Detect mistake of rst directive not ending with double colon or space before the double colon'
+    entry: '^\s*\.\. [a-z]+(| | :):$'
+    language: pygrep
+    types: [rst]
+-   id: rst-inline-touching-normal
+    name: rst ``inline code`` next to normal text
+    description: 'Detect mistake of inline code touching normal text in rst'
+    entry: '\w``\w'
+    language: pygrep
+    types: [rst]
+-   id: text-unicode-replacement-char
+    name: no unicode replacement chars
+    description: 'Forbid files which have a UTF-8 Unicode replacement character'
+    entry: "\uFFFD"
+    language: pygrep
+    types: [text]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b7af5ef
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2018 Anthony Sottile
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..022e3dc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pygrep-hooks/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pygrep-hooks/main)
+
+pygrep-hooks
+============
+
+A collection of fast, cheap, regex based pre-commit hooks.
+
+
+### Adding to your `.pre-commit-config.yaml`
+
+```yaml
+-   repo: https://github.com/pre-commit/pygrep-hooks
+    rev: v1.10.0  # Use the ref you want to point at
+    hooks:
+    -   id: python-use-type-annotations
+    # ...
+```
+
+### Naming conventions
+
+Where possible, these hooks will be prefixed with the file types they target.
+For example, a hook which targets python will be called `python-...`.
+
+### Provided hooks
+
+[generated]: # (generated)
+- **`python-check-blanket-noqa`**: Enforce that `noqa` annotations always occur with specific codes. Sample annotations: `# noqa: F401`, `# noqa: F401,W203`
+- **`python-check-blanket-type-ignore`**: Enforce that `# type: ignore` annotations always occur with specific codes. Sample annotations: `# type: ignore[attr-defined]`, `# type: ignore[attr-defined, name-defined]`
+- **`python-check-mock-methods`**: Prevent common mistakes of `assert mck.not_called()`, `assert mck.called_once_with(...)` and `mck.assert_called`.
+- **`python-no-eval`**: A quick check for the `eval()` built-in function
+- **`python-no-log-warn`**: A quick check for the deprecated `.warn()` method of python loggers
+- **`python-use-type-annotations`**: Enforce that python3.6+ type annotations are used instead of type comments
+- **`rst-backticks`**: Detect common mistake of using single backticks when writing rst
+- **`rst-directive-colons`**: Detect mistake of rst directive not ending with double colon or space before the double colon
+- **`rst-inline-touching-normal`**: Detect mistake of inline code touching normal text in rst
+- **`text-unicode-replacement-char`**: Forbid files which have a UTF-8 Unicode replacement character
diff --git a/generate-readme b/generate-readme
new file mode 100755
index 0000000..ac7dd26
--- /dev/null
+++ b/generate-readme
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import yaml
+
+Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader)
+
+
+def main() -> int:
+    with open('.pre-commit-hooks.yaml') as f:
+        hooks = yaml.load(f, Loader=Loader)
+
+    with open('README.md') as f:
+        contents = f.read()
+    before, delim, _ = contents.partition('[generated]: # (generated)\n')
+
+    rest = '\n'.join(
+        f'- **`{hook["id"]}`**: {hook["description"]}' for hook in hooks
+    )
+
+    with open('README.md', 'w') as f:
+        f.write(before + delim + rest + '\n')
+
+    return 0
+
+
+if __name__ == '__main__':
+    raise SystemExit(main())
diff --git a/tests/hooks_test.py b/tests/hooks_test.py
new file mode 100644
index 0000000..6cee816
--- /dev/null
+++ b/tests/hooks_test.py
@@ -0,0 +1,274 @@
+from __future__ import annotations
+
+import re
+
+import pytest
+from pre_commit.clientlib import load_manifest
+from pre_commit.constants import MANIFEST_FILE
+
+HOOKS = {h['id']: re.compile(h['entry']) for h in load_manifest(MANIFEST_FILE)}
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        'x = 1 # type: ignoreme',
+        'x = 1  # type: int',
+        'x = 1  # type int',
+        'x = 1  # type: int  # noqa',
+    ),
+)
+def test_python_use_type_annotations_positive(s):
+    assert HOOKS['python-use-type-annotations'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        'x = 1',
+        'x = 1  # type:ignore',
+        'x = 1  # type: ignore',
+        'x = 1  # type:  ignore',
+        'x = 1  # type: ignore # noqa',
+        'x = 1  # type: ignore  # noqa',
+        'x = 1  # type: ignore[type-mismatch]',
+        'x = 1  # type: ignore=E123',
+    ),
+)
+def test_python_use_type_annotations_negative(s):
+    assert not HOOKS['python-use-type-annotations'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        '# noqa',
+        '# NOQA',
+        '# noqa:F401',
+        '# noqa:F401,W203',
+    ),
+)
+def test_python_check_blanket_noqa_positive(s):
+    assert HOOKS['python-check-blanket-noqa'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        'x = 1',
+        '# noqa: F401',
+        '# noqa: F401, W203',
+    ),
+)
+def test_python_check_blanket_noqa_negative(s):
+    assert not HOOKS['python-check-blanket-noqa'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        'x = 1  # type: ignore',
+        'x = 1  # type ignore',
+        'x = 1  # type:ignore',
+        'x = 1  # type ignore  # noqa',
+    ),
+)
+def test_python_check_blanket_type_ignore_positive(s):
+    assert HOOKS['python-check-blanket-type-ignore'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        'x = 1',
+        'x = 1  # type: ignore[attr-defined]',
+        'x = 1  # type: ignore[attr-defined, name-defined]',
+        'x = 1  # type: ignore[type-mismatch]  # noqa',
+        'x = 1  # type: Union[int, str]',
+        'x = 1  # type: ignoreme',
+    ),
+)
+def test_python_check_blanket_type_ignore_negative(s):
+    assert not HOOKS['python-check-blanket-type-ignore'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        'assert my_mock.not_called()',
+        'assert my_mock.called_once_with()',
+        'my_mock.assert_not_called',
+        'my_mock.assert_called',
+        'my_mock.assert_called_once_with',
+        'my_mock.assert_called_once_with# noqa',
+        'MyMock.assert_called_once_with',
+    ),
+)
+def test_python_check_mock_methods_positive(s):
+    assert HOOKS['python-check-mock-methods'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        'assert my_mock.call_count == 1',
+        'assert my_mock.called',
+        'my_mock.assert_not_called()',
+        'my_mock.assert_called()',
+        'my_mock.assert_called_once_with()',
+        '"""like :meth:`Mock.assert_called_once_with`"""',
+        '"""like :meth:`MagicMock.assert_called_once_with`"""',
+    ),
+)
+def test_python_check_mock_methods_negative(s):
+    assert not HOOKS['python-check-mock-methods'].search(s)
+
+
+def test_python_noeval_positive():
+    assert HOOKS['python-no-eval'].search('eval("3 + 4")')
+
+
+def test_python_noeval_negative():
+    assert not HOOKS['python-no-eval'].search('literal_eval("{1: 2}")')
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        'log.warn("this is deprecated")',
+    ),
+)
+def test_python_no_log_warn_positive(s):
+    assert HOOKS['python-no-log-warn'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        "warnings.warn('this is ok')",
+        'log.warning("this is ok")',
+        'from warnings import warn',
+        'warn("by itself is also ok")',
+    ),
+)
+def test_python_no_log_warn_negative(s):
+    assert not HOOKS['python-no-log-warn'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        '`[code]`',
+        'i like `_kitty`',
+        'i like `_`',
+        '`a`',
+        '`cd`',
+        ' `indented` literal block',
+        '> quoted `literal` block',
+    ),
+)
+def test_python_rst_backticks_positive(s):
+    assert HOOKS['rst-backticks'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        ' ``[code]``',
+        'i like _`kitty`',
+        'i like `kitty`_',
+        '``b``',
+        '``ef``',
+        '    indented `literal` block',
+    ),
+)
+def test_python_rst_backticks_negative(s):
+    assert not HOOKS['rst-backticks'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        '``PyMem_Realloc()`` indirectly call``PyObject_Malloc()`` and',
+        'This PEP proposes that ``bytes`` and ``bytearray``gain an optimised',
+        'Reading this we first see the``break``, which obviously applies to',
+        'for using``long_description`` and a corresponding',
+        '``inline`` normal``inline',
+        '``inline``normal ``inline',
+        '``inline``normal',
+        '``inline``normal``inline',
+        'normal ``inline``normal',
+        'normal``inline`` normal',
+        'normal``inline``',
+        'normal``inline``normal',
+    ),
+)
+def test_python_rst_inline_touching_normal_positive(s):
+    assert HOOKS['rst-inline-touching-normal'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        '``PyMem_Realloc()`` indirectly call ``PyObject_Malloc()`` and',
+        'This PEP proposes that ``bytes`` and ``bytearray`` gain an optimised',
+        'Reading this we first see the ``break``, which obviously applies to',
+        'for using ``long_description`` and a corresponding',
+        '``inline`` normal ``inline',
+        '``inline`` normal',
+        'normal ``inline`` normal',
+        'normal ``inline``',
+    ),
+)
+def test_python_rst_inline_touching_normal_negative(s):
+    assert not HOOKS['rst-inline-touching-normal'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        str(b'\x80abc', errors='replace'),
+    ),
+)
+def test_text_unicode_replacement_char_positive(s):
+    assert HOOKS['text-unicode-replacement-char'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        'foo',
+    ),
+)
+def test_text_unicode_replacement_char_negative(s):
+    assert not HOOKS['text-unicode-replacement-char'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        '    .. warning:',
+        '.. warning:',
+        '    .. warning ::',
+        '.. warning ::',
+        '    .. warning :',
+        '.. warning :',
+    ),
+)
+def test_rst_directive_colons_positive(s):
+    assert HOOKS['rst-directive-colons'].search(s)
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        '.. warning::',
+        '.. code:: python',
+    ),
+)
+def test_rst_directive_colons_negative(s):
+    assert not HOOKS['rst-directive-colons'].search(s)
+
+
+def test_that_hooks_are_sorted():
+    assert list(HOOKS) == sorted(HOOKS)