Skip to content

Commit

Permalink
refactor to get rid of env argument to constructor
Browse files Browse the repository at this point in the history
  • Loading branch information
swernli committed Dec 11, 2024
1 parent 91b6c48 commit 301a389
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 55 deletions.
6 changes: 2 additions & 4 deletions pip/qsharp/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
"""
...

Expand Down
28 changes: 25 additions & 3 deletions pip/qsharp/_qsharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ def init(
list_directory,
resolve,
fetch_github,
env,
_make_callable,
)

Expand Down Expand Up @@ -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()

Expand All @@ -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):
Expand Down
59 changes: 11 additions & 48 deletions pip/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,6 @@ impl From<ProgramType> 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<PyObject>,
/// The Python function to call to create a new function wrapping a callable invocation.
pub(crate) make_callable: Option<PyObject>,
}
Expand All @@ -232,7 +230,7 @@ thread_local! { static PACKAGE_CACHE: Rc<RefCell<PackageCache>> = 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(
Expand All @@ -244,7 +242,6 @@ impl Interpreter {
list_directory: Option<PyObject>,
resolve_path: Option<PyObject>,
fetch_github: Option<PyObject>,
env: Option<PyObject>,
make_callable: Option<PyObject>,
) -> PyResult<Self> {
let target = Into::<Profile>::into(target_profile).into();
Expand Down Expand Up @@ -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,
})
}
Expand All @@ -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))
Expand Down Expand Up @@ -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<str>],
name: &str,
Expand All @@ -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<PyModule> = 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(())
}
2 changes: 2 additions & 0 deletions pip/tests/test_qsharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 301a389

Please sign in to comment.