Repository: cookiecutter/cookiecutter

Issue:
Page not found on docs

### Description:
 On this page in your docs:
https://cookiecutter.readthedocs.io/en/1.7.0/advanced/choice_variables.html

This link is not working:

![image](https://user-images.githubusercontent.com/56229312/154769019-552c0087-f3b6-443b-892b-b9021b03a2f3.png)

http://jinja.pocoo.org/docs/dev/templates/#if


Discussion:
codeclimate[bot]: Code Climate has analyzed commit 907c8344 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**0%** is the threshold).

This pull request will bring the total coverage in the repository to **100.0%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/cookiecutter/cookiecutter/pull/1649).


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c17a916..f54ae0e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -74,7 +74,7 @@ git clone git@github.com:your_name_here/cookiecutter.git
 ```bash
 
 cd cookiecutter/
-python setup.py develop
+pip install -e .
 ```
 
 4. Create a branch for local development:
diff --git a/README.md b/README.md
index 9b1dfe1..a954fec 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 [![python](https://img.shields.io/pypi/pyversions/cookiecutter.svg)](https://pypi.org/project/cookiecutter/)
 [![Build Status](https://github.com/cookiecutter/cookiecutter/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/cookiecutter/cookiecutter/actions)
 [![codecov](https://codecov.io/gh/cookiecutter/cookiecutter/branch/master/graphs/badge.svg?branch=master)](https://codecov.io/github/cookiecutter/cookiecutter?branch=master)
-[![slack](https://img.shields.io/badge/cookiecutter-Join%20on%20Slack-green?style=flat&logo=slack)](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U)
+[![discord](https://img.shields.io/badge/Discord-cookiecutter-5865F2?style=flat&logo=discord&logoColor=white)](https://discord.gg/9BrxzPKuEW)
 [![docs](https://readthedocs.org/projects/cookiecutter/badge/?version=latest)](https://readthedocs.org/projects/cookiecutter/?badge=latest)
 [![Code Quality](https://img.shields.io/scrutinizer/g/cookiecutter/cookiecutter.svg)](https://scrutinizer-ci.com/g/cookiecutter/cookiecutter/?branch=master)
 
@@ -19,9 +19,6 @@ template.
 
 ![Cookiecutter](https://raw.githubusercontent.com/cookiecutter/cookiecutter/3ac078356adf5a1a72042dfe72ebfa4a9cd5ef38/logo/cookiecutter_medium.png)
 
-We are proud to be an open source sponsor of
-[PyCon 2016](https://us.pycon.org/2016/sponsors/).
-
 ## Features
 
 - Cross-platform: Windows, Mac, and Linux are officially supported.
@@ -178,7 +175,7 @@ Stuck? Try one of the following:
   about the problem, even if it's just "I can't get it to work on this cookiecutter"
   with a link to your cookiecutter. Don't worry about naming/pinpointing the issue
   properly.
-- Ask for help on [Slack](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U)
+- Ask for help on [Discord](https://discord.gg/9BrxzPKuEW)
   if you must (but please try one of the other options first, so that others
   can benefit from the discussion).
 
@@ -189,8 +186,8 @@ Development on Cookiecutter is community-driven:
 - Everyone is invited to contribute. Read the
   [contributing instructions](CONTRIBUTING.md), then get started.
 - Connect with other Cookiecutter contributors and users on
-  [Slack](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U)
-  (note: due to work and commitments, a core committer might not always be available)
+  [Discord](https://discord.gg/9BrxzPKuEW)
+  (note: due to work and other commitments, a core committer might not always be available)
 
 Encouragement is unbelievably motivating. If you want more work done on
 Cookiecutter, show support:
@@ -233,3 +230,7 @@ organizations and individuals to support the project.
 Everyone interacting in the Cookiecutter project's codebases, issue trackers,
 chat rooms, and mailing lists is expected to follow the
 [PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/).
+
+## Creator / Leader
+
+This project was created and is led by [Audrey Roy Greenfeld](https://github.com/audreyfeldroy).
diff --git a/cookiecutter/main.py b/cookiecutter/main.py
index 7c9eef7..bc2f262 100644
--- a/cookiecutter/main.py
+++ b/cookiecutter/main.py
@@ -4,8 +4,10 @@ Main entry point for the `cookiecutter` command.
 The code in this module is also a good example of how to use Cookiecutter as a
 library rather than a script.
 """
+from copy import copy
 import logging
 import os
+import sys
 
 from cookiecutter.config import get_user_config
 from cookiecutter.exceptions import InvalidModeException
@@ -73,15 +75,17 @@ def cookiecutter(
         password=password,
         directory=directory,
     )
+    import_patch = _patch_import_path_for_repo(repo_dir)
 
     template_name = os.path.basename(os.path.abspath(repo_dir))
 
     if replay:
-        if isinstance(replay, bool):
-            context = load(config_dict['replay_dir'], template_name)
-        else:
-            path, template_name = os.path.split(os.path.splitext(replay)[0])
-            context = load(path, template_name)
+        with import_patch:
+            if isinstance(replay, bool):
+                context = load(config_dict['replay_dir'], template_name)
+            else:
+                path, template_name = os.path.split(os.path.splitext(replay)[0])
+                context = load(path, template_name)
     else:
         context_file = os.path.join(repo_dir, 'cookiecutter.json')
         logger.debug('context_file is %s', context_file)
@@ -94,7 +98,8 @@ def cookiecutter(
 
         # prompt the user to manually configure at the command line.
         # except when 'no-input' flag is set
-        context['cookiecutter'] = prompt_for_config(context, no_input)
+        with import_patch:
+            context['cookiecutter'] = prompt_for_config(context, no_input)
 
         # include template dir or url in the context dict
         context['cookiecutter']['_template'] = template
@@ -105,17 +110,31 @@ def cookiecutter(
         dump(config_dict['replay_dir'], template_name, context)
 
     # Create project from local context and project template.
-    result = generate_files(
-        repo_dir=repo_dir,
-        context=context,
-        overwrite_if_exists=overwrite_if_exists,
-        skip_if_file_exists=skip_if_file_exists,
-        output_dir=output_dir,
-        accept_hooks=accept_hooks,
-    )
+    with import_patch:
+        result = generate_files(
+            repo_dir=repo_dir,
+            context=context,
+            overwrite_if_exists=overwrite_if_exists,
+            skip_if_file_exists=skip_if_file_exists,
+            output_dir=output_dir,
+            accept_hooks=accept_hooks,
+        )
 
     # Cleanup (if required)
     if cleanup:
         rmtree(repo_dir)
 
     return result
+
+
+class _patch_import_path_for_repo:
+    def __init__(self, repo_dir):
+        self._repo_dir = repo_dir
+        self._path = None
+
+    def __enter__(self):
+        self._path = copy(sys.path)
+        sys.path.append(self._repo_dir)
+
+    def __exit__(self, type, value, traceback):
+        sys.path = self._path
diff --git a/cookiecutter/utils.py b/cookiecutter/utils.py
index 19b727a..ef53317 100644
--- a/cookiecutter/utils.py
+++ b/cookiecutter/utils.py
@@ -8,6 +8,7 @@ import stat
 import sys
 
 from cookiecutter.prompt import read_user_yes_no
+from jinja2.ext import Extension
 
 logger = logging.getLogger(__name__)
 
@@ -105,3 +106,15 @@ def prompt_and_delete(path, no_input=False):
             return False
 
         sys.exit()
+
+
+def simple_filter(filter_function):
+    """Decorate a function to wrap it in a simplified jinja2 extension."""
+
+    class SimpleFilterExtension(Extension):
+        def __init__(self, environment):
+            super(SimpleFilterExtension, self).__init__(environment)
+            environment.filters[filter_function.__name__] = filter_function
+
+    SimpleFilterExtension.__name__ = filter_function.__name__
+    return SimpleFilterExtension
diff --git a/docs/advanced/choice_variables.rst b/docs/advanced/choice_variables.rst
index c86eb11..ccb35d4 100644
--- a/docs/advanced/choice_variables.rst
+++ b/docs/advanced/choice_variables.rst
@@ -39,7 +39,7 @@ can be used like this::
 
   {% endif %}
 
-Cookiecutter is using `Jinja2's if conditional expression <http://jinja.pocoo.org/docs/dev/templates/#if>`_ to determine the correct license.
+Cookiecutter is using `Jinja2's if conditional expression <https://jinja.palletsprojects.com/en/latest/templates/#if>`_ to determine the correct license.
 
 The created choice variable is still a regular Cookiecutter variable and can be used like this::
 
diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst
index 90e7f29..66d5faa 100644
--- a/docs/advanced/index.rst
+++ b/docs/advanced/index.rst
@@ -23,3 +23,4 @@ Various advanced topics regarding cookiecutter usage.
    template_extensions
    directories
    new_line_characters
+   local_extensions
diff --git a/docs/advanced/local_extensions.rst b/docs/advanced/local_extensions.rst
new file mode 100644
index 0000000..bf87caf
--- /dev/null
+++ b/docs/advanced/local_extensions.rst
@@ -0,0 +1,60 @@
+.. _`template extensions`:
+
+Local Extensions
+----------------
+
+*New in Cookiecutter X.x*
+
+A template may extend the Cookiecutter environment with local extensions.
+These can be part of the template itself, providing it with more sophisticated custom tags and filters.
+
+To do so, a template author must specify the required extensions in ``cookiecutter.json`` as follows:
+
+.. code-block:: json
+
+    {
+        "project_slug": "Foobar",
+        "year": "{% now 'utc', '%Y' %}",
+        "_extensions": ["local_extensions.FoobarExtension"]
+    }
+
+This example assumes that a ``local_extensions`` folder (python module) exists in the template root.
+It will contain a ``main.py`` file, containing the following (for instance):
+
+.. code-block:: python
+
+    # -*- coding: utf-8 -*-
+
+    from jinja2.ext import Extension
+
+
+    class FoobarExtension(Extension):
+        def __init__(self, environment):
+            super(FoobarExtension, self).__init__(environment)
+            environment.filters['foobar'] = lambda v: v * 2
+
+This will register the ``foobar`` filter for the template.
+
+For many cases, this will be unneccessarily complicated. It's likely that we'd only want to register a single function
+as a filter. For this, we can use the ``simple_filter`` decorator:
+
+.. code-block:: json
+
+    {
+        "project_slug": "Foobar",
+        "year": "{% now 'utc', '%Y' %}",
+        "_extensions": ["local_extensions.simplefilterextension"]
+    }
+
+.. code-block:: python
+
+    # -*- coding: utf-8 -*-
+
+    from cookiecutter.utils import simple_filter
+
+
+    @simple_filter
+    def simplefilterextension(v):
+        return v * 2
+
+This snippet will achieve the exact same result as the previous one.
diff --git a/setup.py b/setup.py
index 654010f..d703c3c 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@ with open('README.md', encoding='utf-8') as readme_file:
 requirements = [
     'binaryornot>=0.4.4',
     'Jinja2>=2.7,<4.0.0',
-    'click>=7.0,<8.0.0',
+    'click>=7.0,<9.0.0',
     'pyyaml>=5.3.1',
     'jinja2-time>=0.2.0',
     'python-slugify>=4.0.0',


Test patch:
diff --git a/tests/test-extensions/local_extension/cookiecutter.json b/tests/test-extensions/local_extension/cookiecutter.json
new file mode 100644
index 0000000..8141fd5
--- /dev/null
+++ b/tests/test-extensions/local_extension/cookiecutter.json
@@ -0,0 +1,10 @@
+{
+    "project_slug": "Foobar",
+    "test_value_class_based": "{{cookiecutter.project_slug | foobar}}",
+    "test_value_function_based": "{{cookiecutter.project_slug | simplefilterextension}}",
+    "_extensions": [
+        "local_extensions.simplefilterextension",
+        "local_extensions.FoobarExtension"
+    ]
+}
+
diff --git a/tests/test-extensions/local_extension/local_extensions/__init__.py b/tests/test-extensions/local_extension/local_extensions/__init__.py
new file mode 100644
index 0000000..94e854a
--- /dev/null
+++ b/tests/test-extensions/local_extension/local_extensions/__init__.py
@@ -0,0 +1,1 @@
+from .main import FoobarExtension, simplefilterextension  # noqa
diff --git a/tests/test-extensions/local_extension/local_extensions/main.py b/tests/test-extensions/local_extension/local_extensions/main.py
new file mode 100644
index 0000000..53f6f8f
--- /dev/null
+++ b/tests/test-extensions/local_extension/local_extensions/main.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+
+"""Provides custom extension, exposing a ``foobar`` filter."""
+
+from jinja2.ext import Extension
+from cookiecutter.utils import simple_filter
+
+
+class FoobarExtension(Extension):
+    """Simple jinja2 extension for cookiecutter test purposes."""
+
+    def __init__(self, environment):
+        """Foobar Extension Constructor."""
+        super(FoobarExtension, self).__init__(environment)
+        environment.filters['foobar'] = lambda v: v * 2
+
+
+@simple_filter
+def simplefilterextension(v):
+    """Provide a simple function-based filter extension."""
+    return v.upper()
diff --git a/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst b/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst
new file mode 100644
index 0000000..8bb7c61
--- /dev/null
+++ b/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst
@@ -0,0 +1,8 @@
+History
+-------
+
+0.1.0
+-----
+
+First release of {{cookiecutter.test_value_class_based}} on PyPI.
+{{cookiecutter.test_value_function_based}}
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 19740ef..ad6abd1 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -10,6 +10,8 @@ from click.testing import CliRunner
 
 from cookiecutter import utils
 from cookiecutter.__main__ import main
+from cookiecutter.environment import StrictEnvironment
+from cookiecutter.exceptions import UnknownExtension
 from cookiecutter.main import cookiecutter
 
 
@@ -412,6 +414,35 @@ def test_echo_unknown_extension_error(output_dir, cli_runner):
     assert 'Unable to load extension: ' in result.output
 
 
+def test_local_extension(tmpdir, cli_runner):
+    """Test to verify correct work of extension, included in template."""
+    output_dir = str(tmpdir.mkdir('output'))
+    template_path = 'tests/test-extensions/local_extension/'
+
+    result = cli_runner(
+        '--no-input',
+        '--default-config',
+        '--output-dir',
+        output_dir,
+        template_path,
+    )
+    assert result.exit_code == 0
+    with open(os.path.join(output_dir, 'Foobar', 'HISTORY.rst')) as f:
+        data = f.read()
+        assert 'FoobarFoobar' in data
+        assert 'FOOBAR' in data
+
+
+def test_local_extension_not_available(tmpdir, cli_runner):
+    """Test handling of included but unavailable local extension."""
+    context = {'cookiecutter': {'_extensions': ['foobar']}}
+
+    with pytest.raises(UnknownExtension) as err:
+        StrictEnvironment(context=context, keep_trailing_newline=True)
+
+    assert 'Unable to load extension: ' in str(err.value)
+
+
 @pytest.mark.usefixtures('remove_fake_project_dir')
 def test_cli_extra_context(cli_runner):
     """Cli invocation replace content if called with replacement pairs."""