Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for getting (multiple) FileURLs #10

Merged
merged 2 commits into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,5 @@ dmypy.json
poetry.lock
.DS_Store
README.html

/a.out
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ takes two arguments:

`set_contents` will return `True` if the pasteboard was successfully set; otherwise, `False`. It may also throw [RuntimeError](https://docs.python.org/3/library/exceptions.html#RuntimeError) if `data` can't be converted to an AppKit type.

### Getting file URLs

```pycon
>>> import pasteboard
>>> pb = pasteboard.Pasteboard()
>>> pb.get_file_urls()
('/Users/<user>/Documents/foo.txt', '/Users/<user>/Documents/bar.txt')
```

**Warning** This API is new, and may change in future.

Returns a `Tuple` of strings, or `None`. Also supports the **diff** parameter analogue to `get_contents`.

## Development

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/)).
Expand Down
1 change: 1 addition & 0 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
pasteboard = Extension(
"pasteboard._native",
["src/pasteboard/pasteboard.m"],
extra_compile_args=["-Wall", "-Wextra", "-Wpedantic", "-Werror"],
extra_link_args=["-framework", "AppKit"],
language="objective-c",
)
Expand Down
14 changes: 9 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pasteboard"
version = "0.3.2"
version = "0.3.3"
description = "Pasteboard - Python interface for reading from NSPasteboard (macOS clipboard)"
authors = ["Toby Fleming <[email protected]>"]
license = "MPL-2.0"
Expand All @@ -26,13 +26,17 @@ keywords = ["macOS", "clipboard", "pasteboard"]
build = "build.py"

[tool.poetry.dependencies]
python = "^3.6"
python = ">=3.6,<4.0"

[tool.poetry.dev-dependencies]
black = "^20.8b1"
pytest = "^6.1.1"
hypothesis = "^5.37.4"
mypy = "^0.790"
pytest = "^6.2.0"
hypothesis = "^6.0.0"
mypy = "^0.800"
# This version is the last to support Python 3.6...
ipython = "7.16.1"
# ...and jedi is not properly pinned
jedi = "0.17.2"

[build-system]
requires = ["poetry_core>=1.0.0", "setuptools"]
Expand Down
11 changes: 11 additions & 0 deletions src/pasteboard/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Pasteboard - Python interface for reading from NSPasteboard (macOS clipboard)
# Copyright (C) 2017-2021 Toby Fleming
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
import sys as _sys

assert _sys.platform == "darwin", "pasteboard only works on macOS"

from ._native import *


class PasteboardType:
"""Make type hints not fail on import - don't use this class"""

pass
20 changes: 10 additions & 10 deletions src/pasteboard/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from typing import overload, AnyStr, Optional, Union

# Pasteboard - Python interface for reading from NSPasteboard (macOS clipboard)
# Copyright (C) 2017-2021 Toby Fleming
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
from typing import overload, AnyStr, Optional, Sequence, Union

class PasteboardType: ...


HTML: PasteboardType
PDF: PasteboardType
PNG: PasteboardType
Expand All @@ -12,29 +15,26 @@ 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: ...
def get_file_urls(
self,
diff: bool = ...,
) -> Optional[Sequence[str]]: ...
81 changes: 70 additions & 11 deletions src/pasteboard/pasteboard.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
Pasteboard - Python interface for reading from NSPasteboard (macOS clipboard)
Copyright (C) 2017-2020 Toby Fleming
Copyright (C) 2017-2021 Toby Fleming

This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can
Expand Down Expand Up @@ -157,7 +157,7 @@
}

PyDoc_STRVAR(pasteboard_get_contents__doc__,
"get_contents(type, diff=False) -> str/bytes/None\n\n"
"get_contents(type: PasteboardType = String, diff: bool = False) -> Union[str, bytes, None]\n\n"
"Gets the contents of the pasteboard.\n\n"
"type - The NSPasteboardType to get, see module members. Default is 'String'.\n"
"diff - Only get the contents if it has changed. Otherwise, `None` is returned. "
Expand Down Expand Up @@ -237,13 +237,74 @@
}

PyDoc_STRVAR(pasteboard_set_contents__doc__,
"set_contents(data, type) -> True/False/None\n\n"
"set_contents(data: Union[str, bytes], type: PasteboardType = String) -> bool\n\n"
"Sets the contents of the pasteboard.\n\n"
"data - str or bytes-like object. If type is a string type and bytes is not "
"UTF-8 encoded, the behaviour is undefined.\n"
"type - The NSPasteboardType to get, see module members. Default is 'String'.\n"
"Returns `True` if the operation was successful; otherwise, `False`.");

