Repository: sqlfluff/sqlfluff
Pull request: Jinja templater: Allow "load_macros_from_path" to be a comma-separated list of paths
<!--Firstly, thanks for adding this feature! Secondly, please check the key steps against the checklist below to make your contribution easy to merge.-->

<!--Please give the Pull Request a meaningful title (including the dialect this PR is for if it is dialect specific), as this will automatically be added to the release notes, and then the Change Log.-->

### Brief summary of the change made
Fixes #829

<!--If there is an open issue for this, then please include `fixes #XXXX` or `closes #XXXX` replacing `XXXX` with the issue number and it will automatically close the issue when the pull request is merged. Alternatively if not fully closed you can say `makes progress on #XXXX` to create a link on that issue without closing it.-->


### Are there any other side effects of this change that we should be aware of?


### Pull Request checklist
- [x] Please confirm you have completed any of the necessary steps below.

- Included test cases to demonstrate any code changes, which may be one or more of the following:
  - `.yml` rule test cases in `test/fixtures/rules/std_rule_cases`.
  - `.sql`/`.yml` parser test cases in `test/fixtures/dialects` (note YML files can be auto generated with `tox -e generate-fixture-yml`).
  - Full autofix test cases in `test/fixtures/linter/autofix`.
  - Other.
- Added appropriate documentation for the change.
- Created GitHub issues for any relevant followup/future enhancements if appropriate.


Problem statement:
Are there plans to support Jinja `include` and `from` statements?
Currently, when trying to lint an sql file with one of these jinja statements:

```jinja
{% from 'my_macros.sql' import a_macro %}
```
or 
```jinja
{% include 'file.sql' %}
```
it will fail with the message: "no loader for this environment specified".

Unless I'm missing something, this seems to be caused by the use of a SandboxedEnvironment with, as the message says, no loader to load either _my_macros.sql_ or _file.sql_.

Are there any plans to support these? Or maybe I'm doing something wrong?

Thank you.

Hints from issue discussion:
@hdemers Did you find a way around this? This is the full error i get when running the lint command

"Unrecoverable failure in Jinja templating: no loader for
 this environment specified. Have you configured your variables?
https://docs.sqlfluff.com/en/latest/configuration.html"

I've tried configuring the jinja templater in the setup.cfg file as outlined on that documentation page but no luck.



@jcarpenter12 No, I did not. It's been a while since I've looked into that issue. I'm not using sqlfluff because of that.
+1 to this feature and I'm getting this error
+1 same error
So I've taken a look at this and it's not the SandboxedEnvironement but rather that the Jinja environment needs to be provided with a search path on which to look for these files to import.

Consider this POC:
*./test.sql*
```sql
{% include 'test2.sql' %}
```

*./test2.sql*
```sql
SELECT foo
FROM bar;
```

