Skip to content

Commit e2da207

Browse files
committed
hanlde missing CLI plugin extras when loading entry_point
1 parent 4213b34 commit e2da207

File tree

5 files changed

+74
-70
lines changed

5 files changed

+74
-70
lines changed

pyproject.toml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ dependencies = [
3636

3737
[project.optional-dependencies]
3838
mqtt = ["paho-mqtt >=2.1.0"]
39-
influxdb = ["influxdb >=5.2.0"]
39+
influxdb = ["influxdb >=5.2.0", "pytz >=2020 ; python_version < '3.10'"]
4040

4141
[project.urls]
4242
Homepage = "https://avaldebe.github.io/PyPMS"
@@ -83,8 +83,9 @@ test = [
8383
"packaging >=25.0",
8484
"mock_serial",
8585
"logot[loguru]>=1.3.0",
86-
"coverage >=7.8",
86+
{ include-group = "coverage" },
8787
]
88+
coverage = ["coverage >=7.8"]
8889
docs = ["mkdocs>=1.2.3", "mkdocs-material>=9.4", "pymdown-extensions>=9.5"]
8990

9091
[tool.hatch]
@@ -123,6 +124,9 @@ set_env =
123124
!report: COVERAGE_FILE={env:COVERAGE_FILE:.coverage/coverage.{envname}}
124125
commands =
125126
coverage run -m pytest -ra -q
127+
extras =
128+
influxdb
129+
mqtt
126130
dependency_groups =
127131
test
128132
@@ -133,6 +137,8 @@ parallel_show_output = true
133137
commands =
134138
coverage combine --keep
135139
coverage report
140+
dependency_groups =
141+
coverage
136142
depends =
137143
py39, py310, py311, py312, py313
138144
@@ -160,10 +166,19 @@ dependency_groups =
160166

161167
[tool.pytest.ini_options]
162168
minversion = "6.0"
163-
addopts = "--lf -Werror"
169+
addopts = "--ff"
164170
pythonpath = ["src"]
165171
testpaths = ["tests"]
166172
logot_capturer = "logot.loguru.LoguruCapturer"
173+
filterwarnings = [
174+
# DeprecationWarning from pypms are errors
175+
"error::DeprecationWarning:(pms|tests).*:",
176+
# Python3.9 urlib3-1.21.1
177+
"ignore:Using or importing the ABCs:DeprecationWarning:urllib3.*",
178+
# Python3.12 influxdb-5.3.2
179+
"ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:influxdb.*",
180+
]
181+
167182

168183
[tool.coverage.run]
169184
source = ["pms"]

src/pms/cli.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import re
12
import sys
23
from datetime import datetime
34
from enum import Enum
5+
from functools import partial
46
from pathlib import Path
7+
from textwrap import dedent
58
from typing import Annotated, Optional, Union
69

710
if sys.version_info >= (3, 10):
811
from importlib import metadata
912
else:
1013
import importlib_metadata as metadata
1114

15+
1216
import typer
1317
from loguru import logger
1418

@@ -17,13 +21,50 @@
1721

1822
main = typer.Typer(add_completion=False, no_args_is_help=True)
1923

24+
25+
def pluggin_missing_extras(*extras: str, package: str = "pypms"): # pragma: no cover
26+
green = partial(typer.style, fg=typer.colors.GREEN)
27+
red = partial(typer.style, fg=typer.colors.RED)
28+
29+
required = metadata.requires(package)
30+
assert required is not None
31+
regex = re.compile(rf"extra == '({'|'.join(extras)})'")
32+
missing = tuple(
33+
dep for dep, _, markers in (req.partition(";") for req in required) if regex.search(markers)
34+
)
35+
extra = ", ".join(red(extra, bold=True) for extra in extras)
36+
package = green(package, bold=True)
37+
38+
def command(ctx: typer.Context):
39+
sub_cmd = ctx.command_path
40+
dependencies = " ".join(f"'{red(dep, bold=True)}'" for dep in missing)
41+
msg = f"""
42+
{green(sub_cmd, bold=True)} require dependencies which are not installed.
43+
44+
You can install the required dependencies with
45+
{green("python3 -m pip install --upgrade")} {package}[{extra}]
46+
Or, if you installed {package} with {green("pipx")}
47+
{green("pipx inject")} {package} {dependencies}
48+
Or, if you installed {package} with {green("uv tool")}
49+
{green("uv tool install")} {package}[{extra}]
50+
"""
51+
typer.echo(dedent(msg))
52+
53+
command.__doc__ = f"needs {', '.join(missing)}"
54+
return command
55+
56+
2057
"""
2158
Extra cli commands from plugins
2259
2360
additional Typer commands are loaded from plugins (entry points) advertized as `"pypms.extras"`
2461
"""
62+
ep: metadata.EntryPoint
2563
for ep in metadata.entry_points(group="pypms.extras"):
26-
main.command(name=ep.name)(ep.load())
64+
try:
65+
main.command(name=ep.name)(ep.load())
66+
except ModuleNotFoundError: # pragma: no cover
67+
main.command(name=ep.name)(pluggin_missing_extras(*ep.extras))
2768

2869

2970
def version_callback(value: bool):

src/pms/extra/influxdb.py

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,20 @@
22

33
import json
44
from dataclasses import fields
5-
from functools import partial
6-
from textwrap import dedent
75
from typing import Annotated, Protocol
86

97
import typer
8+
from influxdb import InfluxDBClient as Client
109

1110
from pms.core import exit_on_fail
1211

1312

14-
def __missing_influxdb() -> str: # pragma: no cover
15-
green = partial(typer.style, fg=typer.colors.GREEN)
16-
red = partial(typer.style, fg=typer.colors.GREEN)
17-
package = green("pypms", bold=True)
18-
extra = module = red("influxdb", bold=True)
19-
msg = f"""
20-
{green(__name__, bold=True)} provides additional functionality to {package}.
21-
This functionality requires the {module} module, which is not installed.
22-
23-
You can install this additional dependency with
24-
{green("python3 -m pip install --upgrade")} {package}[{extra}]
25-
Or, if you installed {package} with {green("pipx")}
26-
{green("pipx inject")} {package} {module}
27-
Or, if you installed {package} with {green("uv tool")}
28-
{green("uv tool install")} {package}[{extra}]
29-
"""
30-
return dedent(msg)
31-
32-
3313
class PubFunction(Protocol):
3414
def __call__(self, *, time: int, tags: dict[str, str], data: dict[str, float]) -> None: ...
3515

3616

3717
def client_pub(*, host: str, port: int, username: str, password: str, db_name: str) -> PubFunction:
38-
try:
39-
from influxdb import InfluxDBClient as client
40-
except ModuleNotFoundError: # pragma: no cover
41-
typer.echo(__missing_influxdb())
42-
raise typer.Abort()
43-
44-
c = client(host, port, username, password, None)
18+
c = Client(host, port, username, password, None)
4519
if db_name not in {x["name"] for x in c.get_list_database()}:
4620
c.create_database(db_name)
4721
c.switch_database(db_name)

src/pms/extra/mqtt.py

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,20 @@
22

33
from dataclasses import fields
44
from datetime import datetime
5-
from functools import partial
6-
from textwrap import dedent
75
from typing import Annotated, Callable, NamedTuple
86

97
import typer
108
from loguru import logger
9+
from paho.mqtt.client import Client
1110

1211
from pms.core import exit_on_fail
1312
from pms.sensors.base import ObsData
1413

1514

16-
def __missing_mqtt(): # pragma: no cover
17-
green = partial(typer.style, fg=typer.colors.GREEN)
18-
red = partial(typer.style, fg=typer.colors.GREEN)
19-
package = green("pypms", bold=True)
20-
extra = module = red("paho-mqtt", bold=True)
21-
msg = f"""
22-
{green(__name__, bold=True)} provides additional functionality to {package}.
23-
This functionality requires the {module} module, which is not installed.
24-
25-
You can install this additional dependency with
26-
{green("python3 -m pip install --upgrade")} {package}[{extra}]
27-
Or, if you installed {package} with {green("pipx")}
28-
{green("pipx inject")} {package} {module}
29-
Or, if you installed {package} with {green("uv tool")}
30-
{green("uv tool install")} {package}[{extra}]
31-
"""
32-
return dedent(msg)
33-
34-
3515
def client_pub(
3616
*, topic: str, host: str, port: int, username: str, password: str
3717
) -> Callable[[dict[str, int | str]], None]:
38-
try:
39-
from paho.mqtt import client
40-
except ModuleNotFoundError: # pragma: no cover
41-
typer.echo(__missing_mqtt())
42-
raise typer.Abort()
43-
44-
c = client.Client(client_id=topic)
18+
c = Client(client_id=topic)
4519
c.enable_logger(logger) # type:ignore[arg-type]
4620
if username:
4721
c.username_pw_set(username, password)
@@ -50,7 +24,7 @@ def client_pub(
5024
f"{topic}/$online", "true", 1, True
5125
)
5226
c.will_set(f"{topic}/$online", "false", 1, True)
53-
c.connect(host, port, 60)
27+
c.connect(host, port)
5428
c.loop_start()
5529

5630
def pub(data: dict[str, int | str]) -> None:
@@ -106,28 +80,22 @@ def client_sub(
10680
*,
10781
on_sensordata: Callable[[Data], None],
10882
) -> None:
109-
def on_message(client, userdata, msg):
83+
def on_message(client: Client, userdata, msg):
11084
try:
11185
data = Data.decode(msg.topic, msg.payload)
11286
except UserWarning as e:
11387
logger.debug(e)
11488
else:
11589
on_sensordata(data)
11690

117-
try:
118-
from paho.mqtt import client
119-
except ModuleNotFoundError: # pragma: no cover
120-
typer.echo(__missing_mqtt())
121-
raise typer.Abort()
122-
123-
c = client.Client(client_id=topic)
91+
c = Client(client_id=topic)
12492
c.enable_logger(logger) # type:ignore[arg-type]
12593
if username:
12694
c.username_pw_set(username, password)
12795

12896
c.on_connect = lambda client, userdata, flags, rc: client.subscribe(topic)
12997
c.on_message = on_message
130-
c.connect(host, port, 60)
98+
c.connect(host, port)
13199
c.loop_forever()
132100

133101

uv.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)