Skip to content

Commit baf199e

Browse files
Make non-core action deps optional via lazy imports (#556)
* Make non-core action deps optional via lazy imports Move docker, requests, pexpect, python-gnupg, Flask, werkzeug, pylev, click, and pyyaml to optional extras in pyproject.toml. The core action now only requires PyGithub, Jinja2, semver, and toml. In repo.py, convert top-level imports of docker, requests, pexpect, and gnupg to lazy imports at their call sites so the action runs without them installed. This reduces the mandatory dependency footprint for the GitHub Action while preserving full functionality when extras are installed (e.g. in Docker builds via --extras all). * Fix Dockerfile: use --all-extras, update lock file, fix lint warnings * CI: install all extras for tests and mypy * Fix GPG test mock path for lazy import GPG is now imported locally inside configure_gpg(), so patch gnupg.GPG directly instead of tagbot.action.repo.GPG. * Docs: update install instructions for optional extras DEVGUIDE.md and README.md now reflect that non-core deps are optional and require --all-extras / .[all] to install.
1 parent 1f5c4e5 commit baf199e

8 files changed

Lines changed: 91 additions & 47 deletions

File tree

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
with:
1616
python-version: 3.12
1717
- run: pip install poetry
18-
- run: poetry install
18+
- run: poetry install --all-extras
1919
- run: poetry run make test
2020
docker:
2121
runs-on: ubuntu-latest
@@ -28,7 +28,7 @@ jobs:
2828
- name: Install dependencies
2929
run: |
3030
docker run --name tagbot-deps --mount type=bind,source=$(pwd),target=/repo tagbot:test sh -c '
31-
pip install poetry && cd /repo && poetry install'
31+
pip install poetry && cd /repo && poetry install --all-extras'
3232
docker commit tagbot-deps tagbot:ready
3333
docker rm tagbot-deps
3434
- name: Run pytest

DEVGUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ Or view in [AWS Console](https://console.aws.amazon.com/cloudwatch/home?region=u
194194
```bash
195195
# Setup
196196
python3 -m venv .venv && source .venv/bin/activate
197-
pip install . && pip install pytest pytest-cov black flake8 mypy boto3
197+
pip install '.[all]' && pip install pytest pytest-cov black flake8 mypy boto3
198198

199199
# Run all checks
200200
make test

Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
FROM python:3.14-slim as builder
1+
FROM python:3.14-slim AS builder
22

33
RUN pip install --no-cache-dir poetry poetry-plugin-export
44

55
COPY pyproject.toml .
66
COPY poetry.lock .
7-
RUN poetry export --format requirements.txt --output /root/requirements.txt
7+
RUN poetry export --all-extras --format requirements.txt --output /root/requirements.txt
88

99
FROM python:3.14-slim
10-
LABEL org.opencontainers.image.source https://github.com/JuliaRegistries/TagBot
10+
LABEL org.opencontainers.image.source="https://github.com/JuliaRegistries/TagBot"
1111
RUN apt-get update && apt-get install -y git gnupg make openssh-client
1212
COPY --from=builder /root/requirements.txt /root/requirements.txt
1313
RUN pip install --no-cache-dir --requirement /root/requirements.txt
1414
COPY pyproject.toml /root/pyproject.toml
1515
COPY action.yml /root/action.yml
1616
COPY tagbot /root/tagbot
1717
RUN pip install --no-cache-dir --no-deps /root
18-
CMD python -m tagbot.action
18+
CMD ["python", "-m", "tagbot.action"]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ You can also run the code outside of Docker, but you'll just need to install [Po
542542
```sh
543543
$ git clone https://github.com/JuliaRegistries/TagBot # Consider --branch vA.B.C
544544
$ cd TagBot
545-
$ poetry install
545+
$ poetry install --all-extras
546546
$ poetry run python -m tagbot.local --help
547547
```
548548

poetry.lock

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

pyproject.toml

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,36 @@ license = "MIT"
77

88
[tool.poetry.dependencies]
99
python = "^3.12"
10-
Flask = "3.1.3"
1110
Jinja2 = "^3"
1211
PyGithub = "^2.9.0"
13-
click = "^8"
14-
docker = "^7.1.0"
15-
pexpect = "^4.8.0"
16-
pylev = "^1.3.0"
17-
python-gnupg = "^0.5.6"
18-
pyyaml = "^6"
1912
semver = "^3.0.4"
2013
toml = "^0.10.0"
21-
werkzeug = "3.1.7"
14+
# Optional: only needed when using SSH key passwords
15+
pexpect = { version = "^4.8.0", optional = true }
16+
# Optional: only needed when using GPG signing
17+
python-gnupg = { version = "^0.5.6", optional = true }
18+
# Optional: only needed for error reporting to julia-tagbot.com
19+
docker = { version = "^7.1.0", optional = true }
20+
requests = { version = "^2.28.0", optional = true }
21+
# Optional: only needed for the web service
22+
Flask = { version = "3.1.3", optional = true }
23+
werkzeug = { version = "3.1.7", optional = true }
24+
pylev = { version = "^1.3.0", optional = true }
25+
# Optional: only needed for the local CLI
26+
click = { version = "^8", optional = true }
27+
pyyaml = { version = "^6", optional = true }
28+
# Optional: only needed for GitLab repos
2229
python-gitlab = { version = "^8.2.0", optional = true }
2330

2431
[tool.poetry.extras]
2532
gitlab = ["python-gitlab"]
33+
ssh = ["pexpect"]
34+
gpg = ["python-gnupg"]
35+
reporting = ["docker", "requests"]
36+
web = ["Flask", "werkzeug", "pylev"]
37+
local = ["click", "pyyaml"]
38+
# Install everything (matches previous behavior)
39+
all = ["pexpect", "python-gnupg", "docker", "requests", "Flask", "werkzeug", "pylev", "click", "pyyaml", "python-gitlab"]
2640

2741
[tool.poetry.requires-plugins]
2842
poetry-plugin-export = ">=1.8"

tagbot/action/repo.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@
99

1010
from importlib.metadata import version as pkg_version, PackageNotFoundError
1111

12-
import docker
13-
import pexpect
14-
import requests
1512
import toml
1613

1714
from base64 import b64decode
@@ -36,7 +33,6 @@
3633

3734
from github import Github, Auth, GithubException, UnknownObjectException
3835
from github.PullRequest import PullRequest
39-
from gnupg import GPG
4036
from semver import VersionInfo
4137

4238
from .. import logger
@@ -65,7 +61,10 @@
6561
if GitlabUnknown is not None:
6662
UnknownObjectExceptions = (UnknownObjectException, GitlabUnknown)
6763

68-
RequestException = requests.RequestException
64+
try:
65+
from requests import RequestException
66+
except ImportError:
67+
RequestException = OSError # type: ignore[assignment,misc]
6968

7069
# Maximum number of PRs to check when looking for registry PR
7170
# This prevents excessive API calls on large registries
@@ -1038,9 +1037,14 @@ def _image_id(self) -> str:
10381037
if not host:
10391038
logger.warning("HOSTNAME is not set")
10401039
return "Unknown"
1041-
client = docker.from_env()
1042-
container = client.containers.get(host)
1043-
return container.image.id
1040+
try:
1041+
import docker
1042+
1043+
client = docker.from_env()
1044+
container = client.containers.get(host)
1045+
return container.image.id
1046+
except Exception:
1047+
return "Unknown"
10441048

10451049
def _tag_exists(self, version: str) -> bool:
10461050
"""Check if a tag already exists."""
@@ -1384,9 +1388,14 @@ def _report_error(self, trace: str) -> None:
13841388
}
13851389
if self._manual_intervention_issue_url:
13861390
data["manual_intervention_url"] = self._manual_intervention_issue_url
1387-
resp = requests.post(f"{TAGBOT_WEB}/report", json=data)
1388-
output = json.dumps(resp.json(), indent=2)
1389-
logger.info(f"Response ({resp.status_code}): {output}")
1391+
try:
1392+
import requests
1393+
1394+
resp = requests.post(f"{TAGBOT_WEB}/report", json=data)
1395+
output = json.dumps(resp.json(), indent=2)
1396+
logger.info(f"Response ({resp.status_code}): {output}")
1397+
except ImportError:
1398+
logger.debug("requests not installed, skipping error reporting")
13901399

13911400
def is_registered(self) -> bool:
13921401
"""Check whether or not the repository belongs to a registered package."""
@@ -1477,6 +1486,8 @@ def configure_ssh(self, key: str, password: Optional[str], repo: str = "") -> No
14771486
for k, v in re.findall(r"\s*(.+)=(.+?);", proc.stdout):
14781487
logger.debug(f"Setting environment variable {k}={v}")
14791488
os.environ[k] = v
1489+
import pexpect
1490+
14801491
child = pexpect.spawn(f"ssh-add {priv}")
14811492
child.expect("Enter passphrase")
14821493
child.sendline(password)
@@ -1486,6 +1497,8 @@ def configure_ssh(self, key: str, password: Optional[str], repo: str = "") -> No
14861497

14871498
def configure_gpg(self, key: str, password: Optional[str]) -> None:
14881499
"""Configure the repo to sign tags with GPG."""
1500+
from gnupg import GPG
1501+
14891502
home = os.environ["GNUPGHOME"] = mkdtemp(prefix="tagbot_gpg_")
14901503
os.chmod(home, S_IREAD | S_IWRITE | S_IEXEC)
14911504
logger.debug(f"Set GNUPGHOME to {home}")

0 commit comments

Comments
 (0)