New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP - Hooks enhancement: running real file in place + context serialization #694
Conversation
And default behaviours are preserved by default ;) |
Thank you @eviweb for submitting this PR! 🙇 We will try to give it a thorough review soon. |
@@ -50,7 +52,7 @@ def find_hooks(): | |||
return r | |||
|
|||
|
|||
def run_script(script_path, cwd='.'): | |||
def run_script(script_path, cwd='.', context={}): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this makes run_script()
accept a context dict, you might as well combine run_script()
and run_script_with_context()
into one function. Then if run_script_with_context()
is too long, feel free to refactor it somehow if you think it will make it more readable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 to refactoring and getting rid of the mutable default arg
I like the idea of this pull request. It would allow us to simplify some of the things we've done with Cookiecutter Django. That simplification means we could add more sophistication to that project. So in general, I'm in favor of this pull request. 😄 That said, here is my critique so far:
|
@@ -84,6 +96,13 @@ def run_script_with_context(script_path, cwd, context): | |||
:param cwd: The directory to run the script from. | |||
:param context: Cookiecutter project template context. | |||
""" | |||
if '_no_hookcopy' in context and re.match( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not check this via context['_no_hookcopy'].lower() == 'yes'
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm 👎 on this functionality- seems like an unnecessary optimisation to me?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, I take ☝️ back, it is useful to have access to the template directory in the hooks.
I think the docs would benefit from an example that shows this off.
I think we should change |
Disabling hook duplication | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
As explained in `Using Pre/Post-Generate Hooks (0.7.0+)`_, you can disable the default behaviour of hook duplication. This is interesting when you don't need to use template variables directly in your hook. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a hook example that demonstrates using __file__
to locate and use the template directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's also important to note that you cannot use template variables in hook scripts if you're running in-place (and you would therefore have to use stdin
context if needed)
hi,
This PR was much more a proof of concept about running hooks in place while keeping the ability of getting context information in hooks. Regarding this particular PR, I will try to fix codecov and appveyor failing tests asap. best regards Eric |
Current coverage is 100%@@ master #694 diff @@
====================================
Files 13 14 +1
Lines 583 779 +196
Methods 0 0
Messages 0 0
Branches 0 0
====================================
+ Hits 583 779 +196
Misses 0 0
Partials 0 0
|
@eviweb: regarding appveyor failures, it looks like the hook script is done executing before you can write to stdin, and this behaviour changed in python >= 3.3.5: http://bugs.python.org/issue19612. |
@michaeljoseph, thanks for your feedback. Best regards Eric |
Here we are ! |
Hey folks, I haven't had a chance to look at this in depth. That being said, what do we need |
@hackebrot, for the log capture utility Eric |
Thanks for the explanation @eviweb! 🙇 I am trying to be extra careful with adding dependencies and pytest has |
@hackebrot, you're perfectly right regarding adding dependencies. Hope all will be fine Eric |
@@ -8,6 +8,7 @@ deps = pytest | |||
pytest-cov | |||
pytest-mock | |||
freezegun | |||
mock |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We get the mock dependency from pytest-mock
already...
@michaeljoseph, try:
import mock
except ImportError:
from unittest import mock I don't know if this is the better solution to get rid of the thanks Eric |
All tests passed with the fix explained above. Eric |
@michaeljoseph, please forget my two previous posts. |
Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
the abstract class permit to uniformize the serializer interface and to prevent some encoding issues under the hood the pickle serializer will be needed to keep the serializer facade configuration during hook calls Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
as they are no more needed Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
regarding the facade: - it does not accept a type argument anymore - instead the facade provides a fluent method 'use' to specify the serializer type to use on the config side the _serializers key provides two settings: - classes: to reference the serializers implementations - use: to specify the serializer to use Signed-off-by: Eric Villard <dev@eviweb.fr>
[ci skip] Signed-off-by: Eric Villard <dev@eviweb.fr>
it provides some helper classes: - Runner: run cookierunner main command for testing - SettingObject: object to configure the runtime environment Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
as it needs a serious refactoring Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
extract all common behaviours to support.AbstractAcceptanceTest class it is the base class to write acceptance tests Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
it failed with a real cookiecutter context the base64 encoding contained linebreaks that were not well handled by popen.communicate Signed-off-by: Eric Villard <dev@eviweb.fr>
get_context: to be used in hooks to retrieve the context object put_context: to be used in hooks to update the context object Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
25e2300
to
4017e44
Compare
Hi everyone, Running real hooks in placeFrom a template author point of view, running real hooks in place is simply done by setting the key {
"full_name": "Your name",
"email": "Your address email (eq. you@example.com)",
"github_username": "Your github username",
"project_name": "Name of the project",
"project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}",
"project_short_description": "A short description of the project",
"release_date": "{% now 'local' %}",
"version": "0.1.0",
"_extensions": ["jinja2_time.TimeExtension"],
"_run_hook_in_place": "true"
} Context serializationFrom a python hook author point of view, using the cookiecutter context from within a hook is simply done by consuming the new
Concretely, imagine your
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from cookiecutter.serialization import get_context, put_context
# adding some values to be consumed by the post_gen_project
context = get_context()
context['for_post_gen_project'] = 'From pre_gen_project to post_gen_project'
put_context(context)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from cookiecutter.serialization import get_context, put_context
context = get_context()
print(context['for_post_gen_project']) From a shell hook author it is a little bit more complicated, but the serialization API offers all the services to achieve this goal. Serialization APIIt is composed of five module functions:
and one class:
The
Calling the
So, to come back to the side of a shell hook author, it might be preferable to deal with a different serialization format than JSON. #!/usr/bin/env python
# -*- coding: utf-8 -*-
from cookiecutter.serialization import AbstractSerializer
class OurOwnSerializer(AbstractSerializer):
def _do_serialize(self, subject):
"""
implement this method to do the serialization
"""
def _do_deserialize(self, string):
"""
implement this method to do the deserialization
""" The But what to do now ? {
"_serializers": {
"use": "ourown",
"classes": {
"ourown": "serializers.OurOwnSerializer"
}
}
{ in the template cookiecurrer.json configuration file as below : {
"full_name": "Your name",
"email": "Your address email (eq. you@example.com)",
"github_username": "Your github username",
"project_name": "Name of the project",
"project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}",
"project_short_description": "A short description of the project",
"release_date": "{% now 'local' %}",
"version": "0.1.0",
"_extensions": ["jinja2_time.TimeExtension"],
"_serializers": {
"use": "ourown",
"classes": {
"ourown": "serializers.OurOwnSerializer"
}
}
} A perceptive mind would have noticed this part Coming soonAs a bonus feature, a new hook This PR needs some little refactorings and I will wait for this PR #749 to be merged before updating the documentation. I hope this will find you interested, and please, feel free to share your impressions. best regards Eric |
Signed-off-by: Eric Villard <dev@eviweb.fr>
Signed-off-by: Eric Villard <dev@eviweb.fr>
Hi, def get_from_context(context, key, default=None, update=False):
"""
Get the value referenced by a given key from a given context
Keys can be defined using dot notation to retrieve values from nested
dictionaries
Keys can contain integers to retrieve values from lists
ie.
context = {
'cookiecutter': {
'project_name': 'Project Name'
}
}
project_name = get_from_context(context, 'cookiecutter.project_name')
:param context: context to search in
:param key: key to look for
:param default: default value that will be returned if the key is not found
:param update: if True, create the key in the context and set its value
using the default argument value
""" Next step, the new Eric |
- set_to_context: permit to set key/value pairs to a given context using dot notation - get_from_cookiecutter_context: shorthand function to get values from the cookiecutter sub context using dot notation - set_to_cookiecutter_context: shorthand function to set key/value pairs to the cookiecutter sub context using dot notation Signed-off-by: Eric Villard <dev@eviweb.fr>
it is run before user is prompted for the template configuration this allows to populate the cookiecutter context dynamically (ie. list of licenses generated from files in a directory) Signed-off-by: Eric Villard <dev@eviweb.fr>
Here we are, the development phase is finished. 😅
I think that's all... Thanks for your help Eric |
Hi @eviweb 😄 Thanks for all this code- it's an impressive amount of work!
Thanks again and please keep contributing 😸 |
Hi @michaeljoseph,
That's being said and with the time of the week-end, I decided to put my susceptibility aside 😌. Best regards Eric |
I assume it was abandoned. |
hi,
here is an enhancement of how hooks work.
They now receive through the standard input stream, the cookiecutter context object, serialized in JSON .
This allows to unserialize this object, and then simply read and/or update some values that can be sent back to the main context, by writing a JSON serialized object to the standard output.
It is also possible to disable the hook duplication by adding the setting
"_no_hookcopy": "yes"
in thecookiecutter.json
. Hooks are then run directly in place.This should be useful, when a template author does not need to use template variables directly in his/her hooks and want to keep track of the template directory.
A concrete example is license management. In the documentation - Choice Variables (1.1+) - it is explained how to manage licenses in one file.
It should be easier to get many license file templates in a directory, and then just copy the selected one.
More information and examples are available in the updated
Advanced usage
page of the documentation of this PR.This is the start of a proposition and I'm very interesting by getting some feedbacks.
Best regards
Eric