diff options
-rw-r--r-- | .travis.yml | 1 | ||||
-rw-r--r-- | docs/release-2.5.txt | 91 | ||||
-rw-r--r-- | markdown/__init__.py | 60 | ||||
-rwxr-xr-x | setup.py | 1 | ||||
-rw-r--r-- | tests/test_apis.py | 42 | ||||
-rw-r--r-- | tox.ini | 2 |
6 files changed, 138 insertions, 59 deletions
diff --git a/.travis.yml b/.travis.yml index 1931a20..e912c73 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python env: - - TOXENV=py26 - TOXENV=py27 - TOXENV=py32 - TOXENV=py33 diff --git a/docs/release-2.5.txt b/docs/release-2.5.txt index 0d895bb..a81acfd 100644 --- a/docs/release-2.5.txt +++ b/docs/release-2.5.txt @@ -10,15 +10,23 @@ Python-Markdown 2.5 Release Notes We are pleased to release Python-Markdown 2.5 which adds a few new features and fixes various bugs. See the list of changes below for details. -Python-Markdown supports Python versions 2.6, 2.7, 3.2, 3.3, and 3.4. +Python-Markdown version 2.5 supports Python versions 2.7, 3.2, 3.3, and 3.4. Backwards-incompatible Changes ------------------------------ +* Python-Markdown no longer supports Python version 2.6. You must be using Python + versions 2.7, 3.2, 3.3, or 3.4. While Python-Markdown is no longer tested against + Python 2.6, you may be able to get it working if you install a copy of [importlib] + which has been backported for Python 2.6. However, the developers of Python-Markdown + offer no guarentees in that situation. + +[importlib]: https://pypi.python.org/pypi/importlib + * The `force_linenos` config key on the [CodeHilite Extension] has been deprecated -and will raise a `KeyError` if provided. In the previous release (2.4), it was issuing -a `DeprecationWarning`. The [`linenums`][linenums] keyword should be used instead, -which provides more control of the output. + and will raise a `KeyError` if provided. In the previous release (2.4), it was + issuing a `DeprecationWarning`. The [`linenums`][linenums] keyword should be used + instead, which provides more control of the output. [CodeHilite Extension]: extensions/code_hilite.html [linenumes]: extensions/code_hilite.html#usage @@ -26,36 +34,73 @@ which provides more control of the output. What's New in Python-Markdown 2.5 --------------------------------- -* The [Smarty Extension] has had a number of additional configuration settings -added, which allows one to define their own subtitutions to better support -languages other than English. Thanks to [Martin Altmayer] for implementing this feature. +* The [Smarty Extension] has had a number of additional configuration settings + added, which allows one to define their own subtitutions to better support + languages other than English. Thanks to [Martin Altmayer] for implementing this + feature. [Smarty Extension]: extensions/smarty.html [Martin Altmayer]:https://github.com/MartinAltmayer -* The Extension Configuration code has been refactord to make it a little easier -for extension authors to work with config settings. As a result, the -[`extension_configs`][ec] keyword now accepts a dictionary rather than requiring -a list of tuples. A list of tuples is still supported so no one needs to change -their existing code. This should also simplify the learning curve for new users. +* Named Extensions (strings passed to the [`extensions`][ex] keyword of + `markdown.Markdown`) can now point to any module and/or Class on your PYTHONPATH. + While dot notation was previously supported, a module could not be at the root of + your PYTHONPATH. The name had to contain at least one dot (requiring it to be a + submodule). This restriction no longer exists. + + Additionaly, a Class may be specified in the name. The class must be at the end of + the name (which uses dot notation from PYTHONPATH) and be seperated by a colon from + the module. + + Therefore, if you were to import the class like this: + + from path.to.module import SomeExtensionClass + + Then the named extension would comprise this string: + + "path.to.module:SomeExtensionClass" + + This allows multiple extensions to be implemented within the same module and still + accessable when the user isn't able to import the extension directly (perhaps from + a template filter or the command line). + + This also means that extension modules are no longer required to include the + `makeExtension` function which returns an instance of the extension class. However, + if the user does not specify the class name (she only provides `"path.to.module"`) + the extension will fail to load without the `makeExtension` function included in + the module. Extension authors will want to document carfully what is required to + load their extensions. + +[ex]: reference.html#extensions + +* The Extension Configuration code has been refactord to make it a little easier + for extension authors to work with config settings. As a result, the + [`extension_configs`][ec] keyword now accepts a dictionary rather than requiring + a list of tuples. A list of tuples is still supported so no one needs to change + their existing code. This should also simplify the learning curve for new users. + + Extension authors are encouraged to review the new methods available on the + `markdown.extnesions.Extension` class for handling configs and adjust their + code going forward. The included extensions provide a model for best practices. [ec]: reference.html#extension_configs -* The [Command Line Interface][cli] now accepts a `--extensions_config` (or `-c`) option -which accepts a filename and passes the parsed content of a [YAML] or [JSON] file to the -[`extension_configs`][ec] keyword of the `markdown.Markdown` class. The contents of -the YAML or JSON must map to a Python Dictionary which matches the format required -by the `extension_configs` kerword. Note that [PyYAML] is required to parse YAML files. +* The [Command Line Interface][cli] now accepts a `--extensions_config` (or `-c`) + option which accepts a filename and passes the parsed content of a [YAML] or + [JSON] file to the [`extension_configs`][ec] keyword of the `markdown.Markdown` + class. The contents of the YAML or JSON must map to a Python Dictionary which + matches the format required by the `extension_configs` kerword. Note that + [PyYAML] is required to parse YAML files. [cli]: cli.html#using-extensions [YAML]: http://yaml.org/ [JSON]: http://json.org/ [PyYAML]: http://pyyaml.org/ -* There have been various refactors of the testing framework. While those changes -will not directly effect end users, the code is being better tested which will -benefit everyone. +* There have been various refactors of the testing framework. While those changes + will not directly effect end users, the code is being better tested which will + benefit everyone. -* Various bug fixes have been made. See the -[commit log](https://github.com/waylan/Python-Markdown/commits/master) -for a complete history of the changes. +* Various bug fixes have been made. See the + [commit log](https://github.com/waylan/Python-Markdown/commits/master) + for a complete history of the changes. diff --git a/markdown/__init__.py b/markdown/__init__.py index 59dda4c..28f30c8 100644 --- a/markdown/__init__.py +++ b/markdown/__init__.py @@ -36,6 +36,7 @@ from .__version__ import version, version_info import codecs import sys import logging +import importlib from . import util from .preprocessors import build_preprocessors from .blockprocessors import build_block_parser @@ -163,6 +164,8 @@ class Markdown(object): ext = self.build_extension(ext, configs.get(ext, [])) if isinstance(ext, Extension): ext.extendMarkdown(self, globals()) + logger.info('Successfully loaded extension "%s.%s".' + % (ext.__class__.__module__, ext.__class__.__name__)) elif ext is not None: raise TypeError( 'Extension "%s.%s" must be of type: "markdown.Extension"' @@ -187,35 +190,46 @@ class Markdown(object): pairs = [x.split("=") for x in ext_args.split(",")] configs.update([(x.strip(), y.strip()) for (x, y) in pairs]) - # Setup the module name - module_name = ext_name - if '.' not in ext_name: - module_name = '.'.join(['markdown.extensions', ext_name]) + # Get class name (if provided): `path.to.module:ClassName` + ext_name, class_name = ext_name.split(':', 1) if ':' in ext_name else (ext_name, '') # Try loading the extension first from one place, then another - try: # New style (markdown.extensions.<extension>) - module = __import__(module_name, {}, {}, [str(module_name.rpartition('.')[0])]) + try: + # Assume string uses dot syntax (`path.to.some.module`) + module = importlib.import_module(ext_name) + logger.debug('Successfuly imported extension module "%s".' % ext_name) except ImportError: - module_name_old_style = '_'.join(['mdx', ext_name]) - try: # Old style (mdx_<extension>) - module = __import__(module_name_old_style) - except ImportError as e: - message = "Failed loading extension '%s' from '%s' or '%s'" \ - % (ext_name, module_name, module_name_old_style) + # Preppend `markdown.extensions.` to name + module_name = '.'.join(['markdown.extensions', ext_name]) + try: + module = importlib.import_module(module_name) + logger.debug('Successfuly imported extension module "%s".' % module_name) + except ImportError: + # Preppend `mdx_` to name + module_name_old_style = '_'.join(['mdx', ext_name]) + try: + module = importlib.import_module(module_name_old_style) + logger.debug('Successfuly imported extension module "%s".' % module_name_old_style) + except ImportError as e: + message = "Failed loading extension '%s' from '%s', '%s' or '%s'" \ + % (ext_name, ext_name, module_name, module_name_old_style) + e.args = (message,) + e.args[1:] + raise + + if class_name: + # Load given class name from module. + return getattr(module, class_name)(configs.items()) + else: + # Expect makeExtension() function to return a class. + try: + return module.makeExtension(configs.items()) + except AttributeError as e: + message = e.args[0] + message = "Failed to initiate extension " \ + "'%s': %s" % (ext_name, message) e.args = (message,) + e.args[1:] raise - # If the module is loaded successfully, we expect it to define a - # function called makeExtension() - try: - return module.makeExtension(configs.items()) - except AttributeError as e: - message = e.args[0] - message = "Failed to initiate extension " \ - "'%s': %s" % (ext_name, message) - e.args = (message,) + e.args[1:] - raise - def registerExtension(self, extension): """ This gets called by the extension """ self.registeredExtensions.append(extension) @@ -235,7 +235,6 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', diff --git a/tests/test_apis.py b/tests/test_apis.py index a5e0d73..e347cf9 100644 --- a/tests/test_apis.py +++ b/tests/test_apis.py @@ -40,6 +40,19 @@ class TestMarkdownBasics(unittest.TestCase): """ Test simple input. """ self.assertEqual(self.md.convert('foo'), '<p>foo</p>') + def testInstanceExtension(self): + """ Test Extension loading with a class instance. """ + from markdown.extensions.footnotes import FootnoteExtension + markdown.Markdown(extensions=[FootnoteExtension()]) + + def testNamedExtension(self): + """ Test Extension loading with Name (`path.to.module`). """ + markdown.Markdown(extensions=['markdown.extensions.footnotes']) + + def TestNamedExtensionWithClass(self): + """ Test Extension loading with class name (`path.to.module:Class`). """ + markdown.Markdown(extensions=['markdown.extensions.footnotes:FootnoteExtension']) + class TestBlockParser(unittest.TestCase): """ Tests of the BlockParser class. """ @@ -276,24 +289,33 @@ class TestErrors(unittest.TestCase): def testLoadBadExtension(self): """ Test loading of an Extension with no makeExtension function. """ - _create_fake_extension(name='fake', has_factory_func=False) - self.assertRaises(AttributeError, markdown.Markdown, extensions=['fake']) + _create_fake_extension(name='fake_a', has_factory_func=False) + self.assertRaises(AttributeError, markdown.Markdown, extensions=['fake_a']) def testNonExtension(self): """ Test loading a non Extension object as an extension. """ - _create_fake_extension(name='fake', is_wrong_type=True) - self.assertRaises(TypeError, markdown.Markdown, extensions=['fake']) + _create_fake_extension(name='fake_b', is_wrong_type=True) + self.assertRaises(TypeError, markdown.Markdown, extensions=['fake_b']) def testBaseExtention(self): """ Test that the base Extension class will raise NotImplemented. """ - _create_fake_extension(name='fake') + _create_fake_extension(name='fake_c') + self.assertRaises(NotImplementedError, + markdown.Markdown, extensions=['fake_c']) + + def testDotSyntaxExtention(self): + """ Test that dot syntax imports properly (not using mdx_). """ + _create_fake_extension(name='fake_d', use_old_style=False) self.assertRaises(NotImplementedError, - markdown.Markdown, extensions=['fake']) + markdown.Markdown, extensions=['fake_d']) -def _create_fake_extension(name, has_factory_func=True, is_wrong_type=False): +def _create_fake_extension(name, has_factory_func=True, is_wrong_type=False, use_old_style=True): """ Create a fake extension module for testing. """ - mod_name = '_'.join(['mdx', name]) + if use_old_style: + mod_name = '_'.join(['mdx', name]) + else: + mod_name = name if not PY3: # mod_name must be bytes in Python 2.x mod_name = bytes(mod_name) @@ -563,7 +585,7 @@ class TestCliOptionParsing(unittest.TestCase): 'end_url': '.html', 'html_class': 'test', }, - 'footnotes': { + 'footnotes:FootnotesExtension': { 'PLACE_MARKER': '~~~footnotes~~~' } } @@ -579,7 +601,7 @@ class TestCliOptionParsing(unittest.TestCase): 'end_url': '.html', 'html_class': 'test', }, - 'footnotes': { + 'footnotes:FootnotesExtension': { 'PLACE_MARKER': '~~~footnotes~~~' } } @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, py31, py32, py33, py34 +envlist = py27, py31, py32, py33, py34 [testenv] downloadcache = {toxworkdir}/cache |