PyProd 0.4.0__tar.gz → 0.5.0__tar.gz

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.
Files changed (55) hide show
  1. {pyprod-0.4.0 → pyprod-0.5.0}/.gitignore +3 -1
  2. {pyprod-0.4.0 → pyprod-0.5.0}/PKG-INFO +2 -1
  3. {pyprod-0.4.0 → pyprod-0.5.0}/docs/commandline.rst +6 -3
  4. {pyprod-0.4.0 → pyprod-0.5.0}/docs/conf.py +1 -1
  5. {pyprod-0.4.0 → pyprod-0.5.0}/docs/prodfile.rst +29 -17
  6. {pyprod-0.4.0 → pyprod-0.5.0}/docs/releasenotes.rst +8 -1
  7. {pyprod-0.4.0 → pyprod-0.5.0}/pyproject.toml +7 -2
  8. {pyprod-0.4.0 → pyprod-0.5.0}/samples/build-c/Makefile +1 -1
  9. pyprod-0.4.0/samples/generate-doc/PRODFILE.py → pyprod-0.5.0/samples/generate-doc/Prodfile.py +1 -1
  10. {pyprod-0.4.0 → pyprod-0.5.0}/samples/md-to-pdf/Prodfile.py +1 -1
  11. pyprod-0.5.0/src/pyprod/__init__.py +1 -0
  12. {pyprod-0.4.0 → pyprod-0.5.0}/src/pyprod/main.py +10 -2
  13. {pyprod-0.4.0 → pyprod-0.5.0}/src/pyprod/prod.py +95 -46
  14. {pyprod-0.4.0 → pyprod-0.5.0}/src/pyprod/utils.py +3 -1
  15. {pyprod-0.4.0 → pyprod-0.5.0}/src/pyprod/venv.py +10 -1
  16. {pyprod-0.4.0 → pyprod-0.5.0}/tests/conftest.py +1 -1
  17. {pyprod-0.4.0 → pyprod-0.5.0}/tests/test_prod.py +17 -23
  18. pyprod-0.4.0/.python-version +0 -1
  19. pyprod-0.4.0/.readthedocs.yaml +0 -20
  20. pyprod-0.4.0/samples/generate-doc/.gitignore +0 -2
  21. pyprod-0.4.0/tests/__init__.py +0 -0
  22. pyprod-0.4.0/uv.lock +0 -737
  23. {pyprod-0.4.0 → pyprod-0.5.0}/.github/workflows/publish.yml +0 -0
  24. {pyprod-0.4.0 → pyprod-0.5.0}/.github/workflows/test.yml +0 -0
  25. {pyprod-0.4.0 → pyprod-0.5.0}/LICENSE +0 -0
  26. {pyprod-0.4.0 → pyprod-0.5.0}/README.rst +0 -0
  27. {pyprod-0.4.0 → pyprod-0.5.0}/docs/Makefile +0 -0
  28. {pyprod-0.4.0 → pyprod-0.5.0}/docs/index.rst +0 -0
  29. {pyprod-0.4.0 → pyprod-0.5.0}/docs/make.bat +0 -0
  30. {pyprod-0.4.0 → pyprod-0.5.0}/docs/pyprod2.png +0 -0
  31. {pyprod-0.4.0 → pyprod-0.5.0}/docs/quickstart.rst +0 -0
  32. {pyprod-0.4.0 → pyprod-0.5.0}/docs/requirements.txt +0 -0
  33. {pyprod-0.4.0 → pyprod-0.5.0}/pyprod.webp +0 -0
  34. {pyprod-0.4.0 → pyprod-0.5.0}/pyprod2.png +0 -0
  35. {pyprod-0.4.0 → pyprod-0.5.0}/samples/build-c/Prodfile.py +0 -0
  36. {pyprod-0.4.0 → pyprod-0.5.0}/samples/build-c/hello.c +0 -0
  37. {pyprod-0.4.0 → pyprod-0.5.0}/samples/build-c/hello.h +0 -0
  38. {pyprod-0.4.0 → pyprod-0.5.0}/samples/build-c/main.c +0 -0
  39. {pyprod-0.4.0 → pyprod-0.5.0}/samples/generate-doc/a.txt +0 -0
  40. {pyprod-0.4.0 → pyprod-0.5.0}/samples/generate-doc/b.txt +0 -0
  41. {pyprod-0.4.0 → pyprod-0.5.0}/samples/generate-doc/c.txt +0 -0
  42. {pyprod-0.4.0 → pyprod-0.5.0}/samples/generate-doc/inc1.txt +0 -0
  43. {pyprod-0.4.0 → pyprod-0.5.0}/samples/generate-doc/inc2.txt +0 -0
  44. {pyprod-0.4.0 → pyprod-0.5.0}/samples/md-to-pdf/doc.md +0 -0
  45. {pyprod-0.4.0 → pyprod-0.5.0}/samples/md-to-pdf/md_to_html.py +0 -0
  46. {pyprod-0.4.0 → pyprod-0.5.0}/samples/md-to-pdf/template.html +0 -0
  47. {pyprod-0.4.0 → pyprod-0.5.0}/samples/s3files/Prodfile.py +0 -0
  48. {pyprod-0.4.0 → pyprod-0.5.0}/samples/s3files/S3TEST.txt +0 -0
  49. {pyprod-0.4.0 → pyprod-0.5.0}/samples/tutorial-1/Prodfile.py +0 -0
  50. {pyprod-0.4.0 → pyprod-0.5.0}/samples/tutorial-2/Prodfile.py +0 -0
  51. {pyprod-0.4.0 → pyprod-0.5.0}/src/pyprod/__main__.py +0 -0
  52. {pyprod-0.4.0/src/pyprod → pyprod-0.5.0/tests}/__init__.py +0 -0
  53. {pyprod-0.4.0 → pyprod-0.5.0}/tests/test_prodfuncs.py +0 -0
  54. {pyprod-0.4.0 → pyprod-0.5.0}/tests/test_rule.py +0 -0
  55. {pyprod-0.4.0 → pyprod-0.5.0}/tests/utils.py +0 -0
