Skip to content

Commit

Permalink
Merge pull request #33 from AceofSpades5757/lxml
Browse files Browse the repository at this point in the history
Fix type hints, lints, and update docstrings
  • Loading branch information
AceofSpades5757 committed May 1, 2024
2 parents 703ca0c + d725b4d commit ad002cc
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 56 deletions.
2 changes: 2 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[MESSAGES CONTROL]
disable=redefined-builtin,invalid-name
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

[![PyPI](https://img.shields.io/pypi/v/clip-util?color=darkred)](https://pypi.org/project/clip-util/)
[![Read the Docs](https://img.shields.io/readthedocs/clip-util)](https://clip-util.readthedocs.io/en/latest/)
[![Tests](https://github.com/AceofSpades5757/clip-util/actions/workflows/test.yml/badge.svg)](https://github.com/AceofSpades5757/clip-util/actions/workflows/test.yml)

[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/clip-util?label=Python%20Version&logo=python&logoColor=yellow)](https://pypi.org/project/clip-util/)
[![PyPI - License](https://img.shields.io/pypi/l/clip-util?color=green)](https://github.com/AceofSpades5757/clip-util/blob/main/LICENSE)

[![Tests](https://github.com/AceofSpades5757/clip-util/actions/workflows/test.yml/badge.svg)](https://github.com/AceofSpades5757/clip-util/actions/workflows/test.yml)

[![Read the Docs](https://img.shields.io/readthedocs/clip-util)](https://clip-util.readthedocs.io/en/latest/)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

# Description

Expand Down
2 changes: 2 additions & 0 deletions src/clipboard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from clipboard.errors import EmptyClipboardError
from clipboard.errors import FormatNotSupportedError
from clipboard.errors import GetClipboardError
from clipboard.errors import GetFormatsError
from clipboard.errors import LockError
from clipboard.errors import OpenClipboardError
from clipboard.errors import SetClipboardError
Expand All @@ -29,6 +30,7 @@
"EmptyClipboardError",
"FormatNotSupportedError",
"GetClipboardError",
"GetFormatsError",
"LockError",
"OpenClipboardError",
"SetClipboardError",
Expand Down
112 changes: 80 additions & 32 deletions src/clipboard/clipboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@
TODO: Make `get_clipboard`'s default to check available formats instead of just
using the default format.
FIXME: Copying using normal methods and then using `get_clipboard` doesn't work
properly. It gives a bunch of null bytes as a string.
FIXME: HTML_Format doesn't work for some reason. Fix this.
FIXME: Typing appears to be off.
* There are a lot of parameters with a default of None that are not Optional.
* There are some parameters typed as `int`, but they look like they should
be `Optional[HANDLE]` instead.
"""

import ctypes
import logging
import os
import time
import traceback
from typing import List
from typing import Optional
from typing import Union
Expand All @@ -35,13 +31,15 @@
from clipboard.errors import EmptyClipboardError
from clipboard.errors import FormatNotSupportedError
from clipboard.errors import GetClipboardError
from clipboard.errors import GetFormatsError
from clipboard.errors import LockError
from clipboard.errors import OpenClipboardError
from clipboard.errors import SetClipboardError
from clipboard.formats import ClipboardFormat
from clipboard.html_clipboard import HTMLTemplate


ClipboardFormatType = Union[int, str, ClipboardFormat] # Type Alias
hMem = HANDLE # Type Alias
GMEM_MOVEABLE = 0x0002
GMEM_ZEROINIT = 0x0040
Expand All @@ -64,7 +62,7 @@


def get_clipboard(
format: Optional[Union[int, ClipboardFormat]] = None
format: Optional[ClipboardFormatType] = None,
) -> Optional[Union[str, bytes]]:
"""Conveniency wrapper to get clipboard.
Expand All @@ -77,26 +75,47 @@ def get_clipboard(
format = available[0]
with Clipboard() as cb:
return cb.get_clipboard(format=format)
return None


def set_clipboard(
content: str, format: Union[int, ClipboardFormat] = None
) -> None:
"""Conveniency wrapper to set clipboard."""
content: Union[str, bytes],
format: Optional[ClipboardFormatType] = None,
) -> HANDLE:
"""Conveniency wrapper to set clipboard.
Raises
------
SetClipboardError
If setting the clipboard failed.
"""
with Clipboard() as cb:
return cb.set_clipboard(content=content, format=format)
raise SetClipboardError("Setting the clipboard failed.")


def get_available_formats() -> list[int]:
"""Conveniency wrapper to get available formats."""
"""Conveniency wrapper to get available formats.
Raises
------
OpenClipboardError
If the context manager caught an error.
"""
with Clipboard() as cb:
return cb.available_formats()
raise GetFormatsError("Failed to get available formats.")


class Clipboard:
"""Represents the system clipboard."""

default_format: ClipboardFormat = ClipboardFormat.CF_UNICODETEXT

def __init__(self, format: Union[ClipboardFormat, str, int] = None):
def __init__(
self,
format: Optional[ClipboardFormatType] = None,
):
if format is None:
format = self.default_format.value
else:
Expand Down Expand Up @@ -133,10 +152,19 @@ def get_formats() -> List[int]:
return available_formats

def get_clipboard(
self, format: Union[int, ClipboardFormat] = None
self,
format: Optional[ClipboardFormatType] = None,
) -> Optional[Union[str, bytes]]:
"""Get data from clipboard, returning None if nothing is on it.
**Requires valid HTML content to work as intended.**
Parameters
----------
format : Optional[ClipboardFormatType]
The format of the clipboard data.
If None, the default format is used.
Raises
------
FormatNotSupportedError
Expand All @@ -161,11 +189,12 @@ def get_clipboard(
formats = self.available_formats()
if format not in formats:
raise FormatNotSupportedError(
f"{format} is not supported for getting the clipboard. Choose from following {formats}"
f"{format} is not supported for getting the clipboard."
f" Choose from following {formats}"
)

# Info
self.h_clip_mem: HANDLE = GetClipboardData(format)
self.h_clip_mem = GetClipboardData(format)
if self.h_clip_mem is None:
raise GetClipboardError("The `GetClipboardData` function failed.")
self.address = self._lock(self.h_clip_mem) # type: ignore
Expand All @@ -178,9 +207,10 @@ def get_clipboard(
# A handle to the locale identifier associated with the text in the
# clipboard.
# TODO: There are other types that could be supported as well, such as
# audio data: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
# audio data:
# https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
string: ctypes.Array[ctypes.c_byte]
content: str
content: Union[str, bytes]
if format == ClipboardFormat.CF_UNICODETEXT.value:
string = (ctypes.c_byte * self.size).from_address(
int(self.address) # type: ignore
Expand Down Expand Up @@ -213,7 +243,9 @@ def get_clipboard(
return content

def set_clipboard(
self, content: str, format: Union[int, ClipboardFormat] = None
self,
content: Union[str, bytes],
format: Optional[ClipboardFormatType] = None,
) -> HANDLE:
"""Set clipboard.
Expand All @@ -229,7 +261,9 @@ def set_clipboard(
return set_handle

def _set_clipboard(
self, content: str, format: Union[int, ClipboardFormat] = None
self,
content: Union[str, bytes],
format: Optional[ClipboardFormatType] = None,
) -> HANDLE:
"""Hides the HANDLE.
Expand Down Expand Up @@ -264,7 +298,10 @@ def _set_clipboard(
content_bytes: bytes
contents_ptr: LPVOID
if format == ClipboardFormat.CF_UNICODETEXT.value:
content_bytes = content.encode(encoding="utf-16le")
if isinstance(content, str):
content_bytes = content.encode(encoding=UTF_ENCODING)
else:
content_bytes = content

alloc_handle = GlobalAlloc(
GMEM_MOVEABLE | GMEM_ZEROINIT, len(content_bytes) + 2
Expand All @@ -279,7 +316,12 @@ def _set_clipboard(
format == ClipboardFormat.CF_HTML.value
or format == ClipboardFormat.HTML_Format.value
):
template: HTMLTemplate = HTMLTemplate(content)
content_str: str # utf-8
if isinstance(content, bytes):
content_str = content.decode(encoding=HTML_ENCODING)
else:
content_str = content # type: ignore
template: HTMLTemplate = HTMLTemplate(content_str)
html_content_bytes: bytes = template.generate().encode(
encoding=HTML_ENCODING
)
Expand All @@ -295,7 +337,11 @@ def _set_clipboard(

set_handle = SetClipboardData(format, alloc_handle)
else:
content_bytes = content.encode(encoding="utf-8")
if isinstance(content, str):
# Most general content is going to be utf-8.
content_bytes = content.encode(encoding="utf-8")
else:
content_bytes = content

alloc_handle = GlobalAlloc(GMEM_MOVEABLE, len(content_bytes) + 1)
contents_ptr = GlobalLock(alloc_handle)
Expand All @@ -309,7 +355,7 @@ def _set_clipboard(

return set_handle

def _resolve_format(self, format: Union[ClipboardFormat, str, int]) -> int:
def _resolve_format(self, format: ClipboardFormatType) -> int:
"""Given an integer, respresenting a clipboard format, or a
ClipboardFormat object, return the respective integer.
Expand All @@ -328,19 +374,19 @@ def _resolve_format(self, format: Union[ClipboardFormat, str, int]) -> int:
elif isinstance(format, str):
try:
format = ClipboardFormat[format].value
except KeyError:
except KeyError as exc:
formats = self.available_formats()
raise FormatNotSupportedError(
f"{format} is not a supported clipboard format."
f" Choose from following {formats}"
)
) from exc

# FIXME: There are issues with HTML_Format, so use CF_HTML
if format == ClipboardFormat.HTML_Format.value:
format = ClipboardFormat.CF_HTML.value
return format # type: ignore

def __getitem__(self, format: Union[int, ClipboardFormat] = None):
def __getitem__(self, format: ClipboardFormatType):
"""Get data from clipboard, returning None if nothing is on it.
Raises
Expand Down Expand Up @@ -389,28 +435,28 @@ def __enter__(self):
max_tries = 3
tries = 0
while not self.opened and tries < max_tries:
if tries > 0:
time.sleep(0.01)
tries += 1
if self._open():
return self
self._close()
else:
raise OpenClipboardError("Failed to open clipboard.")

raise OpenClipboardError("Failed to open clipboard.")

def __exit__(
self, exception_type, exception_value, exception_traceback
) -> bool:
logger.info("Exiting context manager")
if exception_type is not None:
import traceback

traceback.print_exception(
exception_type, exception_value, exception_traceback
)

self._close()
return True

def _open(self, handle: int = None) -> bool:
def _open(self, handle: Optional[HANDLE] = None) -> bool:
logger.info("_Opening clipboard")
opened: bool = bool(OpenClipboard(handle))
self.opened = opened
Expand Down Expand Up @@ -443,7 +489,7 @@ def _lock(self, handle: HANDLE) -> LPVOID:

return locked

def _unlock(self, handle: HANDLE = None) -> bool:
def _unlock(self, handle: Optional[HANDLE] = None) -> bool:
"""Unlock clipboard.
Raises
Expand Down Expand Up @@ -478,3 +524,5 @@ def _empty(self) -> bool:
return bool(EmptyClipboard())
else:
raise EmptyClipboardError("Emptying the clipboard failed.")

return False
2 changes: 2 additions & 0 deletions src/clipboard/constants.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
"""Constants"""

UTF_ENCODING: str = "UTF-16"
HTML_ENCODING: str = "UTF-8"
4 changes: 4 additions & 0 deletions src/clipboard/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ class ClipboardError(Exception):

class LockError(Exception):
"""Exception raised when locking the clipboard fails."""


class GetFormatsError(Exception):
"""Exception raised when getting clipboard formats fails."""
Loading

0 comments on commit ad002cc

Please sign in to comment.