Skip to content

Commit 7465986

Browse files
authored
Merge pull request #3 from disguise-one/package_python_plugin
Package python plugin
2 parents 2bd957d + 432f2b3 commit 7465986

File tree

9 files changed

+576
-44
lines changed

9 files changed

+576
-44
lines changed

.github/workflows/ci.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.11"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Install uv
20+
uses: astral-sh/setup-uv@v5
21+
with:
22+
enable-cache: true
23+
24+
- name: Set up Python ${{ matrix.python-version }}
25+
run: uv python install ${{ matrix.python-version }}
26+
27+
- name: Install dependencies
28+
run: uv sync --dev
29+
30+
- name: Run ruff check
31+
run: uv run ruff check .
32+
33+
- name: Run ruff format check
34+
run: uv run ruff format --check .
35+
36+
- name: Run mypy
37+
run: uv run mypy
38+
39+
- name: Run pytest
40+
run: uv run pytest

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.14.6
4+
hooks:
5+
- id: ruff
6+
args: [--fix]
7+
- id: ruff-format

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ To install the plugin, use pip:
1010
pip install git+https://github.com/disguise-one/python-plugin
1111
```
1212

13-
## Usage
13+
## Publish Plugin
1414

1515
The `DesignerPlugin` class allows you to publish a plugin for the Disguise Designer application. The `port` parameter corresponds to an HTTP server that serves the plugin's web user interface. Below is an example of how to use it (without a server, for clarity).
1616

@@ -51,7 +51,7 @@ async def main():
5151
asyncio.run(main())
5252
```
5353

54-
## Plugin options
54+
### Publish options
5555

5656
If you would prefer not to use the `d3plugin.json` file, construct the `DesignerPlugin` object directly. The plugin's name and port number are required parameters. Optionally, the plugin can specify `hostname`, which can be used to direct Designer to a specific hostname when opening the plugin's web UI, and other metadata parameters are available, also.
5757

pyproject.toml

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,99 @@ name = "disguise-designer-plugin"
77
version = "1.1.0"
88
description = "A plugin for the Disguise Designer application."
99
authors = [
10-
{ name = "Tom Whittock", email = "tom.whittock@disguise.one" }
10+
{ name = "Tom Whittock", email = "tom.whittock@disguise.one" },
11+
{ name = "Taegyun Ha", email = "taegyun.ha@disguise.one" }
1112
]
1213
dependencies = [
13-
"zeroconf>=0.39.0"
14+
"zeroconf>=0.39.0",
1415
]
1516
requires-python = ">=3.11"
1617
classifiers = [
1718
"Programming Language :: Python :: 3",
1819
"License :: OSI Approved :: MIT License",
1920
"Operating System :: OS Independent"
2021
]
22+
readme = "README.md"
2123

2224
[tool.setuptools.packages.find]
2325
where = ["src"]
2426

2527
[project.urls]
2628
Homepage = "https://github.com/disguise-one/python-plugin"
2729
Issues = "https://github.com/disguise-one/python-plugin/issues"
30+
31+
# Package development
32+
[dependency-groups]
33+
dev = [
34+
"mypy>=1.18.2",
35+
"pre-commit>=4.4.0",
36+
"pytest>=9.0.1",
37+
"ruff>=0.14.5",
38+
]
39+
40+
[tool.ruff]
41+
line-length = 100
42+
target-version = "py311"
43+
exclude = [
44+
"test",
45+
".venv",
46+
".devcontainer",
47+
]
48+
49+
[tool.ruff.lint]
50+
select = [
51+
"E", # pycodestyle errors
52+
"W", # pycodestyle warnings
53+
"F", # pyflakes
54+
"I", # isort
55+
"B", # flake8-bugbear
56+
"C4", # flake8-comprehensions
57+
"UP", # pyupgrade
58+
]
59+
ignore = [
60+
"E501", # line too long (handled by formatter)
61+
]
62+
63+
[tool.ruff.format]
64+
quote-style = "double"
65+
indent-style = "space"
66+
67+
[tool.mypy]
68+
python_version = "3.11"
69+
mypy_path = "src"
70+
files = ["src"]
71+
exclude = [
72+
".venv/",
73+
".devcontainer/",
74+
]
75+
warn_return_any = true
76+
warn_unused_configs = true
77+
disallow_untyped_defs = true
78+
disallow_incomplete_defs = true
79+
check_untyped_defs = true
80+
disallow_untyped_calls = true
81+
disallow_untyped_decorators = false
82+
warn_redundant_casts = true
83+
warn_unused_ignores = true
84+
warn_no_return = true
85+
warn_unreachable = true
86+
strict_equality = true
87+
strict_optional = true
88+
no_implicit_optional = true
89+
show_error_codes = true
90+
show_column_numbers = true
91+
92+
[[tool.mypy.overrides]]
93+
module = "zeroconf.*"
94+
ignore_missing_imports = true
95+
96+
[tool.pytest.ini_options]
97+
testpaths = ["test"]
98+
python_files = ["test_*.py"]
99+
python_classes = ["Test*"]
100+
python_functions = ["test_*"]
101+
addopts = [
102+
"-v",
103+
"--strict-markers",
104+
"--strict-config",
105+
]

src/designer_plugin/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
from .designer_plugin import DesignerPlugin
2+
3+
__all__ = ["DesignerPlugin"]
Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1+
import asyncio
2+
import socket
3+
from json import load as json_load
4+
15
from zeroconf import ServiceInfo, Zeroconf
26
from zeroconf.asyncio import AsyncZeroconf
3-
import asyncio, socket
4-
from typing import Dict, Optional
5-
from json import load as json_load
7+
68

79
class DesignerPlugin:
810
"""When used as a context manager (using the `with` statement), publish a plugin using DNS-SD for the Disguise Designer application"""
911

