From 3698338c6871b62a71c9f40d097196047784c4c3 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:29:48 +0300 Subject: [PATCH 01/23] pyproject.toml: add mypy types-pytz types-requests --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 0bfe2670..37e4ddf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,9 @@ dev = [ "codecov>=2.1.13", "requests-kerberos>=0.15.0", "ruff>=0.9.6", + "mypy", + "types-pytz", + "types-requests", ] docs = [ "docutils>=0.20.1", From b2b71285ae6690106fd2442e3ecb4b27e3f05b8c Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:51:12 +0300 Subject: [PATCH 02/23] pyproject.toml: add tool.mypy --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 37e4ddf0..5e214060 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,9 @@ upload-dir = "doc/build/html" [tool.distutils.bdist_wheel] universal = 1 +[tool.mypy] +files = "jenkinsapi, jenkinsapi_tests" + [tool.pycodestyle] exclude = ".tox,doc/source/conf.py,build,.venv,.eggs" max-line-length = "99" From fa3e32d6e61b4e3745b12cbeb00043e98a2c9d76 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:50:07 +0300 Subject: [PATCH 03/23] .github/workflows: run mypy as part of "lint" --- .github/workflows/python-package.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index efea44b4..0de03222 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -43,6 +43,10 @@ jobs: run: | uv run ruff check jenkinsapi/ --output-format full + - name: Lint with mypy + run: | + uv run mypy || true + build: runs-on: ubuntu-latest strategy: From dbab0f50132f9f34cf9e6de9f1a803d81d911864 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:04:33 +0300 Subject: [PATCH 04/23] typing: handle circular imports using `if TYPE_CHECKING` --- jenkinsapi/artifact.py | 6 +++++- jenkinsapi/build.py | 6 +++++- jenkinsapi/credentials.py | 5 ++++- jenkinsapi/executor.py | 4 ++++ jenkinsapi/executors.py | 7 +++++-- jenkinsapi/fingerprint.py | 5 ++++- jenkinsapi/job.py | 4 ++++ jenkinsapi/jobs.py | 6 +++++- jenkinsapi/node.py | 4 ++++ jenkinsapi/nodes.py | 5 ++++- jenkinsapi/plugins.py | 6 ++++-- jenkinsapi/queue.py | 7 ++++++- jenkinsapi/result_set.py | 5 +++++ jenkinsapi/view.py | 5 ++++- 14 files changed, 63 insertions(+), 12 deletions(-) diff --git a/jenkinsapi/artifact.py b/jenkinsapi/artifact.py index 72e73818..76a7809a 100644 --- a/jenkinsapi/artifact.py +++ b/jenkinsapi/artifact.py @@ -14,13 +14,17 @@ import os import logging import hashlib -from typing import Any, Literal +from typing import Any, Literal, TYPE_CHECKING from jenkinsapi.fingerprint import Fingerprint from jenkinsapi.custom_exceptions import ArtifactBroken log = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.build import Build + from jenkinsapi.jenkins import Jenkins + class Artifact(object): """ diff --git a/jenkinsapi/build.py b/jenkinsapi/build.py index 2bde2d43..33818c41 100644 --- a/jenkinsapi/build.py +++ b/jenkinsapi/build.py @@ -16,7 +16,7 @@ import datetime from time import sleep -from typing import Iterator, List, Dict, Any +from typing import TYPE_CHECKING, Iterator, List, Dict, Any import pytz from jenkinsapi import config @@ -35,6 +35,10 @@ log = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + from jenkinsapi.job import Job + class Build(JenkinsBase): """ diff --git a/jenkinsapi/credentials.py b/jenkinsapi/credentials.py index e3953efb..a24cfd02 100644 --- a/jenkinsapi/credentials.py +++ b/jenkinsapi/credentials.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Iterator +from typing import TYPE_CHECKING, Iterator import logging from urllib.parse import urlencode @@ -20,6 +20,9 @@ log: logging.Logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + class Credentials(JenkinsBase): """ diff --git a/jenkinsapi/executor.py b/jenkinsapi/executor.py index 4f691356..c2603d7d 100644 --- a/jenkinsapi/executor.py +++ b/jenkinsapi/executor.py @@ -3,12 +3,16 @@ """ from __future__ import annotations +from typing import TYPE_CHECKING from jenkinsapi.jenkinsbase import JenkinsBase import logging log = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + class Executor(JenkinsBase): """ diff --git a/jenkinsapi/executors.py b/jenkinsapi/executors.py index 84f0b6d2..0c36781b 100644 --- a/jenkinsapi/executors.py +++ b/jenkinsapi/executors.py @@ -7,13 +7,16 @@ from __future__ import annotations import logging -from typing import Iterator +from typing import TYPE_CHECKING, Iterator from jenkinsapi.executor import Executor from jenkinsapi.jenkinsbase import JenkinsBase log: logging.Logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + class Executors(JenkinsBase): """ @@ -27,7 +30,7 @@ def __init__( self, baseurl: str, nodename: str, jenkins: "Jenkins" ) -> None: self.nodename: str = nodename - self.jenkins: str = jenkins + self.jenkins: "Jenkins" = jenkins JenkinsBase.__init__(self, baseurl) self.count: int = self._data["numExecutors"] diff --git a/jenkinsapi/fingerprint.py b/jenkinsapi/fingerprint.py index 2d457cec..48eb6275 100644 --- a/jenkinsapi/fingerprint.py +++ b/jenkinsapi/fingerprint.py @@ -6,7 +6,7 @@ import re import logging -from typing import Any +from typing import TYPE_CHECKING, Any import requests @@ -15,6 +15,9 @@ log: logging.Logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + class Fingerprint(JenkinsBase): """ diff --git a/jenkinsapi/job.py b/jenkinsapi/job.py index 00df4714..350fc358 100644 --- a/jenkinsapi/job.py +++ b/jenkinsapi/job.py @@ -6,6 +6,7 @@ import json import logging +from typing import TYPE_CHECKING import xml.etree.ElementTree as ET import urllib.parse as urlparse @@ -34,6 +35,9 @@ log = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + class Job(JenkinsBase, MutableJenkinsThing): """ diff --git a/jenkinsapi/jobs.py b/jenkinsapi/jobs.py index 5eff7035..b55fbd7e 100644 --- a/jenkinsapi/jobs.py +++ b/jenkinsapi/jobs.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import Iterator +from typing import TYPE_CHECKING, Iterator import logging import time @@ -14,6 +14,10 @@ log = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + from jenkinsapi.queue import QueueItem + class Jobs(object): """ diff --git a/jenkinsapi/node.py b/jenkinsapi/node.py index bf258fb2..c48ec10f 100644 --- a/jenkinsapi/node.py +++ b/jenkinsapi/node.py @@ -7,6 +7,7 @@ import json import logging +from typing import TYPE_CHECKING import xml.etree.ElementTree as ET import time @@ -17,6 +18,9 @@ log = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + class Node(JenkinsBase): """ diff --git a/jenkinsapi/nodes.py b/jenkinsapi/nodes.py index 03edea01..ed037306 100644 --- a/jenkinsapi/nodes.py +++ b/jenkinsapi/nodes.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import Iterator +from typing import TYPE_CHECKING, Iterator import logging @@ -17,6 +17,9 @@ log: logging.Logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + class Nodes(JenkinsBase): """ diff --git a/jenkinsapi/plugins.py b/jenkinsapi/plugins.py index 8e3dc6ce..9d9283d6 100644 --- a/jenkinsapi/plugins.py +++ b/jenkinsapi/plugins.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import Generator +from typing import TYPE_CHECKING, Generator import logging import time import re @@ -20,9 +20,11 @@ from jenkinsapi.utils.jsonp_to_json import jsonp_to_json from jenkinsapi.utils.manifest import Manifest, read_manifest - log: logging.Logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + class Plugins(JenkinsBase): """ diff --git a/jenkinsapi/queue.py b/jenkinsapi/queue.py index 3a80ead5..1a14f33f 100644 --- a/jenkinsapi/queue.py +++ b/jenkinsapi/queue.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import Iterator, Tuple +from typing import TYPE_CHECKING, Iterator, Tuple import logging import time from requests import HTTPError @@ -13,6 +13,11 @@ log: logging.Logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.build import Build + from jenkinsapi.jenkins import Jenkins + from jenkinsapi.job import Job + class Queue(JenkinsBase): """ diff --git a/jenkinsapi/result_set.py b/jenkinsapi/result_set.py index 5bf5394f..e821fba1 100644 --- a/jenkinsapi/result_set.py +++ b/jenkinsapi/result_set.py @@ -3,10 +3,15 @@ """ from __future__ import annotations +from typing import TYPE_CHECKING from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.result import Result +if TYPE_CHECKING: + from jenkinsapi.build import Build + from jenkinsapi.jenkins import Jenkins + class ResultSet(JenkinsBase): """ diff --git a/jenkinsapi/view.py b/jenkinsapi/view.py index c9298608..0952e6bf 100644 --- a/jenkinsapi/view.py +++ b/jenkinsapi/view.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import Iterator, Tuple +from typing import TYPE_CHECKING, Iterator, Tuple import logging @@ -15,6 +15,9 @@ log: logging.Logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from jenkinsapi.jenkins import Jenkins + class View(JenkinsBase): """ From 5a2c3e8b139c494ebd88fadf6075de8489819e68 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:08:21 +0300 Subject: [PATCH 05/23] build: use walrus operator to fix typing error in call to get_job --- jenkinsapi/build.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jenkinsapi/build.py b/jenkinsapi/build.py index 33818c41..9de71a16 100644 --- a/jenkinsapi/build.py +++ b/jenkinsapi/build.py @@ -242,8 +242,8 @@ def get_upstream_job(self) -> Job | None: Get the upstream job object if it exist, None otherwise :return: Job or None """ - if self.get_upstream_job_name(): - return self.get_jenkins_obj().get_job(self.get_upstream_job_name()) + if name := self.get_upstream_job_name(): + return self.get_jenkins_obj().get_job(name) return None def get_upstream_build_number(self) -> int | None: @@ -282,8 +282,8 @@ def get_master_job(self) -> Job | None: Get the master job object if it exist, None otherwise :return: Job or None """ - if self.get_master_job_name(): - return self.get_jenkins_obj().get_job(self.get_master_job_name()) + if name := self.get_master_job_name(): + return self.get_jenkins_obj().get_job(name) return None From 170ba5e5d63a3cb2e396a15f8b0e6057c7297245 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:09:32 +0300 Subject: [PATCH 06/23] credential: move field annotation to class level to fix typing errors Fields and variables must have a single type declaration, fix this by moving field declarations to class level instead of on multiple branches on init --- jenkinsapi/credential.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/jenkinsapi/credential.py b/jenkinsapi/credential.py index b3ae7330..69efa38c 100644 --- a/jenkinsapi/credential.py +++ b/jenkinsapi/credential.py @@ -83,6 +83,9 @@ class UsernamePasswordCredential(Credential): dict """ + username: str + password: str + def __init__(self, cred_dict: dict) -> None: jenkins_class: str = ( "com.cloudbees.plugins.credentials.impl." @@ -92,12 +95,11 @@ def __init__(self, cred_dict: dict) -> None: cred_dict, jenkins_class ) if "typeName" in cred_dict: - username: str = cred_dict["displayName"].split("/")[0] + self.username = cred_dict["displayName"].split("/")[0] else: - username: str = cred_dict["userName"] + self.username = cred_dict["userName"] - self.username: str = username - self.password: str = cred_dict.get("password", "") + self.password = cred_dict.get("password", "") def get_attributes(self): """ @@ -223,26 +225,31 @@ class SSHKeyCredential(Credential): dict """ + username: str + passphrase: str + key_type: int + key_value: str + def __init__(self, cred_dict: dict) -> None: jenkins_class: str = ( "com.cloudbees.jenkins.plugins.sshcredentials.impl." "BasicSSHUserPrivateKey" ) super(SSHKeyCredential, self).__init__(cred_dict, jenkins_class) + if "typeName" in cred_dict: - username: str = cred_dict["displayName"].split(" ")[0] + self.username = cred_dict["displayName"].split(" ")[0] else: - username: str = cred_dict["userName"] + self.username = cred_dict["userName"] - self.username: str = username - self.passphrase: str = cred_dict.get("passphrase", "") + self.passphrase = cred_dict.get("passphrase", "") if "private_key" not in cred_dict or cred_dict["private_key"] is None: - self.key_type: int = -1 - self.key_value: str = "" + self.key_type = -1 + self.key_value = "" elif cred_dict["private_key"].startswith("-"): - self.key_type: int = 0 - self.key_value: str = cred_dict["private_key"] + self.key_type = 0 + self.key_value = cred_dict["private_key"] else: raise ValueError("Invalid private_key value") From 299d05efb473eec1c625f7aa2d19786183b8c423 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:20:59 +0300 Subject: [PATCH 07/23] jenkinsbase: annotate JenkinsBase._data as dict This is not technically correct because __init__ assigns None but most code relies on it being a dict. Make this change now to reduce the number of typing errors. --- jenkinsapi/jenkinsbase.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jenkinsapi/jenkinsbase.py b/jenkinsapi/jenkinsbase.py index 02eec9f4..bab6f722 100644 --- a/jenkinsapi/jenkinsbase.py +++ b/jenkinsapi/jenkinsbase.py @@ -20,6 +20,8 @@ class JenkinsBase(object): inherited from """ + _data: dict + def __repr__(self): return """<%s.%s %s>""" % ( self.__class__.__module__, From 7402633c8f8b337d89242f8e9aa37da857cc4294 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:26:20 +0300 Subject: [PATCH 08/23] view: Fix View.keys return annotation - should be Iterable[str] --- jenkinsapi/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkinsapi/view.py b/jenkinsapi/view.py index 0952e6bf..8551a387 100644 --- a/jenkinsapi/view.py +++ b/jenkinsapi/view.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Iterator, Tuple +from typing import TYPE_CHECKING, Iterable, Iterator, Tuple import logging @@ -59,7 +59,7 @@ def delete(self) -> None: self.jenkins_obj.poll() self.deleted = True - def keys(self) -> list[str]: + def keys(self) -> Iterable[str]: return self.get_job_dict().keys() def iteritems(self) -> Iterator[Tuple[str, Job]]: From eb64f3e25f63c1e3b4aa61cd7cab287fc1fd8cc6 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:28:10 +0300 Subject: [PATCH 09/23] credentials: Fix Credentials.iteritems annotation - it is an iterator of tuples --- jenkinsapi/credentials.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkinsapi/credentials.py b/jenkinsapi/credentials.py index a24cfd02..8472de4d 100644 --- a/jenkinsapi/credentials.py +++ b/jenkinsapi/credentials.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING, Iterator, Tuple import logging from urllib.parse import urlencode @@ -68,7 +68,7 @@ def iterkeys(self): def keys(self): return list(self.iterkeys()) - def iteritems(self) -> Iterator[str, "Credential"]: + def iteritems(self) -> Iterator[Tuple[str, "Credential"]]: for cred in self.credentials.values(): yield cred.description, cred From d5ae4a1e03f6fc4d579dc5a5309a27a10b1b2b66 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:28:44 +0300 Subject: [PATCH 10/23] jobs: Fix Jobs.iteritems annotation - it is an iterator of tuples --- jenkinsapi/jobs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkinsapi/jobs.py b/jenkinsapi/jobs.py index b55fbd7e..b0f87999 100644 --- a/jenkinsapi/jobs.py +++ b/jenkinsapi/jobs.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING, Iterator, Tuple import logging import time @@ -98,7 +98,7 @@ def __getitem__(self, job_name: str) -> "Job": else: raise UnknownJob(job_name) - def iteritems(self) -> Iterator[str, "Job"]: + def iteritems(self) -> Iterator[Tuple[str, "Job"]]: """ Iterate over the names & objects for all jobs """ From 762a7d0d7dd4cef40506698606401c3f6fc0f7c0 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:29:22 +0300 Subject: [PATCH 11/23] plugin: Fix iterable return annotations --- jenkinsapi/plugins.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jenkinsapi/plugins.py b/jenkinsapi/plugins.py index 9d9283d6..d59b8eeb 100644 --- a/jenkinsapi/plugins.py +++ b/jenkinsapi/plugins.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING, Iterable, Tuple import logging import time import re @@ -55,18 +55,18 @@ def update_center_dict(self): def _poll(self, tree=None): return self.get_data(self.baseurl, tree=tree) - def keys(self) -> list[str]: + def keys(self) -> Iterable[str]: return self.get_plugins_dict().keys() __iter__ = keys - def iteritems(self) -> Generator[str, "Plugin"]: + def iteritems(self) -> Iterable[Tuple[str, "Plugin"]]: return self._get_plugins() def values(self) -> list["Plugin"]: return [a[1] for a in self.iteritems()] - def _get_plugins(self) -> Generator[str, "Plugin"]: + def _get_plugins(self) -> Iterable[Tuple[str, "Plugin"]]: if "plugins" in self._data: for p_dict in self._data["plugins"]: yield p_dict["shortName"], Plugin(p_dict) From f0ce021be762eee70d4cdb8db26cd149b502204f Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:39:27 +0300 Subject: [PATCH 12/23] simple_post_logger: remove legacy python2 imports because they cause typing warnings --- jenkinsapi/utils/simple_post_logger.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/jenkinsapi/utils/simple_post_logger.py b/jenkinsapi/utils/simple_post_logger.py index 29dbce36..4a80c452 100755 --- a/jenkinsapi/utils/simple_post_logger.py +++ b/jenkinsapi/utils/simple_post_logger.py @@ -1,14 +1,7 @@ #!/usr/bin/env python -try: - from SimpleHTTPServer import SimpleHTTPRequestHandler -except ImportError: - from http.server import SimpleHTTPRequestHandler - -try: - import SocketServer as socketserver -except ImportError: - import socketserver +from http.server import SimpleHTTPRequestHandler +import socketserver import logging import cgi From 56d1c1a959fb1dd5389cb052c621ff54b0d17f82 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:40:45 +0300 Subject: [PATCH 13/23] jenkinsapi_tests: remove legacy import unittest2 --- jenkinsapi_tests/systests/test_scm.py | 4 ---- jenkinsapi_tests/unittests/test_artifact.py | 5 +---- jenkinsapi_tests/unittests/test_job_get_all_builds.py | 6 +----- jenkinsapi_tests/unittests/test_job_scm_hg.py | 6 +----- jenkinsapi_tests/unittests/test_plugins.py | 7 ++----- jenkinsapi_tests/unittests/test_result_set.py | 6 +----- 6 files changed, 6 insertions(+), 28 deletions(-) diff --git a/jenkinsapi_tests/systests/test_scm.py b/jenkinsapi_tests/systests/test_scm.py index 3558b787..7aad8c6b 100644 --- a/jenkinsapi_tests/systests/test_scm.py +++ b/jenkinsapi_tests/systests/test_scm.py @@ -1,10 +1,6 @@ # ''' # System tests for `jenkinsapi.jenkins` module. # ''' -# To run unittests on python 2.6 please use unittest2 library -# try: -# import unittest2 as unittest -# except ImportError: # import unittest # from jenkinsapi_tests.systests.base import BaseSystemTest # from jenkinsapi_tests.test_utils.random_strings import random_string diff --git a/jenkinsapi_tests/unittests/test_artifact.py b/jenkinsapi_tests/unittests/test_artifact.py index 73250d8c..cd760dca 100644 --- a/jenkinsapi_tests/unittests/test_artifact.py +++ b/jenkinsapi_tests/unittests/test_artifact.py @@ -6,10 +6,7 @@ from jenkinsapi.fingerprint import Fingerprint from jenkinsapi.custom_exceptions import ArtifactBroken -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest @pytest.fixture() diff --git a/jenkinsapi_tests/unittests/test_job_get_all_builds.py b/jenkinsapi_tests/unittests/test_job_get_all_builds.py index 54385b9b..d3ef13cd 100644 --- a/jenkinsapi_tests/unittests/test_job_get_all_builds.py +++ b/jenkinsapi_tests/unittests/test_job_get_all_builds.py @@ -1,10 +1,6 @@ import mock -# To run unittests on python 2.6 please use unittest2 library -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest from jenkinsapi import config from jenkinsapi.job import Job diff --git a/jenkinsapi_tests/unittests/test_job_scm_hg.py b/jenkinsapi_tests/unittests/test_job_scm_hg.py index 925d2bdd..df210573 100644 --- a/jenkinsapi_tests/unittests/test_job_scm_hg.py +++ b/jenkinsapi_tests/unittests/test_job_scm_hg.py @@ -1,11 +1,7 @@ # flake8: noqa # import mock # -# # To run unittests on python 2.6 please use unittest2 library -# try: -# import unittest2 as unittest -# except ImportError: -# import unittest +# import unittest # # from jenkinsapi import config # from jenkinsapi.job import Job diff --git a/jenkinsapi_tests/unittests/test_plugins.py b/jenkinsapi_tests/unittests/test_plugins.py index 06da2990..eb9d0fec 100644 --- a/jenkinsapi_tests/unittests/test_plugins.py +++ b/jenkinsapi_tests/unittests/test_plugins.py @@ -4,11 +4,8 @@ import mock -# To run unittests on python 2.6 please use unittest2 library -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest + try: from StringIO import StringIO # python2 except ImportError: diff --git a/jenkinsapi_tests/unittests/test_result_set.py b/jenkinsapi_tests/unittests/test_result_set.py index 78562b05..405f0f17 100644 --- a/jenkinsapi_tests/unittests/test_result_set.py +++ b/jenkinsapi_tests/unittests/test_result_set.py @@ -1,10 +1,6 @@ import mock -# To run unittests on python 2.6 please use unittest2 library -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest from jenkinsapi.result_set import ResultSet from jenkinsapi.result import Result From 337533102a75f4c6f09b402a665f68687f0d8422 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:41:46 +0300 Subject: [PATCH 14/23] jenkinsapi_tests: from unittest import mock All supported versions have mock inside the standard library --- jenkinsapi_tests/unittests/test_artifact.py | 2 +- jenkinsapi_tests/unittests/test_executors.py | 2 +- jenkinsapi_tests/unittests/test_job.py | 2 +- jenkinsapi_tests/unittests/test_job_folders.py | 2 +- jenkinsapi_tests/unittests/test_job_get_all_builds.py | 2 +- jenkinsapi_tests/unittests/test_job_scm_hg.py | 2 +- jenkinsapi_tests/unittests/test_plugins.py | 10 +++++----- jenkinsapi_tests/unittests/test_requester.py | 2 +- jenkinsapi_tests/unittests/test_result_set.py | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/jenkinsapi_tests/unittests/test_artifact.py b/jenkinsapi_tests/unittests/test_artifact.py index cd760dca..0e83ce52 100644 --- a/jenkinsapi_tests/unittests/test_artifact.py +++ b/jenkinsapi_tests/unittests/test_artifact.py @@ -1,5 +1,5 @@ import pytest -from mock import Mock, patch, call +from unittest.mock import Mock, patch, call from requests.exceptions import HTTPError from jenkinsapi.artifact import Artifact from jenkinsapi.jenkinsbase import JenkinsBase diff --git a/jenkinsapi_tests/unittests/test_executors.py b/jenkinsapi_tests/unittests/test_executors.py index d582a489..b62d5a5d 100644 --- a/jenkinsapi_tests/unittests/test_executors.py +++ b/jenkinsapi_tests/unittests/test_executors.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock from jenkinsapi.jenkins import Jenkins from jenkinsapi.executors import Executors from jenkinsapi.executor import Executor diff --git a/jenkinsapi_tests/unittests/test_job.py b/jenkinsapi_tests/unittests/test_job.py index 068103b8..d5a1a678 100644 --- a/jenkinsapi_tests/unittests/test_job.py +++ b/jenkinsapi_tests/unittests/test_job.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import pytest -import mock +from unittest import mock import json from . import configs from jenkinsapi.job import Job diff --git a/jenkinsapi_tests/unittests/test_job_folders.py b/jenkinsapi_tests/unittests/test_job_folders.py index c3ba53c5..b5dae8b2 100644 --- a/jenkinsapi_tests/unittests/test_job_folders.py +++ b/jenkinsapi_tests/unittests/test_job_folders.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock from jenkinsapi.jenkins import JenkinsBase diff --git a/jenkinsapi_tests/unittests/test_job_get_all_builds.py b/jenkinsapi_tests/unittests/test_job_get_all_builds.py index d3ef13cd..5b0798bb 100644 --- a/jenkinsapi_tests/unittests/test_job_get_all_builds.py +++ b/jenkinsapi_tests/unittests/test_job_get_all_builds.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock import unittest diff --git a/jenkinsapi_tests/unittests/test_job_scm_hg.py b/jenkinsapi_tests/unittests/test_job_scm_hg.py index df210573..cbb16c58 100644 --- a/jenkinsapi_tests/unittests/test_job_scm_hg.py +++ b/jenkinsapi_tests/unittests/test_job_scm_hg.py @@ -1,5 +1,5 @@ # flake8: noqa -# import mock +# from unittest import mock # # import unittest # diff --git a/jenkinsapi_tests/unittests/test_plugins.py b/jenkinsapi_tests/unittests/test_plugins.py index eb9d0fec..73374265 100644 --- a/jenkinsapi_tests/unittests/test_plugins.py +++ b/jenkinsapi_tests/unittests/test_plugins.py @@ -2,7 +2,7 @@ jenkinsapi_tests.test_plugins """ -import mock +from unittest import mock import unittest @@ -224,7 +224,7 @@ def test_install_plugin_good_input( @mock.patch.object(Plugins, "_poll") @mock.patch.object(Plugins, "plugin_version_already_installed") @mock.patch.object( - Plugins, "restart_required", new_callable=mock.mock.PropertyMock + Plugins, "restart_required", new_callable=mock.PropertyMock ) @mock.patch.object(Plugins, "_wait_until_plugin_installed") @mock.patch.object(Requester, "post_xml_and_confirm_status") @@ -250,7 +250,7 @@ def test_install_plugins_good_input_no_restart_required( @mock.patch.object(Plugins, "_poll") @mock.patch.object(Plugins, "plugin_version_already_installed") @mock.patch.object( - Plugins, "restart_required", new_callable=mock.mock.PropertyMock + Plugins, "restart_required", new_callable=mock.PropertyMock ) @mock.patch.object(Plugins, "_wait_until_plugin_installed") @mock.patch.object(Requester, "post_xml_and_confirm_status") @@ -314,7 +314,7 @@ def test_plugin_version_already_installed(self, _poll_plugins, _update): @mock.patch.object( Plugins, "update_center_install_status", - new_callable=mock.mock.PropertyMock, + new_callable=mock.PropertyMock, ) def test_restart_required_after_plugin_installation( self, status, _poll_plugins @@ -341,7 +341,7 @@ def test_restart_required_after_plugin_installation( @mock.patch.object( Plugins, "update_center_install_status", - new_callable=mock.mock.PropertyMock, + new_callable=mock.PropertyMock, ) def test_restart_not_required_after_plugin_installation( self, status, _poll_plugins diff --git a/jenkinsapi_tests/unittests/test_requester.py b/jenkinsapi_tests/unittests/test_requester.py index 925696bf..e9ab0289 100644 --- a/jenkinsapi_tests/unittests/test_requester.py +++ b/jenkinsapi_tests/unittests/test_requester.py @@ -2,7 +2,7 @@ import requests from jenkinsapi.jenkins import Requester from jenkinsapi.custom_exceptions import JenkinsAPIException -from mock import patch +from unittest.mock import patch def test_no_parameters_uses_default_values(): diff --git a/jenkinsapi_tests/unittests/test_result_set.py b/jenkinsapi_tests/unittests/test_result_set.py index 405f0f17..8d46b0ca 100644 --- a/jenkinsapi_tests/unittests/test_result_set.py +++ b/jenkinsapi_tests/unittests/test_result_set.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock import unittest From 6aeb2e3c57c00514f531faad2a5d80db1704b9ad Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:42:22 +0300 Subject: [PATCH 15/23] pyproject.toml: remove external mock package --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5e214060..509570b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,6 @@ dev = [ "astroid>=1.4.8", "pylint>=1.7.1", "tox>=2.3.1", - "mock>=5.1.0", "codecov>=2.1.13", "requests-kerberos>=0.15.0", "ruff>=0.9.6", From a10f90dccf70204df7b5064523d80a80c2064ff3 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 13:50:36 +0300 Subject: [PATCH 16/23] node: Annotate launcher variable inside Node.get_node_attributes --- jenkinsapi/node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jenkinsapi/node.py b/jenkinsapi/node.py index c48ec10f..1f95510b 100644 --- a/jenkinsapi/node.py +++ b/jenkinsapi/node.py @@ -121,6 +121,7 @@ def get_node_attributes(self) -> dict: :return: Node attributes dict formatted for Jenkins API request to create node """ + launcher: dict[str, object] na: dict = self.node_attributes if not na.get("credential_description", False): # If credentials description is not present - we will create From a5a7c328e2101e2c651e3ba9331d9fb6b621cd65 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 14:25:13 +0300 Subject: [PATCH 17/23] queue: Fix Queue.get_queue_item_url annotation of item argument - should be dict --- jenkinsapi/queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkinsapi/queue.py b/jenkinsapi/queue.py index 1a14f33f..286af6d4 100644 --- a/jenkinsapi/queue.py +++ b/jenkinsapi/queue.py @@ -82,7 +82,7 @@ def _get_queue_items_for_job(self, job_name: str) -> Iterator["QueueItem"]: def get_queue_items_for_job(self, job_name: str): return list(self._get_queue_items_for_job(job_name)) - def get_queue_item_url(self, item: str) -> str: + def get_queue_item_url(self, item: dict) -> str: return "%s/item/%i" % (self.baseurl, item["id"]) def delete_item(self, queue_item: "QueueItem"): From 9b4f93b56f87d471eef38cbd12125ddbcb92fec6 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 14:26:26 +0300 Subject: [PATCH 18/23] node: Annotate Node._element_tree field --- jenkinsapi/node.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jenkinsapi/node.py b/jenkinsapi/node.py index 1f95510b..c15a0bf0 100644 --- a/jenkinsapi/node.py +++ b/jenkinsapi/node.py @@ -7,7 +7,7 @@ import json import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import xml.etree.ElementTree as ET import time @@ -28,6 +28,8 @@ class Node(JenkinsBase): to the master jenkins instance """ + _element_tree: Optional[ET.Element] + def __init__( self, jenkins_obj: "Jenkins", From d74b324fa16b672524f7b555ba99a48c80fcef47 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 19:33:55 +0300 Subject: [PATCH 19/23] queue: introduce _get_queue_item helper for creating QueueItem objects This slightly reduces code duplication. It also fixes Queue.itervalues() constructing objects in an incorrect way as flagged by type checkers. --- jenkinsapi/queue.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/jenkinsapi/queue.py b/jenkinsapi/queue.py index 286af6d4..43d0548f 100644 --- a/jenkinsapi/queue.py +++ b/jenkinsapi/queue.py @@ -41,12 +41,7 @@ def get_jenkins_obj(self) -> "Jenkins": def iteritems(self) -> Iterator[Tuple[str, "QueueItem"]]: for item in self._data["items"]: - queue_id = item["id"] - item_baseurl = "%s/item/%i" % (self.baseurl, queue_id) - yield ( - item["id"], - QueueItem(baseurl=item_baseurl, jenkins_obj=self.jenkins), - ) + yield item["id"], self._get_queue_item(item) def iterkeys(self) -> Iterator[str]: for item in self._data["items"]: @@ -54,7 +49,7 @@ def iterkeys(self) -> Iterator[str]: def itervalues(self) -> Iterator["QueueItem"]: for item in self._data["items"]: - yield QueueItem(self.jenkins, **item) + yield self._get_queue_item(item) def keys(self) -> list[str]: return list(self.iterkeys()) @@ -75,13 +70,18 @@ def __getitem__(self, item_id: str) -> "QueueItem": def _get_queue_items_for_job(self, job_name: str) -> Iterator["QueueItem"]: for item in self._data["items"]: if "name" in item["task"] and item["task"]["name"] == job_name: - yield QueueItem( - self.get_queue_item_url(item), jenkins_obj=self.jenkins - ) + yield self._get_queue_item(item) def get_queue_items_for_job(self, job_name: str): return list(self._get_queue_items_for_job(job_name)) + def _get_queue_item(self, item: dict) -> QueueItem: + """Get a QueueItem object from a queue item dict""" + return QueueItem( + baseurl=self.get_queue_item_url(item), + jenkins_obj=self.jenkins, + ) + def get_queue_item_url(self, item: dict) -> str: return "%s/item/%i" % (self.baseurl, item["id"]) From 13fdc3b9e467bc1d8f8a6d92baaf504dd2796392 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 19:42:08 +0300 Subject: [PATCH 20/23] systests: add test for Node.get_architecture --- jenkinsapi_tests/systests/test_nodes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jenkinsapi_tests/systests/test_nodes.py b/jenkinsapi_tests/systests/test_nodes.py index 9027d7df..c48ddc94 100644 --- a/jenkinsapi_tests/systests/test_nodes.py +++ b/jenkinsapi_tests/systests/test_nodes.py @@ -316,3 +316,8 @@ def test_offline_reason(jenkins): assert node.offline_reason() == "test2" del jenkins.nodes[node_name] + + +def test_get_node_architecture(jenkins): + mn = jenkins.get_node("Built-In Node") + assert isinstance(mn.get_architecture(), str) From 539a050695776fd6460875a5957e7f08d47c7f95 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 19:56:21 +0300 Subject: [PATCH 21/23] node: annotate Node.upload_config to accept config_xml as bytes This is what set_config_element actually passes so this fixes a type error --- jenkinsapi/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkinsapi/node.py b/jenkinsapi/node.py index c15a0bf0..b78b525e 100644 --- a/jenkinsapi/node.py +++ b/jenkinsapi/node.py @@ -379,7 +379,7 @@ def load_config(self) -> None: self._config = self.get_config() self._get_config_element_tree() - def upload_config(self, config_xml: str) -> None: + def upload_config(self, config_xml: Union[str, bytes]) -> None: """ Uploads config_xml to the config.xml for the node. """ From d87a4012c6873f4c06615acfdb1abb318d87ea8d Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 20:02:55 +0300 Subject: [PATCH 22/23] test_plugins: remove obsolete StringIO/BytesIO compat --- jenkinsapi_tests/unittests/test_plugins.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/jenkinsapi_tests/unittests/test_plugins.py b/jenkinsapi_tests/unittests/test_plugins.py index 73374265..21b13487 100644 --- a/jenkinsapi_tests/unittests/test_plugins.py +++ b/jenkinsapi_tests/unittests/test_plugins.py @@ -6,10 +6,7 @@ import unittest -try: - from StringIO import StringIO # python2 -except ImportError: - from io import BytesIO as StringIO # python3 +from io import BytesIO import zipfile from jenkinsapi.jenkins import Requester @@ -279,7 +276,7 @@ def test_get_plugin_dependencies(self, _poll_plugins): "bla: somestuff\n" "Plugin-Dependencies: aws-java-sdk:1.10.45,aws-credentials:1.15" ) - downloaded_plugin = StringIO() + downloaded_plugin = BytesIO() zipfile.ZipFile(downloaded_plugin, mode="w").writestr( "META-INF/MANIFEST.MF", manifest ) From cf31d9c8a79a47aa16b438fd767b8294d2387353 Mon Sep 17 00:00:00 2001 From: Leonard Crestez Date: Tue, 8 Jul 2025 19:54:27 +0300 Subject: [PATCH 23/23] node: fix get_monitor result type and cast explicitly --- jenkinsapi/node.py | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/jenkinsapi/node.py b/jenkinsapi/node.py index b78b525e..0ec21177 100644 --- a/jenkinsapi/node.py +++ b/jenkinsapi/node.py @@ -7,7 +7,7 @@ import json import logging -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union import xml.etree.ElementTree as ET import time @@ -496,7 +496,7 @@ def set_config_element(self, el_name: str, value: str) -> None: xml_str = ET.tostring(self._et) self.upload_config(xml_str) - def get_monitor(self, monitor_name: str, poll_monitor=True) -> str: + def get_monitor(self, monitor_name: str, poll_monitor=True) -> object: """ Polls the node returning one of the monitors in the monitorData branch of the returned node api tree. @@ -514,60 +514,72 @@ def get_monitor(self, monitor_name: str, poll_monitor=True) -> str: return monitor_data[full_monitor_name] + def get_monitor_dict( + self, + monitor_name: str, + poll_monitor: bool = True, + ) -> dict: + value = self.get_monitor(monitor_name, poll_monitor) + if not isinstance(value, dict): + raise JenkinsAPIException( + f"Monitor {monitor_name!r} did not return a dictionary" + ) + return value + def get_available_physical_memory(self) -> int: """ Returns the node's available physical memory in bytes. """ - monitor_data = self.get_monitor("SwapSpaceMonitor") + monitor_data = self.get_monitor_dict("SwapSpaceMonitor") return monitor_data["availablePhysicalMemory"] def get_available_swap_space(self) -> int: """ Returns the node's available swap space in bytes. """ - monitor_data = self.get_monitor("SwapSpaceMonitor") + monitor_data = self.get_monitor_dict("SwapSpaceMonitor") return monitor_data["availableSwapSpace"] def get_total_physical_memory(self) -> int: """ Returns the node's total physical memory in bytes. """ - monitor_data = self.get_monitor("SwapSpaceMonitor") + monitor_data = self.get_monitor_dict("SwapSpaceMonitor") return monitor_data["totalPhysicalMemory"] def get_total_swap_space(self) -> int: """ Returns the node's total swap space in bytes. """ - monitor_data = self.get_monitor("SwapSpaceMonitor") + monitor_data = self.get_monitor_dict("SwapSpaceMonitor") return monitor_data["totalSwapSpace"] def get_workspace_path(self) -> str: """ Returns the local path to the node's Jenkins workspace directory. """ - monitor_data = self.get_monitor("DiskSpaceMonitor") + monitor_data = self.get_monitor_dict("DiskSpaceMonitor") return monitor_data["path"] def get_workspace_size(self) -> int: """ Returns the size in bytes of the node's Jenkins workspace directory. """ - monitor_data = self.get_monitor("DiskSpaceMonitor") + monitor_data = self.get_monitor_dict("DiskSpaceMonitor") return monitor_data["size"] def get_temp_path(self) -> str: """ Returns the local path to the node's temp directory. """ - monitor_data = self.get_monitor("TemporarySpaceMonitor") + monitor_data = self.get_monitor_dict("TemporarySpaceMonitor") return monitor_data["path"] def get_temp_size(self) -> int: """ Returns the size in bytes of the node's temp directory. """ - monitor_data = self.get_monitor("TemporarySpaceMonitor") + monitor_data = self.get_monitor_dict("TemporarySpaceMonitor") return monitor_data["size"] def get_architecture(self) -> str: @@ -575,7 +587,12 @@ def get_architecture(self) -> str: Returns the system architecture of the node eg. "Linux (amd64)". """ # no need to poll as the architecture will never change - return self.get_monitor("ArchitectureMonitor", poll_monitor=False) + value = self.get_monitor("ArchitectureMonitor", poll_monitor=False) + if not isinstance(value, str): + raise JenkinsAPIException( + "Monitor ArchitectureMonitor did not return a string" + ) + return value def block_until_idle(self, timeout: int, poll_time: int = 5) -> None: """ @@ -603,7 +620,7 @@ def get_response_time(self) -> int: """ Returns the node's average response time. """ - monitor_data = self.get_monitor("ResponseTimeMonitor") + monitor_data = self.get_monitor_dict("ResponseTimeMonitor") return monitor_data["average"] def get_clock_difference(self) -> int: @@ -612,5 +629,5 @@ def get_clock_difference(self) -> int: the master Jenkins clock. Used to detect out of sync clocks. """ - monitor_data = self.get_monitor("ClockMonitor") + monitor_data = self.get_monitor_dict("ClockMonitor") return monitor_data["diff"]