PyProd 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
pyprod/prod.py CHANGED
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyProd
3
- Version: 0.2.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://example.com/pyprod-docs>`_.
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,11 +1,11 @@
1
1
  pyprod/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  pyprod/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
3
3
  pyprod/main.py,sha256=nS0mcLy3Ix7ydnX1Pu5g7ceLzhh6DndwJWO5sjO_qWg,2580
4
- pyprod/prod.py,sha256=poLehE5Gs_vEZvKrSdMicF016tQeaMMAnFc76ssPQQY,14586
4
+ pyprod/prod.py,sha256=r2e-JSHiTw2SOcv-tYFx4aRpVkQbOb5j9vEVrjT0meE,16694
5
5
  pyprod/utils.py,sha256=oiiUkSbeqTazbtJ6gz7ZKqG1OvAeV-nV9u_3Y0DCOOM,401
6
6
  pyprod/venv.py,sha256=_riw56YQvUOSd55u_1m9ElsqPdjM5qVvIZP6dr9Fzt4,1051
7
- pyprod-0.2.0.dist-info/METADATA,sha256=eNMPeqzwuyEaK-fE7bWBkdkJ2DwThKJPuFxh5h84mAs,2468
8
- pyprod-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- pyprod-0.2.0.dist-info/entry_points.txt,sha256=zFycf8BYSMRDTiI0jftmcvtkf9XM4MZ4BL3JaIer_ZM,44
10
- pyprod-0.2.0.dist-info/licenses/LICENSE,sha256=OtPgwnlLrsVEYPnTraun5AqftAT5vUv4rIan-qYj7nE,1071
11
- pyprod-0.2.0.dist-info/RECORD,,
7
+ pyprod-0.3.0.dist-info/METADATA,sha256=t9TMYeBveYE1LFZNpcYJAtOdUo7gPARFHmIZ9p0bnag,2477
8
+ pyprod-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ pyprod-0.3.0.dist-info/entry_points.txt,sha256=zFycf8BYSMRDTiI0jftmcvtkf9XM4MZ4BL3JaIer_ZM,44
10
+ pyprod-0.3.0.dist-info/licenses/LICENSE,sha256=OtPgwnlLrsVEYPnTraun5AqftAT5vUv4rIan-qYj7nE,1071
11
+ pyprod-0.3.0.dist-info/RECORD,,
File without changes