Metadata-Version: 2.4
Name: xpline
Version: 0.1.4
Summary: Bundle a Python program into a single readable source file.
License-Expression: MIT
Project-URL: Homepage, https://github.com/Naimy441/xpline
Project-URL: Repository, https://github.com/Naimy441/xpline
Project-URL: Issues, https://github.com/Naimy441/xpline/issues
Project-URL: Changelog, https://github.com/Naimy441/xpline/blob/main/CHANGELOG.md
Keywords: bundler,inline,ast,single-file
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: numpy; extra == "dev"
Dynamic: license-file

# xpline

Explain + Inline = Expline → Xpline

Flatten a Python program into one readable `.py` file. xpline follows imports, keeps reachable source, and emits a single file you can still read and run.

Local modules and many pure-Python third-party packages are inlined. The standard library, C extensions, and a few identity-sensitive modules stay as normal imports. This is not PyInstaller, zipapp, or a dependency freezer — the output still needs a Python interpreter and whatever it still imports.

Requires **Python 3.9+**.

## Install

```bash
pip install xpline
```

From a checkout:

```bash
pip install .
```

Or skip install and use the self-bundled CLI:

```bash
python3 dist/xpline.py app.py -o app_bundle.py
```

## Usage

```bash
xpline app.py -o app_bundle.py
python3 -m xpline app.py -o app_bundle.py
xpline app.py -o app_bundle.py -n 0          # this file only; leave imports as imports
xpline app.py -o app_bundle.py -n 1          # inline what this file imports
xpline app.py -o app_bundle.py -n 2          # one more hop of imports
```

Without `-n`, xpline follows every inlinable import (including third-party pure Python). `-n` limits how many import hops to recurse.

From a checkout, `python3 bundler.py app.py -o app_bundle.py` is the same command.

### Library

```python
from pathlib import Path
from xpline import bundle_file

bundle_file(Path("app.py"), Path("app_bundle.py"))
```

### Debug a run

`--debug-dir` writes `01_load.json`, `02_analyze.json`, `03_names.json`, and `04_emit.py` so you can see what each stage decided.

```bash
xpline app.py -o app_bundle.py --debug-dir .xpline-debug
xpline --list-stages
xpline --version
xpline --self-bundle -o dist/xpline.py
```

## What stays as an import

- The standard library
- C extensions, builtins, and frozen modules
- `matplotlib.font_manager` — Agg/FreeType compare `FontProperties` by class identity
- Anything xpline cannot load as `.py` source

## Limitations

- `if TYPE_CHECKING:` blocks are dropped
- Analysis errors out after 50,000 reachable symbols
- Bundled output may include `_xpline_setglobal` or `__xpline_restore_class_name` helpers when flattening needs them

## How it works

Source lives in `xpline/`. Each stage is its own module so you can read and debug them separately. Those stages are then bundled back into `dist/xpline.py` to show the tool working on itself.

```
xpline/
  models.py          shared types
  astutil.py         AST helpers
  resolve.py         module lookup
  pipeline.py        runs the stages
  cli.py
  stages/
    load.py          01  parse and index
    analyze.py       02  keep reachable code
    names.py         03  allocate flatten-safe names
    emit.py          04  rewrite and interleave
    rewrite.py       AST rewrite used by emit
tests/
dist/xpline.py       self-bundled CLI (generated)
bundler.py           thin wrapper: python3 bundler.py in.py -o out.py
```

## Develop

```bash
pip install -e ".[dev]"
python3 -m unittest tests.test_pipeline tests.test_bundler
```

`tests/test_popular_modules.py`, `tests/test_extra_modules.py`, and `tests/test_matplotlib.py` skip packages that are not installed.

## Release

Pushing a version tag publishes to PyPI after CI tests pass. Do not publish from `main` — `0.1.0` is already on PyPI and cannot be replaced.

1. Bump the same version in `xpline/_version.py` and `pyproject.toml`, and add a `CHANGELOG.md` section.
2. Commit and push to `main`.
3. Tag and push:

```bash
git tag v0.1.5
git push origin v0.1.5
```

CI runs the core tests on 3.9, 3.11, and 3.12. If they all pass, it rebuilds `dist/xpline.py`, builds the sdist and wheel, uploads them to PyPI, and creates a GitHub Release.

### One-time trusted publishing

No PyPI token goes in the repo. After this is set up, only the tag is needed.

1. GitHub repo **Settings → Environments → New environment** named `pypi`. Require reviewers if you want a click before each upload.
2. [pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/) → add a trusted publisher:
   - PyPI project: `xpline`
   - Owner: `Naimy441`
   - Repository: `xpline`
   - Workflow: `ci.yml`
   - Environment: `pypi`

## License

MIT
