Repository: executablebooks/MyST-Parser
Pull request #696 (2023-02-10 02:17:56)

👌 IMPROVE: Allow heading_slug_func to be a string 
`myst_heading_slug_func` can now also be set to a string,
which will be interpreted as an import path to a function,
e.g. `myst_heading_slug_func = "mypackage.mymodule.slugify"`.

codecov[bot]: # [Codecov](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) Report
Base: **89.99**% // Head: **89.64**% // Decreases project coverage by **`-0.36%`** :warning:
> Coverage data is based on head [(`d067de5`)](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) compared to base [(`84c320b`)](https://codecov.io/gh/executablebooks/MyST-Parser/commit/84c320b27d049d9a223c88b50ca8cf159e7707b6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks).
> Patch coverage: 87.50% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #696      +/-   ##
==========================================
- Coverage   89.99%   89.64%   -0.36%     
==========================================
  Files          23       22       -1     
  Lines        2589     2674      +85     
==========================================
+ Hits         2330     2397      +67     
- Misses        259      277      +18     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytests | `89.64% <87.50%> (-0.36%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) | Coverage Δ | |
|---|---|---|
| [myst\_parser/config/main.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvY29uZmlnL21haW4ucHk=) | `85.63% <81.25%> (-2.89%)` | :arrow_down: |
| [myst\_parser/parsers/docutils\_.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvcGFyc2Vycy9kb2N1dGlsc18ucHk=) | `82.87% <84.00%> (-0.59%)` | :arrow_down: |
| [myst\_parser/mdit\_to\_docutils/base.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvbWRpdF90b19kb2N1dGlscy9iYXNlLnB5) | `93.03% <100.00%> (-0.01%)` | :arrow_down: |
| [myst\_parser/mocking.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvbW9ja2luZy5weQ==) | `84.82% <100.00%> (-0.67%)` | :arrow_down: |
| [myst\_parser/sphinx\_ext/directives.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvc3BoaW54X2V4dC9kaXJlY3RpdmVzLnB5) | `81.53% <100.00%> (ø)` | |
| [myst\_parser/sphinx\_ext/main.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvc3BoaW54X2V4dC9tYWluLnB5) | `88.57% <100.00%> (ø)` | |
| [myst\_parser/config/dc\_validators.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvY29uZmlnL2RjX3ZhbGlkYXRvcnMucHk=) | `83.63% <0.00%> (-1.82%)` | :arrow_down: |
| ... and [1 more](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/executablebooks/MyST-Parser/pull/696?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks).


Patch:
diff --git a/docs/syntax/optional.md b/docs/syntax/optional.md
index 15f8363..0e332eb 100644
--- a/docs/syntax/optional.md
+++ b/docs/syntax/optional.md
@@ -520,6 +520,12 @@ The anchor "slugs" created aim to follow the [GitHub implementation](https://git
 
 To change the slug generation function, set `myst_heading_slug_func` in your `conf.py` to a function that accepts a string and returns a string.
 
+:::{versionadded} 0.19.0
+`myst_heading_slug_func` can now also be set to a string,
+which will be interpreted as an import path to a function,
+e.g. `myst_heading_slug_func = "mypackage.mymodule.slugify"`.
+:::
+
 ### Inspect the links that will be created
 
 You can inspect the links that will be created using the command-line tool:
diff --git a/myst_parser/config/main.py b/myst_parser/config/main.py
index adac9b1..b4cd423 100644
--- a/myst_parser/config/main.py
+++ b/myst_parser/config/main.py
@@ -1,5 +1,6 @@
 """The configuration for the myst parser."""
 import dataclasses as dc
+from importlib import import_module
 from typing import (
     Any,
     Callable,
@@ -21,7 +22,6 @@ from .dc_validators import (
     deep_mapping,
     in_,
     instance_of,
-    is_callable,
     optional,
     validate_field,
     validate_fields,
@@ -139,6 +139,33 @@ def check_inventories(_: "MdParserConfig", field: dc.Field, value: Any) -> None:
             raise TypeError(f"'{field.name}[{key}][1]' is not a null/string: {val[1]}")
 
 
+def check_heading_slug_func(
+    inst: "MdParserConfig", field: dc.Field, value: Any
+) -> None:
+    """Check that the heading_slug_func is a callable."""
+    if value is None:
+        return
+    if isinstance(value, str):
+        # attempt to load the function as a python import
+        try:
+            module_path, function_name = value.rsplit(".", 1)
+            mod = import_module(module_path)
+            value = getattr(mod, function_name)
+        except ImportError as exc:
+            raise TypeError(
+                f"'{field.name}' could not be loaded from string: {value!r}"
+            ) from exc
+        setattr(inst, field.name, value)
+    if not callable(value):
+        raise TypeError(f"'{field.name}' is not callable: {value!r}")
+
+
+def _test_slug_func(text: str) -> str:
+    """Dummy slug function, this is imported during testing."""
+    # reverse the text
+    return text[::-1]
+
+
 @dc.dataclass()
 class MdParserConfig:
     """Configuration options for the Markdown Parser.
@@ -250,10 +277,13 @@ class MdParserConfig:
     heading_slug_func: Optional[Callable[[str], str]] = dc.field(
         default=None,
         metadata={
-            "validator": optional(is_callable),
-            "help": "Function for creating heading anchors",
+            "validator": check_heading_slug_func,
+            "help": (
+                "Function for creating heading anchors, "
+                "or a python import path e.g. `my_package.my_module.my_function`"
+            ),
             "global_only": True,
-            "omit": ["docutils"],  # TODO docutils config doesn't handle callables
+            "doc_type": "None | Callable[[str], str] | str",
         },
     )
 
diff --git a/myst_parser/parsers/docutils_.py b/myst_parser/parsers/docutils_.py
index aeca189..1651f63 100644
--- a/myst_parser/parsers/docutils_.py
+++ b/myst_parser/parsers/docutils_.py
@@ -136,7 +136,7 @@ def _attr_to_optparse_option(at: Field, default: Any) -> Tuple[dict, str]:
             "metavar": "<boolean>",
             "validator": frontend.validate_boolean,
         }, str(default)
-    if at.type is str:
+    if at.type is str or at.name == "heading_slug_func":
         return {
             "metavar": "<str>",
         }, f"(default: '{default}')"


Test patch:
diff --git a/tests/test_renderers/fixtures/myst-config.txt b/tests/test_renderers/fixtures/myst-config.txt
index e772874..ea5c1ae 100644
--- a/tests/test_renderers/fixtures/myst-config.txt
+++ b/tests/test_renderers/fixtures/myst-config.txt
@@ -413,3 +413,17 @@ a
 <string>:1: (WARNING/2) No matches for '*:*:*:other' [myst.iref_missing]
 <string>:3: (WARNING/2) Multiple matches for '*:*:*:*index': key:std:label:genindex, key:std:label:modindex, key:std:label:py-modindex, ... [myst.iref_ambiguous]
 .
+
+[heading_slug_func] --myst-heading-anchors=1 --myst-heading-slug-func=myst_parser.config.main._test_slug_func
+.
+# title
+
+[reversed](#eltit)
+.
+<document ids="title" names="title" slug="eltit" source="<string>" title="title">
+    <title>
+        title
+    <paragraph>
+        <reference id_link="True" refid="title">
+            reversed
+.