mtangle 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.
Files changed (28) hide show
  1. mtangle-0.1.0/.gitignore +3 -0
  2. mtangle-0.1.0/.mtangleignore +1 -0
  3. mtangle-0.1.0/LICENSE +21 -0
  4. mtangle-0.1.0/PKG-INFO +151 -0
  5. mtangle-0.1.0/README.md +138 -0
  6. mtangle-0.1.0/mtangle/__init__.py +3 -0
  7. mtangle-0.1.0/mtangle/cli.py +99 -0
  8. mtangle-0.1.0/mtangle/core.py +417 -0
  9. mtangle-0.1.0/pyproject.toml +26 -0
  10. mtangle-0.1.0/tests/__init__.py +0 -0
  11. mtangle-0.1.0/tests/fixtures/basic_file/expected/src/something.py +2 -0
  12. mtangle-0.1.0/tests/fixtures/basic_file/input.md +8 -0
  13. mtangle-0.1.0/tests/fixtures/id_substitution/expected/src/my_funcs.py +6 -0
  14. mtangle-0.1.0/tests/fixtures/id_substitution/input.md +18 -0
  15. mtangle-0.1.0/tests/fixtures/indentation/expected/src/wrapper.py +4 -0
  16. mtangle-0.1.0/tests/fixtures/indentation/input.md +18 -0
  17. mtangle-0.1.0/tests/fixtures/multi_block_same_file/expected/src/combined.py +4 -0
  18. mtangle-0.1.0/tests/fixtures/multi_block_same_file/input.md +11 -0
  19. mtangle-0.1.0/tests/fixtures/multi_file_same_target/a.md +4 -0
  20. mtangle-0.1.0/tests/fixtures/multi_file_same_target/b.md +4 -0
  21. mtangle-0.1.0/tests/fixtures/multi_file_same_target/expected/src/shared.py +4 -0
  22. mtangle-0.1.0/tests/fixtures/nested_substitution/expected/src/nested.py +2 -0
  23. mtangle-0.1.0/tests/fixtures/nested_substitution/input.md +14 -0
  24. mtangle-0.1.0/tests/fixtures/untagged_ignored/expected/src/only_this.py +1 -0
  25. mtangle-0.1.0/tests/fixtures/untagged_ignored/input.md +9 -0
  26. mtangle-0.1.0/tests/test_core.py +341 -0
  27. mtangle-0.1.0/tests/test_fixtures.py +44 -0
  28. mtangle-0.1.0/uv.lock +169 -0
