# stack


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

When a release fails, the traceback often runs through several of your
own packages, sometimes through installed copies of them rather than the
checkouts you edit. This module answers three questions: which of your
packages are installed, where an import of each one resolves to, and
which one raised an error.

Nothing here imports the packages it reports on. Every answer comes from
import metadata, a file path, or the text of a traceback, so a package
that fails to import can still be described.

`FAMILY` is the default list of your own packages: the ones pullup is
built from, and the only ones a report blames. A frame in `json` or
`asyncio` is someone else’s code and is passed over. Every function that
takes `family=` uses your list instead, and an empty `family` means
`FAMILY`.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/pullup/blob/main/pullup/stack.py#L25"
target="_blank" style="float:right; font-size:smaller">source</a>

### installed

``` python
def installed(
    name
):
```

*Whether `name` is importable, without importing it.*

[`installed`](https://vedicreader.github.io/pullup/stack.html#installed)
says whether Python can find the module, without importing it. A module
that is found but fails when imported still counts as installed.

``` python
installed('nbdev'), installed('no_such_package_here'), installed('')
```

    (True, False, False)

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/pullup/blob/main/pullup/stack.py#L57"
target="_blank" style="float:right; font-size:smaller">source</a>

### checkout_for

``` python
def checkout_for(
    name, checkouts
):
```

*The folder in `checkouts` that holds package `name`, found by its
`__init__.py` rather than the folder name.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/pullup/blob/main/pullup/stack.py#L76"
target="_blank" style="float:right; font-size:smaller">source</a>

### survey

``` python
def survey(
    family:tuple=(), checkouts:tuple=()
):
```

*One row per family package: installed, version, the copy an import
resolves to, and whether a checkout is shadowed.*

[`survey`](https://vedicreader.github.io/pullup/stack.html#survey)
describes each package in the family in one row: whether it is
installed, its version, and the path an import resolves to. `source` is
`checkout` when that path is a working tree and `site-packages` when it
is inside an installed environment, and `editable` is true in the first
case. Pass the folders you work in as `checkouts`, and `checkout` names
the one that holds the package. `shadowed` is true when you have a
checkout of a package but Python imports an installed copy instead, so
your edits have no effect:

``` python
[{k: r[k] for k in ('name', 'installed', 'source', 'shadowed')}
 for r in survey(family=['pullup', 'nbdev', 'dhrishti'])]
```

    [{'name': 'pullup',
      'installed': True,
      'source': 'checkout',
      'shadowed': False},
     {'name': 'nbdev',
      'installed': True,
      'source': 'site-packages',
      'shadowed': False},
     {'name': 'dhrishti', 'installed': False, 'source': '', 'shadowed': False}]

The checkout is matched by what is inside it rather than by its folder
name, so a fork called `nbdev-fork` still holds the `nbdev` package and
is still the checkout for it.

``` python
fork = root/'nbdev-fork'; (fork/'nbdev').mkdir(parents=True)
(fork/'nbdev'/'__init__.py').write_text('')
row = survey(family=['nbdev'], checkouts=[fork])[0]
{k: row[k] for k in ('source', 'editable', 'shadowed')}
```

    {'source': 'site-packages', 'editable': False, 'shadowed': True}

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/pullup/blob/main/pullup/stack.py#L82"
target="_blank" style="float:right; font-size:smaller">source</a>

### frames

``` python
def frames(
    text
):
```

*Every `File ..., line N, in f` in a traceback, oldest first, as the
terminal printed it.*

[`frames`](https://vedicreader.github.io/pullup/stack.html#frames) pulls
every `File "...", line N, in f` line out of a traceback’s text, oldest
first, as the terminal printed them. Text without frames gives an empty
list. The example builds a small package that raises:

``` python
pkg = root/'blowup'; pkg.mkdir()
(pkg/'__init__.py').write_text('')
(pkg/'core.py').write_text("""def push(remote):
    raise RuntimeError(f"no upstream for {remote}")

def sync():
    push("origin")
""")
sys.path.insert(0, str(root))
```

``` python
from blowup.core import sync
try: sync()
except RuntimeError: tb = traceback.format_exc()
print(tb.replace(str(root), '/proj'))
```

    Traceback (most recent call last):
      File "<ipython-input-23-1bc83a927e6d>", line 2, in <module>
        try: sync()
             ^^^^^^
      File "/proj/blowup/core.py", line 5, in sync
        push("origin")
      File "/proj/blowup/core.py", line 2, in push
        raise RuntimeError(f"no upstream for {remote}")
    RuntimeError: no upstream for origin

The deepest frame is the one that ran the `raise`, and it is the last
one [`frames`](https://vedicreader.github.io/pullup/stack.html#frames)
returns.

``` python
{**frames(tb)[-1], 'file': frames(tb)[-1]['file'].replace(str(root), '/proj')}
```

    {'file': '/proj/blowup/core.py', 'line': 2, 'fn': 'push'}

[`_package_of`](https://vedicreader.github.io/pullup/stack.html#_package_of)
names the family package a file belongs to. For a file under
`site-packages` it is the folder directly below `site-packages`, so a
copy vendored inside another package is not blamed. For any other path
it is the last folder whose name is in the family:

``` python
(_package_of('/home/me/code/gheasy/gheasy/repo.py', ['gheasy']),
 _package_of('/x/.venv/lib/python3.11/site-packages/vendorpkg/gheasy/repo.py', ['gheasy']),
 _package_of('/usr/lib/python3.11/json/decoder.py', ['gheasy']))
```

    ('gheasy', '', '')

[`_is_error`](https://vedicreader.github.io/pullup/stack.html#_is_error)
picks out the line that names the exception, such as
`RuntimeError: no upstream for origin` or a bare `RuntimeError`, and
rejects the header, the frame lines and echoed source lines.
[`blame`](https://vedicreader.github.io/pullup/stack.html#blame) reads
the traceback from the bottom and takes the first line it accepts:

``` python
[_is_error(l) for l in ('Traceback (most recent call last):',
                        '  File "core.py", line 2, in push',
                        '    if not upstream:',
                        'RuntimeError',
                        'RuntimeError: no upstream for origin')]
```

    [False, False, False, True, True]

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/pullup/blob/main/pullup/stack.py#L107"
target="_blank" style="float:right; font-size:smaller">source</a>

### blame

``` python
def blame(
    text, checkouts:tuple=(), family:NoneType=None
):
```

*Which family package raised, from the deepest family frame, and the
file to open, preferring your checkout.*

[`blame`](https://vedicreader.github.io/pullup/stack.html#blame) puts
these together. It finds the deepest frame that belongs to a family
package, takes the error line, and returns the package, file, line,
function, error, installed version and checkout. `open` is the file to
open in an editor. When no frame belongs to the family, `package` is
empty but `error` is still filled in:

``` python
r = blame(tb, checkouts=[root], family=['blowup'])
{k: (v.replace(str(root), '/proj') if isinstance(v, str) else v) for k, v in r.items()}
```

    {'package': 'blowup',
     'file': '/proj/blowup/core.py',
     'line': 2,
     'fn': 'push',
     'error': 'RuntimeError: no upstream for origin',
     'open': '/proj/blowup/core.py',
     'checkout': '/proj',
     'version': ''}

The traceback names the copy that ran. When you pass `checkouts` and
that copy was installed rather than checked out, `open` points at the
same file in your checkout, which is the copy you can edit:

``` python
other = root/'blowup-fork'; (other/'blowup').mkdir(parents=True)
(other/'blowup'/'__init__.py').write_text(''); (other/'blowup'/'core.py').write_text('')
moved = blame(tb, checkouts=[other], family=['blowup'])
moved['file'] == moved['open'], moved['open'].startswith(str(other))
```

    (False, True)

An exception raised with no message still has an error line, and it is
the class name alone.

In a chained traceback, `error` is the last exception raised and the
frame is the deepest family frame anywhere in the text. Those two are
not always parts of the same exception.
