PyProd 0.2.0__tar.gz → 0.3.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {pyprod-0.2.0 → pyprod-0.3.0}/.github/workflows/publish.yml +1 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/PKG-INFO +2 -2
- {pyprod-0.2.0 → pyprod-0.3.0}/README.rst +1 -1
- {pyprod-0.2.0 → pyprod-0.3.0}/docs/index.rst +2 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/docs/prodfile.rst +50 -2
- {pyprod-0.2.0 → pyprod-0.3.0}/docs/quickstart.rst +10 -10
- pyprod-0.3.0/docs/releasenotes.rst +9 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/pyproject.toml +2 -2
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/md-to-pdf/Prodfile.py +4 -5
- pyprod-0.2.0/sample/s3files/PRODFILE.py → pyprod-0.3.0/sample/s3files/Prodfile.py +2 -1
- pyprod-0.3.0/sample/tutorial-1/Prodfile.py +4 -0
- pyprod-0.3.0/sample/tutorial-2/Prodfile.py +13 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/src/pyprod/prod.py +147 -62
- {pyprod-0.2.0 → pyprod-0.3.0}/tests/test_prod.py +10 -10
- {pyprod-0.2.0 → pyprod-0.3.0}/tests/test_prodfuncs.py +4 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/tests/test_rule.py +62 -6
- {pyprod-0.2.0 → pyprod-0.3.0}/uv.lock +1 -1
- {pyprod-0.2.0 → pyprod-0.3.0}/.github/workflows/test.yml +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/.gitignore +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/.python-version +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/.readthedocs.yaml +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/LICENSE +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/docs/Makefile +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/docs/commandline.rst +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/docs/conf.py +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/docs/make.bat +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/docs/pyprod2.png +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/docs/requirements.txt +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/pyprod.webp +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/pyprod2.png +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/build-c/Makefile +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/build-c/PRODFILE.py +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/build-c/hello.c +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/build-c/hello.h +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/build-c/main.c +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/generate-doc/.gitignore +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/generate-doc/PRODFILE.py +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/generate-doc/a.txt +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/generate-doc/b.txt +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/generate-doc/c.txt +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/generate-doc/inc1.txt +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/generate-doc/inc2.txt +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/md-to-pdf/doc.md +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/md-to-pdf/md_to_html.py +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/md-to-pdf/template.html +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/sample/s3files/S3TEST.txt +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/src/pyprod/__init__.py +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/src/pyprod/__main__.py +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/src/pyprod/main.py +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/src/pyprod/utils.py +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/src/pyprod/venv.py +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/tests/__init__.py +0 -0
- {pyprod-0.2.0 → pyprod-0.3.0}/tests/conftest.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: PyProd
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: PyProd: More Makeable than Make
|
5
5
|
Project-URL: Homepage, https://github.com/atsuoishimoto/pyprod
|
6
6
|
Project-URL: Documentation, https://pyprod.readthedocs.io/en/latest/
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/x-rst
|
|
17
17
|
PyProd - More Makeable than Make
|
18
18
|
=================================
|
19
19
|
|
20
|
-
PyProd is a Python script that can be used as an alternative to Makefile. By leveraging Python's versatility, it enables you to define build rules and dependencies programmatically, allowing for dynamic configurations, integration with existing Python libraries, and custom build logic not easily achievable with traditional Makefiles. For detailed documentation, please refer to the `official documentation <https://
|
20
|
+
PyProd is a Python script that can be used as an alternative to Makefile. By leveraging Python's versatility, it enables you to define build rules and dependencies programmatically, allowing for dynamic configurations, integration with existing Python libraries, and custom build logic not easily achievable with traditional Makefiles. For detailed documentation, please refer to the `official documentation <https://pyprod.readthedocs.io/en/latest/>`_.
|
21
21
|
|
22
22
|
|
23
23
|
Features
|
@@ -1,7 +1,7 @@
|
|
1
1
|
PyProd - More Makeable than Make
|
2
2
|
=================================
|
3
3
|
|
4
|
-
PyProd is a Python script that can be used as an alternative to Makefile. By leveraging Python's versatility, it enables you to define build rules and dependencies programmatically, allowing for dynamic configurations, integration with existing Python libraries, and custom build logic not easily achievable with traditional Makefiles. For detailed documentation, please refer to the `official documentation <https://
|
4
|
+
PyProd is a Python script that can be used as an alternative to Makefile. By leveraging Python's versatility, it enables you to define build rules and dependencies programmatically, allowing for dynamic configurations, integration with existing Python libraries, and custom build logic not easily achievable with traditional Makefiles. For detailed documentation, please refer to the `official documentation <https://pyprod.readthedocs.io/en/latest/>`_.
|
5
5
|
|
6
6
|
|
7
7
|
Features
|
@@ -29,7 +29,13 @@ A rule is defined using the ``@rule`` decorator, which takes the target file as
|
|
29
29
|
|
30
30
|
Build function
|
31
31
|
~~~~~~~~~~~~~~~~~~~
|
32
|
-
|
32
|
+
|
33
|
+
The function following the ``@rule`` decorator is the build function that generates the target file.
|
34
|
+
|
35
|
+
- The first argument of the build function specifies the target file to be generated.
|
36
|
+
- Subsequent arguments correspond to the filenames listed in the depends parameter. The rule function must accept the target and the same number of arguments as those specified in depends.
|
37
|
+
- ``uses`` dependencies are not passed to the build function.
|
38
|
+
- Even if Path objects are specified in the ``rule``, all arguments passed to the builder function will be of type str.
|
33
39
|
|
34
40
|
For example, the following code prints ``file1 ['file2', 'file3']`` when the target file ``file1`` is built:
|
35
41
|
|
@@ -229,6 +235,36 @@ Example:
|
|
229
235
|
msg = capture("echo Hello, World!")
|
230
236
|
|
231
237
|
|
238
|
+
.. py:function:: read(filename):
|
239
|
+
|
240
|
+
Read the contents of a file.
|
241
|
+
|
242
|
+
:param filename: The file to read.
|
243
|
+
:type filename: str | Path
|
244
|
+
|
245
|
+
:return: The contents of the file.
|
246
|
+
:rtype: str
|
247
|
+
|
248
|
+
.. py:function:: write(filename, txt, append=False):
|
249
|
+
|
250
|
+
Write text to a file.
|
251
|
+
|
252
|
+
:param filename: The file to write to.
|
253
|
+
:type filename: str | Path
|
254
|
+
|
255
|
+
:param txt: The text to write.
|
256
|
+
:type txt: str
|
257
|
+
|
258
|
+
:param append: Append to the file instead of overwriting it (default ``False``).
|
259
|
+
:type append: bool
|
260
|
+
|
261
|
+
.. py:function:: makedirs(path):
|
262
|
+
|
263
|
+
Create a directory along with any necessary parent directories if they do not already exist. This function wraps `os.makedirs() <https://docs.python.org/3/library/os.html#os.makedirs>`_ with the ``exists_ok`` parameter set to ``True``.
|
264
|
+
|
265
|
+
:param path: The directory to create.
|
266
|
+
:type path: str | Path
|
267
|
+
|
232
268
|
.. py:function:: glob(path, dir=".")
|
233
269
|
|
234
270
|
Glob the given relative pattern in the directory represented by this path. This function is a wrapper around `pathlib.Path.glob() <https://docs.python.org/3/library/pathlib.html#pathlib.Path.glob>`_. Unlike ``pathlib.Path.glob()``, this function ignores files and directlies that start with a dot. Also, this function returns a list of Path objects.
|
@@ -250,8 +286,9 @@ Example:
|
|
250
286
|
SRCFILES = glob("**/*.c")
|
251
287
|
|
252
288
|
.. py:function:: quote(s)
|
289
|
+
.. py:function:: q(s)
|
253
290
|
|
254
|
-
|
291
|
+
Convert ``s`` to string and quote for use as a shell command argument. This function is a wrapper around `shlex.quote() <https://docs.python.org/3/library/shlex.html#shlex.quote>`_.
|
255
292
|
|
256
293
|
:param s: The string to quote.
|
257
294
|
:type s: str
|
@@ -259,6 +296,17 @@ Example:
|
|
259
296
|
:return: The quoted string.
|
260
297
|
:rtype: str
|
261
298
|
|
299
|
+
.. py:function:: squote(*s)
|
300
|
+
.. py:function:: sq(*s)
|
301
|
+
|
302
|
+
Quote strings in ``s``. Each ``s`` is flattend.
|
303
|
+
|
304
|
+
:param s: The string to quote.
|
305
|
+
:type s: str | list
|
306
|
+
|
307
|
+
:return: The list of quoted strings.
|
308
|
+
:rtype: list[str]
|
309
|
+
|
262
310
|
.. py:class:: Path
|
263
311
|
|
264
312
|
A class representing file paths. This function is an alias for `pathlib.Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>`_.
|
@@ -52,16 +52,16 @@ Next, let's modify the ``Prodfile.py`` to output the file into an ``output`` dir
|
|
52
52
|
|
53
53
|
.. code-block:: python
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
55
|
+
output = Path("output") # We can use pathlib.Path without importing it
|
56
|
+
|
57
|
+
@rule(output / "hello.txt", depends=output) # hello now depends on output directory
|
58
|
+
def hello(target):
|
59
|
+
with open(target, "w") as f:
|
60
|
+
f.write("Hello, world!")
|
61
|
+
|
62
|
+
@rule(output)
|
63
|
+
def makedir(target):
|
64
|
+
os.makedirs(target)
|
65
65
|
|
66
66
|
In the modified ``Prodfile.py``, we have defined a rule to create the ``output`` directory and added a rule that makes the ``output/hello.txt`` file dependent on the ``output`` directory.
|
67
67
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "PyProd"
|
3
|
-
version = "0.
|
3
|
+
version = "0.3.0"
|
4
4
|
description = "PyProd: More Makeable than Make"
|
5
5
|
readme = "README.rst"
|
6
6
|
requires-python = ">=3.10"
|
@@ -40,4 +40,4 @@ asyncio_default_fixture_loop_scope="function"
|
|
40
40
|
|
41
41
|
[tool.ruff.lint]
|
42
42
|
select = ["E","F","W","B","N","T10","I"]
|
43
|
-
ignore = ["F401"]
|
43
|
+
ignore = ["F401", "B9"]
|
@@ -31,16 +31,15 @@ def make_pdf(target, src):
|
|
31
31
|
# Rebuilds when Python modules change
|
32
32
|
@rule(BUILD / "%.html", depends=(Path("%.md"), TEMPLATE, MODULES), uses=BUILD)
|
33
33
|
def make_html(target, src, template, *_):
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
target.write_text(html)
|
34
|
+
body = md_to_html(open(src).read())
|
35
|
+
html = open(template).read().format(body=body)
|
36
|
+
open(target, "w").write(html)
|
38
37
|
|
39
38
|
|
40
39
|
# create outputs directory
|
41
40
|
@rule(BUILD)
|
42
41
|
def builds(target):
|
43
|
-
|
42
|
+
os.makedirs(target)
|
44
43
|
|
45
44
|
|
46
45
|
def clean():
|
@@ -7,7 +7,8 @@ import boto3, botocore
|
|
7
7
|
from urllib.parse import urlparse
|
8
8
|
|
9
9
|
s3 = boto3.client("s3")
|
10
|
-
|
10
|
+
BUCKET = params.BUCKET or "TESTBUCKET" # Run pyprod with BUCKET=bucket-name
|
11
|
+
TARGET = f"s3://{BUCKET}/S3TEST.txt"
|
11
12
|
|
12
13
|
|
13
14
|
def parse_s3url(s3url):
|
@@ -0,0 +1,13 @@
|
|
1
|
+
output = Path("output") # We can use pathlib.Path without importing it
|
2
|
+
|
3
|
+
|
4
|
+
@rule(output / "hello.txt", depends=output) # hello now depends on output directory
|
5
|
+
def hello(target, *args):
|
6
|
+
# output_dir is not used
|
7
|
+
with open(target, "w") as f:
|
8
|
+
f.write("Hello, world!")
|
9
|
+
|
10
|
+
|
11
|
+
@rule(output)
|
12
|
+
def makedir(target):
|
13
|
+
os.makedirs(target)
|
@@ -33,7 +33,7 @@ class NoRuleToMakeTargetError(Exception):
|
|
33
33
|
pass
|
34
34
|
|
35
35
|
|
36
|
-
class
|
36
|
+
class RuleError(Exception):
|
37
37
|
pass
|
38
38
|
|
39
39
|
|
@@ -109,13 +109,11 @@ def glob(path, dir="."):
|
|
109
109
|
|
110
110
|
def rule_to_re(rule):
|
111
111
|
if not isinstance(rule, (str, Path)):
|
112
|
-
raise
|
112
|
+
raise RuleError(rule)
|
113
113
|
|
114
114
|
srule = str(rule)
|
115
115
|
srule = translate(srule)
|
116
116
|
srule = replace_pattern(srule, "(?P<stem>.*)", maxreplace=1)
|
117
|
-
if isinstance(rule, Path):
|
118
|
-
return Path(srule)
|
119
117
|
return srule
|
120
118
|
|
121
119
|
|
@@ -132,38 +130,97 @@ def replace_pattern(rule, replaceto, *, maxreplace=None):
|
|
132
130
|
if maxreplace is not None:
|
133
131
|
if n > maxreplace:
|
134
132
|
# contains multiple '%'
|
135
|
-
raise
|
133
|
+
raise RuleError(f"{s_rule} contains multiple '%'")
|
134
|
+
|
136
135
|
return replaceto
|
137
136
|
|
138
137
|
s_rule = re.sub("%%|%", f, s_rule)
|
139
|
-
if isinstance(rule, Path):
|
140
|
-
return Path(s_rule)
|
141
138
|
return s_rule
|
142
139
|
|
143
140
|
|
144
|
-
|
141
|
+
def _check_pattern_count(pattern):
|
142
|
+
"""Counts number of '%' in the pattern"""
|
143
|
+
matches = re.finditer(r"%%|%", pattern)
|
144
|
+
num = len([m for m in matches if len(m[0]) == 1])
|
145
|
+
if num > 1:
|
146
|
+
raise RuleError(f"{pattern}: Multiple '%' is not allowed")
|
147
|
+
return num
|
148
|
+
|
149
|
+
|
150
|
+
def _check_pattern(pattern):
|
151
|
+
matches = re.finditer(r"%%|%", pattern)
|
152
|
+
singles = [m for m in matches if len(m[0]) == 1]
|
153
|
+
if len(singles) > 1:
|
154
|
+
raise RuleError(f"{pattern}: Multiple '%' is not allowed")
|
155
|
+
if not len(singles):
|
156
|
+
raise RuleError(f"{pattern}: Pattern should contain a '%'.")
|
157
|
+
|
158
|
+
|
159
|
+
def _strip_dot(path):
|
160
|
+
if not path:
|
161
|
+
return path
|
162
|
+
path = Path(path) # ./aaa/ -> aaa
|
163
|
+
parts = path.parts
|
164
|
+
if ".." in parts:
|
165
|
+
raise RuleError(f"{path}: '..' directory is not allowed")
|
166
|
+
return str(path)
|
167
|
+
|
168
|
+
|
169
|
+
def _check_wildcard(path):
|
170
|
+
if "*" in path:
|
171
|
+
raise RuleError(f"{path}: '*' directory is not allowed")
|
172
|
+
|
173
|
+
|
145
174
|
class Rule:
|
146
|
-
targets:
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
175
|
+
def __init__(self, targets, pattern, depends, uses, builder=None):
|
176
|
+
self.targets = []
|
177
|
+
for target in flatten(targets or ()):
|
178
|
+
target = str(target)
|
179
|
+
_check_pattern_count(target)
|
180
|
+
target = _strip_dot(target)
|
181
|
+
target = rule_to_re(target)
|
182
|
+
self.targets.append(target)
|
183
|
+
|
184
|
+
self.first_target = None
|
185
|
+
for target in flatten(targets or ()):
|
186
|
+
target = str(target)
|
187
|
+
if not target:
|
188
|
+
continue
|
189
|
+
|
190
|
+
if "*" in target:
|
191
|
+
continue
|
192
|
+
|
193
|
+
if _check_pattern_count(target) == 0:
|
194
|
+
# not contain one %
|
195
|
+
self.first_target = target
|
196
|
+
break
|
197
|
+
|
198
|
+
if pattern:
|
199
|
+
pattern = _strip_dot(pattern)
|
200
|
+
if _check_pattern_count(pattern) != 1:
|
201
|
+
raise RuleError(f"{pattern}: Pattern should contain a '%'")
|
202
|
+
|
203
|
+
self.pattern = rule_to_re(pattern)
|
204
|
+
else:
|
205
|
+
self.pattern = None
|
206
|
+
|
207
|
+
self.depends = []
|
208
|
+
for depend in flatten(depends or ()):
|
209
|
+
depend = str(depend)
|
210
|
+
_check_pattern_count(depend)
|
211
|
+
_check_wildcard(depend)
|
212
|
+
depend = _strip_dot(depend)
|
213
|
+
self.depends.append(depend)
|
214
|
+
|
215
|
+
self.uses = []
|
216
|
+
for use in flatten(uses or ()):
|
217
|
+
use = str(use)
|
218
|
+
_check_pattern_count(use)
|
219
|
+
_check_wildcard(use)
|
220
|
+
use = _strip_dot(use)
|
221
|
+
self.uses.append(use)
|
222
|
+
|
223
|
+
self.builder = builder
|
167
224
|
|
168
225
|
def __call__(self, f):
|
169
226
|
self.builder = f
|
@@ -211,12 +268,15 @@ class Rules:
|
|
211
268
|
stem = d
|
212
269
|
elif dep.pattern:
|
213
270
|
m = re.fullmatch(str(dep.pattern), name)
|
214
|
-
|
215
|
-
|
216
|
-
stem = d
|
271
|
+
if m:
|
272
|
+
stem = m.groupdict().get("stem", None)
|
217
273
|
|
218
|
-
|
219
|
-
|
274
|
+
if stem is not None:
|
275
|
+
depends = [replace_pattern(r, stem) for r in dep.depends]
|
276
|
+
uses = [replace_pattern(r, stem) for r in dep.uses]
|
277
|
+
else:
|
278
|
+
depends = dep.depends[:]
|
279
|
+
uses = dep.uses[:]
|
220
280
|
|
221
281
|
yield depends, uses, dep
|
222
282
|
break
|
@@ -237,14 +297,8 @@ class Rules:
|
|
237
297
|
|
238
298
|
def select_first_target(self):
|
239
299
|
for dep in self.rules:
|
240
|
-
|
241
|
-
|
242
|
-
# pattern?
|
243
|
-
matches = re.finditer(r"%%|%", s_target)
|
244
|
-
if any((len(m[0]) == 1) for m in matches):
|
245
|
-
# has %
|
246
|
-
continue
|
247
|
-
return target
|
300
|
+
if dep.first_target:
|
301
|
+
return dep.first_target
|
248
302
|
|
249
303
|
def select_builder(self, name):
|
250
304
|
for depends, uses, dep in self.iter_rule(name):
|
@@ -356,6 +410,30 @@ class Envs:
|
|
356
410
|
return os.environ.get(name, default=default)
|
357
411
|
|
358
412
|
|
413
|
+
def read(filename):
|
414
|
+
with open(filename, "r") as f:
|
415
|
+
return f.read()
|
416
|
+
|
417
|
+
|
418
|
+
def write(filename, s, append=False):
|
419
|
+
mode = "a" if append else "w"
|
420
|
+
with open(filename, mode) as f:
|
421
|
+
f.write(s)
|
422
|
+
|
423
|
+
|
424
|
+
def quote(s):
|
425
|
+
return shlex.quote(str(s))
|
426
|
+
|
427
|
+
|
428
|
+
def squote(*s):
|
429
|
+
ret = [shlex.quote(str(x)) for x in flatten(s)]
|
430
|
+
return ret
|
431
|
+
|
432
|
+
|
433
|
+
def makedirs(path):
|
434
|
+
os.makedirs(path, exist_ok=True)
|
435
|
+
|
436
|
+
|
359
437
|
class Prod:
|
360
438
|
def __init__(self, modulefile, njobs=1, params=None):
|
361
439
|
self.modulefile = Path(modulefile)
|
@@ -372,18 +450,25 @@ class Prod:
|
|
372
450
|
|
373
451
|
def get_module_globals(self):
|
374
452
|
globals = {
|
453
|
+
"capture": capture,
|
454
|
+
"check": self.checkers.check,
|
455
|
+
"environ": Envs(),
|
456
|
+
"glob": glob,
|
457
|
+
"makedirs": makedirs,
|
458
|
+
"os": os,
|
459
|
+
"params": self.params,
|
375
460
|
"pip": pip,
|
461
|
+
"quote": quote,
|
462
|
+
"q": quote,
|
463
|
+
"squote": squote,
|
464
|
+
"sq": squote,
|
465
|
+
"read": read,
|
376
466
|
"rule": self.rules.rule,
|
377
|
-
"check": self.checkers.check,
|
378
|
-
"Path": Path,
|
379
467
|
"run": run,
|
380
|
-
"capture": capture,
|
381
|
-
"glob": glob,
|
382
|
-
"MAX_TS": MAX_TS,
|
383
|
-
"environ": Envs(),
|
384
468
|
"shutil": shutil,
|
385
|
-
"
|
386
|
-
"
|
469
|
+
"write": write,
|
470
|
+
"MAX_TS": MAX_TS,
|
471
|
+
"Path": Path,
|
387
472
|
}
|
388
473
|
return globals
|
389
474
|
|
@@ -432,13 +517,13 @@ class Prod:
|
|
432
517
|
if isinstance(obj, str | Path):
|
433
518
|
builds.append(obj)
|
434
519
|
elif isinstance(obj, Rule):
|
435
|
-
raise
|
520
|
+
raise RuleError(obj)
|
436
521
|
elif callable(obj):
|
437
522
|
self.built += 1
|
438
523
|
task = asyncio.create_task(self.run_in_executor(obj))
|
439
524
|
waitings.append(task)
|
440
525
|
else:
|
441
|
-
raise
|
526
|
+
raise RuleError(obj)
|
442
527
|
|
443
528
|
await self.build(builds)
|
444
529
|
await asyncio.gather(*waitings)
|
@@ -497,11 +582,11 @@ class Prod:
|
|
497
582
|
return Exists(name, True, ret)
|
498
583
|
|
499
584
|
async def run(self, name): # -> Any | int:
|
500
|
-
|
585
|
+
name = str(name)
|
501
586
|
|
502
|
-
self.rules.build_tree(
|
503
|
-
deps, uses = self.rules.get_dep_names(
|
504
|
-
selected = self.rules.select_builder(
|
587
|
+
self.rules.build_tree(name)
|
588
|
+
deps, uses = self.rules.get_dep_names(name)
|
589
|
+
selected = self.rules.select_builder(name)
|
505
590
|
if selected:
|
506
591
|
build_deps, build_uses, builder = selected
|
507
592
|
deps = deps + build_deps
|
@@ -513,19 +598,19 @@ class Prod:
|
|
513
598
|
if uses:
|
514
599
|
await self.build(uses)
|
515
600
|
|
516
|
-
exists = await self.is_exists(
|
601
|
+
exists = await self.is_exists(name)
|
517
602
|
|
518
603
|
if not exists.exists:
|
519
|
-
logger.debug("%r does not exists",
|
604
|
+
logger.debug("%r does not exists", name)
|
520
605
|
elif (ts >= MAX_TS) or (exists.ts < ts):
|
521
|
-
logger.debug("%r should be updated",
|
606
|
+
logger.debug("%r should be updated", name)
|
522
607
|
else:
|
523
|
-
logger.debug("%r already exists",
|
608
|
+
logger.debug("%r already exists", name)
|
524
609
|
|
525
610
|
if not exists.exists and not selected:
|
526
|
-
raise NoRuleToMakeTargetError(
|
611
|
+
raise NoRuleToMakeTargetError(name)
|
527
612
|
elif selected and ((not exists.exists) or (ts >= MAX_TS) or (exists.ts < ts)):
|
528
|
-
logger.warning("building: %r",
|
613
|
+
logger.warning("building: %r", name)
|
529
614
|
await self.run_in_executor(builder.builder, name, *build_deps)
|
530
615
|
self.built += 1
|
531
616
|
return MAX_TS
|
@@ -88,19 +88,19 @@ async def test_pattern(tmp_path):
|
|
88
88
|
src = """
|
89
89
|
@rule(target=("a.o", "b.o"), pattern=Path("%.o"), depends=Path("%.c"))
|
90
90
|
def build(target, src):
|
91
|
-
assert isinstance(target,
|
91
|
+
assert isinstance(target, str)
|
92
92
|
Path(target).write_text(str(target))
|
93
93
|
|
94
94
|
@rule(target=Path("%.c"))
|
95
95
|
def build_c(target):
|
96
|
-
assert isinstance(target,
|
96
|
+
assert isinstance(target, str)
|
97
97
|
Path(target).write_text(str(target))
|
98
98
|
|
99
99
|
@rule(Path("app.exe"), depends=(Path("a.o"), Path("b.o")))
|
100
100
|
def build_app(target, a, b):
|
101
|
-
assert isinstance(target,
|
102
|
-
assert isinstance(a,
|
103
|
-
assert isinstance(b,
|
101
|
+
assert isinstance(target, str)
|
102
|
+
assert isinstance(a, str)
|
103
|
+
assert isinstance(b, str)
|
104
104
|
Path(target).write_text(f"{target}, {a}, {b}")
|
105
105
|
|
106
106
|
all = Path("app.exe")
|
@@ -124,19 +124,19 @@ async def test_preserve_pathobj(tmp_path):
|
|
124
124
|
src = """
|
125
125
|
@rule(target=Path("%.o"), depends=Path("%.c"))
|
126
126
|
def build(target, src):
|
127
|
-
assert isinstance(target,
|
127
|
+
assert isinstance(target, str)
|
128
128
|
Path(target).write_text("a")
|
129
129
|
|
130
130
|
@rule(target=Path("%.c"))
|
131
131
|
def build_c(target):
|
132
|
-
assert isinstance(target,
|
132
|
+
assert isinstance(target, str)
|
133
133
|
Path(target).write_text(str(target))
|
134
134
|
|
135
135
|
@rule(Path("app.exe"), depends=Path("a.o"))
|
136
136
|
def build_app(target, src):
|
137
|
-
assert isinstance(target,
|
138
|
-
assert isinstance(src,
|
139
|
-
target.write_text("app.exe")
|
137
|
+
assert isinstance(target, str)
|
138
|
+
assert isinstance(src, str)
|
139
|
+
Path(target).write_text("app.exe")
|
140
140
|
|
141
141
|
all = Path("app.exe")
|
142
142
|
"""
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import re
|
2
|
+
|
1
3
|
import pytest
|
2
4
|
|
3
5
|
from pyprod import prod
|
@@ -91,20 +93,74 @@ def test_stem_escape():
|
|
91
93
|
|
92
94
|
|
93
95
|
def test_stem_error():
|
94
|
-
with pytest.raises(ValueError):
|
95
|
-
raise ValueError("s;dlf,")
|
96
|
-
|
97
96
|
rules = prod.Rules()
|
98
97
|
|
99
|
-
with pytest.raises(prod.
|
98
|
+
with pytest.raises(prod.RuleError):
|
100
99
|
|
101
100
|
@rules.rule(target="%.%", depends="%.c")
|
102
101
|
def f():
|
103
102
|
pass
|
104
103
|
|
105
|
-
@rules.rule(target="%.xxx", depends="
|
104
|
+
@rules.rule(target="%.xxx", depends="%")
|
106
105
|
def f():
|
107
106
|
pass
|
108
107
|
|
109
108
|
deps, _, _ = rules.select_builder("abc.xxx")
|
110
|
-
assert deps == ["abc
|
109
|
+
assert deps == ["abc"]
|
110
|
+
|
111
|
+
|
112
|
+
def test_validate_target():
|
113
|
+
with pytest.raises(prod.RuleError):
|
114
|
+
prod.Rule("%.%", "", "", "")
|
115
|
+
|
116
|
+
with pytest.raises(prod.RuleError):
|
117
|
+
prod.Rule("../aaa", "", "", "")
|
118
|
+
|
119
|
+
with pytest.raises(prod.RuleError):
|
120
|
+
rule = prod.Rule("../aaa/", "", "", "")
|
121
|
+
|
122
|
+
rule = prod.Rule("./aaa/", None, None, None)
|
123
|
+
assert re.fullmatch(rule.targets[0], "aaa")
|
124
|
+
|
125
|
+
|
126
|
+
def test_validate_pattern():
|
127
|
+
with pytest.raises(prod.RuleError):
|
128
|
+
prod.Rule("a.b", "%.%", "", "")
|
129
|
+
|
130
|
+
with pytest.raises(prod.RuleError):
|
131
|
+
prod.Rule("a.b", "../%.b", "", "")
|
132
|
+
|
133
|
+
with pytest.raises(prod.RuleError):
|
134
|
+
rule = prod.Rule("a.b", "../a.%", "", "")
|
135
|
+
|
136
|
+
with pytest.raises(prod.RuleError):
|
137
|
+
rule = prod.Rule("a.b", "a.b", "", "")
|
138
|
+
|
139
|
+
rule = prod.Rule("a.b", "./a.%", None, None)
|
140
|
+
assert re.fullmatch(rule.pattern, "a.b")
|
141
|
+
|
142
|
+
|
143
|
+
def test_validate_depends():
|
144
|
+
with pytest.raises(prod.RuleError):
|
145
|
+
prod.Rule("a.%", None, "%.%", "")
|
146
|
+
|
147
|
+
with pytest.raises(prod.RuleError):
|
148
|
+
prod.Rule("a.%", None, "*/x.y", "")
|
149
|
+
|
150
|
+
with pytest.raises(prod.RuleError):
|
151
|
+
prod.Rule("a.%", None, "../x.y", "")
|
152
|
+
|
153
|
+
prod.Rule("a.b", None, "x.y", "")
|
154
|
+
|
155
|
+
|
156
|
+
def test_validate_uses():
|
157
|
+
with pytest.raises(prod.RuleError):
|
158
|
+
prod.Rule("a.%", None, "", "%.%")
|
159
|
+
|
160
|
+
with pytest.raises(prod.RuleError):
|
161
|
+
prod.Rule("a.%", None, "", "*/x.y")
|
162
|
+
|
163
|
+
with pytest.raises(prod.RuleError):
|
164
|
+
prod.Rule("a.%", None, "", "../x.y")
|
165
|
+
|
166
|
+
prod.Rule("a.b", None, "x.y", "")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|