PyProd 0.4.0__py3-none-any.whl → 0.6.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.6.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",
@@ -44,6 +53,15 @@ parser.add_argument(
44
53
  help="Increase verbosity level (default: 0)",
45
54
  )
46
55
 
56
+ parser.add_argument(
57
+ "-V",
58
+ "--version",
59
+ dest="version",
60
+ action="store_true",
61
+ default=0,
62
+ help="Show version",
63
+ )
64
+
47
65
 
48
66
  parser.add_argument("targets", nargs="*", help="Build targets")
49
67
 
@@ -65,6 +83,10 @@ def init_args(args=None):
65
83
 
66
84
  def main():
67
85
  args = init_args()
86
+ if args.version:
87
+ print(f"PyProd {pyprod.__version__}")
88
+ sys.exit(0)
89
+
68
90
  pyprod.verbose = args.verbose
69
91
  chdir = args.directory
70
92
  if chdir:
@@ -108,7 +130,6 @@ def main():
108
130
  try:
109
131
  # load module
110
132
  prod = pyprod.prod.Prod(mod, args.job, params)
111
-
112
133
  # select targets
113
134
  if not targets:
114
135
  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):
@@ -211,6 +216,9 @@ class Rule:
211
216
 
212
217
  self.depends = []
213
218
  for depend in flatten(depends or ()):
219
+ if not depend:
220
+ continue
221
+
214
222
  depend = _name_to_str(depend)
215
223
  _check_pattern_count(depend)
216
224
  _check_wildcard(depend)
@@ -218,6 +226,9 @@ class Rule:
218
226
 
219
227
  self.uses = []
220
228
  for use in flatten(uses or ()):
229
+ if not use:
230
+ continue
231
+
221
232
  use = _name_to_str(use)
222
233
  _check_pattern_count(use)
223
234
  _check_wildcard(use)
@@ -239,18 +250,23 @@ class _TaskFunc:
239
250
  return self.f(*args, **kwargs)
240
251
 
241
252
 
242
- def default_builder(self, *args, **kwargs):
253
+ def default_builder(*args, **kwargs):
243
254
  # default builder
244
255
  pass
245
256
 
246
257
 
247
258
  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)
259
+ def __init__(self, name, uses, default, func=None):
260
+ super().__init__((), pattern=None, depends=(), uses=uses, builder=func)
251
261
  if name:
252
- self.targets = [name]
253
- self.first_target = self.name
262
+ self.name = _name_to_str(name)
263
+ if name:
264
+ self.targets = [name]
265
+ self.first_target = self.name
266
+ else:
267
+ self.name = None
268
+
269
+ self.default = default
254
270
  if func:
255
271
  self._set_funcname(func)
256
272
  if not self.builder:
@@ -292,10 +308,10 @@ class Rules:
292
308
  self.rules.append(dep)
293
309
  return dep
294
310
 
295
- def add_task(self, name=None, depends=(), uses=(), func=None):
311
+ def add_task(self, name=None, uses=(), default=False, func=None):
296
312
  if self.frozen:
297
313
  raise RuntimeError("No new rule can be added after initialization")
298
- dep = Task(name, depends, uses, func)
314
+ dep = Task(name, uses, default, func)
299
315
  self.rules.append(dep)
300
316
  return dep
301
317
 
@@ -306,12 +322,12 @@ class Rules:
306
322
  dep = self.add_rule([targets], pattern, depends, uses, None)
307
323
  return dep
308
324
 
309
- def task(self, func=None, *, name=None, depends=(), uses=()):
325
+ def task(self, func=None, *, name=None, uses=(), default=False):
310
326
  if func:
311
327
  if not callable(func):
312
328
  raise ValueError(f"{func} is not callable")
313
329
 
314
- dep = self.add_task(name, depends, uses, func)
330
+ dep = self.add_task(name, uses, default, func)
315
331
  return dep
316
332
 
317
333
  def iter_rule(self, name):
@@ -353,10 +369,16 @@ class Rules:
353
369
  return unique_list(ret_depends), unique_list(ret_uses)
354
370
 
355
371
  def select_first_target(self):
372
+ first = None
356
373
  for dep in self.rules:
374
+ if dep.default and (not first):
375
+ first = dep.name
376
+
357
377
  if dep.first_target:
358
378
  return dep.first_target
359
379
 
380
+ return first
381
+
360
382
  def select_builder(self, name):
361
383
  for depends, uses, dep in self.iter_rule(name):
362
384
  if not dep.builder:
@@ -419,10 +441,6 @@ class Checkers:
419
441
  MAX_TS = 1 << 63
420
442
 
421
443
 
422
- def is_file_exists(name):
423
- return os.path.getmtime(name)
424
-
425
-
426
444
  class Exists:
427
445
  def __init__(self, name, exists, ts=None):
428
446
  self.name = name
@@ -506,10 +524,12 @@ class Prod:
506
524
  self.rules = Rules()
507
525
  self.checkers = Checkers()
508
526
  if njobs > 1:
509
- self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=njobs)
527
+ self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=100)
510
528
  else:
511
529
  self.executor = None
512
530
  self.params = Params(params)
531
+ self.use_git_timestamp = pyprod.args.use_git
532
+
513
533
  self.buildings = {}
514
534
  self.module = None
515
535
  if self.modulefile:
@@ -536,6 +556,7 @@ class Prod:
536
556
  "run": run,
537
557
  "shutil": shutil,
538
558
  "task": self.rules.task,
559
+ "use_git": self.use_git,
539
560
  "write": write,