*./test.py*
```python
from pathlib import Path

from jinja2 import FileSystemLoader
from jinja2.sandbox import SandboxedEnvironment

# When attempting to load a template via
# {% from 'my_macros.sql' import a_macro %}
# or
# {% include 'file.sql' %}
# Jinja requires a search path which
# specifies where to look for those additional files.
# 
# This is done via the FileSystemLoader
# and any number of files can be provided
# (we currently use this for loading macros from a path
# https://docs.sqlfluff.com/en/stable/configuration.html#macro-templating)

# If we were to accomodate the above jinja blocks we would need to come up
# with a sensible default for where to look for these files.
# To me, same folder as the file being parsed sounds sensible.

# e.g. lint ./test.sql
input_file_path = Path("./test.sql")

# Create jinja environment and add parent folder to loader search path
env = SandboxedEnvironment(
    loader=FileSystemLoader([input_file_path.parent.absolute()])
)

# Load sql to be linted
with open("./test.sql", "r") as f:
    sql = f.read()

# Create and render template.
template = env.from_string(sql)
print(template.render())
# Above sources ./test2.sql fine and prints:
# SELECT foo
# FROM bar;
```
N.B. we already kind of make use of this feature for our current macro support functionality ([docs](https://docs.sqlfluff.com/en/stable/configuration.html#macro-templating), [code](https://github.com/sqlfluff/sqlfluff/blob/c6585107126500719657ea2ad13de3c3920d7af4/src/sqlfluff/core/templaters/jinja.py#L203-L213)) so we'll need to expand this to accomodate these jinja blocks.
For those requesting this feature, could you clarify that when you specify `{% include 'file.sql' %}` where are you expecting that `file.sql` to be located (relative to the linted file I assume)?

(tagging @barrywhart as you're good with jinja related changes 😄)

There may be additional complexities for us to sort out in templating but a clear idea of how the feature should work is a good start.
Thanks for looking into this @jpy-git  

The use case I was testing when I received this error was the following:

Query1:
`./dags/sql/dml/query1.sql`
```
{% from 'sql/macros/get_region.sql' import get_region %}
{% from 'sql/macros/normalize_country.sql' import normalize_country %}
{% set columns = ["id", ...]

MERGE {{ params.table_name }} as d
USING ...
    SELECT
        ...
        {{ normalize_country(<arg1>) }} AS country,
        {{ get_region(<arg1>, <arg2>, <arg3>) }} AS region
    FROM ...
```    

Project structure along these lines:
```
.sqlfluff
dags/
    dag1.py
    dag2.py
    sql/
        DDL/
            query1.sql
        DML/
            query1.sql
        macros/
            get_regions.sql
            normalize_country.sql
```

The `.sqlfluff` file includes:
```
[sqlfluff:templater:jinja]
load_macros_from_path = dags\sql\macros\

[sqlfluff:templater:jinja:context]
params = {
    "table_name":"dummy_table_name",
    }
```


For testing the rendering our team used the render_template.py script to define search paths. The dags, sql and macro paths are specific to our setup but hoping this can help.

```
# render_template.py
import ast
from os.path import abspath, dirname, join, sep

import jinja2


def get_default_searchpath():
    root_path = abspath(sep)
    dags_path = join(dirname(dirname(dirname(abspath(__file__)))), "dags")
    sql_path = join(dirname(dirname(dirname(abspath(__file__)))), "dags", "sql")
    macros_path = join(sql_path, "macros")
    return [root_path, dags_path, sql_path, macros_path]


def create_default_environment(searchpath=None):
    if not searchpath:
        searchpath = get_default_searchpath()
    print(searchpath)
    jinja_env_options = {
        'loader': jinja2.FileSystemLoader(searchpath),
        'undefined': jinja2.Undefined,
        'extensions': ["jinja2.ext.do"],
        'cache_size': 0
    }
    return jinja2.Environment(**jinja_env_options)


def parse_args(args_str: str):
    if not args_str:
        return []
    return args_str.split(",")


def parse_kwargs(kwargs_str: str):
    if not kwargs_str:
        return {}
    try:
        template_kwargs = ast.literal_eval(args.template_kwargs)
        if type(template_kwargs) is not dict:
            raise Exception
        return template_kwargs
    except:
        raise Exception(f"Expression '{kwargs_str}' is not a valid dictionary")


def main(template_path: str, args_str: str = None, kwargs_str: str = None):
    env = create_default_environment()
    template = env.get_template(template_path)
    args = parse_args(args_str)
    kwargs = parse_kwargs(kwargs_str)
    output = template.render(*args, **kwargs)

    print(output)


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('template_path', type=str,
                        help='A path with the template file')
    parser.add_argument('--template_args', type=str, required=False,
                        help='positions arguments separated by a comma. '
                             'E.g: foo,bar,baz')
    parser.add_argument('--template_kwargs', type=str, required=False,
                        help='key value in a dict form. '
                             'E.g: \'{"params": {"table_name": "test"}}\'')
    parser.add_argument('--searchpath', type=str, required=False,
                        help='Add additional search paths. '
                             'E.g. /path/to/myFolder1,/path/to/myFolder2')

    args = parser.parse_args()

    main(args.template_path, args.template_args, args.template_kwargs)
```

I did some exploratory work on this. The Jinja templater already configures Jinja with a macro/template loader if `load_macros_from_path` is configured:
```
[sqlfluff:templater:jinja]
load_macros_from_path=macros
```

However, there's a bug -- we create the Jinja environment **multiple times**, and sometimes the loader is omitted.

The diffs below address that. However, this surfaces another issue: after the template is rendered, I'm seeing a runtime error `Position not found` in one of the templater functions `_find_slice_indices_of_templated_pos()`. I think we'll need to update `JinjaTracer` to handle `import` and `include`. I think this will be similar to the current handling for `{% set %}` directives.

```
diff --git a/src/sqlfluff/core/templaters/jinja.py b/src/sqlfluff/core/templaters/jinja.py
index 722b18cc..7110a416 100644
--- a/src/sqlfluff/core/templaters/jinja.py
+++ b/src/sqlfluff/core/templaters/jinja.py
@@ -200,10 +200,10 @@ class JinjaTemplater(PythonTemplater):
                 line_pos=pos,
             )
 
-    @staticmethod
-    def _get_jinja_env(macros_path: Optional[str] = None):
+    def _get_jinja_env(self, config=None):
         """Get a properly configured jinja environment."""
         # We explicitly want to preserve newlines.
+        macros_path = self._get_macros_path(config)
         return SandboxedEnvironment(
             keep_trailing_newline=True,
             # The do extension allows the "do" directive
@@ -212,7 +212,15 @@ class JinjaTemplater(PythonTemplater):
             loader=FileSystemLoader(macros_path) if macros_path else None,
         )
 
-    def get_context(self, fname=None, config=None) -> Dict:
+    def _get_macros_path(self, config):
+        macros_path = None
+        if config:
+            macros_path = config.get_section(
+                (self.templater_selector, self.name, "load_macros_from_path")
+            )
+        return macros_path
+
+    def get_context(self, env, fname=None, config=None) -> Dict:
         """Get the templating context from the config."""
         # Load the context
         live_context = super().get_context(fname=fname, config=config)
@@ -231,15 +239,10 @@ class JinjaTemplater(PythonTemplater):
                     if name not in live_context:
                         live_context[name] = dbt_builtins[name]
 
-        env = self._get_jinja_env()
-
         # Load macros from path (if applicable)
         if config:
-            macros_path = config.get_section(
-                (self.templater_selector, self.name, "load_macros_from_path")
-            )
+            macros_path = self._get_macros_path(config)
             if macros_path:
-                env = self._get_jinja_env(macros_path)
                 live_context.update(
                     self._extract_macros_from_path(
                         macros_path, env=env, ctx=live_context
@@ -261,8 +264,8 @@ class JinjaTemplater(PythonTemplater):
     ) -> Tuple[Environment, dict, Callable[[str], Template]]:
         """Builds and returns objects needed to create and run templates."""
         # Load the context
-        live_context = self.get_context(fname=fname, config=config)
-        env = self._get_jinja_env()
+        env = self._get_jinja_env(config)
+        live_context = self.get_context(env, fname=fname, config=config)
 
         def make_template(in_str):
             """Used by JinjaTracer to instantiate templates.
@@ -378,9 +381,8 @@ class JinjaTemplater(PythonTemplater):
             )
             return None, violations
 
-    @classmethod
     def slice_file(
-        cls, raw_str: str, templated_str: str, config=None, **kwargs
+        self, raw_str: str, templated_str: str, config=None, **kwargs
     ) -> Tuple[List[RawFileSlice], List[TemplatedFileSlice], str]:
         """Slice the file to determine regions where we can fix."""
         # The JinjaTracer slicing algorithm is more robust, but it requires
@@ -396,6 +398,6 @@ class JinjaTemplater(PythonTemplater):
         templater_logger.info("Slicing File Template")
         templater_logger.debug("    Raw String: %r", raw_str)
         templater_logger.debug("    Templated String: %r", templated_str)
-        tracer = JinjaTracer(raw_str, cls._get_jinja_env(), make_template)
+        tracer = JinjaTracer(raw_str, self._get_jinja_env(), make_template)
         trace = tracer.trace()
         return trace.raw_sliced, trace.sliced_file, trace.templated_str
```

@barrywhart awesome thanks 👍 Will assign to you to pick up when you get the chance 😄

Commit message: Merge branch 'main' into bhart-issue_829_multiple_directories_in_macro_path

Patch to review:
diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst
index d2faed4a143..329956c0533 100644
--- a/docs/source/configuration.rst
+++ b/docs/source/configuration.rst
@@ -402,34 +402,44 @@ this introduces the idea of config *blocks* which could be selectively
 overwritten by other configuration files downstream as required.
 
 In addition to macros specified in the config file, macros can also be
-loaded from a file or folder. The path to this macros folder must be
-specified in the config file to function as below:
+loaded from files or folders. This is specified in the config file:
 
 .. code-block:: cfg
 
     [sqlfluff:templater:jinja]
     load_macros_from_path = my_macros
 
-In this case, SQLFluff will load macros from any :code:`.sql` file found at the
-path specified on this variable. The path is interpreted *relative to the
-config file*, and therefore if the config file above was found at
-:code:`/home/my_project/.sqlfluff` then SQLFluff will look for macros in the
-folder :code:`/home/my_project/my_macros/`. Alternatively the path can also
-be a :code:`.sql` itself. Any macros defined in the config will always take
-precedence over a macro defined in the path.
+`load_macros_from_path` is a comma-separated list of files or folders. SQLFluff
+will load macros from any :code:`.sql` file found in the specified locations.
+Locations are *relative to the config file*. For example, if the config file
+above was found at :code:`/home/my_project/.sqlfluff` then SQLFluff will look
+for macros in the folder :code:`/home/my_project/my_macros/` (but not
+subfolders). Alternatively, the path can also be a :code:`.sql` itself. Any
+macros defined in the config will always take precedence over a macro defined
+in the path.
+
+**Note:** The `load_macros_from_path` also defines the search path for Jinja
+[include](https://jinja.palletsprojects.com/en/3.0.x/templates/#include) or
+[import](https://jinja.palletsprojects.com/en/3.0.x/templates/#import).
+Unlike with macros (as noted above), subdirectories are supported. For example,
+if `load_macros_from_path` is set to `my_macros`, and there is a file
+`my_macros/subdir/my_file.sql`, you can do:
 
+.. code-block:: jinja
+
+   {% include 'subdir/include_comment.sql' %}
 
 .. note::
 
     Throughout the templating process **whitespace** will still be treated
     rigorously, and this includes **newlines**. In particular you may choose
-    to provide your *dummy* macros in your configuration with different to
-    the actual macros you may be using in production.
+    to provide *dummy* macros in your configuration different from the actual
+    macros used in production.
 
-    **REMEMBER:** The purpose of providing the option of macros is to *enable*
-    the parsing of templated sql without it being a blocker. It shouldn't
-    be a requirement that the *templating* is accurate - only so far as that
-    is required to enable the *parsing* and *linting* to be helpful.
+    **REMEMBER:** The reason SQLFluff supports macros is to *enable* it to parse
+    templated sql without it being a blocker. It shouldn't be a requirement that
+    the *templating* is accurate - it only needs to work well enough that
+    *parsing* and *linting* are helpful.
 
 Builtin Macro Blocks
 ^^^^^^^^^^^^^^^^^^^^
@@ -440,8 +450,8 @@ repositories of sql files which could potentially benefit from some linting.
 
 .. note::
     *SQLFluff* has now a tighter integration with dbt through the "dbt" templater.
-    It is the recommended templater for dbt projects and removes the need for the
-    overwrites described in this section.
+    It is the recommended templater for dbt projects. If used, it eliminates the
+    need for the overrides described in this section.
 
     To use the dbt templater, go to `dbt Project Configuration`_.
 
@@ -482,7 +492,7 @@ config option:
     library_path = sqlfluff_libs
 
 This will pull in any python modules from that directory and allow sqlfluff
-to use them for templated. In the above example, you might define a file at
+to use them in templates. In the above example, you might define a file at
 `sqlfluff_libs/dbt_utils.py` as:
 
 .. code-block:: python
@@ -527,9 +537,9 @@ dbt Project Configuration
 
 .. note::
     From sqlfluff version 0.7.0 onwards, the dbt templater has been moved
-    to a separate plugin and python package. You may find that projects
-    already using the templater may initially fail after an upgrade to
-    0.7.0+. See install instructions below to install the dbt templater.
+    to a separate plugin and python package. Projects that were already using
+    the dbt templater may initially fail after an upgrade to 0.7.0+. See the
+    installation instructions below to install the dbt templater.
 
     dbt templating is still a relatively new feature added in 0.4.0 and
     is still in very active development! If you encounter an issue, please
diff --git a/src/sqlfluff/core/config.py b/src/sqlfluff/core/config.py
index 2746cab55ef..283079ae290 100644
--- a/src/sqlfluff/core/config.py
+++ b/src/sqlfluff/core/config.py
@@ -127,6 +127,10 @@ def dict_diff(left: dict, right: dict, ignore: Optional[List[str]] = None) -> di
     return buff
 
 
+def _split_comma_separated_string(raw_str: str) -> List[str]:
+    return [s.strip() for s in raw_str.split(",") if s.strip()]
+
+
 class ConfigLoader:
     """The class for loading config files.
 
@@ -174,8 +178,8 @@ def _get_config_elems_from_toml(cls, fpath: str) -> List[Tuple[tuple, Any]]:
 
         return cls._walk_toml(tool)
 
-    @staticmethod
-    def _get_config_elems_from_file(fpath: str) -> List[Tuple[tuple, Any]]:
+    @classmethod
+    def _get_config_elems_from_file(cls, fpath: str) -> List[Tuple[tuple, Any]]:
         """Load a config from a file and return a list of tuples.
 
         The return value is a list of tuples, were each tuple has two elements,
@@ -218,17 +222,28 @@ def _get_config_elems_from_file(fpath: str) -> List[Tuple[tuple, Any]]:
                 v = coerce_value(val)
 
                 # Attempt to resolve paths
-                if name.lower().endswith(("_path", "_dir")):
-                    # Try to resolve the path.
-                    # Make the referenced path.
-                    ref_path = os.path.join(os.path.dirname(fpath), val)
-                    # Check if it exists, and if it does, replace the value with the path.
-                    if os.path.exists(ref_path):
-                        v = ref_path
+                if name.lower() == "load_macros_from_path":
+                    # Comma-separated list of paths.
+                    paths = _split_comma_separated_string(val)
+                    v_temp = []
+                    for path in paths:
+                        v_temp.append(cls._resolve_path(fpath, path))
+                    v = ",".join(v_temp)
+                elif name.lower().endswith(("_path", "_dir")):
+                    # One path
+                    v = cls._resolve_path(fpath, val)
                 # Add the name to the end of the key
                 buff.append((key + (name,), v))
         return buff
 
+    @classmethod
+    def _resolve_path(cls, fpath, val):
+        """Try to resolve a path."""
+        # Make the referenced path.
+        ref_path = os.path.join(os.path.dirname(fpath), val)
+        # Check if it exists, and if it does, replace the value with the path.
+        return ref_path if os.path.exists(ref_path) else val
+
     @staticmethod
     def _incorporate_vals(ctx: dict, vals: List[Tuple[Tuple[str, ...], Any]]) -> dict:
         """Take a list of tuples and incorporate it into a dictionary."""
@@ -463,20 +478,20 @@ def __init__(
         )
         # Deal with potential ignore parameters
         if self._configs["core"].get("ignore", None):
-            self._configs["core"]["ignore"] = self._split_comma_separated_string(
+            self._configs["core"]["ignore"] = _split_comma_separated_string(
                 self._configs["core"]["ignore"]
             )
         else:
             self._configs["core"]["ignore"] = []
         # Allowlists and denylists
         if self._configs["core"].get("rules", None):
-            self._configs["core"][
-                "rule_allowlist"
-            ] = self._split_comma_separated_string(self._configs["core"]["rules"])
+            self._configs["core"]["rule_allowlist"] = _split_comma_separated_string(
+                self._configs["core"]["rules"]
+            )
         else:
             self._configs["core"]["rule_allowlist"] = None
         if self._configs["core"].get("exclude_rules", None):
-            self._configs["core"]["rule_denylist"] = self._split_comma_separated_string(
+            self._configs["core"]["rule_denylist"] = _split_comma_separated_string(
                 self._configs["core"]["exclude_rules"]
             )
         else:
@@ -758,10 +773,6 @@ def process_raw_file_for_config(self, raw_str: str):
                 # Found a in-file config command
                 self.process_inline_config(raw_line)
 
-    @staticmethod
-    def _split_comma_separated_string(raw_str: str) -> List[str]:
-        return [s.strip() for s in raw_str.split(",") if s.strip()]
-
 
 class ProgressBarConfiguration:
     """Singleton-esque progress bar configuration.
diff --git a/src/sqlfluff/core/templaters/jinja.py b/src/sqlfluff/core/templaters/jinja.py
index 38f72742b94..5499b7da6ab 100644
--- a/src/sqlfluff/core/templaters/jinja.py
+++ b/src/sqlfluff/core/templaters/jinja.py
@@ -16,6 +16,7 @@
 from jinja2.environment import Template
 from jinja2.sandbox import SandboxedEnvironment
 
+from sqlfluff.core.config import FluffConfig
 from sqlfluff.core.errors import SQLTemplaterError
 from sqlfluff.core.templaters.base import (
     RawFileSlice,
@@ -64,31 +65,32 @@ def _extract_macros_from_template(template, env, ctx):
         return context
 
     @classmethod
-    def _extract_macros_from_path(cls, path, env, ctx):
+    def _extract_macros_from_path(cls, path: List[str], env: Environment, ctx: Dict):
         """Take a path and extract macros from it."""
-        # Does the path exist? It should as this check was done on config load.
-        if not os.path.exists(path):  # pragma: no cover
-            raise ValueError(f"Path does not exist: {path}")
-
-        macro_ctx = {}
-        if os.path.isfile(path):
-            # It's a file. Extract macros from it.
-            with open(path) as opened_file:
-                template = opened_file.read()
-            # Update the context with macros from the file.
-            macro_ctx.update(
-                cls._extract_macros_from_template(template, env=env, ctx=ctx)
-            )
-        else:
-            # It's a directory. Iterate through files in it and extract from them.
-            for dirpath, _, files in os.walk(path):
-                for fname in files:
-                    if fname.endswith(".sql"):
-                        macro_ctx.update(
-                            cls._extract_macros_from_path(
-                                os.path.join(dirpath, fname), env=env, ctx=ctx
+        for path_entry in path:
+            # Does it exist? It should as this check was done on config load.
+            if not os.path.exists(path_entry):  # pragma: no cover
+                raise ValueError(f"Path does not exist: {path}")
+
+            macro_ctx = {}
+            if os.path.isfile(path_entry):
+                # It's a file. Extract macros from it.
+                with open(path_entry) as opened_file:
+                    template = opened_file.read()
+                # Update the context with macros from the file.
+                macro_ctx.update(
+                    cls._extract_macros_from_template(template, env=env, ctx=ctx)
+                )
+            else:
+                # It's a directory. Iterate through files in it and extract from them.
+                for dirpath, _, files in os.walk(path_entry):
+                    for fname in files:
+                        if fname.endswith(".sql"):
+                            macro_ctx.update(
+                                cls._extract_macros_from_path(
+                                    [os.path.join(dirpath, fname)], env=env, ctx=ctx
+                                )
                             )
-                        )
         return macro_ctx
 
     def _extract_macros_from_config(self, config, env, ctx):
@@ -212,13 +214,16 @@ def _get_jinja_env(self, config=None):
             loader=FileSystemLoader(macros_path) if macros_path else None,
         )
 
-    def _get_macros_path(self, config):
-        macros_path = None
+    def _get_macros_path(self, config: FluffConfig) -> Optional[List[str]]:
         if config:
             macros_path = config.get_section(
                 (self.templater_selector, self.name, "load_macros_from_path")
             )
-        return macros_path
+            if macros_path:
+                result = [s.strip() for s in macros_path.split(",") if s.strip()]  # type: ignore
+                if result:
+                    return result
+        return None
 
     def get_context(self, fname=None, config=None, **kw) -> Dict:
         """Get the templating context from the config."""
diff --git a/test/core/config_test.py b/test/core/config_test.py
index 73474912749..4004cc05223 100644
--- a/test/core/config_test.py
+++ b/test/core/config_test.py
@@ -3,6 +3,7 @@
 import os
 import sys
 
+from sqlfluff.core import config
 from sqlfluff.core.config import ConfigLoader, nested_combine, dict_diff
 from sqlfluff.core import Linter, FluffConfig
 from sqlfluff.core.templaters import (
@@ -207,7 +208,7 @@ def path_exists(x):
 )
 def test__config__split_comma_separated_string(raw_str, expected):
     """Tests that comma separated string config is handled correctly."""
-    assert FluffConfig._split_comma_separated_string(raw_str) == expected
+    assert config._split_comma_separated_string(raw_str) == expected
 
 
 def test__config__templater_selection():
diff --git a/test/core/templaters/jinja_test.py b/test/core/templaters/jinja_test.py
index d3a88761397..c132cfde298 100644
--- a/test/core/templaters/jinja_test.py
+++ b/test/core/templaters/jinja_test.py
@@ -694,15 +694,22 @@ def test__templater_jinja_slice_template(test, result):
         (
             # Tests Jinja "from import" directive..
             """{% from 'echo.sql' import echo %}
+{% from 'echoecho.sql' import echoecho %}
 
-SELECT {{ echo("foo") }}
+SELECT
+    {{ echo("foo") }},
+    {{ echoecho("bar") }}
 """,
             None,
             [
                 ("block_start", slice(0, 33, None), slice(0, 0, None)),
-                ("literal", slice(33, 42, None), slice(0, 9, None)),
-                ("templated", slice(42, 59, None), slice(9, 14, None)),
-                ("literal", slice(59, 60, None), slice(14, 15, None)),
+                ("literal", slice(33, 34, None), slice(0, 1, None)),
+                ("block_start", slice(34, 75, None), slice(1, 1, None)),
+                ("literal", slice(75, 88, None), slice(1, 14, None)),
+                ("templated", slice(88, 105, None), slice(14, 19, None)),
+                ("literal", slice(105, 111, None), slice(19, 25, None)),
+                ("templated", slice(111, 132, None), slice(25, 34, None)),
+                ("literal", slice(132, 133, None), slice(34, 35, None)),
             ],
         ),
     ],
@@ -721,7 +728,7 @@ def test__templater_jinja_slice_file(raw_file, override_context, result, caplog)
         _, resp, _ = templater.slice_file(
             raw_file, templated_file, make_template=make_template
         )
-    # Check contigious on the TEMPLATED VERSION
+    # Check contiguous on the TEMPLATED VERSION
     print(resp)
     prev_slice = None
     for elem in resp:
diff --git a/test/fixtures/templater/jinja_slice_template_macros/.sqlfluff b/test/fixtures/templater/jinja_slice_template_macros/.sqlfluff
index 0c80c5b567f..a13209394e5 100644
--- a/test/fixtures/templater/jinja_slice_template_macros/.sqlfluff
+++ b/test/fixtures/templater/jinja_slice_template_macros/.sqlfluff
@@ -1,2 +1,2 @@
 [sqlfluff:templater:jinja]
-load_macros_from_path=macros
+load_macros_from_path=macros,more_macros
diff --git a/test/fixtures/templater/jinja_slice_template_macros/more_macros/echoecho.sql b/test/fixtures/templater/jinja_slice_template_macros/more_macros/echoecho.sql
new file mode 100644
index 00000000000..f9828ade41a
--- /dev/null
+++ b/test/fixtures/templater/jinja_slice_template_macros/more_macros/echoecho.sql
@@ -0,0 +1,3 @@
+{% macro echoecho(text) %}
+{{text}} {{text}}
+{% endmacro %}


Code review comments:
diff_hunk: @@ -212,13 +214,16 @@ def _get_jinja_env(self, config=None):
             loader=FileSystemLoader(macros_path) if macros_path else None,
         )
 
-    def _get_macros_path(self, config):
-        macros_path = None
+    def _get_macros_path(self, config: FluffConfig) -> Optional[List[str]]:
         if config:
             macros_path = config.get_section(
                 (self.templater_selector, self.name, "load_macros_from_path")
             )
-        return macros_path
+            if macros_path:
+                result = [s.strip() for s in macros_path.split(",") if s.strip()]  # type: ignore
path: src/sqlfluff/core/templaters/jinja.py
text: @user1:
This mypy error is due to some dodgy typing in `FluffConfig.get_section`:
![image](https://user-images.githubusercontent.com/80432516/150508412-7a28147f-cf7a-4a16-af18-95248cbb4408.png)

That return type should really just be `Any` so could you update that and remove the type ignore here

@author:
Done!