Skip to content

Commit

Permalink
Type hints (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
tobywf authored Feb 21, 2020
1 parent 49ecae8 commit 8b010f0
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 4 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

[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.

Now with type hints!

## Installation

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

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/)).

This project uses [pre-commit](https://pre-commit.com/) to run some linting hooks when committing. When you first clone the repo, please run:

```
pre-commit install
```

You may also run the hooks at any time:

```
pre-commit run --all-files
```

Dependencies are managed via [poetry](https://python-poetry.org/). To install all dependencies, use:

```
poetry install
```

This will also install development dependencies (`pytest`). To run the tests:

```
poetry run pytest tests.py --verbose
```

## License

From version 0.3.0 and forwards, this library is licensed under the Mozilla Public License Version 2.0. For more information, see `LICENSE`.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pasteboard"
version = "0.3.0"
version = "0.3.1"
description = "Pasteboard - Python interface for reading from NSPasteboard (macOS clipboard)"
authors = ["Toby Fleming <[email protected]>"]
license = "MPL-2.0"
Expand Down Expand Up @@ -31,6 +31,7 @@ python = "^3.6"
black = "^19.10b0"
pytest = "^5.3.5"
hypothesis = "^5.5.4"
mypy = "^0.761"

[build-system]
requires = ["poetry>=1.0.0"]
40 changes: 40 additions & 0 deletions src/pasteboard/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import overload, AnyStr, Optional, Union


class PasteboardType: ...


HTML: PasteboardType
PDF: PasteboardType
PNG: PasteboardType
RTF: PasteboardType
String: PasteboardType
TIFF: PasteboardType
TabularText: PasteboardType


class Pasteboard:
@classmethod
def __init__(self) -> None: ...

@overload
def get_contents(self) -> str: ...

@overload
def get_contents(
self,
diff: bool = ...,
) -> Optional[str]: ...

@overload
def get_contents(
self,
type: PasteboardType = ...,
diff: bool = ...,
) -> Union[str, bytes, None]: ...

def set_contents(
self,
data: AnyStr,
type: PasteboardType = ...,
) -> bool: ...
13 changes: 10 additions & 3 deletions src/pasteboard/pasteboard.m
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@
PyObject *__##name = pasteboardtype_new(NSPasteboardType##name, read); \
Py_INCREF(__##name); \
if (PyModule_AddObject(module, QUOTE(name), __##name) < 0) { \
return NULL; \
goto except; \
}

PyMODINIT_FUNC
Expand All @@ -315,7 +315,7 @@

PyObject *module = PyModule_Create(&pasteboard_module);
if (!module) {
return NULL;
goto except;
}

// PASTEBOARD_TYPE(Color, ???)
Expand All @@ -336,8 +336,15 @@
// PASTEBOARD_TYPE(TextFinderOptions, PROP)

Py_INCREF((PyObject *)&PasteboardType);
PyModule_AddObject(module, "Pasteboard", (PyObject *)&PasteboardType);
if (PyModule_AddObject(module, "Pasteboard", (PyObject *)&PasteboardType) < 0) {
goto except;
}

goto finally;
except:
Py_XDECREF(module);
module = NULL;
finally:
return module;
}

Expand Down
Empty file added src/pasteboard/py.typed
Empty file.
205 changes: 205 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pasteboard
import pytest
import mypy.api

from hypothesis import assume, given, strategies as st

Expand Down Expand Up @@ -76,3 +77,207 @@ def test_get_set_contents_with_emoji_santa():
)
def test_types_repr(type, name):
assert repr(type) == "<PasteboardType {}>".format(name)


def mypy_run(tmp_path, content):
py = tmp_path / "test.py"
py.write_text(content)
filename = str(py)
normal_report, error_report, exit_status = mypy.api.run([filename, "--strict"])
return normal_report.replace(filename, "test.py"), error_report, exit_status


def test_type_hints_pasteboard_valid(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
"""
from pasteboard import Pasteboard
pb = Pasteboard()
""",
)
assert exit_status == 0, normal_report


def test_type_hints_pasteboard_invalid_args(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
"""
from pasteboard import Pasteboard
pb = Pasteboard("bar")
""",
)
assert exit_status == 1, normal_report
assert 'Too many arguments for "Pasteboard"' in normal_report


def test_type_hints_pasteboard_invalid_kwargs(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
"""
from pasteboard import Pasteboard
pb = Pasteboard(foo="bar")
""",
)
assert exit_status == 1, normal_report
assert 'Unexpected keyword argument "foo" for "Pasteboard"' in normal_report


def test_type_hints_get_contents_valid_no_args(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
"""
from pasteboard import Pasteboard
pb = Pasteboard()
s: str = pb.get_contents()
""",
)
assert exit_status == 0, normal_report


def test_type_hints_get_contents_valid_diff_arg(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
"""
from pasteboard import Pasteboard
pb = Pasteboard()
s = pb.get_contents(diff=True)
if s:
s += "foo"
""",
)
assert exit_status == 0, normal_report


def test_type_hints_get_contents_valid_type_args(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
"""
from pasteboard import Pasteboard, PNG
from typing import Union
pb = Pasteboard()
s = pb.get_contents(type=PNG)
if s:
if isinstance(s, str):
s += "foo"
else:
s += b"foo"
""",
)
assert exit_status == 0, normal_report


def test_type_hints_get_contents_valid_both_args(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
"""
from pasteboard import Pasteboard, PNG
from typing import Union
pb = Pasteboard()
s = pb.get_contents(type=PNG, diff=True)
if s:
if isinstance(s, str):
s += "foo"
else:
s += b"foo"
""",
)
assert exit_status == 0, normal_report


@pytest.mark.parametrize("arg", ['"bar"', 'foo="bar"', 'type="bar"', 'diff="bar"',])
def test_type_hints_get_contents_invalid_arg(arg, tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
f"""
from pasteboard import Pasteboard
pb = Pasteboard()
pb.get_contents({arg})
""",
)
assert exit_status == 1, normal_report
assert "No overload variant" in normal_report


@pytest.mark.parametrize("arg", ['"bar"', 'b"bar"',])
def test_type_hints_set_contents_valid_no_args(arg, tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
f"""
from pasteboard import Pasteboard
pb = Pasteboard()
result: bool = pb.set_contents({arg})
""",
)
assert exit_status == 0, normal_report


@pytest.mark.parametrize("arg", ['"bar"', 'b"bar"',])
def test_type_hints_set_contents_valid_type_args(arg, tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
f"""
from pasteboard import Pasteboard, PNG
pb = Pasteboard()
result: bool = pb.set_contents({arg}, type=PNG)
""",
)
assert exit_status == 0, normal_report


def test_type_hints_set_contents_invalid_arg(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
f"""
from pasteboard import Pasteboard
pb = Pasteboard()
result: bool = pb.set_contents(0)
""",
)
assert exit_status == 1, normal_report
assert '"set_contents" of "Pasteboard" cannot be "int"' in normal_report


def test_type_hints_set_contents_invalid_type_arg(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
f"""
from pasteboard import Pasteboard
pb = Pasteboard()
result: bool = pb.set_contents("", type="bar")
""",
)
assert exit_status == 1, normal_report
msg = 'Argument "type" to "set_contents" of "Pasteboard" has incompatible type "str"; expected "PasteboardType'
assert msg in normal_report


def test_type_hints_set_contents_invalid_kwarg(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
f"""
from pasteboard import Pasteboard
pb = Pasteboard()
result: bool = pb.set_contents("", foo="bar")
""",
)
assert exit_status == 1, normal_report
assert (
'Unexpected keyword argument "foo" for "set_contents" of "Pasteboard"'
in normal_report
)


def test_type_hints_set_contents_invalid_result(tmp_path):
normal_report, error_report, exit_status = mypy_run(
tmp_path,
f"""
from pasteboard import Pasteboard
pb = Pasteboard()
result: str = pb.set_contents("")
""",
)
assert exit_status == 1, normal_report
assert (
'Incompatible types in assignment (expression has type "bool", variable has type "str")'
in normal_report
)

0 comments on commit 8b010f0

Please sign in to comment.