From cd7324333a995eeb62a3e6eacdb3b179c6256133 Mon Sep 17 00:00:00 2001 From: Waylan Limberg Date: Fri, 12 Jan 2018 22:48:41 -0500 Subject: Refactor Extension loading (#627) Deprecated naming support is removed: * Removed special treatment for modules in `markdown.extensions` * Removed support for `mdx_` prefixes. Support for Entry Point names added: Support for "short names" are now implemented with entry points. Therefore all the users who call extension names as `toc` will not get errors as the builtin extensions all have entry points defined which match the old "short names" for modules in `markdown.extensions`. The benefit is that any extension can offer the same support without requiring the user to manually copy a file to that location on the file system (way to many extension authors have included such instructions in their installation documentation). The one odd thing about this is that we have been issuing a DeprecationWarning for short names and now they are fully supported again. But I think it's the right thing to do. Support for using dot notation is not removed. After all, it was never deprecated. And we shouldn't "force" entry points. There are plenty of reasons why users may not want that and not all of them can be resolved by using class instances instead. All of the following ways to load an extension are valid: # Class instance from markdown.extensions.toc import TocExtension markdown.markdown(src, extensions=[TocExtension()] # Entry point name markdown.markdown(src, extensions=['toc']) # Dot notation with class markdown.markdown(src, extensions=['markdown.extensions.toc:TocExtension']) # Dot notation without class markdown.markdown(src, extensions=['markdown.extensions.toc']) --- .spell-dict | 1 + docs/cli.md | 34 ++++++++------ docs/extensions/abbreviations.md | 4 +- docs/extensions/admonition.md | 11 ++++- docs/extensions/api.md | 77 +++++++++++++++++++++++-------- docs/extensions/attr_list.md | 4 +- docs/extensions/code_hilite.md | 4 +- docs/extensions/definition_lists.md | 4 +- docs/extensions/extra.md | 2 +- docs/extensions/fenced_code_blocks.md | 4 +- docs/extensions/footnotes.md | 5 +- docs/extensions/index.md | 48 ++++++++++--------- docs/extensions/meta_data.md | 6 +-- docs/extensions/nl2br.md | 6 +-- docs/extensions/sane_lists.md | 4 +- docs/extensions/smart_strong.md | 13 ++---- docs/extensions/smarty.md | 6 +-- docs/extensions/tables.md | 4 +- docs/extensions/toc.md | 6 +-- docs/extensions/wikilinks.md | 4 +- docs/reference.md | 57 +++++++++++++++-------- markdown/core.py | 86 +++++++++++------------------------ markdown/extensions/extra.py | 14 +++--- setup.py | 19 ++++++++ tests/test_apis.py | 49 +++----------------- tests/test_extensions.py | 38 ++++++++-------- tests/test_legacy.py | 69 ++++++++++++---------------- 27 files changed, 288 insertions(+), 291 deletions(-) diff --git a/.spell-dict b/.spell-dict index ab6b7a5..139a195 100644 --- a/.spell-dict +++ b/.spell-dict @@ -86,6 +86,7 @@ sanitizer sanitizers Sauder schemeless +setuptools Sergej serializer serializers diff --git a/docs/cli.md b/docs/cli.md index dbac2c4..35c77b4 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -118,16 +118,26 @@ Using Extensions To load a Python-Markdown extension from the command line use the `-x` (or `--extension`) option. The extension module must be on your `PYTHONPATH` (see the [Extension API](extensions/api.md) for details). The extension can -then be invoked by the name of that module using Python's dot syntax: +then be invoked by the name assigned to an entry point or using Python's dot +notation to point to an extension + +For example, to load an extension with the assigned entry point name `myext`, +run the following command: + +```bash +python -m markdown -x myext input.txt +``` + +And to load an extension with Python's dot notation: ```bash -python -m markdown -x path.to.module input.txt +python -m markdown -x path.to.module:MyExtClass input.txt ``` To load multiple extensions, specify an `-x` option for each extension: ```bash -python -m markdown -x markdown.extensions.footnotes -x markdown.extensions.codehilite input.txt +python -m markdown -x myext -x path.to.module:MyExtClass input.txt ``` If the extension supports configuration options (see the documentation for the @@ -135,7 +145,7 @@ extension you are using to determine what settings it supports, if any), you can pass them in as well: ```bash -python -m markdown -x markdown.extensions.footnotes -c config.yml input.txt +python -m markdown -x myext -c config.yml input.txt ``` The `-c` (or `--extension_configs`) option accepts a file name. The file must be @@ -145,25 +155,19 @@ map to a Python Dictionary in the format required by the the file `config.yaml` referenced in the above example might look like this: ```yaml -markdown.extensions.footnotes: - PLACE_MARKER: ~~~~~~~~ - UNIQUE_IDS: True +myext: + option1: 'value1' + option2: True ``` Note that while the `--extension_configs` option does specify the -"markdown.extensions.footnotes" extension, you still need to load the extension -with the `-x` option, or the configuration for that extension will be ignored. +`myext` extension, you still need to load the extension with the `-x` option, +or the configuration for that extension will be ignored. The `--extension_configs` option will only support YAML configuration files if [PyYAML] is installed on your system. JSON should work with no additional dependencies. The format of your configuration file is automatically detected. -!!!warning - The previously documented method of appending the extension configuration - options as a string to the extension name will be deprecated in - Python-Markdown version 2.6. The `--extension_configs` option should be used - instead. See the [2.5 release notes] for more information. - [ec]: reference.html#extension_configs [YAML]: http://yaml.org/ [JSON]: http://json.org/ diff --git a/docs/extensions/abbreviations.md b/docs/extensions/abbreviations.md index d580d56..eed8788 100644 --- a/docs/extensions/abbreviations.md +++ b/docs/extensions/abbreviations.md @@ -39,7 +39,7 @@ is maintained by the W3C.

Usage ----- -See [Extensions](index.md) for general extension usage, specify `markdown.extensions.abbr` -as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `abbr` as the name +of the extension. This extension does not accept any special configuration options. diff --git a/docs/extensions/admonition.md b/docs/extensions/admonition.md index 26e6299..a6a8398 100644 --- a/docs/extensions/admonition.md +++ b/docs/extensions/admonition.md @@ -78,7 +78,14 @@ rST suggests the following `types`, but you're free to use whatever you want: Styling ------- -There is no CSS included as part of this extension. Look up the default -[Sphinx][sphinx] theme if you need inspiration. +There is no CSS included as part of this extension. Check out the default +[Sphinx][sphinx] theme for inspiration. [sphinx]: http://sphinx.pocoo.org/ + +## Usage + +See [Extensions](index.md) for general extension usage. Use `admonition` as the +name of the extension. + +This extension does not accept any special configuration options. diff --git a/docs/extensions/api.md b/docs/extensions/api.md index 3d8cfff..cba4ea7 100644 --- a/docs/extensions/api.md +++ b/docs/extensions/api.md @@ -639,7 +639,7 @@ following methods available to assist in working with configuration settings: Sets multiple configuration settings given a dict of key/value pairs. -### `makeExtension` {: #makeextension } +### Naming an Extension { #naming_an_extension } As noted in the [library reference] an instance of an extension can be passed directly to Markdown. In fact, this is the preferred way to use third-party @@ -649,36 +649,70 @@ For example: ```python import markdown -import myextension -myext = myextension.MyExtension(option='value') -md = markdown.Markdown(extensions=[myext]) +from path.to.module import MyExtention +md = markdown.Markdown(extensions=[MyExtension(option='value')]) ``` -Markdown also accepts "named" third party extensions for those occasions when it -is impractical to import an extension directly (from the command line or from -within templates). +However, Markdown also accepts "named" third party extensions for those +occasions when it is impractical to import an extension directly (from the +command line or from within templates). A "name" can either be a registered +[entry point](#entry_point) or a string using Python's [dot +notation](#dot_notation). -The "name" of your extension must be a string consisting of the importable path to -your module using Python's dot notation. Therefore, if you are providing a library -to your users and would like to include a custom markdown extension within your -library, that extension would be named `"mylib.mdext.myext"` where `mylib/mdext/myext.py` -contains the extension and the `mylib` directory is on the PYTHONPATH. +#### Entry Point { #entry_point } + +[Entry points] are defined in a Python package's `setup.py` script. The script +must use [setuptools] to support entry points. Python-Markdown extensions must +be assigned to the `markdown.extensions` group. An entry point definition might +look like this: + +```python +from setuptools import setup + +setup( + # ... + entry_points={ + 'markdown.extensions': ['myextension = path.to.module:MyExtension'] + } +) +``` + +After a user installs your extension using the above script, they could then +call the extension using the `myextension` string name like this: + +```python +markdown.markdown(text, extensions=['myextention']) +``` + +Note that if two or more entry points within the same group are assigned the +same name, Python-Markdown will only ever use the first one found and ignore all +others. Therefore, be sure to give your extension a unique name. + +For more information on writing `setup.py` scripts, see the Python documentation +on [Packaging and Distributing Projects]. + +#### Dot Notation { #dot_notation } + +If an extension does not have a registered entry point, Python's dot notation +may be used instead. The extension must be installed as a Python module on your +PYTHONPATH. Generally, a class should be specified in the name. The class must +be at the end of the name and be separated by a colon from the module. -The string can also include the name of the class separated by a colon. Therefore, if you were to import the class like this: ```python -from path.to.module import SomeExtensionClass +from path.to.module import MyExtention ``` -Then the named extension would comprise this string: +Then the extension can be loaded as follows: ```python -"path.to.module:SomeExtensionClass" +markdown.markdown(text, extensions=['path.to.module:MyExtention']) ``` You do not need to do anything special to support this feature. As long as your -extension class is able to be imported, a user can include it with the above syntax. +extension class is able to be imported, a user can include it with the above +syntax. The above two methods are especially useful if you need to implement a large number of extensions with more than one residing in a module. However, if you do @@ -697,9 +731,9 @@ def makeExtension(**kwargs): return MyExtension(**kwargs) ``` -When Markdown is passed the "name" of your extension as a dot notation string, -it will import the module and call the `makeExtension` function to initiate your -extension. +When Markdown is passed the "name" of your extension as a dot notation string +that does not include a class (for example `path.to.module`), it will import the +module and call the `makeExtension` function to initiate your extension. [Preprocessors]: #preprocessors [Inline Patterns]: #inlinepatterns @@ -718,3 +752,6 @@ extension. [Footnotes]: https://github.com/Python-Markdown/mdx_footnotes [Definition Lists]: https://github.com/Python-Markdown/mdx_definition_lists [library reference]: ../reference.md +[setuptools]: https://packaging.python.org/key_projects/#setuptools +[Entry points]: https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins +[Packaging and Distributing Projects]: https://packaging.python.org/tutorials/distributing-packages/ diff --git a/docs/extensions/attr_list.md b/docs/extensions/attr_list.md index 7b2e19f..9fc8b6e 100644 --- a/docs/extensions/attr_list.md +++ b/docs/extensions/attr_list.md @@ -92,7 +92,7 @@ The above results in the following output: ## Usage -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.attr_list` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `attr_list` as the +name of the extension. This extension does not accept any special configuration options. diff --git a/docs/extensions/code_hilite.md b/docs/extensions/code_hilite.md index 6490dcc..552a82d 100644 --- a/docs/extensions/code_hilite.md +++ b/docs/extensions/code_hilite.md @@ -168,8 +168,8 @@ Lets see the source for that: ## Usage -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.codehilite` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `codehilite` as the +name of the extension. See the [Library Reference](../reference.md#extensions) for information about configuring extensions. diff --git a/docs/extensions/definition_lists.md b/docs/extensions/definition_lists.md index e9f8984..0d42fd0 100644 --- a/docs/extensions/definition_lists.md +++ b/docs/extensions/definition_lists.md @@ -46,7 +46,7 @@ the family Rosaceae. Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.def_list` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `def_list` as the +name of the extension. This extension does not accept any special configuration options. diff --git a/docs/extensions/extra.md b/docs/extensions/extra.md index 0639d3d..3d9f374 100644 --- a/docs/extensions/extra.md +++ b/docs/extensions/extra.md @@ -26,7 +26,7 @@ From the Python interpreter: ```pycon >>> import markdown ->>> html = markdown.markdown(text, ['markdown.extensions.extra']) +>>> html = markdown.markdown(text, ['extra']) ``` There may be [additional extensions](index.md) that are distributed with diff --git a/docs/extensions/fenced_code_blocks.md b/docs/extensions/fenced_code_blocks.md index b7a657e..96fe786 100644 --- a/docs/extensions/fenced_code_blocks.md +++ b/docs/extensions/fenced_code_blocks.md @@ -110,7 +110,7 @@ The lines can be specified with PHP Extra's syntax: ## Usage -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.fenced_code` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `fenced_code` as +the name of the extension. This extension does not accept any special configuration options. diff --git a/docs/extensions/footnotes.md b/docs/extensions/footnotes.md index aaff184..4df12a1 100644 --- a/docs/extensions/footnotes.md +++ b/docs/extensions/footnotes.md @@ -63,8 +63,8 @@ is indented consistently and any errors are more easily discernible by the autho Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.footnotes` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `footnotes` as the +name of the extension. See the [Library Reference](../reference.md#extensions) for information about configuring extensions. @@ -90,4 +90,3 @@ The following options are provided to configure the output: The text string for the `title` HTML attribute of the footnote definition link. `%d` will be replaced by the footnote number. Defaults to `Jump back to footnote %d in the text` - diff --git a/docs/extensions/index.md b/docs/extensions/index.md index 34f8084..3d98760 100644 --- a/docs/extensions/index.md +++ b/docs/extensions/index.md @@ -1,7 +1,6 @@ title: Extensions -Available Extensions -==================== +# Extensions Python Markdown offers a flexible extension mechanism, which makes it possible to change and/or extend the behavior of the parser without having to edit the @@ -10,7 +9,7 @@ actual source files. To use an extension, pass it to markdown with the `extensions` keyword. ```python -markdown.markdown(some_text, extensions=[MyExtension(), 'path.to.my.ext', 'markdown.extensions.footnotes']) +markdown.markdown(some_text, extensions=[MyExtClass(), 'myext', 'path.to.my.ext:MyExtClass']) ``` See the [Library Reference](../reference.md#extensions) for more details. @@ -18,7 +17,7 @@ See the [Library Reference](../reference.md#extensions) for more details. From the command line, specify an extension with the `-x` option. ```bash -python -m markdown -x markdown.extensions.footnotes -x markdown.extensions.tables input.txt > output.html +python -m markdown -x myext -x path.to.module:MyExtClass input.txt > output.html ``` See the [Command Line docs](../cli.md) or use the `--help` option for more details. @@ -34,27 +33,26 @@ The extensions listed below are included with (at least) the most recent release and are officially supported by Python-Markdown. Any documentation is maintained here and all bug reports should be made to the project. If you have a typical install of Python-Markdown, these extensions are already -available to you using the "name" listed in the second column below. - -Extension | "Name" ------------------------------------- | --------------- -[Extra] | `markdown.extensions.extra` -    [Abbreviations][] | `markdown.extensions.abbr` -    [Attribute Lists][] | `markdown.extensions.attr_list` -    [Definition Lists][] | `markdown.extensions.def_list` -    [Fenced Code Blocks][] | `markdown.extensions.fenced_code` -    [Footnotes][] | `markdown.extensions.footnotes` -    [Tables][] | `markdown.extensions.tables` -    [Smart Strong][] | `markdown.extensions.smart_strong` -[Admonition][] | `markdown.extensions.admonition` -[CodeHilite][] | `markdown.extensions.codehilite` -[HeaderId] | `markdown.extensions.headerid` -[Meta-Data] | `markdown.extensions.meta` -[New Line to Break] | `markdown.extensions.nl2br` -[Sane Lists] | `markdown.extensions.sane_lists` -[SmartyPants] | `markdown.extensions.smarty` -[Table of Contents] | `markdown.extensions.toc` -[WikiLinks] | `markdown.extensions.wikilinks` +available to you using the "Entry Point" name listed in the second column below. + +Extension | Entry Point | Dot Notation +------------------------------------ | -------------- | ------------ +[Extra] | `extra` | `markdown.extensions.extra` +    [Abbreviations][] | `abbr` | `markdown.extensions.abbr` +    [Attribute Lists][] | `attr_list` | `markdown.extensions.attr_list` +    [Definition Lists][] | `def_list` | `markdown.extensions.def_list` +    [Fenced Code Blocks][] | `fenced_code` | `markdown.extensions.fenced_code` +    [Footnotes][] | `footnotes` | `markdown.extensions.footnotes` +    [Tables][] | `tables` | `markdown.extensions.tables` +    [Smart Strong][] | `smart_strong` | `markdown.extensions.smart_strong` +[Admonition][] | `admonition` | `markdown.extensions.admonition` +[CodeHilite][] | `codehilite` | `markdown.extensions.codehilite` +[Meta-Data] | `meta` | `markdown.extensions.meta` +[New Line to Break] | `nl2br` | `markdown.extensions.nl2br` +[Sane Lists] | `sane_lists` | `markdown.extensions.sane_lists` +[SmartyPants] | `smarty` | `markdown.extensions.smarty` +[Table of Contents] | `toc` | `markdown.extensions.toc` +[WikiLinks] | `wikilinks` | `markdown.extensions.wikilinks` [Extra]: extra.md [Abbreviations]: abbreviations.md diff --git a/docs/extensions/meta_data.md b/docs/extensions/meta_data.md index 36d5e7a..29f3e0b 100644 --- a/docs/extensions/meta_data.md +++ b/docs/extensions/meta_data.md @@ -57,8 +57,8 @@ by Markdown. Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.meta` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `meta` as the name +of the extension. Accessing the Meta-Data ----------------------- @@ -67,7 +67,7 @@ The meta-data is made available as a python Dict in the `Meta` attribute of an instance of the Markdown class. For example, using the above document: ```pycon ->>> md = markdown.Markdown(extensions = ['markdown.extensions.meta']) +>>> md = markdown.Markdown(extensions = ['meta']) >>> html = md.convert(text) >>> # Meta-data has been stripped from output >>> print html diff --git a/docs/extensions/nl2br.md b/docs/extensions/nl2br.md index 4f5e611..8c53d33 100644 --- a/docs/extensions/nl2br.md +++ b/docs/extensions/nl2br.md @@ -20,7 +20,7 @@ Example ... Line 1 ... Line 2 ... """ ->>> html = markdown.markdown(text, extensions=['markdown.extensions.nl2br']) +>>> html = markdown.markdown(text, extensions=['nl2br']) >>> print html

Line 1
Line 2

@@ -29,7 +29,7 @@ Line 2

Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.nl2br` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `nl2br` as the name +of the extension. This extension does not accept any special configuration options. diff --git a/docs/extensions/sane_lists.md b/docs/extensions/sane_lists.md index 49a7a85..81f8d84 100644 --- a/docs/extensions/sane_lists.md +++ b/docs/extensions/sane_lists.md @@ -71,7 +71,7 @@ In all other ways, Sane Lists should behave as normal Markdown lists. Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.sane_lists` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `sane_lists` as the +name of the extension. This extension does not accept any special configuration options. diff --git a/docs/extensions/smart_strong.md b/docs/extensions/smart_strong.md index 1fb3c68..65c88d6 100644 --- a/docs/extensions/smart_strong.md +++ b/docs/extensions/smart_strong.md @@ -19,21 +19,18 @@ Example ```pycon >>> import markdown ->>> markdown.markdown('Text with double__underscore__words.', \ - extensions=['markdown.extensions.smart_strong']) +>>> markdown.markdown('Text with double__underscore__words.', extensions=['smart_strong']) u'

Text with double__underscore__words.

' ->>> markdown.markdown('__Strong__ still works.', \ - extensions=['markdown.extensions.smart_strong']) +>>> markdown.markdown('__Strong__ still works.', extensions=['smart_strong']) u'

Strong still works.

' ->>> markdown.markdown('__this__works__too__.', \ - extensions=['markdown.extensions.smart_strong']) +>>> markdown.markdown('__this__works__too__.', extensions=['smart_strong']) u'

this__works__too.

' ``` Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.smart_strong` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `smart_strong` as +the name of the extension. This extension does not accept any special configuration options. diff --git a/docs/extensions/smarty.md b/docs/extensions/smarty.md index 9353d9b..109d78c 100644 --- a/docs/extensions/smarty.md +++ b/docs/extensions/smarty.md @@ -27,7 +27,7 @@ the German language: ```python extension_configs = { - 'markdown.extensions.smarty': { + 'smarty': { 'substitutions': { 'left-single-quote': '‚', # sb is not a typo! 'right-single-quote': '‘', @@ -53,8 +53,8 @@ extension_configs = { Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.smarty` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `smarty` as the +name of the extension. See the [Library Reference](../reference.md#extensions) for information about configuring extensions. diff --git a/docs/extensions/tables.md b/docs/extensions/tables.md index 2bea470..59693f0 100644 --- a/docs/extensions/tables.md +++ b/docs/extensions/tables.md @@ -52,7 +52,7 @@ will be rendered as: Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.tables` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `tables` as the +name of the extension. This extension does not accept any special configuration options. diff --git a/docs/extensions/toc.md b/docs/extensions/toc.md index a1c583f..e358132 100644 --- a/docs/extensions/toc.md +++ b/docs/extensions/toc.md @@ -65,7 +65,7 @@ This allows one to insert the Table of Contents elsewhere in their page template. For example: ```pycon ->>> md = markdown.Markdown(extensions=['markdown.extensions.toc']) +>>> md = markdown.Markdown(extensions=['toc']) >>> html = md.convert(text) >>> page = render_some_template(context={'body': html, 'toc': md.toc}) ``` @@ -73,8 +73,8 @@ template. For example: Usage ----- -See [Extensions](index.md) for general extension usage, specify `markdown.extensions.toc` -as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `toc` as the name +of the extension. See the [Library Reference](../reference.md#extensions) for information about configuring extensions. diff --git a/docs/extensions/wikilinks.md b/docs/extensions/wikilinks.md index a46265a..dd82a96 100644 --- a/docs/extensions/wikilinks.md +++ b/docs/extensions/wikilinks.md @@ -46,8 +46,8 @@ becomes ## Usage -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.wikilinks` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `wikilinks` as the +name of the extension. See the [Library Reference](../reference.md#extensions) for information about configuring extensions. diff --git a/docs/reference.md b/docs/reference.md index 55fb501..1d73439 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -67,7 +67,7 @@ __extensions__{: #extensions } of extension names. :::python - extensions=[MyExtension(), 'path.to.my.ext'] + extensions=[MyExtClass(), 'myext', 'path.to.my.ext:MyExtClass'] !!! note The preferred method is to pass in an instance of an extension. Strings @@ -81,37 +81,48 @@ __extensions__{: #extensions } :::python from markdown.extensions import Extension - class MyExtension(Extension): + class MyExtClass(Extension): # define your extension here... - markdown.markdown(text, extensions=[MyExtension(option='value')]) + markdown.markdown(text, extensions=[MyExtClass(option='value')]) - If an extension name is provided as a string, the extension must be - importable as a python module on your PYTHONPATH. Python's dot notation is - required. Therefore, to import the 'extra' extension, one would do - `extensions=['markdown.extensions.extra']` + If an extension name is provided as a string, the string must either be the + registered entry point of any installed extension or the importable path + using Python's dot notation. - Additionally, a Class may be specified in the name. The class must be at the - end of the name and be separated by a colon from the module. + See the documentation specific to an extension for the string name assigned + to an extension as an entry point. Simply include the defined name as + a string in the list of extensions. For example, if an extension has the + name `myext` assigned to it and the extension is properly installed, then + do the following: + + :::python + markdown.markdown(text, extensions=['myext']) + + If an extension does not have a registered entry point, Python's dot + notation may be used instead. The extension must be installed as a + Python module on your PYTHONPATH. Generally, a class should be specified in + the name. The class must be at the end of the name and be separated by a + colon from the module. Therefore, if you were to import the class like this: :::python - from path.to.module import SomeExtensionClass + from path.to.module import MyExtClass - Then the named extension would comprise this string: + Then load the extension as follows: :::python - "path.to.module:SomeExtensionClass" + markdown.markdown(text, extensions=['path.to.module:MyExtClass']) - !!! note - You should only need to specify the class name if more than one extension - is defined within the same module. The extensions that come with - Python-Markdown do *not* need to have the class name specified. However, - doing so will not effect the behavior of the parser. + If only one extension is defined within a module and the module includes a + `makeExtension` function which returns an instance of the extension, then + the class name is not necessary. For example, in that case one could do + `extensions=['path.to.module']`. Check the documentation for a specific + extension to determine if it supports this feature. - When loading an extension by name (as a string), you may pass in - configuration settings to the extension using the + When loading an extension by name (as a string), you can only pass in + configuration settings to the extension by using the [`extension_configs`](#extension_configs) keyword. !!! seealso "See Also" @@ -144,6 +155,14 @@ __extension_configs__{: #extension_configs } } } + When specifying the extension name, be sure to use the exact same + string as is used in the [extensions](#extensions) keyword to load the + extension. Otherwise, the configuration settings will not be applied to + the extension. In other words, you cannot use the entry point in on + place and Python dot notation in the other. While both may be valid for + a given extension, they will not be recognized as being the same + extension by Markdown. + See the documentation specific to the extension you are using for help in specifying configuration settings for that extension. diff --git a/markdown/core.py b/markdown/core.py index 7d9d839..ce5c333 100644 --- a/markdown/core.py +++ b/markdown/core.py @@ -2,9 +2,9 @@ from __future__ import absolute_import from __future__ import unicode_literals import codecs import sys -import warnings import logging import importlib +import pkg_resources from . import util from .preprocessors import build_preprocessors from .blockprocessors import build_block_parser @@ -49,10 +49,11 @@ class Markdown(object): Keyword arguments: * extensions: A list of extensions. - If they are of type string, the module mdx_name.py will be loaded. - If they are a subclass of markdown.Extension, they will be used - as-is. - * extension_configs: Configuration settingis for extensions. + If an item is an instance of a subclass of `markdown.extension.Extension`, the instance will be used + as-is. If an item is of type string, first an entry point will be loaded. If that fails, the string is + assumed to use Python dot notation (`path.to.module:ClassName`) to load a markdown.Extension subclass. If + no class is specified, then a `makeExtension` function is called within the specified module. + * extension_configs: Configuration settings for extensions. * output_format: Format of output. Supported formats are: * "xhtml1": Outputs XHTML 1.x. Default. * "xhtml5": Outputs XHTML style tags of HTML 5 @@ -108,8 +109,8 @@ class Markdown(object): Keyword arguments: * extensions: A list of extensions, which can either - be strings or objects. See the docstring on Markdown. - * configs: A dictionary mapping module names to config options. + be strings or objects. + * configs: A dictionary mapping extension names to config options. """ for ext in extensions: @@ -130,72 +131,37 @@ class Markdown(object): def build_extension(self, ext_name, configs): """ - Build extension by name, then return the module. + Build extension from a string name, then return an instance. - """ + First attempt to load an entry point. The string name must be registered as an entry point in the + `markdown.extensions` group which points to a subclass of the `markdown.extensions.Extension` class. If + multiple distributions have registered the same name, the first one found by `pkg_resources.iter_entry_points` + is returned. + If no entry point is found, assume dot notation (`path.to.module:ClassName`). Load the specified class and + return an instance. If no class is specified, import the module and call a `makeExtension` function and return + the Extension instance returned by that function. + """ configs = dict(configs) + entry_points = [ep for ep in pkg_resources.iter_entry_points('markdown.extensions', ext_name)] + if entry_points: + ext = entry_points[0].load() + return ext(**configs) + # 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: - # Assume string uses dot syntax (`path.to.some.module`) module = importlib.import_module(ext_name) logger.debug( 'Successfuly imported extension module "%s".' % ext_name ) - # For backward compat (until deprecation) - # check that this is an extension. - if ('.' not in ext_name and not (hasattr(module, 'makeExtension') or - (class_name and hasattr(module, class_name)))): - # We have a name conflict - # eg: extensions=['tables'] and PyTables is installed - raise ImportError - except ImportError: - # 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 - ) - warnings.warn('Using short names for Markdown\'s builtin ' - 'extensions is deprecated. Use the ' - 'full path to the extension with Python\'s dot ' - 'notation (eg: "%s" instead of "%s"). The ' - 'current behavior will raise an error in version ' - '2.7. See the Release Notes for ' - 'Python-Markdown version 2.6 for more info.' % - (module_name, ext_name), - DeprecationWarning) - 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) - warnings.warn('Markdown\'s behavior of prepending "mdx_" ' - 'to an extension name is deprecated. ' - 'Use the full path to the ' - 'extension with Python\'s dot notation ' - '(eg: "%s" instead of "%s"). The current ' - 'behavior will raise an error in version 2.7. ' - 'See the Release Notes for Python-Markdown ' - 'version 2.6 for more info.' % - (module_name_old_style, ext_name), - DeprecationWarning) - 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 + except ImportError as e: + message = 'Failed loading extension "%s".' % ext_name + e.args = (message,) + e.args[1:] + raise if class_name: # Load given class name from module. diff --git a/markdown/extensions/extra.py b/markdown/extensions/extra.py index f59e09e..cea18ed 100644 --- a/markdown/extensions/extra.py +++ b/markdown/extensions/extra.py @@ -37,13 +37,13 @@ from .. import util import re extensions = [ - 'markdown.extensions.smart_strong', - 'markdown.extensions.fenced_code', - 'markdown.extensions.footnotes', - 'markdown.extensions.attr_list', - 'markdown.extensions.def_list', - 'markdown.extensions.tables', - 'markdown.extensions.abbr' + 'smart_strong', + 'fenced_code', + 'footnotes', + 'attr_list', + 'def_list', + 'tables', + 'abbr' ] diff --git a/setup.py b/setup.py index 6f0a1e7..73c7ab3 100755 --- a/setup.py +++ b/setup.py @@ -59,6 +59,25 @@ setup( entry_points={ 'console_scripts': [ '%s = markdown.__main__:run' % SCRIPT_NAME, + ], + # Register the built in extensions + 'markdown.extensions': [ + 'abbr = markdown.extensions.abbr:AbbrExtension', + 'admonition = markdown.extensions.admonition:AdmonitionExtension', + 'attr_list = markdown.extensions.attr_list:AttrListExtension', + 'codehilite = markdown.extensions.codehilite:CodeHiliteExtension', + 'def_list = markdown.extensions.def_list:DefListExtension', + 'extra = markdown.extensions.extra:ExtraExtension', + 'fenced_code = markdown.extensions.fenced_code:FencedCodeExtension', + 'footnotes = markdown.extensions.footnotes:FootnoteExtension', + 'meta = markdown.extensions.meta:MetaExtension', + 'nl2br = markdown.extensions.nl2br:Nl2BrExtension', + 'sane_lists = markdown.extensions.sane_lists:SaneListExtension', + 'smart_strong = markdown.extensions.smart_strong:SmartEmphasisExtension', + 'smarty = markdown.extensions.smarty:SmartyExtension', + 'tables = markdown.extensions.tables:TableExtension', + 'toc = markdown.extensions.toc:TocExtension', + 'wikilinks = markdown.extensions.wikilinks:WikiLinkExtension', ] }, classifiers=[ diff --git a/tests/test_apis.py b/tests/test_apis.py index 6a1829b..42e7496 100644 --- a/tests/test_apis.py +++ b/tests/test_apis.py @@ -11,7 +11,6 @@ from __future__ import unicode_literals import unittest import sys import os -import types import markdown import warnings from markdown.__main__ import parse_options @@ -46,11 +45,15 @@ class TestMarkdownBasics(unittest.TestCase): from markdown.extensions.footnotes import FootnoteExtension markdown.Markdown(extensions=[FootnoteExtension()]) - def testNamedExtension(self): + def testEntryPointExtension(self): + """ Test Extension loading with an entry point. """ + markdown.Markdown(extensions=['footnotes']) + + def testDotNotationExtension(self): """ Test Extension loading with Name (`path.to.module`). """ markdown.Markdown(extensions=['markdown.extensions.footnotes']) - def TestNamedExtensionWithClass(self): + def TestDotNotationExtensionWithClass(self): """ Test Extension loading with class name (`path.to.module:Class`). """ markdown.Markdown(extensions=['markdown.extensions.footnotes:FootnoteExtension']) @@ -343,46 +346,6 @@ class TestErrors(unittest.TestCase): markdown.Markdown, extensions=[markdown.extensions.Extension()] ) - def testMdxExtention(self): - """ Test that prepending mdx_ raises a DeprecationWarning. """ - _create_fake_extension(name='fake', use_old_style=True) - self.assertRaises( - DeprecationWarning, - markdown.Markdown, extensions=['fake'] - ) - - def testShortNameExtention(self): - """ Test that using a short name raises a DeprecationWarning. """ - self.assertRaises( - DeprecationWarning, - markdown.Markdown, extensions=['footnotes'] - ) - - -def _create_fake_extension(name, has_factory_func=True, is_wrong_type=False, use_old_style=False): - """ Create a fake extension module for testing. """ - 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) - ext_mod = types.ModuleType(mod_name) - - def makeExtension(*args, **kwargs): - if is_wrong_type: - return object - else: - return markdown.extensions.Extension(*args, **kwargs) - - if has_factory_func: - ext_mod.makeExtension = makeExtension - # Warning: this brute forces the extenson module onto the system. Either - # this needs to be specificly overriden or a new python session needs to - # be started to get rid of this. This should be ok in a testing context. - sys.modules[mod_name] = ext_mod - class testETreeComments(unittest.TestCase): """ diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 5dee0fd..5a04e64 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -72,7 +72,7 @@ class TestAbbr(unittest.TestCase): """ Test abbr extension. """ def setUp(self): - self.md = markdown.Markdown(extensions=['markdown.extensions.abbr']) + self.md = markdown.Markdown(extensions=['abbr']) def testSimpleAbbr(self): """ Test Abbreviations. """ @@ -109,7 +109,7 @@ class TestCodeHilite(TestCaseWithAssertStartsWith): def testBasicCodeHilite(self): text = '\t# A Code Comment' - md = markdown.Markdown(extensions=['markdown.extensions.codehilite']) + md = markdown.Markdown(extensions=['codehilite']) if self.has_pygments: # Pygments can use random lexer here as we did not specify the language self.assertStartsWith('
', md.convert(text))
@@ -205,7 +205,7 @@ class TestCodeHilite(TestCaseWithAssertStartsWith):
         text1 = "\t:::Python hl_lines='1'\n\t#line 1\n\t#line 2\n\t#line 3"
 
         for text in (text0, text1):
-            md = markdown.Markdown(extensions=['markdown.extensions.codehilite'])
+            md = markdown.Markdown(extensions=['codehilite'])
             if self.has_pygments:
                 self.assertStartsWith(
                     '
' in md.convert(text))
@@ -392,7 +392,7 @@ class TestMetaData(unittest.TestCase):
     """ Test MetaData extension. """
 
     def setUp(self):
-        self.md = markdown.Markdown(extensions=['markdown.extensions.meta'])
+        self.md = markdown.Markdown(extensions=['meta'])
 
     def testBasicMetaData(self):
         """ Test basic metadata. """
@@ -459,7 +459,7 @@ class TestWikiLinks(unittest.TestCase):
     """ Test Wikilinks Extension. """
 
     def setUp(self):
-        self.md = markdown.Markdown(extensions=['markdown.extensions.wikilinks'])
+        self.md = markdown.Markdown(extensions=['wikilinks'])
         self.text = "Some text with a [[WikiLink]]."
 
     def testBasicWikilinks(self):
@@ -500,9 +500,9 @@ class TestWikiLinks(unittest.TestCase):
         """ Test Complex Settings. """
 
         md = markdown.Markdown(
-            extensions=['markdown.extensions.wikilinks'],
+            extensions=['wikilinks'],
             extension_configs={
-                'markdown.extensions.wikilinks': [
+                'wikilinks': [
                     ('base_url', 'http://example.com/'),
                     ('end_url', '.html'),
                     ('html_class', '')
@@ -524,7 +524,7 @@ wiki_end_url:   .html
 wiki_html_class:
 
 Some text with a [[WikiLink]]."""
-        md = markdown.Markdown(extensions=['markdown.extensions.meta', 'markdown.extensions.wikilinks'])
+        md = markdown.Markdown(extensions=['meta', 'wikilinks'])
         self.assertEqual(
             md.convert(text),
             '

Some text with a ' @@ -557,7 +557,7 @@ class TestAdmonition(unittest.TestCase): """ Test Admonition Extension. """ def setUp(self): - self.md = markdown.Markdown(extensions=['markdown.extensions.admonition']) + self.md = markdown.Markdown(extensions=['admonition']) def testRE(self): RE = self.md.parser.blockprocessors['admonition'].RE @@ -574,7 +574,7 @@ class TestTOC(TestCaseWithAssertStartsWith): """ Test TOC Extension. """ def setUp(self): - self.md = markdown.Markdown(extensions=['markdown.extensions.toc']) + self.md = markdown.Markdown(extensions=['toc']) def testMarker(self): """ Test TOC with a Marker. """ @@ -818,7 +818,7 @@ class TestTOC(TestCaseWithAssertStartsWith): def testWithAttrList(self): """ Test TOC with attr_list Extension. """ - md = markdown.Markdown(extensions=['markdown.extensions.toc', 'markdown.extensions.attr_list']) + md = markdown.Markdown(extensions=['toc', 'attr_list']) text = '# Header 1\n\n## Header 2 { #foo }' self.assertEqual( md.convert(text), @@ -849,7 +849,7 @@ class TestTOC(TestCaseWithAssertStartsWith): class TestSmarty(unittest.TestCase): def setUp(self): config = { - 'markdown.extensions.smarty': [ + 'smarty': [ ('smart_angled_quotes', True), ('substitutions', { 'ndash': '\u2013', @@ -865,7 +865,7 @@ class TestSmarty(unittest.TestCase): ] } self.md = markdown.Markdown( - extensions=['markdown.extensions.smarty'], + extensions=['smarty'], extension_configs=config ) @@ -885,8 +885,8 @@ class TestFootnotes(unittest.TestCase): def testBacklinkText(self): md = markdown.Markdown( - extensions=['markdown.extensions.footnotes'], - extension_configs={'markdown.extensions.footnotes': {'BACKLINK_TEXT': 'back'}} + extensions=['footnotes'], + extension_configs={'footnotes': {'BACKLINK_TEXT': 'back'}} ) text = 'paragraph[^1]\n\n[^1]: A Footnote' self.assertEqual( diff --git a/tests/test_legacy.py b/tests/test_legacy.py index ddc54bb..7fca02a 100644 --- a/tests/test_legacy.py +++ b/tests/test_legacy.py @@ -123,80 +123,67 @@ class TestExtensions(LegacyTestCase): location = os.path.join(parent_test_dir, 'extensions') exclude = ['codehilite'] - attr_list = Kwargs( - extensions=[ - 'markdown.extensions.attr_list', - 'markdown.extensions.def_list', - 'markdown.extensions.smarty' - ] - ) + attr_list = Kwargs(extensions=['attr_list', 'def_list', 'smarty']) - codehilite = Kwargs(extensions=['markdown.extensions.codehilite']) + codehilite = Kwargs(extensions=['codehilite']) - toc = Kwargs(extensions=['markdown.extensions.toc']) + toc = Kwargs(extensions=['toc']) - toc_invalid = Kwargs(extensions=['markdown.extensions.toc']) + toc_invalid = Kwargs(extensions=['toc']) - toc_out_of_order = Kwargs(extensions=['markdown.extensions.toc']) + toc_out_of_order = Kwargs(extensions=['toc']) toc_nested = Kwargs( - extensions=['markdown.extensions.toc'], - extension_configs={'markdown.extensions.toc': {'permalink': True}} + extensions=['toc'], + extension_configs={'toc': {'permalink': True}} ) toc_nested2 = Kwargs( - extensions=['markdown.extensions.toc'], - extension_configs={'markdown.extensions.toc': {'permalink': "[link]"}} + extensions=['toc'], + extension_configs={'toc': {'permalink': "[link]"}} ) - toc_nested_list = Kwargs(extensions=['markdown.extensions.toc']) + toc_nested_list = Kwargs(extensions=['toc']) - wikilinks = Kwargs(extensions=['markdown.extensions.wikilinks']) + wikilinks = Kwargs(extensions=['wikilinks']) - fenced_code = Kwargs(extensions=['markdown.extensions.fenced_code']) + fenced_code = Kwargs(extensions=['fenced_code']) - github_flavored = Kwargs(extensions=['markdown.extensions.fenced_code']) + github_flavored = Kwargs(extensions=['fenced_code']) - sane_lists = Kwargs(extensions=['markdown.extensions.sane_lists']) + sane_lists = Kwargs(extensions=['sane_lists']) - nl2br_w_attr_list = Kwargs( - extensions=[ - 'markdown.extensions.nl2br', - 'markdown.extensions.attr_list' - ] - ) + nl2br_w_attr_list = Kwargs(extensions=['nl2br', 'attr_list']) - admonition = Kwargs(extensions=['markdown.extensions.admonition']) + admonition = Kwargs(extensions=['admonition']) smarty = Kwargs( - extensions=['markdown.extensions.smarty'], - extension_configs={'markdown.extensions.smarty': {'smart_angled_quotes': True}} + extensions=['smarty'], + extension_configs={'smarty': {'smart_angled_quotes': True}} ) class TestExtensionsExtra(LegacyTestCase): location = os.path.join(parent_test_dir, 'extensions/extra') - default_kwargs = Kwargs(extensions=['markdown.extensions.extra']) + default_kwargs = Kwargs(extensions=['extra']) - loose_def_list = Kwargs(extensions=['markdown.extensions.def_list']) + loose_def_list = Kwargs(extensions=['def_list']) - simple_def_lists = Kwargs(extensions=['markdown.extensions.def_list']) + simple_def_lists = Kwargs(extensions=['def_list']) - abbr = Kwargs(extensions=['markdown.extensions.abbr']) + abbr = Kwargs(extensions=['abbr']) - footnotes = Kwargs(extensions=['markdown.extensions.footnotes']) + footnotes = Kwargs(extensions=['footnotes']) - tables = Kwargs(extensions=['markdown.extensions.tables']) + tables = Kwargs(extensions=['tables']) - tables_and_attr_list = Kwargs( - extensions=['markdown.extensions.tables', 'markdown.extensions.attr_list'] - ) + tables_and_attr_list = Kwargs(extensions=['tables', 'attr_list']) extra_config = Kwargs( - extensions=['markdown.extensions.extra'], + extensions=['extra'], extension_configs={ - 'markdown.extensions.extra': { - 'markdown.extensions.footnotes': { + 'extra': { + 'footnotes': { 'PLACE_MARKER': '~~~placemarker~~~' } } -- cgit v1.2.3