540
561
  "MAX_TS": MAX_TS,
541
562
  "Path": Path,
@@ -567,6 +588,52 @@ class Prod:
567
588
 
568
589
  return ret
569
590
 
591
+ def get_file_mtime(self, name):
592
+ return os.path.getmtime(name)
593
+
594
+ def get_file_mtime_git(self, name):
595
+ ret = subprocess.check_output(
596
+ ["git", "log", "-1", "--format=%ai", "--", name], text=True
597
+ ).strip()
598
+ if not ret:
599
+ raise FileNotFoundError(f"{name} did not match any file in git")
600
+
601
+ # 2025-01-17 00:05:48 +0900
602
+ return dateutil.parser.parse(ret)
603
+
604
+ async def is_exists(self, name):
605
+ checker = self.checkers.get_checker(name)
606
+ try:
607
+ if checker:
608
+ ret = await self.run_in_executor(checker, name)
609
+ elif self.use_git_timestamp:
610
+ ret = await self.run_in_executor(self.get_file_mtime_git, name)
611
+ else:
612
+ ret = await self.run_in_executor(self.get_file_mtime, name)
613
+ except FileNotFoundError:
614
+ ret = False
615
+
616
+ if isinstance(ret, FileNotFoundError):
617
+ ret = False
618
+
619
+ if not ret:
620
+ return Exists(name, False)
621
+ if isinstance(ret, datetime.datetime):
622
+ ret = ret.timestamp()
623
+ if ret < 0:
624
+ ret = MAX_TS
625
+ return Exists(name, True, ret)
626
+
627
+ def build(self, *deps):
628
+ children = []
629
+ for elem in deps:
630
+ child = [_name_to_str(name) for name in flatten(elem)]
631
+ children.append(child)
632
+ self.deps[0:0] = children
633
+
634
+ def use_git(self, use):
635
+ self.use_git_timestamp = use
636
+
570
637
  def get_default_target(self):
571
638
  return self.rules.select_first_target()
572
639
 
@@ -581,24 +648,26 @@ class Prod:
581
648
  return self.built
582
649
 
583
650
  async def schedule(self, deps):
651
+ deps = list(flatten(deps))
584
652
  tasks = []
585
653
  waits = []
586
654
  for dep in deps:
587
655
  if dep not in self.buildings:
588
656
  ev = asyncio.Event()
589
657
  self.buildings[dep] = ev
590
- task = self.run(dep)
591
- tasks.append((dep, task))
658
+ coro = self.run(dep)
659
+ tasks.append((dep, coro))
592
660
  waits.append(ev)
593
661
  else:
594
662
  obj = self.buildings[dep]
595
663
  if isinstance(obj, asyncio.Event):
596
664
  waits.append(obj)
597
665
 
598
- for dep, task in tasks:
666
+ results = await asyncio.gather(*(coro for _, coro in tasks))
667
+ for ret, (dep, _) in zip(results, tasks):
599
668
  ev = self.buildings[dep]
600
669
  try:
601
- self.buildings[dep] = await task
670
+ self.buildings[dep] = ret
602
671
  finally:
603
672
  ev.set()
604
673
 
@@ -614,27 +683,6 @@ class Prod:
614
683
  return max(ts)
615
684
  return 0
616
685
 
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
686
  async def run(self, name): # -> Any | int:
639
687
  name = _name_to_str(name)
640
688
  self.rules.build_tree(name)
@@ -645,11 +693,18 @@ class Prod:
645
693
  deps = deps + build_deps
646
694
  uses = uses + build_uses
647
695
 
648
- ts = 0
696
+ tasks = []
649
697
  if deps:
650
- ts = await self.schedule(deps)
698
+ deps_task = asyncio.create_task(self.schedule(deps))
699
+ tasks.append(deps_task)
651
700
  if uses:
652
- await self.schedule(uses)
701
+ uses_task = self.schedule(uses)
702
+ tasks.append(uses_task)
703
+
704
+ await asyncio.gather(*tasks)
705
+ ts = 0
706
+ if deps:
707
+ ts = deps_task.result()
653
708
 
654
709
  if selected and isinstance(builder, Task):
655
710
  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.6.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=cID1jLnC_vj48GgMN6Yb1FA3JsQ95zNmCHmRYE8TFhY,22
2
+ pyprod/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
3
+ pyprod/main.py,sha256=SWMmGVnVALwgENHmFfrTnYJcNyjWjw_qSO3o4aGre4E,3169
4
+ pyprod/prod.py,sha256=Msuqe914lblxBAx_bP6_-P2ZCHrYyn-9qplMWvkg45s,20103
5
+ pyprod/utils.py,sha256=6bA06MtxvzcEArAozeJVMgCvoTT185OPEGypM1jjoG0,481
6
+ pyprod/venv.py,sha256=ZNMtHDBdC-eNFJE0-GxDlh6tlGy5Y-2m1r86SqxJJR0,1229
7
+ pyprod-0.6.0.dist-info/METADATA,sha256=MNlfNV0nd0ikLscGAQFp_r8B5fsG4Xe-d7pS4Pyj-cE,2683
8
+ pyprod-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ pyprod-0.6.0.dist-info/entry_points.txt,sha256=zFycf8BYSMRDTiI0jftmcvtkf9XM4MZ4BL3JaIer_ZM,44
10
+ pyprod-0.6.0.dist-info/licenses/LICENSE,sha256=OtPgwnlLrsVEYPnTraun5AqftAT5vUv4rIan-qYj7nE,1071
11
+ pyprod-0.6.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