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 +1 -0
- pyprod/main.py +10 -2
- pyprod/prod.py +95 -46
- pyprod/utils.py +3 -1
- pyprod/venv.py +10 -1
- {pyprod-0.4.0.dist-info → pyprod-0.5.0.dist-info}/METADATA +2 -1
- pyprod-0.5.0.dist-info/RECORD +11 -0
- pyprod-0.4.0.dist-info/RECORD +0 -11
- {pyprod-0.4.0.dist-info → pyprod-0.5.0.dist-info}/WHEEL +0 -0
- {pyprod-0.4.0.dist-info → pyprod-0.5.0.dist-info}/entry_points.txt +0 -0
- {pyprod-0.4.0.dist-info → pyprod-0.5.0.dist-info}/licenses/LICENSE +0 -0
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: '
|
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
|
-
|
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(
|
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,
|
249
|
-
super().__init__((), pattern=None, depends=
|
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.
|
253
|
-
|
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,
|
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,
|
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,
|
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,
|
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=
|
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
|
-
|
591
|
-
tasks.append((dep,
|
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
|
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] =
|
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
|
-
|
690
|
+
tasks = []
|
649
691
|
if deps:
|
650
|
-
|
692
|
+
deps_task = asyncio.create_task(self.schedule(deps))
|
693
|
+
tasks.append(deps_task)
|
651
694
|
if uses:
|
652
|
-
|
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
|
-
[
|
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.
|
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,,
|
pyprod-0.4.0.dist-info/RECORD
DELETED
@@ -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
|
File without changes
|
File without changes
|