pyshellforge 0.0.0__tar.gz → 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.
- pyshellforge-0.1.0/.gitignore +61 -0
- pyshellforge-0.1.0/PKG-INFO +154 -0
- pyshellforge-0.1.0/README.md +106 -0
- pyshellforge-0.1.0/features/bash_rendering.feature +10 -0
- pyshellforge-0.1.0/pyproject.toml +147 -0
- pyshellforge-0.1.0/src/pyshellforge/__init__.py +113 -0
- pyshellforge-0.1.0/src/pyshellforge/ast/__init__.py +34 -0
- pyshellforge-0.1.0/src/pyshellforge/ast/base.py +5 -0
- pyshellforge-0.1.0/src/pyshellforge/ast/nodes/__init__.py +1 -0
- pyshellforge-0.1.0/src/pyshellforge/ast/nodes/echo.py +17 -0
- pyshellforge-0.1.0/src/pyshellforge/core/__init__.py +36 -0
- pyshellforge-0.1.0/src/pyshellforge/renderers/__init__.py +14 -0
- pyshellforge-0.1.0/src/pyshellforge/renderers/bash.py +157 -0
- pyshellforge-0.1.0/tests/bdd/__init__.py +1 -0
- pyshellforge-0.1.0/tests/bdd/steps_render.py +42 -0
- pyshellforge-0.1.0/tests/bdd/test_render.py +7 -0
- pyshellforge-0.1.0/tests/golden/hello.sh +5 -0
- pyshellforge-0.1.0/tests/integration/test_bash_syntax.py +60 -0
- pyshellforge-0.1.0/tests/integration/test_readme.py +349 -0
- pyshellforge-0.1.0/tests/unit/test_bash_renderer.py +169 -0
- pyshellforge-0.1.0/tests/unit/test_version.py +149 -0
- pyshellforge-0.0.0/PKG-INFO +0 -4
- pyshellforge-0.0.0/pyproject.toml +0 -11
- pyshellforge-0.0.0/setup.cfg +0 -4
- pyshellforge-0.0.0/src/pyshellforge.egg-info/PKG-INFO +0 -4
- pyshellforge-0.0.0/src/pyshellforge.egg-info/SOURCES.txt +0 -6
- pyshellforge-0.0.0/src/pyshellforge.egg-info/dependency_links.txt +0 -1
- pyshellforge-0.0.0/src/pyshellforge.egg-info/top_level.txt +0 -1
- /pyshellforge-0.0.0/src/pyshellforge/__init__.py → /pyshellforge-0.1.0/src/pyshellforge/py.typed +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Byte-compiled / optimized files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
.eggs/
|
|
12
|
+
wheels/
|
|
13
|
+
|
|
14
|
+
# Virtual environments
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
env/
|
|
18
|
+
|
|
19
|
+
# Test & type-check caches
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
.coverage.*
|
|
25
|
+
coverage.xml
|
|
26
|
+
htmlcov/
|
|
27
|
+
.tox/
|
|
28
|
+
.nox/
|
|
29
|
+
|
|
30
|
+
# Rendered artefacts produced while experimenting
|
|
31
|
+
/deploy.sh
|
|
32
|
+
/backup.sh
|
|
33
|
+
/build.sh
|
|
34
|
+
/out/
|
|
35
|
+
rendered/
|
|
36
|
+
|
|
37
|
+
# README generation tooling (optional, Node only)
|
|
38
|
+
node_modules/
|
|
39
|
+
npm-debug.log*
|
|
40
|
+
|
|
41
|
+
# Developer environment and sandbox
|
|
42
|
+
.pixi/
|
|
43
|
+
pixi-sandbox-pack/
|
|
44
|
+
|
|
45
|
+
# Coverage, prose and link-check caches
|
|
46
|
+
coverage.xml
|
|
47
|
+
coverage_html/
|
|
48
|
+
.lycheecache
|
|
49
|
+
|
|
50
|
+
# Repository script output
|
|
51
|
+
sandbox/
|
|
52
|
+
|
|
53
|
+
# doit task state
|
|
54
|
+
.doit.db
|
|
55
|
+
.doit.db.*
|
|
56
|
+
|
|
57
|
+
# Editors / OS
|
|
58
|
+
.idea/
|
|
59
|
+
.vscode/
|
|
60
|
+
.DS_Store
|
|
61
|
+
*.swp
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pyshellforge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python-native DSL for building, composing and rendering structured Bash scripts.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Archont561/PyShellForge
|
|
6
|
+
Project-URL: Documentation, https://github.com/Archont561/PyShellForge/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/Archont561/PyShellForge
|
|
8
|
+
Project-URL: Issues, https://github.com/Archont561/PyShellForge/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/Archont561/PyShellForge/blob/main/CHANGELOG.md
|
|
10
|
+
Author: PyShellForge contributors
|
|
11
|
+
License: MIT
|
|
12
|
+
Keywords: ast,automation,bash,code-generation,compiler,developer-tools,dsl,python,shell,shell-script,transpiler
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: POSIX
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Unix Shell
|
|
23
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
24
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
25
|
+
Classifier: Topic :: System :: Shells
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
30
|
+
Requires-Dist: hypothesis>=6.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
32
|
+
Requires-Dist: pyfiglet<2,>=1.0.4; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-bdd<10,>=9; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: rude-clean-code<0.2,>=0.1; extra == 'dev'
|
|
38
|
+
Requires-Dist: rude<0.2,>=0.1a4; extra == 'dev'
|
|
39
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
40
|
+
Requires-Dist: tox>=4.20; extra == 'dev'
|
|
41
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
42
|
+
Provides-Extra: docs
|
|
43
|
+
Requires-Dist: alint>=0.20; extra == 'docs'
|
|
44
|
+
Requires-Dist: gherlint>=0.5; extra == 'docs'
|
|
45
|
+
Requires-Dist: pyfiglet<2,>=1.0.4; extra == 'docs'
|
|
46
|
+
Requires-Dist: pyyaml>=6.0; extra == 'docs'
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
<!--
|
|
50
|
+
Generated file - do not edit README.md by hand.
|
|
51
|
+
|
|
52
|
+
Source template : automation/readme/README.ejs
|
|
53
|
+
Generator : readme-md-generator (pinned in package.json)
|
|
54
|
+
Regenerate with : pixi run automation readme (python -m automation.readme)
|
|
55
|
+
Verify in CI : pixi run automation readme-check
|
|
56
|
+
-->
|
|
57
|
+
|
|
58
|
+
<h1 align="center">PyShellForge</h1>
|
|
59
|
+
|
|
60
|
+
<pre style="font-family: monospace">
|
|
61
|
+
██████╗ ██╗ ██╗███████╗██╗ ██╗███████╗██╗ ██╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
62
|
+
██╔══██╗╚██╗ ██╔╝██╔════╝██║ ██║██╔════╝██║ ██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
|
|
63
|
+
██████╔╝ ╚████╔╝ ███████╗███████║█████╗ ██║ ██║ █████╗ ██║ ██║██████╔╝██║ ███╗█████╗
|
|
64
|
+
██╔═══╝ ╚██╔╝ ╚════██║██╔══██║██╔══╝ ██║ ██║ ██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝
|
|
65
|
+
██║ ██║ ███████║██║ ██║███████╗███████╗███████╗██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗
|
|
66
|
+
╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝</pre>
|
|
67
|
+
|
|
68
|
+
<p>
|
|
69
|
+
<img alt="Version: 0.1.0" src="https://img.shields.io/badge/version-0.1.0-blue.svg?cacheSeconds=2592000" />
|
|
70
|
+
<a href="https://github.com/Archont561/pyshellforge/blob/main/docs/README.md" target="_blank">
|
|
71
|
+
<img alt="Documentation" src="https://img.shields.io/badge/documentation-yes-brightgreen.svg" />
|
|
72
|
+
</a>
|
|
73
|
+
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%3E%3D3.11-blue.svg" />
|
|
74
|
+
<a href="https://codecov.io/gh/Archont561/pyshellforge" target="_blank">
|
|
75
|
+
<img alt="Coverage" src="https://img.shields.io/codecov/c/github/Archont561/pyshellforge.svg" />
|
|
76
|
+
</a>
|
|
77
|
+
<a href="https://github.com/Archont561/pyshellforge/blob/main/LICENSE" target="_blank">
|
|
78
|
+
<img alt="License: MIT" src="https://img.shields.io/github/license/Archont561/pyshellforge" />
|
|
79
|
+
</a>
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
> Write shell scripts in Python. Render clean Bash.
|
|
83
|
+
>
|
|
84
|
+
> A Python-native DSL for building, composing and rendering structured Bash
|
|
85
|
+
> scripts. The first Bash renderer supports comments, literal assignments,
|
|
86
|
+
> commands and checked text interpolation.
|
|
87
|
+
|
|
88
|
+
### 🏠 [Homepage](https://github.com/Archont561/PyShellForge#readme)
|
|
89
|
+
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
The [`docs/`](../../docs/README.md) directory is the source of truth for using and
|
|
93
|
+
working on this repository:
|
|
94
|
+
|
|
95
|
+
- [Documentation index](../../docs/README.md): everything, organized by purpose
|
|
96
|
+
- [Install](../../docs/guides/install.md): prerequisites, installation, the locked developer sandbox
|
|
97
|
+
- [AST design](../../docs/concepts/ast-design.md): nodes, renderers and interpolation
|
|
98
|
+
- [API reference](../../docs/reference/api-reference.md): the script, nodes and Bash renderer
|
|
99
|
+
- [Repository layout](../../docs/reference/repository-layout.md): what lives where and why
|
|
100
|
+
- [Linting](../../docs/reference/linting.md): the Rude plugin, its rule codes, and the rules that wait for the validator
|
|
101
|
+
- [Project status](../../docs/concepts/version-only-scaffold.md): what exists today and what the roadmap adds
|
|
102
|
+
- [Contributing](../../docs/development/contributing.md): setup, the checks CI runs, the commit and merge policy
|
|
103
|
+
- [Testing](../../docs/development/testing.md): test layout, what each layer proves, coverage
|
|
104
|
+
- [Tooling](../../docs/development/tooling.md): the `automation` CLI, its task graph, hooks and linters
|
|
105
|
+
- [Documentation](../../docs/development/documentation.md): policy, structure, validation and definition of done
|
|
106
|
+
- [Releasing](../../docs/development/releasing.md): the tag-triggered release and trusted publishing
|
|
107
|
+
|
|
108
|
+
Planned work is tracked in [`backlog/`](../../backlog/), and the release history is
|
|
109
|
+
[`CHANGELOG.md`](../../CHANGELOG.md).
|
|
110
|
+
|
|
111
|
+
## Install
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
pip install pyshellforge
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Usage
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
python -c 'from pyshellforge import ShellScript, Echo; print(ShellScript().add(Echo("Hello")).render())'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Run tests
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
pixi run automation test-all
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Author
|
|
130
|
+
|
|
131
|
+
👤 **PyShellForge contributors**
|
|
132
|
+
|
|
133
|
+
- Website: [archont561.github.io/Archont561](https://archont561.github.io/Archont561/)
|
|
134
|
+
- GitHub: [@Archont561](https://github.com/Archont561)
|
|
135
|
+
|
|
136
|
+
## 🤝 Contributing
|
|
137
|
+
|
|
138
|
+
Contributions, issues and feature requests are welcome!<br />Feel free to check
|
|
139
|
+
the [issues page](https://github.com/Archont561/PyShellForge/issues). You can also take a look at the
|
|
140
|
+
[contributing guide](../../CONTRIBUTING.md): it covers setup, the checks a change has
|
|
141
|
+
to pass and the merge policy.
|
|
142
|
+
|
|
143
|
+
## Show your support
|
|
144
|
+
|
|
145
|
+
Give a ⭐️ if this project helped you!
|
|
146
|
+
|
|
147
|
+
## 📝 License
|
|
148
|
+
|
|
149
|
+
Copyright © 2026 [PyShellForge contributors](https://github.com/Archont561).<br />
|
|
150
|
+
This project is [MIT](https://github.com/Archont561/pyshellforge/blob/main/LICENSE) licensed.
|
|
151
|
+
|
|
152
|
+
***
|
|
153
|
+
|
|
154
|
+
*This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator) from [`automation/readme/README.ejs`](../../automation/readme/README.ejs).*
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Generated file - do not edit README.md by hand.
|
|
3
|
+
|
|
4
|
+
Source template : automation/readme/README.ejs
|
|
5
|
+
Generator : readme-md-generator (pinned in package.json)
|
|
6
|
+
Regenerate with : pixi run automation readme (python -m automation.readme)
|
|
7
|
+
Verify in CI : pixi run automation readme-check
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
<h1 align="center">PyShellForge</h1>
|
|
11
|
+
|
|
12
|
+
<pre style="font-family: monospace">
|
|
13
|
+
██████╗ ██╗ ██╗███████╗██╗ ██╗███████╗██╗ ██╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
14
|
+
██╔══██╗╚██╗ ██╔╝██╔════╝██║ ██║██╔════╝██║ ██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
|
|
15
|
+
██████╔╝ ╚████╔╝ ███████╗███████║█████╗ ██║ ██║ █████╗ ██║ ██║██████╔╝██║ ███╗█████╗
|
|
16
|
+
██╔═══╝ ╚██╔╝ ╚════██║██╔══██║██╔══╝ ██║ ██║ ██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝
|
|
17
|
+
██║ ██║ ███████║██║ ██║███████╗███████╗███████╗██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗
|
|
18
|
+
╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝</pre>
|
|
19
|
+
|
|
20
|
+
<p>
|
|
21
|
+
<img alt="Version: 0.1.0" src="https://img.shields.io/badge/version-0.1.0-blue.svg?cacheSeconds=2592000" />
|
|
22
|
+
<a href="https://github.com/Archont561/pyshellforge/blob/main/docs/README.md" target="_blank">
|
|
23
|
+
<img alt="Documentation" src="https://img.shields.io/badge/documentation-yes-brightgreen.svg" />
|
|
24
|
+
</a>
|
|
25
|
+
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%3E%3D3.11-blue.svg" />
|
|
26
|
+
<a href="https://codecov.io/gh/Archont561/pyshellforge" target="_blank">
|
|
27
|
+
<img alt="Coverage" src="https://img.shields.io/codecov/c/github/Archont561/pyshellforge.svg" />
|
|
28
|
+
</a>
|
|
29
|
+
<a href="https://github.com/Archont561/pyshellforge/blob/main/LICENSE" target="_blank">
|
|
30
|
+
<img alt="License: MIT" src="https://img.shields.io/github/license/Archont561/pyshellforge" />
|
|
31
|
+
</a>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
> Write shell scripts in Python. Render clean Bash.
|
|
35
|
+
>
|
|
36
|
+
> A Python-native DSL for building, composing and rendering structured Bash
|
|
37
|
+
> scripts. The first Bash renderer supports comments, literal assignments,
|
|
38
|
+
> commands and checked text interpolation.
|
|
39
|
+
|
|
40
|
+
### 🏠 [Homepage](https://github.com/Archont561/PyShellForge#readme)
|
|
41
|
+
|
|
42
|
+
## Documentation
|
|
43
|
+
|
|
44
|
+
The [`docs/`](../../docs/README.md) directory is the source of truth for using and
|
|
45
|
+
working on this repository:
|
|
46
|
+
|
|
47
|
+
- [Documentation index](../../docs/README.md): everything, organized by purpose
|
|
48
|
+
- [Install](../../docs/guides/install.md): prerequisites, installation, the locked developer sandbox
|
|
49
|
+
- [AST design](../../docs/concepts/ast-design.md): nodes, renderers and interpolation
|
|
50
|
+
- [API reference](../../docs/reference/api-reference.md): the script, nodes and Bash renderer
|
|
51
|
+
- [Repository layout](../../docs/reference/repository-layout.md): what lives where and why
|
|
52
|
+
- [Linting](../../docs/reference/linting.md): the Rude plugin, its rule codes, and the rules that wait for the validator
|
|
53
|
+
- [Project status](../../docs/concepts/version-only-scaffold.md): what exists today and what the roadmap adds
|
|
54
|
+
- [Contributing](../../docs/development/contributing.md): setup, the checks CI runs, the commit and merge policy
|
|
55
|
+
- [Testing](../../docs/development/testing.md): test layout, what each layer proves, coverage
|
|
56
|
+
- [Tooling](../../docs/development/tooling.md): the `automation` CLI, its task graph, hooks and linters
|
|
57
|
+
- [Documentation](../../docs/development/documentation.md): policy, structure, validation and definition of done
|
|
58
|
+
- [Releasing](../../docs/development/releasing.md): the tag-triggered release and trusted publishing
|
|
59
|
+
|
|
60
|
+
Planned work is tracked in [`backlog/`](../../backlog/), and the release history is
|
|
61
|
+
[`CHANGELOG.md`](../../CHANGELOG.md).
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
pip install pyshellforge
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
python -c 'from pyshellforge import ShellScript, Echo; print(ShellScript().add(Echo("Hello")).render())'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Run tests
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
pixi run automation test-all
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Author
|
|
82
|
+
|
|
83
|
+
👤 **PyShellForge contributors**
|
|
84
|
+
|
|
85
|
+
- Website: [archont561.github.io/Archont561](https://archont561.github.io/Archont561/)
|
|
86
|
+
- GitHub: [@Archont561](https://github.com/Archont561)
|
|
87
|
+
|
|
88
|
+
## 🤝 Contributing
|
|
89
|
+
|
|
90
|
+
Contributions, issues and feature requests are welcome!<br />Feel free to check
|
|
91
|
+
the [issues page](https://github.com/Archont561/PyShellForge/issues). You can also take a look at the
|
|
92
|
+
[contributing guide](../../CONTRIBUTING.md): it covers setup, the checks a change has
|
|
93
|
+
to pass and the merge policy.
|
|
94
|
+
|
|
95
|
+
## Show your support
|
|
96
|
+
|
|
97
|
+
Give a ⭐️ if this project helped you!
|
|
98
|
+
|
|
99
|
+
## 📝 License
|
|
100
|
+
|
|
101
|
+
Copyright © 2026 [PyShellForge contributors](https://github.com/Archont561).<br />
|
|
102
|
+
This project is [MIT](https://github.com/Archont561/pyshellforge/blob/main/LICENSE) licensed.
|
|
103
|
+
|
|
104
|
+
***
|
|
105
|
+
|
|
106
|
+
*This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator) from [`automation/readme/README.ejs`](../../automation/readme/README.ejs).*
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
@product
|
|
2
|
+
Feature: Render a Bash script
|
|
3
|
+
A script is a sequence of shell-neutral nodes. The Bash renderer quotes literal
|
|
4
|
+
values and expands only variables declared earlier in the script.
|
|
5
|
+
|
|
6
|
+
Scenario: Render and run a greeting
|
|
7
|
+
Given a script that assigns a greeting name
|
|
8
|
+
When the script is rendered as Bash
|
|
9
|
+
Then the generated Bash matches the greeting golden file
|
|
10
|
+
And the generated Bash prints the greeting
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
# The distribution lives in this directory: the package under src/, its test
|
|
6
|
+
# suite, its Gherkin features and this manifest. Everything repository-scoped
|
|
7
|
+
# (docs/, automation/, CI, the Pixi environment) stays at the repository root and
|
|
8
|
+
# builds, tests and checks this project from there.
|
|
9
|
+
[project]
|
|
10
|
+
name = "pyshellforge"
|
|
11
|
+
version = "0.1.0"
|
|
12
|
+
description = "A Python-native DSL for building, composing and rendering structured Bash scripts."
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
|
+
license = { text = "MIT" }
|
|
16
|
+
authors = [{ name = "PyShellForge contributors" }]
|
|
17
|
+
keywords = [
|
|
18
|
+
"python",
|
|
19
|
+
"bash",
|
|
20
|
+
"shell",
|
|
21
|
+
"shell-script",
|
|
22
|
+
"code-generation",
|
|
23
|
+
"compiler",
|
|
24
|
+
"transpiler",
|
|
25
|
+
"dsl",
|
|
26
|
+
"ast",
|
|
27
|
+
"developer-tools",
|
|
28
|
+
"automation",
|
|
29
|
+
]
|
|
30
|
+
classifiers = [
|
|
31
|
+
"Development Status :: 4 - Beta",
|
|
32
|
+
"Environment :: Console",
|
|
33
|
+
"Intended Audience :: Developers",
|
|
34
|
+
"License :: OSI Approved :: MIT License",
|
|
35
|
+
"Operating System :: POSIX",
|
|
36
|
+
"Programming Language :: Python :: 3",
|
|
37
|
+
"Programming Language :: Python :: 3.11",
|
|
38
|
+
"Programming Language :: Python :: 3.12",
|
|
39
|
+
"Programming Language :: Python :: 3.13",
|
|
40
|
+
"Programming Language :: Unix Shell",
|
|
41
|
+
"Topic :: Software Development :: Code Generators",
|
|
42
|
+
"Topic :: Software Development :: Compilers",
|
|
43
|
+
"Topic :: System :: Shells",
|
|
44
|
+
"Typing :: Typed",
|
|
45
|
+
]
|
|
46
|
+
dependencies = []
|
|
47
|
+
|
|
48
|
+
[project.optional-dependencies]
|
|
49
|
+
dev = [
|
|
50
|
+
"pytest>=8.0",
|
|
51
|
+
# 9 emits every step definition as a fixture, which is how the automation
|
|
52
|
+
# suite registers its step modules; the Pixi environment pins the same major.
|
|
53
|
+
"pytest-bdd>=9,<10",
|
|
54
|
+
"pytest-cov>=5.0",
|
|
55
|
+
"hypothesis>=6.0",
|
|
56
|
+
"ruff>=0.6",
|
|
57
|
+
"mypy>=1.11",
|
|
58
|
+
"build>=1.2",
|
|
59
|
+
"tox>=4.20",
|
|
60
|
+
# Issue-form validation (automation.forms) reads YAML.
|
|
61
|
+
"pyyaml>=6.0",
|
|
62
|
+
"types-pyyaml>=6.0",
|
|
63
|
+
# The README banner is FIGlet art (automation.banner), rendered, not pasted.
|
|
64
|
+
"pyfiglet>=1.0.4,<2",
|
|
65
|
+
# Clean Code rules: rude is the linter, rude-clean-code the rule plugin.
|
|
66
|
+
"rude>=0.1a4,<0.2",
|
|
67
|
+
"rude-clean-code>=0.1,<0.2",
|
|
68
|
+
]
|
|
69
|
+
# Documentation tooling: repository policy and Gherkin style. markdownlint-cli2,
|
|
70
|
+
# Vale and lychee are standalone binaries wired through Pixi (see pixi.toml).
|
|
71
|
+
docs = [
|
|
72
|
+
"alint>=0.20",
|
|
73
|
+
"gherlint>=0.5",
|
|
74
|
+
# automation.forms validates the GitHub Issue Forms.
|
|
75
|
+
"pyyaml>=6.0",
|
|
76
|
+
# automation.readme renders the banner, and CI checks the README here.
|
|
77
|
+
"pyfiglet>=1.0.4,<2",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[project.urls]
|
|
81
|
+
Homepage = "https://github.com/Archont561/PyShellForge"
|
|
82
|
+
Documentation = "https://github.com/Archont561/PyShellForge/tree/main/docs"
|
|
83
|
+
Repository = "https://github.com/Archont561/PyShellForge"
|
|
84
|
+
Issues = "https://github.com/Archont561/PyShellForge/issues"
|
|
85
|
+
Changelog = "https://github.com/Archont561/PyShellForge/blob/main/CHANGELOG.md"
|
|
86
|
+
|
|
87
|
+
[tool.hatch.build.targets.wheel]
|
|
88
|
+
packages = ["src/pyshellforge"]
|
|
89
|
+
|
|
90
|
+
[tool.hatch.build.targets.sdist]
|
|
91
|
+
# The sdist carries what the suite needs if it is ever unpacked and tested:
|
|
92
|
+
include = ["src", "tests", "features", "README.md"]
|
|
93
|
+
|
|
94
|
+
# Run from the repository root with an explicit configuration file:
|
|
95
|
+
# python -m pytest -c packages/pyshellforge/pyproject.toml # the product suite
|
|
96
|
+
# python -m pytest -c packages/pyshellforge/pyproject.toml ../automation/tests
|
|
97
|
+
# `pythonpath` is resolved against this file, so "src" is the package tree and
|
|
98
|
+
# "../.." is the repository root the suites import `automation` from. The automation
|
|
99
|
+
# suite brings its own conftest: its scenarios and step definitions live beside
|
|
100
|
+
# the automation, outside this rootdir.
|
|
101
|
+
[tool.pytest.ini_options]
|
|
102
|
+
minversion = "8.0"
|
|
103
|
+
testpaths = ["tests"]
|
|
104
|
+
pythonpath = ["src", "../.."]
|
|
105
|
+
addopts = "-ra --strict-markers"
|
|
106
|
+
markers = [
|
|
107
|
+
"integration: end-to-end rendering and shell syntax checks",
|
|
108
|
+
"shellcheck: requires the shellcheck binary",
|
|
109
|
+
"tooling: automation scenarios (automation/features/)",
|
|
110
|
+
"product: product behaviour scenarios (packages/pyshellforge/features/)",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
# ruff configuration: ../../ruff.toml
|
|
114
|
+
# coverage configuration: ../../.coveragerc
|
|
115
|
+
|
|
116
|
+
# Clean Code linting: `python -m automation.clean_code` runs
|
|
117
|
+
# python -m rude check --config packages/pyshellforge/pyproject.toml <paths>
|
|
118
|
+
# over the same trees mypy checks. Only the CC family is selected, because ruff
|
|
119
|
+
# owns everything else; the rules come from the rude-clean-code plugin, which
|
|
120
|
+
# rude discovers through its `rude.plugins` entry point.
|
|
121
|
+
[tool.rude]
|
|
122
|
+
select = ["CC"]
|
|
123
|
+
|
|
124
|
+
[tool.rude.rules.CC101]
|
|
125
|
+
# The rule counts the whole definition: signature, docstring and blank lines.
|
|
126
|
+
# Every function in this repository carries a docstring, so 60 keeps a function
|
|
127
|
+
# to one operation while leaving room for the prose that explains it.
|
|
128
|
+
max_lines = 60
|
|
129
|
+
|
|
130
|
+
# Types are checked with `mypy --config-file packages/pyshellforge/pyproject.toml` from the
|
|
131
|
+
# repository root; mypy resolves these paths against the working directory.
|
|
132
|
+
[tool.mypy]
|
|
133
|
+
python_version = "3.11"
|
|
134
|
+
files = ["packages/pyshellforge/src", "packages/pyshellforge-rude/src", "automation", "dodo.py"]
|
|
135
|
+
# `src/` layout: name the module base explicitly so `src/pyshellforge` is checked
|
|
136
|
+
# as `pyshellforge`, not `pyshellforge.src.pyshellforge`. The second entry is the
|
|
137
|
+
# BDD step directory, whose modules the suite imports by bare name.
|
|
138
|
+
mypy_path = "packages/pyshellforge/src:packages/pyshellforge-rude/src:automation/tests/bdd"
|
|
139
|
+
explicit_package_bases = true
|
|
140
|
+
warn_unused_configs = true
|
|
141
|
+
warn_redundant_casts = true
|
|
142
|
+
warn_unused_ignores = true
|
|
143
|
+
disallow_untyped_defs = true
|
|
144
|
+
disallow_incomplete_defs = true
|
|
145
|
+
no_implicit_optional = true
|
|
146
|
+
check_untyped_defs = true
|
|
147
|
+
strict_equality = true
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""PyShellForge — the package surface.
|
|
2
|
+
|
|
3
|
+
The package exposes the script container, initial nodes and Bash renderer.
|
|
4
|
+
|
|
5
|
+
The version has a single source of truth, ``[project].version`` in
|
|
6
|
+
``pyproject.toml``. Reading it at import time keeps the value honest — there is no
|
|
7
|
+
second copy to forget — and it needs care in one respect: a wheel does not ship
|
|
8
|
+
``pyproject.toml``. The order below therefore degrades instead of failing, so that
|
|
9
|
+
``import pyshellforge`` works in a source checkout, from an installed
|
|
10
|
+
distribution and from a vendored package:
|
|
11
|
+
|
|
12
|
+
1. ``pyproject.toml`` found by walking up from this file (source checkout);
|
|
13
|
+
2. installed distribution metadata (wheel, sdist, editable install);
|
|
14
|
+
3. :data:`FALLBACK_VERSION`, which exists so that an import never raises.
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
>>> import pyshellforge
|
|
19
|
+
>>> pyshellforge.VERSION
|
|
20
|
+
'0.1.0'
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
import tomllib
|
|
27
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from .ast import Command, Comment, Echo, Node, VariableAssignment
|
|
31
|
+
from .core import ShellScript
|
|
32
|
+
from .renderers import InterpolationError, UnsupportedNodeError
|
|
33
|
+
from .renderers.bash import BashRenderer
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"VERSION",
|
|
37
|
+
"BashRenderer",
|
|
38
|
+
"Command",
|
|
39
|
+
"Comment",
|
|
40
|
+
"Echo",
|
|
41
|
+
"InterpolationError",
|
|
42
|
+
"Node",
|
|
43
|
+
"ShellScript",
|
|
44
|
+
"UnsupportedNodeError",
|
|
45
|
+
"VariableAssignment",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
#: Distribution name used for metadata lookups.
|
|
49
|
+
DIST_NAME = "pyshellforge"
|
|
50
|
+
|
|
51
|
+
#: File searched for when no metadata is installed.
|
|
52
|
+
PYPROJECT_NAME = "pyproject.toml"
|
|
53
|
+
|
|
54
|
+
#: Last resort, so that a vendored package still imports.
|
|
55
|
+
FALLBACK_VERSION = "0.1.0"
|
|
56
|
+
|
|
57
|
+
#: Conservative check so a malformed file cannot leak garbage into a version string.
|
|
58
|
+
VERSION_RE = re.compile(r"^\d+(\.\d+)*([.-]?[0-9A-Za-z.]+)?$")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_pyproject(start: str | Path | None = None) -> Path | None:
|
|
62
|
+
"""Return the ``pyproject.toml`` above ``start`` (default: this file), or ``None``."""
|
|
63
|
+
location = Path(start) if start is not None else Path(__file__)
|
|
64
|
+
try:
|
|
65
|
+
location = location.resolve()
|
|
66
|
+
except OSError: # pragma: no cover - broken symlink
|
|
67
|
+
return None
|
|
68
|
+
directory = location if location.is_dir() else location.parent
|
|
69
|
+
for candidate_dir in (directory, *directory.parents):
|
|
70
|
+
candidate = candidate_dir / PYPROJECT_NAME
|
|
71
|
+
if candidate.is_file():
|
|
72
|
+
return candidate
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def read_version(pyproject: str | Path) -> str | None:
|
|
77
|
+
"""Return ``[project].version`` from ``pyproject.toml``, or ``None`` when unusable."""
|
|
78
|
+
path = Path(pyproject)
|
|
79
|
+
try:
|
|
80
|
+
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
81
|
+
except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError):
|
|
82
|
+
return None
|
|
83
|
+
project = data.get("project")
|
|
84
|
+
if not isinstance(project, dict):
|
|
85
|
+
return None
|
|
86
|
+
value = project.get("version")
|
|
87
|
+
if not isinstance(value, str) or not VERSION_RE.match(value.strip()):
|
|
88
|
+
return None
|
|
89
|
+
return value.strip()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def installed_version(dist: str = DIST_NAME) -> str | None:
|
|
93
|
+
"""Return the version recorded in installed distribution metadata, or ``None``."""
|
|
94
|
+
try:
|
|
95
|
+
return version(dist)
|
|
96
|
+
except PackageNotFoundError:
|
|
97
|
+
return None
|
|
98
|
+
except Exception: # pragma: no cover - defensive: corrupt metadata
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def resolve_version() -> str:
|
|
103
|
+
"""Resolve the project version in the documented order."""
|
|
104
|
+
pyproject = find_pyproject()
|
|
105
|
+
if pyproject is not None:
|
|
106
|
+
resolved = read_version(pyproject)
|
|
107
|
+
if resolved:
|
|
108
|
+
return resolved
|
|
109
|
+
return installed_version() or FALLBACK_VERSION
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
#: The project version, read from ``pyproject.toml`` (see :func:`resolve_version`).
|
|
113
|
+
VERSION = resolve_version()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Shell-neutral building blocks for a script."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .base import Node
|
|
8
|
+
from .nodes.echo import Echo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Comment(Node):
|
|
13
|
+
"""A comment, with no leading comment marker."""
|
|
14
|
+
|
|
15
|
+
text: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class VariableAssignment(Node):
|
|
20
|
+
"""Assign a literal value to a shell variable."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
value: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Command(Node):
|
|
28
|
+
"""A command and literal arguments, never a raw shell fragment."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
args: tuple[str, ...] = ()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["Command", "Comment", "Echo", "Node", "VariableAssignment"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Additional shell-neutral nodes."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""A safe, explicit template for printing text."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..base import Node
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Echo(Node):
|
|
9
|
+
"""Print a template: ``{NAME}`` expands a declared variable, ``{{`` is literal.
|
|
10
|
+
|
|
11
|
+
``{NAME!q}`` requests Bash's runtime shell quoting; other conversions and
|
|
12
|
+
format specifications are not supported. Templates are strings, not Python
|
|
13
|
+
f-strings, so Python cannot interpolate them before the renderer sees them.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, template: str) -> None:
|
|
17
|
+
self.template = template
|