@@ -1,3 +1,5 @@
1
+ .*
2
+ !.github
1
3
  # Byte-compiled / optimized / DLL files
2
4
  __pycache__/
3
5
  *.py[cod]
@@ -98,7 +100,7 @@ ipython_config.py
98
100
  # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
101
  # This is especially recommended for binary packages to ensure reproducibility, and is more
100
102
  # commonly ignored for libraries.
101
- #uv.lock
103
+ uv.lock
102
104
 
103
105
  # poetry
104
106
  # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
@@ -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
@@ -5,16 +5,19 @@ Command line options
5
5
  ------------------------
6
6
 
7
7
 
8
- usage: pyprod [-h] [-C DIRECTORY] [-f FILE] [-j JOB] [-r] [-v] [targets ...]
8
+ usage: pyprod [-h] [-C DIRECTORY] [-f FILE] [-j JOB] [-r] [-g] [-v] [targets ...]
9
+
10
+ PyProd - More makable than make
9
11
 
10
12
  positional arguments:
11
- targets Build targets. If no specific target is provided on the command line, the first target defined in the Prodfile is selected by default. Arguments containing ``=`` specifies the value of a :ref:`params <params>` (e.g., ``key=value``).
13
+ targets Build targets
12
14
 
13
15
  options:
14
16
  -h, --help show this help message and exit
15
17
  -C, --directory DIRECTORY
16
18
  Change to DIRECTORY before performing any operations
17
- -f, --file FILE Use FILE as the Prodfile (default: 'PRODFILE.py')
19
+ -f, --file FILE Use FILE as the Prodfile (default: 'Prodfile.py')
18
20
  -j, --job JOB Allow up to N jobs to run simultaneously (default: 1)
19
21
  -r, --rebuild Rebuild all
22
+ -g, --use-git Get file timestamps from Git
20
23
  -v Increase verbosity level (default: 0)
@@ -7,7 +7,7 @@
7
7
  # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8
8
 
9
9
  project = "PyProd"
10
- copyright = "2024, Atsuo Ishimoto"
10
+ copyright = "2024-2025, Atsuo Ishimoto"
11
11
  author = "Atsuo Ishimoto"
