[#8396] remove dependency on ipython
diff --git a/Allura/allura/command/show_models.py b/Allura/allura/command/show_models.py
index f4623de..b0fb3ac 100644
--- a/Allura/allura/command/show_models.py
+++ b/Allura/allura/command/show_models.py
@@ -385,25 +385,3 @@
     for node in graph[root][1]:
         for r in dfs(node, graph, depth + 1):
             yield r
-
-
-def pm(etype, value, tb):  # pragma no cover
-    import pdb
-    import traceback
-    try:
-        from IPython.ipapi import make_session
-        make_session()
-        from IPython.Debugger import Pdb
-        sys.stderr.write('Entering post-mortem IPDB shell\n')
-        p = Pdb(color_scheme='Linux')
-        p.reset()
-        p.setup(None, tb)
-        p.print_stack_trace()
-        sys.stderr.write('%s: %s\n' % (etype, value))
-        p.cmdloop()
-        p.forget()
-        # p.interaction(None, tb)
-    except ImportError:
-        sys.stderr.write('Entering post-mortem PDB shell\n')
-        traceback.print_exception(etype, value, tb)
-        pdb.post_mortem(tb)
diff --git a/Allura/allura/lib/utils.py b/Allura/allura/lib/utils.py
index 3f566a3..5adc4f4 100644
--- a/Allura/allura/lib/utils.py
+++ b/Allura/allura/lib/utils.py
@@ -440,23 +440,9 @@
     import sys
     import pdb
     import traceback
-    try:
-        from IPython.ipapi import make_session
-        make_session()
-        from IPython.Debugger import Pdb
-        sys.stderr.write('Entering post-mortem IPDB shell\n')
-        p = Pdb(color_scheme='Linux')
-        p.reset()
-        p.setup(None, tb)
-        p.print_stack_trace()
-        sys.stderr.write('%s: %s\n' % (etype, value))
-        p.cmdloop()
-        p.forget()
-        # p.interaction(None, tb)
-    except ImportError:
-        sys.stderr.write('Entering post-mortem PDB shell\n')
-        traceback.print_exception(etype, value, tb)
-        pdb.post_mortem(tb)
+    sys.stderr.write('Entering post-mortem PDB shell\n')
+    traceback.print_exception(etype, value, tb)
+    pdb.post_mortem(tb)
 
 
 class LineAnchorCodeHtmlFormatter(HtmlFormatter):
diff --git a/Allura/allura/tests/functional/test_root.py b/Allura/allura/tests/functional/test_root.py
index 3caf217..31efd55 100644
--- a/Allura/allura/tests/functional/test_root.py
+++ b/Allura/allura/tests/functional/test_root.py
@@ -31,14 +31,14 @@
 from __future__ import unicode_literals
 from __future__ import absolute_import
 import os
+from unittest import skipIf
 
 import six
 
 from tg import tmpl_context as c
-from alluratest.tools import assert_equal, assert_in
+from alluratest.tools import assert_equal, module_not_available
 from ming.orm.ormsession import ThreadLocalORMSession
 import mock
-from IPython.testing.decorators import module_not_available, skipif
 
 from allura.tests import decorators as td
 from allura.tests import TestController
@@ -177,7 +177,7 @@
         self.app.get('/p', status=301)
         self.app.get('/p/', status=302)
 
-    @skipif(module_not_available('newrelic'))
+    @skipIf(module_not_available('newrelic'), 'requires newrelic')
     def test_newrelic_set_transaction_name(self):
         from allura.controllers.project import NeighborhoodController
         with mock.patch('newrelic.agent.callable_name') as callable_name,\
diff --git a/Allura/allura/tests/test_helpers.py b/Allura/allura/tests/test_helpers.py
index 4e35c01..d743a95 100644
--- a/Allura/allura/tests/test_helpers.py
+++ b/Allura/allura/tests/test_helpers.py
@@ -19,7 +19,7 @@
 
 from __future__ import unicode_literals
 from __future__ import absolute_import
-from unittest import TestCase
+from unittest import TestCase, skipIf
 from os import path
 from datetime import datetime, timedelta
 import time
@@ -27,8 +27,7 @@
 import PIL
 from mock import Mock, patch
 from tg import tmpl_context as c
-from alluratest.tools import assert_equals, assert_raises
-from IPython.testing.decorators import skipif, module_not_available
+from alluratest.tools import assert_equals, assert_raises, module_not_available
 from datadiff import tools as dd
 from webob import Request
 from webob.exc import HTTPUnauthorized
@@ -368,7 +367,7 @@
     assert_equals(project.notifications_disabled, False)
 
 
-@skipif(module_not_available('html2text'))
+@skipIf(module_not_available('html2text'), 'html2text required')
 def test_plain2markdown_with_html2text():
     """Test plain2markdown using html2text to escape markdown, if available."""
     text = '''paragraph
diff --git a/AlluraTest/alluratest/tools.py b/AlluraTest/alluratest/tools.py
index dd3e4d2..8831b12 100644
--- a/AlluraTest/alluratest/tools.py
+++ b/AlluraTest/alluratest/tools.py
@@ -84,3 +84,21 @@
 
 def assert_regexp_matches(*a, **kw):
     return testcase.assertRegexpMatches(*a, **kw)
+
+
+#
+# copied from IPython.testing.decorators
+#   BSD license
+def module_not_available(module):
+    """Can module be imported?  Returns true if module does NOT import.
+
+    This is used to make a decorator to skip tests that require module to be
+    available, but delay the 'import numpy' to test execution time.
+    """
+    try:
+        mod = __import__(module)
+        mod_not_avail = False
+    except ImportError:
+        mod_not_avail = True
+
+    return mod_not_avail
diff --git a/ForgeBlog/forgeblog/tests/test_commands.py b/ForgeBlog/forgeblog/tests/test_commands.py
index addc8ee..b275067 100644
--- a/ForgeBlog/forgeblog/tests/test_commands.py
+++ b/ForgeBlog/forgeblog/tests/test_commands.py
@@ -20,7 +20,7 @@
 from datetime import datetime, timedelta
 from tg import app_globals as g
 from datadiff.tools import assert_equal
-from IPython.testing.decorators import module_not_available, skipif
+from unittest import skipIf
 import pkg_resources
 import mock
 import feedparser
@@ -28,6 +28,7 @@
 from ming.orm.ormsession import ThreadLocalORMSession
 
 from alluratest.controller import setup_basic_test, setup_global_objects
+from alluratest.tools import module_not_available
 from allura import model as M
 from forgeblog import model as BM
 
@@ -71,7 +72,7 @@
 _mock_feed.i = 0
 
 
-@skipif(module_not_available('html2text'))
+@skipIf(module_not_available('html2text'), 'requires html2text')
 @mock.patch.object(feedparser, 'parse')
 def test_pull_rss_feeds(parsefeed):
     html_content = (
diff --git a/ForgeImporters/forgeimporters/github/tests/test_wiki.py b/ForgeImporters/forgeimporters/github/tests/test_wiki.py
index 578a875..1be0a94 100644
--- a/ForgeImporters/forgeimporters/github/tests/test_wiki.py
+++ b/ForgeImporters/forgeimporters/github/tests/test_wiki.py
@@ -19,17 +19,17 @@
 
 from __future__ import unicode_literals
 from __future__ import absolute_import
-from unittest import TestCase
+from unittest import TestCase, skipIf
 from alluratest.tools import assert_equal
 from mock import Mock, patch, call
 from ming.odm import ThreadLocalORMSession
 import git
 
-from IPython.testing.decorators import module_not_available, skipif
 from allura import model as M
 from allura.tests import TestController
 from allura.tests.decorators import with_tool, without_module
 from alluratest.controller import setup_basic_test
+from alluratest.tools import module_not_available
 from forgeimporters.github.wiki import GitHubWikiImporter
 from forgeimporters.github.utils import GitHubMarkdownConverter
 from forgeimporters.github import GitHubOAuthMixin
@@ -187,7 +187,7 @@
         assert_equal(render.call_args_list,
                      [call('Home.rst', '# test message')])
 
-    @skipif(module_not_available('html2text'))
+    @skipIf(module_not_available('html2text'), 'html2text required')
     @patch('forgeimporters.github.wiki.WM.Page.upsert')
     @patch('forgeimporters.github.wiki.mediawiki2markdown')
     def test_with_history_mediawiki(self, md2mkm, upsert):
@@ -300,7 +300,7 @@
 
         assert_equal(f(source), result)
 
-    @skipif(module_not_available('html2text'))
+    @skipIf(module_not_available('html2text'), 'html2text required')
     def test_convert_markup(self):
         importer = GitHubWikiImporter()
         importer.github_wiki_url = 'https://github.com/a/b/wiki'
@@ -403,7 +403,7 @@
               prefix, new),
             '<a href="/p/test/wiki/Test Page">Test <b>Page</b></a>')
 
-    @skipif(module_not_available('html2text'))
+    @skipIf(module_not_available('html2text'), 'html2text required')
     def test_convert_markup_with_mediawiki2markdown(self):
         importer = GitHubWikiImporter()
         importer.github_wiki_url = 'https://github.com/a/b/wiki'
@@ -431,7 +431,7 @@
 
         assert_equal(f(source, 'test.mediawiki'), result)
 
-    @skipif(module_not_available('html2text'))
+    @skipIf(module_not_available('html2text'), 'html2text required')
     def test_convert_textile_no_leading_tabs(self):
         importer = GitHubWikiImporter()
         importer.github_wiki_url = 'https://github.com/a/b/wiki'
@@ -455,7 +455,7 @@
 See [Page]'''
         assert_equal(f(source, 'test.textile').strip(), result)
 
-    @skipif(module_not_available('html2text'))
+    @skipIf(module_not_available('html2text'), 'html2text required')
     def test_convert_markup_with_amp_in_links(self):
         importer = GitHubWikiImporter()
         importer.github_wiki_url = 'https://github.com/a/b/wiki'
@@ -467,7 +467,7 @@
         # markdown should be untouched
         assert_equal(f(source, 'test.rst').strip(), result)
 
-    @skipif(module_not_available('html2text'))
+    @skipIf(module_not_available('html2text'), 'html2text required')
     def test_convert_markup_textile(self):
         importer = GitHubWikiImporter()
         importer.github_wiki_url = 'https://github.com/a/b/wiki'
@@ -513,7 +513,7 @@
 '''
         assert_equal(f(source, 'test3.textile'), result)
 
-    @skipif(module_not_available('html2text'))
+    @skipIf(module_not_available('html2text'), 'html2text required')
     def test_convert_textile_special_tag(self):
         importer = GitHubWikiImporter()
         importer.github_wiki_url = 'https://github.com/a/b/wiki'
diff --git a/ForgeImporters/forgeimporters/trac/tests/test_tickets.py b/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
index a942fd5..0b010e0 100644
--- a/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
+++ b/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
@@ -20,15 +20,15 @@
 import json
 import os
 
-from unittest import TestCase
+from unittest import TestCase, skipIf
 from mock import Mock, patch
 from ming.orm import ThreadLocalORMSession
 from tg import tmpl_context as c
-from IPython.testing.decorators import module_not_available, skipif
 
 from allura.tests import TestController
 from allura.tests.decorators import with_tracker
 from alluratest.controller import TestRestApiBase, setup_unit_test
+from alluratest.tools import module_not_available
 
 from allura import model as M
 from forgetracker import model as TM
@@ -262,7 +262,7 @@
             import_support.get_slug_by_id('204', '2'), comments[1].slug)
 
     @with_tracker
-    @skipif(module_not_available('html2text'))
+    @skipIf(module_not_available('html2text'), 'html2text required')
     def test_list(self):
         from allura.scripts.trac_export import TracExport, DateJSONEncoder
         csv_fp = open(os.path.dirname(__file__) + '/data/test-list.csv')
diff --git a/ForgeSVN/forgesvn/tests/functional/test_controllers.py b/ForgeSVN/forgesvn/tests/functional/test_controllers.py
index 116542c..99100f3 100644
--- a/ForgeSVN/forgesvn/tests/functional/test_controllers.py
+++ b/ForgeSVN/forgesvn/tests/functional/test_controllers.py
@@ -23,6 +23,7 @@
 import re
 import shutil
 import os
+from unittest import skipUnless
 
 import six
 import tg
@@ -31,7 +32,6 @@
 from ming.orm import ThreadLocalORMSession
 from mock import patch
 from alluratest.tools import assert_equal, assert_in
-from IPython.testing.decorators import onlyif
 
 from allura import model as M
 from allura.lib import helpers as h
@@ -230,7 +230,7 @@
         r = self.app.get('/src/2/log/?path=does/not/exist/')
         assert 'No (more) commits' in r
 
-    @onlyif(os.path.exists(tg.config.get('scm.repos.tarball.zip_binary', '/usr/bin/zip')), 'zip binary is missing')
+    @skipUnless(os.path.exists(tg.config.get('scm.repos.tarball.zip_binary', '/usr/bin/zip')), 'zip binary is missing')
     def test_tarball(self):
         r = self.app.get('/src/3/tree/')
         assert 'Download Snapshot' in r
@@ -245,7 +245,7 @@
         r = self.app.get('/src/3/tarball')
         assert 'Your download will begin shortly' in r
 
-    @onlyif(os.path.exists(tg.config.get('scm.repos.tarball.zip_binary', '/usr/bin/zip')), 'zip binary is missing')
+    @skipUnless(os.path.exists(tg.config.get('scm.repos.tarball.zip_binary', '/usr/bin/zip')), 'zip binary is missing')
     def test_tarball_cyrillic(self):
         r = self.app.get('/src/6/tree/')
         assert 'Download Snapshot' in r
@@ -260,7 +260,7 @@
         r = self.app.get('/src/6/tarball')
         assert 'Your download will begin shortly' in r
 
-    @onlyif(os.path.exists(tg.config.get('scm.repos.tarball.zip_binary', '/usr/bin/zip')), 'zip binary is missing')
+    @skipUnless(os.path.exists(tg.config.get('scm.repos.tarball.zip_binary', '/usr/bin/zip')), 'zip binary is missing')
     def test_tarball_path(self):
         h.set_context('test', 'svn-tags', neighborhood='Projects')
         shutil.rmtree(c.app.repo.tarball_path, ignore_errors=True)
diff --git a/ForgeSVN/forgesvn/tests/model/test_repository.py b/ForgeSVN/forgesvn/tests/model/test_repository.py
index 6781b9f..f71a787 100644
--- a/ForgeSVN/forgesvn/tests/model/test_repository.py
+++ b/ForgeSVN/forgesvn/tests/model/test_repository.py
@@ -22,6 +22,8 @@
 import os
 import shutil
 import unittest
+from unittest import skipUnless
+
 import pkg_resources
 from itertools import count, product
 from datetime import datetime
@@ -38,7 +40,6 @@
 from ming.base import Object
 from ming.orm import session, ThreadLocalORMSession
 from testfixtures import TempDirectory
-from IPython.testing.decorators import onlyif
 
 from alluratest.controller import setup_basic_test, setup_global_objects
 from allura import model as M
@@ -434,7 +435,7 @@
                 revision=pysvn.Revision.return_value,
                 recurse=False)
 
-    @onlyif(os.path.exists(tg.config.get('scm.repos.tarball.zip_binary', '/usr/bin/zip')), 'zip binary is missing')
+    @skipUnless(os.path.exists(tg.config.get('scm.repos.tarball.zip_binary', '/usr/bin/zip')), 'zip binary is missing')
     def test_tarball(self):
         tmpdir = tg.config['scm.repos.tarball.root']
         assert_equal(self.repo.tarball_path,
@@ -451,7 +452,7 @@
         shutil.rmtree(self.repo.tarball_path.encode('utf-8'),
                       ignore_errors=True)
 
-    @onlyif(os.path.exists(tg.config.get('scm.repos.tarball.zip_binary', '/usr/bin/zip')), 'zip binary is missing')
+    @skipUnless(os.path.exists(tg.config.get('scm.repos.tarball.zip_binary', '/usr/bin/zip')), 'zip binary is missing')
     def test_tarball_paths(self):
         rev = '19'
         h.set_context('test', 'svn-tags', neighborhood='Projects')
diff --git a/ForgeWiki/forgewiki/tests/test_converters.py b/ForgeWiki/forgewiki/tests/test_converters.py
index 398c0a8..bd8fa4d 100644
--- a/ForgeWiki/forgewiki/tests/test_converters.py
+++ b/ForgeWiki/forgewiki/tests/test_converters.py
@@ -17,12 +17,15 @@
 
 from __future__ import unicode_literals
 from __future__ import absolute_import
-from IPython.testing.decorators import module_not_available, skipif
+
+from unittest import skipIf
+
+from alluratest.tools import module_not_available
 
 from forgewiki import converters
 
 
-@skipif(module_not_available('mediawiki'))
+@skipIf(module_not_available('mediawiki'), 'mediawiki required')
 def test_mediawiki2markdown():
     mediawiki_text = """
 '''bold''' ''italics''
diff --git a/requirements.in b/requirements.in
index 1357e79..1512c40 100644
--- a/requirements.in
+++ b/requirements.in
@@ -54,7 +54,6 @@
 
 # testing
 datadiff
-ipython<6  # Ipython 7 starts to require py3
 mock
 pyflakes
 #pylint -- disabled due to [#8346]  (also requires diff versions on py2 vs 3, including transitive deps which gets tricky with pip-compile)
diff --git a/requirements.txt b/requirements.txt
index a79e0be..ff20e15 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -33,10 +33,7 @@
 datadiff==2.0.0
     # via -r requirements.in
 decorator==4.4.0
-    # via
-    #   -r requirements.in
-    #   ipython
-    #   traitlets
+    # via -r requirements.in
 docutils==0.15.2
     # via pypeline
 easywidgets==0.3.6
@@ -72,10 +69,6 @@
     # via requests
 inflection==0.5.1
     # via profanityfilter
-ipython==5.8.0
-    # via -r requirements.in
-ipython-genutils==0.2.0
-    # via traitlets
 iso8601==0.1.12
     # via colander
 jinja2==2.11.3
@@ -117,26 +110,16 @@
     #   pastescript
 pastescript==3.1.0
     # via -r requirements.in
-pexpect==4.7.0
-    # via ipython
-pickleshare==0.7.5
-    # via ipython
 pillow==8.3.2
     # via -r requirements.in
 profanityfilter==2.0.6
     # via -r requirements.in
-prompt-toolkit==1.0.16
-    # via ipython
-ptyprocess==0.6.0
-    # via pexpect
 pycparser==2.19
     # via cffi
 pyflakes==2.1.1
     # via -r requirements.in
 pygments==2.9.0
-    # via
-    #   -r requirements.in
-    #   ipython
+    # via -r requirements.in
 pymongo==3.10.1
     # via
     #   -r requirements.in
@@ -178,8 +161,6 @@
     # via -r requirements.in
 sgmllib3k==1.0.0
     # via feedparser
-simplegeneric==0.8.1
-    # via ipython
 six==1.15.0
     # via
     #   -r requirements.in
@@ -194,11 +175,9 @@
     #   mock
     #   paste
     #   pastescript
-    #   prompt-toolkit
     #   python-dateutil
     #   qrcode
     #   textile
-    #   traitlets
     #   webhelpers2
     #   webtest
 smmap2==2.0.4
@@ -211,8 +190,6 @@
     # via pypeline
 timermiddleware==0.5.1
     # via -r requirements.in
-traitlets==4.3.2
-    # via ipython
 translationstring==1.3
     # via colander
 turbogears2==2.3.12
@@ -221,8 +198,6 @@
     # via requests
 waitress==1.4.3
     # via webtest
-wcwidth==0.1.7
-    # via prompt-toolkit
 webencodings==0.5.1
     # via
     #   bleach