pobol 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.
- pobol-0.1.0/PKG-INFO +208 -0
- pobol-0.1.0/README.md +200 -0
- pobol-0.1.0/pyproject.toml +22 -0
- pobol-0.1.0/src/pobol/__init__.py +25 -0
- pobol-0.1.0/src/pobol/compiler.py +79 -0
- pobol-0.1.0/src/pobol/copybook.py +283 -0
- pobol-0.1.0/src/pobol/exceptions.py +35 -0
- pobol-0.1.0/src/pobol/program.py +366 -0
- pobol-0.1.0/src/pobol/py.typed +0 -0
- pobol-0.1.0/src/pobol/source_parser.py +480 -0
pobol-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pobol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Call COBOL programs as Python functions — file I/O handled for you
|
|
5
|
+
Author-email: andy@andyreagan.com
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# pobol
|
|
10
|
+
|
|
11
|
+
[](https://github.com/andyreagan/pobol/actions/workflows/ci.yml)
|
|
12
|
+
[](https://pypi.org/project/pobol/)
|
|
13
|
+
|
|
14
|
+
Call COBOL programs as Python functions. Like [maturin](https://github.com/PyO3/maturin) for Rust, but for GnuCOBOL.
|
|
15
|
+
|
|
16
|
+
**Motivation:** You're migrating COBOL off a mainframe. The programs do real work you want to keep, but they expect VSAM files, DD names, and JCL — none of which exist on Linux/macOS. pobol wraps `cobc` (GnuCOBOL) so you can:
|
|
17
|
+
|
|
18
|
+
1. Point it at a `.cbl` file — it parses the source and discovers everything
|
|
19
|
+
2. Pass Python dicts as input records
|
|
20
|
+
3. Get Python dicts back from the output files
|
|
21
|
+
|
|
22
|
+
No copybook transcription, no temp-file juggling, no batch scripts.
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv add pobol # or: pip install pobol
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Zero-config mode (auto-discovery)
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from pobol import load
|
|
34
|
+
|
|
35
|
+
# pobol parses the COBOL source, discovers SELECT/FD clauses,
|
|
36
|
+
# extracts record layouts, strips mainframe artifacts, compiles, and
|
|
37
|
+
# handles all file I/O automatically.
|
|
38
|
+
report = load("customer_report.cob")
|
|
39
|
+
|
|
40
|
+
print(report.file_info)
|
|
41
|
+
# COBOL Program: customer_report.cob
|
|
42
|
+
#
|
|
43
|
+
# INPUTS:
|
|
44
|
+
# INPUT-FILE (45 bytes): IN-CUST-ID, IN-CUST-NAME, IN-BALANCE
|
|
45
|
+
# OUTPUTS:
|
|
46
|
+
# OUTPUT-FILE (54 bytes): OUT-CUST-ID, OUT-CUST-NAME, OUT-BALANCE, OUT-DISCOUNT
|
|
47
|
+
|
|
48
|
+
result = report(input_file=[
|
|
49
|
+
{"IN-CUST-ID": 1, "IN-CUST-NAME": "Alice", "IN-BALANCE": 2500.00},
|
|
50
|
+
{"IN-CUST-ID": 2, "IN-CUST-NAME": "Bob", "IN-BALANCE": 800.00},
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
for rec in result.output_file:
|
|
54
|
+
print(rec)
|
|
55
|
+
# {'out_cust_id': 1, 'out_cust_name': 'Alice', 'out_balance': 2500.0, 'out_discount': 250.0}
|
|
56
|
+
# {'out_cust_id': 2, 'out_cust_name': 'Bob', 'out_balance': 800.0, 'out_discount': 0.0}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Mainframe source — works directly
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# Point at raw mainframe COBOL with sequence numbers, LABEL RECORDS,
|
|
63
|
+
# IBM-370, DD-name assigns — pobol handles it all:
|
|
64
|
+
prog = load("check_disbursement.cbl")
|
|
65
|
+
|
|
66
|
+
print(prog.file_info)
|
|
67
|
+
# Discovers all 6 files, 4 record layouts under one FD, 60+ fields...
|
|
68
|
+
# Strips sequence numbers, rewrites ASSIGN TO for env-var mapping,
|
|
69
|
+
# removes LABEL RECORDS/RECORDING MODE/BLOCK CONTAINS.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Explicit copybooks (optional override)
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from pobol import load, parse_copybook
|
|
76
|
+
|
|
77
|
+
input_cb = parse_copybook("""
|
|
78
|
+
01 INPUT-RECORD.
|
|
79
|
+
05 CUST-ID PIC 9(6).
|
|
80
|
+
05 CUST-NAME PIC X(30).
|
|
81
|
+
05 BALANCE PIC 9(7)V9(2).
|
|
82
|
+
""")
|
|
83
|
+
output_cb = parse_copybook("""
|
|
84
|
+
01 OUTPUT-RECORD.
|
|
85
|
+
05 CUST-ID PIC 9(6).
|
|
86
|
+
05 CUST-NAME PIC X(30).
|
|
87
|
+
05 BALANCE PIC 9(7)V9(2).
|
|
88
|
+
05 DISCOUNT PIC 9(7)V9(2).
|
|
89
|
+
""")
|
|
90
|
+
|
|
91
|
+
report = load(
|
|
92
|
+
"customer_report.cob",
|
|
93
|
+
inputs={"INPUT-FILE": input_cb},
|
|
94
|
+
outputs={"OUTPUT-FILE": output_cb},
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## How It Works
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
┌──────────────────┐
|
|
102
|
+
Python dict ──▶ Copybook.encode() ──▶ temp file ──▶ │ │
|
|
103
|
+
│ cobc-compiled │
|
|
104
|
+
Python dict ◀── Copybook.decode() ◀── temp file ◀── │ COBOL program │
|
|
105
|
+
│ │
|
|
106
|
+
└──────────────────┘
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
1. **Parse source** — discovers SELECT/FD/OPEN clauses, extracts PIC field layouts, detects input vs output files
|
|
110
|
+
2. **Strip mainframe** — removes sequence numbers (cols 1-6), identification (cols 73-80), LABEL RECORDS, RECORDING MODE, BLOCK CONTAINS, fixes SOURCE-COMPUTER
|
|
111
|
+
3. **Rewrite assigns** — converts `ASSIGN TO DATAIN` (DD names) to working-storage paths loaded from `DD_*` environment variables
|
|
112
|
+
4. **Compile** — `cobc -x` compiles to a native executable (cached by content hash)
|
|
113
|
+
5. **Write inputs** — your Python dicts are encoded as fixed-width records to temp files
|
|
114
|
+
6. **Run** — the executable runs with `DD_*` env vars pointing to temp files
|
|
115
|
+
7. **Read outputs** — output temp files are decoded back into Python dicts
|
|
116
|
+
|
|
117
|
+
## Handling Multiple Record Types
|
|
118
|
+
|
|
119
|
+
Real mainframe COBOL often has multiple 01-level records under one FD (header, detail, trailer). pobol discovers all of them:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
prog = load("check_disbursement.cbl")
|
|
123
|
+
|
|
124
|
+
# See all discovered layouts
|
|
125
|
+
for file_name, layouts in prog.record_layouts.items():
|
|
126
|
+
for rec_name, copybook in layouts.items():
|
|
127
|
+
print(f"{file_name}/{rec_name}: {len(copybook.fields)} fields")
|
|
128
|
+
# TXN-DATA-FILE/TXN-DATA-RECORD: 2 fields
|
|
129
|
+
# TXN-DATA-FILE/TXN-DATA-HDR-RECORD: 5 fields
|
|
130
|
+
# TXN-DATA-FILE/TXN-DATA-DETAIL-RECORD: 60 fields
|
|
131
|
+
# TXN-DATA-FILE/TXN-DATA-TRLR-RECORD: 5 fields
|
|
132
|
+
|
|
133
|
+
# For multi-record files, use raw_files with pre-encoded bytes:
|
|
134
|
+
header_bytes = header_copybook.encode(header_dict)
|
|
135
|
+
detail_bytes = detail_copybook.encode_many(detail_dicts)
|
|
136
|
+
trailer_bytes = trailer_copybook.encode(trailer_dict)
|
|
137
|
+
|
|
138
|
+
result = prog(raw_files={
|
|
139
|
+
"TXN-DATA-FILE": header_bytes + detail_bytes + trailer_bytes,
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## API Reference
|
|
144
|
+
|
|
145
|
+
### `load(source, **kwargs) → CobolProgram`
|
|
146
|
+
|
|
147
|
+
Compile a COBOL source file and return a callable.
|
|
148
|
+
|
|
149
|
+
| Parameter | Type | Default | Description |
|
|
150
|
+
|-----------|------|---------|-------------|
|
|
151
|
+
| `source` | `str \| Path` | required | Path to .cob/.cbl file |
|
|
152
|
+
| `inputs` | `dict[str, Copybook]` | `None` | Override auto-discovered input layouts |
|
|
153
|
+
| `outputs` | `dict[str, Copybook]` | `None` | Override auto-discovered output layouts |
|
|
154
|
+
| `dialect` | `str` | `None` | GnuCOBOL `-std=` dialect |
|
|
155
|
+
| `extra_flags` | `list[str]` | `None` | Extra `cobc` flags |
|
|
156
|
+
| `strip_mainframe` | `bool` | `True` | Strip mainframe format artifacts |
|
|
157
|
+
| `rewrite_assigns` | `bool` | `True` | Rewrite ASSIGN TO for env-var mapping |
|
|
158
|
+
|
|
159
|
+
### `CobolProgram.__call__(**kwargs) → CobolResult`
|
|
160
|
+
|
|
161
|
+
| Parameter | Type | Description |
|
|
162
|
+
|-----------|------|-------------|
|
|
163
|
+
| `stdin` | `str` | Data for ACCEPT/stdin |
|
|
164
|
+
| `env` | `dict` | Extra environment variables |
|
|
165
|
+
| `timeout` | `float` | Execution timeout (default 30s) |
|
|
166
|
+
| `raw_files` | `dict[str, bytes]` | Pre-encoded file data by SELECT name |
|
|
167
|
+
| `**file_inputs` | `list[dict]` | Input data as list of dicts, keyed by SELECT name |
|
|
168
|
+
|
|
169
|
+
### `CobolResult`
|
|
170
|
+
|
|
171
|
+
| Attribute | Type | Description |
|
|
172
|
+
|-----------|------|-------------|
|
|
173
|
+
| `.stdout` | `str` | Program stdout (DISPLAY output) |
|
|
174
|
+
| `.stderr` | `str` | Program stderr |
|
|
175
|
+
| `.return_code` | `int` | Exit code |
|
|
176
|
+
| `.outputs` | `dict[str, list[dict]]` | Decoded output files |
|
|
177
|
+
| `.output_file` | `list[dict]` | Shorthand for `.outputs["OUTPUT-FILE"]` |
|
|
178
|
+
|
|
179
|
+
### `parse_cobol_source(source, **kwargs) → ParsedSource`
|
|
180
|
+
|
|
181
|
+
Parse without compiling. Returns discovered files, record layouts, and cleaned source.
|
|
182
|
+
|
|
183
|
+
### Supported PIC Clauses
|
|
184
|
+
|
|
185
|
+
| PIC | Python type | Notes |
|
|
186
|
+
|-----|-------------|-------|
|
|
187
|
+
| `X(n)`, `XX` | `str` | Left-justified, space-padded |
|
|
188
|
+
| `9(n)`, `999` | `int` | Zero-padded |
|
|
189
|
+
| `S9(n)` | `int` | Trailing sign overpunch |
|
|
190
|
+
| `9(n)V9(m)`, `9(5)V99` | `float` | Implied decimal (mixed forms supported) |
|
|
191
|
+
| `S9(n)V9(m)` | `float` | Signed implied decimal |
|
|
192
|
+
|
|
193
|
+
## Development
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
uv sync
|
|
197
|
+
uv run pytest -v
|
|
198
|
+
uv run python examples/demo.py
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Prerequisites
|
|
202
|
+
|
|
203
|
+
- **GnuCOBOL** (`cobc`) — `brew install gnucobol` / `apt install gnucobol`
|
|
204
|
+
- **Python 3.11+**
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
pobol-0.1.0/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# pobol
|
|
2
|
+
|
|
3
|
+
[](https://github.com/andyreagan/pobol/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/pobol/)
|
|
5
|
+
|
|
6
|
+
Call COBOL programs as Python functions. Like [maturin](https://github.com/PyO3/maturin) for Rust, but for GnuCOBOL.
|
|
7
|
+
|
|
8
|
+
**Motivation:** You're migrating COBOL off a mainframe. The programs do real work you want to keep, but they expect VSAM files, DD names, and JCL — none of which exist on Linux/macOS. pobol wraps `cobc` (GnuCOBOL) so you can:
|
|
9
|
+
|
|
10
|
+
1. Point it at a `.cbl` file — it parses the source and discovers everything
|
|
11
|
+
2. Pass Python dicts as input records
|
|
12
|
+
3. Get Python dicts back from the output files
|
|
13
|
+
|
|
14
|
+
No copybook transcription, no temp-file juggling, no batch scripts.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv add pobol # or: pip install pobol
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Zero-config mode (auto-discovery)
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pobol import load
|
|
26
|
+
|
|
27
|
+
# pobol parses the COBOL source, discovers SELECT/FD clauses,
|
|
28
|
+
# extracts record layouts, strips mainframe artifacts, compiles, and
|
|
29
|
+
# handles all file I/O automatically.
|
|
30
|
+
report = load("customer_report.cob")
|
|
31
|
+
|
|
32
|
+
print(report.file_info)
|
|
33
|
+
# COBOL Program: customer_report.cob
|
|
34
|
+
#
|
|
35
|
+
# INPUTS:
|
|
36
|
+
# INPUT-FILE (45 bytes): IN-CUST-ID, IN-CUST-NAME, IN-BALANCE
|
|
37
|
+
# OUTPUTS:
|
|
38
|
+
# OUTPUT-FILE (54 bytes): OUT-CUST-ID, OUT-CUST-NAME, OUT-BALANCE, OUT-DISCOUNT
|
|
39
|
+
|
|
40
|
+
result = report(input_file=[
|
|
41
|
+
{"IN-CUST-ID": 1, "IN-CUST-NAME": "Alice", "IN-BALANCE": 2500.00},
|
|
42
|
+
{"IN-CUST-ID": 2, "IN-CUST-NAME": "Bob", "IN-BALANCE": 800.00},
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
for rec in result.output_file:
|
|
46
|
+
print(rec)
|
|
47
|
+
# {'out_cust_id': 1, 'out_cust_name': 'Alice', 'out_balance': 2500.0, 'out_discount': 250.0}
|
|
48
|
+
# {'out_cust_id': 2, 'out_cust_name': 'Bob', 'out_balance': 800.0, 'out_discount': 0.0}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Mainframe source — works directly
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Point at raw mainframe COBOL with sequence numbers, LABEL RECORDS,
|
|
55
|
+
# IBM-370, DD-name assigns — pobol handles it all:
|
|
56
|
+
prog = load("check_disbursement.cbl")
|
|
57
|
+
|
|
58
|
+
print(prog.file_info)
|
|
59
|
+
# Discovers all 6 files, 4 record layouts under one FD, 60+ fields...
|
|
60
|
+
# Strips sequence numbers, rewrites ASSIGN TO for env-var mapping,
|
|
61
|
+
# removes LABEL RECORDS/RECORDING MODE/BLOCK CONTAINS.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Explicit copybooks (optional override)
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pobol import load, parse_copybook
|
|
68
|
+
|
|
69
|
+
input_cb = parse_copybook("""
|
|
70
|
+
01 INPUT-RECORD.
|
|
71
|
+
05 CUST-ID PIC 9(6).
|
|
72
|
+
05 CUST-NAME PIC X(30).
|
|
73
|
+
05 BALANCE PIC 9(7)V9(2).
|
|
74
|
+
""")
|
|
75
|
+
output_cb = parse_copybook("""
|
|
76
|
+
01 OUTPUT-RECORD.
|
|
77
|
+
05 CUST-ID PIC 9(6).
|
|
78
|
+
05 CUST-NAME PIC X(30).
|
|
79
|
+
05 BALANCE PIC 9(7)V9(2).
|
|
80
|
+
05 DISCOUNT PIC 9(7)V9(2).
|
|
81
|
+
""")
|
|
82
|
+
|
|
83
|
+
report = load(
|
|
84
|
+
"customer_report.cob",
|
|
85
|
+
inputs={"INPUT-FILE": input_cb},
|
|
86
|
+
outputs={"OUTPUT-FILE": output_cb},
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## How It Works
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
┌──────────────────┐
|
|
94
|
+
Python dict ──▶ Copybook.encode() ──▶ temp file ──▶ │ │
|
|
95
|
+
│ cobc-compiled │
|
|
96
|
+
Python dict ◀── Copybook.decode() ◀── temp file ◀── │ COBOL program │
|
|
97
|
+
│ │
|
|
98
|
+
└──────────────────┘
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
1. **Parse source** — discovers SELECT/FD/OPEN clauses, extracts PIC field layouts, detects input vs output files
|
|
102
|
+
2. **Strip mainframe** — removes sequence numbers (cols 1-6), identification (cols 73-80), LABEL RECORDS, RECORDING MODE, BLOCK CONTAINS, fixes SOURCE-COMPUTER
|
|
103
|
+
3. **Rewrite assigns** — converts `ASSIGN TO DATAIN` (DD names) to working-storage paths loaded from `DD_*` environment variables
|
|
104
|
+
4. **Compile** — `cobc -x` compiles to a native executable (cached by content hash)
|
|
105
|
+
5. **Write inputs** — your Python dicts are encoded as fixed-width records to temp files
|
|
106
|
+
6. **Run** — the executable runs with `DD_*` env vars pointing to temp files
|
|
107
|
+
7. **Read outputs** — output temp files are decoded back into Python dicts
|
|
108
|
+
|
|
109
|
+
## Handling Multiple Record Types
|
|
110
|
+
|
|
111
|
+
Real mainframe COBOL often has multiple 01-level records under one FD (header, detail, trailer). pobol discovers all of them:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
prog = load("check_disbursement.cbl")
|
|
115
|
+
|
|
116
|
+
# See all discovered layouts
|
|
117
|
+
for file_name, layouts in prog.record_layouts.items():
|
|
118
|
+
for rec_name, copybook in layouts.items():
|
|
119
|
+
print(f"{file_name}/{rec_name}: {len(copybook.fields)} fields")
|
|
120
|
+
# TXN-DATA-FILE/TXN-DATA-RECORD: 2 fields
|
|
121
|
+
# TXN-DATA-FILE/TXN-DATA-HDR-RECORD: 5 fields
|
|
122
|
+
# TXN-DATA-FILE/TXN-DATA-DETAIL-RECORD: 60 fields
|
|
123
|
+
# TXN-DATA-FILE/TXN-DATA-TRLR-RECORD: 5 fields
|
|
124
|
+
|
|
125
|
+
# For multi-record files, use raw_files with pre-encoded bytes:
|
|
126
|
+
header_bytes = header_copybook.encode(header_dict)
|
|
127
|
+
detail_bytes = detail_copybook.encode_many(detail_dicts)
|
|
128
|
+
trailer_bytes = trailer_copybook.encode(trailer_dict)
|
|
129
|
+
|
|
130
|
+
result = prog(raw_files={
|
|
131
|
+
"TXN-DATA-FILE": header_bytes + detail_bytes + trailer_bytes,
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## API Reference
|
|
136
|
+
|
|
137
|
+
### `load(source, **kwargs) → CobolProgram`
|
|
138
|
+
|
|
139
|
+
Compile a COBOL source file and return a callable.
|
|
140
|
+
|
|
141
|
+
| Parameter | Type | Default | Description |
|
|
142
|
+
|-----------|------|---------|-------------|
|
|
143
|
+
| `source` | `str \| Path` | required | Path to .cob/.cbl file |
|
|
144
|
+
| `inputs` | `dict[str, Copybook]` | `None` | Override auto-discovered input layouts |
|
|
145
|
+
| `outputs` | `dict[str, Copybook]` | `None` | Override auto-discovered output layouts |
|
|
146
|
+
| `dialect` | `str` | `None` | GnuCOBOL `-std=` dialect |
|
|
147
|
+
| `extra_flags` | `list[str]` | `None` | Extra `cobc` flags |
|
|
148
|
+
| `strip_mainframe` | `bool` | `True` | Strip mainframe format artifacts |
|
|
149
|
+
| `rewrite_assigns` | `bool` | `True` | Rewrite ASSIGN TO for env-var mapping |
|
|
150
|
+
|
|
151
|
+
### `CobolProgram.__call__(**kwargs) → CobolResult`
|
|
152
|
+
|
|
153
|
+
| Parameter | Type | Description |
|
|
154
|
+
|-----------|------|-------------|
|
|
155
|
+
| `stdin` | `str` | Data for ACCEPT/stdin |
|
|
156
|
+
| `env` | `dict` | Extra environment variables |
|
|
157
|
+
| `timeout` | `float` | Execution timeout (default 30s) |
|
|
158
|
+
| `raw_files` | `dict[str, bytes]` | Pre-encoded file data by SELECT name |
|
|
159
|
+
| `**file_inputs` | `list[dict]` | Input data as list of dicts, keyed by SELECT name |
|
|
160
|
+
|
|
161
|
+
### `CobolResult`
|
|
162
|
+
|
|
163
|
+
| Attribute | Type | Description |
|
|
164
|
+
|-----------|------|-------------|
|
|
165
|
+
| `.stdout` | `str` | Program stdout (DISPLAY output) |
|
|
166
|
+
| `.stderr` | `str` | Program stderr |
|
|
167
|
+
| `.return_code` | `int` | Exit code |
|
|
168
|
+
| `.outputs` | `dict[str, list[dict]]` | Decoded output files |
|
|
169
|
+
| `.output_file` | `list[dict]` | Shorthand for `.outputs["OUTPUT-FILE"]` |
|
|
170
|
+
|
|
171
|
+
### `parse_cobol_source(source, **kwargs) → ParsedSource`
|
|
172
|
+
|
|
173
|
+
Parse without compiling. Returns discovered files, record layouts, and cleaned source.
|
|
174
|
+
|
|
175
|
+
### Supported PIC Clauses
|
|
176
|
+
|
|
177
|
+
| PIC | Python type | Notes |
|
|
178
|
+
|-----|-------------|-------|
|
|
179
|
+
| `X(n)`, `XX` | `str` | Left-justified, space-padded |
|
|
180
|
+
| `9(n)`, `999` | `int` | Zero-padded |
|
|
181
|
+
| `S9(n)` | `int` | Trailing sign overpunch |
|
|
182
|
+
| `9(n)V9(m)`, `9(5)V99` | `float` | Implied decimal (mixed forms supported) |
|
|
183
|
+
| `S9(n)V9(m)` | `float` | Signed implied decimal |
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
uv sync
|
|
189
|
+
uv run pytest -v
|
|
190
|
+
uv run python examples/demo.py
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Prerequisites
|
|
194
|
+
|
|
195
|
+
- **GnuCOBOL** (`cobc`) — `brew install gnucobol` / `apt install gnucobol`
|
|
196
|
+
- **Python 3.11+**
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pobol"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Call COBOL programs as Python functions — file I/O handled for you"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ email = "andy@andyreagan.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["uv_build>=0.9.30,<0.10.0"]
|
|
14
|
+
build-backend = "uv_build"
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=8.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.pytest.ini_options]
|
|
22
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""pobol — call COBOL programs as Python functions."""
|
|
2
|
+
|
|
3
|
+
from pobol.program import CobolProgram, load
|
|
4
|
+
from pobol.copybook import Copybook, parse_copybook
|
|
5
|
+
from pobol.source_parser import parse_cobol_source, strip_mainframe_format, ParsedSource
|
|
6
|
+
from pobol.exceptions import (
|
|
7
|
+
CompileError,
|
|
8
|
+
CobolRuntimeError,
|
|
9
|
+
CopybookParseError,
|
|
10
|
+
PyCobolError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CobolProgram",
|
|
15
|
+
"load",
|
|
16
|
+
"Copybook",
|
|
17
|
+
"parse_copybook",
|
|
18
|
+
"parse_cobol_source",
|
|
19
|
+
"strip_mainframe_format",
|
|
20
|
+
"ParsedSource",
|
|
21
|
+
"CompileError",
|
|
22
|
+
"CobolRuntimeError",
|
|
23
|
+
"CopybookParseError",
|
|
24
|
+
"PyCobolError",
|
|
25
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Thin wrapper around the ``cobc`` compiler shipped with GnuCOBOL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pobol.exceptions import CompileError
|
|
12
|
+
|
|
13
|
+
# Default directory for caching compiled binaries
|
|
14
|
+
_CACHE_DIR = Path(tempfile.gettempdir()) / "pobol_cache"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _source_hash(source_path: Path) -> str:
|
|
18
|
+
"""Return a short content-hash of the source file for cache-busting."""
|
|
19
|
+
h = hashlib.sha256(source_path.read_bytes()).hexdigest()[:16]
|
|
20
|
+
return h
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def compile_program(
|
|
24
|
+
source_path: str | Path,
|
|
25
|
+
*,
|
|
26
|
+
output_dir: str | Path | None = None,
|
|
27
|
+
extra_flags: list[str] | None = None,
|
|
28
|
+
dialect: str | None = None,
|
|
29
|
+
force: bool = False,
|
|
30
|
+
) -> Path:
|
|
31
|
+
"""Compile a COBOL source file to an executable using ``cobc -x``.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
source_path:
|
|
36
|
+
Path to the ``.cob`` / ``.cbl`` file.
|
|
37
|
+
output_dir:
|
|
38
|
+
Where to put the binary. Defaults to a temp cache dir.
|
|
39
|
+
extra_flags:
|
|
40
|
+
Additional flags passed to ``cobc``.
|
|
41
|
+
dialect:
|
|
42
|
+
E.g. ``"ibm"``, ``"mf"``, ``"cobol85"`` — passed as ``-std=<dialect>``.
|
|
43
|
+
force:
|
|
44
|
+
Re-compile even if a cached binary exists.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
Path to the compiled executable.
|
|
49
|
+
"""
|
|
50
|
+
source_path = Path(source_path).resolve()
|
|
51
|
+
if not source_path.exists():
|
|
52
|
+
raise FileNotFoundError(f"COBOL source not found: {source_path}")
|
|
53
|
+
|
|
54
|
+
if output_dir is None:
|
|
55
|
+
output_dir = _CACHE_DIR
|
|
56
|
+
output_dir = Path(output_dir)
|
|
57
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
h = _source_hash(source_path)
|
|
60
|
+
exe_name = f"{source_path.stem}_{h}"
|
|
61
|
+
exe_path = output_dir / exe_name
|
|
62
|
+
|
|
63
|
+
if exe_path.exists() and not force:
|
|
64
|
+
return exe_path
|
|
65
|
+
|
|
66
|
+
cmd: list[str] = ["cobc", "-x", "-o", str(exe_path)]
|
|
67
|
+
if dialect:
|
|
68
|
+
cmd.append(f"-std={dialect}")
|
|
69
|
+
if extra_flags:
|
|
70
|
+
cmd.extend(extra_flags)
|
|
71
|
+
cmd.append(str(source_path))
|
|
72
|
+
|
|
73
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
74
|
+
if result.returncode != 0:
|
|
75
|
+
raise CompileError(str(source_path), result.returncode, result.stderr)
|
|
76
|
+
|
|
77
|
+
# Ensure executable bit
|
|
78
|
+
exe_path.chmod(exe_path.stat().st_mode | 0o111)
|
|
79
|
+
return exe_path
|