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 .tsxtsconfig.json path aliases and follows barrel re-exports..js .jsx.py
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:
defandasync def→ kindfunctionclass→ kindclass- 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:
-
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. -
Otherwise Python's
import *convention applies: names beginning with an underscore are private and excluded.
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 mod → pkg/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
ifandtrybodies — which coversTYPE_CHECKINGguards andtry/except ImportErrorfallbacks. 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__.pyfiles, 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, sotest_*.pyand*_test.pyare scanned and will be summarized. Add them torepos[].ignoreif 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.