12
12
 
13
13
  # -- General configuration ---------------------------------------------------
@@ -142,19 +142,17 @@ Task definition
142
142
 
143
143
  A task is similar to a rule but does not have a target and is always executed when it is depended upon.
144
144
 
145
- .. py:function:: @task(*, name=None, depends=(), uses=())
145
+ .. py:function:: @task(*, name=None, uses=(), default=False)
146
146
 
147
147
  Defines a task to be executed.
148
148
 
149
149
  :param name: The name of the task. Defaults to the function name.
150
150
  :type name: str
151
151
 
152
- :param depends: Specify the dependencies of the task. The task is alwayes excused regardless of the timestamp of the dependencies. The dependencies are passed to the task function as arguments.
153
- :type depends: str | Path | list[str | Path]
154
-
155
- :param uses: Specify the dependencies of the target file. Unline the ``depends`` parameter, ``uses`` are not passed to the task function.
152
+ :param uses: Specify the dependencies of the target file.
156
153
  :type uses: str | Path | list[str | Path]
157
154
 
155
+ :param default: If True, this task will be executed when no target is specified in the command-line arguments.
158
156
 
159
157
  .. code-block:: python
160
158
 
@@ -180,24 +178,32 @@ In addition to the ``@rule`` and ``@check`` decorators, PyProd provides several
180
178
 
181
179
  The following built-ins are available:
182
180
 
183
- .. py:function:: build(*deps):
181
+
182
+ .. py:function:: build(*deps)
184
183
 
185
184
  Schedule dependencies. The specified deps are built sequentially after the current build completes.
186
185
 
187
- :param args: name or functions to be built.
186
+ :param deps: name of dependencies to be built.
187
+
188
+ Example:
189
+
190
+ .. code-block:: python
191
+
192
+ @task
193
+ def rebuild():
194
+ build(clean, EXE)
188
195
 
189
196
  .. py:function:: pip(*args)
190
197
 
191
198
  Install Python packages. It creates a virtual environment if one does not already exist and installs the specified packages.
192
199
 
193
200
  :param args: Arguments to pass to the pip install command.
194
- :type target: str
195
201
 
196
- Example:
202
+ Example:
197
203
 
198
- .. code-block:: python
204
+ .. code-block:: python
199
205
 
200
- pip("numpy", "pandas")
206
+ pip("numpy", "pandas")
201
207
 
202
208
  .. _run:
203
209
 
@@ -241,7 +247,7 @@ Example:
241
247
  files = run("ls", stdout=True).stdout # Capture output
242
248
 
243
249
 
244
- .. py:function:: def capture(*args, echo=True, cwd=None, check=True, text=True, shell=None)
250
+ .. py:function:: capture(*args, echo=True, cwd=None, check=True, text=True, shell=None)
245
251
 
246
252
  Execute a command and capture the output. This function is a wrapper around
247
253
  :ref:`run <run>`.
@@ -273,7 +279,7 @@ Example:
273
279
  msg = capture("echo Hello, World!")
274
280
 
275
281
 
276
- .. py:function:: read(filename):
282
+ .. py:function:: read(filename)
277
283
 
278
284
  Read the contents of a file.
279
285
 
@@ -283,7 +289,7 @@ Example:
283
289
  :return: The contents of the file.
284
290
  :rtype: str
285
291
 
286
- .. py:function:: write(filename, txt, append=False):
292
+ .. py:function:: write(filename, txt, append=False)
287
293
 
288
294
  Write text to a file.
289
295
 
@@ -296,7 +302,7 @@ Example:
296
302
  :param append: Append to the file instead of overwriting it (default ``False``).
297
303
  :type append: bool
298
304
 
299
- .. py:function:: makedirs(path):
305
+ .. py:function:: makedirs(path)
300
306
 
301
307
  Create a directory along with any necessary parent directories if they do not already exist. This function wraps `os.makedirs() <https://docs.python.org/3/library/os.html#os.makedirs>`_ with the ``exists_ok`` parameter set to ``True``.
302
308
 
@@ -345,6 +351,13 @@ Example:
345
351
  :return: The quoted string.
346
352
  :rtype: str
