Conversation
_copier_conf.operation variable_copier_conf.operation variable
2a53803 to
eea53da
Compare
yajo
left a comment
There was a problem hiding this comment.
The docs would need to reflect this change too.
| method() | ||
|
|
||
| def _check_unsafe(self, mode: Literal["copy", "update"]) -> None: | ||
| def _check_unsafe(self, mode: Operation) -> None: |
There was a problem hiding this comment.
No need to pass ˋmodeˋ if it is in ˋself.operationˋ, right?
There was a problem hiding this comment.
Initially I did it like that, but noticed that this would cause a behavior change (at least in theory):
During _apply_update(), self.operation is update, but it calls on run_copy() several times, which would pass copy to _check_unsafe() before this patch.
There was a problem hiding this comment.
Yes. However, that is correct. You will notice that there are calls to replace. In those calls, you can replace some configuration for the sub-worker that is created. Could you please try doing it that way?
There was a problem hiding this comment.
Sorry, I'm not sure I'm following. First, let me sum up:
- Introducing
_copier_conf.operationmeans we have an attribute on the worker representing the current high-level (user-requested) operation. - You're proposing to use this reference for
_check_unsafeinstead of the parameter. - I noted that doing this will change how
_check_unsafebehaves during the individual copy operations that run during an update, where the high-level operation isupdate, but the low-level one iscopy, advocating for keeping the parameter.
I'm already using replace for overriding the operation during update. Are you saying the high-level operation during the individual copy operations should be copy? Because that would mean _copier_conf.operation is always copy during template rendering, i.e. defeat the purpose of this feature.
There was a problem hiding this comment.
During an update, the current template version is used to re-generate a fresh project. It makes sense to me that this project generation would use the copy phase, to make sure tasks run accordingly. But I 100% guarantee that many users will find this un-intuitive: they want copy tasks to only apply when they generate a project, not when Copier does so during an update. They don't care what Copier is doing behind the scene.
So, in my opinion, we need a third operation, like "copy-during-update". It's hard to find a name that conveys what happens. tmp-copy? Maybe update-diff, since that's what the re-generated project is used for, to compute a diff?
With these three operations, users can make sure things only run when they generate a project (not when Copier does so during an update), and when they update a project. If they want a task to be repeated in temporary generated projects, they can accept update-diff too:
if "{{ _copier_operation }}" in ("copy", "update-diff"):
# do something
Typical examples of tasks that shouldn't be run during update-diff:
git init(there's no point in doing so)- printing messages to the user (you don't want the message to appear in the middle of an update's output)
- any task with side-effects, like HTTP POSTing stuff, or creating/removing files on the file-system, DB queries, and so many more things
There was a problem hiding this comment.
The current implementation, which uses a context variable and decorator, exposes _operation = "update" to the copy tasks during copier update, so Copier's internals of re-generating a fresh project aren't leaked to the template developer. So, a task can be limited to running only on copier copy with:
_tasks:
- command: ./run-task.sh
when: "{{ _operation == 'copy' }}"Does this address your concern?
There was a problem hiding this comment.
What about update tasks that will run in temporary copies then? 🤔 My point is:
copier copy->copyop -> all goodcopier update->updateop (in temp copy) thenupdateop again (in actual project) -> update tasks run in temp copy too, might cause issues
But I checked #240 again and I only see people with git init-like use-cases, i.e. they generally want to run something at copy and not at update, not the opposite. So for now the current changes might be enough 🙂
There was a problem hiding this comment.
Only example I found, that is implicit, would be template writers running a "resolve and install" update task. It can take some time and is useless in the temporary copy.
There was a problem hiding this comment.
Your overview of the situation is very helpful, thanks! Then, I see two scenarios and solutions:
-
Run tasks only after
copier copy(e.g.,git init– i.e., no task run after internal temporary copy):_tasks: - command: git init when: "{{ _operation == 'copy' }}"
-
Run tasks only after
copier update(e.g., "resolve and install"):_migrations: - ./resolve-and-install.sh
I must admit that this situation is not entirely obvious. We might need to add documentation to clarify it better.
There was a problem hiding this comment.
Yep it's a bit counter-intuitive to have to use a migration to avoid running update tasks in the temp copy. Docs would be a good first step!
4ba043b to
e0cac30
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #1733 +/- ##
==========================================
+ Coverage 98.00% 98.02% +0.01%
==========================================
Files 53 54 +1
Lines 5720 5775 +55
==========================================
+ Hits 5606 5661 +55
Misses 114 114
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@yajo Is there anything I still need to do here? Just making sure you noticed the changes. :) |
yajo
left a comment
There was a problem hiding this comment.
Sorry for taking so long to review! I don't have a lot of time and this one required deep thinking.
| method() | ||
|
|
||
| def _check_unsafe(self, mode: Literal["copy", "update"]) -> None: | ||
| def _check_unsafe(self, mode: Operation) -> None: |
There was a problem hiding this comment.
Yes. However, that is correct. You will notice that there are calls to replace. In those calls, you can replace some configuration for the sub-worker that is created. Could you please try doing it that way?
f16e203 to
cf4934a
Compare
735f33e to
1e67a47
Compare
There was a problem hiding this comment.
Sorry for taking so long to give feedback. 🫣 This PR really needs some deep thinking.
Adding the attribute operation to the Worker class seems conceptually wrong to me because we'd make the operation mode part of its state while it also has methods to execute either operation (run_copy() and run_update()). Thus, it would be possible to set the operation attribute to "update" and call run_copy() (or set the operation attribute to "copy" and call run_update()) which would lead to incorrect behavior. This indicates a bad design to me.
How about using a context variable to manage the operation context? This is a simplified example of what I mean:
from __future__ import annotations
import sys
from contextvars import ContextVar
from dataclasses import dataclass, replace
from functools import wraps
from typing import Callable, Literal, TypeVar
if sys.version_info >= (3, 10):
from typing import ParamSpec
else:
from typing_extensions import ParamSpec
Operation = Literal["copy", "update"]
P = ParamSpec("P")
R = TypeVar("R")
_operation: ContextVar[Operation] = ContextVar("_operation")
def as_operation(value: Operation) -> Callable[[Callable[P, R]], Callable[P, R]]:
def _decorator(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
token = _operation.set(_operation.get(value))
try:
return func(*args, **kwargs)
finally:
_operation.reset(token)
return _wrapper
return _decorator
@dataclass
class Worker:
template: str
@as_operation("copy")
def run_copy(self) -> None:
print(f"Worker({self.template}).run_copy(): {_operation.get()}")
@as_operation("update")
def run_update(self) -> None:
print(f"Worker({self.template}).run_update(): {_operation.get()}")
replace(self, template="old").run_copy()
replace(self, template="new").run_copy()
worker = Worker("new")
print("$ copier copy ...")
worker.run_copy()
# -> Worker(new).run_copy(): copy
print("$ copier update ...")
worker.run_update()
# -> Worker(new).run_update(): update
# -> Worker(old).run_copy(): update
# -> Worker(new).run_copy(): updateThe Worker.run_copy() and Worker.run_update() methods both set their operation context via the decorator as_operation(). The decorator wraps the decorated method and sets the operation context to the provided value only when the operation context has not been set yet; if the operation context has been set, it remains as is. Thus, a call of Worker.run_update() sets the operation context to "update", and when Worker.run_copy() gets called within this context, the operation context will remain "update".
Using ContextVar is thread-safe. Also, the following requirement stated in the Python docs
Important: Context Variables should be created at the top module level and never in closures. Context objects hold strong references to context variables which prevents context variables from being properly garbage collected.
is met.
I agree that the Jinja variable _copier_conf.operation only seems to make sense when rendering template expressions in copier.yaml. And although the update seems to produce correct results, the fact that the _copier_conf.operation value is unstable during an update (while rendering files etc.), it is asking for trouble IMO. So, how about omitting the _copier_conf.operation variable in the Jinja context while rendering files, directories, and path names? We'd need to document this behavior of course.
@sisp Thank you for your thoughtful feedback and sorry for taking my time to reply as well.
Agreed, I wasn't really happy with it myself, also because it meant the forced inclusion of the operation in inappropriate contexts. Part of this design was caused by the tight coupling of the Copier worker with the renderer imho, I wasn't sure how to solve it properly without a major refactoring.
Note that it's only the former situation that leads to incorrect behavior, the latter is the expected path since the
Thanks a lot for your well thought-out proposal. I agree this design is superior and am currently implementing it. I'm still contemplating a detail since it aggravates a flaw I just noticed in the Since [Note: An analog of this is currently present in I might need to address this by
👍 Your proposal allows to restrict it to the scopes where it makes sense, namely |
1e67a47 to
ab02a1a
Compare
_copier_conf.operation variable_operation variable
819b58b to
a55f2c9
Compare
6db03a2 to
126337f
Compare
7a27c87 to
2a80d01
Compare
|
Do we agree that this PR also fixes the long-standing #240? |
|
@pawamoy I think it addresses part of #240, but pre/post copy/update granularity is not addressed. There's #1511, but it seems stale. @lkubb @copier-org/maintainers A thought that originates from #1948 (review) (and subsequent comments): I think we should rename |
|
If I read #240 correctly, pre/post were specific to migrations, and are now handled with a I'm not sure what pre-copy, post-copy, pre-update, or post-update would mean exactly. IMO this PR solves #240. Let me also link again to my comment above about a third update-diff operation, which I think is super important to have. |
|
You're right, I forgot about the availability of (I've also replied to your comment above.) |
|
Actually after seeing a few duplicates of #240 like #635, I think this PR only partially solves it, as you pointed @sisp. Users want to run things before and after a copy, or before and after an update. The operation won't allow that. Currently tasks only run after. Using context hooks to run things before would be a hack (earliest is when the first Jinja template string is rendered, which is definitely not before anything). But anyway we could close #240 and re-open a fresh issue to get rid of all the legacy discussions about it. |
|
Ah, you're right, @pawamoy. I got confused. 😕 But looking at https://copier.readthedocs.io/en/stable/updating/#how-the-update-works (and thinking about the detailed implementation), I think pre-/post-migrations are equivalent to pre-/post-update tasks for |
Seems like it, yes! I feel like this deserves a proper entry in the docs, something like "Pre/post hooks", which is how Cookiecutter named them, and will be familiar to most users. This entry would document that you can use migration as pre/post-update hooks (with examples), tasks as post-copy or post-update hooks, and... pre-copy hooks are not supported yet. The docs entry should emphasize that for simple message display, users can set |
2a80d01 to
8f1ab8f
Compare
@sisp Done and rebased. Edit: Having read the discussion in #1948, I want to emphasize that (because of #1733 (comment)) this patch does NOT make Given the many comments this PR has accumulated over the half year it has been open, I'll try to give a final summary of what it provides and where it fits in the Copier hook system. This PR
(1+2) allows to provide dumb boilerplate files which will be excluded during updates If you need your logic to run... before/after any `update`Use a `migration` without specifying a version constraint and with appropriate `when` (or even `when: 'true'` to run both before and after). Example:_migrations:
- this_script_runs_after_all_updates.sh
- command: this_script_runs_before_all_updates.sh
when: "{{ _stage == 'before' }}"
- command: this_script_runs_before_and_after_all_updates.sh
when: "true"You can rely on the provided env vars (STAGE/VERSION_{,PEP440_}{FROM,TO,CURRENT} to target specific commands. before a `copy`Come up with an ugly hack possibly involving a custom extension or provide a patch to core, not officially supported atm.after a `copy`Use a `task` with `when: '{{ _copier_operation == "copy" }}'`. Example:_tasks:
- command: this_script_runs_after_copy_operations_only.sh
when: "{{ _copier_operation == 'copy' }}"Note All of the hook suggestions apply to the generated project only, not the two intermediate copies involved in Is there anything I can do to help carry this PR over the line? It has been stalled for a while now and the release of Copier 9.5.0 broke the ugly hack I was using to simulate _tasks:
# Don't run this when updating.
# This breaks when recopying.
- >-
{%- if
not _copier_conf.answers.last
and '.new_copy.' not in _copier_conf.dst_path.name
and '.old_copy.' not in _copier_conf.dst_path.name -%}
"{{ _copier_python }}" "{{ "{src}{sep}tasks{sep}initialize.py".format(src=_copier_conf.src_path, sep=_copier_conf.sep) }}" init
{%- endif -%}Specifically #1880, which added this line and thereby removed Line 382 in e444514 |
d62d231 to
a3f9b6b
Compare
sisp
left a comment
There was a problem hiding this comment.
[...] this patch does NOT make
_operation/_copier_operationavailable in all rendering contexts, it's limited totasksandexclude. Maybe this aspect should be reconsidered in light of extensions.
Thanks for pointing this out. I think we can keep _copier_operation limited to only those two contexts and expand if needed when there is a real use case.
I'm sorry that this PR has been open for so long. I took the liberty to do a (hopefully) final rebase to resolve merge conflicts. Everything looks good to me. 🎉 Thanks for your patience with the reviews/discussions and keeping this PR up to date! 🙇 👏
As @yajo has requested changes some time ago, I'm afraid I need to wait for his approval to merge.
933d83b to
5c597e4
Compare
5c597e4 to
0812dbb
Compare
yajo
left a comment
There was a problem hiding this comment.
I think there's a needed small extra explanation in the docs. Other than that, if it looks good to you, it's good to me :D
excludeconfiguration templatable._copier_operationvariable to the rendering contexts forexcludeandtasks, representing the current operation - eithercopy,orrecopyupdate.Adds the (semi-)undocumented_copier_python,_copier_conf.osand_folder_namevariables to the docs in a second commit (since I restructured them anyways)extracted into docs: Restructure context vars, add undocumented ones #1856, will rebase once it's mergedrebasedNotes:
This was proposed here: #1718 (comment)
I hope the way it's implemented and tested is as intended by @yajo.
The tests are quite synthetic,I would expect the usual application to be_copier_operation (!/=)= "update".Fixes: #1725
Fixes: #1625
(mostly) Fixes: #240
Alternative to #1732 (regarding update exclusions only).