1
# Copyright (C) 2005 by Canonical Ltd
1
# Copyright (C) 2005, 2007 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
17
"""Tests for plugins"""
20
19
# XXX: There are no plugin tests at the moment because the plugin module
21
20
# affects the global state of the process. See bzrlib/plugins.py for more
25
from StringIO import StringIO
29
from bzrlib import plugin, tests
26
30
import bzrlib.plugin
27
31
import bzrlib.plugins
28
from bzrlib.tests import TestCaseInTempDir
29
from bzrlib.osutils import pathjoin, abspath
31
class PluginTest(TestCaseInTempDir):
32
"""Create an external plugin and test loading."""
33
# def test_plugin_loading(self):
34
# orig_help = self.run_bzr_captured('bzr help commands')[0]
35
# os.mkdir('plugin_test')
36
# f = open(pathjoin('plugin_test', 'myplug.py'), 'wt')
37
# f.write(PLUGIN_TEXT)
39
# newhelp = self.run_bzr_captured('bzr help commands')[0]
40
# assert newhelp.startswith('You have been overridden\n')
41
# # We added a line, but the rest should work
42
# assert newhelp[25:] == help
44
# assert backtick('bzr commit -m test') == "I'm sorry dave, you can't do that\n"
46
# shutil.rmtree('plugin_test')
49
# os.environ['BZRPLUGINPATH'] = abspath('plugin_test')
50
# help = backtick('bzr help commands')
51
# assert help.find('myplug') != -1
52
# assert help.find('Just a simple test plugin.') != -1
55
# assert backtick('bzr myplug') == 'Hello from my plugin\n'
56
# assert backtick('bzr mplg') == 'Hello from my plugin\n'
58
# f = open(pathjoin('plugin_test', 'override.py'), 'wb')
59
# f.write("""import bzrlib, bzrlib.commands
60
# class cmd_commit(bzrlib.commands.cmd_commit):
61
# '''Commit changes into a new revision.'''
62
# def run(self, *args, **kwargs):
63
# print "I'm sorry dave, you can't do that"
65
# class cmd_help(bzrlib.commands.cmd_help):
66
# '''Show help on a command or other topic.'''
67
# def run(self, *args, **kwargs):
68
# print "You have been overridden"
69
# bzrlib.commands.cmd_help.run(self, *args, **kwargs)
32
import bzrlib.commands
34
from bzrlib.symbol_versioning import zero_ninetyone
35
from bzrlib.tests import TestCase, TestCaseInTempDir
36
from bzrlib.osutils import pathjoin, abspath, normpath
74
40
import bzrlib.commands
82
48
# TODO: Write a test for plugin decoration of commands.
84
class TestOneNamedPluginOnly(TestCaseInTempDir):
50
class TestLoadingPlugins(TestCaseInTempDir):
86
52
activeattributes = {}
88
54
def test_plugins_with_the_same_name_are_not_loaded(self):
55
# This test tests that having two plugins in different directories does
56
# not result in both being loaded when they have the same name. get a
57
# file name we can use which is also a valid attribute for accessing in
58
# activeattributes. - we cannot give import parameters.
60
self.failIf(tempattribute in self.activeattributes)
61
# set a place for the plugins to record their loading, and at the same
62
# time validate that the location the plugins should record to is
64
bzrlib.tests.test_plugins.TestLoadingPlugins.activeattributes \
66
self.failUnless(tempattribute in self.activeattributes)
67
# create two plugin directories
70
# write a plugin that will record when its loaded in the
72
template = ("from bzrlib.tests.test_plugins import TestLoadingPlugins\n"
73
"TestLoadingPlugins.activeattributes[%r].append('%s')\n")
75
outfile = open(os.path.join('first', 'plugin.py'), 'w')
77
outfile.write(template % (tempattribute, 'first'))
82
outfile = open(os.path.join('second', 'plugin.py'), 'w')
84
outfile.write(template % (tempattribute, 'second'))
90
bzrlib.plugin.load_from_path(['first', 'second'])
91
self.assertEqual(['first'], self.activeattributes[tempattribute])
93
# remove the plugin 'plugin'
94
del self.activeattributes[tempattribute]
95
if 'bzrlib.plugins.plugin' in sys.modules:
96
del sys.modules['bzrlib.plugins.plugin']
97
if getattr(bzrlib.plugins, 'plugin', None):
98
del bzrlib.plugins.plugin
99
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
101
def test_plugins_from_different_dirs_can_demand_load(self):
89
102
# This test tests that having two plugins in different
90
# directories does not result in both being loaded.
91
# get a file name we can use which is also a valid attribute
103
# directories with different names allows them both to be loaded, when
104
# we do a direct import statement.
105
# Determine a file name we can use which is also a valid attribute
92
106
# for accessing in activeattributes. - we cannot give import parameters.
107
tempattribute = "different-dirs"
94
108
self.failIf(tempattribute in self.activeattributes)
95
109
# set a place for the plugins to record their loading, and at the same
96
110
# time validate that the location the plugins should record to is
97
111
# valid and correct.
98
bzrlib.tests.test_plugins.TestOneNamedPluginOnly.activeattributes \
112
bzrlib.tests.test_plugins.TestLoadingPlugins.activeattributes \
99
113
[tempattribute] = []
100
114
self.failUnless(tempattribute in self.activeattributes)
101
115
# create two plugin directories
102
116
os.mkdir('first')
103
117
os.mkdir('second')
118
# write plugins that will record when they are loaded in the
119
# tempattribute list.
120
template = ("from bzrlib.tests.test_plugins import TestLoadingPlugins\n"
121
"TestLoadingPlugins.activeattributes[%r].append('%s')\n")
123
outfile = open(os.path.join('first', 'pluginone.py'), 'w')
125
outfile.write(template % (tempattribute, 'first'))
130
outfile = open(os.path.join('second', 'plugintwo.py'), 'w')
132
outfile.write(template % (tempattribute, 'second'))
137
oldpath = bzrlib.plugins.__path__
139
bzrlib.plugins.__path__ = ['first', 'second']
140
exec "import bzrlib.plugins.pluginone"
141
self.assertEqual(['first'], self.activeattributes[tempattribute])
142
exec "import bzrlib.plugins.plugintwo"
143
self.assertEqual(['first', 'second'],
144
self.activeattributes[tempattribute])
146
# remove the plugin 'plugin'
147
del self.activeattributes[tempattribute]
148
if getattr(bzrlib.plugins, 'pluginone', None):
149
del bzrlib.plugins.pluginone
150
if getattr(bzrlib.plugins, 'plugintwo', None):
151
del bzrlib.plugins.plugintwo
152
self.failIf(getattr(bzrlib.plugins, 'pluginone', None))
153
self.failIf(getattr(bzrlib.plugins, 'plugintwo', None))
155
def test_plugins_can_load_from_directory_with_trailing_slash(self):
156
# This test tests that a plugin can load from a directory when the
157
# directory in the path has a trailing slash.
158
# check the plugin is not loaded already
159
self.failIf(getattr(bzrlib.plugins, 'ts_plugin', None))
160
tempattribute = "trailing-slash"
161
self.failIf(tempattribute in self.activeattributes)
162
# set a place for the plugin to record its loading, and at the same
163
# time validate that the location the plugin should record to is
165
bzrlib.tests.test_plugins.TestLoadingPlugins.activeattributes \
167
self.failUnless(tempattribute in self.activeattributes)
168
# create a directory for the plugin
169
os.mkdir('plugin_test')
104
170
# write a plugin that will record when its loaded in the
105
171
# tempattribute list.
106
template = ("from bzrlib.tests.test_plugins import TestOneNamedPluginOnly\n"
107
"TestOneNamedPluginOnly.activeattributes[%r].append('%s')\n")
108
print >> file(os.path.join('first', 'plugin.py'), 'w'), template % (tempattribute, 'first')
109
print >> file(os.path.join('second', 'plugin.py'), 'w'), template % (tempattribute, 'second')
111
bzrlib.plugin.load_from_dirs(['first', 'second'])
112
self.assertEqual(['first'], self.activeattributes[tempattribute])
172
template = ("from bzrlib.tests.test_plugins import TestLoadingPlugins\n"
173
"TestLoadingPlugins.activeattributes[%r].append('%s')\n")
175
outfile = open(os.path.join('plugin_test', 'ts_plugin.py'), 'w')
177
outfile.write(template % (tempattribute, 'plugin'))
183
bzrlib.plugin.load_from_path(['plugin_test'+os.sep])
184
self.assertEqual(['plugin'], self.activeattributes[tempattribute])
114
186
# remove the plugin 'plugin'
115
187
del self.activeattributes[tempattribute]
116
if getattr(bzrlib.plugins, 'plugin', None):
117
del bzrlib.plugins.plugin
118
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
188
if getattr(bzrlib.plugins, 'ts_plugin', None):
189
del bzrlib.plugins.ts_plugin
190
self.failIf(getattr(bzrlib.plugins, 'ts_plugin', None))
192
def test_plugin_with_bad_name_does_not_load(self):
193
# Create badly-named plugin
194
file('bad plugin-name..py', 'w').close()
198
handler = logging.StreamHandler(stream)
199
log = logging.getLogger('bzr')
200
log.addHandler(handler)
202
bzrlib.plugin.load_from_dir('.')
204
# Stop capturing output
207
log.removeHandler(handler)
209
self.assertContainsRe(stream.getvalue(),
210
r"Unable to load 'bad plugin-name\.' in '\.' as a plugin because"
211
" file path isn't a valid module name; try renaming it to"
212
" 'bad_plugin_name_'\.")
121
217
class TestAllPlugins(TestCaseInTempDir):
125
221
# check the plugin is not loaded already
126
222
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
127
223
# write a plugin that _cannot_ fail to load.
128
print >> file('plugin.py', 'w'), ""
224
file('plugin.py', 'w').write("\n")
130
bzrlib.plugin.load_from_dirs(['.'])
131
self.failUnless('plugin' in bzrlib.plugin.all_plugins())
226
bzrlib.plugin.load_from_path(['.'])
227
all_plugins = self.applyDeprecated(zero_ninetyone,
228
bzrlib.plugin.all_plugins)
229
self.failUnless('plugin' in all_plugins)
132
230
self.failUnless(getattr(bzrlib.plugins, 'plugin', None))
133
self.assertEqual(bzrlib.plugin.all_plugins()['plugin'],
134
bzrlib.plugins.plugin)
231
self.assertEqual(all_plugins['plugin'], bzrlib.plugins.plugin)
136
233
# remove the plugin 'plugin'
234
if 'bzrlib.plugins.plugin' in sys.modules:
235
del sys.modules['bzrlib.plugins.plugin']
137
236
if getattr(bzrlib.plugins, 'plugin', None):
138
237
del bzrlib.plugins.plugin
139
238
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
241
class TestPlugins(TestCaseInTempDir):
243
def setup_plugin(self, source=""):
244
# This test tests a new plugin appears in bzrlib.plugin.plugins().
245
# check the plugin is not loaded already
246
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
247
# write a plugin that _cannot_ fail to load.
248
file('plugin.py', 'w').write(source + '\n')
249
self.addCleanup(self.teardown_plugin)
250
bzrlib.plugin.load_from_path(['.'])
252
def teardown_plugin(self):
253
# remove the plugin 'plugin'
254
if 'bzrlib.plugins.plugin' in sys.modules:
255
del sys.modules['bzrlib.plugins.plugin']
256
if getattr(bzrlib.plugins, 'plugin', None):
257
del bzrlib.plugins.plugin
258
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
260
def test_plugin_appears_in_plugins(self):
262
self.failUnless('plugin' in bzrlib.plugin.plugins())
263
self.failUnless(getattr(bzrlib.plugins, 'plugin', None))
264
plugins = bzrlib.plugin.plugins()
265
plugin = plugins['plugin']
266
self.assertIsInstance(plugin, bzrlib.plugin.PlugIn)
267
self.assertEqual(bzrlib.plugins.plugin, plugin.module)
269
def test_trivial_plugin_get_path(self):
271
plugins = bzrlib.plugin.plugins()
272
plugin = plugins['plugin']
273
plugin_path = self.test_dir + '/plugin.py'
274
self.assertIsSameRealPath(plugin_path, normpath(plugin.path()))
276
def test_plugin_get_path_py_not_pyc(self):
277
self.setup_plugin() # after first import there will be plugin.pyc
278
self.teardown_plugin()
279
bzrlib.plugin.load_from_path(['.']) # import plugin.pyc
280
plugins = bzrlib.plugin.plugins()
281
plugin = plugins['plugin']
282
plugin_path = self.test_dir + '/plugin.py'
283
self.assertIsSameRealPath(plugin_path, normpath(plugin.path()))
285
def test_plugin_get_path_pyc_only(self):
286
self.setup_plugin() # after first import there will be plugin.pyc
287
self.teardown_plugin()
288
os.unlink(self.test_dir + '/plugin.py')
289
bzrlib.plugin.load_from_path(['.']) # import plugin.pyc
290
plugins = bzrlib.plugin.plugins()
291
plugin = plugins['plugin']
293
plugin_path = self.test_dir + '/plugin.pyc'
295
plugin_path = self.test_dir + '/plugin.pyo'
296
self.assertIsSameRealPath(plugin_path, normpath(plugin.path()))
298
def test_no_test_suite_gives_None_for_test_suite(self):
300
plugin = bzrlib.plugin.plugins()['plugin']
301
self.assertEqual(None, plugin.test_suite())
303
def test_test_suite_gives_test_suite_result(self):
304
source = """def test_suite(): return 'foo'"""
305
self.setup_plugin(source)
306
plugin = bzrlib.plugin.plugins()['plugin']
307
self.assertEqual('foo', plugin.test_suite())
309
def test_no_version_info(self):
311
plugin = bzrlib.plugin.plugins()['plugin']
312
self.assertEqual(None, plugin.version_info())
314
def test_with_version_info(self):
315
self.setup_plugin("version_info = (1, 2, 3, 'dev', 4)")
316
plugin = bzrlib.plugin.plugins()['plugin']
317
self.assertEqual((1, 2, 3, 'dev', 4), plugin.version_info())
319
def test_short_version_info_gets_padded(self):
320
# the gtk plugin has version_info = (1,2,3) rather than the 5-tuple.
322
self.setup_plugin("version_info = (1, 2, 3)")
323
plugin = bzrlib.plugin.plugins()['plugin']
324
self.assertEqual((1, 2, 3, 'final', 0), plugin.version_info())
326
def test_no_version_info___version__(self):
328
plugin = bzrlib.plugin.plugins()['plugin']
329
self.assertEqual("unknown", plugin.__version__)
331
def test___version__with_version_info(self):
332
self.setup_plugin("version_info = (1, 2, 3, 'dev', 4)")
333
plugin = bzrlib.plugin.plugins()['plugin']
334
self.assertEqual("1.2.3dev4", plugin.__version__)
336
def test_final__version__with_version_info(self):
337
self.setup_plugin("version_info = (1, 2, 3, 'final', 4)")
338
plugin = bzrlib.plugin.plugins()['plugin']
339
self.assertEqual("1.2.3", plugin.__version__)
342
class TestPluginHelp(TestCaseInTempDir):
344
def split_help_commands(self):
347
for line in self.run_bzr('help commands')[0].splitlines():
348
if not line.startswith(' '):
349
current = line.split()[0]
350
help[current] = help.get(current, '') + line
354
def test_plugin_help_builtins_unaffected(self):
355
# Check we don't get false positives
356
help_commands = self.split_help_commands()
357
for cmd_name in bzrlib.commands.builtin_command_names():
358
if cmd_name in bzrlib.commands.plugin_command_names():
361
help = bzrlib.commands.get_cmd_object(cmd_name).get_help_text()
362
except NotImplementedError:
363
# some commands have no help
366
self.assertNotContainsRe(help, 'plugin "[^"]*"')
368
if cmd_name in help_commands.keys():
369
# some commands are hidden
370
help = help_commands[cmd_name]
371
self.assertNotContainsRe(help, 'plugin "[^"]*"')
373
def test_plugin_help_shows_plugin(self):
374
# Create a test plugin
375
os.mkdir('plugin_test')
376
f = open(pathjoin('plugin_test', 'myplug.py'), 'w')
382
bzrlib.plugin.load_from_path(['plugin_test'])
383
bzrlib.commands.register_command( bzrlib.plugins.myplug.cmd_myplug)
384
help = self.run_bzr('help myplug')[0]
385
self.assertContainsRe(help, 'plugin "myplug"')
386
help = self.split_help_commands()['myplug']
387
self.assertContainsRe(help, '\[myplug\]')
390
if bzrlib.commands.plugin_cmds.get('myplug', None):
391
del bzrlib.commands.plugin_cmds['myplug']
392
# remove the plugin 'myplug'
393
if getattr(bzrlib.plugins, 'myplug', None):
394
delattr(bzrlib.plugins, 'myplug')
397
class TestPluginFromZip(TestCaseInTempDir):
399
def make_zipped_plugin(self, zip_name, filename):
400
z = zipfile.ZipFile(zip_name, 'w')
401
z.writestr(filename, PLUGIN_TEXT)
404
def check_plugin_load(self, zip_name, plugin_name):
405
self.assertFalse(plugin_name in dir(bzrlib.plugins),
406
'Plugin already loaded')
407
old_path = bzrlib.plugins.__path__
409
# this is normally done by load_plugins -> set_plugins_path
410
bzrlib.plugins.__path__ = [zip_name]
411
bzrlib.plugin.load_from_zip(zip_name)
412
self.assertTrue(plugin_name in dir(bzrlib.plugins),
413
'Plugin is not loaded')
416
if getattr(bzrlib.plugins, plugin_name, None):
417
delattr(bzrlib.plugins, plugin_name)
418
del sys.modules['bzrlib.plugins.' + plugin_name]
419
bzrlib.plugins.__path__ = old_path
421
def test_load_module(self):
422
self.make_zipped_plugin('./test.zip', 'ziplug.py')
423
self.check_plugin_load('./test.zip', 'ziplug')
425
def test_load_package(self):
426
self.make_zipped_plugin('./test.zip', 'ziplug/__init__.py')
427
self.check_plugin_load('./test.zip', 'ziplug')
430
class TestSetPluginsPath(TestCase):
432
def test_set_plugins_path(self):
433
"""set_plugins_path should set the module __path__ correctly."""
434
old_path = bzrlib.plugins.__path__
436
bzrlib.plugins.__path__ = []
437
expected_path = bzrlib.plugin.set_plugins_path()
438
self.assertEqual(expected_path, bzrlib.plugins.__path__)
440
bzrlib.plugins.__path__ = old_path
442
def test_set_plugins_path_with_trailing_slashes(self):
443
"""set_plugins_path should set the module __path__ based on
445
old_path = bzrlib.plugins.__path__
446
old_env = os.environ.get('BZR_PLUGIN_PATH')
448
bzrlib.plugins.__path__ = []
449
os.environ['BZR_PLUGIN_PATH'] = "first\\//\\" + os.pathsep + \
451
bzrlib.plugin.set_plugins_path()
452
expected_path = ['first', 'second',
453
os.path.dirname(bzrlib.plugins.__file__)]
454
self.assertEqual(expected_path, bzrlib.plugins.__path__)
456
bzrlib.plugins.__path__ = old_path
458
os.environ['BZR_PLUGIN_PATH'] = old_env
460
del os.environ['BZR_PLUGIN_PATH']
462
class TestHelpIndex(tests.TestCase):
463
"""Tests for the PluginsHelpIndex class."""
465
def test_default_constructable(self):
466
index = plugin.PluginsHelpIndex()
468
def test_get_topics_None(self):
469
"""Searching for None returns an empty list."""
470
index = plugin.PluginsHelpIndex()
471
self.assertEqual([], index.get_topics(None))
473
def test_get_topics_for_plugin(self):
474
"""Searching for plugin name gets its docstring."""
475
index = plugin.PluginsHelpIndex()
476
# make a new plugin here for this test, even if we're run with
478
self.assertFalse(sys.modules.has_key('bzrlib.plugins.demo_module'))
479
demo_module = FakeModule('', 'bzrlib.plugins.demo_module')
480
sys.modules['bzrlib.plugins.demo_module'] = demo_module
482
topics = index.get_topics('demo_module')
483
self.assertEqual(1, len(topics))
484
self.assertIsInstance(topics[0], plugin.ModuleHelpTopic)
485
self.assertEqual(demo_module, topics[0].module)
487
del sys.modules['bzrlib.plugins.demo_module']
489
def test_get_topics_no_topic(self):
490
"""Searching for something that is not a plugin returns []."""
491
# test this by using a name that cannot be a plugin - its not
492
# a valid python identifier.
493
index = plugin.PluginsHelpIndex()
494
self.assertEqual([], index.get_topics('nothing by this name'))
496
def test_prefix(self):
497
"""PluginsHelpIndex has a prefix of 'plugins/'."""
498
index = plugin.PluginsHelpIndex()
499
self.assertEqual('plugins/', index.prefix)
501
def test_get_plugin_topic_with_prefix(self):
502
"""Searching for plugins/demo_module returns help."""
503
index = plugin.PluginsHelpIndex()
504
self.assertFalse(sys.modules.has_key('bzrlib.plugins.demo_module'))
505
demo_module = FakeModule('', 'bzrlib.plugins.demo_module')
506
sys.modules['bzrlib.plugins.demo_module'] = demo_module
508
topics = index.get_topics('plugins/demo_module')
509
self.assertEqual(1, len(topics))
510
self.assertIsInstance(topics[0], plugin.ModuleHelpTopic)
511
self.assertEqual(demo_module, topics[0].module)
513
del sys.modules['bzrlib.plugins.demo_module']
516
class FakeModule(object):
517
"""A fake module to test with."""
519
def __init__(self, doc, name):
524
class TestModuleHelpTopic(tests.TestCase):
525
"""Tests for the ModuleHelpTopic class."""
527
def test_contruct(self):
528
"""Construction takes the module to document."""
529
mod = FakeModule('foo', 'foo')
530
topic = plugin.ModuleHelpTopic(mod)
531
self.assertEqual(mod, topic.module)
533
def test_get_help_text_None(self):
534
"""A ModuleHelpTopic returns the docstring for get_help_text."""
535
mod = FakeModule(None, 'demo')
536
topic = plugin.ModuleHelpTopic(mod)
537
self.assertEqual("Plugin 'demo' has no docstring.\n",
538
topic.get_help_text())
540
def test_get_help_text_no_carriage_return(self):
541
"""ModuleHelpTopic.get_help_text adds a \n if needed."""
542
mod = FakeModule('one line of help', 'demo')
543
topic = plugin.ModuleHelpTopic(mod)
544
self.assertEqual("one line of help\n",
545
topic.get_help_text())
547
def test_get_help_text_carriage_return(self):
548
"""ModuleHelpTopic.get_help_text adds a \n if needed."""
549
mod = FakeModule('two lines of help\nand more\n', 'demo')
550
topic = plugin.ModuleHelpTopic(mod)
551
self.assertEqual("two lines of help\nand more\n",
552
topic.get_help_text())
554
def test_get_help_text_with_additional_see_also(self):
555
mod = FakeModule('two lines of help\nand more', 'demo')
556
topic = plugin.ModuleHelpTopic(mod)
557
self.assertEqual("two lines of help\nand more\nSee also: bar, foo\n",
558
topic.get_help_text(['foo', 'bar']))
560
def test_get_help_topic(self):
561
"""The help topic for a plugin is its module name."""
562
mod = FakeModule('two lines of help\nand more', 'bzrlib.plugins.demo')
563
topic = plugin.ModuleHelpTopic(mod)
564
self.assertEqual('demo', topic.get_help_topic())
565
mod = FakeModule('two lines of help\nand more', 'bzrlib.plugins.foo_bar')
566
topic = plugin.ModuleHelpTopic(mod)
567
self.assertEqual('foo_bar', topic.get_help_topic())