static PyObject *
pasteboard_get_file_urls(PyObject *self, PyObject *args, PyObject *kwargs)
{
PasteboardState *state = (PasteboardState *)self;
int diff = 0; // FALSE

static char *kwlist[] = {"diff", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|p", kwlist, &diff)) {
return NULL;
}

long long change_count = [state->board changeCount];

if (diff && (change_count == state->change_count)) {
Py_RETURN_NONE;
}

state->change_count = change_count;

// hopefully guard against non-file URLs
NSArray *supportedTypes = @[NSPasteboardTypeFileURL];
NSString *bestType = [state->board availableTypeFromArray:supportedTypes];
if (!bestType) {
Py_RETURN_NONE;
}

NSArray<Class> *classes = @[[NSURL class]];
NSDictionary *options = @{};
NSArray<NSURL*> *files = [state->board readObjectsForClasses:classes options:options];

Py_ssize_t len = (Py_ssize_t)[files count];
PyObject *urls = PyTuple_New(len);
Py_ssize_t pos = 0;
for (NSURL *url in files) {
NSString *str = [url path];
if ([url isFileURL] && str) {
PyTuple_SetItem(urls, pos, PyUnicode_FromString([str UTF8String]));
pos++;
}
}

if (len != pos) {
if (_PyTuple_Resize(&urls, pos) != 0) {
PyErr_SetString(PyExc_RuntimeError, "Internal error: failed to resize tuple");
return NULL;
}
}
return urls;
}

PyDoc_STRVAR(pasteboard_get_file_urls__doc__,
"get_file_urls(diff: bool = False) -> Optional[Sequence[str]]\n\n"
"Gets the contents of the pasteboard as file URLs.\n\n"
"diff - Only get the contents if it has changed. Otherwise, `None` is returned. "
"Can be used for efficiently querying the pasteboard when polling for changes."
"Default is `False`.\n\n"
"Returns a sequence of strings corresponding to the file URL's path. "
"`None` is returned if an error occurred, there is no data of the requested "
"type, or `diff` was set to `True` and the contents has not changed since "
"the last query.");

static PyMethodDef pasteboard_methods[] = {
{
"get_contents",
Expand All @@ -257,6 +318,12 @@
METH_VARARGS | METH_KEYWORDS,
pasteboard_set_contents__doc__,
},
{
"get_file_urls",
(PyCFunction)pasteboard_get_file_urls,
METH_VARARGS | METH_KEYWORDS,
pasteboard_get_file_urls__doc__,
},
{NULL, NULL, 0, NULL} /* Sentinel */
};

Expand Down Expand Up @@ -318,22 +385,14 @@
goto except;
}

// PASTEBOARD_TYPE(Color, ???)
// PASTEBOARD_TYPE(FindPanelSearchOptions, PROP)
// PASTEBOARD_TYPE(Font, ???)
PASTEBOARD_TYPE(HTML, STRING)
// PASTEBOARD_TYPE(MultipleTextSelection, ???)
PASTEBOARD_TYPE(PDF, DATA)
PASTEBOARD_TYPE(PNG, DATA)
PASTEBOARD_TYPE(RTF, STRING)
// PASTEBOARD_TYPE(RTFD, STRING)
// PASTEBOARD_TYPE(Ruler, ???)
// PASTEBOARD_TYPE(Sound, ???)
PASTEBOARD_TYPE(String, STRING)
PasteboardType_Default = __String;
PASTEBOARD_TYPE(TIFF, DATA)
PASTEBOARD_TYPE(TabularText, STRING)
// PASTEBOARD_TYPE(TextFinderOptions, PROP)

Py_INCREF((PyObject *)&PasteboardType);
if (PyModule_AddObject(module, "Pasteboard", (PyObject *)&PasteboardType) < 0) {
Expand Down
14 changes: 14 additions & 0 deletions test_fileurl.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#include <AppKit/AppKit.h>

int main() {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSPasteboard *pb = [NSPasteboard generalPasteboard];
[pb clearContents];
NSArray *objects = @[
[NSURL fileURLWithPath:@"/bin/ls"],
[NSURL URLWithString:@"https://developer.apple.com/"]
];
[pb writeObjects:objects];
[pool drain];
return 0;
}
Loading