Language support

Orstrum scans TypeScript, JavaScript, and Python. Every language is parsed from a real AST, but each one has its own notion of what an "export" is and how an import resolves to a file — this page is the reference for those differences.

Parsers

.ts .tsx
typescript
Parsed with ts-morph. Resolves tsconfig.json path aliases and follows barrel re-exports.
.js .jsx
javascript
Parsed with ts-morph, same resolution rules as TypeScript.
.py
python
Parsed with a bundled tree-sitter WASM grammar. No Python toolchain required on your machine.

Both parsers ship inside the npm package. The language is recorded per file at scan time and travels with the graph — see nodes.language.

What counts as an export in Python

Python has no export keyword, so Orstrum reconstructs a file's public surface from module scope. Anything defined at the top level of the module is a candidate:

  • def and async def → kind function
  • class → kind class
  • module-level assignment, including tuple unpacking and chained assignment → kind variable

Decorated definitions are unwrapped, so @dataclass, @pytest.fixture, and route decorators do not hide the symbol underneath. Subscript and attribute assignment targets (d["k"] = 1, obj.x = 1) are not exports.

That candidate list is then filtered, in this order:

  1. If the module declares __all__ as a literal list or tuple of strings, that wins — only the names it lists are exported. An __all__ that can't be resolved to a literal (e.g. __all__ = list(x.keys())) is ignored rather than treated as empty.
  2. Otherwise Python's import * convention applies: names beginning with an underscore are private and excluded.
Python files therefore only ever produce three of the nine export kinds: function, class, and variable. The type-level kinds (type, interface, enum, namespace) and the module-level kinds (reexport, default) are TypeScript/JavaScript only.

Python signatures

Python is largely untyped at the call site, so signatures are compact and parameter-name based rather than the full type signature you get from TypeScript:

# def run_backtest(symbol, start, end=None, **opts)
signature: "(symbol, start, end, opts)"

# async def fetch(session, url)
signature: "async (session, url)"

# class LiveRunner:  /  MAX_RETRIES = 5
signature: "class"
signature: "variable"

Python import resolution

Absolute imports are resolved against each of the repo's sourceRoots first, then the repo root itself. A dotted module path maps to a file:

from strategy.live_runner import LiveRunner

  # tries, in order:
   strategy/live_runner.py
   strategy/live_runner/__init__.py

Relative imports keep their leading dots and walk up the package tree — from ..lib import y resolves from the importing file's directory, and a bare from . import z resolves to the package's own __init__.py.

When the module part of a from … import doesn't resolve on its own and exactly one symbol is imported, Orstrum retries with that symbol appended (from pkg.sub import modpkg/sub/mod.py). This is the common PEP 420 namespace-package shape, where pkg/sub/ has no __init__.py to resolve against. Multi-symbol imports are left unresolved rather than guessed.

Dynamic imports are followed when the module name is a literal string — importlib.import_module("pkg.mod") produces a real edge, mirroring how literal import() and require() calls are handled in TS/JS. A non-literal argument can't be resolved to a file, so no edge is emitted.

An import that resolves to no file in the scan set is recorded as an external dependency with its top-level module name in packageName — this includes the standard library, so import argparse is stored the same way a third-party import requests is. Cross-repo Python edges use that same top-level name, matched against a repo's packageNames.

Known limitations

  • Import depth. Imports are collected at module top level, plus one hop into if and try bodies — which covers TYPE_CHECKING guards and try/except ImportError fallbacks. Imports nested deeper, such as inside a function body, are not tracked.
  • Ancestor packages. A dotted import links to the module it names and not to its parent packages' __init__.py files, even though Python executes those on import. This keeps every package root from becoming an artificial hub in the graph.
  • Test files. The built-in ignore defaults exclude TS/JS test files (*.test.ts, *.spec.ts) but have no Python equivalent, so test_*.py and *_test.py are scanned and will be summarized. Add them to repos[].ignore if you don't want them in the graph or on your summarize bill.
  • Cross-language edges. A Python file can't import a TS/JS file or vice versa, so no edge is ever created between them. A polyglot repo produces one graph with two disconnected regions.