10-
def __init__(self,
11-
name: str,
12-
port: int,
13-
hostname: Optional[str] = None,
14-
url: Optional[str] = None,
15-
requires_session: bool = False,
16-
is_disguise: bool = False):
12+
def __init__(
13+
self,
14+
name: str,
15+
port: int,
16+
hostname: str | None = None,
17+
url: str | None = None,
18+
requires_session: bool = False,
19+
is_disguise: bool = False,
20+
):
1721
self.name = name
1822
self.port = port
1923
self.hostname = hostname or socket.gethostname()
@@ -22,36 +26,37 @@ def __init__(self,
2226
self.requires_session = requires_session
2327
self.is_disguise = is_disguise
2428

29+
self._zeroconf: Zeroconf | None = None
30+
self._azeroconf: AsyncZeroconf | None = None
31+
2532
@staticmethod
26-
def default_init(port: int, hostname: Optional[str] = None):
33+
def default_init(port: int, hostname: str | None = None) -> "DesignerPlugin":
2734
"""Initialize the plugin options with the values in d3plugin.json."""
2835
return DesignerPlugin.from_json_file(
29-
file_path="./d3plugin.json",
30-
port=port,
31-
hostname=hostname
36+
file_path="./d3plugin.json", port=port, hostname=hostname
3237
)
3338

3439
@staticmethod
35-
def from_json_file(file_path, port: int, hostname: Optional[str] = None):
40+
def from_json_file(file_path: str, port: int, hostname: str | None = None) -> "DesignerPlugin":
3641
"""Convert a JSON file (expected d3plugin.json) to PluginOptions. hostname and port are required."""
37-
with open(file_path, 'r') as f:
42+
with open(file_path) as f:
3843
options = json_load(f)
3944
return DesignerPlugin(
40-
name=options['name'],
45+
name=options["name"],
4146
port=port,
4247
hostname=hostname,
43-
url=options.get('url', None),
44-
requires_session=options.get('requiresSession', False),
45-
is_disguise=options.get('isDisguise', False)
48+
url=options.get("url", None),
49+
requires_session=options.get("requiresSession", False),
50+
is_disguise=options.get("isDisguise", False),
4651
)
47-
52+
4853
@property
49-
def service_info(self):
54+
def service_info(self) -> ServiceInfo:
5055
"""Convert the options to a dictionary suitable for DNS-SD service properties."""
51-
properties={
52-
b"t": b'web',
53-
b"s": b'true' if self.requires_session else b'false',
54-
b"d": b'true' if self.is_disguise else b'false',
56+
properties = {
57+
b"t": b"web",
58+
b"s": b"true" if self.requires_session else b"false",
59+
b"d": b"true" if self.is_disguise else b"false",
5560
}
5661
if self.custom_url:
5762
properties[b"u"] = self.custom_url.encode()
@@ -61,21 +66,25 @@ def service_info(self):
6166
name=f"{self.name}._d3plugin._tcp.local.",
6267
port=self.port,
6368
properties=properties,
64-
server=f"{self.hostname}.local."
69+
server=f"{self.hostname}.local.",
6570
)
6671

67-
def __enter__(self):
68-
self.zeroconf = Zeroconf()
69-
self.zeroconf.register_service(self.service_info)
72+
def __enter__(self) -> "DesignerPlugin":
73+
self._zeroconf = Zeroconf()
74+
self._zeroconf.register_service(self.service_info)
7075
return self
7176

72-
def __exit__(self, exc_type, exc_value, traceback):
73-
self.zeroconf.close()
77+
def __exit__(self, exc_type, exc_value, traceback): # type: ignore
78+
if self._zeroconf:
79+
self._zeroconf.close()
80+
self._zeroconf = None
7481

75-
async def __aenter__(self):
76-
self.zeroconf = AsyncZeroconf()
77-
asyncio.create_task(self.zeroconf.async_register_service(self.service_info))
82+
async def __aenter__(self) -> "DesignerPlugin":
83+
self._azeroconf = AsyncZeroconf()
84+
asyncio.create_task(self._azeroconf.async_register_service(self.service_info))
7885
return self
7986

80-
async def __aexit__(self, exc_type, exc_value, traceback):
81-
await self.zeroconf.async_close()
87+
async def __aexit__(self, exc_type, exc_value, traceback): # type: ignore
88+
if self._azeroconf:
89+
await self._azeroconf.async_close()
90+
self._azeroconf = None

test/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
23
sys.path.append("./src")
34

45
from designer_plugin import DesignerPlugin

test/test_plugin.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
from unittest.mock import patch, mock_open
1+
from json import JSONDecodeError
2+
from json import dumps as json_dumps
23
from unittest import TestCase
3-
from json import dumps as json_dumps, JSONDecodeError
4+
from unittest.mock import mock_open, patch
5+
46
from . import DesignerPlugin
57

8+
69
def _escaped(name):
710
"""Escape the name for Zeroconf."""
811
invalid_removed = ''.join(c if 0x20 <= ord(c) <= 0x7E else '\\%02X' % ord(c) for c in name)
@@ -76,7 +79,7 @@ def test_name_override(self):
7679
_zeroconf().register_service.assert_called_once()
7780
service_info = _zeroconf(
7881
).register_service.mock_calls[0].args[0]
79-
self.assertEqual(service_info.name, f"Different Name._d3plugin._tcp.local.")
82+
self.assertEqual(service_info.name, "Different Name._d3plugin._tcp.local.")
8083
self.assertEqual(service_info.type, "_d3plugin._tcp.local.")
8184
self.assertEqual(service_info.port, 9999)
8285
self.assertEqual(service_info.server, f"{plugin.hostname}.local.")

0 commit comments

Comments
 (0)