Introduce QdkContext API for isolated interpreter sessions#3029
Introduce QdkContext API for isolated interpreter sessions#3029minestarks wants to merge 1 commit intomainfrom
QdkContext API for isolated interpreter sessions#3029Conversation
Add QdkContext class with instance methods (.eval(), .run(), .compile(), .circuit(), .estimate(), .logical_counts(), etc.) that mirror module-level functions. Module-level functions delegate to a global default context. New public API: - qsharp.new_context(...) creates an isolated context - qsharp.get_context() returns the global context (lazy init) - qsharp.context_of(callable) returns the context that compiled it - init() now returns QdkContext (backward-compatible) Cross-context safety: passing a callable from one context to another's method raises QSharpError. Stale callables (from a prior init) raise QSharpError when invoked. Includes 20 test cases covering isolation, cross-context validation, stale callable detection, backward compatibility, and config access.
| result = ctx.eval("1 + 2") | ||
| assert result == 3 | ||
|
|
||
|
|
||
| def test_context_isolation() -> None: | ||
| ctx1 = qsharp.new_context() | ||
| ctx2 = qsharp.new_context() | ||
| ctx1.eval("function Foo() : Int { 42 }") | ||
| result1 = ctx1.eval("Foo()") | ||
| assert result1 == 42 | ||
| # ctx2 should not have Foo defined | ||
| with pytest.raises(Exception): | ||
| ctx2.eval("Foo()") | ||
|
|
||
|
|
||
| def test_context_run() -> None: | ||
| ctx = qsharp.new_context() | ||
| ctx.eval('operation Foo() : Result { Message("hi"); Zero }') | ||
| results = ctx.run("Foo()", 3) |
Check notice
Code scanning / devskim
If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test
| result = qsharp.eval("1 + 1") | ||
| assert result == 2 | ||
|
|
||
|
|
||
| def test_init_returns_context() -> None: | ||
| ctx = qsharp.init() |
Check notice
Code scanning / devskim
If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test
| result = ctx.eval("3 + 4") | ||
| assert result == 7 | ||
| # Module-level eval should use the same context | ||
| result2 = qsharp.eval("3 + 4") | ||
| assert result2 == 7 | ||
|
|
||
|
|
||
| def test_context_callable_has_interpreter_ref() -> None: | ||
| """Callables created via eval carry a _qdk_get_interpreter attribute.""" | ||
| ctx = qsharp.new_context() |
Check notice
Code scanning / devskim
If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test
| ctx.eval("function Hello() : Int { 1 }") | ||
| fn = ctx.code.Hello | ||
| assert qsharp.context_of(fn) is ctx | ||
|
|
||
|
|
||
| def test_context_of_global_callable() -> None: | ||
| """context_of() works for callables in the global context.""" | ||
| ctx = qsharp.init() | ||
| qsharp.eval("function Hi() : Int { 2 }") | ||
| fn = qsharp.code.Hi | ||
| assert qsharp.context_of(fn) is ctx | ||
|
|
||
|
|
||
| def test_context_of_rejects_non_callable() -> None: | ||
| """context_of() raises TypeError for non-QDK objects.""" |
Check notice
Code scanning / devskim
If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test
| """Passing a callable from one context to another's run() raises.""" | ||
| ctx_a = qsharp.new_context() | ||
| ctx_b = qsharp.new_context() | ||
| ctx_a.eval("operation Foo() : Result { use q = Qubit(); M(q) }") |
Check notice
Code scanning / devskim
If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test
| """Passing a callable from one context to another's compile() raises.""" | ||
| ctx_a = qsharp.new_context(target_profile=qsharp.TargetProfile.Base) | ||
| ctx_b = qsharp.new_context(target_profile=qsharp.TargetProfile.Base) | ||
| ctx_a.eval("operation Bar() : Result { use q = Qubit(); M(q) }") |
Check notice
Code scanning / devskim
If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test
| """Passing a callable from one context to another's circuit() raises.""" | ||
| ctx_a = qsharp.new_context() | ||
| ctx_b = qsharp.new_context() | ||
| ctx_a.eval("operation Baz() : Unit { use q = Qubit(); H(q); }") |
Check notice
Code scanning / devskim
If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test
| """Passing a callable from one context to another's estimate() raises.""" | ||
| ctx_a = qsharp.new_context() | ||
| ctx_b = qsharp.new_context() | ||
| ctx_a.eval("operation Qux() : Unit { use q = Qubit(); H(q); }") |
Check notice
Code scanning / devskim
If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test
| """Passing a callable from one context to another's logical_counts() raises.""" | ||
| ctx_a = qsharp.new_context() | ||
| ctx_b = qsharp.new_context() | ||
| ctx_a.eval("operation Corge() : Unit { use q = Qubit(); H(q); }") |
Check notice
Code scanning / devskim
If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test
| qsharp.eval("function Stale() : Int { 99 }") | ||
| old_fn = qsharp.code.Stale | ||
| # Reinitialize — old callable should now be stale | ||
| qsharp.init() |
Check notice
Code scanning / devskim
If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test
QdkContext API for isolated interpreter sessions
Closes #2998
Summary
This PR introduces the
QdkContextclass, which provides isolated Q# interpreter contexts with their own configuration, compiled code, and state. This directly addresses the architectural problem described in #2998: theqsharppackage previously stored a single global interpreter, making it impossible for two libraries (or a library and end-user code) to coexist without silently clobbering each other's state.See #2998 (comment) for full context.
What changed
New public API
QdkContext.eval(),.run(),.compile(),.circuit(),.estimate(),.logical_counts(),.set_quantum_seed(),.set_classical_seed(),.dump_machine(),.dump_circuit(),.import_openqasm(), and a.configproperty.qsharp.new_context(...)QdkContext.qsharp.get_context()qsharp.context_of(callable)QdkContextthat compiled a given callable.Behavioral changes
init()now returnsQdkContextinstead ofConfig. The context proxies__repr__and_repr_mimebundle_from its config, so Jupyter notebook display is unchanged._qdk_get_contextattribute. Passing a callable to a different context's method (e.g.,ctx_b.run(ctx_a.code.Foo)) raisesQSharpErrorwith a clear message.init()is called, callables from the prior context raiseQSharpError("disposed") when invoked.qsharp.eval(),qsharp.run(), etc. delegate to the global default context exactly as before.