guppylang 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. guppylang-0.1.0/PKG-INFO +139 -0
  2. guppylang-0.1.0/README.md +117 -0
  3. guppylang-0.1.0/guppylang/__init__.py +5 -0
  4. guppylang-0.1.0/guppylang/ast_util.py +327 -0
  5. guppylang-0.1.0/guppylang/cfg/__init__.py +0 -0
  6. guppylang-0.1.0/guppylang/cfg/analysis.py +186 -0
  7. guppylang-0.1.0/guppylang/cfg/bb.py +166 -0
  8. guppylang-0.1.0/guppylang/cfg/builder.py +514 -0
  9. guppylang-0.1.0/guppylang/cfg/cfg.py +69 -0
  10. guppylang-0.1.0/guppylang/checker/__init__.py +0 -0
  11. guppylang-0.1.0/guppylang/checker/cfg_checker.py +241 -0
  12. guppylang-0.1.0/guppylang/checker/core.py +209 -0
  13. guppylang-0.1.0/guppylang/checker/expr_checker.py +944 -0
  14. guppylang-0.1.0/guppylang/checker/func_checker.py +207 -0
  15. guppylang-0.1.0/guppylang/checker/stmt_checker.py +154 -0
  16. guppylang-0.1.0/guppylang/compiler/__init__.py +0 -0
  17. guppylang-0.1.0/guppylang/compiler/cfg_compiler.py +177 -0
  18. guppylang-0.1.0/guppylang/compiler/core.py +120 -0
  19. guppylang-0.1.0/guppylang/compiler/expr_compiler.py +331 -0
  20. guppylang-0.1.0/guppylang/compiler/func_compiler.py +121 -0
  21. guppylang-0.1.0/guppylang/compiler/stmt_compiler.py +92 -0
  22. guppylang-0.1.0/guppylang/custom.py +251 -0
  23. guppylang-0.1.0/guppylang/declared.py +73 -0
  24. guppylang-0.1.0/guppylang/decorator.py +299 -0
  25. guppylang-0.1.0/guppylang/error.py +195 -0
  26. guppylang-0.1.0/guppylang/gtypes.py +654 -0
  27. guppylang-0.1.0/guppylang/hugr/__init__.py +0 -0
  28. guppylang-0.1.0/guppylang/hugr/hugr.py +799 -0
  29. guppylang-0.1.0/guppylang/hugr/ops.py +475 -0
  30. guppylang-0.1.0/guppylang/hugr/raw.py +23 -0
  31. guppylang-0.1.0/guppylang/hugr/tys.py +306 -0
  32. guppylang-0.1.0/guppylang/hugr/val.py +60 -0
  33. guppylang-0.1.0/guppylang/hugr/visualise.py +285 -0
  34. guppylang-0.1.0/guppylang/module.py +303 -0
  35. guppylang-0.1.0/guppylang/nodes.py +184 -0
  36. guppylang-0.1.0/guppylang/prelude/__init__.py +0 -0
  37. guppylang-0.1.0/guppylang/prelude/_internal.py +327 -0
  38. guppylang-0.1.0/guppylang/prelude/builtins.py +885 -0
  39. guppylang-0.1.0/guppylang/prelude/quantum.py +130 -0
  40. guppylang-0.1.0/pyproject.toml +50 -0
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.1
2
+ Name: guppylang
3
+ Version: 0.1.0
4
+ Summary:
5
+ Home-page: https://github.com/CQCL/guppy
6
+ License: Apache-2.0
7
+ Author: Mark Koch
8
+ Author-email: mark.koch@quantinuum.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Dist: graphviz (>=0.20.1,<0.21.0)
16
+ Requires-Dist: networkx (>=3.2.1,<4.0.0)
17
+ Requires-Dist: pydantic (>=2.5.3,<3.0.0)
18
+ Requires-Dist: typing-extensions (>=4.9.0,<5.0.0)
19
+ Project-URL: Repository, https://github.com/CQCL/guppy
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Guppy
23
+
24
+ Guppy is a quantum programming language that is fully embedded into Python.
25
+ It allows you to write high-level hybrid quantum programs with classical control flow and mid-circuit measurements using Pythonic syntax:
26
+
27
+ ```python
28
+ from guppylang import guppy, Qubit, quantum
29
+
30
+ guppy.load(quantum)
31
+
32
+ # Teleports the state in `src` to `tgt`.
33
+ @guppy
34
+ def teleport(src: Qubit, tgt: Qubit) -> Qubit:
35
+ # Create ancilla and entangle it with src and tgt
36
+ tmp = Qubit()
37
+ tmp, tgt = cx(h(tmp), tgt)
38
+ src, tmp = cx(src, tmp)
39
+
40
+ # Apply classical corrections
41
+ if measure(h(src)):
42
+ tgt = z(tgt)
43
+ if measure(tmp):
44
+ tgt = x(tgt)
45
+ return tgt
46
+ ```
47
+
48
+ More examples and tutorials are available [here][examples].
49
+
50
+ [examples]: ./examples/
51
+
52
+
53
+ ## Install
54
+
55
+ Guppy can be installed via `pip`. Requires Python >= 3.10.
56
+
57
+ ```sh
58
+ pip install guppylang
59
+ ```
60
+
61
+
62
+ ## Usage
63
+
64
+ See the [Getting Started][getting-started] guide and the other [examples].
65
+
66
+ [getting-started]: ./examples/1-Getting-Started.md
67
+
68
+
69
+ ## Development
70
+
71
+ These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
72
+
73
+ ### Prerequisites
74
+
75
+ - Python >= 3.10
76
+ - [Poetry](https://python-poetry.org/docs/#installation)
77
+ - [Rust](https://www.rust-lang.org/tools/install) >= 1.75.0 (only needed for tests)
78
+
79
+ ### Installing
80
+
81
+ Run the following to setup your virtual environment and install dependencies:
82
+
83
+ ```sh
84
+ poetry install --with validation
85
+ ```
86
+
87
+ Note that the `--with validation` flag is optional and only needed to run integration tests.
88
+
89
+ You can then activate the virtual environment and work within it with:
90
+
91
+ ```sh
92
+ poetry shell
93
+ ```
94
+
95
+ Consider using [direnv](https://github.com/direnv/direnv/wiki/Python#poetry) to
96
+ automate this when entering and leaving a directory.
97
+
98
+ To run a single command in the shell, just prefix it with `poetry run`.
99
+
100
+ ### Pre-commit
101
+
102
+ Install the pre-commit hook by running:
103
+
104
+ ```sh
105
+ poetry run pre-commit install
106
+ ```
107
+
108
+
109
+ ### Testing
110
+
111
+ Run tests using
112
+
113
+ ```sh
114
+ poetry run pytest -v
115
+ ```
116
+
117
+ You have to install extra dependencies to test automatic circuit conversion from `pytket`:
118
+
119
+ ```sh
120
+ poetry install --with pytket
121
+ poetry run pytest -v # Now rerun tests
122
+ ```
123
+
124
+
125
+ Integration test cases can be exported to a directory using
126
+
127
+ ```sh
128
+ poetry run pytest --export-test-cases=guppy-exports
129
+ ```
130
+
131
+ which will create a directory `./guppy-exports` populated with hugr modules serialised in JSON.
132
+
133
+
134
+ ## License
135
+
136
+ This project is licensed under Apache License, Version 2.0 ([LICENCE][] or http://www.apache.org/licenses/LICENSE-2.0).
137
+
138
+ [LICENCE]: ./LICENCE
139
+
@@ -0,0 +1,117 @@
1
+ # Guppy
2
+
3
+ Guppy is a quantum programming language that is fully embedded into Python.
4
+ It allows you to write high-level hybrid quantum programs with classical control flow and mid-circuit measurements using Pythonic syntax:
5
+
6
+ ```python
7
+ from guppylang import guppy, Qubit, quantum
8
+
9
+ guppy.load(quantum)
10
+
11
+ # Teleports the state in `src` to `tgt`.
12
+ @guppy
13
+ def teleport(src: Qubit, tgt: Qubit) -> Qubit:
14
+ # Create ancilla and entangle it with src and tgt
15
+ tmp = Qubit()
16
+ tmp, tgt = cx(h(tmp), tgt)
17
+ src, tmp = cx(src, tmp)
18
+
19
+ # Apply classical corrections
20
+ if measure(h(src)):
21
+ tgt = z(tgt)
22
+ if measure(tmp):
23
+ tgt = x(tgt)
24
+ return tgt
25
+ ```
26
+
27
+ More examples and tutorials are available [here][examples].
28
+
29
+ [examples]: ./examples/
30
+
31
+
32
+ ## Install
33
+
34
+ Guppy can be installed via `pip`. Requires Python >= 3.10.
35
+
36
+ ```sh
37
+ pip install guppylang
38
+ ```
39
+
40
+
41
+ ## Usage
42
+
43
+ See the [Getting Started][getting-started] guide and the other [examples].
44
+
45
+ [getting-started]: ./examples/1-Getting-Started.md
46
+
47
+
48
+ ## Development
49
+
50
+ These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
51
+
52
+ ### Prerequisites
53
+
54
+ - Python >= 3.10
55
+ - [Poetry](https://python-poetry.org/docs/#installation)
56
+ - [Rust](https://www.rust-lang.org/tools/install) >= 1.75.0 (only needed for tests)
57
+
58
+ ### Installing
59
+
60
+ Run the following to setup your virtual environment and install dependencies:
61
+
62
+ ```sh
63
+ poetry install --with validation
64
+ ```
65
+
66
+ Note that the `--with validation` flag is optional and only needed to run integration tests.
67
+
68
+ You can then activate the virtual environment and work within it with:
69
+
70
+ ```sh
71
+ poetry shell
72
+ ```
73
+
74
+ Consider using [direnv](https://github.com/direnv/direnv/wiki/Python#poetry) to
75
+ automate this when entering and leaving a directory.
76
+
77
+ To run a single command in the shell, just prefix it with `poetry run`.
78
+
79
+ ### Pre-commit
80
+
81
+ Install the pre-commit hook by running:
82
+
83
+ ```sh
84
+ poetry run pre-commit install
85
+ ```
86
+
87
+
88
+ ### Testing
89
+
90
+ Run tests using
91
+
92
+ ```sh
93
+ poetry run pytest -v
94
+ ```
95
+
96
+ You have to install extra dependencies to test automatic circuit conversion from `pytket`:
97
+
98
+ ```sh
99
+ poetry install --with pytket
100
+ poetry run pytest -v # Now rerun tests
101
+ ```
102
+
103
+
104
+ Integration test cases can be exported to a directory using
105
+
106
+ ```sh
107
+ poetry run pytest --export-test-cases=guppy-exports
108
+ ```
109
+
110
+ which will create a directory `./guppy-exports` populated with hugr modules serialised in JSON.
111
+
112
+
113
+ ## License
114
+
115
+ This project is licensed under Apache License, Version 2.0 ([LICENCE][] or http://www.apache.org/licenses/LICENSE-2.0).
116
+
117
+ [LICENCE]: ./LICENCE
@@ -0,0 +1,5 @@
1
+ from guppylang.decorator import guppy
2
+ from guppylang.module import GuppyModule
3
+ from guppylang.prelude import builtins, quantum
4
+ from guppylang.prelude.builtins import Bool, Float, Int, List, linst
5
+ from guppylang.prelude.quantum import Qubit
@@ -0,0 +1,327 @@
1
+ import ast
2
+ import textwrap
3
+ from collections.abc import Callable, Mapping, Sequence
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, cast
6
+
7
+ if TYPE_CHECKING:
8
+ from guppylang.gtypes import GuppyType
9
+
10
+ AstNode = (
11
+ ast.AST
12
+ | ast.operator
13
+ | ast.expr
14
+ | ast.arg
15
+ | ast.stmt
16
+ | ast.Name
17
+ | ast.keyword
18
+ | ast.FunctionDef
19
+ )
20
+
21
+ T = TypeVar("T", covariant=True)
22
+
23
+
24
+ class AstVisitor(Generic[T]):
25
+ """
26
+ Note: This class is based on the implementation of `ast.NodeVisitor` but
27
+ allows extra arguments to be passed to the `visit` functions.
28
+
29
+ Original documentation:
30
+
31
+ A node visitor base class that walks the abstract syntax tree and calls a
32
+ visitor function for every node found. This function may return a value
33
+ which is forwarded by the `visit` method.
34
+
35
+ This class is meant to be subclassed, with the subclass adding visitor
36
+ methods.
37
+
38
+ Per default the visitor functions for the nodes are ``'visit_'`` +
39
+ class name of the node. So a `TryFinally` node visit function would
40
+ be `visit_TryFinally`. This behavior can be changed by overriding
41
+ the `visit` method. If no visitor function exists for a node
42
+ (return value `None`) the `generic_visit` visitor is used instead.
43
+
44
+ Don't use the `NodeVisitor` if you want to apply changes to nodes during
45
+ traversing. For this a special visitor exists (`NodeTransformer`) that
46
+ allows modifications.
47
+ """
48
+
49
+ def visit(self, node: Any, *args: Any, **kwargs: Any) -> T:
50
+ """Visit a node."""
51
+ method = "visit_" + node.__class__.__name__
52
+ visitor = getattr(self, method, self.generic_visit)
53
+ return visitor(node, *args, **kwargs)
54
+
55
+ def generic_visit(self, node: Any, *args: Any, **kwargs: Any) -> T:
56
+ """Called if no explicit visitor function exists for a node."""
57
+ raise NotImplementedError(f"visit_{node.__class__.__name__} is not implemented")
58
+
59
+
60
+ class AstSearcher(ast.NodeVisitor):
61
+ """Visitor that searches for occurrences of specific nodes in an AST."""
62
+
63
+ matcher: Callable[[ast.AST], bool]
64
+ dont_recurse_into: set[type[ast.AST]]
65
+ found: list[ast.AST]
66
+ is_first_node: bool
67
+
68
+ def __init__(
69
+ self,
70
+ matcher: Callable[[ast.AST], bool],
71
+ dont_recurse_into: set[type[ast.AST]] | None = None,
72
+ ) -> None:
73
+ self.matcher = matcher
74
+ self.dont_recurse_into = dont_recurse_into or set()
75
+ self.found = []
76
+ self.is_first_node = True
77
+
78
+ def generic_visit(self, node: ast.AST) -> None:
79
+ if self.matcher(node):
80
+ self.found.append(node)
81
+ if self.is_first_node or type(node) not in self.dont_recurse_into:
82
+ self.is_first_node = False
83
+ super().generic_visit(node)
84
+
85
+
86
+ def find_nodes(
87
+ matcher: Callable[[ast.AST], bool],
88
+ node: ast.AST,
89
+ dont_recurse_into: set[type[ast.AST]] | None = None,
90
+ ) -> list[ast.AST]:
91
+ """Returns all nodes in the AST that satisfy the matcher."""
92
+ v = AstSearcher(matcher, dont_recurse_into)
93
+ v.visit(node)
94
+ return v.found
95
+
96
+
97
+ def name_nodes_in_ast(node: Any) -> list[ast.Name]:
98
+ """Returns all `Name` nodes occurring in an AST."""
99
+ found = find_nodes(lambda n: isinstance(n, ast.Name), node)
100
+ return cast(list[ast.Name], found)
101
+
102
+
103
+ def return_nodes_in_ast(node: Any) -> list[ast.Return]:
104
+ """Returns all `Return` nodes occurring in an AST."""
105
+ found = find_nodes(lambda n: isinstance(n, ast.Return), node, {ast.FunctionDef})
106
+ return cast(list[ast.Return], found)
107
+
108
+
109
+ def breaks_in_loop(node: Any) -> list[ast.Break]:
110
+ """Returns all `Break` nodes occurring in a loop.
111
+
112
+ Note that breaks in nested loops are excluded.
113
+ """
114
+ found = find_nodes(
115
+ lambda n: isinstance(n, ast.Break), node, {ast.For, ast.While, ast.FunctionDef}
116
+ )
117
+ return cast(list[ast.Break], found)
118
+
119
+
120
+ class ContextAdjuster(ast.NodeTransformer):
121
+ """Updates the `ast.Context` indicating if expressions occur on the LHS or RHS."""
122
+
123
+ ctx: ast.expr_context
124
+
125
+ def __init__(self, ctx: ast.expr_context) -> None:
126
+ self.ctx = ctx
127
+
128
+ def visit(self, node: ast.AST) -> ast.AST:
129
+ return cast(ast.AST, super().visit(node))
130
+
131
+ def visit_Name(self, node: ast.Name) -> ast.Name:
132
+ return with_loc(node, ast.Name(id=node.id, ctx=self.ctx))
133
+
134
+ def visit_Starred(self, node: ast.Starred) -> ast.Starred:
135
+ return with_loc(node, ast.Starred(value=self.visit(node.value), ctx=self.ctx))
136
+
137
+ def visit_Tuple(self, node: ast.Tuple) -> ast.Tuple:
138
+ return with_loc(
139
+ node, ast.Tuple(elts=[self.visit(elt) for elt in node.elts], ctx=self.ctx)
140
+ )
141
+
142
+ def visit_List(self, node: ast.List) -> ast.List:
143
+ return with_loc(
144
+ node, ast.List(elts=[self.visit(elt) for elt in node.elts], ctx=self.ctx)
145
+ )
146
+
147
+ def visit_Subscript(self, node: ast.Subscript) -> ast.Subscript:
148
+ # Don't adjust the slice!
149
+ return with_loc(
150
+ node,
151
+ ast.Subscript(value=self.visit(node.value), slice=node.slice, ctx=self.ctx),
152
+ )
153
+
154
+ def visit_Attribute(self, node: ast.Attribute) -> ast.Attribute:
155
+ return with_loc(
156
+ node,
157
+ ast.Attribute(value=self.visit(node.value), attr=node.attr, ctx=self.ctx),
158
+ )
159
+
160
+
161
+ @dataclass(frozen=True, eq=False)
162
+ class TemplateReplacer(ast.NodeTransformer):
163
+ """Replaces nodes in a template."""
164
+
165
+ replacements: Mapping[str, ast.AST | Sequence[ast.AST]]
166
+ default_loc: ast.AST
167
+
168
+ def _get_replacement(self, x: str) -> ast.AST | Sequence[ast.AST]:
169
+ if x not in self.replacements:
170
+ msg = f"No replacement for `{x}` is given"
171
+ raise ValueError(msg)
172
+ return self.replacements[x]
173
+
174
+ def visit_Name(self, node: ast.Name) -> ast.AST:
175
+ repl = self._get_replacement(node.id)
176
+ if not isinstance(repl, ast.expr):
177
+ msg = f"Replacement for `{node.id}` must be an expression"
178
+ raise TypeError(msg)
179
+
180
+ # Update the context
181
+ adjuster = ContextAdjuster(node.ctx)
182
+ return with_loc(repl, adjuster.visit(repl))
183
+
184
+ def visit_Expr(self, node: ast.Expr) -> ast.AST | Sequence[ast.AST]:
185
+ if isinstance(node.value, ast.Name):
186
+ repl = self._get_replacement(node.value.id)
187
+ repls = [repl] if not isinstance(repl, Sequence) else repl
188
+ # Wrap expressions to turn them into statements
189
+ return [
190
+ with_loc(r, ast.Expr(value=r)) if isinstance(r, ast.expr) else r
191
+ for r in repls
192
+ ]
193
+ return self.generic_visit(node)
194
+
195
+ def generic_visit(self, node: ast.AST) -> ast.AST:
196
+ # Insert the default location
197
+ node = super().generic_visit(node)
198
+ return with_loc(self.default_loc, node)
199
+
200
+
201
+ def template_replace(
202
+ template: str, default_loc: ast.AST, **kwargs: ast.AST | Sequence[ast.AST]
203
+ ) -> list[ast.stmt]:
204
+ """Turns a template into a proper AST by substituting all placeholders."""
205
+ nodes = ast.parse(textwrap.dedent(template)).body
206
+ replacer = TemplateReplacer(kwargs, default_loc)
207
+ new_nodes = []
208
+ for n in nodes:
209
+ new = replacer.visit(n)
210
+ if isinstance(new, list):
211
+ new_nodes.extend(new)
212
+ else:
213
+ new_nodes.append(new)
214
+ return new_nodes
215
+
216
+
217
+ def line_col(node: ast.AST) -> tuple[int, int]:
218
+ """Returns the line and column of an ast node."""
219
+ return node.lineno, node.col_offset
220
+
221
+
222
+ def set_location_from(node: ast.AST, loc: ast.AST) -> None:
223
+ """Copy source location from one AST node to the other."""
224
+ node.lineno = loc.lineno
225
+ node.col_offset = loc.col_offset
226
+ node.end_lineno = loc.end_lineno
227
+ node.end_col_offset = loc.end_col_offset
228
+
229
+ source, file, line_offset = get_source(loc), get_file(loc), get_line_offset(loc)
230
+ assert source is not None
231
+ assert file is not None
232
+ assert line_offset is not None
233
+ annotate_location(node, source, file, line_offset)
234
+
235
+
236
+ def annotate_location(
237
+ node: ast.AST, source: str, file: str, line_offset: int, recurse: bool = True
238
+ ) -> None:
239
+ node.line_offset = line_offset # type: ignore[attr-defined]
240
+ node.file = file # type: ignore[attr-defined]
241
+ node.source = source # type: ignore[attr-defined]
242
+
243
+ if recurse:
244
+ for _field, value in ast.iter_fields(node):
245
+ if isinstance(value, list):
246
+ for item in value:
247
+ if isinstance(item, ast.AST):
248
+ annotate_location(item, source, file, line_offset, recurse)
249
+ elif isinstance(value, ast.AST):
250
+ annotate_location(value, source, file, line_offset, recurse)
251
+
252
+
253
+ def get_file(node: AstNode) -> str | None:
254
+ """Tries to retrieve a file annotation from an AST node."""
255
+ try:
256
+ file = node.file # type: ignore[union-attr]
257
+ return file if isinstance(file, str) else None
258
+ except AttributeError:
259
+ return None
260
+
261
+
262
+ def get_source(node: AstNode) -> str | None:
263
+ """Tries to retrieve a source annotation from an AST node."""
264
+ try:
265
+ source = node.source # type: ignore[union-attr]
266
+ return source if isinstance(source, str) else None
267
+ except AttributeError:
268
+ return None
269
+
270
+
271
+ def get_line_offset(node: AstNode) -> int | None:
272
+ """Tries to retrieve a line offset annotation from an AST node."""
273
+ try:
274
+ line_offset = node.line_offset # type: ignore[union-attr]
275
+ return line_offset if isinstance(line_offset, int) else None
276
+ except AttributeError:
277
+ return None
278
+
279
+
280
+ A = TypeVar("A", bound=ast.AST)
281
+
282
+
283
+ def with_loc(loc: ast.AST, node: A) -> A:
284
+ """Copy source location from one AST node to the other."""
285
+ set_location_from(node, loc)
286
+ return node
287
+
288
+
289
+ def with_type(ty: "GuppyType", node: A) -> A:
290
+ """Annotates an AST node with a type."""
291
+ node.type = ty # type: ignore[attr-defined]
292
+ return node
293
+
294
+
295
+ def get_type_opt(node: AstNode) -> Optional["GuppyType"]:
296
+ """Tries to retrieve a type annotation from an AST node."""
297
+ from guppylang.gtypes import GuppyType
298
+
299
+ try:
300
+ ty = node.type # type: ignore[union-attr]
301
+ return ty if isinstance(ty, GuppyType) else None
302
+ except AttributeError:
303
+ return None
304
+
305
+
306
+ def get_type(node: AstNode) -> "GuppyType":
307
+ """Retrieve a type annotation from an AST node.
308
+
309
+ Fails if the node is not annotated.
310
+ """
311
+ ty = get_type_opt(node)
312
+ assert ty is not None
313
+ return ty
314
+
315
+
316
+ def has_empty_body(func_ast: ast.FunctionDef) -> bool:
317
+ """Returns `True` if the body of a function definition is empty.
318
+
319
+ This is the case if the body only contains a single `pass` statement or an ellipsis
320
+ `...` expression.
321
+ """
322
+ if len(func_ast.body) == 0:
323
+ return True
324
+ if len(func_ast.body) > 1:
325
+ return False
326
+ [n] = func_ast.body
327
+ return isinstance(n, ast.Expr) and isinstance(n.value, ast.Ellipsis)
File without changes