7
7
import subprocess
8
8
import sys
9
9
from contextlib import suppress
10
+ from contextvars import ContextVar
10
11
from dataclasses import asdict , field , replace
11
12
from filecmp import dircmp
12
- from functools import cached_property , partial
13
+ from functools import cached_property , partial , wraps
13
14
from itertools import chain
14
15
from pathlib import Path
15
16
from shutil import rmtree
64
65
AnyByStrDict ,
65
66
AnyByStrMutableMapping ,
66
67
JSONSerializable ,
68
+ Operation ,
69
+ ParamSpec ,
67
70
RelativePath ,
68
71
StrOrPath ,
69
72
)
70
73
from .user_data import AnswersMap , Question , load_answersfile_data
71
74
from .vcs import get_git
72
75
73
76
_T = TypeVar ("_T" )
77
+ _P = ParamSpec ("_P" )
78
+
79
+ _operation : ContextVar [Operation ] = ContextVar ("_operation" )
80
+
81
+
82
+ def as_operation (value : Operation ) -> Callable [[Callable [_P , _T ]], Callable [_P , _T ]]:
83
+ """Decorator to set the current operation context, if not defined already.
84
+
85
+ This value is used to template specific configuration options.
86
+ """
87
+
88
+ def _decorator (func : Callable [_P , _T ]) -> Callable [_P , _T ]:
89
+ @wraps (func )
90
+ def _wrapper (* args : _P .args , ** kwargs : _P .kwargs ) -> _T :
91
+ token = _operation .set (_operation .get (value ))
92
+ try :
93
+ return func (* args , ** kwargs )
94
+ finally :
95
+ _operation .reset (token )
96
+
97
+ return _wrapper
98
+
99
+ return _decorator
74
100
75
101
76
102
# HACK https://github.com/copier-org/copier/pull/1880#discussion_r1887491497
@@ -260,7 +286,7 @@ def _cleanup(self) -> None:
260
286
for method in self ._cleanup_hooks :
261
287
method ()
262
288
263
- def _check_unsafe (self , mode : Literal [ "copy" , "update" ] ) -> None :
289
+ def _check_unsafe (self , mode : Operation ) -> None :
264
290
"""Check whether a template uses unsafe features."""
265
291
if self .unsafe or self .settings .is_trusted (self .template .url ):
266
292
return
@@ -333,8 +359,10 @@ def _execute_tasks(self, tasks: Sequence[Task]) -> None:
333
359
Arguments:
334
360
tasks: The list of tasks to run.
335
361
"""
362
+ operation = _operation .get ()
336
363
for i , task in enumerate (tasks ):
337
364
extra_context = {f"_{ k } " : v for k , v in task .extra_vars .items ()}
365
+ extra_context ["_operation" ] = operation
338
366
339
367
if not cast_to_bool (self ._render_value (task .condition , extra_context )):
340
368
continue
@@ -364,7 +392,7 @@ def _execute_tasks(self, tasks: Sequence[Task]) -> None:
364
392
/ Path (self ._render_string (str (task .working_directory ), extra_context ))
365
393
).absolute ()
366
394
367
- extra_env = {k .upper (): str (v ) for k , v in task . extra_vars .items ()}
395
+ extra_env = {k [ 1 :] .upper (): str (v ) for k , v in extra_context .items ()}
368
396
with local .cwd (working_directory ), local .env (** extra_env ):
369
397
subprocess .run (task_cmd , shell = use_shell , check = True , env = local .env )
370
398
@@ -632,7 +660,14 @@ def _pathjoin(
632
660
@cached_property
633
661
def match_exclude (self ) -> Callable [[Path ], bool ]:
634
662
"""Get a callable to match paths against all exclusions."""
635
- return self ._path_matcher (self .all_exclusions )
663
+ # Include the current operation in the rendering context.
664
+ # Note: This method is a cached property, it needs to be regenerated
665
+ # when reusing an instance in different contexts.
666
+ extra_context = {"_operation" : _operation .get ()}
667
+ return self ._path_matcher (
668
+ self ._render_string (exclusion , extra_context = extra_context )
669
+ for exclusion in self .all_exclusions
670
+ )
636
671
637
672
@cached_property
638
673
def match_skip (self ) -> Callable [[Path ], bool ]:
@@ -935,6 +970,7 @@ def template_copy_root(self) -> Path:
935
970
return self .template .local_abspath / subdir
936
971
937
972
# Main operations
973
+ @as_operation ("copy" )
938
974
def run_copy (self ) -> None :
939
975
"""Generate a subproject from zero, ignoring what was in the folder.
940
976
@@ -945,6 +981,11 @@ def run_copy(self) -> None:
945
981
946
982
See [generating a project][generating-a-project].
947
983
"""
984
+ with suppress (AttributeError ):
985
+ # We might have switched operation context, ensure the cached property
986
+ # is regenerated to re-render templates.
987
+ del self .match_exclude
988
+
948
989
self ._check_unsafe ("copy" )
949
990
self ._print_message (self .template .message_before_copy )
950
991
self ._ask ()
@@ -971,6 +1012,7 @@ def run_copy(self) -> None:
971
1012
# TODO Unify printing tools
972
1013
print ("" ) # padding space
973
1014
1015
+ @as_operation ("copy" )
974
1016
def run_recopy (self ) -> None :
975
1017
"""Update a subproject, keeping answers but discarding evolution."""
976
1018
if self .subproject .template is None :
@@ -981,6 +1023,7 @@ def run_recopy(self) -> None:
981
1023
with replace (self , src_path = self .subproject .template .url ) as new_worker :
982
1024
new_worker .run_copy ()
983
1025
1026
+ @as_operation ("update" )
984
1027
def run_update (self ) -> None :
985
1028
"""Update a subproject that was already generated.
986
1029
@@ -1028,6 +1071,11 @@ def run_update(self) -> None:
1028
1071
print (
1029
1072
f"Updating to template version { self .template .version } " , file = sys .stderr
1030
1073
)
1074
+ with suppress (AttributeError ):
1075
+ # We might have switched operation context, ensure the cached property
1076
+ # is regenerated to re-render templates.
1077
+ del self .match_exclude
1078
+
1031
1079
self ._apply_update ()
1032
1080
self ._print_message (self .template .message_after_update )
1033
1081
0 commit comments