diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi index 5e9cc4ed0f..37a50b7f9b 100644 --- a/pip/qsharp/_native.pyi +++ b/pip/qsharp/_native.pyi @@ -121,8 +121,7 @@ class Interpreter: read_file: Callable[[str], Tuple[str, str]], list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], - env: Optional[Any], - make_callable: Optional[Callable[[GlobalCallable], Callable]], + make_callable: Optional[Callable[[GlobalCallable], None]], ) -> None: """ Initializes the Q# interpreter. @@ -132,8 +131,7 @@ class Interpreter: :param read_file: A function that reads a file from the file system. :param list_directory: A function that lists the contents of a directory. :param resolve_path: A function that joins path segments and normalizes the resulting path. - :param env: An object on which to add any globally accesible callables from the compiled source. - :param make_callable: A function that converts a GlobalCallable into a callable object. + :param make_callable: A function that registers a Q# callable in the in the environment module. """ ... diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py index 80b77227fe..84c457a07e 100644 --- a/pip/qsharp/_qsharp.py +++ b/pip/qsharp/_qsharp.py @@ -219,7 +219,6 @@ def init( list_directory, resolve, fetch_github, - env, _make_callable, ) @@ -278,7 +277,26 @@ def callback(output: Output) -> None: # Helper function that knows how to create a function that invokes a callable. This will be # used by the underlying native code to create functions for callables on the fly that know # how to get the currently intitialized global interpreter instance. -def _make_callable(callable): +def _make_callable(callable, namespace, callable_name): + module = env + # Create a name that will be used to collect the hierachy of namespace identifiers if they exist and use that + # to register created modules with the system. + accumulated_namespace = "qsharp.env" + accumulated_namespace += "." + for name in namespace: + accumulated_namespace += name + # Use the existing entry, which should already be a module. + if hasattr(module, name): + module = module.__getattribute__(name) + else: + # This namespace entry doesn't exist as a module yet, so create it, add it to the environment, and + # add it to sys.modules so it supports import properly. + new_module = types.ModuleType(accumulated_namespace) + module.__setattr__(name, new_module) + sys.modules[accumulated_namespace] = new_module + module = new_module + accumulated_namespace += "." + def _callable(*args): ipython_helper() @@ -299,7 +317,11 @@ def callback(output: Output) -> None: return get_interpreter().invoke(callable, args, callback) - return _callable + # Each callable is annotated so that we know it is auto-generated and can be removed on a re-init of the interpreter. + _callable.__qs_gen = True + + # Add the callable to the module. + module.__setattr__(callable_name, _callable) class ShotResult(TypedDict): diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index aadb611ac4..8cb886015e 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -219,8 +219,6 @@ impl From for qsc_qasm3::ProgramType { #[pyclass(unsendable)] pub(crate) struct Interpreter { pub(crate) interpreter: interpret::Interpreter, - /// The Python environment to which new callables will be added. - pub(crate) env: Option, /// The Python function to call to create a new function wrapping a callable invocation. pub(crate) make_callable: Option, } @@ -232,7 +230,7 @@ thread_local! { static PACKAGE_CACHE: Rc> = Rc::default(); impl Interpreter { #[allow(clippy::too_many_arguments)] #[allow(clippy::needless_pass_by_value)] - #[pyo3(signature = (target_profile, language_features=None, project_root=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None, env=None, make_callable=None))] + #[pyo3(signature = (target_profile, language_features=None, project_root=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None, make_callable=None))] #[new] /// Initializes a new Q# interpreter. pub(crate) fn new( @@ -244,7 +242,6 @@ impl Interpreter { list_directory: Option, resolve_path: Option, fetch_github: Option, - env: Option, make_callable: Option, ) -> PyResult { let target = Into::::into(target_profile).into(); @@ -288,16 +285,15 @@ impl Interpreter { &buildable_program.user_code_dependencies, ) { Ok(interpreter) => { - if let (Some(env), Some(make_callable)) = (&env, &make_callable) { + if let Some(make_callable) = &make_callable { // Add any global callables from the user source as Python functions to the environment. let exported_items = interpreter.get_source_package_global_items(); for (namespace, name, item_id) in exported_items { - create_py_callable(py, env, make_callable, &namespace, &name, item_id)?; + create_py_callable(py, make_callable, &namespace, &name, item_id)?; } } Ok(Self { interpreter, - env, make_callable, }) } @@ -323,14 +319,14 @@ impl Interpreter { let mut receiver = OptionalCallbackReceiver { callback, py }; match self.interpreter.eval_fragments(&mut receiver, input) { Ok(value) => { - if let (Some(env), Some(make_callable)) = (&self.env, &self.make_callable) { + if let Some(make_callable) = &self.make_callable { // Get any global callables from the evaluated input and add them to the environment. This will grab // every callable that was defined in the input and by previous calls that added to the open package. // This is safe because either the callable will be replaced with itself or a new callable with the // same name will shadow the previous one, which is the expected behavior. let new_items = self.interpreter.get_open_package_global_items(); for (namespace, name, item_id) in new_items { - create_py_callable(py, env, make_callable, &namespace, &name, item_id)?; + create_py_callable(py, make_callable, &namespace, &name, item_id)?; } } Ok(ValueWrapper(value).into_py(py)) @@ -1001,7 +997,6 @@ struct GlobalCallable { /// Create a Python callable from a Q# callable and adds it to the given environment. fn create_py_callable( py: Python, - env: &PyObject, make_callable: &PyObject, namespace: &[Rc], name: &str, @@ -1012,46 +1007,14 @@ fn create_py_callable( return Ok(()); } - // env is expected to be a module, any other type will raise an error. - let mut module: Bound = env.extract(py)?; - - // Create a name that will be used to collect the hieirchy of namespace identifiers if they exist and use that - // to register created modules with the system. - let mut accumulated_namespace: String = module.name()?.extract()?; - accumulated_namespace.push('.'); - for name in namespace { - accumulated_namespace.push_str(name); - let py_name = PyString::new_bound(py, name); - module = if let Ok(module) = module.as_any().getattr(py_name.clone()) { - // Use the existing entry, which should already be a module. - module.extract()? - } else { - // This namespace entry doesn't exist as a module yet, so create it, add it to the environment, and - // add it to sys.modules so it support import properly. - let new_module = PyModule::new_bound(py, &accumulated_namespace)?; - module.add(py_name, &new_module)?; - py.import_bound("sys")? - .getattr("modules")? - .set_item(accumulated_namespace.clone(), new_module.clone())?; - new_module - }; - accumulated_namespace.push('.'); - } + let args = ( + Py::new(py, GlobalCallable { id: item_id }).expect("should be able to create callable"), // callable id + PyList::new_bound(py, namespace.iter().map(ToString::to_string)), // namespace as string array + PyString::new_bound(py, name), // name of callable + ); // Call into the Python layer to create the function wrapping the callable invocation. - let callable = make_callable.call1( - py, - PyTuple::new_bound( - py, - &[Py::new(py, GlobalCallable { id: item_id }) - .expect("should be able to create callable")], - ), - )?; - - // Each callable is annotated so that we know it is auto-generated and can be removed on a re-init of the interpreter. - callable.setattr(py, "__qs_gen", true.into_py(py))?; + make_callable.call1(py, args)?; - // Add the callable to the module. - module.add(name, callable)?; Ok(()) } diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index 86caac8ceb..84d4434f0b 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -521,6 +521,8 @@ def test_callables_in_namespaces_exposed_into_env_submodules_and_removed_on_rein # namespaces should be removed with pytest.raises(AttributeError): qsharp.env.Test + with pytest.raises(AttributeError): + qsharp.env.Identity() # imported callables should fail gracefully with pytest.raises(qsharp.QSharpError): Four()