Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ jobs:
- name: npm check
run: npm run check

python-check:
name: Python static checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: "true"
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Run python static checks
run: python ./build.py --check-py --no-check-prereqs

build:
name: Build and test
runs-on: ubuntu-latest
Expand Down Expand Up @@ -203,6 +216,7 @@ jobs:
format,
clippy,
web-check,
python-check,
build,
unit-tests,
format-qsc,
Expand Down
53 changes: 53 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@
help="Run the prerequisites check (default is --check-prereqs)",
)

parser.add_argument(
"--check-py",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing check flag does JavaScript/TypeScript and Rust. Is there a reason to add a distinct Python check flag than just roll it into the existing check (if building a Python package).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes,

  1. So I can run Python checks only without having to wait for Rust and TypeScript checks that might be irrelevant for me if I am only working on Python code.
  2. So we can call call build.py --check-py in CI workflow for python static checks.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to split it out, I'd lean towards splitting all of them out and then having check mean "check all".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I talked to @swernli about this and he suggested the same - having an option in build.py to run every check separately, but this can be done in later PR.

This option exists for the following scenario: let's say you did some change to Python and "static Python check" workflow failed. You want to do fixes, verify them locally, push your changes and get workflow green as soon as possible. You do your local changes, run build.py --check-py, it passes, then you push your changes and the workflow is green.

Without this option, there would be no way to run python static checks only.

action=argparse.BooleanOptionalAction,
default=False,
help="Run static checks (linting, formatting and typing) for Python and nothing else",
)

parser.add_argument(
"--integration-tests",
action=argparse.BooleanOptionalAction,
Expand Down Expand Up @@ -269,6 +276,48 @@ def use_python_env(folder):
return (python_bin, pip_env)


# Static checks for Python code in source/qdk_package.
# Currently only includes type checking with pyright.
# Runs if:
# 1. Enabled explicitly (will run these checks only): build.py --check-py.
# 2. Or if this build builds qdk package and checks are not disabled.
def run_python_checks():
python_bin, env = use_python_env(qdk_python_src)

step_start("Install requirements for Python static checks")
run(
[
python_bin,
"-m",
"pip",
"install",
"-r",
"check_requirements.txt",
"-q",
],
cwd=qdk_python_src,
env=env,
)
step_end()

step_start("Running pyright checks")
run(
[
python_bin,
"-m",
"pyright",
],
cwd=root_dir,
env=env,
)
step_end()


if args.check_py:
run_python_checks()
exit(0)


if npm_install_needed:
command = [npm_cmd, "install"]
if not args.optional_dependencies:
Expand Down Expand Up @@ -330,6 +379,10 @@ def use_python_env(folder):
)
step_end()

if build_qdk:
run_python_checks()


if build_cli:
if run_tests:
step_start("Running Rust unit tests")
Expand Down
11 changes: 10 additions & 1 deletion pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"pythonVersion": "3.10",
"extraPaths": ["source/pip", "source/widgets/src", "source/qdk_package/src"],
"include": ["source/qdk_package/qdk"],
"exclude": [
"source/qdk_package/qdk/applications",
"source/qdk_package/qdk/azure",
"source/qdk_package/qdk/cirq",
"source/qdk_package/qdk/simulation",
"source/qdk_package/qdk/qiskit",
"source/qdk_package/qdk/qre"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we plan to include this at a later point?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in follow-up PRs.

],
"reportMissingModuleSource": "none", // Allow .pyi without .py
"typeCheckingMode": "basic"
}
6 changes: 6 additions & 0 deletions source/qdk_package/check_requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Libraries that need to be installed to run Python static checks.
matplotlib
pandas>=2.1
pandas-stubs
pyqir>=0.12.5,<0.13

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do these need to be installed for the checks? Does it need to verify the types from any package your our code may use? (In which case, would it need to ultimately extend to qiskit, cirq, etc.)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When typechecker sees import statement and library is not installed, it raises an error (both mypy and pyright).
There are 2 ways to deal with it:

  1. Have the library installed (or "type stubs" which provides type signatures for public API) - then you get all calls to the library type-checked.
  2. Ignore this import - then all calls to the library are not type-checked, reducing usefulness of type checking.

We can make this choice for every library we import, this is why I created this separate requirements file. I chose to install matplotlib, pandas and pyqir rather than ignoring them.

Yes, I intend to ultimately extend it here to use other libraries, like cirq and qiskit, but I want to be able to make this choice for each library. If I see that some library would take a lot of time to install (e.g. cirq that has a lot of dependencies) and is used only in few limited places, I can ignore it or maybe only install type stubs.

