-
-
Notifications
You must be signed in to change notification settings - Fork 185
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
feat: Add _operation
variable
#1733
base: master
Are you sure you want to change the base?
Conversation
_copier_conf.operation
variable_copier_conf.operation
variable
2a53803
to
eea53da
Compare
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.
The docs would need to reflect this change too.
@@ -234,7 +238,7 @@ def _cleanup(self) -> None: | |||
for method in self._cleanup_hooks: | |||
method() | |||
|
|||
def _check_unsafe(self, mode: Literal["copy", "update"]) -> None: | |||
def _check_unsafe(self, mode: Operation) -> None: |
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.
No need to pass ˋmodeˋ if it is in ˋself.operationˋ, right?
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'm not sure I'm following. First, let me sum up:
- Introducing
_copier_conf.operation
means we have an attribute on the worker representing the current high-level (user-requested) operation. - You're proposing to use this reference for
_check_unsafe
instead of the parameter. - I noted that doing this will change how
_check_unsafe
behaves 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.
4ba043b
to
e0cac30
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1733 +/- ##
==========================================
- Coverage 97.67% 95.75% -1.92%
==========================================
Files 49 50 +1
Lines 5238 5306 +68
==========================================
- Hits 5116 5081 -35
- Misses 122 225 +103
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚨 Try these New Features:
|
@yajo Is there anything I still need to do here? Just making sure you noticed the changes. :) |
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.
Sorry for taking so long to review! I don't have a lot of time and this one required deep thinking.
@@ -234,7 +238,7 @@ def _cleanup(self) -> None: | |||
for method in self._cleanup_hooks: | |||
method() | |||
|
|||
def _check_unsafe(self, mode: Literal["copy", "update"]) -> None: | |||
def _check_unsafe(self, mode: Operation) -> None: |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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(): update
The 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
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.
Thanks for updating this PR, @lkubb! 👍 This looks very good, just a couple of quite minor requests and ideas from my side.
6db03a2
to
126337f
Compare
126337f
to
75c5603
Compare
What this PR adds is exactly what I am missing. I recently started using copier for an infrastructure project using compose, env files etc. with various different configuration options. There are some things, such as questions/answers and tasks, that I don’t want to ask/run on update. And, other tasks should only run when an update is performed. I am wondering what the rationale is behind not making the operation available to questions (conditions), if I read the above correctly. Would it be possible to use it in |
For the latter I'd recommend using migrations. They can be configured to run independent of the involved versions.
No, the reason is that |
204741f
to
98746b5
Compare
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.
7a27c87
to
2a80d01
Compare
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.
Fantastic work, @lkubb! 👌 LGTM! 🎉
exclude
configuration templatable._operation
variable to the rendering contexts forexclude
andtasks
, representing the current operation - eithercopy
,orrecopy
update
.Adds the (semi-)undocumented_copier_python
,_copier_conf.os
and_folder_name
variables 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_operation (!/=)= "update"
.Fixes: #1725
Fixes: #1625
Alternative to #1732