PyProd 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyprod/__init__.py CHANGED
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
pyprod/main.py CHANGED
@@ -22,7 +22,7 @@ parser.add_argument(
22
22
  )
23
23
 
24
24
  parser.add_argument(
25
- "-f", "--file", help="Use FILE as the Prodfile (default: 'PRODFILE.py')"
25
+ "-f", "--file", help="Use FILE as the Prodfile (default: 'Prodfile.py')"
26
26
  )
27
27
 
28
28
  parser.add_argument(
@@ -36,6 +36,15 @@ parser.add_argument(
36
36
  parser.add_argument(
37
37
  "-r", "--rebuild", dest="rebuild", action="store_true", help="Rebuild all"
38
38
  )
39
+
40
+ parser.add_argument(
41
+ "-g",
42
+ "--use-git",
43
+ dest="use_git",
44
+ action="store_true",
45
+ help="Get file timestamps from Git",
46
+ )
47
+
39
48
  parser.add_argument(
40
49
  "-v",
41
50
  dest="verbose",
@@ -108,7 +117,6 @@ def main():
108
117
  try:
109
118
  # load module
110
119
  prod = pyprod.prod.Prod(mod, args.job, params)
111
-
112
120
  # select targets
113
121
  if not targets:
114
122
  target = prod.get_default_target()
pyprod/prod.py CHANGED
@@ -15,8 +15,9 @@ from collections import defaultdict
15
15
  from collections.abc import Collection
16
16
  from dataclasses import dataclass, field
17
17
  from fnmatch import fnmatch, translate
18
+ from functools import wraps
18
19
  from pathlib import Path
19
-
20
+ import dateutil.parser
20
21
  import pyprod
21
22
 
22
23
  from .utils import flatten, unique_list
@@ -103,9 +104,10 @@ def capture(*args, echo=True, cwd=None, check=True, text=True, shell=None):
103
104
 
104
105
  def glob(path, dir="."):
105
106
  ret = []
106
- for c in Path(dir).glob(path):
107
+ root = Path(dir)
108
+ for c in root.glob(path):
107
109
  # ignore dot files
108
- if any(p.startswith(".") for p in c.parts):
110
+ if any((p not in (".", "..")) and p.startswith(".") for p in c.parts):
109
111
  continue
110
112
  ret.append(c)
111
113
  return ret
@@ -175,6 +177,8 @@ def _name_to_str(name):
175
177
  return str(name)
176
178
  case str():
177
179
  return name
180
+ case _:
181
+ raise ValueError(f"Invalid dependency name: {name}")
178
182
 
179
183
  return name
180
184
 
@@ -182,6 +186,7 @@ def _name_to_str(name):
182
186
  class Rule:
183
187
  def __init__(self, targets, pattern=None, depends=(), uses=(), builder=None):
184
188
  self.targets = []
189
+ self.default = False
185
190
  self.first_target = None
186
191
  if targets:
187
192
  for target in flatten(targets):
@@ -239,18 +244,23 @@ class _TaskFunc:
239
244
  return self.f(*args, **kwargs)
240
245
 
241
246
 
242
- def default_builder(self, *args, **kwargs):
247
+ def default_builder(*args, **kwargs):
243
248
  # default builder
244
249
  pass
245
250
 
246
251
 
247
252
  class Task(Rule):
248
- def __init__(self, name, depends, uses, func=None):
249
- super().__init__((), pattern=None, depends=depends, uses=uses, builder=func)
250
- self.name = _name_to_str(name)
253
+ def __init__(self, name, uses, default, func=None):
254
+ super().__init__((), pattern=None, depends=(), uses=uses, builder=func)
251
255
  if name:
252
- self.targets = [name]
253
- self.first_target = self.name
256
+ self.name = _name_to_str(name)
257
+ if name:
258
+ self.targets = [name]
259
+ self.first_target = self.name
260
+ else:
261
+ self.name = None
262
+
263
+ self.default = default
254
264
  if func:
255
265
  self._set_funcname(func)
256
266
  if not self.builder:
@@ -292,10 +302,10 @@ class Rules:
292
302
  self.rules.append(dep)
293
303
  return dep
294
304
 
295
- def add_task(self, name=None, depends=(), uses=(), func=None):
305
+ def add_task(self, name=None, uses=(), default=False, func=None):
296
306
  if self.frozen:
297
307
  raise RuntimeError("No new rule can be added after initialization")
298
- dep = Task(name, depends, uses, func)
308
+ dep = Task(name, uses, default, func)
299
309
  self.rules.append(dep)
300
310
  return dep
301
311
 
@@ -306,12 +316,12 @@ class Rules:
306
316
  dep = self.add_rule([targets], pattern, depends, uses, None)
307
317
  return dep
308
318
 
309
- def task(self, func=None, *, name=None, depends=(), uses=()):
319
+ def task(self, func=None, *, name=None, uses=(), default=False):
310
320
  if func:
311
321
  if not callable(func):
312
322
  raise ValueError(f"{func} is not callable")
313
323
 
314
- dep = self.add_task(name, depends, uses, func)
324
+ dep = self.add_task(name, uses, default, func)
315
325
  return dep
316
326
 
317
327
  def iter_rule(self, name):
@@ -353,10 +363,16 @@ class Rules:
353
363
  return unique_list(ret_depends), unique_list(ret_uses)
354
364
 
355
365
  def select_first_target(self):
366
+ first = None
356
367
  for dep in self.rules:
368
+ if dep.default and (not first):
369
+ first = dep.name
370
+
357
371
  if dep.first_target:
358
372
  return dep.first_target
359
373
 
374
+ return first
375
+
360
376
  def select_builder(self, name):
361
377
  for depends, uses, dep in self.iter_rule(name):
362
378
  if not dep.builder:
@@ -419,10 +435,6 @@ class Checkers:
419
435
  MAX_TS = 1 << 63
420
436
 
421
437
 
422
- def is_file_exists(name):
423
- return os.path.getmtime(name)
424
-
425
-
426
438
  class Exists:
427
439
  def __init__(self, name, exists, ts=None):
428
440
  self.name = name
@@ -506,10 +518,12 @@ class Prod:
506
518
  self.rules = Rules()
507
519
  self.checkers = Checkers()
508
520
  if njobs > 1:
509
- self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=njobs)
521
+ self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=100)
510
522
  else:
511
523
  self.executor = None
512
524
  self.params = Params(params)
525
+ self.use_git_timestamp = pyprod.args.use_git
526
+
513
527
  self.buildings = {}
514
528
  self.module = None
515
529
  if self.modulefile:
@@ -536,6 +550,7 @@ class Prod:
536
550
  "run": run,
537
551
  "shutil": shutil,
538
552
  "task": self.rules.task,
553
+ "use_git": self.use_git,
539
554
  "write": write,
540
555
  "MAX_TS": MAX_TS,
541
556
  "Path": Path,
@@ -567,6 +582,52 @@ class Prod:
567
582
 
568
583
  return ret
569
584
 
585
+ def get_file_mtime(self, name):
586
+ return os.path.getmtime(name)
587
+
588
+ def get_file_mtime_git(self, name):
589
+ ret = subprocess.check_output(
590
+ ["git", "log", "-1", "--format=%ai", "--", name], text=True
591
+ ).strip()
592
+ if not ret:
593
+ raise FileNotFoundError(f"{name} did not match any file in git")
594
+
595
+ # 2025-01-17 00:05:48 +0900
596
+ return dateutil.parser.parse(ret)
597
+
598
+ async def is_exists(self, name):
599
+ checker = self.checkers.get_checker(name)
600
+ try:
601
+ if checker:
602
+ ret = await self.run_in_executor(checker, name)
603
+ elif self.use_git_timestamp:
604
+ ret = await self.run_in_executor(self.get_file_mtime_git, name)
605
+ else:
606
+ ret = await self.run_in_executor(self.get_file_mtime, name)
607
+ except FileNotFoundError:
608
+ ret = False
609
+
610
+ if isinstance(ret, FileNotFoundError):
611
+ ret = False
612
+
613
+ if not ret:
614
+ return Exists(name, False)
615
+ if isinstance(ret, datetime.datetime):
616
+ ret = ret.timestamp()
617
+ if ret < 0:
618
+ ret = MAX_TS
619
+ return Exists(name, True, ret)
620
+
621
+ def build(self, *deps):
622
+ children = []
623
+ for elem in deps:
624
+ child = [_name_to_str(name) for name in flatten(elem)]
625
+ children.append(child)
626
+ self.deps[0:0] = children
627
+
628
+ def use_git(self, use):
629
+ self.use_git_timestamp = use
630
+
570
631
  def get_default_target(self):
571
632
  return self.rules.select_first_target()
572
633
 
@@ -581,24 +642,26 @@ class Prod:
581
642
  return self.built
582
643
 
583
644
  async def schedule(self, deps):
645
+ deps = list(flatten(deps))
584
646
  tasks = []
585
647
  waits = []
586
648
  for dep in deps:
587
649
  if dep not in self.buildings:
588
650
  ev = asyncio.Event()
589
651
  self.buildings[dep] = ev
590
- task = self.run(dep)
591
- tasks.append((dep, task))
652
+ coro = self.run(dep)
653
+ tasks.append((dep, coro))
592
654
  waits.append(ev)
593
655
  else:
594
656
  obj = self.buildings[dep]
595
657
  if isinstance(obj, asyncio.Event):
596
658
  waits.append(obj)
597
659
 
598
- for dep, task in tasks:
660
+ results = await asyncio.gather(*(coro for _, coro in tasks))
661
+ for ret, (dep, _) in zip(results, tasks):
599
662
  ev = self.buildings[dep]
600
663
  try:
601
- self.buildings[dep] = await task
664
+ self.buildings[dep] = ret
602
665
  finally:
603
666
  ev.set()
604
667
 
@@ -614,27 +677,6 @@ class Prod:
614
677
  return max(ts)
615
678
  return 0
616
679
 
617
- async def is_exists(self, name):
618
- checker = self.checkers.get_checker(name)
619
- try:
620
- if checker:
621
- ret = await self.run_in_executor(checker, name)
622
- else:
623
- ret = await self.run_in_executor(is_file_exists, name)
624
- except FileNotFoundError:
625
- ret = False
626
-
627
- if not ret:
628
- return Exists(name, False)
629
- if isinstance(ret, datetime.datetime):
630
- ret = ret.timestamp()
631
- if ret < 0:
632
- ret = MAX_TS
633
- return Exists(name, True, ret)
634
-
635
- def build(self, *deps):
636
- self.deps[0:0] = [_name_to_str(name) for name in flatten(deps)]
637
-
638
680
  async def run(self, name): # -> Any | int:
639
681
  name = _name_to_str(name)
640
682
  self.rules.build_tree(name)
@@ -645,11 +687,18 @@ class Prod:
645
687
  deps = deps + build_deps
646
688
  uses = uses + build_uses
647
689
 
648
- ts = 0
690
+ tasks = []
649
691
  if deps:
650
- ts = await self.schedule(deps)
692
+ deps_task = asyncio.create_task(self.schedule(deps))
693
+ tasks.append(deps_task)
651
694
  if uses:
652
- await self.schedule(uses)
695
+ uses_task = self.schedule(uses)
696
+ tasks.append(uses_task)
697
+
698
+ await asyncio.gather(*tasks)
699
+ ts = 0
700
+ if deps:
701
+ ts = deps_task.result()
653
702
 
654
703
  if selected and isinstance(builder, Task):
655
704
  self.built += 1
pyprod/utils.py CHANGED
@@ -1,13 +1,15 @@
1
1
  from collections.abc import Iterable
2
2
 
3
3
 
4
- def flatten(seq):
4
+ def flatten(seq, ignore_none=True):
5
5
  if isinstance(seq, str) or (not isinstance(seq, Iterable)):
6
6
  yield seq
7
7
  return
8
8
 
9
9
  for item in seq:
10
10
  if isinstance(item, str) or (not isinstance(item, Iterable)):
11
+ if ignore_none and (item is None):
12
+ continue
11
13
  yield item
12
14
  else:
13
15
  yield from flatten(item)
pyprod/venv.py CHANGED
@@ -42,6 +42,15 @@ def pip(*args):
42
42
  makevenv(pyprod.modulefile)
43
43
  args = flatten(args)
44
44
  subprocess.run(
45
- [venvdir / "bin/python", "-m", "pip", "--no-input", "install", *args],
45
+ [
46
+ venvdir / "bin/python",
47
+ "-m",
48
+ "pip",
49
+ "--disable-pip-version-check",
50
+ "--no-input",
51
+ "install",
52
+ "-q",
53
+ *args,
54
+ ],
46
55
  check=True,
47
56
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyProd
3
- Version: 0.4.0
3
+ Version: 0.5.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/
@@ -12,6 +12,7 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Topic :: Software Development :: Build Tools
14
14
  Requires-Python: >=3.10
15
+ Requires-Dist: python-dateutil
15
16
  Description-Content-Type: text/x-rst
16
17
 
17
18
  PyProd - More Makeable than Make
@@ -0,0 +1,11 @@
1
+ pyprod/__init__.py,sha256=LBK46heutvn3KmsCrKIYu8RQikbfnjZaj2xFrXaeCzQ,22
2
+ pyprod/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
3
+ pyprod/main.py,sha256=O1tkuZzZ7nuoI2DjFyJlKn-2YYn-UL-jsgrxb5EvmVQ,2945
4
+ pyprod/prod.py,sha256=Tdhr3cTnJv3m61157H48WNuFhvQGJ8lZuaSD6R9XspM,20000
5
+ pyprod/utils.py,sha256=6bA06MtxvzcEArAozeJVMgCvoTT185OPEGypM1jjoG0,481
6
+ pyprod/venv.py,sha256=ZNMtHDBdC-eNFJE0-GxDlh6tlGy5Y-2m1r86SqxJJR0,1229
7
+ pyprod-0.5.0.dist-info/METADATA,sha256=q28RfZ10Oodv3EHGo9Fa1x5U42cihFb9XTV-J9uaNFc,2683
8
+ pyprod-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ pyprod-0.5.0.dist-info/entry_points.txt,sha256=zFycf8BYSMRDTiI0jftmcvtkf9XM4MZ4BL3JaIer_ZM,44
10
+ pyprod-0.5.0.dist-info/licenses/LICENSE,sha256=OtPgwnlLrsVEYPnTraun5AqftAT5vUv4rIan-qYj7nE,1071
11
+ pyprod-0.5.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- pyprod/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- pyprod/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
3
- pyprod/main.py,sha256=w8xm7Dz4eXF6D6FhZuxlPfU24KpnXciJLuAMT6_y3zI,2808
4
- pyprod/prod.py,sha256=Vu376JyNqiieCMy8jVvBml3TAf32Pq4XxSdpC4bBdgY,18424
5
- pyprod/utils.py,sha256=hvqeZhXyVAsHAdPUG6LhBOs2dMVXuDUWBYiIPBcsoKw,391
6
- pyprod/venv.py,sha256=lRTYxvtX876FQ9-bTYmsOHYV3nuMkKHO5hTLog0iHro,1085
7
- pyprod-0.4.0.dist-info/METADATA,sha256=zkpPbp07U627hgqWE6yDYGe_dZEDQTCiE3FidIfQ5mM,2652
8
- pyprod-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- pyprod-0.4.0.dist-info/entry_points.txt,sha256=zFycf8BYSMRDTiI0jftmcvtkf9XM4MZ4BL3JaIer_ZM,44
10
- pyprod-0.4.0.dist-info/licenses/LICENSE,sha256=OtPgwnlLrsVEYPnTraun5AqftAT5vUv4rIan-qYj7nE,1071
11
- pyprod-0.4.0.dist-info/RECORD,,
File without changes