347
353
 
354
+ .. py:function:: use_git(use)
355
+
356
+ Enable or disable git support. If enabled, PyProd retrieves the last modified time of the files from the git log.
357
+
358
+ :param use: Enable or disable git support.
359
+ :type bool: bool
360
+
348
361
  .. py:class:: Path
349
362
 
350
363
  A class representing file paths. This function is an alias for `pathlib.Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>`_.
@@ -354,8 +367,7 @@ Example:
354
367
 
355
368
  .. py:data:: shutil
356
369
 
357
- Module to perform high-level file operations. See `shutil <https://docs.python.org/3/library/shutil.html>`_ for detail.
358
-
370
+ An alias for the Python Standard Library's `shutil <https://docs.python.org/3/library/shutil.html>`_ module. This module provides a higher-level interface for file operations than the built-in `os <https://docs.python.org/3/library/os.html>`_ module.
359
371
 
360
372
  .. py:data:: env
361
373
 
@@ -1,12 +1,19 @@
1
1
  Release Notes
2
2
  ================
3
3
 
4
+ 0.5.0 (2025-2-12)
5
+ -----------------------------
6
+
7
+ - Added ``default`` to the ``@task``.
8
+ - Removed ``depends`` from thr ``@task``.
9
+ - Added --use-git commandline option.
10
+
4
11
  0.4.0 (2025-1-17)
5
12
  -------------------------
6
13
  - Swapped the behavior of quote() and squote() to make their naming more intuitive.
7
14
  - Add @task decorator.
8
15
  - Change the parameter name target to targets.
9
- - Add --rebuild option.
16
+ - Added --rebuild option.
10
17
 
11
18
  0.3.0 (2025-01-03)
12
19
  ------------------
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyProd"
3
- version = "0.4.0"
3
+ dynamic = ["version"]
4
4
  description = "PyProd: More Makeable than Make"
5
5
  readme = "README.rst"
6
6
  requires-python = ">=3.10"
@@ -12,7 +12,9 @@ classifiers = [
12
12
  "Environment :: Console",
13
13
  "Topic :: Software Development :: Build Tools",
14
14
  ]
15
- dependencies = []
15
+ dependencies = [
16
+ "python-dateutil",
17
+ ]
16
18
 
17
19
  [project.urls]
18
20
  Homepage = "https://github.com/atsuoishimoto/pyprod"
@@ -25,6 +27,9 @@ pyprod = "pyprod.main:main"
25
27
  requires = ["hatchling"]
26
28
  build-backend = "hatchling.build"
27
29
 
30
+ [tool.hatch.version]
31
+ path = "src/pyprod/__init__.py"
32
+
28
33
  [dependency-groups]
29
34
  dev = [
30
35
  "pytest>=8.3.4",
@@ -7,7 +7,7 @@ OBJS = main.o hello.o
7
7
  %.o: %.c $(DEPS)
8
8
  $(CC) -c -o $@ $< $(CFLAGS)
9
9
 
10
- hello.exe a.x: $(OBJS)
10
+ hello.exe: $(OBJS)
11
11
  $(CC) -o $@ $^
12
12
 
13
13
  clean:
@@ -15,7 +15,7 @@ def build_app(target, *src):
15
15
 
16
16
  @rule(BUILDDIR)
17
17
  def build_dir(target):
18
- run("mkdir", target)
18
+ run("mkdir -p", target)
19
19
 
20
20
 
21
21
  @rule(BUILDDIR / "%.o", depends=("%.txt", COMMON), uses=BUILDDIR)
@@ -39,7 +39,7 @@ def make_html(target, src, template, *_):
39
39
  # create outputs directory
40
40
  @rule(BUILD)
41
41
  def builds(target):
42
- os.makedirs(target)
42
+ os.makedirs(target, exist_ok=True)
43
43
 
44
44
 
45
45
  @task
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
@@ -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()
@@ -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
@@ -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)
@@ -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
  )
@@ -8,4 +8,4 @@ pyprod.verbose = 2
8
8
 
9
9
  @pytest.fixture(autouse=True)
10
10
  def init_args():
11
- main.init_args()
11
+ main.init_args([])