Skip to content

Commit ae9c85f

Browse files
committed
Use entry points to load extensions by name.
All previously supported methods of loading an extension given a string name have now been replaced with entry points. Extension classes must be registered as entry points in the 'markdown.extensions' group to support string names. The old module-level 'makeExtension' function is no longer needed. In fact, multiple extensions could be defined in one module and still use string names if entry points are defined pointing to each Extension subclass. All of the build-in extensions are now registered with names that mimic their paths (using dot notation). That may change in the future. It is expected that all extensions will use short names. We may support both ('extra' & 'markdown.extensions.extra') in the interim. Also, all extension configs must now be dicts. Support for a list of tuples has been removed.
1 parent a08c7c1 commit ae9c85f

File tree

4 files changed

+57
-59
lines changed

4 files changed

+57
-59
lines changed

markdown/core.py

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
import logging
66
import importlib
7+
import pkg_resources
78
from . import util
89
from .preprocessors import build_preprocessors
910
from .blockprocessors import build_block_parser
@@ -51,10 +52,10 @@ def __init__(self, **kwargs):
5152
Keyword arguments:
5253
5354
* extensions: A list of extensions.
54-
If they are of type string, the module mdx_name.py will be loaded.
55-
If they are a subclass of markdown.Extension, they will be used
56-
as-is.
57-
* extension_configs: Configuration settingis for extensions.
55+
If they are of type string, an entry point will be loaded.
56+
If they are a subclass of `markdown.extensions.Extension`,
57+
they will be used as-is.
58+
* extension_configs: Configuration settings for extensions.
5859
* output_format: Format of output. Supported formats are:
5960
* "xhtml1": Outputs XHTML 1.x. Default.
6061
* "xhtml5": Outputs XHTML style tags of HTML 5
@@ -107,8 +108,8 @@ def registerExtensions(self, extensions, configs):
107108
Keyword arguments:
108109
109110
* extensions: A list of extensions, which can either
110-
be strings or objects. See the docstring on Markdown.
111-
* configs: A dictionary mapping module names to config options.
111+
be strings or objects.
112+
* configs: A dictionary mapping extension names to config options.
112113
113114
"""
114115
for ext in extensions:
@@ -127,41 +128,24 @@ def registerExtensions(self, extensions, configs):
127128

128129
return self
129130

130-
def build_extension(self, ext_name, configs):
131+
def build_extension(self, name, configs):
131132
"""
132-
Build extension by name, then return the module.
133-
133+
Build an extension from a string name, and return an instance.
134+
135+
The string name must be registered as an entry point in the
136+
`markdown.extensions` group which points to a subclass of
137+
the `markdown.extensions.Extension` class. If multiple
138+
distributions have registered the same name, the first one
139+
found by `pkg_resources.iter_entry_points` is returned.
134140
"""
135-
136-
configs = dict(configs)
137-
138-
# Get class name (if provided): `path.to.module:ClassName`
139-
ext_name, class_name = ext_name.split(':', 1) \
140-
if ':' in ext_name else (ext_name, '')
141-
142-
try:
143-
module = importlib.import_module(ext_name)
144-
logger.debug(
145-
'Successfuly imported extension module "%s".' % ext_name
146-
)
147-
except ImportError as e:
148-
message = 'Failed loading extension "%s".' % ext_name
149-
e.args = (message,) + e.args[1:]
150-
raise
151-
152-
if class_name:
153-
# Load given class name from module.
154-
return getattr(module, class_name)(**configs)
155-
else:
156-
# Expect makeExtension() function to return a class.
157-
try:
158-
return module.makeExtension(**configs)
159-
except AttributeError as e:
160-
message = e.args[0]
161-
message = "Failed to initiate extension " \
162-
"'%s': %s" % (ext_name, message)
163-
e.args = (message,) + e.args[1:]
164-
raise
141+
ep = None
142+
for x in pkg_resources.iter_entry_points('markdown.extensions', name):
143+
ep = x
144+
break
145+
if ep is None:
146+
raise ImportError('Can\'t find entry point "%s" in group "markdown.extensions".' % name)
147+
ext = ep.load()
148+
return ext(**configs)
165149

166150
def registerExtension(self, extension):
167151
""" This gets called by the extension """

setup.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import with_statement
44
import sys
55
import os
6-
from distutils.core import setup
6+
from setuptools import setup
77
from distutils.command.install_scripts import install_scripts
88
from distutils.command.build import build
99
from distutils.core import Command
@@ -241,6 +241,28 @@ def has_docs(self):
241241
'build_docs': build_docs,
242242
'build': md_build
243243
},
244+
entry_points={
245+
# Register the built in extensions
246+
'markdown.extensions': [
247+
'markdown.extensions.abbr = markdown.extensions.abbr:AbbrExtension',
248+
'markdown.extensions.admonition = markdown.extensions.admonition:AdmonitionExtension',
249+
'markdown.extensions.attr_list = markdown.extensions.attr_list:AttrListExtension',
250+
'markdown.extensions.codehilite = markdown.extensions.codehilite:CodeHiliteExtension',
251+
'markdown.extensions.def_list = markdown.extensions.def_list:DefListExtension',
252+
'markdown.extensions.extra = markdown.extensions.extra:ExtraExtension',
253+
'markdown.extensions.fenced_code = markdown.extensions.fenced_code:FencedCodeExtension',
254+
'markdown.extensions.footnotes = markdown.extensions.footnotes:FootnoteExtension',
255+
'markdown.extensions.headerid = markdown.extensions.headerid:HeaderIdExtension',
256+
'markdown.extensions.meta = markdown.extensions.meta:MetaExtension',
257+
'markdown.extensions.nl2br = markdown.extensions.nl2br:Nl2BrExtension',
258+
'markdown.extensions.sane_lists = markdown.extensions.sane_lists:SaneListExtension',
259+
'markdown.extensions.smart_strong = markdown.extensions.smart_strong:SmartEmphasisExtension',
260+
'markdown.extensions.smarty = markdown.extensions.smarty:SmartyExtension',
261+
'markdown.extensions.tables = markdown.extensions.tables:TableExtension',
262+
'markdown.extensions.toc = markdown.extensions.toc:TocExtension',
263+
'markdown.extensions.wikilinks = markdown.extensions.wikilinks:WikiLinkExtension',
264+
]
265+
},
244266
classifiers=[
245267
'Development Status :: %s' % DEVSTATUS,
246268
'License :: OSI Approved :: BSD License',

tests/test_apis.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,9 @@ def testInstanceExtension(self):
5353
markdown.Markdown(extensions=[FootnoteExtension()])
5454

5555
def testNamedExtension(self):
56-
""" Test Extension loading with Name (`path.to.module`). """
56+
""" Test Extension loading with Name. """
5757
markdown.Markdown(extensions=['markdown.extensions.footnotes'])
5858

59-
def TestNamedExtensionWithClass(self):
60-
""" Test Extension loading with class name (`path.to.module:Class`). """
61-
markdown.Markdown(extensions=['markdown.extensions.footnotes:FootnoteExtension'])
62-
6359

6460
class TestConvertFile(unittest.TestCase):
6561
""" Tests of ConvertFile. """
@@ -373,10 +369,6 @@ def testLoadExtensionFailure(self):
373369
markdown.Markdown, extensions=['non_existant_ext']
374370
)
375371

376-
def testLoadBadExtension(self):
377-
""" Test loading of an Extension with no makeExtension function. """
378-
self.assertRaises(AttributeError, markdown.Markdown, extensions=['markdown.util'])
379-
380372
def testNonExtension(self):
381373
""" Test loading a non Extension object as an extension. """
382374
self.assertRaises(TypeError, markdown.Markdown, extensions=[object])

tests/test_extensions.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -557,11 +557,11 @@ def testComplexSettings(self):
557557
md = markdown.Markdown(
558558
extensions=['markdown.extensions.wikilinks'],
559559
extension_configs={
560-
'markdown.extensions.wikilinks': [
561-
('base_url', 'http://example.com/'),
562-
('end_url', '.html'),
563-
('html_class', '')
564-
]
560+
'markdown.extensions.wikilinks': {
561+
'base_url': 'http://example.com/',
562+
'end_url': '.html',
563+
'html_class': ''
564+
}
565565
},
566566
safe_mode=True
567567
)
@@ -829,9 +829,9 @@ def testUniqueFunc(self):
829829
class TestSmarty(unittest.TestCase):
830830
def setUp(self):
831831
config = {
832-
'markdown.extensions.smarty': [
833-
('smart_angled_quotes', True),
834-
('substitutions', {
832+
'markdown.extensions.smarty': {
833+
'smart_angled_quotes': True,
834+
'substitutions': {
835835
'ndash': '\u2013',
836836
'mdash': '\u2014',
837837
'ellipsis': '\u2026',
@@ -841,8 +841,8 @@ def setUp(self):
841841
'right-double-quote': '“',
842842
'left-angle-quote': '[',
843843
'right-angle-quote': ']',
844-
}),
845-
]
844+
},
845+
}
846846
}
847847
self.md = markdown.Markdown(
848848
extensions=['markdown.extensions.smarty'],

0 commit comments

Comments
 (0)