1
|
|
|
# coding: utf8 |
2
|
|
|
|
3
|
|
|
# Copyright 2013-2017 Vincent Jacques <[email protected]> |
4
|
|
|
|
5
|
|
|
""" |
6
|
|
|
Stock actions are predefined common tasks (manipulating the filesystem, calling an external program, etc.) |
7
|
|
|
They all specialize :class:`.Action`. |
8
|
|
|
""" |
9
|
|
|
|
10
|
|
|
from __future__ import division, absolute_import, print_function |
11
|
|
|
|
12
|
|
|
import errno |
13
|
|
|
import os |
14
|
|
|
import shutil |
15
|
|
|
import subprocess |
16
|
|
|
import time |
17
|
|
|
|
18
|
|
|
|
19
|
|
|
from . import Action |
20
|
|
|
|
21
|
|
|
|
22
|
|
|
DEFAULT = object() |
23
|
|
|
|
24
|
|
|
|
25
|
|
|
class NullAction(Action): |
26
|
|
|
""" |
27
|
|
|
A stock action that does nothing. |
28
|
|
|
Useful as a placeholder for several dependencies. |
29
|
|
|
""" |
30
|
|
|
def __init__(self, label=None, *args, **kwds): |
31
|
|
|
""" |
32
|
|
|
@todoc |
33
|
|
|
""" |
34
|
|
|
Action.__init__(self, label, *args, **kwds) |
35
|
|
|
|
36
|
|
|
def do_execute(self, dependency_statuses): |
37
|
|
|
pass |
38
|
|
|
|
39
|
|
|
|
40
|
|
|
class CallSubprocess(Action): |
41
|
|
|
""" |
42
|
|
|
A stock action that calls a subprocess. |
43
|
|
|
|
44
|
|
|
Note: if the process fails, |
45
|
|
|
:func:`~subprocess.check_call` raises a :exc:`subprocess.CalledProcessError`, |
46
|
|
|
which `cannot be pickled <http://bugs.python.org/issue1692335>`__ in Python 2. |
47
|
|
|
So, in that case, this action catches the original exception and |
48
|
|
|
raises a :exc:`CalledProcessError`. |
49
|
|
|
""" |
50
|
|
|
def __init__(self, command, kwargs={}, label=DEFAULT, *args, **kwds): |
51
|
|
|
""" |
52
|
|
|
@todoc |
53
|
|
|
""" |
54
|
|
|
Action.__init__(self, " ".join(command) if label is DEFAULT else label, *args, **kwds) |
55
|
|
|
self.__command = command |
56
|
|
|
self.__kwargs = kwargs |
57
|
|
|
|
58
|
|
|
def do_execute(self, dependency_statuses): |
59
|
|
|
# subprocess.CalledProcessError can't be pickled in Python2 |
60
|
|
|
# See http://bugs.python.org/issue1692335 |
61
|
|
|
try: |
62
|
|
|
subprocess.check_call(self.__command, **self.__kwargs) |
63
|
|
|
except subprocess.CalledProcessError as e: # Not doctested: implementation detail |
64
|
|
|
raise CalledProcessError(e.returncode, e.cmd, e.output) |
65
|
|
|
|
66
|
|
|
|
67
|
|
|
class CalledProcessError(Exception): |
68
|
|
|
""" |
69
|
|
|
Raised by :class:`CallSubprocess` |
70
|
|
|
""" |
71
|
|
|
|
72
|
|
|
|
73
|
|
|
class CreateDirectory(Action): |
74
|
|
|
""" |
75
|
|
|
A stock action that creates a directory. |
76
|
|
|
No error will be raised if the directory already exists. |
77
|
|
|
If the directory to create is nested, intermediate directories will be created as well. |
78
|
|
|
|
79
|
|
|
:param str name: the directory to create, passed to :func:`os.makedirs`. |
80
|
|
|
""" |
81
|
|
|
def __init__(self, name, label=DEFAULT, *args, **kwds): |
82
|
|
|
""" |
83
|
|
|
@todoc |
84
|
|
|
""" |
85
|
|
|
Action.__init__(self, "mkdir {}".format(name) if label is DEFAULT else label, *args, **kwds) |
86
|
|
|
self.__name = name |
87
|
|
|
|
88
|
|
|
def do_execute(self, dependency_statuses): |
89
|
|
|
try: |
90
|
|
|
os.makedirs(self.__name) |
91
|
|
|
except OSError as e: # Not doctested: implementation detail |
92
|
|
|
if e.errno != errno.EEXIST or not os.path.isdir(self.__name): |
93
|
|
|
raise |
94
|
|
|
|
95
|
|
|
|
96
|
|
|
class DeleteFile(Action): # Not doctested: could be |
97
|
|
|
""" |
98
|
|
|
A stock action that deletes a file. |
99
|
|
|
No error will be raise if the file doesn't exist. |
100
|
|
|
|
101
|
|
|
:param str name: the name of the file to delete, passed to :func:`os.unlink`. |
102
|
|
|
""" |
103
|
|
|
def __init__(self, name, label=DEFAULT, *args, **kwds): |
104
|
|
|
""" |
105
|
|
|
@todoc |
106
|
|
|
""" |
107
|
|
|
Action.__init__(self, "rm {}".format(name) if label is DEFAULT else label, *args, **kwds) |
108
|
|
|
self.__name = name |
109
|
|
|
|
110
|
|
|
def do_execute(self, dependency_statuses): |
111
|
|
|
try: |
112
|
|
|
os.unlink(self.__name) |
113
|
|
|
except OSError as e: |
114
|
|
|
if e.errno != errno.ENOENT: |
115
|
|
|
raise |
116
|
|
|
|
117
|
|
|
|
118
|
|
|
class DeleteDirectory(Action): # Not doctested: could be |
119
|
|
|
""" |
120
|
|
|
A stock action that deletes a directory (recursively). |
121
|
|
|
No error will be raise if the directory doesn't exist. |
122
|
|
|
|
123
|
|
|
:param str name: the name of the directory to delete, passed to :func:`shutil.rmtree`. |
124
|
|
|
""" |
125
|
|
|
def __init__(self, name, label=DEFAULT, *args, **kwds): |
126
|
|
|
""" |
127
|
|
|
@todoc |
128
|
|
|
""" |
129
|
|
|
Action.__init__(self, "rm -r {}".format(name) if label is DEFAULT else label, *args, **kwds) |
130
|
|
|
self.__name = name |
131
|
|
|
|
132
|
|
|
def do_execute(self, dependency_statuses): |
133
|
|
|
try: |
134
|
|
|
shutil.rmtree(self.__name) |
135
|
|
|
except OSError as e: |
136
|
|
|
if e.errno != errno.ENOENT: |
137
|
|
|
raise |
138
|
|
|
|
139
|
|
|
|
140
|
|
|
class CopyFile(Action): # Not doctested: could be |
141
|
|
|
""" |
142
|
|
|
A stock action that copies a file. Arguments are passed to :func:`shutil.copy`. |
143
|
|
|
|
144
|
|
|
:param str src: the file to copy |
145
|
|
|
:param str dst: the destination |
146
|
|
|
""" |
147
|
|
|
def __init__(self, src, dst, label=DEFAULT, *args, **kwds): |
148
|
|
|
""" |
149
|
|
|
@todoc |
150
|
|
|
""" |
151
|
|
|
Action.__init__(self, "cp {} {}".format(src, dst) if label is DEFAULT else label, *args, **kwds) |
152
|
|
|
self.__src = src |
153
|
|
|
self.__dst = dst |
154
|
|
|
|
155
|
|
|
def do_execute(self, dependency_statuses): |
156
|
|
|
shutil.copy(self.__src, self.__dst) |
157
|
|
|
|
158
|
|
|
|
159
|
|
|
class TouchFile(Action): # Not doctested: could be |
160
|
|
|
""" |
161
|
|
|
A stock action that touches a file. |
162
|
|
|
If the file already exists, its modification time will be modified. |
163
|
|
|
Else, it will be created, empty. |
164
|
|
|
|
165
|
|
|
Note that the containing directory must exist. |
166
|
|
|
You might want to ensure that by adding a :class:`CreateDirectory` as a dependency. |
167
|
|
|
|
168
|
|
|
:param str name: the name of the file to touch. Passed to :func:`open` and/or :func:`os.utime`. |
169
|
|
|
""" |
170
|
|
|
|
171
|
|
|
def __init__(self, name, label=DEFAULT, *args, **kwds): |
172
|
|
|
""" |
173
|
|
|
@todoc |
174
|
|
|
""" |
175
|
|
|
Action.__init__(self, "touch {}".format(name) if label is DEFAULT else label, *args, **kwds) |
176
|
|
|
self.__name = name |
177
|
|
|
|
178
|
|
|
def do_execute(self, dependency_statuses): |
179
|
|
|
open(self.__name, "ab").close() # Create the file if needed |
180
|
|
|
os.utime(self.__name, None) # Actually change its time |
181
|
|
|
|
182
|
|
|
|
183
|
|
|
class Sleep(Action): |
184
|
|
|
""" |
185
|
|
|
A stock action that sleeps for a certain duration. |
186
|
|
|
|
187
|
|
|
:param float secs: seconds to sleep, passed to :func:`time.sleep`. |
188
|
|
|
""" |
189
|
|
|
def __init__(self, secs, label=DEFAULT, *args, **kwds): |
190
|
|
|
""" |
191
|
|
|
@todoc |
192
|
|
|
""" |
193
|
|
|
Action.__init__(self, "sleep {}".format(secs) if label is DEFAULT else label, *args, **kwds) |
194
|
|
|
self.__secs = secs |
195
|
|
|
|
196
|
|
|
def do_execute(self, dependency_statuses): |
197
|
|
|
time.sleep(self.__secs) |
198
|
|
|
|