Skip to content

Commit a6c2ef4

Browse files
authored
Use Sybil for doctests (#142)
* Use Sybil for doctests Signed-off-by: nstarman <[email protected]> * also test the docs Signed-off-by: nstarman <[email protected]> * try fixing basic_usage Signed-off-by: nstarman <[email protected]> * implement fixes Signed-off-by: nstarman <[email protected]> * push sctype test to future fix Signed-off-by: nstarman <[email protected]> * improve _split_parts --------- Signed-off-by: nstarman <[email protected]>
1 parent 3b6afc4 commit a6c2ef4

15 files changed

+303
-101
lines changed

conftest.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Doctest configuration."""
2+
3+
from __future__ import annotations
4+
5+
from doctest import ELLIPSIS, NORMALIZE_WHITESPACE
6+
7+
import pytest
8+
from sybil import Sybil
9+
from sybil.parsers.myst import DocTestDirectiveParser as MarkdownDocTestParser
10+
from sybil.parsers.myst import PythonCodeBlockParser as MarkdownPythonCodeBlockParser
11+
from sybil.parsers.myst import SkipParser as MarkdownSkipParser
12+
from sybil.parsers.rest import DocTestParser as ReSTDocTestParser
13+
from sybil.parsers.rest import PythonCodeBlockParser as ReSTPythonCodeBlockParser
14+
from sybil.parsers.rest import SkipParser as ReSTSkipParser
15+
16+
OPTIONS = ELLIPSIS | NORMALIZE_WHITESPACE
17+
18+
19+
@pytest.fixture(scope="module")
20+
def use_clean_dispatcher():
21+
import plum
22+
23+
# Save the original dispatcher
24+
dispatcher = plum.dispatch
25+
# Swap the dispatcher with a temporary one
26+
temp_dispatcher = plum.Dispatcher()
27+
plum.dispatch = temp_dispatcher
28+
yield
29+
# Restore the original dispatcher
30+
plum.dispatch = dispatcher
31+
32+
33+
markdown_examples = Sybil(
34+
parsers=[
35+
MarkdownDocTestParser(optionflags=OPTIONS),
36+
MarkdownPythonCodeBlockParser(doctest_optionflags=OPTIONS),
37+
MarkdownSkipParser(),
38+
],
39+
patterns=["*.md"],
40+
fixtures=["use_clean_dispatcher"],
41+
)
42+
43+
rest_examples = Sybil(
44+
parsers=[
45+
ReSTDocTestParser(optionflags=OPTIONS),
46+
ReSTPythonCodeBlockParser(),
47+
ReSTSkipParser(),
48+
],
49+
patterns=["*.py"],
50+
)
51+
52+
pytest_collect_file = (markdown_examples + rest_examples).pytest()

docs/basic_usage.md

+10-5
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ We haven't implemented a method for `float`s, so in that case an exception
2929
will be raised:
3030

3131
```python
32-
>>> f(1.0)
33-
NotFoundLookupError: For function `f`, `(1.0,)` could not be resolved.
32+
>>> try: f(1.0)
33+
... except Exception as e: print(f"{type(e).__name__}: {e}")
34+
NotFoundLookupError: `f(1.0)` could not be resolved...
3435
```
3536

3637
Instead of implementing a method for `float`s, let's implement a method for
@@ -62,9 +63,13 @@ For a function `f`, all available methods can be obtained with `f.methods`:
6263

6364
```python
6465
>>> f.methods
65-
[Signature(str, implementation=<function f at 0x7f9f6014f160>),
66-
Signature(int, implementation=<function f at 0x7f9f6029c280>),
67-
Signature(numbers.Number, implementation=<function f at 0x7f9f801fdf70>)]
66+
List of 3 method(s):
67+
[0] f(x: str)
68+
<function f at ...> @ ...
69+
[1] f(x: int)
70+
<function f at ...> @ ...
71+
[2] f(x: numbers.Number)
72+
<function f at ...> @ ...
6873
```
6974

7075
For an excellent and way more detailed overview of multiple dispatch, see the

docs/classes.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ You can use dispatch within classes:
55
```python
66
from plum import dispatch
77

8-
98
class Real:
109
@dispatch
1110
def __add__(self, other: int):
@@ -54,7 +53,7 @@ class MyClass:
5453

5554
>>> a.name = "1" # OK
5655

57-
>>> a.name = 1 # Not OK
56+
>>> a.name = 1 # Not OK # doctest:+SKIP
5857
NotFoundLookupError: For function `name` of `__main__.MyClass`, `(<__main__.MyClass object at 0x7f8cb8813eb0>, 1)` could not be resolved.
5958
```
6059

@@ -76,6 +75,8 @@ class Real:
7675

7776
If we try to run this, we get the following error:
7877

78+
% skip: next
79+
7980
```python
8081
NameError Traceback (most recent call last)
8182
<ipython-input-1-2c6fe56c8a98> in <module>

docs/comparison.md

+16-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ This is not necessarily the case for other packages.
2222

2323
For example,
2424

25+
% skip: start "multipledispatch is not installed or tested"
26+
2527
```python
2628
from numbers import Number
2729

@@ -62,6 +64,8 @@ Possible fix, define
6264
f(::Int64, ::Int64)
6365
```
6466

67+
% skip: end
68+
6569
Plum handles this correctly.
6670

6771
```python
@@ -82,10 +86,14 @@ def f(x: int, y: Number):
8286
```
8387

8488
```python
85-
>>> f(1, 1)
86-
AmbiguousLookupError: For function `f`, `(1, 1)` is ambiguous among the following:
87-
Signature(typing.Union[int, numbers.Number], int, implementation=<function f at 0x7fbbe8e3cdc0>) (precedence: 0)
88-
Signature(int, numbers.Number, implementation=<function f at 0x7fbbc81813a0>) (precedence: 0)
89+
>>> try: f(1, 1)
90+
... except Exception as e: print(f"{type(e).__name__}: {e}")
91+
AmbiguousLookupError: `f(1, 1)` is ambiguous.
92+
Candidates:
93+
f(x: typing.Union[int, numbers.Number], y: int)
94+
<function f at ...> @ ...
95+
f(x: int, y: numbers.Number)
96+
<function f at ...> @ ...
8997
```
9098

9199
Just to sanity check that things are indeed working correctly:
@@ -104,6 +112,8 @@ Plum takes OOP very seriously.
104112

105113
Consider the following snippet:
106114

115+
% skip: start "other multiple-dispatchers are not installed or tested"
116+
107117
```python
108118
from multipledispatch import dispatch
109119

@@ -156,6 +166,8 @@ class B(A):
156166
DispatchError: ('f: 0 methods found', (<class '__main__.B'>, <class 'str'>), [])
157167
```
158168

169+
% skip: end
170+
159171
This behaviour is undesirable.
160172
Since `B.f` isn't matched, according to OOP principles, `A.f` should be tried next.
161173
Plum correctly implements this:

docs/conversion_promotion.md

+14-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
When a return type is not matched, Plum will attempt to convert the result to the
77
right type.
88

9+
% clear-namespace
10+
911
```python
1012
from typing import Union
1113

@@ -21,7 +23,8 @@ def f(x: Union[int, str]) -> int:
2123
>>> f(1)
2224
1
2325

24-
>>> f("1")
26+
>>> try: f("1")
27+
... except Exception as e: print(f"{type(e).__name__}: {e}")
2528
TypeError: Cannot convert `1` to `int`.
2629
```
2730

@@ -60,8 +63,9 @@ class Rational:
6063
>>> convert(0.5, Number)
6164
0.5
6265

63-
>>> convert(Rational(1, 2), Number)
64-
TypeError: Cannot convert `<__main__.Rational object at 0x7f88f8369310>` to `numbers.Number`.
66+
>>> try: convert(Rational(1, 2), Number)
67+
... except Exception as e: print(f"{type(e).__name__}: {e}")
68+
TypeError: Cannot convert `<Rational object at ...>` to `numbers.Number`.
6569
```
6670

6771
The `TypeError` indicates that `convert` does not know how to convert a
@@ -87,13 +91,16 @@ def rational_to_number(q):
8791
As above, instead of the decorator `conversion_method`, one can also use
8892
`add_conversion_method`:
8993

94+
% skip: start
9095

9196
```python
9297
>>> from plum import add_conversion_method
9398

9499
>>> add_conversion_method(type_from, type_to, conversion_function)
95100
```
96101

102+
% skip: end
103+
97104
## Promotion
98105

99106
The function `promote` can be used to promote objects to a common type:
@@ -124,7 +131,8 @@ def add(x: float, y: float):
124131
>>> add(1.0, 2.0)
125132
3.0
126133

127-
>>> add(1, 2.0)
134+
>>> try: add(1, 2.0)
135+
... except Exception as e: print(f"{type(e).__name__}: {e}")
128136
TypeError: No promotion rule for `int` and `float`.
129137
```
130138

@@ -133,7 +141,8 @@ You can add promotion rules with `add_promotion_rule`:
133141
```python
134142
>>> add_promotion_rule(int, float, float)
135143

136-
>>> add(1, 2.0)
144+
>>> try: add(1, 2.0)
145+
... except Exception as e: print(f"{type(e).__name__}: {e}")
137146
TypeError: Cannot convert `1` to `float`.
138147

139148
>>> add_conversion_method(type_from=int, type_to=float, f=float)

docs/dispatch.md

+17-4
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ Then
3838

3939
```python
4040
>>> f.methods # No implementation for `Number`s!
41-
[Signature(float, float, implementation=<function f at 0x7f8c4015e9d0>),
42-
Signature(int, int, implementation=<function f at 0x7f8c4015ea60>)]
41+
List of 2 method(s):
42+
[0] f(x: float, y: float)
43+
<function f at ...> @ ...
44+
[1] f(x: int, y: int)
45+
<function f at ...> @ ...
4346
```
4447

4548
and calling `help(f)` produces
@@ -68,6 +71,8 @@ f(x: numbers.Number, y: numbers.Number)
6871
`Function.dispatch` can be used to extend a particular function from an external
6972
package:
7073

74+
% skip: start "`package` is not a real package"
75+
7176
```python
7277
from package import f
7378

@@ -85,6 +90,8 @@ def f(x: int):
8590
'new behaviour'
8691
```
8792

93+
% skip: end
94+
8895
## Directly Invoke a Method
8996

9097
`Function.invoke` can be used to invoke a method given types of the arguments:
@@ -139,6 +146,12 @@ def add(x: Union[int, float], y: Union[int, float]):
139146
>>> add(1.0, 1.0)
140147
2.0
141148

142-
>>> add(1, 1.0)
143-
NotFoundLookupError: For function "add", signature Signature(builtins.int, builtins.float) could not be resolved.
149+
>>> try: add(1, 1.0)
150+
... except Exception as e: print(f"{type(e).__name__}: {e}")
151+
NotFoundLookupError: `add(1, 1.0)` could not be resolved.
152+
Closest candidates are the following:
153+
add(x: int, y: int)
154+
<function add at ...> @ ...
155+
add(x: float, y: float)
156+
<function add at ...> @ ...
144157
```

docs/keyword_arguments.md

+13-7
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ def f(x: int):
2323
>>> f(1) # OK
2424
1
2525
26-
>>> f(x=1) # Not OK
27-
NotFoundLookupError: For function `f`, `()` could not be resolved.
26+
>>> try: f(x=1) # Not OK
27+
... except Exception as e: print(f"{type(e).__name__}: {e}")
28+
NotFoundLookupError: `f()` could not be resolved...
2829
```
2930
3031
See [below](why) for why this is the case.
@@ -75,19 +76,20 @@ from plum import dispatch
7576

7677

7778
@dispatch
78-
def f(x, *, option="a"):
79+
def g(x, *, option="a"):
7980
return option
8081
```
8182

8283
```python
83-
>>> f(1)
84+
>>> g(1)
8485
'a'
8586

86-
>>> f(1, option="b")
87+
>>> g(1, option="b")
8788
'b'
8889

89-
>>> f(1, "b") # This will not work, because `option` must be given as a keyword.
90-
NotFoundLookupError: For function `f`, `(1, 'b')` could not be resolved.
90+
>>> try: g(1, "b") # This will not work, because `option` must be given as a keyword.
91+
... except Exception as e: print(f"{type(e).__name__}: {e}")
92+
NotFoundLookupError: `g(1, 'b')` could not be resolved...
9193
```
9294

9395
(why)=
@@ -99,6 +101,8 @@ Is not entirely clear how
99101
this would work.
100102
For example, consider the following scenario:
101103

104+
% skip: start "pseudocode"
105+
102106
```python
103107
@dispatch
104108
def f(x: int, y: float):
@@ -139,3 +143,5 @@ and once you position something, the name of the argument becomes irrelevant.
139143

140144
Therefore, if we were to support naming arguments,
141145
how precisely this would work would have to be spelled out in detail.
146+
147+
% skip: end

0 commit comments

Comments
 (0)