diff --git a/bot/setup.py b/bot/setup.py
index a008d2c452645fa6e83b6d965d3fc21a2e412476..e8b5e363dcc5752f7456babaa41cd0215ca27d5d 100755
--- a/bot/setup.py
+++ b/bot/setup.py
@@ -1,10 +1,65 @@
 #!/usr/bin/env python
 
+import os
+import subprocess
+import sys
+
 from setuptools import find_packages, setup
+from setuptools.command.develop import develop
 
 from reviewbot import get_package_version
 
 
+class DevelopCommand(develop):
+    """Installs Review Bot in developer mode.
+
+    This will install all standard and development dependencies and add the
+    source tree to the Python module search path.
+
+    Version Added:
+        3.2.1
+    """
+
+    def install_for_development(self):
+        """Install the package for development.
+
+        This takes care of the work of installing all dependencies.
+        """
+        if self.no_deps:
+            # In this case, we don't want to install any of the dependencies
+            # below. However, it's really unlikely that a user is going to
+            # want to pass --no-deps.
+            #
+            # Instead, what this really does is give us a way to know we've
+            # been called by `pip install -e .`. That will call us with
+            # --no-deps, as it's going to actually handle all dependency
+            # installation, rather than having easy_install do it.
+            develop.install_for_development(self)
+            return
+
+        # Install the dependencies using pip instead of easy_install. This
+        # will use wheels instead of legacy eggs.
+        self._run_pip(['install', '-e', '.'])
+        self._run_pip(['install', '-r', 'dev-requirements.txt'])
+
+    def _run_pip(self, args):
+        """Run pip.
+
+        Args:
+            args (list):
+                Arguments to pass to :command:`pip`.
+
+        Raises:
+            RuntimeError:
+                The :command:`pip` command returned a non-zero exit code.
+        """
+        cmd = subprocess.list2cmdline([sys.executable, '-m', 'pip'] + args)
+        ret = os.system(cmd)
+
+        if ret != 0:
+            raise RuntimeError('Failed to run `%s`' % cmd)
+
+
 with open('README.rst', 'r') as fp:
     long_description = fp.read()
 
@@ -97,6 +152,9 @@ setup(
         '!=3.4.*',
         '!=3.5.*',
     ]),
+    cmdclass={
+        'develop': DevelopCommand,
+    },
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Environment :: Console',
diff --git a/extension/setup.py b/extension/setup.py
index 79e8a3fa9b0811ec3f6323f159a937ca4fea7ea9..16812130622405ce5cb4ad14c0a661badceb0546 100755
--- a/extension/setup.py
+++ b/extension/setup.py
@@ -1,11 +1,65 @@
 #!/usr/bin/env python
 
+import os
+import subprocess
+import sys
+
 from reviewboard.extensions.packaging import setup
 from setuptools import find_packages
+from setuptools.command.develop import develop
 
 from reviewbotext import get_package_version
 
 
+class DevelopCommand(develop):
+    """Installs the Review Bot extension in developer mode.
+
+    This will install all standard and development dependencies and add the
+    source tree to the Python module search path.
+
+    Version Added:
+        3.2.1
+    """
+
+    def install_for_development(self):
+        """Install the package for development.
+
+        This takes care of the work of installing all dependencies.
+        """
+        if self.no_deps:
+            # In this case, we don't want to install any of the dependencies
+            # below. However, it's really unlikely that a user is going to
+            # want to pass --no-deps.
+            #
+            # Instead, what this really does is give us a way to know we've
+            # been called by `pip install -e .`. That will call us with
+            # --no-deps, as it's going to actually handle all dependency
+            # installation, rather than having easy_install do it.
+            develop.install_for_development(self)
+            return
+
+        # Install the dependencies using pip instead of easy_install. This
+        # will use wheels instead of legacy eggs.
+        self._run_pip(['install', '-e', '.'])
+
+    def _run_pip(self, args):
+        """Run pip.
+
+        Args:
+            args (list):
+                Arguments to pass to :command:`pip`.
+
+        Raises:
+            RuntimeError:
+                The :command:`pip` command returned a non-zero exit code.
+        """
+        cmd = subprocess.list2cmdline([sys.executable, '-m', 'pip'] + args)
+        ret = os.system(cmd)
+
+        if ret != 0:
+            raise RuntimeError('Failed to run `%s`' % cmd)
+
+
 with open('README.rst', 'r') as fp:
     long_description = fp.read()
 
@@ -55,6 +109,9 @@ setup(
         '!=3.4.*',
         '!=3.5.*',
     ]),
+    cmdclass={
+        'develop': DevelopCommand,
+    },
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Environment :: Web Environment',
