Skip to content

Commit 8b010f0

Browse files
authored
Type hints (#6)
1 parent 49ecae8 commit 8b010f0

File tree

6 files changed

+283
-4
lines changed

6 files changed

+283
-4
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
[Pasteboard](https://pypi.org/project/pasteboard/) exposes Python bindings for reading and writing macOS' AppKit [NSPasteboard](https://developer.apple.com/documentation/appkit/nspasteboard). This allows retrieving different formats (HTML/RTF fragments, PDF/PNG/TIFF) and efficient polling of the pasteboard.
66

7+
Now with type hints!
8+
79
## Installation
810

911
Obviously, this module will only compile on **macOS**:
@@ -64,6 +66,30 @@ takes two arguments:
6466

6567
You don't need to know this if you're not changing `pasteboard.m` code. There are some integration tests in `tests.py` to check the module works as designed (using [pytest](https://docs.pytest.org/en/latest/) and [hypothesis](https://hypothesis.readthedocs.io/en/latest/)).
6668

69+
This project uses [pre-commit](https://pre-commit.com/) to run some linting hooks when committing. When you first clone the repo, please run:
70+
71+
```
72+
pre-commit install
73+
```
74+
75+
You may also run the hooks at any time:
76+
77+
```
78+
pre-commit run --all-files
79+
```
80+
81+
Dependencies are managed via [poetry](https://python-poetry.org/). To install all dependencies, use:
82+
83+
```
84+
poetry install
85+
```
86+
87+
This will also install development dependencies (`pytest`). To run the tests:
88+
89+
```
90+
poetry run pytest tests.py --verbose
91+
```
92+
6793
## License
6894

6995
From version 0.3.0 and forwards, this library is licensed under the Mozilla Public License Version 2.0. For more information, see `LICENSE`.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pasteboard"
3-
version = "0.3.0"
3+
version = "0.3.1"
44
description = "Pasteboard - Python interface for reading from NSPasteboard (macOS clipboard)"
55
authors = ["Toby Fleming <[email protected]>"]
66
license = "MPL-2.0"
@@ -31,6 +31,7 @@ python = "^3.6"
3131
black = "^19.10b0"
3232
pytest = "^5.3.5"
3333
hypothesis = "^5.5.4"
34+
mypy = "^0.761"
3435

3536
[build-system]
3637
requires = ["poetry>=1.0.0"]

src/pasteboard/__init__.pyi

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from typing import overload, AnyStr, Optional, Union
2+
3+
4+
class PasteboardType: ...
5+
6+
7+
HTML: PasteboardType
8+
PDF: PasteboardType
9+
PNG: PasteboardType
10+
RTF: PasteboardType
11+
String: PasteboardType
12+
TIFF: PasteboardType
13+
TabularText: PasteboardType
14+
15+
16+
class Pasteboard:
17+
@classmethod
18+
def __init__(self) -> None: ...
19+
20+
@overload
21+
def get_contents(self) -> str: ...
22+
23+
@overload
24+
def get_contents(
25+
self,
26+
diff: bool = ...,
27+
) -> Optional[str]: ...
28+
29+
@overload
30+
def get_contents(
31+
self,
32+
type: PasteboardType = ...,
33+
diff: bool = ...,
34+
) -> Union[str, bytes, None]: ...
35+
36+
def set_contents(
37+
self,
38+
data: AnyStr,
39+
type: PasteboardType = ...,
40+
) -> bool: ...

src/pasteboard/pasteboard.m

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@
294294
PyObject *__##name = pasteboardtype_new(NSPasteboardType##name, read); \
295295
Py_INCREF(__##name); \
296296
if (PyModule_AddObject(module, QUOTE(name), __##name) < 0) { \
297-
return NULL; \
297+
goto except; \
298298
}
299299

300300
PyMODINIT_FUNC
@@ -315,7 +315,7 @@
315315

316316
PyObject *module = PyModule_Create(&pasteboard_module);
317317
if (!module) {
318-
return NULL;
318+
goto except;
319319
}
320320

321321
// PASTEBOARD_TYPE(Color, ???)
@@ -336,8 +336,15 @@
336336
// PASTEBOARD_TYPE(TextFinderOptions, PROP)
337337

338338
Py_INCREF((PyObject *)&PasteboardType);
339-
PyModule_AddObject(module, "Pasteboard", (PyObject *)&PasteboardType);
339+
if (PyModule_AddObject(module, "Pasteboard", (PyObject *)&PasteboardType) < 0) {
340+
goto except;
341+
}
340342

343+
goto finally;
344+
except:
345+
Py_XDECREF(module);
346+
module = NULL;
347+
finally:
341348
return module;
342349
}
343350

src/pasteboard/py.typed

Whitespace-only changes.

tests.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pasteboard
22
import pytest
3+
import mypy.api
34

45
from hypothesis import assume, given, strategies as st
56

@@ -76,3 +77,207 @@ def test_get_set_contents_with_emoji_santa():
7677
)
7778
def test_types_repr(type, name):
7879
assert repr(type) == "<PasteboardType {}>".format(name)
80+
81+
82+
def mypy_run(tmp_path, content):
83+
py = tmp_path / "test.py"
84+
py.write_text(content)
85+
filename = str(py)
86+
normal_report, error_report, exit_status = mypy.api.run([filename, "--strict"])
87+
return normal_report.replace(filename, "test.py"), error_report, exit_status
88+
89+
90+
def test_type_hints_pasteboard_valid(tmp_path):
91+
normal_report, error_report, exit_status = mypy_run(
92+
tmp_path,
93+
"""
94+
from pasteboard import Pasteboard
95+
pb = Pasteboard()
96+
""",
97+
)
98+
assert exit_status == 0, normal_report
99+
100+
101+
def test_type_hints_pasteboard_invalid_args(tmp_path):
102+
normal_report, error_report, exit_status = mypy_run(
103+
tmp_path,
104+
"""
105+
from pasteboard import Pasteboard
106+
pb = Pasteboard("bar")
107+
""",
108+
)
109+
assert exit_status == 1, normal_report
110+
assert 'Too many arguments for "Pasteboard"' in normal_report
111+
112+
113+
def test_type_hints_pasteboard_invalid_kwargs(tmp_path):
114+
normal_report, error_report, exit_status = mypy_run(
115+
tmp_path,
116+
"""
117+
from pasteboard import Pasteboard
118+
pb = Pasteboard(foo="bar")
119+
""",
120+
)
121+
assert exit_status == 1, normal_report
122+
assert 'Unexpected keyword argument "foo" for "Pasteboard"' in normal_report
123+
124+
125+
def test_type_hints_get_contents_valid_no_args(tmp_path):
126+
normal_report, error_report, exit_status = mypy_run(
127+
tmp_path,
128+
"""
129+
from pasteboard import Pasteboard
130+
pb = Pasteboard()
131+
s: str = pb.get_contents()
132+
""",
133+
)
134+
assert exit_status == 0, normal_report
135+
136+
137+
def test_type_hints_get_contents_valid_diff_arg(tmp_path):
138+
normal_report, error_report, exit_status = mypy_run(
139+
tmp_path,
140+
"""
141+
from pasteboard import Pasteboard
142+
pb = Pasteboard()
143+
s = pb.get_contents(diff=True)
144+
if s:
145+
s += "foo"
146+
""",
147+
)
148+
assert exit_status == 0, normal_report
149+
150+
151+
def test_type_hints_get_contents_valid_type_args(tmp_path):
152+
normal_report, error_report, exit_status = mypy_run(
153+
tmp_path,
154+
"""
155+
from pasteboard import Pasteboard, PNG
156+
from typing import Union
157+
pb = Pasteboard()
158+
s = pb.get_contents(type=PNG)
159+
if s:
160+
if isinstance(s, str):
161+
s += "foo"
162+
else:
163+
s += b"foo"
164+
""",
165+
)
166+
assert exit_status == 0, normal_report
167+
168+
169+
def test_type_hints_get_contents_valid_both_args(tmp_path):
170+
normal_report, error_report, exit_status = mypy_run(
171+
tmp_path,
172+
"""
173+
from pasteboard import Pasteboard, PNG
174+
from typing import Union
175+
pb = Pasteboard()
176+
s = pb.get_contents(type=PNG, diff=True)
177+
if s:
178+
if isinstance(s, str):
179+
s += "foo"
180+
else:
181+
s += b"foo"
182+
""",
183+
)
184+
assert exit_status == 0, normal_report
185+
186+
187+
@pytest.mark.parametrize("arg", ['"bar"', 'foo="bar"', 'type="bar"', 'diff="bar"',])
188+
def test_type_hints_get_contents_invalid_arg(arg, tmp_path):
189+
normal_report, error_report, exit_status = mypy_run(
190+
tmp_path,
191+
f"""
192+
from pasteboard import Pasteboard
193+
pb = Pasteboard()
194+
pb.get_contents({arg})
195+
""",
196+
)
197+
assert exit_status == 1, normal_report
198+
assert "No overload variant" in normal_report
199+
200+
201+
@pytest.mark.parametrize("arg", ['"bar"', 'b"bar"',])
202+
def test_type_hints_set_contents_valid_no_args(arg, tmp_path):
203+
normal_report, error_report, exit_status = mypy_run(
204+
tmp_path,
205+
f"""
206+
from pasteboard import Pasteboard
207+
pb = Pasteboard()
208+
result: bool = pb.set_contents({arg})
209+
""",
210+
)
211+
assert exit_status == 0, normal_report
212+
213+
214+
@pytest.mark.parametrize("arg", ['"bar"', 'b"bar"',])
215+
def test_type_hints_set_contents_valid_type_args(arg, tmp_path):
216+
normal_report, error_report, exit_status = mypy_run(
217+
tmp_path,
218+
f"""
219+
from pasteboard import Pasteboard, PNG
220+
pb = Pasteboard()
221+
result: bool = pb.set_contents({arg}, type=PNG)
222+
""",
223+
)
224+
assert exit_status == 0, normal_report
225+
226+
227+
def test_type_hints_set_contents_invalid_arg(tmp_path):
228+
normal_report, error_report, exit_status = mypy_run(
229+
tmp_path,
230+
f"""
231+
from pasteboard import Pasteboard
232+
pb = Pasteboard()
233+
result: bool = pb.set_contents(0)
234+
""",
235+
)
236+
assert exit_status == 1, normal_report
237+
assert '"set_contents" of "Pasteboard" cannot be "int"' in normal_report
238+
239+
240+
def test_type_hints_set_contents_invalid_type_arg(tmp_path):
241+
normal_report, error_report, exit_status = mypy_run(
242+
tmp_path,
243+
f"""
244+
from pasteboard import Pasteboard
245+
pb = Pasteboard()
246+
result: bool = pb.set_contents("", type="bar")
247+
""",
248+
)
249+
assert exit_status == 1, normal_report
250+
msg = 'Argument "type" to "set_contents" of "Pasteboard" has incompatible type "str"; expected "PasteboardType'
251+
assert msg in normal_report
252+
253+
254+
def test_type_hints_set_contents_invalid_kwarg(tmp_path):
255+
normal_report, error_report, exit_status = mypy_run(
256+
tmp_path,
257+
f"""
258+
from pasteboard import Pasteboard
259+
pb = Pasteboard()
260+
result: bool = pb.set_contents("", foo="bar")
261+
""",
262+
)
263+
assert exit_status == 1, normal_report
264+
assert (
265+
'Unexpected keyword argument "foo" for "set_contents" of "Pasteboard"'
266+
in normal_report
267+
)
268+
269+
270+
def test_type_hints_set_contents_invalid_result(tmp_path):
271+
normal_report, error_report, exit_status = mypy_run(
272+
tmp_path,
273+
f"""
274+
from pasteboard import Pasteboard
275+
pb = Pasteboard()
276+
result: str = pb.set_contents("")
277+
""",
278+
)
279+
assert exit_status == 1, normal_report
280+
assert (
281+
'Incompatible types in assignment (expression has type "bool", variable has type "str")'
282+
in normal_report
283+
)

0 commit comments

Comments
 (0)