chorelib 0.1.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.
chorelib-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Atsuo Ishimoto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: chorelib
3
+ Version: 0.1.0
4
+ Summary: Python build automation framework with decorator-based DSL
5
+ Keywords: build,automation,make,async,task-runner
6
+ Author: Atsuo Ishimoto
7
+ Author-email: Atsuo Ishimoto <atsuoishimoto@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.10
20
+ Project-URL: Documentation, https://chorelib.readthedocs.io/en/latest/
21
+ Project-URL: Homepage, https://github.com/atsuoishimoto/chorelib
22
+ Project-URL: Issues, https://github.com/atsuoishimoto/chorelib/issues
23
+ Project-URL: Repository, https://github.com/atsuoishimoto/chorelib
24
+ Description-Content-Type: text/markdown
25
+
26
+ # chorelib
27
+
28
+ A Python build automation framework — like Make, but in Python.
29
+ chorelib focuses on keeping Make-style speed and rebuild behavior while letting you write build logic in real Python.
30
+
31
+ chorelib uses a decorator-based DSL for defining build rules and tasks, with dependency management, parallel execution, and mtime-based rebuild detection.
32
+
33
+ ## Features
34
+
35
+ - **Decorator-based DSL** — Define build rules with `@rule` and tasks with `@task`, using familiar Python syntax
36
+ - **Regex target patterns** — Match multiple targets with regex and use backreferences (`\1`) in dependency specifications
37
+ - **Async parallel execution** — Run independent build steps concurrently with configurable worker count
38
+ - **mtime-based rebuild** — Skip up-to-date targets automatically, just like Make
39
+ - **Custom mtime functions** — Override mtime checking per target pattern to manage non-file resources (databases, S3 objects, etc.)
40
+ - **Order-only prerequisites** — `needs` dependencies ensure build order without triggering rebuilds
41
+ - **Auto-generated help** — `-h` shows usage with target documentation from docstrings, `-l` lists all available targets
42
+ - **Zero dependencies** — Pure Python, no external packages required
43
+
44
+ **Best for:** developers who want Make-like rebuild behavior but need Python for complex logic or non-file resources.
45
+ It also works well as a simple task runner for everyday scripts.
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install chorelib
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ Here is a Makefile and its chorelib equivalent side by side.
56
+
57
+ **Makefile:**
58
+
59
+ ```makefile
60
+ CC=gcc
61
+ CFLAGS=-I.
62
+ DEPS = hello.h
63
+ OBJS = main.o hello.o
64
+ .PHONY: clean
65
+
66
+ %.o: %.c $(DEPS)
67
+ $(CC) -c -o $@ $< $(CFLAGS)
68
+
69
+ hello.exe: $(OBJS)
70
+ $(CC) -o $@ $^
71
+
72
+ clean:
73
+ rm -f $(OBJS) hello.exe
74
+ ```
75
+
76
+ **chorelib (`make.py`):**
77
+
78
+ ```python
79
+ import re
80
+ from chorelib import Main, command, rule, task
81
+
82
+ APP = "hello.exe"
83
+ CC = "gcc"
84
+ CFLAGS = ["-c", "-I."]
85
+ DEPS = ["hello.h"]
86
+ OBJS = ["hello.o", "main.o"]
87
+
88
+ @rule(APP, depends=OBJS, default=True)
89
+ def link(target, deps, needs):
90
+ """Build executable"""
91
+ command(CC, "-o", target, deps)
92
+
93
+ @rule(re.compile(r"(.+)\.o"), depends=(r"\1.c", DEPS))
94
+ def compile(target, deps, needs):
95
+ command(CC, "-o", target, deps[0], CFLAGS)
96
+
97
+ @task
98
+ def clean():
99
+ """Remove the built files."""
100
+ command("rm", "-f", OBJS, APP)
101
+
102
+ if __name__ == "__main__":
103
+ Main().run()
104
+ ```
105
+
106
+ ```bash
107
+ uv run python make.py # Build default target
108
+ uv run python make.py clean # Run the clean task
109
+ uv run python make.py -w 4 # Build with 4 parallel workers
110
+ uv run python make.py -r # Force rebuild all
111
+ uv run python make.py -h # Show help with target docs
112
+ uv run python make.py -l # List all available targets
113
+ ```
114
+
115
+ `-h` displays usage information along with target documentation extracted from docstrings:
116
+
117
+ ```
118
+ $ uv run python make.py -h
119
+ usage: make.py [-h] [-C DIRECTORY] [-w WORKERS] [-r] [-l] [-v] [-V] [targets ...]
120
+
121
+ Sample build script for building a C program using chorelib.
122
+
123
+ hello.exe [default]:
124
+ Build executable
125
+
126
+ clean:
127
+ Remove the built files.
128
+
129
+ rebuild:
130
+ Clean and rebuild all files.
131
+
132
+ execute:
133
+ Rebuild and execute the program.
134
+ ...
135
+ ```
136
+
137
+ `-l` lists all registered targets with their types, dependencies, and builder functions:
138
+
139
+ ```
140
+ $ uv run python make.py -l
141
+ [rule] hello.exe:
142
+ depends: hello.o, main.o
143
+ needs:
144
+ function: link
145
+
146
+ [rule] re: (.+)\.o:
147
+ depends: \1.c, hello.h
148
+ needs:
149
+ function: compile
150
+
151
+ [task] clean:
152
+ needs:
153
+ builder: clean
154
+ ...
155
+ ```
156
+
157
+ ## Why chorelib over Make?
158
+
159
+ | | Make | chorelib |
160
+ |---|---|---|
161
+ | Pattern rules | `%.o: %.c` | `r"^(.+)\.o"` with backreferences |
162
+ | Variables & logic | Limited macro language | Full Python |
163
+ | Parallel builds | `make -j4` | `python make.py -w 4` |
164
+ | Non-file targets | `.PHONY` | `@task` decorator |
165
+ | Custom mtime | Not supported | `@mtime` decorator for databases, remote objects, etc. |
166
+ | Dependencies | File-only | Files, functions, or any callable |
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,145 @@
1
+ # chorelib
2
+
3
+ A Python build automation framework — like Make, but in Python.
4
+ chorelib focuses on keeping Make-style speed and rebuild behavior while letting you write build logic in real Python.
5
+
6
+ chorelib uses a decorator-based DSL for defining build rules and tasks, with dependency management, parallel execution, and mtime-based rebuild detection.
7
+
8
+ ## Features
9
+
10
+ - **Decorator-based DSL** — Define build rules with `@rule` and tasks with `@task`, using familiar Python syntax
11
+ - **Regex target patterns** — Match multiple targets with regex and use backreferences (`\1`) in dependency specifications
12
+ - **Async parallel execution** — Run independent build steps concurrently with configurable worker count
13
+ - **mtime-based rebuild** — Skip up-to-date targets automatically, just like Make
14
+ - **Custom mtime functions** — Override mtime checking per target pattern to manage non-file resources (databases, S3 objects, etc.)
15
+ - **Order-only prerequisites** — `needs` dependencies ensure build order without triggering rebuilds
16
+ - **Auto-generated help** — `-h` shows usage with target documentation from docstrings, `-l` lists all available targets
17
+ - **Zero dependencies** — Pure Python, no external packages required
18
+
19
+ **Best for:** developers who want Make-like rebuild behavior but need Python for complex logic or non-file resources.
20
+ It also works well as a simple task runner for everyday scripts.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install chorelib
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ Here is a Makefile and its chorelib equivalent side by side.
31
+
32
+ **Makefile:**
33
+
34
+ ```makefile
35
+ CC=gcc
36
+ CFLAGS=-I.
37
+ DEPS = hello.h
38
+ OBJS = main.o hello.o
39
+ .PHONY: clean
40
+
41
+ %.o: %.c $(DEPS)
42
+ $(CC) -c -o $@ $< $(CFLAGS)
43
+
44
+ hello.exe: $(OBJS)
45
+ $(CC) -o $@ $^
46
+
47
+ clean:
48
+ rm -f $(OBJS) hello.exe
49
+ ```
50
+
51
+ **chorelib (`make.py`):**
52
+
53
+ ```python
54
+ import re
55
+ from chorelib import Main, command, rule, task
56
+
57
+ APP = "hello.exe"
58
+ CC = "gcc"
59
+ CFLAGS = ["-c", "-I."]
60
+ DEPS = ["hello.h"]
61
+ OBJS = ["hello.o", "main.o"]
62
+
63
+ @rule(APP, depends=OBJS, default=True)
64
+ def link(target, deps, needs):
65
+ """Build executable"""
66
+ command(CC, "-o", target, deps)
67
+
68
+ @rule(re.compile(r"(.+)\.o"), depends=(r"\1.c", DEPS))
69
+ def compile(target, deps, needs):
70
+ command(CC, "-o", target, deps[0], CFLAGS)
71
+
72
+ @task
73
+ def clean():
74
+ """Remove the built files."""
75
+ command("rm", "-f", OBJS, APP)
76
+
77
+ if __name__ == "__main__":
78
+ Main().run()
79
+ ```
80
+
81
+ ```bash
82
+ uv run python make.py # Build default target
83
+ uv run python make.py clean # Run the clean task
84
+ uv run python make.py -w 4 # Build with 4 parallel workers
85
+ uv run python make.py -r # Force rebuild all
86
+ uv run python make.py -h # Show help with target docs
87
+ uv run python make.py -l # List all available targets
88
+ ```
89
+
90
+ `-h` displays usage information along with target documentation extracted from docstrings:
91
+
92
+ ```
93
+ $ uv run python make.py -h
94
+ usage: make.py [-h] [-C DIRECTORY] [-w WORKERS] [-r] [-l] [-v] [-V] [targets ...]
95
+
96
+ Sample build script for building a C program using chorelib.
97
+
98
+ hello.exe [default]:
99
+ Build executable
100
+
101
+ clean:
102
+ Remove the built files.
103
+
104
+ rebuild:
105
+ Clean and rebuild all files.
106
+
107
+ execute:
108
+ Rebuild and execute the program.
109
+ ...
110
+ ```
111
+
112
+ `-l` lists all registered targets with their types, dependencies, and builder functions:
113
+
114
+ ```
115
+ $ uv run python make.py -l
116
+ [rule] hello.exe:
117
+ depends: hello.o, main.o
118
+ needs:
119
+ function: link
120
+
121
+ [rule] re: (.+)\.o:
122
+ depends: \1.c, hello.h
123
+ needs:
124
+ function: compile
125
+
126
+ [task] clean:
127
+ needs:
128
+ builder: clean
129
+ ...
130
+ ```
131
+
132
+ ## Why chorelib over Make?
133
+
134
+ | | Make | chorelib |
135
+ |---|---|---|
136
+ | Pattern rules | `%.o: %.c` | `r"^(.+)\.o"` with backreferences |
137
+ | Variables & logic | Limited macro language | Full Python |
138
+ | Parallel builds | `make -j4` | `python make.py -w 4` |
139
+ | Non-file targets | `.PHONY` | `@task` decorator |
140
+ | Custom mtime | Not supported | `@mtime` decorator for databases, remote objects, etc. |
141
+ | Dependencies | File-only | Files, functions, or any callable |
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "chorelib"
3
+ version = "0.1.0"
4
+ description = "Python build automation framework with decorator-based DSL"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Atsuo Ishimoto", email = "atsuoishimoto@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ keywords = ["build", "automation", "make", "async", "task-runner"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Topic :: Software Development :: Build Tools",
17
+ "Framework :: AsyncIO",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ ]
24
+ dependencies = []
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/atsuoishimoto/chorelib"
28
+ Repository = "https://github.com/atsuoishimoto/chorelib"
29
+ Documentation = "https://chorelib.readthedocs.io/en/latest/"
30
+ Issues = "https://github.com/atsuoishimoto/chorelib/issues"
31
+
32
+ [build-system]
33
+ requires = ["uv_build>=0.8.15,<0.9.0"]
34
+ build-backend = "uv_build"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "mypy>=1.19.1",
39
+ "pytest>=9.0.2",
40
+ "ruff>=0.15.0",
41
+ "sphinx>=8.1.3",
42
+ "sphinx-rtd-theme>=3.1.0",
43
+ ]
44
+
45
+ [tool.ruff]
46
+ lint.select = [
47
+ "E4",
48
+ "E7",
49
+ "E9",
50
+ "F",
51
+ "I"
52
+ ]
53
+ line-length = 99
@@ -0,0 +1,2 @@
1
+ # Automatically created by ruff.
2
+ *
@@ -0,0 +1 @@
1
+ Signature: 8a477f597d28d172789f06886806bc55
@@ -0,0 +1,48 @@
1
+ """Chorelib: An async-first Python build automation framework.
2
+
3
+ Provides a decorator-based DSL for defining build rules and tasks with
4
+ dependency management, parallel execution, and mtime-based rebuild detection.
5
+
6
+ Typical usage::
7
+
8
+ import chorelib
9
+
10
+ @chorelib.rule("output.txt", depends="input.txt")
11
+ def build_output(target, depends, needs):
12
+ chorelib.shell(f"cp {depends[0]} {target}")
13
+
14
+ if __name__ == "__main__":
15
+ chorelib.Main().run()
16
+ """
17
+
18
+ from .depmain import Main
19
+ from .deprunner import schedule
20
+ from .ruledef import RuleSet
21
+ from .utils import command, message, shell
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ # Default RuleSet instance used by the module-level decorators.
26
+ # Users can use `@chorelib.rule(...)` and `@chorelib.task` directly
27
+ # without creating a RuleSet manually.
28
+ _default_rules = RuleSet()
29
+ rule = _default_rules.rule
30
+ task = _default_rules.task
31
+ mtime = _default_rules.mtime
32
+
33
+ # Global verbosity level controlling message output.
34
+ # 0 = normal, 1 = verbose, 2 = debug messages, 3+ = logging debug.
35
+ verbose: int = 0
36
+
37
+ __all__ = [
38
+ "RuleSet",
39
+ "rule",
40
+ "task",
41
+ "mtime",
42
+ "Main",
43
+ "schedule",
44
+ "verbose",
45
+ "command",
46
+ "message",
47
+ "shell",
48
+ ]
@@ -0,0 +1,122 @@
1
+ """Dependency graph construction and cycle detection.
2
+
3
+ Builds a directed acyclic graph (DAG) of build targets and their
4
+ dependencies. Detects circular dependencies via depth-first search
5
+ before any build execution begins.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from .errors import RuleError
11
+ from .ruledef import RuleBase, RuleSet, Task
12
+
13
+
14
+ class BuildInfo:
15
+ """A node in the dependency graph representing a single build target.
16
+
17
+ Attributes:
18
+ target: The target name (file path or task name).
19
+ rule: The Rule or Task that produces this target.
20
+ depends: List of dependency target names that trigger rebuilds.
21
+ needs: List of order-only prerequisite target names.
22
+ """
23
+
24
+ def __init__(self, target: str, rule: RuleBase, depends: list[str], needs: list[str]) -> None:
25
+ self.rule = rule
26
+ self.target = target
27
+ self.depends = depends
28
+ self.needs = needs
29
+
30
+ def __repr__(self) -> str:
31
+ return f"<BuildInfo> {self.rule}:{self.target}:{self.depends}:{self.needs}"
32
+
33
+ def is_task(self) -> bool:
34
+ """Return True if this node represents a Task (always-execute)."""
35
+ return isinstance(self.rule, Task)
36
+
37
+ def run_builder(self) -> Any:
38
+ """Execute the builder function for this target."""
39
+ return self.rule.run_builder(target=self.target, depends=self.depends, needs=self.needs)
40
+
41
+
42
+ class DepGraph:
43
+ """Dependency graph that manages BuildInfo nodes and detects cycles.
44
+
45
+ Nodes are registered by recursively resolving each target's dependencies
46
+ through the RuleSet. After registration, ``detectloop`` should be called
47
+ to verify the graph is acyclic.
48
+ """
49
+
50
+ def __init__(self) -> None:
51
+ self._nodes: dict[str, BuildInfo] = {}
52
+
53
+ def addtarget(self, ruleset: RuleSet, target: str) -> bool:
54
+ """Add a target and all its transitive dependencies to the graph.
55
+
56
+ Returns:
57
+ True if the target was newly registered, False if already present.
58
+ """
59
+ return self._register(ruleset, target)
60
+
61
+ def get(self, target: str) -> BuildInfo | None:
62
+ """Return the BuildInfo node for a target, or None if not registered."""
63
+ return self._nodes.get(target)
64
+
65
+ def _register(self, ruleset: RuleSet, target: str) -> bool:
66
+ """Recursively register a target and its dependencies.
67
+
68
+ Looks up the matching rule in the ruleset, creates a BuildInfo node,
69
+ and recurses into each dependency and need.
70
+ """
71
+ if target in self._nodes:
72
+ return False
73
+
74
+ ret = ruleset.select_rule(target)
75
+ if ret:
76
+ rule, deps, needs = ret
77
+ node = BuildInfo(target, rule, deps, needs)
78
+ self._nodes[target] = node
79
+
80
+ # Recursively register all dependencies
81
+ for dep in deps:
82
+ self._register(ruleset, dep)
83
+
84
+ # Recursively register all order-only prerequisites
85
+ for need in needs:
86
+ self._register(ruleset, need)
87
+
88
+ return True
89
+
90
+ def detectloop(self) -> None:
91
+ """Detect circular dependencies in the graph using DFS.
92
+
93
+ Raises:
94
+ RuleError: If a dependency cycle is found.
95
+ """
96
+ done: set[BuildInfo] = set()
97
+
98
+ def _walk(node: BuildInfo, seen: set[BuildInfo]) -> None:
99
+ """DFS traversal tracking the current path in ``seen``."""
100
+ if node in seen:
101
+ raise RuleError(f"Dependency cycle detected: {node}")
102
+
103
+ if node in done:
104
+ return
105
+
106
+ seen.add(node)
107
+ try:
108
+ for dep in node.depends:
109
+ depnode = self._nodes.get(dep)
110
+ if depnode:
111
+ _walk(depnode, seen)
112
+ for need in node.needs:
113
+ neednode = self._nodes.get(need)
114
+ if neednode:
115
+ _walk(neednode, seen)
116
+ finally:
117
+ seen.remove(node)
118
+ done.add(node)
119
+
120
+ for node in self._nodes.values():
121
+ seen: set[BuildInfo] = set()
122
+ _walk(node, seen)