From 1e980065482be2161f26efd93135f6ee1a6db23d Mon Sep 17 00:00:00 2001 From: mstechly Date: Fri, 14 Jun 2024 14:00:29 -0400 Subject: [PATCH 1/4] refactor: changes visilibity of classes and methods in schemav1 --- docs/library/reference/qref.md | 5 +-- docs/library/reference/qref.schema_v1.md | 6 +++ mkdocs.yml | 1 + src/qref/__init__.py | 5 ++- src/qref/{_schema_v1.py => schema_v1.py} | 56 ++++++++++++++---------- src/qref/verification.py | 4 +- 6 files changed, 47 insertions(+), 30 deletions(-) create mode 100644 docs/library/reference/qref.schema_v1.md rename src/qref/{_schema_v1.py => schema_v1.py} (74%) diff --git a/docs/library/reference/qref.md b/docs/library/reference/qref.md index 2eda782..50671bd 100644 --- a/docs/library/reference/qref.md +++ b/docs/library/reference/qref.md @@ -1,6 +1,5 @@ ::: qref handler: python options: - members: - - generate_program_schema - - SchemaV1 + filters: + - "!__all__" \ No newline at end of file diff --git a/docs/library/reference/qref.schema_v1.md b/docs/library/reference/qref.schema_v1.md new file mode 100644 index 0000000..63ae471 --- /dev/null +++ b/docs/library/reference/qref.schema_v1.md @@ -0,0 +1,6 @@ +::: qref.schema_v1 + handler: python + options: + filters: + - "!^_[^_]" + - "!SchemaV1" diff --git a/mkdocs.yml b/mkdocs.yml index c75a8e2..5f5bcbf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,6 +11,7 @@ nav: - library/userguide.md - API Reference: - qref: library/reference/qref.md + - qref.schema_v1: library/reference/qref.schema_v1.md - qref.experimental.rendering: library/reference/qref.experimental.rendering.md - development.md theme: diff --git a/src/qref/__init__.py b/src/qref/__init__.py index 58509a2..3a7a3a3 100644 --- a/src/qref/__init__.py +++ b/src/qref/__init__.py @@ -16,7 +16,8 @@ from typing import Any -from ._schema_v1 import SchemaV1, generate_schema_v1 +from .schema_v1 import SchemaV1, generate_schema_v1 +from .verification import verify_topology SCHEMA_GENERATORS = {"v1": generate_schema_v1} MODELS = {"v1": SchemaV1} @@ -41,4 +42,4 @@ def generate_program_schema(version: str = LATEST_SCHEMA_VERSION) -> dict[str, A raise ValueError(f"Unknown schema version {version}") -__all__ = ["generate_program_schema", "SchemaV1"] +__all__ = ["generate_program_schema", "SchemaV1", "verify_topology"] diff --git a/src/qref/_schema_v1.py b/src/qref/schema_v1.py similarity index 74% rename from src/qref/_schema_v1.py rename to src/qref/schema_v1.py index 64866c7..0607561 100644 --- a/src/qref/_schema_v1.py +++ b/src/qref/schema_v1.py @@ -31,50 +31,58 @@ NAME_PATTERN = "[A-Za-z_][A-Za-z0-9_]*" NAMESPACED_NAME_PATTERN = rf"{NAME_PATTERN}\.{NAME_PATTERN}" -Name = Annotated[str, StringConstraints(pattern=rf"^{NAME_PATTERN}$")] -NamespacedName = Annotated[str, StringConstraints(pattern=rf"^{NAMESPACED_NAME_PATTERN}")] -OptionallyNamespacedName = Annotated[ +_Name = Annotated[str, StringConstraints(pattern=rf"^{NAME_PATTERN}$")] +_NamespacedName = Annotated[str, StringConstraints(pattern=rf"^{NAMESPACED_NAME_PATTERN}")] +_OptionallyNamespacedName = Annotated[ str, StringConstraints(pattern=rf"^(({NAME_PATTERN})|({NAMESPACED_NAME_PATTERN}))$") ] _Value = Union[int, float, str] -def sorter(key): +def _sorter(key): def _inner(v): return sorted(v, key=key) return _inner -name_sorter = AfterValidator(sorter(lambda p: p.name)) -source_sorter = AfterValidator(sorter(lambda c: c.source)) +_name_sorter = AfterValidator(_sorter(lambda p: p.name)) +_source_sorter = AfterValidator(_sorter(lambda c: c.source)) -class _PortV1(BaseModel): - name: Name +class PortV1(BaseModel): + """Description of Port in V1 schema""" + + name: _Name direction: Literal["input", "output", "through"] size: Optional[_Value] model_config = ConfigDict(title="Port") -class _ConnectionV1(BaseModel): - source: OptionallyNamespacedName - target: OptionallyNamespacedName +class ConnectionV1(BaseModel): + """Description of Connection in V1 schema""" + + source: _OptionallyNamespacedName + target: _OptionallyNamespacedName model_config = ConfigDict(title="Connection", use_enum_values=True) -class _ResourceV1(BaseModel): - name: Name +class ResourceV1(BaseModel): + """Description of Resource in V1 schema""" + + name: _Name type: Literal["additive", "multiplicative", "qubits", "other"] value: Union[int, float, str, None] model_config = ConfigDict(title="Resource") -class _ParamLinkV1(BaseModel): - source: Name - targets: list[NamespacedName] +class ParamLinkV1(BaseModel): + """Description of Parameter link in V1 schema""" + + source: _Name + targets: list[_NamespacedName] model_config = ConfigDict(title="ParamLink") @@ -87,15 +95,15 @@ class RoutineV1(BaseModel): SchemaV1. """ - name: Name - children: Annotated[list[RoutineV1], name_sorter] = Field(default_factory=list) + name: _Name + children: Annotated[list[RoutineV1], _name_sorter] = Field(default_factory=list) type: Optional[str] = None - ports: Annotated[list[_PortV1], name_sorter] = Field(default_factory=list) - resources: Annotated[list[_ResourceV1], name_sorter] = Field(default_factory=list) - connections: Annotated[list[_ConnectionV1], source_sorter] = Field(default_factory=list) - input_params: list[Name] = Field(default_factory=list) + ports: Annotated[list[PortV1], _name_sorter] = Field(default_factory=list) + resources: Annotated[list[ResourceV1], _name_sorter] = Field(default_factory=list) + connections: Annotated[list[ConnectionV1], _source_sorter] = Field(default_factory=list) + input_params: list[_Name] = Field(default_factory=list) local_variables: list[str] = Field(default_factory=list) - linked_params: Annotated[list[_ParamLinkV1], source_sorter] = Field(default_factory=list) + linked_params: Annotated[list[ParamLinkV1], _source_sorter] = Field(default_factory=list) meta: dict[str, Any] = Field(default_factory=dict) model_config = ConfigDict(title="Routine") @@ -105,7 +113,7 @@ def __init__(self, **data: Any): @field_validator("connections", mode="after") @classmethod - def _validate_connections(cls, v, values) -> list[_ConnectionV1]: + def _validate_connections(cls, v, values) -> list[ConnectionV1]: children_port_names = [ f"{child.name}.{port.name}" for child in values.data.get("children") diff --git a/src/qref/verification.py b/src/qref/verification.py index 1d899a1..b9b897b 100644 --- a/src/qref/verification.py +++ b/src/qref/verification.py @@ -16,7 +16,7 @@ from dataclasses import dataclass from typing import Optional, Union -from ._schema_v1 import RoutineV1, SchemaV1 +from .schema_v1 import RoutineV1, SchemaV1 @dataclass @@ -36,6 +36,8 @@ def __bool__(self) -> bool: def verify_topology(routine: Union[SchemaV1, RoutineV1]) -> TopologyVerificationOutput: """Checks whether program has correct topology. + Correct topology cannot include cycles or disconnected ports. + Args: routine: Routine or program to be verified. """ From 86320355ad9e348f250e16dc0ac6f41e3e3eecb4 Mon Sep 17 00:00:00 2001 From: mstechly Date: Tue, 25 Jun 2024 11:11:54 -0400 Subject: [PATCH 2/4] feat: update rules for object names --- pyproject.toml | 2 +- src/qref/schema_v1.py | 18 ++++++++++++------ tests/qref/data/invalid_yaml_programs.yaml | 22 +++------------------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8789283..c00d85b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ build-backend = "poetry_dynamic_versioning.backend" [tool.black] -line-length = 100 +line-length = 120 target-version = ['py39'] diff --git a/src/qref/schema_v1.py b/src/qref/schema_v1.py index 0607561..87b9d6d 100644 --- a/src/qref/schema_v1.py +++ b/src/qref/schema_v1.py @@ -29,13 +29,19 @@ from pydantic.json_schema import GenerateJsonSchema NAME_PATTERN = "[A-Za-z_][A-Za-z0-9_]*" -NAMESPACED_NAME_PATTERN = rf"{NAME_PATTERN}\.{NAME_PATTERN}" +OPTIONALLY_NAMESPACED_NAME_PATTERN = rf"^({NAME_PATTERN}\.)?{NAME_PATTERN}$" +MULTINAMESPACED_NAME_PATTERN = rf"^({NAME_PATTERN}\.)+{NAME_PATTERN}$" +OPTIONALLY_MULTINAMESPACED_NAME_PATTERN = rf"^({NAME_PATTERN}\.)*{NAME_PATTERN}$" _Name = Annotated[str, StringConstraints(pattern=rf"^{NAME_PATTERN}$")] -_NamespacedName = Annotated[str, StringConstraints(pattern=rf"^{NAMESPACED_NAME_PATTERN}")] _OptionallyNamespacedName = Annotated[ - str, StringConstraints(pattern=rf"^(({NAME_PATTERN})|({NAMESPACED_NAME_PATTERN}))$") + str, StringConstraints(pattern=rf"{OPTIONALLY_NAMESPACED_NAME_PATTERN}") ] +_MultiNamespacedName = Annotated[str, StringConstraints(pattern=rf"{MULTINAMESPACED_NAME_PATTERN}")] +_OptionallyMultiNamespacedName = Annotated[ + str, StringConstraints(pattern=rf"{OPTIONALLY_MULTINAMESPACED_NAME_PATTERN}") +] + _Value = Union[int, float, str] @@ -81,8 +87,8 @@ class ResourceV1(BaseModel): class ParamLinkV1(BaseModel): """Description of Parameter link in V1 schema""" - source: _Name - targets: list[_NamespacedName] + source: _OptionallyNamespacedName + targets: list[_MultiNamespacedName] model_config = ConfigDict(title="ParamLink") @@ -101,7 +107,7 @@ class RoutineV1(BaseModel): ports: Annotated[list[PortV1], _name_sorter] = Field(default_factory=list) resources: Annotated[list[ResourceV1], _name_sorter] = Field(default_factory=list) connections: Annotated[list[ConnectionV1], _source_sorter] = Field(default_factory=list) - input_params: list[_Name] = Field(default_factory=list) + input_params: list[_OptionallyMultiNamespacedName] = Field(default_factory=list) local_variables: list[str] = Field(default_factory=list) linked_params: Annotated[list[ParamLinkV1], _source_sorter] = Field(default_factory=list) meta: dict[str, Any] = Field(default_factory=dict) diff --git a/tests/qref/data/invalid_yaml_programs.yaml b/tests/qref/data/invalid_yaml_programs.yaml index a700d2c..0062dc5 100644 --- a/tests/qref/data/invalid_yaml_programs.yaml +++ b/tests/qref/data/invalid_yaml_programs.yaml @@ -194,7 +194,7 @@ target: bar.in_0 description: "Connections have more than one namespace" error_path: "$.program.connections[0].source" - error_message: "'foo.foo.out_0' does not match '^(([A-Za-z_][A-Za-z0-9_]*)|([A-Za-z_][A-Za-z0-9_]*\\\\.[A-Za-z_][A-Za-z0-9_]*))$'" + error_message: "'foo.foo.out_0' does not match '^([A-Za-z_][A-Za-z0-9_]*\\\\.)?[A-Za-z_][A-Za-z0-9_]*$'" - input: version: v1 program: @@ -204,7 +204,7 @@ - "my-input-param" description: "Input param has invalid name" error_path: "$.program.input_params[1]" - error_message: "'my-input-param' does not match '^[A-Za-z_][A-Za-z0-9_]*$'" + error_message: "'my-input-param' does not match '^([A-Za-z_][A-Za-z0-9_]*\\\\.)*[A-Za-z_][A-Za-z0-9_]*$'" - input: version: v1 program: @@ -212,22 +212,6 @@ description: "Program has an empty name" error_path: "$.program.name" error_message: "'' does not match '^[A-Za-z_][A-Za-z0-9_]*$'" -- input: - version: v1 - program: - name: "root" - input_params: - - N - linked_params: - - source: foo.N - targets: [foo.N] - children: - - name: foo - input_params: - - N - description: Source of a paramater link is namespaced - error_path: "$.program.linked_params[0].source" - error_message: "'foo.N' does not match '^[A-Za-z_][A-Za-z0-9_]*$'" - input: version: v1 program: @@ -243,5 +227,5 @@ - N description: "Target of a paramater link is not namespaced" error_path: "$.program.linked_params[0].targets[0]" - error_message: "'N' does not match '^[A-Za-z_][A-Za-z0-9_]*\\\\.[A-Za-z_][A-Za-z0-9_]*'" + error_message: "'N' does not match '^([A-Za-z_][A-Za-z0-9_]*\\\\.)+[A-Za-z_][A-Za-z0-9_]*$'" \ No newline at end of file From 3fe4cb4fe75580f4693ca5fc583469e97405cdfb Mon Sep 17 00:00:00 2001 From: mstechly Date: Tue, 25 Jun 2024 11:23:20 -0400 Subject: [PATCH 3/4] style: fix black issues --- .flake8 | 1 - src/qref/experimental/rendering.py | 12 +++--------- src/qref/schema_v1.py | 8 ++------ src/qref/verification.py | 12 +++--------- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/.flake8 b/.flake8 index 5f6e78d..b32f462 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,4 @@ [flake8] -max-line-length: 100 docstring-convention = google ignore = # Allow continuation line under-indented for visual indent. diff --git a/src/qref/experimental/rendering.py b/src/qref/experimental/rendering.py index d5d25e7..aa89aa6 100644 --- a/src/qref/experimental/rendering.py +++ b/src/qref/experimental/rendering.py @@ -90,9 +90,7 @@ def _format_node_name(node_name, parent): def _add_nonleaf_ports(ports, parent_cluster, parent_path: str, group_name): - with parent_cluster.subgraph( - name=f"{parent_path}: {group_name}", graph_attr=PORT_GROUP_ATTRS - ) as subgraph: + with parent_cluster.subgraph(name=f"{parent_path}: {group_name}", graph_attr=PORT_GROUP_ATTRS) as subgraph: for port in ports: subgraph.node(name=f"{parent_path}.{port.name}", label=port.name, **PORT_NODE_KWARGS) @@ -114,9 +112,7 @@ def _add_nonleaf(routine, dag: graphviz.Digraph, parent_path: str) -> None: input_ports, output_ports = _split_ports(routine.ports) full_path = f"{parent_path}.{routine.name}" - with dag.subgraph( - name=f"cluster_{full_path}", graph_attr={"label": routine.name, **CLUSTER_KWARGS} - ) as cluster: + with dag.subgraph(name=f"cluster_{full_path}", graph_attr={"label": routine.name, **CLUSTER_KWARGS}) as cluster: _add_nonleaf_ports(input_ports, cluster, full_path, "inputs") _add_nonleaf_ports(output_ports, cluster, full_path, "outputs") @@ -161,9 +157,7 @@ def to_graphviz(data: Union[dict, SchemaV1]) -> graphviz.Digraph: def render_entry_point(): parser = ArgumentParser() - parser.add_argument( - "input", help="Path to the YAML or JSON file with Routine in V1 schema", type=Path - ) + parser.add_argument("input", help="Path to the YAML or JSON file with Routine in V1 schema", type=Path) parser.add_argument( "output", help=( diff --git a/src/qref/schema_v1.py b/src/qref/schema_v1.py index 87b9d6d..10cc7ce 100644 --- a/src/qref/schema_v1.py +++ b/src/qref/schema_v1.py @@ -34,9 +34,7 @@ OPTIONALLY_MULTINAMESPACED_NAME_PATTERN = rf"^({NAME_PATTERN}\.)*{NAME_PATTERN}$" _Name = Annotated[str, StringConstraints(pattern=rf"^{NAME_PATTERN}$")] -_OptionallyNamespacedName = Annotated[ - str, StringConstraints(pattern=rf"{OPTIONALLY_NAMESPACED_NAME_PATTERN}") -] +_OptionallyNamespacedName = Annotated[str, StringConstraints(pattern=rf"{OPTIONALLY_NAMESPACED_NAME_PATTERN}")] _MultiNamespacedName = Annotated[str, StringConstraints(pattern=rf"{MULTINAMESPACED_NAME_PATTERN}")] _OptionallyMultiNamespacedName = Annotated[ str, StringConstraints(pattern=rf"{OPTIONALLY_MULTINAMESPACED_NAME_PATTERN}") @@ -121,9 +119,7 @@ def __init__(self, **data: Any): @classmethod def _validate_connections(cls, v, values) -> list[ConnectionV1]: children_port_names = [ - f"{child.name}.{port.name}" - for child in values.data.get("children") - for port in child.ports + f"{child.name}.{port.name}" for child in values.data.get("children") for port in child.ports ] parent_port_names = [port.name for port in values.data["ports"]] available_port_names = set(children_port_names + parent_port_names) diff --git a/src/qref/verification.py b/src/qref/verification.py index b9b897b..701846d 100644 --- a/src/qref/verification.py +++ b/src/qref/verification.py @@ -60,9 +60,7 @@ def _verify_routine_topology(routine: RoutineV1) -> list[str]: return problems -def _get_adjacency_list_from_routine( - routine: RoutineV1, path: Optional[str] -) -> dict[str, list[str]]: +def _get_adjacency_list_from_routine(routine: RoutineV1, path: Optional[str]) -> dict[str, list[str]]: """This function creates a flat graph representing one hierarchy level of a routine. Nodes represent ports and edges represent connections (they're directed). @@ -137,17 +135,13 @@ def _find_disconnected_ports(routine: RoutineV1): for port in child.ports: pname = f"{routine.name}.{child.name}.{port.name}" if port.direction == "input": - matches_in = [ - c for c in routine.connections if c.target == f"{child.name}.{port.name}" - ] + matches_in = [c for c in routine.connections if c.target == f"{child.name}.{port.name}"] if len(matches_in) == 0: problems.append(f"No incoming connections to {pname}.") elif len(matches_in) > 1: problems.append(f"Too many incoming connections to {pname}.") elif port.direction == "output": - matches_out = [ - c for c in routine.connections if c.source == f"{child.name}.{port.name}" - ] + matches_out = [c for c in routine.connections if c.source == f"{child.name}.{port.name}"] if len(matches_out) == 0: problems.append(f"No outgoing connections from {pname}.") elif len(matches_out) > 1: From e6f02741d60523cc04a6ac43d1f903f43b960e0c Mon Sep 17 00:00:00 2001 From: mstechly Date: Tue, 25 Jun 2024 11:24:20 -0400 Subject: [PATCH 4/4] style: fixed flake8 settings --- .flake8 | 1 + 1 file changed, 1 insertion(+) diff --git a/.flake8 b/.flake8 index b32f462..c793ca9 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,5 @@ [flake8] +max-line-length: 120 docstring-convention = google ignore = # Allow continuation line under-indented for visual indent.