PyProd 0.2.0.post1__tar.gz → 0.3.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. {pyprod-0.2.0.post1 → pyprod-0.3.0}/PKG-INFO +1 -1
  2. {pyprod-0.2.0.post1 → pyprod-0.3.0}/docs/index.rst +2 -0
  3. {pyprod-0.2.0.post1 → pyprod-0.3.0}/docs/prodfile.rst +50 -2
  4. {pyprod-0.2.0.post1 → pyprod-0.3.0}/docs/quickstart.rst +10 -10
  5. pyprod-0.3.0/docs/releasenotes.rst +9 -0
  6. {pyprod-0.2.0.post1 → pyprod-0.3.0}/pyproject.toml +2 -2
  7. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/md-to-pdf/Prodfile.py +4 -5
  8. pyprod-0.2.0.post1/sample/s3files/PRODFILE.py → pyprod-0.3.0/sample/s3files/Prodfile.py +2 -1
  9. pyprod-0.3.0/sample/tutorial-1/Prodfile.py +4 -0
  10. pyprod-0.3.0/sample/tutorial-2/Prodfile.py +13 -0
  11. {pyprod-0.2.0.post1 → pyprod-0.3.0}/src/pyprod/prod.py +147 -62
  12. {pyprod-0.2.0.post1 → pyprod-0.3.0}/tests/test_prod.py +10 -10
  13. {pyprod-0.2.0.post1 → pyprod-0.3.0}/tests/test_prodfuncs.py +4 -0
  14. {pyprod-0.2.0.post1 → pyprod-0.3.0}/tests/test_rule.py +62 -6
  15. {pyprod-0.2.0.post1 → pyprod-0.3.0}/uv.lock +1 -1
  16. {pyprod-0.2.0.post1 → pyprod-0.3.0}/.github/workflows/publish.yml +0 -0
  17. {pyprod-0.2.0.post1 → pyprod-0.3.0}/.github/workflows/test.yml +0 -0
  18. {pyprod-0.2.0.post1 → pyprod-0.3.0}/.gitignore +0 -0
  19. {pyprod-0.2.0.post1 → pyprod-0.3.0}/.python-version +0 -0
  20. {pyprod-0.2.0.post1 → pyprod-0.3.0}/.readthedocs.yaml +0 -0
  21. {pyprod-0.2.0.post1 → pyprod-0.3.0}/LICENSE +0 -0
  22. {pyprod-0.2.0.post1 → pyprod-0.3.0}/README.rst +0 -0
  23. {pyprod-0.2.0.post1 → pyprod-0.3.0}/docs/Makefile +0 -0
  24. {pyprod-0.2.0.post1 → pyprod-0.3.0}/docs/commandline.rst +0 -0
  25. {pyprod-0.2.0.post1 → pyprod-0.3.0}/docs/conf.py +0 -0
  26. {pyprod-0.2.0.post1 → pyprod-0.3.0}/docs/make.bat +0 -0
  27. {pyprod-0.2.0.post1 → pyprod-0.3.0}/docs/pyprod2.png +0 -0
  28. {pyprod-0.2.0.post1 → pyprod-0.3.0}/docs/requirements.txt +0 -0
  29. {pyprod-0.2.0.post1 → pyprod-0.3.0}/pyprod.webp +0 -0
  30. {pyprod-0.2.0.post1 → pyprod-0.3.0}/pyprod2.png +0 -0
  31. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/build-c/Makefile +0 -0
  32. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/build-c/PRODFILE.py +0 -0
  33. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/build-c/hello.c +0 -0
  34. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/build-c/hello.h +0 -0
  35. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/build-c/main.c +0 -0
  36. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/generate-doc/.gitignore +0 -0
  37. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/generate-doc/PRODFILE.py +0 -0
  38. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/generate-doc/a.txt +0 -0
  39. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/generate-doc/b.txt +0 -0
  40. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/generate-doc/c.txt +0 -0
  41. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/generate-doc/inc1.txt +0 -0
  42. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/generate-doc/inc2.txt +0 -0
  43. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/md-to-pdf/doc.md +0 -0
  44. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/md-to-pdf/md_to_html.py +0 -0
  45. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/md-to-pdf/template.html +0 -0
  46. {pyprod-0.2.0.post1 → pyprod-0.3.0}/sample/s3files/S3TEST.txt +0 -0
  47. {pyprod-0.2.0.post1 → pyprod-0.3.0}/src/pyprod/__init__.py +0 -0
  48. {pyprod-0.2.0.post1 → pyprod-0.3.0}/src/pyprod/__main__.py +0 -0
  49. {pyprod-0.2.0.post1 → pyprod-0.3.0}/src/pyprod/main.py +0 -0
  50. {pyprod-0.2.0.post1 → pyprod-0.3.0}/src/pyprod/utils.py +0 -0
  51. {pyprod-0.2.0.post1 → pyprod-0.3.0}/src/pyprod/venv.py +0 -0
  52. {pyprod-0.2.0.post1 → pyprod-0.3.0}/tests/__init__.py +0 -0
  53. {pyprod-0.2.0.post1 → 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.2.0.post1
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/
@@ -35,6 +35,8 @@ Table of Contents
35
35
  quickstart
36
36
  prodfile
37
37
  commandline
38
+ releasenotes
39
+
38
40
 
39
41
 
40
42
 
@@ -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
- The first argument of the decorated function specifies the target file to be generated. The target can be a string or a Path object, depending on the value provided in the target parameter. 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.
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
- Quote a string 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>`_.
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
- 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
- target.mkdir()
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
 
@@ -0,0 +1,9 @@
1
+ Release Notes
2
+ ================
3
+
4
+ 0.3.0 (2025-01-03)
5
+ ------------------
6
+
7
+ - Arguments for build and check function are converted to string.
8
+ - Add built-in functions.
9
+ - Validate rule dependencies.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyProd"
3
- version = "0.2.0.post1"
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
- src = src.read_text()
35
- body = md_to_html(src)
36
- html = template.read_text().format(body=body)
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
- target.mkdir(parents=True)
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
- TARGET = f"s3://TESTBUCKET/S3TEST.txt"
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,4 @@
1
+ @rule("hello.txt")
2
+ def hello(target):
3
+ with open(target, "w") as f:
4
+ f.write("Hello, world!")
@@ -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 InvalidRuleError(Exception):
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 InvalidRuleError(rule)
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 InvalidRuleError(s_rule)
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
- @dataclass
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: list
147
- pattern: str | None
148
- depends: list
149
- uses: list | None
150
- builder: str | None
151
-
152
- orig_targets: list = field(init=False)
153
- orig_depends: list = field(init=False)
154
- orig_uses: list = field(init=False)
155
-
156
- def __post_init__(self):
157
- self.orig_targets = self.targets
158
- self.orig_depends = self.depends
159
- self.orig_uses = self.uses
160
-
161
- self.targets = [rule_to_re(r) for r in flatten(self.targets)]
162
- self.depends = list(flatten(self.depends))
163
- if self.pattern:
164
- self.pattern = rule_to_re(self.pattern)
165
- if self.uses:
166
- self.uses = list(flatten(self.uses))
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
- d = m.groupdict().get("stem", None)
215
- if d is not None:
216
- stem = d
271
+ if m:
272
+ stem = m.groupdict().get("stem", None)
217
273
 
218
- depends = [replace_pattern(r, stem) for r in dep.depends]
219
- uses = [replace_pattern(r, stem) for r in dep.uses]
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
- for target in dep.orig_targets:
241
- s_target = str(target)
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
- "quote": shlex.quote,
386
- "params": self.params,
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 InvalidRuleError(obj)
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 InvalidRuleError(obj)
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
- s_name = str(name)
585
+ name = str(name)
501
586
 
502
- self.rules.build_tree(s_name)
503
- deps, uses = self.rules.get_dep_names(s_name)
504
- selected = self.rules.select_builder(s_name)
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(s_name)
601
+ exists = await self.is_exists(name)
517
602
 
518
603
  if not exists.exists:
519
- logger.debug("%r does not exists", str(s_name))
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", str(s_name))
606
+ logger.debug("%r should be updated", name)
522
607
  else:
523
- logger.debug("%r already exists", str(s_name))
608
+ logger.debug("%r already exists", name)
524
609
 
525
610
  if not exists.exists and not selected:
526
- raise NoRuleToMakeTargetError(s_name)
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", s_name)
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, Path)
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, Path)
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, Path)
102
- assert isinstance(a, Path)
103
- assert isinstance(b, Path)
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, Path)
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, Path)
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, Path)
138
- assert isinstance(src, Path)
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
  """
@@ -47,3 +47,7 @@ def test_glob(tmp_path):
47
47
  d1 / "b.txt",
48
48
  d2 / "a.txt",
49
49
  }
50
+
51
+
52
+ def test_squote():
53
+ assert prod.squote("abc", ["12 3"]) == ["abc", "'12 3'"]
@@ -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.InvalidRuleError):
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.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", "")
@@ -278,7 +278,7 @@ wheels = [
278
278
 
279
279
  [[package]]
280
280
  name = "pyprod"
281
- version = "0.1.0"
281
+ version = "0.2.0.post1"
282
282
  source = { editable = "." }
283
283
 
284
284
  [package.dev-dependencies]
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