@@ -0,0 +1,3 @@
1
+ .claude/*
2
+ __pycache__
3
+ .venv/
@@ -0,0 +1 @@
1
+ tests/
mtangle-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brendan Baldwin
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.
mtangle-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: mtangle
3
+ Version: 0.1.0
4
+ Summary: A simple literate programming utility for markdown
5
+ Project-URL: Homepage, https://github.com/usergenic/mtangle
6
+ Project-URL: Repository, https://github.com/usergenic/mtangle
7
+ Author: Brendan Baldwin
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: pathspec>=0.12
12
+ Description-Content-Type: text/markdown
13
+
14
+ # mtangle
15
+
16
+ A simple literate-programming utility that extracts codeblocks from markdown
17
+ files and writes them to source files.
18
+
19
+ ## What it does
20
+
21
+ Given a markdown file:
22
+
23
+ ~~~markdown
24
+ ```python file=src/something.py
25
+ def cool():
26
+ return 0
27
+ ```
28
+ ~~~
29
+
30
+ Running `mtangle` writes the block's contents to `src/something.py`.
31
+
32
+ Codeblocks are recognized by attributes on the info string:
33
+
34
+ - `file=PATH` — write this block's contents to `PATH` (relative to the output
35
+ directory).
36
+ - `id=NAME` — register this block under a name so it can be referenced from
37
+ another block.
38
+
39
+ Blocks without either attribute are ignored.
40
+
41
+ ## Composition via `<<name>>`
42
+
43
+ Blocks with `file=` can reference `id=` blocks using `<<name>>` markers on their
44
+ own line:
45
+
46
+ ~~~markdown
47
+ ```python id=cool_func
48
+ def cool():
49
+ return 0
50
+ ```
51
+
52
+ ```python id=neat_func
53
+ def neat():
54
+ return cool()
55
+ ```
56
+
57
+ ```python file=src/my_funcs.py
58
+ <<cool_func>>
59
+
60
+
61
+ <<neat_func>>
62
+ ```
63
+ ~~~
64
+
65
+ Produces `src/my_funcs.py`:
66
+
67
+ ```python
68
+ def cool():
69
+ return 0
70
+
71
+
72
+ def neat():
73
+ return cool()
74
+ ```
75
+
76
+ ### Indentation
77
+
78
+ mtangle is language-agnostic. When a codeblock is parsed, its non-blank lines
79
+ are left-aligned against the block's least-indented line. When a `<<marker>>`
80
+ is substituted, the substituted content is re-indented to match the marker's
81
+ column. This handles the common case of embedding a snippet inside an already-
82
+ indented body.
83
+
84
+ ### Escaping
85
+
86
+ - `<< name >>` (spaces inside) is not a marker — emitted literally.
87
+ - `\<<name>>` emits `<<name>>` literally (no substitution).
88
+ - `\\` emits a literal backslash.
89
+ - To emit a literal `\<<name>>`: use `\\\<<name>>`.
90
+
91
+ ## Merging into the same file
92
+
93
+ Multiple codeblocks targeting the same `file=` are concatenated in the order
94
+ they are encountered (across all input markdown files, sorted by path).
95
+ mtangle warns when this happens.
96
+
97
+ ## CLI
98
+
99
+ ```
100
+ mtangle [SOURCES...] [-o OUTPUT_DIR] [OPTIONS]
101
+ ```
102
+
103
+ With no arguments, `mtangle` is equivalent to `mtangle . -o .` — it recursively
104
+ scans the current directory for `.md` files and writes tangled output to the
105
+ current directory.
106
+
107
+ ### Options
108
+
109
+ | Flag | Description |
110
+ |------|-------------|
111
+ | `-o`, `--output-dir DIR` | Where to write files (default: cwd) |
112
+ | `-n`, `--dry-run` | Parse and resolve everything but write nothing |
113
+ | `-v`, `--verbose` | Print progress to stderr |
114
+ | `-i`, `--ignore PATTERN` | Gitignore-style skip pattern (repeatable) |
115
+ | `--disable-ignore-dotfiles` | Include dotfiles/dotdirs (default: skipped) |
116
+ | `--respect-gitignore` | Also apply `.gitignore` at each input-dir root |
117
+ | `--disable-path-safety` | Permit `file=` targets outside the output dir |
118
+
119
+ ### Ignore behavior
120
+
121
+ - Dotfiles and dotdirs (`.git`, `.venv`, etc.) are skipped by default.
122
+ - A `.mtangleignore` file at the root of each input directory is respected;
123
+ every non-empty, non-`#` line is treated like an `-i` pattern
124
+ (gitignore syntax).
125
+ - `.gitignore` is **not** consulted unless `--respect-gitignore` is set.
126
+ - Explicitly-named files (e.g. `mtangle README.md`) bypass all ignore rules.
127
+
128
+ ### Path safety
129
+
130
+ By default, `file=` targets that are absolute or that resolve outside the
131
+ output directory are warned and skipped. Use `--disable-path-safety` to opt
132
+ out.
133
+
134
+ ## Install
135
+
136
+ ```
137
+ uv pip install git+https://github.com/usergenic/mtangle.git
138
+ ```
139
+
140
+ Or add to your project:
141
+
142
+ ```
143
+ uv add git+https://github.com/usergenic/mtangle.git
144
+ ```
145
+
146
+ ## Development
147
+
148
+ ```
149
+ uv sync
150
+ uv run pytest
151
+ ```
@@ -0,0 +1,138 @@
1
+ # mtangle
2
+
3
+ A simple literate-programming utility that extracts codeblocks from markdown
4
+ files and writes them to source files.
5
+
6
+ ## What it does
7
+
8
+ Given a markdown file:
9
+
10
+ ~~~markdown
11
+ ```python file=src/something.py
12
+ def cool():
13
+ return 0
14
+ ```
15
+ ~~~
16
+
17
+ Running `mtangle` writes the block's contents to `src/something.py`.
18
+
19
+ Codeblocks are recognized by attributes on the info string:
20
+
21
+ - `file=PATH` — write this block's contents to `PATH` (relative to the output
22
+ directory).
23
+ - `id=NAME` — register this block under a name so it can be referenced from
24
+ another block.
25
+
26
+ Blocks without either attribute are ignored.
27
+
28
+ ## Composition via `<<name>>`
29
+
30
+ Blocks with `file=` can reference `id=` blocks using `<<name>>` markers on their
31
+ own line:
32
+
33
+ ~~~markdown
34
+ ```python id=cool_func
35
+ def cool():
36
+ return 0
37
+ ```
38
+
39
+ ```python id=neat_func
40
+ def neat():
41
+ return cool()
42
+ ```
43
+
44
+ ```python file=src/my_funcs.py
45
+ <<cool_func>>
46
+
47
+
48
+ <<neat_func>>
49
+ ```
50
+ ~~~
51
+
52
+ Produces `src/my_funcs.py`:
53
+
54
+ ```python
55
+ def cool():
56
+ return 0
57
+
58
+
59
+ def neat():
60
+ return cool()
61
+ ```
62
+
63
+ ### Indentation
64
+
65
+ mtangle is language-agnostic. When a codeblock is parsed, its non-blank lines
66
+ are left-aligned against the block's least-indented line. When a `<<marker>>`
67
+ is substituted, the substituted content is re-indented to match the marker's
68
+ column. This handles the common case of embedding a snippet inside an already-
69
+ indented body.
70
+
71
+ ### Escaping
72
+
73
+ - `<< name >>` (spaces inside) is not a marker — emitted literally.
74
+ - `\<<name>>` emits `<<name>>` literally (no substitution).
75
+ - `\\` emits a literal backslash.
76
+ - To emit a literal `\<<name>>`: use `\\\<<name>>`.
77
+
78
+ ## Merging into the same file
79
+
80
+ Multiple codeblocks targeting the same `file=` are concatenated in the order
81
+ they are encountered (across all input markdown files, sorted by path).
82
+ mtangle warns when this happens.
83
+
84
+ ## CLI
85
+
86
+ ```
87
+ mtangle [SOURCES...] [-o OUTPUT_DIR] [OPTIONS]
88
+ ```
89
+
90
+ With no arguments, `mtangle` is equivalent to `mtangle . -o .` — it recursively
91
+ scans the current directory for `.md` files and writes tangled output to the
92
+ current directory.
93
+
94
+ ### Options
95
+
96
+ | Flag | Description |
97
+ |------|-------------|
98
+ | `-o`, `--output-dir DIR` | Where to write files (default: cwd) |
99
+ | `-n`, `--dry-run` | Parse and resolve everything but write nothing |
100
+ | `-v`, `--verbose` | Print progress to stderr |
101
+ | `-i`, `--ignore PATTERN` | Gitignore-style skip pattern (repeatable) |
102
+ | `--disable-ignore-dotfiles` | Include dotfiles/dotdirs (default: skipped) |
103
+ | `--respect-gitignore` | Also apply `.gitignore` at each input-dir root |
104
+ | `--disable-path-safety` | Permit `file=` targets outside the output dir |
105
+
106
+ ### Ignore behavior
107
+
108
+ - Dotfiles and dotdirs (`.git`, `.venv`, etc.) are skipped by default.
109
+ - A `.mtangleignore` file at the root of each input directory is respected;
110
+ every non-empty, non-`#` line is treated like an `-i` pattern
111
+ (gitignore syntax).
112
+ - `.gitignore` is **not** consulted unless `--respect-gitignore` is set.
113
+ - Explicitly-named files (e.g. `mtangle README.md`) bypass all ignore rules.
114
+
115
+ ### Path safety
116
+
117
+ By default, `file=` targets that are absolute or that resolve outside the
118
+ output directory are warned and skipped. Use `--disable-path-safety` to opt
119
+ out.
120
+
121
+ ## Install
122
+
123
+ ```
124
+ uv pip install git+https://github.com/usergenic/mtangle.git
125
+ ```
126
+
127
+ Or add to your project:
128
+
129
+ ```
130
+ uv add git+https://github.com/usergenic/mtangle.git
131
+ ```
132
+
133
+ ## Development
134
+
135
+ ```
136
+ uv sync
137
+ uv run pytest
138
+ ```
@@ -0,0 +1,3 @@
1
+ from mtangle.core import tangle, parse_markdown, CodeBlock, TangleError, TangleWarning
2
+
3
+ __all__ = ["tangle", "parse_markdown", "CodeBlock", "TangleError", "TangleWarning"]
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from mtangle.core import TangleError, tangle
8
+
9
+
10
+ def main(argv: list[str] | None = None) -> int:
11
+ parser = argparse.ArgumentParser(
12
+ prog="mtangle",
13
+ description="Tangle codeblocks out of markdown files into source files.",
14
+ )
15
+ parser.add_argument(
16
+ "sources",
17
+ nargs="*",
18
+ help="Markdown files or directories to scan. Defaults to '.'",
19
+ )
20
+ parser.add_argument(
21
+ "-o",
22
+ "--output-dir",
23
+ default=".",
24
+ help="Directory to write tangled sources into (default: cwd)",
25
+ )
26
+ parser.add_argument(
27
+ "-n",
28
+ "--dry-run",
29
+ action="store_true",
30
+ help="Parse and resolve everything but don't write any files",
31
+ )
32
+ parser.add_argument(
33
+ "-v",
34
+ "--verbose",
35
+ action="store_true",
36
+ help="Print progress details to stderr",
37
+ )
38
+ parser.add_argument(
39
+ "--disable-path-safety",
40
+ action="store_true",
41
+ help=(
42
+ "Allow file= targets to be absolute or resolve outside the output "
43
+ "directory. Off by default; such targets are warned and skipped."
44
+ ),
45
+ )
46
+ parser.add_argument(
47
+ "-i",
48
+ "--ignore",
49
+ action="append",
50
+ default=[],
51
+ metavar="PATTERN",
52
+ help=(
53
+ "Gitignore-style pattern to skip during directory walks. "
54
+ "May be given multiple times."
55
+ ),
56
+ )
57
+ parser.add_argument(
58
+ "--disable-ignore-dotfiles",
59
+ action="store_true",
60
+ help="Include dotfiles/dotdirs in directory walks (default: skipped).",
61
+ )
62
+ parser.add_argument(
63
+ "--respect-gitignore",
64
+ action="store_true",
65
+ help="Also apply .gitignore patterns at each walk root (default: off).",
66
+ )
67
+ args = parser.parse_args(argv)
68
+
69
+ sources = args.sources or ["."]
70
+ inputs = [Path(s) for s in sources]
71
+ output_dir = Path(args.output_dir)
72
+
73
+ def info(msg: str) -> None:
74
+ if args.verbose:
75
+ print(msg, file=sys.stderr)
76
+
77
+ try:
78
+ results = tangle(
79
+ inputs,
80
+ output_dir,
81
+ on_info=info,
82
+ dry_run=args.dry_run,
83
+ path_safety=not args.disable_path_safety,
84
+ ignore_dotfiles=not args.disable_ignore_dotfiles,
85
+ ignore_patterns=args.ignore,
86
+ respect_gitignore=args.respect_gitignore,
87
+ )
88
+ except TangleError as e:
89
+ print(f"error: {e}", file=sys.stderr)
90
+ return 1
91
+
92
+ verb = "would write" if args.dry_run else "wrote"
93
+ for path in sorted(results):
94
+ print(f"{verb} {path}")
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__":
99
+ raise SystemExit(main())