Package: copier-templates-extensions
Copier Templates ExtensionsSpecial Jinja2 extension for Copier that allows to load extensions using file paths relative to the template root instead of Python dotted paths.InstallationWithpip:pipinstallcopier-templates-extensionsWithpipx:pipinstall--userpipx

pipxinstallcopier
pipxinjectcopiercopier-templates-extensionsUsageIn your template configuration,
first add our loader extension,
then add your templates extensions
using relative file paths,
and the class name after a colon:_jinja_extensions:-copier_templates_extensions.TemplateExtensionLoader-extensions/context.py:ContextUpdater-extensions/slugify.py:SlugifyExtensionWith this example, you are supposed to have anextensionsdirectory at the root of your template containing two modules:context.pyandslugify.py.📁 template_root
├── 📄 abc.txt.jinja
├── 📄 copier.yml
└── 📁 extensions
    ├── 📄 context.py
    └── 📄 slugify.pySeeContext hook extensionto see how theContextUpdaterclass can be written.TheSlugifyExtensionclass could be written like this:importreimportunicodedatafromjinja2.extimportExtension# taken from Django# https://github.com/django/django/blob/main/django/utils/text.pydefslugify(value,allow_unicode=False):"""Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeateddashes to single dashes. Remove characters that aren't alphanumerics,underscores, or hyphens. Convert to lowercase. Also strip leading andtrailing whitespace, dashes, and underscores."""value=str(value)ifallow_unicode:value=unicodedata.normalize('NFKC',value)else:value=unicodedata.normalize('NFKD',value).encode('ascii','ignore').decode('ascii')value=re.sub(r'[^\w\s-]','',value.lower())returnre.sub(r'[-\s]+','-',value).strip('-_')classSlugifyExtension(Extension):def__init__(self,environment):super().__init__(environment)environment.filters["slugify"]=slugifyContext hook extensionThis package also provides a convenient extension class
allowing template writers to update the context used
to render templates, in order to add, modify or remove
items of the context.In one of your relative path extensions modules,
create a class that inherits fromContextHook,
and override itshookmethod:fromcopier_templates_extensionsimportContextHookclassContextUpdater(ContextHook):defhook(self,context):new_context={}new_context["say"]="hello "+context["name"]returnnew_contextUsing the above example, your context will be updated
with thenew_contextreturned by the method.
If you prefer to modify the context in-place instead,
for example toremoveitems from it,
set theupdateclass attribute toFalse:fromcopier_templates_extensionsimportContextHookclassContextUpdater(ContextHook):update=Falsedefhook(self,context):context["say"]="hello "+context["name"]delcontext["name"]In your Jinja templates, you will now have access
to the{{ say }}variable directly.This can be extremely useful in template projects
where you don't want to ask too many questions to the users
and instead infer some values from their answers.Consider the following example:
you ask your users if they want to generate
a CLI app or a web API.
Depending on their answer,
the main Python module should be namedcli.pyorapp.py.Without the context hook,
you would need to write a Jinja macro somewhere,
or update the context directly in Jinja,
and import this file (still using Jinja)in the filename of the module:{# using macros #}{%-macromodule_name()%}{%-ifproject_type=="webapi"%}app{%else%}cli{%endif%}{%-endmacro%}{# or enhancing the context #}{#- Initiate context with a copy of Copier answers -#}{%-setctx=_copier_answers.copy()-%}{#- Populate our new variables -#}{%-set_=ctx.update({"module_name":("app"ifproject_type=="webapi"else"cli")-%}📁 template_root
├── 📄 copier.yml
├── 📄 macros      # the macros file
├── 📄 context     # the context file
├── 📁 extensions
│   └── 📄 slugify.py
└── 📁 {{project_name|slugify}}
    │
    │   # using the macros
    ├── 📄 {% import 'macros' as macros with context %}{{macros.module_name()}}.py.jinja
    │
    │   # or using the enhanced context
    └── 📄 {% from 'context' import ctx with context %}{{ctx.module_name}}.py.jinjaAs you can see, both forms are really ugly to write:themacrosorcontextcan only be placed in the root,
as slashes/are not allowed in filenamesyou must use spaces and single-quotes
(double-quotes are not valid filename characters on Windows)
in your templated filenames, which is not cleanfilenames are very longUsing our context hook instead makes it so easy and clean!fromcopier_templates_extensionsimportContextHookclassContextUpdater(ContextHook):defhook(self,context):return{"module_name":"app"ifcontext["project_type"]=="webapi"else"cli"}📁 template_root
├── 📄 copier.yml
├── 📁 extensions
│   ├── 📄 slugify.py
│   └── 📄 context.py
└── 📁 {{project_name|slugify}}
    └── 📄 {{module_name}}.py.jinjaHow does it work?Beware the ugly hack!
Upon loading the specialloaderextension,
the function responsible for importing
a Python object using its dotted-path (a string)
is patched in thejinja.environmentmodule,
where it's used to load extensions.
The patched version adds support
for loading extensions using relative file paths.
The file system loader of the Jinja environment
and itssearchpathsattribute are used to
find the local clone of the template and determine
the absolute path of the extensions to load.