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.
Files changed (29) hide show
  1. pyshellforge-0.1.0/.gitignore +61 -0
  2. pyshellforge-0.1.0/PKG-INFO +154 -0
  3. pyshellforge-0.1.0/README.md +106 -0
  4. pyshellforge-0.1.0/features/bash_rendering.feature +10 -0
  5. pyshellforge-0.1.0/pyproject.toml +147 -0
  6. pyshellforge-0.1.0/src/pyshellforge/__init__.py +113 -0
  7. pyshellforge-0.1.0/src/pyshellforge/ast/__init__.py +34 -0
  8. pyshellforge-0.1.0/src/pyshellforge/ast/base.py +5 -0
  9. pyshellforge-0.1.0/src/pyshellforge/ast/nodes/__init__.py +1 -0
  10. pyshellforge-0.1.0/src/pyshellforge/ast/nodes/echo.py +17 -0
  11. pyshellforge-0.1.0/src/pyshellforge/core/__init__.py +36 -0
  12. pyshellforge-0.1.0/src/pyshellforge/renderers/__init__.py +14 -0
  13. pyshellforge-0.1.0/src/pyshellforge/renderers/bash.py +157 -0
  14. pyshellforge-0.1.0/tests/bdd/__init__.py +1 -0
  15. pyshellforge-0.1.0/tests/bdd/steps_render.py +42 -0
  16. pyshellforge-0.1.0/tests/bdd/test_render.py +7 -0
  17. pyshellforge-0.1.0/tests/golden/hello.sh +5 -0
  18. pyshellforge-0.1.0/tests/integration/test_bash_syntax.py +60 -0
  19. pyshellforge-0.1.0/tests/integration/test_readme.py +349 -0
  20. pyshellforge-0.1.0/tests/unit/test_bash_renderer.py +169 -0
  21. pyshellforge-0.1.0/tests/unit/test_version.py +149 -0
  22. pyshellforge-0.0.0/PKG-INFO +0 -4
  23. pyshellforge-0.0.0/pyproject.toml +0 -11
  24. pyshellforge-0.0.0/setup.cfg +0 -4
  25. pyshellforge-0.0.0/src/pyshellforge.egg-info/PKG-INFO +0 -4
  26. pyshellforge-0.0.0/src/pyshellforge.egg-info/SOURCES.txt +0 -6
  27. pyshellforge-0.0.0/src/pyshellforge.egg-info/dependency_links.txt +0 -1
  28. pyshellforge-0.0.0/src/pyshellforge.egg-info/top_level.txt +0 -1
  29. /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,5 @@
1
+ """Base type shared by shell-neutral nodes."""
2
+
3
+
4
+ class Node:
5
+ """Base class for a shell construct."""
@@ -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