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 +1 -0
- pyprod/main.py +23 -2
- pyprod/prod.py +101 -46
- pyprod/utils.py +3 -1
- pyprod/venv.py +10 -1
- {pyprod-0.4.0.dist-info → pyprod-0.6.0.dist-info}/METADATA +2 -1
- pyprod-0.6.0.dist-info/RECORD +11 -0
- pyprod-0.4.0.dist-info/RECORD +0 -11
- {pyprod-0.4.0.dist-info → pyprod-0.6.0.dist-info}/WHEEL +0 -0
- {pyprod-0.4.0.dist-info → pyprod-0.6.0.dist-info}/entry_points.txt +0 -0
- {pyprod-0.4.0.dist-info → pyprod-0.6.0.dist-info}/licenses/LICENSE +0 -0
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: '
|
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
|
-
|
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(
|
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,
|
249
|
-
super().__init__((), pattern=None, depends=
|
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.
|
253
|
-
|
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,
|
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,
|
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,
|
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,
|
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=
|
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
|
-
|
591
|
-
tasks.append((dep,
|
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
|
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] =
|
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
|
-
|
696
|
+
tasks = []
|
649
697
|
if deps:
|
650
|
-
|
698
|
+
deps_task = asyncio.create_task(self.schedule(deps))
|
699
|
+
tasks.append(deps_task)
|
651
700
|
if uses:
|
652
|
-
|
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
|
-
[
|
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.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,,
|
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
|