logic.code component’s code in a namespace with no __import__ at all, so a surviving
import statement fails at runtime with ImportError: __import__ not found. Code that runs there has to be one
flat namespace with every import already resolved.
The compiler is what turns an ordinary Python package into that namespace. It runs inside
build and deploy, and in memory inside
diff. It parses with ast and never imports, executes or evaluates your code,
so compiling is safe on a package you have not run.
Everything up to the port is a pure function of the files on disk and the import allowlist. Nothing here talks to
the platform.
The walk
Starting fromentry_point, each module is parsed and every import / from ... import in it is classified —
including the ones nested inside functions and classes, which are classified exactly where they sit rather than
hoisted.
Resolution is by module path, not by bare file existence.
from a.b import c looks for a/b.py, then
a/b/__init__.py; landing on a/b/c.py instead means c is a module, not a name, and that is
LOCAL_MODULE_IMPORT_FORM rather than a resolution.
Relative imports resolve against the importing module’s package. from . import x inside that package’s own
__init__.py resolves to the sibling x.py, never to __init__.py itself, which would report a self-cycle. A
path that resolves outside base_path — including an entry_point that is not under it at all — is
BASE_PATH_ESCAPE.
Where a local module shadows a pre-bound stdlib name, local wins, because the sandbox’s own merge order does the
same thing. The LOCAL_SHADOWS_SANDBOX_MODULE advisory names the shadow.
Emission
Modules are ordered topologically: dependencies first, the entry point last. Each becomes onecontent[] part,
headed by a frozen marker naming the file it came from:
eject possible, and it is part of the contract rather
than a comment.
The module body is the source with every import statement replaced in place — by exact character span, so several
statements on one physical line are each spliced separately and the surrounding indentation and text are
untouched. Nothing else is reformatted or reordered.
The import-rewrite table
What replaces an import depends on what the runtime actually binds, which is the module’s own name, never the names inside it. The sandbox pre-bindsjson, datetime, re and the rest as module objects, and an
allowlisted third-party package is merged in under its literal allowlist name the same way. A local import is
different: once its target is inlined, the names it exports are already bound flatly under their own names.
Getting this wrong would be worse than a
NameError. Stripping from datetime import datetime to nothing leaves
the name datetime bound to the module rather than the class, so datetime.now() fails with a confusing
attribute error — or silently resolves against the wrong object. The rebinding line is what keeps the flat
namespace behaving the way the source reads. It has one sharp edge of its own, in
Limitations below: that same rebinding takes over the module’s name.
A block left empty by the rewrite gets a pass, so a function whose whole body was an import stays parseable —
def configure(): followed only by import json compiles to def configure(): followed by pass.
Hard checks
Each of these stops the command before anything is written, and each prevents a failure that would otherwise surface only at execution time, with no plausible cause.
Dotted imports of non-local modules are fine — only local ones are rejected by
LOCAL_MODULE_IMPORT_FORM.
Determinism
Compilation is a pure function of the sources, and the writer touches no key it does not own:content_hash— SHA-256 over the canonical compiledcontent[]: the JSON array serialized with sorted keys,ensure_ascii=Falseand no insignificant whitespace.attributes.id—uuid5of that hash under a fixed namespace, lower-cased. Identical content always yields the identical id.attributes.created_at— carried forward from whatever is already in the definition file, and stamped with the current time only when there is nothing to carry.attributes.syntax_mode— always written aspython.- Any other attribute key already present is preserved.
git status clean. A code port
this CLI has never built changes once, on its first build, and is stable from the second onward.
“Byte-for-byte” is anchored after newline translation. Module sources are read in universal-newlines mode, so
\r\n and \r become \n before the compiler sees them. A CRLF-authored file therefore deploys as LF and
hashes identically to its LF-authored twin — which is the point, since the same package must compile the same
way whatever checked it out — but the deployed text is not literally the file’s bytes, and an eject round trip
will not restore CRLF endings.
The remote-edit gate
content_hash is what lets the CLI refuse to overwrite code someone edited in the visual editor. Before writing
a code port that already has content:
--force skips the classification. The other way out is eject, which brings the
edited code onto disk so you can reconcile it by hand first. diff reports the
same three states without writing anything.
The scheme holds whichever way the visual editor treats an attribute it does not recognize: preserved, the hash
goes stale and mismatches; regenerated, the hash disappears. Both stop and tell you, so the CLI never depends on
the editor cooperating.
Note which document is classified. build and deploy read the code port in the local definition file,
because that is the only document they have; diff is the command that classifies the remote port.
Limitations
Stated plainly rather than omitted, because each one is a surprise the first time you meet it in deployed code. A rebinding can shadow the module it came from.from datetime import datetime, timezone emits
datetime = datetime.datetime; timezone = datetime.timezone, and the first assignment rebinds datetime to the
class before the second reads .timezone off it — an AttributeError at execution time. The same happens across
modules: one inlined module doing from datetime import datetime breaks a later module’s from datetime import timezone. It bites whenever the imported name equals its module’s name, datetime being the common case. Either
reach through the module, which needs no rebinding at all, or alias to a different name:
__init__.py. Real Python executes a/__init__.py
before a/b.py; this compiler resolves a.b straight to a/b.py and never visits a/__init__.py unless some
other statement reaches it directly. Inlining every ancestor on every submodule import would drag unrelated
package-level code into the flat namespace and manufacture spurious SYMBOL_COLLISIONs. The consequence: side
effects or definitions living only in a parent __init__.py are silently absent at runtime.
A conditional import pair reports as SYMBOL_COLLISION. try: from a import helper /
except ImportError: from b import helper classifies both branches statically — the compiler never evaluates
which would win — so both modules are inlined and their two helpers collide, and the message names two modules
without mentioning the try. Rewrite to a single branch; the sandbox has no __import__ at all, so an
ImportError fallback could never fire there anyway.
A module with a top-level from x import * opts out of IMPORT_NAME_NOT_FOUND entirely. Its exports cannot
be enumerated without evaluating __all__ or executing code, so every name requested from it is let through. The
wildcard’s own source module is still inlined, so a genuinely re-exported name really is bound; this only widens
what the compiler accepts, never invents a binding.
A dotted non-local import is classified by its root, but rebinds through an attribute. import numpy.linalg as la is accepted when numpy is allowlisted and emits la = numpy.linalg — which resolves at runtime only if
the sandbox actually imported the submodule, that is, only if numpy.linalg is itself an allowlist entry. See
Sandbox for why a dotted entry is worth an advisory of its own.
The allowlist
workflow_component_code_imports is the one compilation input that lives on the server, and reading it needs an
admin-scoped key — which the read and write keys the platform recommends for automation are not.
So the compiler classifies against a local cache at ~/.blobhub/cache/allowlist/, keyed on the manifest’s own
blob: reference. That is what keeps build networkless. With no cache entry, ALLOWLIST_UNREADABLE is raised
as an advisory and an unresolved import degrades from IMPORT_UNAVAILABLE to the advisory IMPORT_UNVERIFIED;
--strict-imports restores the hard failure, which is what a CI job should set. A cache entry older than seven
days additionally raises ALLOWLIST_STALE.
Everything that does not depend on the allowlist is enforced regardless: local resolution, cycles, collisions,
name lookups and sandbox shadowing.
blobhub blob limits covers the allowlist itself, the key that can read it, and how the
cache is written.
Errors
Every code named on this page, with its remediation, is in Error codes. The compiler’s own family isIMPORT_CYCLE, SYMBOL_COLLISION, SANDBOX_NAME_SHADOWED, LOCAL_MODULE_IMPORT_FORM,
IMPORT_NAME_NOT_FOUND, IMPORT_UNAVAILABLE, UNSUPPORTED_IMPORT_FORM, BASE_PATH_ESCAPE and
ENTRY_POINT_NOT_FOUND, each naming the module and the statement that caused it.
See also
blobhub workflow build— the command that runs this, offline.blobhub workflow eject— the closest thing to an inverse, and explicitly not one.- Manifest — where
base_pathandentry_pointare declared. - Sandbox — the names the compiler may drop, and the ones your code must not redefine.
- Component code — what the platform executes once this has run.

