Giter Site home page Giter Site logo

copier-org / copier-templates-extensions Goto Github PK

View Code? Open in Web Editor NEW
14.0 2.0 2.0 1.11 MB

Special Jinja2 extension for Copier that allows to load extensions using file paths relative to the template root instead of Python dotted paths.

License: ISC License

Makefile 3.71% Python 87.73% Shell 7.95% Dockerfile 0.61%
copier copier-extension jinja2-extension

copier-templates-extensions's People

Contributors

pawamoy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

dsz182 bswck

copier-templates-extensions's Issues

Add metadata for current file in ContextHook

Is your feature request related to a problem? Please describe.

Please see the following discussion I started in Copier for more context: copier-org/copier#695

Describe the solution you'd like

It would be great if we could get the current file being "worked on" before the hook method is called so that we can add some conditional logic based on the file's name.

Describe alternatives you've considered

See this comment: copier-org/copier#695 (reply in thread)

Improve the inplace/update API

Current solution

The current API for handling in-place and "update" hooks is as follows:

  • For "update" context hooks that don't mutate contexts, but produce dicts that can be incorporated into those contexts, we define a class (with update = True being unnecessary, as it's a default of ContextHook):
    class MyHook(ContextHook):
        def hook(self, context: dict[str, Any]) -> dict[str, Any]:
            return {"this will": "be incorpored into *context*"}
    Provided an instance of MyHook named my_hook, my_hook.hook(context) (where context is of the dict[str, Any] type) is expected to produce a dictionary that will be merged into context through context.update:
    if extension_self.update: # type: ignore[attr-defined]
    parent.update(extension_self.hook(parent)) # type: ignore[attr-defined]
  • For in-place context hooks that do mutate the context they receive, we define a class with update = False attribute, for example:
    class MyInplaceHook(ContextHook):
        update = False
    
        def hook(self, context: dict[str, Any]) -> dict[str, Any]:
            self.context.update({"this is": "incorporated into *context* immediately"})
    But oh, wait. This produces a type-checking error, MyInplaceHook.hook() doesn't return anything, but it's expected to return an object of type dict[str, Any].
    But why would it be, if it operates in-place, and provided an instance of MyInplaceHook named my_inplace_hook, my_inplace_hook.hook(context) return value is ignored โ†“ and likely not expected to exist at all?
    else:
    extension_self.hook(parent) # type: ignore[attr-defined]

    Ok, let's redefine our hook:
        def hook(self, context: dict[str, Any]) -> None:
            self.context.update({"this is": "incorporated into *context* immediately"})
    Oh, there we go. This time we get an error that the MyInplaceHook.hook() signature is inconsistent with the parent signature ContextHook.hook() that is typed to always return dict[str, Any].

There are a few problems with this approach:

  • The aforementioned typing toil that will usually end up in using # type: ignore[override] or, even worse, # type: ignore (we really don't want it!).
  • Context hooks become in-place implicitly if we subclass them, i.e. they inherit the update attribute set to False, potentially without the user realizing that the hook they subclass is in-place because (1) the hook return types are not affected and (2) update is True by default in the root class ContextHook.
  • If someone is overall into inplace hooks, they have to set update to False in every subclass or they end up using a boilerplate InplaceContextHook class like
    class InplaceContextHook(ContextHook):
        update = False
    
        @wraps(ContextHook.hook)
        def hook(self, context: dict[str, Any]) -> None:  # type: ignore[override]
            raise NotImplementedError
    (Good luck with reusing it among other copier extension modules without altering sys.path with the current import resolution).

Desired solution

My first idea was to simplify the mechanism by bridging methods like apply()->hook().
Instead of

if extension_self.update: # type: ignore[attr-defined]
parent.update(extension_self.hook(parent)) # type: ignore[attr-defined]
else:
extension_self.hook(parent) # type: ignore[attr-defined]

we would have

extension_self.apply(parent)

with the following ContextHook methods:

def apply(self, context: dict[str, Any]) -> None:
    context.update(self.hook(context))

def hook(self, context: dict[str, Any]) -> dict[str, Any]:
    raise NotImplementedError

It:

  • gives more freedom,
  • resolves all the typing toil, doesn't change the API drastically (a migration from ContextHook.update is however required),
  • decouples every context hook into generation and application stages (uhhh, this sounds way too smart),
  • enables reusability of the mutation behavior, e.g. one might want to reuse the logic of the apply() method in a custom implementation and may simply override hook() of a relevant context hook class to achieve that.

If we need a deprecation strategy for this change, we can use this:

def apply(self, context: dict[str, Any]) -> None:
    missing = object()
    do_update = getattr(self, "update", missing)
    if do_update is not missing and not callable(do_update):
        warning = DeprecationWarning(
            "`ContextHook.update` attribute is deprecated "
            "and scheduled for removal in "
            "copier-templates-extensions 0.5.0, "
            + (
                "consider removing that attribute"
                if do_update else
                "override `ContextHook.apply()` instead"
            )
        )
        warnings.warn(warning, stacklevel=0)
    # TODO(#4): drop backwards compatibility
    context.update(self.hook(context) or context)

def hook(self, context: dict[str, Any]) -> dict[str, Any]:
    raise NotImplementedError

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.