pyright==1.1.410
21 changes: 11 additions & 10 deletions source/qdk_package/qdk/_adaptive_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,12 +478,12 @@ def _resolve_operand(self, value: pyqir.Value) -> IntOperand | FloatOperand | Re
return self._value_to_reg[value]

if isinstance(value, pyqir.IntConstant):
val = value.value
return IntOperand(val, self._int_bits)
int_val = value.value
return IntOperand(int_val, self._int_bits)

if isinstance(value, pyqir.FloatConstant):
val = value.value
return FloatOperand(val, self._bytecode_kind)
float_val = value.value
return FloatOperand(float_val, self._bytecode_kind)

# Global variable reference (e.g., @array0)
if hasattr(value, "name") and value.name in self._global_to_address:
Expand Down Expand Up @@ -895,7 +895,7 @@ def _emit_call(self, call: pyqir.Call) -> None:
self._emit_quantum_call(call)
case _ if callee in self._func_to_id:
self._emit_ir_function_call(call)
case _ if "qdk_noise" in call.callee.attributes.func:
case _ if "qdk_noise" in cast(pyqir.Function, call.callee).attributes.func:
# Check if this is a noise intrinsic (custom gate with qdk_noise attribute)
self._emit_noise_intrinsic_call(call)
case _:
Expand Down Expand Up @@ -964,11 +964,13 @@ def _emit_quantum_call(self, call: pyqir.Call) -> None:
# the runtime invokes ``Simulator::mov`` (which applies the
# configured ``noise.mov`` faults to that qubit).
q1, q2, q3 = self._resolve_qubit_operands([call.args[0]])
angle = FloatOperand(0.0, self._bytecode_kind)
qop_idx = self._emit_quantum_op(op_id, q1.val, q2.val, q3.val, angle.val)
move_angle = FloatOperand(0.0, self._bytecode_kind)
qop_idx = self._emit_quantum_op(
op_id, q1.val, q2.val, q3.val, move_angle.val
)
self._emit(
OP_QUANTUM_GATE,
src0=angle,
src0=move_angle,
aux0=qop_idx,
aux1=q1,
aux2=q2,
Expand All @@ -977,11 +979,10 @@ def _emit_quantum_call(self, call: pyqir.Call) -> None:
return
if gate_name in ROTATION_GATES:
qubit_arg_offset = 1
angle = self._resolve_angle_operand(call.args[0])
angle: FloatOperand | Reg = self._resolve_angle_operand(call.args[0])
else:
qubit_arg_offset = 0
angle = FloatOperand(0.0, self._bytecode_kind)
qubit_arg_offset = 1 if gate_name in ROTATION_GATES else 0
q1, q2, q3 = self._resolve_qubit_operands(call.args[qubit_arg_offset:])
qop_idx = self._emit_quantum_op(op_id, q1.val, q2.val, q3.val, angle.val)
self._emit(
Expand Down
47 changes: 22 additions & 25 deletions source/qdk_package/qdk/_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""

import json
import builtins
import sys
import types
import weakref
Expand Down Expand Up @@ -66,7 +67,7 @@
# Check if we are running in a Jupyter notebook to use the IPython display function
_in_jupyter = False
try:
from IPython.display import display
from IPython.display import display # type: ignore[import-not-found]

if get_ipython().__class__.__name__ == "ZMQInteractiveShell": # type: ignore
_in_jupyter = True # Jupyter notebook or qtconsole
Expand All @@ -81,7 +82,7 @@
def ipython_helper():
try:
if __IPYTHON__: # type: ignore
from IPython.display import display
from IPython.display import display # type: ignore[import-not-found]
except NameError:
pass

Expand All @@ -106,9 +107,9 @@ def _visual_circuit_count(circuit_json: str) -> int:

def make_class_rec(qsharp_type: TypeIR) -> type:
class_name = qsharp_type.unwrap_udt().name
fields = {}
fields: dict[str, Any] = {}
for field in qsharp_type.unwrap_udt().fields:
ty = None
ty: type[Any] | None = None
kind = field[1].kind()

if kind == TypeKind.Primitive:
Expand Down Expand Up @@ -141,6 +142,7 @@ def make_class_rec(qsharp_type: TypeIR) -> type:
ty = make_class_rec(field[1])
else:
raise QSharpError(f"unknown type {kind}")
assert ty is not None
fields[field[0]] = ty

return make_dataclass(
Expand Down Expand Up @@ -384,7 +386,7 @@ def _lower_python_obj(
finally:
visited.remove(obj_id)

def _python_args_to_interpreter_args(self, args: tuple[Any, ...]):
def _python_args_to_interpreter_args(self, args: tuple[Any, ...]) -> Any:
"""Turns `args` to the format expected by the Q# interpreter."""
if len(args) == 0:
return None
Expand All @@ -408,7 +410,7 @@ def _get_code_module(
self, namespace: List[str]
) -> types.ModuleType | types.SimpleNamespace:
"""Returns module for given path, creating it if it doesn't exist."""
module = self.code
module: Any = self.code
if self._is_global_context:
accumulated_namespace = code.__name__
for name in namespace:
Expand All @@ -427,9 +429,9 @@ def _get_code_module(
if hasattr(module, name):
module = module.__getattribute__(name)
else:
new_module = types.SimpleNamespace()
module.__setattr__(name, new_module)
module = new_module
new_namespace_module = types.SimpleNamespace()
module.__setattr__(name, new_namespace_module)
module = new_namespace_module
return module

def _make_callable(
Expand Down Expand Up @@ -465,7 +467,7 @@ def _make_class(self, qsharp_type: TypeIR, namespace: List[str], class_name: str
"""Registers a Q# type as a Python dataclass in this context's code module."""
module = self._get_code_module(namespace)
QSharpClass = make_class_rec(qsharp_type)
QSharpClass.__qsharp_class = True
setattr(QSharpClass, "__qsharp_class", True)
setattr(QSharpClass, "_qdk_context", self)
module.__setattr__(class_name, QSharpClass)

Expand Down Expand Up @@ -527,7 +529,9 @@ def import_circuit(
try:
_, circuit_json = read_file(resolved_path)
except Exception as err:
raise QSharpError(f"Error reading visual circuit file {resolved_path}.") from err
raise QSharpError(
f"Error reading visual circuit file {resolved_path}."
) from err

circuit_count = _visual_circuit_count(circuit_json)
operation_base_name = name if name is not None else _path_stem(resolved_path)
Expand Down Expand Up @@ -684,9 +688,7 @@ def on_save_events(output: Output) -> None:

callable = None
run_entry_expr = None
if isinstance(entry_expr, Callable) and hasattr(
entry_expr, "__global_callable"
):
if builtins.callable(entry_expr) and hasattr(entry_expr, "__global_callable"):
self._check_same_context_callable(entry_expr)
args = self._python_args_to_interpreter_args(args)
callable = getattr(entry_expr, "__global_callable")
Expand Down Expand Up @@ -775,9 +777,7 @@ def compile(
start = monotonic()
target_profile = self._config.get_target_profile()
telemetry_events.on_compile(target_profile)
if isinstance(entry_expr, Callable) and hasattr(
entry_expr, "__global_callable"
):
if builtins.callable(entry_expr) and hasattr(entry_expr, "__global_callable"):
self._check_same_context_callable(entry_expr)
args = self._python_args_to_interpreter_args(args)
ll_str = self._interpreter.qir(
Expand Down Expand Up @@ -861,9 +861,7 @@ def circuit(
prune_classical_qubits=prune_classical_qubits,
)

if isinstance(entry_expr, Callable) and hasattr(
entry_expr, "__global_callable"
):
if builtins.callable(entry_expr) and hasattr(entry_expr, "__global_callable"):
self._check_same_context_callable(entry_expr)
args = self._python_args_to_interpreter_args(args)
res = self._interpreter.circuit(
Expand All @@ -872,9 +870,10 @@ def circuit(
args=args,
)
elif isinstance(entry_expr, (GlobalCallable, Closure)):
args = self._python_args_to_interpreter_args(args)
res = self._interpreter.circuit(
config=config, callable=entry_expr, args=args
config=config,
callable=entry_expr,
args=self._python_args_to_interpreter_args(args),
)
else:
assert entry_expr is None or isinstance(entry_expr, str)
Expand Down Expand Up @@ -902,9 +901,7 @@ def logical_counts(
"""
ipython_helper()

if isinstance(entry_expr, Callable) and hasattr(
entry_expr, "__global_callable"
):
if builtins.callable(entry_expr) and hasattr(entry_expr, "__global_callable"):
self._check_same_context_callable(entry_expr)
args = self._python_args_to_interpreter_args(args)
res_dict = self._interpreter.logical_counts(
Expand Down
4 changes: 2 additions & 2 deletions source/qdk_package/qdk/_device/_atom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def show_trace(self, qir: str | QirInputData):
"""

try:
from qsharp_widgets import Atoms
from qsharp_widgets import Atoms # type: ignore[import-not-found]
except ImportError:
raise ImportError(
"The qsharp-widgets package is required for showing atom trace visualization. "
Expand All @@ -208,7 +208,7 @@ def show_trace(self, qir: str | QirInputData):
from ._validate import ValidateNoConditionalBranches
from ._scheduler import Schedule
from pyqir import Module, Context
from IPython.display import display
from IPython.display import display # type: ignore[import-not-found]

start_time = time.monotonic()
telemetry_events.on_neutral_atom_trace()
Expand Down
Loading
Loading