From d99076a160446beaede4701a67e4af4de396411f Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Wed, 31 Jul 2024 15:53:11 +0200 Subject: [PATCH 01/48] Add all, external, and label to Image.prune() Param all is now supported by prune. Param external is now supported by prune. Filter by label, which was already supported, is now documented and tested. Resolves: https://github.com/containers/podman-py/issues/312 Signed-off-by: Nicola Sella --- podman/domain/images_manager.py | 23 ++++++++-- podman/tests/unit/test_imagesmanager.py | 58 +++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/podman/domain/images_manager.py b/podman/domain/images_manager.py index 8cc3cdbe..0a14853c 100644 --- a/podman/domain/images_manager.py +++ b/podman/domain/images_manager.py @@ -135,24 +135,39 @@ def load(self, data: bytes) -> Generator[Image, None, None]: yield self.get(item) def prune( - self, filters: Optional[Mapping[str, Any]] = None + self, + all: Optional[bool] = False, # pylint: disable=redefined-builtin + external: Optional[bool] = False, + filters: Optional[Mapping[str, Any]] = None, ) -> Dict[Literal["ImagesDeleted", "SpaceReclaimed"], Any]: """Delete unused images. The Untagged keys will always be "". Args: + all: Remove all images not in use by containers, not just dangling ones. + external: Remove images even when they are used by external containers + (e.g, by build containers). filters: Qualify Images to prune. Available filters: - dangling (bool): when true, only delete unused and untagged images. + - label: (dict): filter by label. + Examples: + filters={"label": {"key": "value"}} + filters={"label!": {"key": "value"}} - until (str): Delete images older than this timestamp. Raises: APIError: when service returns an error """ - response = self.client.post( - "/images/prune", params={"filters": api.prepare_filters(filters)} - ) + + params = { + "all": all, + "external": external, + "filters": api.prepare_filters(filters), + } + + response = self.client.post("/images/prune", params=params) response.raise_for_status() deleted: List[Dict[str, str]] = [] diff --git a/podman/tests/unit/test_imagesmanager.py b/podman/tests/unit/test_imagesmanager.py index c51cef8b..bcadd2b7 100644 --- a/podman/tests/unit/test_imagesmanager.py +++ b/podman/tests/unit/test_imagesmanager.py @@ -207,6 +207,64 @@ def test_prune_filters(self, mock): self.assertEqual(len(untagged), 2) self.assertEqual(len("".join(untagged)), 0) + @requests_mock.Mocker() + def test_prune_filters_label(self, mock): + """Unit test filters param label for Images prune().""" + mock.post( + tests.LIBPOD_URL + + "/images/prune?filters=%7B%22label%22%3A+%5B%22%7B%27license%27%3A+%27Apache-2.0%27%7D%22%5D%7D", + json=[ + { + "Id": "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", + "Size": 1024, + }, + ], + ) + + report = self.client.images.prune(filters={"label": {"license": "Apache-2.0"}}) + self.assertIn("ImagesDeleted", report) + self.assertIn("SpaceReclaimed", report) + + self.assertEqual(report["SpaceReclaimed"], 1024) + + deleted = [r["Deleted"] for r in report["ImagesDeleted"] if "Deleted" in r] + self.assertEqual(len(deleted), 1) + self.assertIn("326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", deleted) + self.assertGreater(len("".join(deleted)), 0) + + untagged = [r["Untagged"] for r in report["ImagesDeleted"] if "Untagged" in r] + self.assertEqual(len(untagged), 1) + self.assertEqual(len("".join(untagged)), 0) + + @requests_mock.Mocker() + def test_prune_filters_not_label(self, mock): + """Unit test filters param NOT-label for Images prune().""" + mock.post( + tests.LIBPOD_URL + + "/images/prune?filters=%7B%22label%21%22%3A+%5B%22%7B%27license%27%3A+%27Apache-2.0%27%7D%22%5D%7D", + json=[ + { + "Id": "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", + "Size": 1024, + }, + ], + ) + + report = self.client.images.prune(filters={"label!": {"license": "Apache-2.0"}}) + self.assertIn("ImagesDeleted", report) + self.assertIn("SpaceReclaimed", report) + + self.assertEqual(report["SpaceReclaimed"], 1024) + + deleted = [r["Deleted"] for r in report["ImagesDeleted"] if "Deleted" in r] + self.assertEqual(len(deleted), 1) + self.assertIn("c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", deleted) + self.assertGreater(len("".join(deleted)), 0) + + untagged = [r["Untagged"] for r in report["ImagesDeleted"] if "Untagged" in r] + self.assertEqual(len(untagged), 1) + self.assertEqual(len("".join(untagged)), 0) + @requests_mock.Mocker() def test_prune_failure(self, mock): """Unit test to report error carried in response body.""" From c9b3d671a93215fa11bfc5651b1389d674c39875 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Fri, 15 Nov 2024 23:49:46 +0530 Subject: [PATCH 02/48] fix[docs]: Unindented example code on the index page Signed-off-by: Kanishk Pachauri --- docs/source/index.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index f9db0485..d254b58d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -34,13 +34,14 @@ Example .. code-block:: python :linenos: - import podman + import podman + + with podman.PodmanClient() as client: + if client.ping(): + images = client.images.list() + for image in images: + print(image.id) - with podman.PodmanClient() as client: - if client.ping(): - images = client.images.list() - for image in images: - print(image.id) .. toctree:: :caption: Podman Client @@ -73,4 +74,4 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` -* :ref:`search` +* :ref:`search` \ No newline at end of file From e9967daeaa2df93bea2b3938cd7ed3318169e92e Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Sat, 16 Nov 2024 04:12:02 +0530 Subject: [PATCH 03/48] fix: name filter in images.list() Signed-off-by: Kanishk Pachauri --- podman/domain/images_manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/podman/domain/images_manager.py b/podman/domain/images_manager.py index 8edf2bc8..e19478c3 100644 --- a/podman/domain/images_manager.py +++ b/podman/domain/images_manager.py @@ -61,10 +61,13 @@ def list(self, **kwargs) -> List[Image]: Raises: APIError: when service returns an error """ + filters = kwargs.get("filters", {}).copy() + if name := kwargs.get("name"): + filters["reference"] = name + params = { "all": kwargs.get("all"), - "name": kwargs.get("name"), - "filters": api.prepare_filters(kwargs.get("filters")), + "filters": api.prepare_filters(filters=filters), } response = self.client.get("/images/json", params=params) if response.status_code == requests.codes.not_found: From 58587879fa8e0f4bcde54daa1f742fa1b316fd68 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 20 Nov 2024 01:36:09 +0530 Subject: [PATCH 04/48] tests: Add test for name filter Signed-off-by: Kanishk Pachauri --- podman/tests/unit/test_imagesmanager.py | 88 +++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/podman/tests/unit/test_imagesmanager.py b/podman/tests/unit/test_imagesmanager.py index 6de6cc8a..3b3322b0 100644 --- a/podman/tests/unit/test_imagesmanager.py +++ b/podman/tests/unit/test_imagesmanager.py @@ -589,6 +589,94 @@ def test_pull_2x(self, mock): images[1].id, "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e" ) + @requests_mock.Mocker() + def test_list_with_name_parameter(self, mock): + """Test that name parameter is correctly converted to a reference filter""" + mock.get( + tests.LIBPOD_URL + "/images/json?filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%7D", + json=[FIRST_IMAGE] + ) + + images = self.client.images.list(name="fedora") + + self.assertEqual(len(images), 1) + self.assertIsInstance(images[0], Image) + self.assertEqual(images[0].tags, ["fedora:latest", "fedora:33"]) + + @requests_mock.Mocker() + def test_list_with_name_and_existing_filters(self, mock): + """Test that name parameter works alongside other filters""" + mock.get( + tests.LIBPOD_URL + "/images/json?filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%2C+%22dangling%22%3A+%5B%22true%22%5D%7D", + json=[FIRST_IMAGE] + ) + + images = self.client.images.list( + name="fedora", + filters={"dangling": True} + ) + + self.assertEqual(len(images), 1) + self.assertIsInstance(images[0], Image) + + @requests_mock.Mocker() + def test_list_with_name_overrides_reference_filter(self, mock): + """Test that name parameter takes precedence over existing reference filter""" + mock.get( + tests.LIBPOD_URL + "/images/json?filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%7D", + json=[FIRST_IMAGE] + ) + + # The name parameter should override the reference filter + images = self.client.images.list( + name="fedora", + filters={"reference": "ubuntu"} # This should be overridden + ) + + self.assertEqual(len(images), 1) + self.assertIsInstance(images[0], Image) + + @requests_mock.Mocker() + def test_list_with_all_and_name(self, mock): + """Test that all parameter works alongside name filter""" + mock.get( + tests.LIBPOD_URL + "/images/json?all=true&filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%7D", + json=[FIRST_IMAGE] + ) + + images = self.client.images.list( + all=True, + name="fedora" + ) + + self.assertEqual(len(images), 1) + self.assertIsInstance(images[0], Image) + + @requests_mock.Mocker() + def test_list_with_empty_name(self, mock): + """Test that empty name parameter doesn't add a reference filter""" + mock.get( + tests.LIBPOD_URL + "/images/json", + json=[FIRST_IMAGE] + ) + + images = self.client.images.list(name="") + + self.assertEqual(len(images), 1) + self.assertIsInstance(images[0], Image) + + @requests_mock.Mocker() + def test_list_with_none_name(self, mock): + """Test that None name parameter doesn't add a reference filter""" + mock.get( + tests.LIBPOD_URL + "/images/json", + json=[FIRST_IMAGE] + ) + + images = self.client.images.list(name=None) + + self.assertEqual(len(images), 1) + self.assertIsInstance(images[0], Image) if __name__ == '__main__': unittest.main() From 1fb6c1ce98217fa318cc6dfb82d90e703c4de64f Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 20 Nov 2024 01:41:31 +0530 Subject: [PATCH 05/48] chore: format with black Signed-off-by: Kanishk Pachauri --- podman/tests/unit/test_imagesmanager.py | 50 ++++++++++--------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/podman/tests/unit/test_imagesmanager.py b/podman/tests/unit/test_imagesmanager.py index 3b3322b0..573eb202 100644 --- a/podman/tests/unit/test_imagesmanager.py +++ b/podman/tests/unit/test_imagesmanager.py @@ -594,11 +594,11 @@ def test_list_with_name_parameter(self, mock): """Test that name parameter is correctly converted to a reference filter""" mock.get( tests.LIBPOD_URL + "/images/json?filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%7D", - json=[FIRST_IMAGE] + json=[FIRST_IMAGE], ) images = self.client.images.list(name="fedora") - + self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) self.assertEqual(images[0].tags, ["fedora:latest", "fedora:33"]) @@ -607,15 +607,13 @@ def test_list_with_name_parameter(self, mock): def test_list_with_name_and_existing_filters(self, mock): """Test that name parameter works alongside other filters""" mock.get( - tests.LIBPOD_URL + "/images/json?filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%2C+%22dangling%22%3A+%5B%22true%22%5D%7D", - json=[FIRST_IMAGE] + tests.LIBPOD_URL + + "/images/json?filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%2C+%22dangling%22%3A+%5B%22true%22%5D%7D", + json=[FIRST_IMAGE], ) - images = self.client.images.list( - name="fedora", - filters={"dangling": True} - ) - + images = self.client.images.list(name="fedora", filters={"dangling": True}) + self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) @@ -624,15 +622,14 @@ def test_list_with_name_overrides_reference_filter(self, mock): """Test that name parameter takes precedence over existing reference filter""" mock.get( tests.LIBPOD_URL + "/images/json?filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%7D", - json=[FIRST_IMAGE] + json=[FIRST_IMAGE], ) # The name parameter should override the reference filter images = self.client.images.list( - name="fedora", - filters={"reference": "ubuntu"} # This should be overridden + name="fedora", filters={"reference": "ubuntu"} # This should be overridden ) - + self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) @@ -640,43 +637,36 @@ def test_list_with_name_overrides_reference_filter(self, mock): def test_list_with_all_and_name(self, mock): """Test that all parameter works alongside name filter""" mock.get( - tests.LIBPOD_URL + "/images/json?all=true&filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%7D", - json=[FIRST_IMAGE] + tests.LIBPOD_URL + + "/images/json?all=true&filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%7D", + json=[FIRST_IMAGE], ) - images = self.client.images.list( - all=True, - name="fedora" - ) - + images = self.client.images.list(all=True, name="fedora") + self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) @requests_mock.Mocker() def test_list_with_empty_name(self, mock): """Test that empty name parameter doesn't add a reference filter""" - mock.get( - tests.LIBPOD_URL + "/images/json", - json=[FIRST_IMAGE] - ) + mock.get(tests.LIBPOD_URL + "/images/json", json=[FIRST_IMAGE]) images = self.client.images.list(name="") - + self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) @requests_mock.Mocker() def test_list_with_none_name(self, mock): """Test that None name parameter doesn't add a reference filter""" - mock.get( - tests.LIBPOD_URL + "/images/json", - json=[FIRST_IMAGE] - ) + mock.get(tests.LIBPOD_URL + "/images/json", json=[FIRST_IMAGE]) images = self.client.images.list(name=None) - + self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) + if __name__ == '__main__': unittest.main() From 986ba477e1f0cb363eac6444d02d5a16eea48a71 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 20 Nov 2024 02:09:22 +0530 Subject: [PATCH 06/48] fix: broken tests Signed-off-by: Kanishk Pachauri --- podman/tests/unit/test_imagesmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podman/tests/unit/test_imagesmanager.py b/podman/tests/unit/test_imagesmanager.py index 573eb202..4906daf2 100644 --- a/podman/tests/unit/test_imagesmanager.py +++ b/podman/tests/unit/test_imagesmanager.py @@ -608,7 +608,7 @@ def test_list_with_name_and_existing_filters(self, mock): """Test that name parameter works alongside other filters""" mock.get( tests.LIBPOD_URL - + "/images/json?filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%2C+%22dangling%22%3A+%5B%22true%22%5D%7D", + + "/images/json?filters=%7B%22dangling%22%3A+%5B%22True%22%5D%2C+%22reference%22%3A+%5B%22fedora%22%5D%7D", json=[FIRST_IMAGE], ) From 02e5829e7de7f4c212afa6c872df5b7715987409 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 22 Nov 2024 14:31:30 +0100 Subject: [PATCH 07/48] Drop python<3.8 and enable testing up to py3.13 Signed-off-by: Nicola Sella --- Makefile | 6 +++--- pyproject.toml | 2 +- setup.cfg | 6 ++---- tox.ini | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 40a08827..26da832e 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ lint: tox .PHONY: tests tests: tox # see tox.ini for environment variable settings - $(PYTHON) -m tox -e coverage,py36,py38,py39,py310,py311 + $(PYTHON) -m tox -e coverage,py39,py310,py311,py312,py313 .PHONY: unittest unittest: @@ -39,9 +39,9 @@ integration: .PHONY: tox tox: ifeq (, $(shell which dnf)) - brew install python@3.8 python@3.9 python@3.10 python@3.11 + brew install python@3.9 python@3.10 python@3.11 python@3.12 python@3.13 else - -dnf install -y python3 python3.6 python3.8 python3.9 + -dnf install -y python3 python3.9 python3.10 python3.11 python3.12 python3.13 endif # ensure tox is available. It will take care of other testing requirements $(PYTHON) -m pip install --user tox diff --git a/pyproject.toml b/pyproject.toml index 944b64e9..9db36747 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ line-length = 100 skip-string-normalization = true preview = true -target-version = ["py36"] +target-version = ["py39"] include = '\.pyi?$' exclude = ''' /( diff --git a/setup.cfg b/setup.cfg index 7a76fa86..5d1700d1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,19 +19,17 @@ classifiers = License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Topic :: Software Development :: Libraries :: Python Modules keywords = podman, libpod [options] include_package_data = True -python_requires = >=3.6 +python_requires = >=3.9 test_suite = # Any changes should be copied into pyproject.toml install_requires = diff --git a/tox.ini b/tox.ini index 6dccae96..94d6b07a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.2.0 -envlist = pylint,coverage,py36,py38,py39,py310,py311,py312 +envlist = pylint,coverage,py39,py310,py311,py312,py313 ignore_basepython_conflict = true [testenv] From 7e649973c8643baa3c58b9afadfcda966dd597ad Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 22 Nov 2024 16:21:04 +0100 Subject: [PATCH 08/48] Bump release to 5.3.0 Signed-off-by: Nicola Sella --- Makefile | 2 +- podman/tests/__init__.py | 2 +- podman/version.py | 2 +- setup.cfg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 26da832e..11ab79da 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ DESTDIR ?= EPOCH_TEST_COMMIT ?= $(shell git merge-base $${DEST_BRANCH:-main} HEAD) HEAD ?= HEAD -export PODMAN_VERSION ?= "5.2.0" +export PODMAN_VERSION ?= "5.3.0" .PHONY: podman podman: diff --git a/podman/tests/__init__.py b/podman/tests/__init__.py index 6b3e030b..9b777808 100644 --- a/podman/tests/__init__.py +++ b/podman/tests/__init__.py @@ -3,5 +3,5 @@ # Do not auto-update these from version.py, # as test code should be changed to reflect changes in Podman API versions BASE_SOCK = "unix:///run/api.sock" -LIBPOD_URL = "http://%2Frun%2Fapi.sock/v5.2.0/libpod" +LIBPOD_URL = "http://%2Frun%2Fapi.sock/v5.3.0/libpod" COMPATIBLE_URL = "http://%2Frun%2Fapi.sock/v1.40" diff --git a/podman/version.py b/podman/version.py index 92c5ee52..705ff65d 100644 --- a/podman/version.py +++ b/podman/version.py @@ -1,4 +1,4 @@ """Version of PodmanPy.""" -__version__ = "5.2.0" +__version__ = "5.3.0" __compatible_version__ = "1.40" diff --git a/setup.cfg b/setup.cfg index 5d1700d1..d31a2479 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = podman -version = 5.2.0 +version = 5.3.0 author = Brent Baude, Jhon Honce, Urvashi Mohnani, Nicola Sella author_email = jhonce@redhat.com description = Bindings for Podman RESTful API From 3968fc201bf6473d72caf5ce2b75d9b39817e885 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 28 Nov 2024 15:55:25 +0100 Subject: [PATCH 09/48] /run/user/$UID as fallback if XDG_RUNTIME_DIR is not set XDG mentions `/run/user/$UID` as the value for `XDG_RUNTIME_DIR`: https://www.freedesktop.org/software/systemd/man/latest/pam_systemd.html https://serverfault.com/questions/388840/good-default-for-xdg-runtime-di r/727994#727994 Archlinux, Debian, RedHat, Ubuntu, etc all use `/run/user/$UID` because they follow XDG: https://wiki.archlinux.org/title/XDG_Base_Directory Signed-off-by: Hans-Christoph Steiner --- podman/api/path_utils.py | 10 +++++++++- podman/tests/unit/test_path_utils.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 podman/tests/unit/test_path_utils.py diff --git a/podman/api/path_utils.py b/podman/api/path_utils.py index 84f9769e..5b40bd8c 100644 --- a/podman/api/path_utils.py +++ b/podman/api/path_utils.py @@ -7,11 +7,19 @@ def get_runtime_dir() -> str: - """Returns the runtime directory for the current user""" + """Returns the runtime directory for the current user + + The value in XDG_RUNTIME_DIR is preferred, but that is not always set, for + example, on headless servers. /run/user/$UID is defined in the XDG documentation. + + """ try: return os.environ['XDG_RUNTIME_DIR'] except KeyError: user = getpass.getuser() + run_user = f'/run/user/{user}' + if os.path.isdir(run_user): + return run_user fallback = f'/tmp/podmanpy-runtime-dir-fallback-{user}' try: diff --git a/podman/tests/unit/test_path_utils.py b/podman/tests/unit/test_path_utils.py new file mode 100644 index 00000000..29e02443 --- /dev/null +++ b/podman/tests/unit/test_path_utils.py @@ -0,0 +1,28 @@ +import datetime +import os +import unittest +import tempfile +from unittest import mock + +from podman import api + + +class PathUtilsTestCase(unittest.TestCase): + def setUp(self): + self.xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR') + print('XDG_RUNTIME_DIR', self.xdg_runtime_dir) + + @mock.patch.dict(os.environ, clear=True) + def test_get_runtime_dir_env_var_set(self): + with tempfile.TemporaryDirectory() as tmpdir: + os.environ['XDG_RUNTIME_DIR'] = str(tmpdir) + self.assertEqual(str(tmpdir), api.path_utils.get_runtime_dir()) + + @unittest.skipUnless(os.getenv('XDG_RUNTIME_DIR'), 'XDG_RUNTIME_DIR must be set') + @mock.patch.dict(os.environ, clear=True) + def test_get_runtime_dir_env_var_not_set(self): + self.assertNotEqual(self.xdg_runtime_dir, api.path_utils.get_runtime_dir()) + + +if __name__ == '__main__': + unittest.main() From 61cb204f92d9f2a0cc319d925cdef15f535dae47 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 2 Dec 2024 09:25:21 +0100 Subject: [PATCH 10/48] fix: /run/user/ is based on UID not username Signed-off-by: Hans-Christoph Steiner --- podman/api/path_utils.py | 2 +- podman/tests/unit/test_path_utils.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/podman/api/path_utils.py b/podman/api/path_utils.py index 5b40bd8c..041663b4 100644 --- a/podman/api/path_utils.py +++ b/podman/api/path_utils.py @@ -17,7 +17,7 @@ def get_runtime_dir() -> str: return os.environ['XDG_RUNTIME_DIR'] except KeyError: user = getpass.getuser() - run_user = f'/run/user/{user}' + run_user = f'/run/user/{os.getuid()}' if os.path.isdir(run_user): return run_user fallback = f'/tmp/podmanpy-runtime-dir-fallback-{user}' diff --git a/podman/tests/unit/test_path_utils.py b/podman/tests/unit/test_path_utils.py index 29e02443..eda2dd62 100644 --- a/podman/tests/unit/test_path_utils.py +++ b/podman/tests/unit/test_path_utils.py @@ -10,7 +10,6 @@ class PathUtilsTestCase(unittest.TestCase): def setUp(self): self.xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR') - print('XDG_RUNTIME_DIR', self.xdg_runtime_dir) @mock.patch.dict(os.environ, clear=True) def test_get_runtime_dir_env_var_set(self): @@ -18,9 +17,20 @@ def test_get_runtime_dir_env_var_set(self): os.environ['XDG_RUNTIME_DIR'] = str(tmpdir) self.assertEqual(str(tmpdir), api.path_utils.get_runtime_dir()) - @unittest.skipUnless(os.getenv('XDG_RUNTIME_DIR'), 'XDG_RUNTIME_DIR must be set') @mock.patch.dict(os.environ, clear=True) def test_get_runtime_dir_env_var_not_set(self): + if not self.xdg_runtime_dir: + self.skipTest('XDG_RUNTIME_DIR must be set for this test.') + if self.xdg_runtime_dir.startswith('/run/user/'): + self.skipTest("XDG_RUNTIME_DIR in /run/user/, can't check") + self.assertNotEqual(self.xdg_runtime_dir, api.path_utils.get_runtime_dir()) + + @mock.patch('os.path.isdir', lambda d: False) + @mock.patch.dict(os.environ, clear=True) + def test_get_runtime_dir_env_var_not_set_and_no_run(self): + """Fake that XDG_RUNTIME_DIR is not set and /run/user/ does not exist.""" + if not self.xdg_runtime_dir: + self.skipTest('XDG_RUNTIME_DIR must be set to fetch a working dir.') self.assertNotEqual(self.xdg_runtime_dir, api.path_utils.get_runtime_dir()) From 74e449fa4cb69e794511672f9bce0c52cb878293 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Thu, 28 Nov 2024 15:21:56 +0100 Subject: [PATCH 11/48] Add pre-commit-workflow Signed-off-by: Nicola Sella --- .github/workflows/pre-commit.yml | 18 ++++++++++++ .pre-commit-config.yaml | 16 ++++++++++ ruff.toml | 50 ++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .pre-commit-config.yaml create mode 100644 ruff.toml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..c6ece29f --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,18 @@ +name: pre-commit +on: + pull_request: + push: + branches: [main] +jobs: + pre-commit: + runs-on: ubuntu-latest + env: + SKIP: no-commit-to-branch + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: | + 3.9 + 3.x + - uses: pre-commit/action@v3.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..67fc4d6c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.6.0 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..02b60ac5 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,50 @@ + +line-length = 100 +[format] +exclude = [ + ".git", + ".venv", + ".history", + "build", + "dist", + "docs", + "hack", +] +quote-style = "preserve" +[lint] +select = [ + # More stuff here https://docs.astral.sh/ruff/rules/ + "F", # Pyflakes + "E", # Pycodestyle Error + "W", # Pycodestyle Warning + "N", # PEP8 Naming + # TODO "UP", # Pyupgrade + # TODO "ANN", + # TODO "S", # Bandit + # "B", # Bugbear + "A", # flake-8-builtins + "YTT", # flake-8-2020 + "PLC", # Pylint Convention + "PLE", # Pylint Error + "PLW", # Pylint Warning +] +# Some checks should be enabled for code sanity disabled now +# to avoid changing too many lines +ignore = [ + "F821", # TODO Undefined name + "F541", # TODO f-string is missing placeholders + "F401", # TODO Module imported but unused + "F841", # TODO Local variable is assigned to but never used + "E402", # TODO Module level import not at top of file + "E741", # TODO ambiguous variable name + "E722", # TODO do not use bare 'except' + "E501", # TODO line too long + "N818", # TODO Error Suffix in exception name + "N80", # TODO Invalid Name + "ANN10", # Missing type annotation + "PLW2901", # TODO Redefined Loop Name +] +[lint.per-file-ignores] +"podman/tests/*.py" = ["S"] +[lint.flake8-builtins] +builtins-ignorelist = ["copyright", "all"] From 3a29d248ee2ff7c2a2d2d5a432b5b5d2ca3baa88 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Thu, 28 Nov 2024 15:53:36 +0100 Subject: [PATCH 12/48] Update lint, format checks in tox and cirrus files Signed-off-by: Nicola Sella --- Makefile | 2 +- test-requirements.txt | 3 +-- tox.ini | 22 +++++++++++++--------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 11ab79da..cbf87e67 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ podman: .PHONY: lint lint: tox - $(PYTHON) -m tox -e black,pylint + $(PYTHON) -m tox -e format,lint .PHONY: tests tests: tox diff --git a/test-requirements.txt b/test-requirements.txt index b208312e..1a70e359 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,9 +1,8 @@ # Any changes should be copied into pyproject.toml -r requirements.txt -black +ruff coverage fixtures -pylint pytest requests-mock >= 1.11.0 tox diff --git a/tox.ini b/tox.ini index 94d6b07a..66e6288a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.2.0 -envlist = pylint,coverage,py39,py310,py311,py312,py313 +envlist = coverage,py39,py310,py311,py312,py313 ignore_basepython_conflict = true [testenv] @@ -17,21 +17,25 @@ setenv = [testenv:venv] commands = {posargs} -[testenv:pylint] -depends = py310 -basepython = python3.10 -allowlist_externals = pylint -commands = pylint podman +[testenv:lint] +depends = ruff +allowlist_externals = ruff +commands = ruff check --diff + +# TODO: add pylint as alias of lint for compatibility [testenv:coverage] commands = coverage run -m pytest coverage report -m --skip-covered --fail-under=80 --omit=podman/tests/* --omit=.tox/* -[testenv:black] -deps = black +[testenv:format] +deps = ruff +allowlist_externals = ruff commands = - black --diff --check . + ruff format --diff + +# TODO: add black as alias of format for compatibility [testenv:black-format] deps = black From 59985eaf972898d98382f191474fd504e9bb9a98 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Tue, 3 Dec 2024 16:13:54 +0100 Subject: [PATCH 13/48] Fix files to comply with pre-commit Signed-off-by: Nicola Sella --- contrib/cirrus/build_podman.sh | 1 - docs/source/_templates/apidoc/toc.rst_t | 1 - docs/source/conf.py | 6 +++--- docs/source/index.rst | 2 +- podman/api/typing_extensions.py | 1 + podman/domain/volumes.py | 3 ++- podman/tests/README.md | 1 - podman/tests/integration/base.py | 1 + podman/tests/integration/test_container_create.py | 2 +- podman/tests/integration/test_images.py | 1 + podman/tests/integration/test_networks.py | 1 + podman/tests/integration/utils.py | 5 ++--- podman/tests/unit/test_imagesmanager.py | 3 ++- pyproject.toml | 1 - 14 files changed, 15 insertions(+), 14 deletions(-) diff --git a/contrib/cirrus/build_podman.sh b/contrib/cirrus/build_podman.sh index 9a217b0b..87218d71 100755 --- a/contrib/cirrus/build_podman.sh +++ b/contrib/cirrus/build_podman.sh @@ -7,4 +7,3 @@ systemctl stop podman.socket || : dnf erase podman -y dnf copr enable rhcontainerbot/podman-next -y dnf install podman -y - diff --git a/docs/source/_templates/apidoc/toc.rst_t b/docs/source/_templates/apidoc/toc.rst_t index f0877eeb..878540ce 100644 --- a/docs/source/_templates/apidoc/toc.rst_t +++ b/docs/source/_templates/apidoc/toc.rst_t @@ -5,4 +5,3 @@ {% for docname in docnames %} {{ docname }} {%- endfor %} - diff --git a/docs/source/conf.py b/docs/source/conf.py index caf916d3..e1e0845a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,9 +20,9 @@ # -- Project information ----------------------------------------------------- -project = u'Podman Python SDK' -copyright = u'2021, Red Hat Inc' -author = u'Red Hat Inc' +project = 'Podman Python SDK' +copyright = '2021, Red Hat Inc' +author = 'Red Hat Inc' # The full version, including alpha/beta/rc tags version = '3.2.1.0' diff --git a/docs/source/index.rst b/docs/source/index.rst index d254b58d..fd729289 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -74,4 +74,4 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` -* :ref:`search` \ No newline at end of file +* :ref:`search` diff --git a/podman/api/typing_extensions.py b/podman/api/typing_extensions.py index ff698f4c..ebddfa27 100644 --- a/podman/api/typing_extensions.py +++ b/podman/api/typing_extensions.py @@ -1604,6 +1604,7 @@ class GenProto(Protocol[T]): def meth(self) -> T: ... """ + __slots__ = () _is_protocol = True diff --git a/podman/domain/volumes.py b/podman/domain/volumes.py index 842ed7a4..6867d5c8 100644 --- a/podman/domain/volumes.py +++ b/podman/domain/volumes.py @@ -112,7 +112,8 @@ def list(self, *_, **kwargs) -> List[Volume]: return [self.prepare_model(i) for i in response.json()] def prune( - self, filters: Optional[Dict[str, str]] = None # pylint: disable=unused-argument + self, + filters: Optional[Dict[str, str]] = None, # pylint: disable=unused-argument ) -> Dict[Literal["VolumesDeleted", "SpaceReclaimed"], Any]: """Delete unused volumes. diff --git a/podman/tests/README.md b/podman/tests/README.md index ad5e01c0..ad35f5d8 100644 --- a/podman/tests/README.md +++ b/podman/tests/README.md @@ -7,4 +7,3 @@ ## Coverage Reporting Framework `coverage.py` see https://coverage.readthedocs.io/en/coverage-5.0.3/#quick-start - diff --git a/podman/tests/integration/base.py b/podman/tests/integration/base.py index d79711d9..3086730f 100644 --- a/podman/tests/integration/base.py +++ b/podman/tests/integration/base.py @@ -13,6 +13,7 @@ # under the License. # """Base integration test code""" + import logging import os import shutil diff --git a/podman/tests/integration/test_container_create.py b/podman/tests/integration/test_container_create.py index 45f2f202..376f9282 100644 --- a/podman/tests/integration/test_container_create.py +++ b/podman/tests/integration/test_container_create.py @@ -57,7 +57,7 @@ def test_container_directory_volume_mount(self): with self.subTest("Check bind mount"): volumes = { "/etc/hosts": dict(bind="/test_ro", mode='ro'), - "/etc/hosts": dict(bind="/test_rw", mode='rw'), + "/etc/hosts": dict(bind="/test_rw", mode='rw'), # noqa: F601 } container = self.client.containers.create( self.alpine_image, command=["cat", "/test_ro", "/test_rw"], volumes=volumes diff --git a/podman/tests/integration/test_images.py b/podman/tests/integration/test_images.py index 298d13ec..91db11a0 100644 --- a/podman/tests/integration/test_images.py +++ b/podman/tests/integration/test_images.py @@ -13,6 +13,7 @@ # under the License. # """Images integration tests.""" + import io import queue import tarfile diff --git a/podman/tests/integration/test_networks.py b/podman/tests/integration/test_networks.py index 0b5d44d1..c034ca8d 100644 --- a/podman/tests/integration/test_networks.py +++ b/podman/tests/integration/test_networks.py @@ -13,6 +13,7 @@ # under the License. # """Network integration tests.""" + import os import random import unittest diff --git a/podman/tests/integration/utils.py b/podman/tests/integration/utils.py index 78aea63a..262bf86e 100644 --- a/podman/tests/integration/utils.py +++ b/podman/tests/integration/utils.py @@ -13,6 +13,7 @@ # under the License. # """Integration Test Utils""" + import logging import os import shutil @@ -97,9 +98,7 @@ def consume_lines(pipe, consume_fn): def consume(line: str): logger.debug(line.strip("\n") + f" refid={self.reference_id}") - self.proc = subprocess.Popen( - self.cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT - ) # pylint: disable=consider-using-with + self.proc = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) # pylint: disable=consider-using-with threading.Thread(target=consume_lines, args=[self.proc.stdout, consume]).start() if not check_socket: diff --git a/podman/tests/unit/test_imagesmanager.py b/podman/tests/unit/test_imagesmanager.py index 4906daf2..cab896c7 100644 --- a/podman/tests/unit/test_imagesmanager.py +++ b/podman/tests/unit/test_imagesmanager.py @@ -627,7 +627,8 @@ def test_list_with_name_overrides_reference_filter(self, mock): # The name parameter should override the reference filter images = self.client.images.list( - name="fedora", filters={"reference": "ubuntu"} # This should be overridden + name="fedora", + filters={"reference": "ubuntu"}, # This should be overridden ) self.assertEqual(len(images), 1) diff --git a/pyproject.toml b/pyproject.toml index 9db36747..5361b2fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,4 +30,3 @@ log_cli = true log_cli_level = "DEBUG" log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" log_cli_date_format = "%Y-%m-%d %H:%M:%S" - From ebf9ce6b9d3bed52f253fd05b0678cca2460bed1 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Mon, 9 Dec 2024 16:26:20 +0100 Subject: [PATCH 14/48] Update docs Signed-off-by: Nicola Sella --- CONTRIBUTING.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b63672cc..d0fd2751 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,9 +25,9 @@ Please don't include any private/sensitive information in your issue! ## Tools we use -- Python 3.6 -- [pylint](https://www.pylint.org/) -- [black](https://github.com/psf/black) +- Python >= 3.9 +- [pre-commit](https://pre-commit.com/) +- [ruff](https://docs.astral.sh/ruff/) - [tox](https://tox.readthedocs.io/en/latest/) - You may need to use [virtualenv](https://virtualenv.pypa.io/en/latest/) to support Python 3.6 @@ -65,10 +65,12 @@ tox -e coverage ## Coding conventions -- Use [black](https://github.com/psf/black) code formatter. If you have tox - installed, run `tox -e black` to see what changes will be made. You can use - `tox -e black-format` to update the code formatting prior to committing. -- Pass pylint +- Formatting and linting are incorporated using [ruff](https://docs.astral.sh/ruff/). +- If you use [pre-commit](https://pre-commit.com/) the checks will run automatically when you commit some changes +- If you prefer to run the ckecks with pre-commit, use `pre-commit run -a` to run the pre-commit checks for you. +- If you'd like to see what's happening with the checks you can run the [linter](https://docs.astral.sh/ruff/linter/) + and [formatter](https://docs.astral.sh/ruff/formatter/) separately with `ruff check --diff` and `ruff format --diff` +- Checks need to pass pylint - exceptions are possible, but you will need to make a good argument - Use spaces not tabs for indentation - This is open source software. Consider the people who will read your code, From 11a606967e8e188377ecef67c9e512b4d3d263fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 23:31:03 +0000 Subject: [PATCH 15/48] [skip-ci] Update pre-commit/action action to v3.0.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pre-commit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index c6ece29f..988a2932 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -15,4 +15,4 @@ jobs: python-version: | 3.9 3.x - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 From d2cdfc701652c84c6a26bcc7aaaf75f3204cefac Mon Sep 17 00:00:00 2001 From: Jhon Honce Date: Thu, 12 Dec 2024 10:47:16 -0700 Subject: [PATCH 16/48] Add edward5hen as reviewer Signed-off-by: Jhon Honce --- OWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/OWNERS b/OWNERS index 8ee65598..eaae8c06 100644 --- a/OWNERS +++ b/OWNERS @@ -14,3 +14,4 @@ reviewers: - baude - rhatdan - TomSweeneyRedHat + - Edward5hen From e876b07a402d0c6eb88e04bc85b54a41bdce1e8e Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Fri, 13 Dec 2024 16:40:58 +0100 Subject: [PATCH 17/48] update CI images build from https://github.com/containers/automation_images/pull/396 Signed-off-by: Paul Holzinger --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 5dc87f7c..a10be912 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -15,7 +15,7 @@ env: #### Cache-image names to test with (double-quotes around names are critical) #### # Google-cloud VM Images - IMAGE_SUFFIX: "c20241015t085508z-f40f39d13" + IMAGE_SUFFIX: "c20241212t122344z-f41f40d13" FEDORA_CACHE_IMAGE_NAME: "fedora-podman-py-${IMAGE_SUFFIX}" From f8324c2e0a466e2899feb4bd05b36774fd4b8fd5 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Fri, 13 Dec 2024 16:41:32 +0100 Subject: [PATCH 18/48] cirrus: replace dnf command The new images updated to f41 and thus contain dnf5, dnf erase is no longer valid so just call dnf remove to remove the package. Signed-off-by: Paul Holzinger --- contrib/cirrus/build_podman.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/cirrus/build_podman.sh b/contrib/cirrus/build_podman.sh index 87218d71..224d2e93 100755 --- a/contrib/cirrus/build_podman.sh +++ b/contrib/cirrus/build_podman.sh @@ -4,6 +4,6 @@ set -xeo pipefail systemctl stop podman.socket || : -dnf erase podman -y +dnf remove podman -y dnf copr enable rhcontainerbot/podman-next -y dnf install podman -y From 27b4be6200bed47422679fd1902d84075d6da164 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 19 Dec 2024 18:03:10 +0100 Subject: [PATCH 19/48] Support uppercase mount attributes Signed-off-by: Antonio --- podman/domain/containers_create.py | 8 ++++--- .../integration/test_container_create.py | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/podman/domain/containers_create.py b/podman/domain/containers_create.py index b6b1fadd..b2d5b824 100644 --- a/podman/domain/containers_create.py +++ b/podman/domain/containers_create.py @@ -553,11 +553,12 @@ def to_bytes(size: Union[int, str, None]) -> Union[int, None]: args.pop("log_config") for item in args.pop("mounts", []): + normalized_item = {key.lower(): value for key, value in item.items()} mount_point = { - "destination": item.get("target"), + "destination": normalized_item.get("target"), "options": [], - "source": item.get("source"), - "type": item.get("type"), + "source": normalized_item.get("source"), + "type": normalized_item.get("type"), } # some names are different for podman-py vs REST API due to compatibility with docker @@ -570,6 +571,7 @@ def to_bytes(size: Union[int, str, None]) -> Union[int, None]: regular_options = ["consistency", "mode", "size"] for k, v in item.items(): + k = k.lower() option_name = names_dict.get(k, k) if k in bool_options and v is True: options.append(option_name) diff --git a/podman/tests/integration/test_container_create.py b/podman/tests/integration/test_container_create.py index 376f9282..2c4b56d9 100644 --- a/podman/tests/integration/test_container_create.py +++ b/podman/tests/integration/test_container_create.py @@ -288,6 +288,29 @@ def test_container_mounts(self): ) ) + with self.subTest("Check uppercase mount option attributes"): + mount = { + "TypE": "bind", + "SouRce": "/etc/hosts", + "TarGet": "/test", + "Read_Only": True, + "ReLabel": "Z", + } + container = self.client.containers.create( + self.alpine_image, command=["cat", "/test"], mounts=[mount] + ) + self.containers.append(container) + self.assertIn( + f"{mount['SouRce']}:{mount['TarGet']}:ro,Z,rprivate,rbind", + container.attrs.get('HostConfig', {}).get('Binds', list()), + ) + + # check if container can be started and exits with EC == 0 + container.start() + container.wait() + + self.assertEqual(container.attrs.get('State', dict()).get('ExitCode', 256), 0) + def test_container_devices(self): devices = ["/dev/null:/dev/foo", "/dev/zero:/dev/bar"] container = self.client.containers.create( From 11101c2cbce39abb27238152a47960f4d37910ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:29:20 +0000 Subject: [PATCH 20/48] chore(deps): update dependency containers/automation_images to v20250107 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index a10be912..a51b805b 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -15,7 +15,7 @@ env: #### Cache-image names to test with (double-quotes around names are critical) #### # Google-cloud VM Images - IMAGE_SUFFIX: "c20241212t122344z-f41f40d13" + IMAGE_SUFFIX: "c20250107t132430z-f41f40d13" FEDORA_CACHE_IMAGE_NAME: "fedora-podman-py-${IMAGE_SUFFIX}" From 9a0fb3b31f483c7ab55d45dc89a262dfba6cb33e Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 19 Dec 2024 05:00:37 +0100 Subject: [PATCH 21/48] Implement "decode" parameter in pull() Implement `decode (bool)` parameter in `pull()`. Decode the JSON data from the server into dicts. Only applies with `stream=True`. Signed-off-by: Antonio --- podman/domain/images_manager.py | 26 ++++++++- podman/domain/json_stream.py | 74 +++++++++++++++++++++++++ podman/errors/__init__.py | 2 + podman/errors/exceptions.py | 5 ++ podman/tests/integration/test_images.py | 4 ++ 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 podman/domain/json_stream.py diff --git a/podman/domain/images_manager.py b/podman/domain/images_manager.py index 810e92f6..cc997331 100644 --- a/podman/domain/images_manager.py +++ b/podman/domain/images_manager.py @@ -14,6 +14,7 @@ from podman.api.http_utils import encode_auth_header from podman.domain.images import Image from podman.domain.images_build import BuildMixin +from podman.domain.json_stream import json_stream from podman.domain.manager import Manager from podman.domain.registry_data import RegistryData from podman.errors import APIError, ImageNotFound, PodmanError @@ -323,6 +324,8 @@ def pull( auth_config (Mapping[str, str]) – Override the credentials that are found in the config for this request. auth_config should contain the username and password keys to be valid. + decode (bool) – Decode the JSON data from the server into dicts. + Only applies with ``stream=True`` platform (str) – Platform in the format os[/arch[/variant]] progress_bar (bool) - Display a progress bar with the image pull progress (uses the compat endpoint). Default: False @@ -404,7 +407,7 @@ def pull( return None if stream: - return response.iter_lines() + return self._stream_helper(response, decode=kwargs.get("decode")) for item in response.iter_lines(): obj = json.loads(item) @@ -541,3 +544,24 @@ def scp( response = self.client.post(f"/images/scp/{source}", params=params) response.raise_for_status() return response.json() + + def _stream_helper(self, response, decode=False): + """Generator for data coming from a chunked-encoded HTTP response.""" + + if response.raw._fp.chunked: + if decode: + yield from json_stream(self._stream_helper(response, False)) + else: + reader = response.raw + while not reader.closed: + # this read call will block until we get a chunk + data = reader.read(1) + if not data: + break + if reader._fp.chunk_left: + data += reader.read(reader._fp.chunk_left) + yield data + else: + # Response isn't chunked, meaning we probably + # encountered an error immediately + yield self._result(response, json=decode) diff --git a/podman/domain/json_stream.py b/podman/domain/json_stream.py new file mode 100644 index 00000000..6978fc2f --- /dev/null +++ b/podman/domain/json_stream.py @@ -0,0 +1,74 @@ +import json +import json.decoder + +from podman.errors import StreamParseError + +json_decoder = json.JSONDecoder() + + +def stream_as_text(stream): + """ + Given a stream of bytes or text, if any of the items in the stream + are bytes convert them to text. + This function can be removed once we return text streams + instead of byte streams. + """ + for data in stream: + if not isinstance(data, str): + data = data.decode('utf-8', 'replace') + yield data + + +def json_splitter(buffer): + """Attempt to parse a json object from a buffer. If there is at least one + object, return it and the rest of the buffer, otherwise return None. + """ + buffer = buffer.strip() + try: + obj, index = json_decoder.raw_decode(buffer) + rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end() :] + return obj, rest + except ValueError: + return None + + +def json_stream(stream): + """Given a stream of text, return a stream of json objects. + This handles streams which are inconsistently buffered (some entries may + be newline delimited, and others are not). + """ + return split_buffer(stream, json_splitter, json_decoder.decode) + + +def line_splitter(buffer, separator='\n'): + index = buffer.find(str(separator)) + if index == -1: + return None + return buffer[: index + 1], buffer[index + 1 :] + + +def split_buffer(stream, splitter=None, decoder=lambda a: a): + """Given a generator which yields strings and a splitter function, + joins all input, splits on the separator and yields each chunk. + Unlike string.split(), each chunk includes the trailing + separator, except for the last one if none was found on the end + of the input. + """ + splitter = splitter or line_splitter + buffered = '' + + for data in stream_as_text(stream): + buffered += data + while True: + buffer_split = splitter(buffered) + if buffer_split is None: + break + + item, buffered = buffer_split + yield item + + if buffered: + try: + yield decoder(buffered) + except Exception as e: + raise StreamParseError(e) from e diff --git a/podman/errors/__init__.py b/podman/errors/__init__.py index 49a65157..9a339112 100644 --- a/podman/errors/__init__.py +++ b/podman/errors/__init__.py @@ -21,6 +21,7 @@ 'NotFound', 'NotFoundError', 'PodmanError', + 'StreamParseError', ] try: @@ -32,6 +33,7 @@ InvalidArgument, NotFound, PodmanError, + StreamParseError, ) except ImportError: pass diff --git a/podman/errors/exceptions.py b/podman/errors/exceptions.py index 7cf5970a..ef3af2a0 100644 --- a/podman/errors/exceptions.py +++ b/podman/errors/exceptions.py @@ -142,3 +142,8 @@ def __init__( class InvalidArgument(PodmanError): """Parameter to method/function was not valid.""" + + +class StreamParseError(RuntimeError): + def __init__(self, reason): + self.msg = reason diff --git a/podman/tests/integration/test_images.py b/podman/tests/integration/test_images.py index 91db11a0..19470168 100644 --- a/podman/tests/integration/test_images.py +++ b/podman/tests/integration/test_images.py @@ -151,6 +151,10 @@ def test_pull_stream(self): generator = self.client.images.pull("ubi8", tag="latest", stream=True) self.assertIsInstance(generator, types.GeneratorType) + def test_pull_stream_decode(self): + generator = self.client.images.pull("ubi8", tag="latest", stream=True, decode=True) + self.assertIsInstance(generator, types.GeneratorType) + def test_scp(self): with self.assertRaises(APIError) as e: next( From 99a7296f089aa90a12f750f478038b9fa7a2bfed Mon Sep 17 00:00:00 2001 From: Riccardo Paolo Bestetti Date: Sun, 5 Jan 2025 17:55:20 +0100 Subject: [PATCH 22/48] Add support for container initialization This commit contributes support for container initialization (i.e., the operation performed by `podman container init`.) Alongside that, it introduces: - unit test ContainersTestCase::test_init - integration subtest `Create-Init-Start Container` in ContainersIntegrationTest::test_container_crud A small fix to the docstring of Container.status has also been contributed to reflect the existance of the `created` and `initialized` states. Signed-off-by: Riccardo Paolo Bestetti --- podman/domain/containers.py | 7 ++++++- podman/tests/integration/test_containers.py | 18 ++++++++++++++++++ podman/tests/unit/test_container.py | 11 +++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/podman/domain/containers.py b/podman/domain/containers.py index c99ed926..f603a9c6 100644 --- a/podman/domain/containers.py +++ b/podman/domain/containers.py @@ -54,7 +54,7 @@ def labels(self): @property def status(self): - """Literal["running", "stopped", "exited", "unknown"]: Returns status of container.""" + """Literal["created", "initialized", "running", "stopped", "exited", "unknown"]: Returns status of container.""" with suppress(KeyError): return self.attrs["State"]["Status"] return "unknown" @@ -262,6 +262,11 @@ def get_archive( stat = api.decode_header(stat) return response.iter_content(chunk_size=chunk_size), stat + def init(self) -> None: + """Initialize the container.""" + response = self.client.post(f"/containers/{self.id}/init") + response.raise_for_status() + def inspect(self) -> Dict: """Inspect a container. diff --git a/podman/tests/integration/test_containers.py b/podman/tests/integration/test_containers.py index d92a7d7a..11b7ea81 100644 --- a/podman/tests/integration/test_containers.py +++ b/podman/tests/integration/test_containers.py @@ -143,6 +143,24 @@ def test_container_crud(self): top_ctnr.reload() self.assertIn(top_ctnr.status, ("exited", "stopped")) + with self.subTest("Create-Init-Start Container"): + top_ctnr = self.client.containers.create( + self.alpine_image, ["/usr/bin/top"], name="TestInitPs", detach=True + ) + self.assertEqual(top_ctnr.status, "created") + + top_ctnr.init() + top_ctnr.reload() + self.assertEqual(top_ctnr.status, "initialized") + + top_ctnr.start() + top_ctnr.reload() + self.assertEqual(top_ctnr.status, "running") + + top_ctnr.stop() + top_ctnr.reload() + self.assertIn(top_ctnr.status, ("exited", "stopped")) + with self.subTest("Prune Containers"): report = self.client.containers.prune() self.assertIn(top_ctnr.id, report["ContainersDeleted"]) diff --git a/podman/tests/unit/test_container.py b/podman/tests/unit/test_container.py index 5d0023e0..f2c41601 100644 --- a/podman/tests/unit/test_container.py +++ b/podman/tests/unit/test_container.py @@ -101,6 +101,17 @@ def test_start(self, mock): container.start() self.assertTrue(adapter.called_once) + @requests_mock.Mocker() + def test_init(self, mock): + adapter = mock.post( + tests.LIBPOD_URL + + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/init", + status_code=204, + ) + container = Container(attrs=FIRST_CONTAINER, client=self.client.api) + container.init() + self.assertTrue(adapter.called_once) + @requests_mock.Mocker() def test_stats(self, mock): stream = [ From 10d9c0a2e848f242c80b8c429a15014621ad8d7e Mon Sep 17 00:00:00 2001 From: Riccardo Paolo Bestetti Date: Fri, 10 Jan 2025 18:51:22 +0100 Subject: [PATCH 23/48] fix: accept a string for the `command` argument of Container.start This makes its interface consistent with its own documentation, and with Container.run. Signed-off-by: Riccardo Paolo Bestetti --- podman/domain/containers_create.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/podman/domain/containers_create.py b/podman/domain/containers_create.py index b2d5b824..3bdfcb0b 100644 --- a/podman/domain/containers_create.py +++ b/podman/domain/containers_create.py @@ -344,6 +344,8 @@ def create( """ if isinstance(image, Image): image = image.id + if isinstance(command, str): + command = [command] payload = {"image": image, "command": command} payload.update(kwargs) From c79b7a9d4738278ac332a88e13a53ca45c8c6527 Mon Sep 17 00:00:00 2001 From: Riccardo Paolo Bestetti Date: Fri, 10 Jan 2025 19:12:58 +0100 Subject: [PATCH 24/48] Remove the `remove` flag from docstring of Container.create() This commit removes the `remove` flag from the docstring entirely, as the create() operation doesn't support the `remove` flag. It is tolerated as an input because run() supports it and it internally calls the start() operation relaying its own kwargs. Signed-off-by: Riccardo Paolo Bestetti --- podman/domain/containers_create.py | 1 - 1 file changed, 1 deletion(-) diff --git a/podman/domain/containers_create.py b/podman/domain/containers_create.py index b2d5b824..c94171d5 100644 --- a/podman/domain/containers_create.py +++ b/podman/domain/containers_create.py @@ -225,7 +225,6 @@ def create( read_only (bool): Mount the container's root filesystem as read only. read_write_tmpfs (bool): Mount temporary file systems as read write, in case of read_only options set to True. Default: False - remove (bool): Remove the container when it has finished running. Default: False. restart_policy (Dict[str, Union[str, int]]): Restart the container when it exits. Configured as a dictionary with keys: From 7eaad537bcb811b38fb3e03ad2aab7ba71f23a89 Mon Sep 17 00:00:00 2001 From: Riccardo Paolo Bestetti Date: Fri, 10 Jan 2025 19:14:59 +0100 Subject: [PATCH 25/48] Update documentation for the `remove` flag in Container.run() This commit specifies more in depth the semantics of the `remove` flag of the run() operation: - it describes its interaction with detach=True - it clarifies that it is a client-initiated operation - it specifies that a similar daemon-side flag also exists with the name of `auto_remove` Signed-off-by: Riccardo Paolo Bestetti --- podman/domain/containers_run.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/podman/domain/containers_run.py b/podman/domain/containers_run.py index 0806994a..c20d807a 100644 --- a/podman/domain/containers_run.py +++ b/podman/domain/containers_run.py @@ -30,14 +30,17 @@ def run( By default, run() will wait for the container to finish and return its logs. If detach=True, run() will start the container and return a Container object rather - than logs. + than logs. In this case, if remove=True, run() will monitor and remove the + container after it finishes running; the logs will be lost in this case. Args: image: Image to run. command: Command to run in the container. stdout: Include stdout. Default: True. stderr: Include stderr. Default: False. - remove: Delete container when the container's processes exit. Default: False. + remove: Delete container on the client side when the container's processes exit. + The `auto_remove` flag is also available to manage the removal on the daemon + side. Default: False. Keyword Args: - See the create() method for keyword arguments. From 4bd2d33e1022ecc0708c53ee8dc2cb380357952a Mon Sep 17 00:00:00 2001 From: Antonio Date: Wed, 15 Jan 2025 16:21:08 +0100 Subject: [PATCH 26/48] Add compatMode raw JSON output and fix tls_verify init on pull() Signed-off-by: Antonio --- podman/domain/images_manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/podman/domain/images_manager.py b/podman/domain/images_manager.py index cc997331..ce2c641f 100644 --- a/podman/domain/images_manager.py +++ b/podman/domain/images_manager.py @@ -324,6 +324,8 @@ def pull( auth_config (Mapping[str, str]) – Override the credentials that are found in the config for this request. auth_config should contain the username and password keys to be valid. + compatMode (bool) – Return the same JSON payload as the Docker-compat endpoint. + Default: True. decode (bool) – Decode the JSON data from the server into dicts. Only applies with ``stream=True`` platform (str) – Platform in the format os[/arch[/variant]] @@ -357,7 +359,8 @@ def pull( params = { "reference": repository, - "tlsVerify": kwargs.get("tls_verify"), + "tlsVerify": kwargs.get("tls_verify", True), + "compatMode": kwargs.get("compatMode", True), } if all_tags: @@ -409,7 +412,7 @@ def pull( if stream: return self._stream_helper(response, decode=kwargs.get("decode")) - for item in response.iter_lines(): + for item in reversed(list(response.iter_lines())): obj = json.loads(item) if all_tags and "images" in obj: images: List[Image] = [] From cb5a3f617044540b46c043d62813727339abadc2 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Wed, 22 Jan 2025 18:00:28 +0100 Subject: [PATCH 27/48] Honor port numbers in urls for image.pull Fixes: https://issues.redhat.com/browse/RUN-2313 Signed-off-by: Nicola Sella --- podman/api/parse_utils.py | 4 +++- podman/domain/images_manager.py | 8 ++++---- podman/tests/unit/test_parse_utils.py | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/podman/api/parse_utils.py b/podman/api/parse_utils.py index c07762ea..6259ec13 100644 --- a/podman/api/parse_utils.py +++ b/podman/api/parse_utils.py @@ -24,7 +24,9 @@ def parse_repository(name: str) -> Tuple[str, Optional[str]]: return elements[0], elements[1] # split repository and image name from tag - elements = name.split(":", 1) + # tags need to be split from the right since + # a port number might increase the split list len by 1 + elements = name.rsplit(":", 1) if len(elements) == 2 and "/" not in elements[1]: return elements[0], elements[1] diff --git a/podman/domain/images_manager.py b/podman/domain/images_manager.py index ce2c641f..b7fcdf74 100644 --- a/podman/domain/images_manager.py +++ b/podman/domain/images_manager.py @@ -11,6 +11,7 @@ from podman import api from podman.api import Literal +from podman.api.parse_utils import parse_repository from podman.api.http_utils import encode_auth_header from podman.domain.images import Image from podman.domain.images_build import BuildMixin @@ -343,10 +344,9 @@ def pull( APIError: when service returns an error """ if tag is None or len(tag) == 0: - tokens = repository.split(":") - if len(tokens) == 2: - repository = tokens[0] - tag = tokens[1] + repository, parsed_tag = parse_repository(repository) + if parsed_tag is not None: + tag = parsed_tag else: tag = "latest" diff --git a/podman/tests/unit/test_parse_utils.py b/podman/tests/unit/test_parse_utils.py index a7768deb..5468272a 100644 --- a/podman/tests/unit/test_parse_utils.py +++ b/podman/tests/unit/test_parse_utils.py @@ -36,6 +36,21 @@ class TestCase: input="quay.io/libpod/testimage:latest", expected=("quay.io/libpod/testimage", "latest"), ), + TestCase( + name=":port", + input="quay.io:5000/libpod/testimage", + expected=("quay.io:5000/libpod/testimage", None), + ), + TestCase( + name=":port@digest", + input="quay.io:5000/libpod/testimage@71f1b47263fc", + expected=("quay.io:5000/libpod/testimage", "71f1b47263fc"), + ), + TestCase( + name=":port:tag", + input="quay.io:5000/libpod/testimage:latest", + expected=("quay.io:5000/libpod/testimage", "latest"), + ), ] for case in cases: From 06b5dd9d336c462877e3fe5533b6ecb1d6e3277d Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 12:48:54 +0100 Subject: [PATCH 28/48] Fix Pycodestyle E722: bare-except Catching BaseException can make it hard to interrupt the program (e.g., with Ctrl-C) and can disguise other problems. Signed-off-by: Nicola Sella --- podman/domain/config.py | 2 +- podman/tests/integration/test_containers.py | 2 +- podman/tests/unit/test_build.py | 2 +- podman/tests/unit/test_container.py | 2 +- podman/tests/unit/test_containersmanager.py | 2 +- podman/tests/unit/test_imagesmanager.py | 2 +- ruff.toml | 1 - 7 files changed, 6 insertions(+), 7 deletions(-) diff --git a/podman/domain/config.py b/podman/domain/config.py index f683d446..02cc5113 100644 --- a/podman/domain/config.py +++ b/podman/domain/config.py @@ -87,7 +87,7 @@ def __init__(self, path: Optional[str] = None): try: with open(self.path, encoding='utf-8') as file: self.attrs = json.load(file) - except: # pylint: disable=bare-except + except Exception as ex: # pylint: disable=bare-except # if the user specifies a path, it can either be a JSON file # or a TOML file - so try TOML next try: diff --git a/podman/tests/integration/test_containers.py b/podman/tests/integration/test_containers.py index 11b7ea81..dd596693 100644 --- a/podman/tests/integration/test_containers.py +++ b/podman/tests/integration/test_containers.py @@ -7,7 +7,7 @@ try: # Python >= 3.10 from collections.abc import Iterator -except: +except ImportError: # Python < 3.10 from collections import Iterator diff --git a/podman/tests/unit/test_build.py b/podman/tests/unit/test_build.py index a12187ea..b29371f8 100644 --- a/podman/tests/unit/test_build.py +++ b/podman/tests/unit/test_build.py @@ -5,7 +5,7 @@ try: # Python >= 3.10 from collections.abc import Iterable -except: +except ImportError: # Python < 3.10 from collections import Iterable from unittest.mock import patch diff --git a/podman/tests/unit/test_container.py b/podman/tests/unit/test_container.py index f2c41601..154c8801 100644 --- a/podman/tests/unit/test_container.py +++ b/podman/tests/unit/test_container.py @@ -6,7 +6,7 @@ try: # Python >= 3.10 from collections.abc import Iterable -except: +except ImportError: # Python < 3.10 from collections import Iterable diff --git a/podman/tests/unit/test_containersmanager.py b/podman/tests/unit/test_containersmanager.py index cb0ec167..5e9852f2 100644 --- a/podman/tests/unit/test_containersmanager.py +++ b/podman/tests/unit/test_containersmanager.py @@ -4,7 +4,7 @@ try: # Python >= 3.10 from collections.abc import Iterator -except: +except ImportError: # Python < 3.10 from collections import Iterator diff --git a/podman/tests/unit/test_imagesmanager.py b/podman/tests/unit/test_imagesmanager.py index a98c22c6..9a9f963e 100644 --- a/podman/tests/unit/test_imagesmanager.py +++ b/podman/tests/unit/test_imagesmanager.py @@ -5,7 +5,7 @@ try: # Python >= 3.10 from collections.abc import Iterable -except: +except ImportError: # Python < 3.10 from collections import Iterable diff --git a/ruff.toml b/ruff.toml index 02b60ac5..266bbf21 100644 --- a/ruff.toml +++ b/ruff.toml @@ -37,7 +37,6 @@ ignore = [ "F841", # TODO Local variable is assigned to but never used "E402", # TODO Module level import not at top of file "E741", # TODO ambiguous variable name - "E722", # TODO do not use bare 'except' "E501", # TODO line too long "N818", # TODO Error Suffix in exception name "N80", # TODO Invalid Name From 2e15b456492bd63039b2e19126192f6312c71b6e Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 12:51:32 +0100 Subject: [PATCH 29/48] Fix Pycodestyle E741: ambiguous-variable-name Checks for the use of the characters 'l', 'O', or 'I' as variable names. Signed-off-by: Nicola Sella --- podman/api/tar_utils.py | 2 +- ruff.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/podman/api/tar_utils.py b/podman/api/tar_utils.py index 950cc44f..b77a2353 100644 --- a/podman/api/tar_utils.py +++ b/podman/api/tar_utils.py @@ -24,7 +24,7 @@ def prepare_containerignore(anchor: str) -> List[str]: with ignore.open(encoding='utf-8') as file: return list( filter( - lambda l: l and not l.startswith("#"), + lambda L: L and not L.startswith("#"), (line.strip() for line in file.readlines()), ) ) diff --git a/ruff.toml b/ruff.toml index 266bbf21..dfa384d1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -36,7 +36,6 @@ ignore = [ "F401", # TODO Module imported but unused "F841", # TODO Local variable is assigned to but never used "E402", # TODO Module level import not at top of file - "E741", # TODO ambiguous variable name "E501", # TODO line too long "N818", # TODO Error Suffix in exception name "N80", # TODO Invalid Name From e0524b1beedeb42aaba7c54d1195e4cf560459f8 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 12:52:14 +0100 Subject: [PATCH 30/48] Fix Pycodestyle E402, Pyflakes F401 and Bandit S101 F401: unused-import E402: module-import-not-at-top-of-file S101: assert Assertions are removed when Python is run with optimization requested (i.e., when the -O flag is present), which is a common practice in production environments. As such, assertions should not be used for runtime validation of user input or to enforce interface constraints. Signed-off-by: Nicola Sella Unused import Signed-off-by: Nicola Sella --- podman/__init__.py | 5 +++-- podman/api/client.py | 2 +- podman/tests/integration/test_adapters.py | 4 ++-- podman/tests/integration/test_container_exec.py | 2 -- podman/tests/integration/test_images.py | 6 +----- podman/tests/integration/test_networks.py | 1 - podman/tests/unit/test_api_utils.py | 2 +- podman/tests/unit/test_containersmanager.py | 2 +- podman/tests/unit/test_imagesmanager.py | 2 +- podman/tests/unit/test_path_utils.py | 1 - ruff.toml | 1 - 11 files changed, 10 insertions(+), 18 deletions(-) diff --git a/podman/__init__.py b/podman/__init__.py index 3f259ea2..826405e1 100644 --- a/podman/__init__.py +++ b/podman/__init__.py @@ -2,10 +2,11 @@ import sys -assert sys.version_info >= (3, 6), "Python 3.6 or greater is required." - from podman.client import PodmanClient, from_env from podman.version import __version__ +if sys.version_info < (3, 9): + raise ImportError("Python 3.6 or greater is required.") + # isort: unique-list __all__ = ['PodmanClient', '__version__', 'from_env'] diff --git a/podman/api/client.py b/podman/api/client.py index 062c3cad..f38e83f8 100644 --- a/podman/api/client.py +++ b/podman/api/client.py @@ -156,7 +156,7 @@ def __init__( self.mount("http://", HTTPAdapter(**http_adapter_kwargs)) self.mount("https://", HTTPAdapter(**http_adapter_kwargs)) else: - assert False, "APIClient.supported_schemes changed without adding a branch here." + raise PodmanError("APIClient.supported_schemes changed without adding a branch here.") self.version = version or VERSION self.path_prefix = f"/v{self.version}/libpod/" diff --git a/podman/tests/integration/test_adapters.py b/podman/tests/integration/test_adapters.py index 598597cb..9b6a74cb 100644 --- a/podman/tests/integration/test_adapters.py +++ b/podman/tests/integration/test_adapters.py @@ -39,10 +39,10 @@ def test_tcp_ping(self): podman.start(check_socket=False) time.sleep(0.5) - with PodmanClient(base_url=f"tcp:localhost:8889") as client: + with PodmanClient(base_url="tcp:localhost:8889") as client: self.assertTrue(client.ping()) - with PodmanClient(base_url=f"http://localhost:8889") as client: + with PodmanClient(base_url="http://localhost:8889") as client: self.assertTrue(client.ping()) finally: podman.stop() diff --git a/podman/tests/integration/test_container_exec.py b/podman/tests/integration/test_container_exec.py index 43190797..71ece37e 100644 --- a/podman/tests/integration/test_container_exec.py +++ b/podman/tests/integration/test_container_exec.py @@ -1,5 +1,3 @@ -import unittest - import podman.tests.integration.base as base from podman import PodmanClient diff --git a/podman/tests/integration/test_images.py b/podman/tests/integration/test_images.py index 19470168..9f712f14 100644 --- a/podman/tests/integration/test_images.py +++ b/podman/tests/integration/test_images.py @@ -15,13 +15,9 @@ """Images integration tests.""" import io -import queue import tarfile -import threading import types import unittest -from contextlib import suppress -from datetime import datetime, timedelta import podman.tests.integration.base as base from podman import PodmanClient @@ -141,7 +137,7 @@ def test_corrupt_load(self): self.assertIn("payload does not match", e.exception.explanation) def test_build(self): - buffer = io.StringIO(f"""FROM quay.io/libpod/alpine_labels:latest""") + buffer = io.StringIO("""FROM quay.io/libpod/alpine_labels:latest""") image, stream = self.client.images.build(fileobj=buffer) self.assertIsNotNone(image) diff --git a/podman/tests/integration/test_networks.py b/podman/tests/integration/test_networks.py index c034ca8d..76e9b854 100644 --- a/podman/tests/integration/test_networks.py +++ b/podman/tests/integration/test_networks.py @@ -14,7 +14,6 @@ # """Network integration tests.""" -import os import random import unittest from contextlib import suppress diff --git a/podman/tests/unit/test_api_utils.py b/podman/tests/unit/test_api_utils.py index dcafc294..ea389143 100644 --- a/podman/tests/unit/test_api_utils.py +++ b/podman/tests/unit/test_api_utils.py @@ -3,7 +3,7 @@ import unittest from typing import Any, Optional from unittest import mock -from unittest.mock import Mock, mock_open, patch +from unittest.mock import mock_open, patch from dataclasses import dataclass diff --git a/podman/tests/unit/test_containersmanager.py b/podman/tests/unit/test_containersmanager.py index 5e9852f2..78e8bffd 100644 --- a/podman/tests/unit/test_containersmanager.py +++ b/podman/tests/unit/test_containersmanager.py @@ -8,7 +8,7 @@ # Python < 3.10 from collections import Iterator -from unittest.mock import ANY, DEFAULT, patch, MagicMock +from unittest.mock import DEFAULT, patch, MagicMock import requests_mock diff --git a/podman/tests/unit/test_imagesmanager.py b/podman/tests/unit/test_imagesmanager.py index 9a9f963e..ce85898d 100644 --- a/podman/tests/unit/test_imagesmanager.py +++ b/podman/tests/unit/test_imagesmanager.py @@ -1,6 +1,6 @@ import types import unittest -from unittest.mock import mock_open, patch +from unittest.mock import patch try: # Python >= 3.10 diff --git a/podman/tests/unit/test_path_utils.py b/podman/tests/unit/test_path_utils.py index eda2dd62..83ee217f 100644 --- a/podman/tests/unit/test_path_utils.py +++ b/podman/tests/unit/test_path_utils.py @@ -1,4 +1,3 @@ -import datetime import os import unittest import tempfile diff --git a/ruff.toml b/ruff.toml index dfa384d1..81513bdd 100644 --- a/ruff.toml +++ b/ruff.toml @@ -35,7 +35,6 @@ ignore = [ "F541", # TODO f-string is missing placeholders "F401", # TODO Module imported but unused "F841", # TODO Local variable is assigned to but never used - "E402", # TODO Module level import not at top of file "E501", # TODO line too long "N818", # TODO Error Suffix in exception name "N80", # TODO Invalid Name From ca9fdd6d1d634abd52d0c107f67a2db5d1370481 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 12:59:11 +0100 Subject: [PATCH 31/48] Fix Pyflakes F841: unused-variable Check for unused variables. Unused variables should be prefixed with '_' Signed-off-by: Nicola Sella --- podman/domain/config.py | 2 +- podman/tests/unit/test_containersmanager.py | 4 ++-- podman/tests/unit/test_events.py | 2 +- podman/tests/unit/test_networksmanager.py | 2 ++ podman/tests/unit/test_podmanclient.py | 2 +- ruff.toml | 3 --- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/podman/domain/config.py b/podman/domain/config.py index 02cc5113..c62d2067 100644 --- a/podman/domain/config.py +++ b/podman/domain/config.py @@ -87,7 +87,7 @@ def __init__(self, path: Optional[str] = None): try: with open(self.path, encoding='utf-8') as file: self.attrs = json.load(file) - except Exception as ex: # pylint: disable=bare-except + except Exception: # pylint: disable=bare-except # if the user specifies a path, it can either be a JSON file # or a TOML file - so try TOML next try: diff --git a/podman/tests/unit/test_containersmanager.py b/podman/tests/unit/test_containersmanager.py index 78e8bffd..76e06dc0 100644 --- a/podman/tests/unit/test_containersmanager.py +++ b/podman/tests/unit/test_containersmanager.py @@ -258,11 +258,11 @@ def test_create_parse_host_port(self, mock): self.assertEqual(expected_ports, actual_ports) def test_create_unsupported_key(self): - with self.assertRaises(TypeError) as e: + with self.assertRaises(TypeError): self.client.containers.create("fedora", "/usr/bin/ls", blkio_weight=100.0) def test_create_unknown_key(self): - with self.assertRaises(TypeError) as e: + with self.assertRaises(TypeError): self.client.containers.create("fedora", "/usr/bin/ls", unknown_key=100.0) @requests_mock.Mocker() diff --git a/podman/tests/unit/test_events.py b/podman/tests/unit/test_events.py index 2ac3a9a7..41638337 100644 --- a/podman/tests/unit/test_events.py +++ b/podman/tests/unit/test_events.py @@ -44,7 +44,7 @@ def test_list(self, mock): buffer.write(json.JSONEncoder().encode(item)) buffer.write("\n") - adapter = mock.get(tests.LIBPOD_URL + "/events", text=buffer.getvalue()) + adapter = mock.get(tests.LIBPOD_URL + "/events", text=buffer.getvalue()) # noqa: F841 manager = EventsManager(client=self.client.api) actual = manager.list(decode=True) diff --git a/podman/tests/unit/test_networksmanager.py b/podman/tests/unit/test_networksmanager.py index 1219bb54..483a8510 100644 --- a/podman/tests/unit/test_networksmanager.py +++ b/podman/tests/unit/test_networksmanager.py @@ -171,6 +171,8 @@ def test_create_defaults(self, mock): adapter = mock.post(tests.LIBPOD_URL + "/networks/create", json=FIRST_NETWORK_LIBPOD) network = self.client.networks.create("podman") + self.assertIsInstance(network, Network) + self.assertEqual(adapter.call_count, 1) self.assertDictEqual( adapter.last_request.json(), diff --git a/podman/tests/unit/test_podmanclient.py b/podman/tests/unit/test_podmanclient.py index 7456bad3..fdf8d344 100644 --- a/podman/tests/unit/test_podmanclient.py +++ b/podman/tests/unit/test_podmanclient.py @@ -59,7 +59,7 @@ def test_contextmanager(self, mock): "os": "linux", } } - adapter = mock.get(tests.LIBPOD_URL + "/info", json=body) + adapter = mock.get(tests.LIBPOD_URL + "/info", json=body) # noqa: F841 with PodmanClient(base_url=tests.BASE_SOCK) as client: actual = client.info() diff --git a/ruff.toml b/ruff.toml index 81513bdd..db9f20af 100644 --- a/ruff.toml +++ b/ruff.toml @@ -32,9 +32,6 @@ select = [ # to avoid changing too many lines ignore = [ "F821", # TODO Undefined name - "F541", # TODO f-string is missing placeholders - "F401", # TODO Module imported but unused - "F841", # TODO Local variable is assigned to but never used "E501", # TODO line too long "N818", # TODO Error Suffix in exception name "N80", # TODO Invalid Name From 961d5e0254a6e3fc08036111ba2933e8bd6926b8 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 13:20:39 +0100 Subject: [PATCH 32/48] Fix Pylint PLW2901: redefined-loop-name More on why it is bad here: https://docs.astral.sh/ruff/rules/redefined-loop-name/ Signed-off-by: Nicola Sella --- podman/api/http_utils.py | 6 +++--- podman/domain/containers_create.py | 22 ++++++++++++---------- podman/domain/json_stream.py | 5 +++-- podman/domain/manifests.py | 16 ++++++++++------ ruff.toml | 1 - 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/podman/api/http_utils.py b/podman/api/http_utils.py index 0f327c91..d1254940 100644 --- a/podman/api/http_utils.py +++ b/podman/api/http_utils.py @@ -42,12 +42,12 @@ def _format_dict(filters, criteria): for key, value in filters.items(): if value is None: continue - value = str(value) + str_value = str(value) if key in criteria: - criteria[key].append(value) + criteria[key].append(str_value) else: - criteria[key] = [value] + criteria[key] = [str_value] def _format_string(filters, criteria): diff --git a/podman/domain/containers_create.py b/podman/domain/containers_create.py index e85eafed..c160eb76 100644 --- a/podman/domain/containers_create.py +++ b/podman/domain/containers_create.py @@ -572,13 +572,13 @@ def to_bytes(size: Union[int, str, None]) -> Union[int, None]: regular_options = ["consistency", "mode", "size"] for k, v in item.items(): - k = k.lower() - option_name = names_dict.get(k, k) - if k in bool_options and v is True: + _k = k.lower() + option_name = names_dict.get(_k, _k) + if _k in bool_options and v is True: options.append(option_name) - elif k in regular_options: + elif _k in regular_options: options.append(f'{option_name}={v}') - elif k in simple_options: + elif _k in simple_options: options.append(v) mount_point["options"] = options @@ -627,13 +627,15 @@ def parse_host_port(_container_port, _protocol, _host): return result for container, host in args.pop("ports", {}).items(): - if isinstance(container, int): - container = str(container) + # avoid redefinition of the loop variable, then ensure it's a string + str_container = container + if isinstance(str_container, int): + str_container = str(str_container) - if "/" in container: - container_port, protocol = container.split("/") + if "/" in str_container: + container_port, protocol = str_container.split("/") else: - container_port, protocol = container, "tcp" + container_port, protocol = str_container, "tcp" port_map_list = parse_host_port(container_port, protocol, host) params["portmappings"].extend(port_map_list) diff --git a/podman/domain/json_stream.py b/podman/domain/json_stream.py index 6978fc2f..399e4295 100644 --- a/podman/domain/json_stream.py +++ b/podman/domain/json_stream.py @@ -14,9 +14,10 @@ def stream_as_text(stream): instead of byte streams. """ for data in stream: + _data = data if not isinstance(data, str): - data = data.decode('utf-8', 'replace') - yield data + _data = data.decode('utf-8', 'replace') + yield _data def json_splitter(buffer): diff --git a/podman/domain/manifests.py b/podman/domain/manifests.py index 98bd3a00..b150f8ad 100644 --- a/podman/domain/manifests.py +++ b/podman/domain/manifests.py @@ -82,9 +82,11 @@ def add(self, images: List[Union[Image, str]], **kwargs) -> None: "operation": "update", } for item in images: - if isinstance(item, Image): - item = item.attrs["RepoTags"][0] - data["images"].append(item) + # avoid redefinition of the loop variable, then ensure it's an image + img_item = item + if isinstance(img_item, Image): + img_item = img_item.attrs["RepoTags"][0] + data["images"].append(img_item) data = api.prepare_body(data) response = self.client.put(f"/manifests/{self.quoted_name}", data=data) @@ -169,9 +171,11 @@ def create( if images is not None: params["images"] = [] for item in images: - if isinstance(item, Image): - item = item.attrs["RepoTags"][0] - params["images"].append(item) + # avoid redefinition of the loop variable, then ensure it's an image + img_item = item + if isinstance(img_item, Image): + img_item = img_item.attrs["RepoTags"][0] + params["images"].append(img_item) if all is not None: params["all"] = all diff --git a/ruff.toml b/ruff.toml index db9f20af..04e6df19 100644 --- a/ruff.toml +++ b/ruff.toml @@ -36,7 +36,6 @@ ignore = [ "N818", # TODO Error Suffix in exception name "N80", # TODO Invalid Name "ANN10", # Missing type annotation - "PLW2901", # TODO Redefined Loop Name ] [lint.per-file-ignores] "podman/tests/*.py" = ["S"] From eac30c657e8bd87ba837d2e7f24cbb112d49d3b8 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 13:23:09 +0100 Subject: [PATCH 33/48] Add comments in ruff.toml Signed-off-by: Nicola Sella --- ruff.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 04e6df19..c41f9132 100644 --- a/ruff.toml +++ b/ruff.toml @@ -33,9 +33,11 @@ select = [ ignore = [ "F821", # TODO Undefined name "E501", # TODO line too long + # Some Exceptions such as NotFound and NotFoundError can be ambiguous + # This change need to be performed with carefulness "N818", # TODO Error Suffix in exception name + # This can lead to API breaking changes so it's disabled for now "N80", # TODO Invalid Name - "ANN10", # Missing type annotation ] [lint.per-file-ignores] "podman/tests/*.py" = ["S"] From baf076f3adad0aee07192397202b062b686ac11a Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 13:34:38 +0100 Subject: [PATCH 34/48] Fix Pycodestyle E501: line-too-long This is a quality of life improvement and it should be backward compatible with our previous set line-length Signed-off-by: Nicola Sella --- podman/domain/containers.py | 3 ++- podman/domain/containers_create.py | 24 ++++++++++++++++--- .../tests/integration/test_container_exec.py | 7 +++--- podman/tests/unit/test_container.py | 7 ++++-- podman/tests/unit/test_imagesmanager.py | 11 ++++++--- ruff.toml | 1 - 6 files changed, 40 insertions(+), 13 deletions(-) diff --git a/podman/domain/containers.py b/podman/domain/containers.py index f603a9c6..61930c9a 100644 --- a/podman/domain/containers.py +++ b/podman/domain/containers.py @@ -54,7 +54,8 @@ def labels(self): @property def status(self): - """Literal["created", "initialized", "running", "stopped", "exited", "unknown"]: Returns status of container.""" + """Literal["created", "initialized", "running", "stopped", "exited", "unknown"]: + Returns status of container.""" with suppress(KeyError): return self.attrs["State"]["Status"] return "unknown" diff --git a/podman/domain/containers_create.py b/podman/domain/containers_create.py index c160eb76..09ebd070 100644 --- a/podman/domain/containers_create.py +++ b/podman/domain/containers_create.py @@ -176,7 +176,23 @@ def create( pids_limit (int): Tune a container's pids limit. Set -1 for unlimited. platform (str): Platform in the format os[/arch[/variant]]. Only used if the method needs to pull the requested image. - ports (Dict[Union[int, str], Union[int, Tuple[str, int], List[int], Dict[str, Union[int, Tuple[str, int], List[int]]]]]): Ports to bind inside the container. + ports ( + Dict[ + Union[int, str], + Union[ + int, + Tuple[str, int], + List[int], + Dict[ + str, + Union[ + int, + Tuple[str, int], + List[int] + ] + ] + ] + ]): Ports to bind inside the container. The keys of the dictionary are the ports to bind inside the container, either as an integer or a string in the form port/protocol, where the protocol is either @@ -296,9 +312,11 @@ def create( the corresponding environment variables will be set in the container being built. user (Union[str, int]): Username or UID to run commands as inside the container. userns_mode (str): Sets the user namespace mode for the container when user namespace - remapping option is enabled. Supported values documented `here `_ + remapping option is enabled. Supported values documented + `here `_ uts_mode (str): Sets the UTS namespace mode for the container. - `These `_ are the supported values. + `These `_ + are the supported values. version (str): The version of the API to use. Set to auto to automatically detect the server's version. Default: 3.0.0 volume_driver (str): The name of a volume driver/plugin. diff --git a/podman/tests/integration/test_container_exec.py b/podman/tests/integration/test_container_exec.py index 71ece37e..f0fd72b7 100644 --- a/podman/tests/integration/test_container_exec.py +++ b/podman/tests/integration/test_container_exec.py @@ -111,10 +111,11 @@ def test_container_exec_run_stream_detach(self): ] error_code, output = container.exec_run(command, stream=True, detach=True) - # Detach should make the ``exec_run`` ignore the ``stream`` flag so we will assert against the standard, - # non-streaming behavior. + # Detach should make the ``exec_run`` ignore the ``stream`` flag so we will + # assert against the standard, non-streaming behavior. self.assertEqual(error_code, 0) - # The endpoint should return immediately, before we are able to actually get any of the output. + # The endpoint should return immediately, before we are able to actually + # get any of the output. self.assertEqual( output, b'\n', diff --git a/podman/tests/unit/test_container.py b/podman/tests/unit/test_container.py index 154c8801..90708d95 100644 --- a/podman/tests/unit/test_container.py +++ b/podman/tests/unit/test_container.py @@ -119,7 +119,9 @@ def test_stats(self, mock): "Error": None, "Stats": [ { - "ContainerId": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", + "ContainerId": ( + "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd" + ), "Name": "evil_ptolemy", "CPU": 1000.0, } @@ -421,7 +423,8 @@ def test_top_with_streaming(self, mock): 'Mar01', '?', '00:00:01', - '/usr/bin/ssh-agent /bin/sh -c exec -l /bin/bash -c "/usr/bin/gnome-session"', + '/usr/bin/ssh-agent /bin/sh -c exec -l /bin/bash' + + '-c "/usr/bin/gnome-session"', ], ['jhonce', '5544', '3522', '0', 'Mar01', 'pts/1', '00:00:02', '-bash'], ['jhonce', '6140', '3522', '0', 'Mar01', 'pts/2', '00:00:00', '-bash'], diff --git a/podman/tests/unit/test_imagesmanager.py b/podman/tests/unit/test_imagesmanager.py index ce85898d..008228d6 100644 --- a/podman/tests/unit/test_imagesmanager.py +++ b/podman/tests/unit/test_imagesmanager.py @@ -213,7 +213,8 @@ def test_prune_filters_label(self, mock): """Unit test filters param label for Images prune().""" mock.post( tests.LIBPOD_URL - + "/images/prune?filters=%7B%22label%22%3A+%5B%22%7B%27license%27%3A+%27Apache-2.0%27%7D%22%5D%7D", + + "/images/prune?filters=%7B%22label%22%3A+%5B%22%7B%27license%27%3A+" + + "%27Apache-2.0%27%7D%22%5D%7D", json=[ { "Id": "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", @@ -242,7 +243,8 @@ def test_prune_filters_not_label(self, mock): """Unit test filters param NOT-label for Images prune().""" mock.post( tests.LIBPOD_URL - + "/images/prune?filters=%7B%22label%21%22%3A+%5B%22%7B%27license%27%3A+%27Apache-2.0%27%7D%22%5D%7D", + + "/images/prune?filters=%7B%22label%21%22%3A+%5B%22%7B%27license%27%3A+" + + "%27Apache-2.0%27%7D%22%5D%7D", json=[ { "Id": "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", @@ -666,7 +668,10 @@ def test_list_with_name_and_existing_filters(self, mock): """Test that name parameter works alongside other filters""" mock.get( tests.LIBPOD_URL - + "/images/json?filters=%7B%22dangling%22%3A+%5B%22True%22%5D%2C+%22reference%22%3A+%5B%22fedora%22%5D%7D", + + ( + "/images/json?filters=%7B%22dangling%22%3A+%5B%22True%22%5D%2C+" + "%22reference%22%3A+%5B%22fedora%22%5D%7D" + ), json=[FIRST_IMAGE], ) diff --git a/ruff.toml b/ruff.toml index c41f9132..6f522409 100644 --- a/ruff.toml +++ b/ruff.toml @@ -32,7 +32,6 @@ select = [ # to avoid changing too many lines ignore = [ "F821", # TODO Undefined name - "E501", # TODO line too long # Some Exceptions such as NotFound and NotFoundError can be ambiguous # This change need to be performed with carefulness "N818", # TODO Error Suffix in exception name From 4847d28d7c98f9245b6f12034a434a0ac5341acf Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 17:04:06 +0100 Subject: [PATCH 35/48] Fix Pyupgrades Fix sys.version_info comparisons and drop unsupported python code Furthermore, addresses UP008: super-call-with-parameters Super is not called with parameters anymore when the first argument is __class__ and the second argument is equivalent to the first argument of the enclosing method Signed-off-by: Nicola Sella --- docs/source/conf.py | 4 +- podman/api/__init__.py | 9 +- podman/api/http_utils.py | 2 +- podman/api/typing_extensions.py | 3040 ------------------- podman/tests/integration/test_containers.py | 2 +- podman/tests/integration/test_images.py | 2 +- podman/tests/unit/test_build.py | 2 +- podman/tests/unit/test_container.py | 2 +- podman/tests/unit/test_containersmanager.py | 2 +- podman/tests/unit/test_imagesmanager.py | 2 +- rpm/python-podman.spec | 3 - ruff.toml | 2 +- 12 files changed, 11 insertions(+), 3061 deletions(-) delete mode 100644 podman/api/typing_extensions.py diff --git a/docs/source/conf.py b/docs/source/conf.py index e1e0845a..890a6e40 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -125,9 +125,7 @@ class PatchedPythonDomain(PythonDomain): def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): if 'refspecific' in node: del node['refspecific'] - return super(PatchedPythonDomain, self).resolve_xref( - env, fromdocname, builder, typ, target, node, contnode - ) + return super().resolve_xref(env, fromdocname, builder, typ, target, node, contnode) def skip(app, what, name, obj, would_skip, options): diff --git a/podman/api/__init__.py b/podman/api/__init__.py index 8ad99451..6b197bad 100644 --- a/podman/api/__init__.py +++ b/podman/api/__init__.py @@ -15,15 +15,10 @@ ) from podman.api.tar_utils import create_tar, prepare_containerfile, prepare_containerignore +from typing import Literal + DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024 -try: - from typing import Literal -except (ImportError, ModuleNotFoundError): - try: - from typing_extensions import Literal - except (ImportError, ModuleNotFoundError): - from podman.api.typing_extensions import Literal # pylint: disable=ungrouped-imports # isort: unique-list __all__ = [ diff --git a/podman/api/http_utils.py b/podman/api/http_utils.py index d1254940..5fb8599c 100644 --- a/podman/api/http_utils.py +++ b/podman/api/http_utils.py @@ -91,7 +91,7 @@ def _filter_values(mapping: Mapping[str, Any], recursion=False) -> Dict[str, Any else: proposal = value - if not recursion and proposal not in (None, str(), [], {}): + if not recursion and proposal not in (None, "", [], {}): canonical[key] = proposal elif recursion and proposal not in (None, [], {}): canonical[key] = proposal diff --git a/podman/api/typing_extensions.py b/podman/api/typing_extensions.py deleted file mode 100644 index ebddfa27..00000000 --- a/podman/api/typing_extensions.py +++ /dev/null @@ -1,3040 +0,0 @@ -"""Provide typing.Literal when not supported by OS release. - -FIXME: Remove file when supported Python >= 3.8 -""" - -# Code is backup for missing typing_extensions... -# pylint: disable-all - -import abc -import collections -import collections.abc as collections_abc -import contextlib -import operator -import typing - -# These are used by Protocol implementation -# We use internal typing helpers here, but this significantly reduces -# code duplication. (Also this is only until Protocol is in typing.) -from typing import Generic, Callable, TypeVar, Tuple - -import sys - -# After PEP 560, internal typing API was substantially reworked. -# This is especially important for Protocol class which uses internal APIs -# quite extensively. -PEP_560 = sys.version_info[:3] >= (3, 7, 0) - -if PEP_560: - GenericMeta = TypingMeta = type -else: - from typing import GenericMeta, TypingMeta -OLD_GENERICS = False -try: - from typing import _type_vars, _next_in_mro, _type_check -except ImportError: - OLD_GENERICS = True -try: - from typing import _subs_tree # noqa - - SUBS_TREE = True -except ImportError: - SUBS_TREE = False -try: - from typing import _tp_cache -except ImportError: - - def _tp_cache(x): - return x - - -try: - from typing import _TypingEllipsis, _TypingEmpty -except ImportError: - - class _TypingEllipsis: - pass - - class _TypingEmpty: - pass - - -# The two functions below are copies of typing internal helpers. -# They are needed by _ProtocolMeta - - -def _no_slots_copy(dct): - dict_copy = dict(dct) - if '__slots__' in dict_copy: - for slot in dict_copy['__slots__']: - dict_copy.pop(slot, None) - return dict_copy - - -def _check_generic(cls, parameters): - if not cls.__parameters__: - raise TypeError("%s is not a generic class" % repr(cls)) - alen = len(parameters) - elen = len(cls.__parameters__) - if alen != elen: - raise TypeError( - "Too %s parameters for %s; actual %s, expected %s" - % ("many" if alen > elen else "few", repr(cls), alen, elen) - ) - - -if hasattr(typing, '_generic_new'): - _generic_new = typing._generic_new -else: - # Note: The '_generic_new(...)' function is used as a part of the - # process of creating a generic type and was added to the typing module - # as of Python 3.5.3. - # - # We've defined '_generic_new(...)' below to exactly match the behavior - # implemented in older versions of 'typing' bundled with Python 3.5.0 to - # 3.5.2. This helps eliminate redundancy when defining collection types - # like 'Deque' later. - # - # See https://github.com/python/typing/pull/308 for more details -- in - # particular, compare and contrast the definition of types like - # 'typing.List' before and after the merge. - - def _generic_new(base_cls, cls, *args, **kwargs): - return base_cls.__new__(cls, *args, **kwargs) - - -# See https://github.com/python/typing/pull/439 -if hasattr(typing, '_geqv'): - from typing import _geqv - - _geqv_defined = True -else: - _geqv = None - _geqv_defined = False - -if sys.version_info[:2] >= (3, 6): - import _collections_abc - - _check_methods_in_mro = _collections_abc._check_methods -else: - - def _check_methods_in_mro(C, *methods): - mro = C.__mro__ - for method in methods: - for B in mro: - if method in B.__dict__: - if B.__dict__[method] is None: - return NotImplemented - break - else: - return NotImplemented - return True - - -# Please keep __all__ alphabetized within each category. -__all__ = [ - # Super-special typing primitives. - 'ClassVar', - 'Concatenate', - 'Final', - 'ParamSpec', - 'Type', - # ABCs (from collections.abc). - # The following are added depending on presence - # of their non-generic counterparts in stdlib: - # 'Awaitable', - # 'AsyncIterator', - # 'AsyncIterable', - # 'Coroutine', - # 'AsyncGenerator', - # 'AsyncContextManager', - # 'ChainMap', - # Concrete collection types. - 'ContextManager', - 'Counter', - 'Deque', - 'DefaultDict', - 'OrderedDict', - 'TypedDict', - # Structural checks, a.k.a. protocols. - 'SupportsIndex', - # One-off things. - 'final', - 'IntVar', - 'Literal', - 'NewType', - 'overload', - 'Text', - 'TypeAlias', - 'TypeGuard', - 'TYPE_CHECKING', -] - -# Annotated relies on substitution trees of pep 560. It will not work for -# versions of typing older than 3.5.3 -HAVE_ANNOTATED = PEP_560 or SUBS_TREE - -if PEP_560: - __all__.extend(["get_args", "get_origin", "get_type_hints"]) - -if HAVE_ANNOTATED: - __all__.append("Annotated") - -# Protocols are hard to backport to the original version of typing 3.5.0 -HAVE_PROTOCOLS = sys.version_info[:3] != (3, 5, 0) - -if HAVE_PROTOCOLS: - __all__.extend(['Protocol', 'runtime', 'runtime_checkable']) - -# TODO -if hasattr(typing, 'NoReturn'): - NoReturn = typing.NoReturn -elif hasattr(typing, '_FinalTypingBase'): - - class _NoReturn(typing._FinalTypingBase, _root=True): - """Special type indicating functions that never return. - Example:: - - from typing import NoReturn - - def stop() -> NoReturn: - raise Exception('no way') - - This type is invalid in other positions, e.g., ``List[NoReturn]`` - will fail in static type checkers. - """ - - __slots__ = () - - def __instancecheck__(self, obj): - raise TypeError("NoReturn cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("NoReturn cannot be used with issubclass().") - - NoReturn = _NoReturn(_root=True) -else: - - class _NoReturnMeta(typing.TypingMeta): - """Metaclass for NoReturn""" - - def __new__(cls, name, bases, namespace, _root=False): - return super().__new__(cls, name, bases, namespace, _root=_root) - - def __instancecheck__(self, obj): - raise TypeError("NoReturn cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("NoReturn cannot be used with issubclass().") - - class NoReturn(typing.Final, metaclass=_NoReturnMeta, _root=True): - """Special type indicating functions that never return. - Example:: - - from typing import NoReturn - - def stop() -> NoReturn: - raise Exception('no way') - - This type is invalid in other positions, e.g., ``List[NoReturn]`` - will fail in static type checkers. - """ - - __slots__ = () - - -# Some unconstrained type variables. These are used by the container types. -# (These are not for export.) -T = typing.TypeVar('T') # Any type. -KT = typing.TypeVar('KT') # Key type. -VT = typing.TypeVar('VT') # Value type. -T_co = typing.TypeVar('T_co', covariant=True) # Any type covariant containers. -V_co = typing.TypeVar('V_co', covariant=True) # Any type covariant containers. -VT_co = typing.TypeVar('VT_co', covariant=True) # Value type covariant containers. -T_contra = typing.TypeVar('T_contra', contravariant=True) # Ditto contravariant. - -if hasattr(typing, 'ClassVar'): - ClassVar = typing.ClassVar -elif hasattr(typing, '_FinalTypingBase'): - - class _ClassVar(typing._FinalTypingBase, _root=True): - """Special type construct to mark class variables. - - An annotation wrapped in ClassVar indicates that a given - attribute is intended to be used as a class variable and - should not be set on instances of that class. Usage:: - - class Starship: - stats: ClassVar[Dict[str, int]] = {} # class variable - damage: int = 10 # instance variable - - ClassVar accepts only types and cannot be further subscribed. - - Note that ClassVar is not a class itself, and should not - be used with isinstance() or issubclass(). - """ - - __slots__ = ('__type__',) - - def __init__(self, tp=None, **kwds): - self.__type__ = tp - - def __getitem__(self, item): - cls = type(self) - if self.__type__ is None: - return cls( - typing._type_check( - item, '{} accepts only single type.'.format(cls.__name__[1:]) - ), - _root=True, - ) - raise TypeError('{} cannot be further subscripted'.format(cls.__name__[1:])) - - def _eval_type(self, globalns, localns): - new_tp = typing._eval_type(self.__type__, globalns, localns) - if new_tp == self.__type__: - return self - return type(self)(new_tp, _root=True) - - def __repr__(self): - r = super().__repr__() - if self.__type__ is not None: - r += '[{}]'.format(typing._type_repr(self.__type__)) - return r - - def __hash__(self): - return hash((type(self).__name__, self.__type__)) - - def __eq__(self, other): - if not isinstance(other, _ClassVar): - return NotImplemented - if self.__type__ is not None: - return self.__type__ == other.__type__ - return self is other - - ClassVar = _ClassVar(_root=True) -else: - - class _ClassVarMeta(typing.TypingMeta): - """Metaclass for ClassVar""" - - def __new__(cls, name, bases, namespace, tp=None, _root=False): - self = super().__new__(cls, name, bases, namespace, _root=_root) - if tp is not None: - self.__type__ = tp - return self - - def __instancecheck__(self, obj): - raise TypeError("ClassVar cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("ClassVar cannot be used with issubclass().") - - def __getitem__(self, item): - cls = type(self) - if self.__type__ is not None: - raise TypeError('{} cannot be further subscripted'.format(cls.__name__[1:])) - - param = typing._type_check( - item, '{} accepts only single type.'.format(cls.__name__[1:]) - ) - return cls(self.__name__, self.__bases__, dict(self.__dict__), tp=param, _root=True) - - def _eval_type(self, globalns, localns): - new_tp = typing._eval_type(self.__type__, globalns, localns) - if new_tp == self.__type__: - return self - return type(self)( - self.__name__, self.__bases__, dict(self.__dict__), tp=self.__type__, _root=True - ) - - def __repr__(self): - r = super().__repr__() - if self.__type__ is not None: - r += '[{}]'.format(typing._type_repr(self.__type__)) - return r - - def __hash__(self): - return hash((type(self).__name__, self.__type__)) - - def __eq__(self, other): - if not isinstance(other, ClassVar): - return NotImplemented - if self.__type__ is not None: - return self.__type__ == other.__type__ - return self is other - - class ClassVar(typing.Final, metaclass=_ClassVarMeta, _root=True): - """Special type construct to mark class variables. - - An annotation wrapped in ClassVar indicates that a given - attribute is intended to be used as a class variable and - should not be set on instances of that class. Usage:: - - class Starship: - stats: ClassVar[Dict[str, int]] = {} # class variable - damage: int = 10 # instance variable - - ClassVar accepts only types and cannot be further subscribed. - - Note that ClassVar is not a class itself, and should not - be used with isinstance() or issubclass(). - """ - - __type__ = None - - -# On older versions of typing there is an internal class named "Final". -if hasattr(typing, 'Final') and sys.version_info[:2] >= (3, 7): - Final = typing.Final -elif sys.version_info[:2] >= (3, 7): - - class _FinalForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - - def __getitem__(self, parameters): - item = typing._type_check(parameters, '{} accepts only single type'.format(self._name)) - return _GenericAlias(self, (item,)) - - Final = _FinalForm( - 'Final', - doc="""A special typing construct to indicate that a name - cannot be re-assigned or overridden in a subclass. - For example: - - MAX_SIZE: Final = 9000 - MAX_SIZE += 1 # Error reported by type checker - - class Connection: - TIMEOUT: Final[int] = 10 - class FastConnector(Connection): - TIMEOUT = 1 # Error reported by type checker - - There is no runtime checking of these properties.""", - ) -elif hasattr(typing, '_FinalTypingBase'): - - class _Final(typing._FinalTypingBase, _root=True): - """A special typing construct to indicate that a name - cannot be re-assigned or overridden in a subclass. - For example: - - MAX_SIZE: Final = 9000 - MAX_SIZE += 1 # Error reported by type checker - - class Connection: - TIMEOUT: Final[int] = 10 - class FastConnector(Connection): - TIMEOUT = 1 # Error reported by type checker - - There is no runtime checking of these properties. - """ - - __slots__ = ('__type__',) - - def __init__(self, tp=None, **kwds): - self.__type__ = tp - - def __getitem__(self, item): - cls = type(self) - if self.__type__ is None: - return cls( - typing._type_check( - item, '{} accepts only single type.'.format(cls.__name__[1:]) - ), - _root=True, - ) - raise TypeError('{} cannot be further subscripted'.format(cls.__name__[1:])) - - def _eval_type(self, globalns, localns): - new_tp = typing._eval_type(self.__type__, globalns, localns) - if new_tp == self.__type__: - return self - return type(self)(new_tp, _root=True) - - def __repr__(self): - r = super().__repr__() - if self.__type__ is not None: - r += '[{}]'.format(typing._type_repr(self.__type__)) - return r - - def __hash__(self): - return hash((type(self).__name__, self.__type__)) - - def __eq__(self, other): - if not isinstance(other, _Final): - return NotImplemented - if self.__type__ is not None: - return self.__type__ == other.__type__ - return self is other - - Final = _Final(_root=True) -else: - - class _FinalMeta(typing.TypingMeta): - """Metaclass for Final""" - - def __new__(cls, name, bases, namespace, tp=None, _root=False): - self = super().__new__(cls, name, bases, namespace, _root=_root) - if tp is not None: - self.__type__ = tp - return self - - def __instancecheck__(self, obj): - raise TypeError("Final cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("Final cannot be used with issubclass().") - - def __getitem__(self, item): - cls = type(self) - if self.__type__ is not None: - raise TypeError('{} cannot be further subscripted'.format(cls.__name__[1:])) - - param = typing._type_check( - item, '{} accepts only single type.'.format(cls.__name__[1:]) - ) - return cls(self.__name__, self.__bases__, dict(self.__dict__), tp=param, _root=True) - - def _eval_type(self, globalns, localns): - new_tp = typing._eval_type(self.__type__, globalns, localns) - if new_tp == self.__type__: - return self - return type(self)( - self.__name__, self.__bases__, dict(self.__dict__), tp=self.__type__, _root=True - ) - - def __repr__(self): - r = super().__repr__() - if self.__type__ is not None: - r += '[{}]'.format(typing._type_repr(self.__type__)) - return r - - def __hash__(self): - return hash((type(self).__name__, self.__type__)) - - def __eq__(self, other): - if not isinstance(other, Final): - return NotImplemented - if self.__type__ is not None: - return self.__type__ == other.__type__ - return self is other - - class Final(typing.Final, metaclass=_FinalMeta, _root=True): - """A special typing construct to indicate that a name - cannot be re-assigned or overridden in a subclass. - For example: - - MAX_SIZE: Final = 9000 - MAX_SIZE += 1 # Error reported by type checker - - class Connection: - TIMEOUT: Final[int] = 10 - class FastConnector(Connection): - TIMEOUT = 1 # Error reported by type checker - - There is no runtime checking of these properties. - """ - - __type__ = None - - -if hasattr(typing, 'final'): - final = typing.final -else: - - def final(f): - """This decorator can be used to indicate to type checkers that - the decorated method cannot be overridden, and decorated class - cannot be subclassed. For example: - - class Base: - @final - def done(self) -> None: - ... - class Sub(Base): - def done(self) -> None: # Error reported by type checker - ... - @final - class Leaf: - ... - class Other(Leaf): # Error reported by type checker - ... - - There is no runtime checking of these properties. - """ - return f - - -def IntVar(name): - return TypeVar(name) - - -if hasattr(typing, 'Literal'): - Literal = typing.Literal -elif sys.version_info[:2] >= (3, 7): - - class _LiteralForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - - def __getitem__(self, parameters): - return _GenericAlias(self, parameters) - - Literal = _LiteralForm( - 'Literal', - doc="""A type that can be used to indicate to type checkers - that the corresponding value has a value literally equivalent - to the provided parameter. For example: - - var: Literal[4] = 4 - - The type checker understands that 'var' is literally equal to - the value 4 and no other value. - - Literal[...] cannot be subclassed. There is no runtime - checking verifying that the parameter is actually a value - instead of a type.""", - ) -elif hasattr(typing, '_FinalTypingBase'): - - class _Literal(typing._FinalTypingBase, _root=True): - """A type that can be used to indicate to type checkers that the - corresponding value has a value literally equivalent to the - provided parameter. For example: - - var: Literal[4] = 4 - - The type checker understands that 'var' is literally equal to the - value 4 and no other value. - - Literal[...] cannot be subclassed. There is no runtime checking - verifying that the parameter is actually a value instead of a type. - """ - - __slots__ = ('__values__',) - - def __init__(self, values=None, **kwds): - self.__values__ = values - - def __getitem__(self, values): - cls = type(self) - if self.__values__ is None: - if not isinstance(values, tuple): - values = (values,) - return cls(values, _root=True) - raise TypeError('{} cannot be further subscripted'.format(cls.__name__[1:])) - - def _eval_type(self, globalns, localns): - return self - - def __repr__(self): - r = super().__repr__() - if self.__values__ is not None: - r += '[{}]'.format(', '.join(map(typing._type_repr, self.__values__))) - return r - - def __hash__(self): - return hash((type(self).__name__, self.__values__)) - - def __eq__(self, other): - if not isinstance(other, _Literal): - return NotImplemented - if self.__values__ is not None: - return self.__values__ == other.__values__ - return self is other - - Literal = _Literal(_root=True) -else: - - class _LiteralMeta(typing.TypingMeta): - """Metaclass for Literal""" - - def __new__(cls, name, bases, namespace, values=None, _root=False): - self = super().__new__(cls, name, bases, namespace, _root=_root) - if values is not None: - self.__values__ = values - return self - - def __instancecheck__(self, obj): - raise TypeError("Literal cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("Literal cannot be used with issubclass().") - - def __getitem__(self, item): - cls = type(self) - if self.__values__ is not None: - raise TypeError('{} cannot be further subscripted'.format(cls.__name__[1:])) - - if not isinstance(item, tuple): - item = (item,) - return cls(self.__name__, self.__bases__, dict(self.__dict__), values=item, _root=True) - - def _eval_type(self, globalns, localns): - return self - - def __repr__(self): - r = super().__repr__() - if self.__values__ is not None: - r += '[{}]'.format(', '.join(map(typing._type_repr, self.__values__))) - return r - - def __hash__(self): - return hash((type(self).__name__, self.__values__)) - - def __eq__(self, other): - if not isinstance(other, Literal): - return NotImplemented - if self.__values__ is not None: - return self.__values__ == other.__values__ - return self is other - - class Literal(typing.Final, metaclass=_LiteralMeta, _root=True): - """A type that can be used to indicate to type checkers that the - corresponding value has a value literally equivalent to the - provided parameter. For example: - - var: Literal[4] = 4 - - The type checker understands that 'var' is literally equal to the - value 4 and no other value. - - Literal[...] cannot be subclassed. There is no runtime checking - verifying that the parameter is actually a value instead of a type. - """ - - __values__ = None - - -def _overload_dummy(*args, **kwds): - """Helper for @overload to raise when called.""" - raise NotImplementedError( - "You should not call an overloaded function. " - "A series of @overload-decorated functions " - "outside a stub module should always be followed " - "by an implementation that is not @overload-ed." - ) - - -def overload(func): - """Decorator for overloaded functions/methods. - - In a stub file, place two or more stub definitions for the same - function in a row, each decorated with @overload. For example: - - @overload - def utf8(value: None) -> None: ... - @overload - def utf8(value: bytes) -> bytes: ... - @overload - def utf8(value: str) -> bytes: ... - - In a non-stub file (i.e. a regular .py file), do the same but - follow it with an implementation. The implementation should *not* - be decorated with @overload. For example: - - @overload - def utf8(value: None) -> None: ... - @overload - def utf8(value: bytes) -> bytes: ... - @overload - def utf8(value: str) -> bytes: ... - def utf8(value): - # implementation goes here - """ - return _overload_dummy - - -# This is not a real generic class. Don't use outside annotations. -if hasattr(typing, 'Type'): - Type = typing.Type -else: - # Internal type variable used for Type[]. - CT_co = typing.TypeVar('CT_co', covariant=True, bound=type) - - class Type(typing.Generic[CT_co], extra=type): - """A special construct usable to annotate class objects. - - For example, suppose we have the following classes:: - - class User: ... # Abstract base for User classes - class BasicUser(User): ... - class ProUser(User): ... - class TeamUser(User): ... - - And a function that takes a class argument that's a subclass of - User and returns an instance of the corresponding class:: - - U = TypeVar('U', bound=User) - def new_user(user_class: Type[U]) -> U: - user = user_class() - # (Here we could write the user object to a database) - return user - joe = new_user(BasicUser) - - At this point the type checker knows that joe has type BasicUser. - """ - - __slots__ = () - - -# Various ABCs mimicking those in collections.abc. -# A few are simply re-exported for completeness. - - -def _define_guard(type_name): - """ - Returns True if the given type isn't defined in typing but - is defined in collections_abc. - - Adds the type to __all__ if the collection is found in either - typing or collection_abc. - """ - if hasattr(typing, type_name): - __all__.append(type_name) - globals()[type_name] = getattr(typing, type_name) - return False - elif hasattr(collections_abc, type_name): - __all__.append(type_name) - return True - else: - return False - - -class _ExtensionsGenericMeta(GenericMeta): - def __subclasscheck__(self, subclass): - """This mimics a more modern GenericMeta.__subclasscheck__() logic - (that does not have problems with recursion) to work around interactions - between collections, typing, and typing_extensions on older - versions of Python, see https://github.com/python/typing/issues/501. - """ - if sys.version_info[:3] >= (3, 5, 3) or sys.version_info[:3] < (3, 5, 0): - if self.__origin__ is not None: - if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']: - raise TypeError( - "Parameterized generics cannot be used with class or instance checks" - ) - return False - if not self.__extra__: - return super().__subclasscheck__(subclass) - res = self.__extra__.__subclasshook__(subclass) - if res is not NotImplemented: - return res - if self.__extra__ in subclass.__mro__: - return True - for scls in self.__extra__.__subclasses__(): - if isinstance(scls, GenericMeta): - continue - if issubclass(subclass, scls): - return True - return False - - -if _define_guard('Awaitable'): - - class Awaitable( - typing.Generic[T_co], metaclass=_ExtensionsGenericMeta, extra=collections_abc.Awaitable - ): - __slots__ = () - - -if _define_guard('Coroutine'): - - class Coroutine( - Awaitable[V_co], - typing.Generic[T_co, T_contra, V_co], - metaclass=_ExtensionsGenericMeta, - extra=collections_abc.Coroutine, - ): - __slots__ = () - - -if _define_guard('AsyncIterable'): - - class AsyncIterable( - typing.Generic[T_co], metaclass=_ExtensionsGenericMeta, extra=collections_abc.AsyncIterable - ): - __slots__ = () - - -if _define_guard('AsyncIterator'): - - class AsyncIterator( - AsyncIterable[T_co], metaclass=_ExtensionsGenericMeta, extra=collections_abc.AsyncIterator - ): - __slots__ = () - - -if hasattr(typing, 'Deque'): - Deque = typing.Deque -elif _geqv_defined: - - class Deque( - collections.deque, - typing.MutableSequence[T], - metaclass=_ExtensionsGenericMeta, - extra=collections.deque, - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if _geqv(cls, Deque): - return collections.deque(*args, **kwds) - return _generic_new(collections.deque, cls, *args, **kwds) - -else: - - class Deque( - collections.deque, - typing.MutableSequence[T], - metaclass=_ExtensionsGenericMeta, - extra=collections.deque, - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if cls._gorg is Deque: - return collections.deque(*args, **kwds) - return _generic_new(collections.deque, cls, *args, **kwds) - - -if hasattr(typing, 'ContextManager'): - ContextManager = typing.ContextManager -elif hasattr(contextlib, 'AbstractContextManager'): - - class ContextManager( - typing.Generic[T_co], - metaclass=_ExtensionsGenericMeta, - extra=contextlib.AbstractContextManager, - ): - __slots__ = () - -else: - - class ContextManager(typing.Generic[T_co]): - __slots__ = () - - def __enter__(self): - return self - - @abc.abstractmethod - def __exit__(self, exc_type, exc_value, traceback): - return None - - @classmethod - def __subclasshook__(cls, C): - if cls is ContextManager: - # In Python 3.6+, it is possible to set a method to None to - # explicitly indicate that the class does not implement an ABC - # (https://bugs.python.org/issue25958), but we do not support - # that pattern here because this fallback class is only used - # in Python 3.5 and earlier. - if any("__enter__" in B.__dict__ for B in C.__mro__) and any( - "__exit__" in B.__dict__ for B in C.__mro__ - ): - return True - return NotImplemented - - -if hasattr(typing, 'AsyncContextManager'): - AsyncContextManager = typing.AsyncContextManager - __all__.append('AsyncContextManager') -elif hasattr(contextlib, 'AbstractAsyncContextManager'): - - class AsyncContextManager( - typing.Generic[T_co], - metaclass=_ExtensionsGenericMeta, - extra=contextlib.AbstractAsyncContextManager, - ): - __slots__ = () - - __all__.append('AsyncContextManager') -elif sys.version_info[:2] >= (3, 5): - exec( - """ -class AsyncContextManager(typing.Generic[T_co]): - __slots__ = () - - async def __aenter__(self): - return self - - @abc.abstractmethod - async def __aexit__(self, exc_type, exc_value, traceback): - return None - - @classmethod - def __subclasshook__(cls, C): - if cls is AsyncContextManager: - return _check_methods_in_mro(C, "__aenter__", "__aexit__") - return NotImplemented - -__all__.append('AsyncContextManager') -""" - ) - -if hasattr(typing, 'DefaultDict'): - DefaultDict = typing.DefaultDict -elif _geqv_defined: - - class DefaultDict( - collections.defaultdict, - typing.MutableMapping[KT, VT], - metaclass=_ExtensionsGenericMeta, - extra=collections.defaultdict, - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if _geqv(cls, DefaultDict): - return collections.defaultdict(*args, **kwds) - return _generic_new(collections.defaultdict, cls, *args, **kwds) - -else: - - class DefaultDict( - collections.defaultdict, - typing.MutableMapping[KT, VT], - metaclass=_ExtensionsGenericMeta, - extra=collections.defaultdict, - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if cls._gorg is DefaultDict: - return collections.defaultdict(*args, **kwds) - return _generic_new(collections.defaultdict, cls, *args, **kwds) - - -if hasattr(typing, 'OrderedDict'): - OrderedDict = typing.OrderedDict -elif (3, 7, 0) <= sys.version_info[:3] < (3, 7, 2): - OrderedDict = typing._alias(collections.OrderedDict, (KT, VT)) -elif _geqv_defined: - - class OrderedDict( - collections.OrderedDict, - typing.MutableMapping[KT, VT], - metaclass=_ExtensionsGenericMeta, - extra=collections.OrderedDict, - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if _geqv(cls, OrderedDict): - return collections.OrderedDict(*args, **kwds) - return _generic_new(collections.OrderedDict, cls, *args, **kwds) - -else: - - class OrderedDict( - collections.OrderedDict, - typing.MutableMapping[KT, VT], - metaclass=_ExtensionsGenericMeta, - extra=collections.OrderedDict, - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if cls._gorg is OrderedDict: - return collections.OrderedDict(*args, **kwds) - return _generic_new(collections.OrderedDict, cls, *args, **kwds) - - -if hasattr(typing, 'Counter'): - Counter = typing.Counter -elif (3, 5, 0) <= sys.version_info[:3] <= (3, 5, 1): - assert _geqv_defined - _TInt = typing.TypeVar('_TInt') - - class _CounterMeta(typing.GenericMeta): - """Metaclass for Counter""" - - def __getitem__(self, item): - return super().__getitem__((item, int)) - - class Counter( - collections.Counter, typing.Dict[T, int], metaclass=_CounterMeta, extra=collections.Counter - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if _geqv(cls, Counter): - return collections.Counter(*args, **kwds) - return _generic_new(collections.Counter, cls, *args, **kwds) - -elif _geqv_defined: - - class Counter( - collections.Counter, - typing.Dict[T, int], - metaclass=_ExtensionsGenericMeta, - extra=collections.Counter, - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if _geqv(cls, Counter): - return collections.Counter(*args, **kwds) - return _generic_new(collections.Counter, cls, *args, **kwds) - -else: - - class Counter( - collections.Counter, - typing.Dict[T, int], - metaclass=_ExtensionsGenericMeta, - extra=collections.Counter, - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if cls._gorg is Counter: - return collections.Counter(*args, **kwds) - return _generic_new(collections.Counter, cls, *args, **kwds) - - -if hasattr(typing, 'ChainMap'): - ChainMap = typing.ChainMap - __all__.append('ChainMap') -elif hasattr(collections, 'ChainMap'): - # ChainMap only exists in 3.3+ - if _geqv_defined: - - class ChainMap( - collections.ChainMap, - typing.MutableMapping[KT, VT], - metaclass=_ExtensionsGenericMeta, - extra=collections.ChainMap, - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if _geqv(cls, ChainMap): - return collections.ChainMap(*args, **kwds) - return _generic_new(collections.ChainMap, cls, *args, **kwds) - - else: - - class ChainMap( - collections.ChainMap, - typing.MutableMapping[KT, VT], - metaclass=_ExtensionsGenericMeta, - extra=collections.ChainMap, - ): - __slots__ = () - - def __new__(cls, *args, **kwds): - if cls._gorg is ChainMap: - return collections.ChainMap(*args, **kwds) - return _generic_new(collections.ChainMap, cls, *args, **kwds) - - __all__.append('ChainMap') - -if _define_guard('AsyncGenerator'): - - class AsyncGenerator( - AsyncIterator[T_co], - typing.Generic[T_co, T_contra], - metaclass=_ExtensionsGenericMeta, - extra=collections_abc.AsyncGenerator, - ): - __slots__ = () - - -if hasattr(typing, 'NewType'): - NewType = typing.NewType -else: - - def NewType(name, tp): - """NewType creates simple unique types with almost zero - runtime overhead. NewType(name, tp) is considered a subtype of tp - by static type checkers. At runtime, NewType(name, tp) returns - a dummy function that simply returns its argument. Usage:: - - UserId = NewType('UserId', int) - - def name_by_id(user_id: UserId) -> str: - ... - - UserId('user') # Fails type check - - name_by_id(42) # Fails type check - name_by_id(UserId(42)) # OK - - num = UserId(5) + 1 # type: int - """ - - def new_type(x): - return x - - new_type.__name__ = name - new_type.__supertype__ = tp - return new_type - - -if hasattr(typing, 'Text'): - Text = typing.Text -else: - Text = str - -if hasattr(typing, 'TYPE_CHECKING'): - TYPE_CHECKING = typing.TYPE_CHECKING -else: - # Constant that's True when type checking, but False here. - TYPE_CHECKING = False - - -def _gorg(cls): - """This function exists for compatibility with old typing versions.""" - assert isinstance(cls, GenericMeta) - if hasattr(cls, '_gorg'): - return cls._gorg - while cls.__origin__ is not None: - cls = cls.__origin__ - return cls - - -if OLD_GENERICS: - - def _next_in_mro(cls): # noqa - """This function exists for compatibility with old typing versions.""" - next_in_mro = object - for i, c in enumerate(cls.__mro__[:-1]): - if isinstance(c, GenericMeta) and _gorg(c) is Generic: - next_in_mro = cls.__mro__[i + 1] - return next_in_mro - - -_PROTO_WHITELIST = [ - 'Callable', - 'Awaitable', - 'Iterable', - 'Iterator', - 'AsyncIterable', - 'AsyncIterator', - 'Hashable', - 'Sized', - 'Container', - 'Collection', - 'Reversible', - 'ContextManager', - 'AsyncContextManager', -] - - -def _get_protocol_attrs(cls): - attrs = set() - for base in cls.__mro__[:-1]: # without object - if base.__name__ in ('Protocol', 'Generic'): - continue - annotations = getattr(base, '__annotations__', {}) - for attr in list(base.__dict__.keys()) + list(annotations.keys()): - if not attr.startswith('_abc_') and attr not in ( - '__abstractmethods__', - '__annotations__', - '__weakref__', - '_is_protocol', - '_is_runtime_protocol', - '__dict__', - '__args__', - '__slots__', - '__next_in_mro__', - '__parameters__', - '__origin__', - '__orig_bases__', - '__extra__', - '__tree_hash__', - '__doc__', - '__subclasshook__', - '__init__', - '__new__', - '__module__', - '_MutableMapping__marker', - '_gorg', - ): - attrs.add(attr) - return attrs - - -def _is_callable_members_only(cls): - return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls)) - - -if hasattr(typing, 'Protocol'): - Protocol = typing.Protocol -elif HAVE_PROTOCOLS and not PEP_560: - - def _no_init(self, *args, **kwargs): - if type(self)._is_protocol: - raise TypeError('Protocols cannot be instantiated') - - class _ProtocolMeta(GenericMeta): - """Internal metaclass for Protocol. - - This exists so Protocol classes can be generic without deriving - from Generic. - """ - - if not OLD_GENERICS: - - def __new__( - cls, - name, - bases, - namespace, - tvars=None, - args=None, - origin=None, - extra=None, - orig_bases=None, - ): - # This is just a version copied from GenericMeta.__new__ that - # includes "Protocol" special treatment. (Comments removed for brevity.) - assert extra is None # Protocols should not have extra - if tvars is not None: - assert origin is not None - assert all(isinstance(t, TypeVar) for t in tvars), tvars - else: - tvars = _type_vars(bases) - gvars = None - for base in bases: - if base is Generic: - raise TypeError("Cannot inherit from plain Generic") - if isinstance(base, GenericMeta) and base.__origin__ in ( - Generic, - Protocol, - ): - if gvars is not None: - raise TypeError( - "Cannot inherit from Generic[...] or" - " Protocol[...] multiple times." - ) - gvars = base.__parameters__ - if gvars is None: - gvars = tvars - else: - tvarset = set(tvars) - gvarset = set(gvars) - if not tvarset <= gvarset: - raise TypeError( - "Some type variables (%s) are not listed in %s[%s]" - % ( - ", ".join(str(t) for t in tvars if t not in gvarset), - ( - "Generic" - if any(b.__origin__ is Generic for b in bases) - else "Protocol" - ), - ", ".join(str(g) for g in gvars), - ) - ) - tvars = gvars - - initial_bases = bases - if extra is not None and type(extra) is abc.ABCMeta and extra not in bases: - bases = (extra,) + bases - bases = tuple(_gorg(b) if isinstance(b, GenericMeta) else b for b in bases) - if any(isinstance(b, GenericMeta) and b is not Generic for b in bases): - bases = tuple(b for b in bases if b is not Generic) - namespace.update({'__origin__': origin, '__extra__': extra}) - self = super(GenericMeta, cls).__new__(cls, name, bases, namespace, _root=True) - super(GenericMeta, self).__setattr__('_gorg', self if not origin else _gorg(origin)) - self.__parameters__ = tvars - self.__args__ = ( - tuple( - ... if a is _TypingEllipsis else () if a is _TypingEmpty else a - for a in args - ) - if args - else None - ) - self.__next_in_mro__ = _next_in_mro(self) - if orig_bases is None: - self.__orig_bases__ = initial_bases - elif origin is not None: - self._abc_registry = origin._abc_registry - self._abc_cache = origin._abc_cache - if hasattr(self, '_subs_tree'): - self.__tree_hash__ = ( - hash(self._subs_tree()) if origin else super(GenericMeta, self).__hash__() - ) - return self - - def __init__(cls, *args, **kwargs): - super().__init__(*args, **kwargs) - if not cls.__dict__.get('_is_protocol', None): - cls._is_protocol = any( - b is Protocol or isinstance(b, _ProtocolMeta) and b.__origin__ is Protocol - for b in cls.__bases__ - ) - if cls._is_protocol: - for base in cls.__mro__[1:]: - if not ( - base in (object, Generic) - or base.__module__ == 'collections.abc' - and base.__name__ in _PROTO_WHITELIST - or isinstance(base, TypingMeta) - and base._is_protocol - or isinstance(base, GenericMeta) - and base.__origin__ is Generic - ): - raise TypeError( - 'Protocols can only inherit from other protocols, got %r' % base - ) - - cls.__init__ = _no_init - - def _proto_hook(other): - if not cls.__dict__.get('_is_protocol', None): - return NotImplemented - if not isinstance(other, type): - # Same error as for issubclass(1, int) - raise TypeError('issubclass() arg 1 must be a class') - for attr in _get_protocol_attrs(cls): - for base in other.__mro__: - if attr in base.__dict__: - if base.__dict__[attr] is None: - return NotImplemented - break - annotations = getattr(base, '__annotations__', {}) - if ( - isinstance(annotations, typing.Mapping) - and attr in annotations - and isinstance(other, _ProtocolMeta) - and other._is_protocol - ): - break - else: - return NotImplemented - return True - - if '__subclasshook__' not in cls.__dict__: - cls.__subclasshook__ = _proto_hook - - def __instancecheck__(self, instance): - # We need this method for situations where attributes are - # assigned in __init__. - if ( - not getattr(self, '_is_protocol', False) or _is_callable_members_only(self) - ) and issubclass(instance.__class__, self): - return True - if self._is_protocol: - if all( - hasattr(instance, attr) - and ( - not callable(getattr(self, attr, None)) - or getattr(instance, attr) is not None - ) - for attr in _get_protocol_attrs(self) - ): - return True - return super(GenericMeta, self).__instancecheck__(instance) - - def __subclasscheck__(self, cls): - if self.__origin__ is not None: - if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']: - raise TypeError( - "Parameterized generics cannot be used with class or instance checks" - ) - return False - if self.__dict__.get('_is_protocol', None) and not self.__dict__.get( - '_is_runtime_protocol', None - ): - if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools', 'typing']: - return False - raise TypeError( - "Instance and class checks can only be used with @runtime protocols" - ) - if self.__dict__.get('_is_runtime_protocol', None) and not _is_callable_members_only( - self - ): - if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools', 'typing']: - return super(GenericMeta, self).__subclasscheck__(cls) - raise TypeError("Protocols with non-method members don't support issubclass()") - return super(GenericMeta, self).__subclasscheck__(cls) - - if not OLD_GENERICS: - - @_tp_cache - def __getitem__(self, params): - # We also need to copy this from GenericMeta.__getitem__ to get - # special treatment of "Protocol". (Comments removed for brevity.) - if not isinstance(params, tuple): - params = (params,) - if not params and _gorg(self) is not Tuple: - raise TypeError("Parameter list to %s[...] cannot be empty" % self.__qualname__) - msg = "Parameters to generic types must be types." - params = tuple(_type_check(p, msg) for p in params) - if self in (Generic, Protocol): - if not all(isinstance(p, TypeVar) for p in params): - raise TypeError("Parameters to %r[...] must all be type variables" % self) - if len(set(params)) != len(params): - raise TypeError("Parameters to %r[...] must all be unique" % self) - tvars = params - args = params - elif self in (Tuple, Callable): - tvars = _type_vars(params) - args = params - elif self.__origin__ in (Generic, Protocol): - raise TypeError("Cannot subscript already-subscripted %s" % repr(self)) - else: - _check_generic(self, params) - tvars = _type_vars(params) - args = params - - prepend = (self,) if self.__origin__ is None else () - return self.__class__( - self.__name__, - prepend + self.__bases__, - _no_slots_copy(self.__dict__), - tvars=tvars, - args=args, - origin=self, - extra=self.__extra__, - orig_bases=self.__orig_bases__, - ) - - class Protocol(metaclass=_ProtocolMeta): - """Base class for protocol classes. Protocol classes are defined as:: - - class Proto(Protocol): - def meth(self) -> int: - ... - - Such classes are primarily used with static type checkers that recognize - structural subtyping (static duck-typing), for example:: - - class C: - def meth(self) -> int: - return 0 - - def func(x: Proto) -> int: - return x.meth() - - func(C()) # Passes static type check - - See PEP 544 for details. Protocol classes decorated with - @typing_extensions.runtime act as simple-minded runtime protocol that checks - only the presence of given attributes, ignoring their type signatures. - - Protocol classes can be generic, they are defined as:: - - class GenProto({bases}): - def meth(self) -> T: - ... - """ - - __slots__ = () - _is_protocol = True - - def __new__(cls, *args, **kwds): - if _gorg(cls) is Protocol: - raise TypeError( - "Type Protocol cannot be instantiated; it can be used only as a base class" - ) - if OLD_GENERICS: - return _generic_new(_next_in_mro(cls), cls, *args, **kwds) - return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) - - if Protocol.__doc__ is not None: - Protocol.__doc__ = Protocol.__doc__.format( - bases="Protocol, Generic[T]" if OLD_GENERICS else "Protocol[T]" - ) - -elif PEP_560: - from typing import _type_check, _GenericAlias, _collect_type_vars # noqa - - def _no_init(self, *args, **kwargs): - if type(self)._is_protocol: - raise TypeError('Protocols cannot be instantiated') - - class _ProtocolMeta(abc.ABCMeta): - # This metaclass is a bit unfortunate and exists only because of the lack - # of __instancehook__. - def __instancecheck__(cls, instance): - # We need this method for situations where attributes are - # assigned in __init__. - if ( - not getattr(cls, '_is_protocol', False) or _is_callable_members_only(cls) - ) and issubclass(instance.__class__, cls): - return True - if cls._is_protocol: - if all( - hasattr(instance, attr) - and ( - not callable(getattr(cls, attr, None)) - or getattr(instance, attr) is not None - ) - for attr in _get_protocol_attrs(cls) - ): - return True - return super().__instancecheck__(instance) - - class Protocol(metaclass=_ProtocolMeta): - # There is quite a lot of overlapping code with typing.Generic. - # Unfortunately it is hard to avoid this while these live in two different - # modules. The duplicated code will be removed when Protocol is moved to typing. - """Base class for protocol classes. Protocol classes are defined as:: - - class Proto(Protocol): - def meth(self) -> int: - ... - - Such classes are primarily used with static type checkers that recognize - structural subtyping (static duck-typing), for example:: - - class C: - def meth(self) -> int: - return 0 - - def func(x: Proto) -> int: - return x.meth() - - func(C()) # Passes static type check - - See PEP 544 for details. Protocol classes decorated with - @typing_extensions.runtime act as simple-minded runtime protocol that checks - only the presence of given attributes, ignoring their type signatures. - - Protocol classes can be generic, they are defined as:: - - class GenProto(Protocol[T]): - def meth(self) -> T: - ... - """ - - __slots__ = () - _is_protocol = True - - def __new__(cls, *args, **kwds): - if cls is Protocol: - raise TypeError( - "Type Protocol cannot be instantiated; it can only be used as a base class" - ) - return super().__new__(cls) - - @_tp_cache - def __class_getitem__(cls, params): - if not isinstance(params, tuple): - params = (params,) - if not params and cls is not Tuple: - raise TypeError( - "Parameter list to {}[...] cannot be empty".format(cls.__qualname__) - ) - msg = "Parameters to generic types must be types." - params = tuple(_type_check(p, msg) for p in params) - if cls is Protocol: - # Generic can only be subscripted with unique type variables. - if not all(isinstance(p, TypeVar) for p in params): - i = 0 - while isinstance(params[i], TypeVar): - i += 1 - raise TypeError( - "Parameters to Protocol[...] must all be type variables." - " Parameter {} is {}".format(i + 1, params[i]) - ) - if len(set(params)) != len(params): - raise TypeError("Parameters to Protocol[...] must all be unique") - else: - # Subscripting a regular Generic subclass. - _check_generic(cls, params) - return _GenericAlias(cls, params) - - def __init_subclass__(cls, *args, **kwargs): - tvars = [] - if '__orig_bases__' in cls.__dict__: - error = Generic in cls.__orig_bases__ - else: - error = Generic in cls.__bases__ - if error: - raise TypeError("Cannot inherit from plain Generic") - if '__orig_bases__' in cls.__dict__: - tvars = _collect_type_vars(cls.__orig_bases__) - # Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn]. - # If found, tvars must be a subset of it. - # If not found, tvars is it. - # Also check for and reject plain Generic, - # and reject multiple Generic[...] and/or Protocol[...]. - gvars = None - for base in cls.__orig_bases__: - if isinstance(base, _GenericAlias) and base.__origin__ in (Generic, Protocol): - # for error messages - the_base = 'Generic' if base.__origin__ is Generic else 'Protocol' - if gvars is not None: - raise TypeError( - "Cannot inherit from Generic[...]" - " and/or Protocol[...] multiple types." - ) - gvars = base.__parameters__ - if gvars is None: - gvars = tvars - else: - tvarset = set(tvars) - gvarset = set(gvars) - if not tvarset <= gvarset: - s_vars = ', '.join(str(t) for t in tvars if t not in gvarset) - s_args = ', '.join(str(g) for g in gvars) - raise TypeError( - "Some type variables ({}) are not listed in {}[{}]".format( - s_vars, the_base, s_args - ) - ) - tvars = gvars - cls.__parameters__ = tuple(tvars) - - # Determine if this is a protocol or a concrete subclass. - if not cls.__dict__.get('_is_protocol', None): - cls._is_protocol = any(b is Protocol for b in cls.__bases__) - - # Set (or override) the protocol subclass hook. - def _proto_hook(other): - if not cls.__dict__.get('_is_protocol', None): - return NotImplemented - if not getattr(cls, '_is_runtime_protocol', False): - if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']: - return NotImplemented - raise TypeError( - "Instance and class checks can only be used with @runtime protocols" - ) - if not _is_callable_members_only(cls): - if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']: - return NotImplemented - raise TypeError("Protocols with non-method members don't support issubclass()") - if not isinstance(other, type): - # Same error as for issubclass(1, int) - raise TypeError('issubclass() arg 1 must be a class') - for attr in _get_protocol_attrs(cls): - for base in other.__mro__: - if attr in base.__dict__: - if base.__dict__[attr] is None: - return NotImplemented - break - annotations = getattr(base, '__annotations__', {}) - if ( - isinstance(annotations, typing.Mapping) - and attr in annotations - and isinstance(other, _ProtocolMeta) - and other._is_protocol - ): - break - else: - return NotImplemented - return True - - if '__subclasshook__' not in cls.__dict__: - cls.__subclasshook__ = _proto_hook - - # We have nothing more to do for non-protocols. - if not cls._is_protocol: - return - - # Check consistency of bases. - for base in cls.__bases__: - if not ( - base in (object, Generic) - or base.__module__ == 'collections.abc' - and base.__name__ in _PROTO_WHITELIST - or isinstance(base, _ProtocolMeta) - and base._is_protocol - ): - raise TypeError( - 'Protocols can only inherit from other protocols, got %r' % base - ) - cls.__init__ = _no_init - - -if hasattr(typing, 'runtime_checkable'): - runtime_checkable = typing.runtime_checkable -elif HAVE_PROTOCOLS: - - def runtime_checkable(cls): - """Mark a protocol class as a runtime protocol, so that it - can be used with isinstance() and issubclass(). Raise TypeError - if applied to a non-protocol class. - - This allows a simple-minded structural check very similar to the - one-offs in collections.abc such as Hashable. - """ - if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol: - raise TypeError( - '@runtime_checkable can be only applied to protocol classes, got %r' % cls - ) - cls._is_runtime_protocol = True - return cls - - -if HAVE_PROTOCOLS: - # Exists for backwards compatibility. - runtime = runtime_checkable - -if hasattr(typing, 'SupportsIndex'): - SupportsIndex = typing.SupportsIndex -elif HAVE_PROTOCOLS: - - @runtime_checkable - class SupportsIndex(Protocol): - __slots__ = () - - @abc.abstractmethod - def __index__(self) -> int: - pass - - -if sys.version_info >= (3, 9, 2): - # The standard library TypedDict in Python 3.8 does not store runtime information - # about which (if any) keys are optional. See https://bugs.python.org/issue38834 - # The standard library TypedDict in Python 3.9.0/1 does not honour the "total" - # keyword with old-style TypedDict(). See https://bugs.python.org/issue42059 - TypedDict = typing.TypedDict -else: - - def _check_fails(cls, other): - try: - if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools', 'typing']: - # Typed dicts are only for static structural subtyping. - raise TypeError('TypedDict does not support instance and class checks') - except (AttributeError, ValueError): - pass - return False - - def _dict_new(*args, **kwargs): - if not args: - raise TypeError('TypedDict.__new__(): not enough arguments') - _, args = args[0], args[1:] # allow the "cls" keyword be passed - return dict(*args, **kwargs) - - _dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)' - - def _typeddict_new(*args, total=True, **kwargs): - if not args: - raise TypeError('TypedDict.__new__(): not enough arguments') - _, args = args[0], args[1:] # allow the "cls" keyword be passed - if args: - typename, args = args[0], args[1:] # allow the "_typename" keyword be passed - elif '_typename' in kwargs: - typename = kwargs.pop('_typename') - import warnings - - warnings.warn( - "Passing '_typename' as keyword argument is deprecated", - DeprecationWarning, - stacklevel=2, - ) - else: - raise TypeError( - "TypedDict.__new__() missing 1 required positional argument: '_typename'" - ) - if args: - try: - (fields,) = args # allow the "_fields" keyword be passed - except ValueError: - raise TypeError( - 'TypedDict.__new__() takes from 2 to 3 ' - 'positional arguments but {} ' - 'were given'.format(len(args) + 2) - ) - elif '_fields' in kwargs and len(kwargs) == 1: - fields = kwargs.pop('_fields') - import warnings - - warnings.warn( - "Passing '_fields' as keyword argument is deprecated", - DeprecationWarning, - stacklevel=2, - ) - else: - fields = None - - if fields is None: - fields = kwargs - elif kwargs: - raise TypeError("TypedDict takes either a dict or keyword arguments, but not both") - - ns = {'__annotations__': dict(fields)} - try: - # Setting correct module is necessary to make typed dict classes pickleable. - ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass - - return _TypedDictMeta(typename, (), ns, total=total) - - _typeddict_new.__text_signature__ = ( - '($cls, _typename, _fields=None, /, *, total=True, **kwargs)' - ) - - class _TypedDictMeta(type): - def __init__(cls, name, bases, ns, total=True): - # In Python 3.4 and 3.5 the __init__ method also needs to support the keyword arguments. - # See https://www.python.org/dev/peps/pep-0487/#implementation-details - super(_TypedDictMeta, cls).__init__(name, bases, ns) - - def __new__(cls, name, bases, ns, total=True): - # Create new typed dict class object. - # This method is called directly when TypedDict is subclassed, - # or via _typeddict_new when TypedDict is instantiated. This way - # TypedDict supports all three syntaxes described in its docstring. - # Subclasses and instances of TypedDict return actual dictionaries - # via _dict_new. - ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new - tp_dict = super(_TypedDictMeta, cls).__new__(cls, name, (dict,), ns) - - annotations = {} - own_annotations = ns.get('__annotations__', {}) - own_annotation_keys = set(own_annotations.keys()) - msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" - own_annotations = {n: typing._type_check(tp, msg) for n, tp in own_annotations.items()} - required_keys = set() - optional_keys = set() - - for base in bases: - annotations.update(base.__dict__.get('__annotations__', {})) - required_keys.update(base.__dict__.get('__required_keys__', ())) - optional_keys.update(base.__dict__.get('__optional_keys__', ())) - - annotations.update(own_annotations) - if total: - required_keys.update(own_annotation_keys) - else: - optional_keys.update(own_annotation_keys) - - tp_dict.__annotations__ = annotations - tp_dict.__required_keys__ = frozenset(required_keys) - tp_dict.__optional_keys__ = frozenset(optional_keys) - if not hasattr(tp_dict, '__total__'): - tp_dict.__total__ = total - return tp_dict - - __instancecheck__ = __subclasscheck__ = _check_fails - - TypedDict = _TypedDictMeta('TypedDict', (dict,), {}) - TypedDict.__module__ = __name__ - TypedDict.__doc__ = """A simple typed name space. At runtime it is equivalent to a plain dict. - - TypedDict creates a dictionary type that expects all of its - instances to have a certain set of keys, with each key - associated with a value of a consistent type. This expectation - is not checked at runtime but is only enforced by type checkers. - Usage:: - - class Point2D(TypedDict): - x: int - y: int - label: str - - a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK - b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check - - assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') - - The type info can be accessed via the Point2D.__annotations__ dict, and - the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. - TypedDict supports two additional equivalent forms:: - - Point2D = TypedDict('Point2D', x=int, y=int, label=str) - Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) - - The class syntax is only supported in Python 3.6+, while two other - syntax forms work for Python 2.7 and 3.2+ - """ - -# Python 3.9+ has PEP 593 (Annotated and modified get_type_hints) -if hasattr(typing, 'Annotated'): - Annotated = typing.Annotated - get_type_hints = typing.get_type_hints - # Not exported and not a public API, but needed for get_origin() and get_args() - # to work. - _AnnotatedAlias = typing._AnnotatedAlias -elif PEP_560: - - class _AnnotatedAlias(typing._GenericAlias, _root=True): - """Runtime representation of an annotated type. - - At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't' - with extra annotations. The alias behaves like a normal typing alias, - instantiating is the same as instantiating the underlying type, binding - it to types is also the same. - """ - - def __init__(self, origin, metadata): - if isinstance(origin, _AnnotatedAlias): - metadata = origin.__metadata__ + metadata - origin = origin.__origin__ - super().__init__(origin, origin) - self.__metadata__ = metadata - - def copy_with(self, params): - assert len(params) == 1 - new_type = params[0] - return _AnnotatedAlias(new_type, self.__metadata__) - - def __repr__(self): - return "typing_extensions.Annotated[{}, {}]".format( - typing._type_repr(self.__origin__), ", ".join(repr(a) for a in self.__metadata__) - ) - - def __reduce__(self): - return operator.getitem, (Annotated, (self.__origin__,) + self.__metadata__) - - def __eq__(self, other): - if not isinstance(other, _AnnotatedAlias): - return NotImplemented - if self.__origin__ != other.__origin__: - return False - return self.__metadata__ == other.__metadata__ - - def __hash__(self): - return hash((self.__origin__, self.__metadata__)) - - class Annotated: - """Add context specific metadata to a type. - - Example: Annotated[int, runtime_check.Unsigned] indicates to the - hypothetical runtime_check module that this type is an unsigned int. - Every other consumer of this type can ignore this metadata and treat - this type as int. - - The first argument to Annotated must be a valid type (and will be in - the __origin__ field), the remaining arguments are kept as a tuple in - the __extra__ field. - - Details: - - - It's an error to call `Annotated` with less than two arguments. - - Nested Annotated are flattened:: - - Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3] - - - Instantiating an annotated type is equivalent to instantiating the - underlying type:: - - Annotated[C, Ann1](5) == C(5) - - - Annotated can be used as a generic type alias:: - - Optimized = Annotated[T, runtime.Optimize()] - Optimized[int] == Annotated[int, runtime.Optimize()] - - OptimizedList = Annotated[List[T], runtime.Optimize()] - OptimizedList[int] == Annotated[List[int], runtime.Optimize()] - """ - - __slots__ = () - - def __new__(cls, *args, **kwargs): - raise TypeError("Type Annotated cannot be instantiated.") - - @_tp_cache - def __class_getitem__(cls, params): - if not isinstance(params, tuple) or len(params) < 2: - raise TypeError( - "Annotated[...] should be used " - "with at least two arguments (a type and an " - "annotation)." - ) - msg = "Annotated[t, ...]: t must be a type." - origin = typing._type_check(params[0], msg) - metadata = tuple(params[1:]) - return _AnnotatedAlias(origin, metadata) - - def __init_subclass__(cls, *args, **kwargs): - raise TypeError("Cannot subclass {}.Annotated".format(cls.__module__)) - - def _strip_annotations(t): - """Strips the annotations from a given type.""" - if isinstance(t, _AnnotatedAlias): - return _strip_annotations(t.__origin__) - if isinstance(t, typing._GenericAlias): - stripped_args = tuple(_strip_annotations(a) for a in t.__args__) - if stripped_args == t.__args__: - return t - res = t.copy_with(stripped_args) - res._special = t._special - return res - return t - - def get_type_hints(obj, globalns=None, localns=None, include_extras=False): - """Return type hints for an object. - - This is often the same as obj.__annotations__, but it handles - forward references encoded as string literals, adds Optional[t] if a - default value equal to None is set and recursively replaces all - 'Annotated[T, ...]' with 'T' (unless 'include_extras=True'). - - The argument may be a module, class, method, or function. The annotations - are returned as a dictionary. For classes, annotations include also - inherited members. - - TypeError is raised if the argument is not of a type that can contain - annotations, and an empty dictionary is returned if no annotations are - present. - - BEWARE -- the behavior of globalns and localns is counterintuitive - (unless you are familiar with how eval() and exec() work). The - search order is locals first, then globals. - - - If no dict arguments are passed, an attempt is made to use the - globals from obj (or the respective module's globals for classes), - and these are also used as the locals. If the object does not appear - to have globals, an empty dictionary is used. - - - If one dict argument is passed, it is used for both globals and - locals. - - - If two dict arguments are passed, they specify globals and - locals, respectively. - """ - hint = typing.get_type_hints(obj, globalns=globalns, localns=localns) - if include_extras: - return hint - return {k: _strip_annotations(t) for k, t in hint.items()} - -elif HAVE_ANNOTATED: - - def _is_dunder(name): - """Returns True if name is a __dunder_variable_name__.""" - return len(name) > 4 and name.startswith('__') and name.endswith('__') - - # Prior to Python 3.7 types did not have `copy_with`. A lot of the equality - # checks, argument expansion etc. are done on the _subs_tree. As a result we - # can't provide a get_type_hints function that strips out annotations. - - class AnnotatedMeta(typing.GenericMeta): - """Metaclass for Annotated""" - - def __new__(cls, name, bases, namespace, **kwargs): - if any(b is not object for b in bases): - raise TypeError("Cannot subclass " + str(Annotated)) - return super().__new__(cls, name, bases, namespace, **kwargs) - - @property - def __metadata__(self): - return self._subs_tree()[2] - - def _tree_repr(self, tree): - cls, origin, metadata = tree - if not isinstance(origin, tuple): - tp_repr = typing._type_repr(origin) - else: - tp_repr = origin[0]._tree_repr(origin) - metadata_reprs = ", ".join(repr(arg) for arg in metadata) - return '%s[%s, %s]' % (cls, tp_repr, metadata_reprs) - - def _subs_tree(self, tvars=None, args=None): # noqa - if self is Annotated: - return Annotated - res = super()._subs_tree(tvars=tvars, args=args) - # Flatten nested Annotated - if isinstance(res[1], tuple) and res[1][0] is Annotated: - sub_tp = res[1][1] - sub_annot = res[1][2] - return (Annotated, sub_tp, sub_annot + res[2]) - return res - - def _get_cons(self): - """Return the class used to create instance of this type.""" - if self.__origin__ is None: - raise TypeError( - "Cannot get the underlying type of a non-specialized Annotated type." - ) - tree = self._subs_tree() - while isinstance(tree, tuple) and tree[0] is Annotated: - tree = tree[1] - if isinstance(tree, tuple): - return tree[0] - else: - return tree - - @_tp_cache - def __getitem__(self, params): - if not isinstance(params, tuple): - params = (params,) - if self.__origin__ is not None: # specializing an instantiated type - return super().__getitem__(params) - elif not isinstance(params, tuple) or len(params) < 2: - raise TypeError( - "Annotated[...] should be instantiated " - "with at least two arguments (a type and an " - "annotation)." - ) - else: - msg = "Annotated[t, ...]: t must be a type." - tp = typing._type_check(params[0], msg) - metadata = tuple(params[1:]) - return self.__class__( - self.__name__, - self.__bases__, - _no_slots_copy(self.__dict__), - tvars=_type_vars((tp,)), - # Metadata is a tuple so it won't be touched by _replace_args et al. - args=(tp, metadata), - origin=self, - ) - - def __call__(self, *args, **kwargs): - cons = self._get_cons() - result = cons(*args, **kwargs) - try: - result.__orig_class__ = self - except AttributeError: - pass - return result - - def __getattr__(self, attr): - # For simplicity we just don't relay all dunder names - if self.__origin__ is not None and not _is_dunder(attr): - return getattr(self._get_cons(), attr) - raise AttributeError(attr) - - def __setattr__(self, attr, value): - if _is_dunder(attr) or attr.startswith('_abc_'): - super().__setattr__(attr, value) - elif self.__origin__ is None: - raise AttributeError(attr) - else: - setattr(self._get_cons(), attr, value) - - def __instancecheck__(self, obj): - raise TypeError("Annotated cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("Annotated cannot be used with issubclass().") - - class Annotated(metaclass=AnnotatedMeta): - """Add context specific metadata to a type. - - Example: Annotated[int, runtime_check.Unsigned] indicates to the - hypothetical runtime_check module that this type is an unsigned int. - Every other consumer of this type can ignore this metadata and treat - this type as int. - - The first argument to Annotated must be a valid type, the remaining - arguments are kept as a tuple in the __metadata__ field. - - Details: - - - It's an error to call `Annotated` with less than two arguments. - - Nested Annotated are flattened:: - - Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3] - - - Instantiating an annotated type is equivalent to instantiating the - underlying type:: - - Annotated[C, Ann1](5) == C(5) - - - Annotated can be used as a generic type alias:: - - Optimized = Annotated[T, runtime.Optimize()] - Optimized[int] == Annotated[int, runtime.Optimize()] - - OptimizedList = Annotated[List[T], runtime.Optimize()] - OptimizedList[int] == Annotated[List[int], runtime.Optimize()] - """ - - -# Python 3.8 has get_origin() and get_args() but those implementations aren't -# Annotated-aware, so we can't use those, only Python 3.9 versions will do. -# Similarly, Python 3.9's implementation doesn't support ParamSpecArgs and -# ParamSpecKwargs. -if sys.version_info[:2] >= (3, 10): - get_origin = typing.get_origin - get_args = typing.get_args -elif PEP_560: - from typing import _GenericAlias - - try: - # 3.9+ - from typing import _BaseGenericAlias - except ImportError: - _BaseGenericAlias = _GenericAlias - try: - # 3.9+ - from typing import GenericAlias - except ImportError: - GenericAlias = _GenericAlias - - def get_origin(tp): - """Get the unsubscripted version of a type. - - This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar - and Annotated. Return None for unsupported types. Examples:: - - get_origin(Literal[42]) is Literal - get_origin(int) is None - get_origin(ClassVar[int]) is ClassVar - get_origin(Generic) is Generic - get_origin(Generic[T]) is Generic - get_origin(Union[T, int]) is Union - get_origin(List[Tuple[T, T]][int]) == list - get_origin(P.args) is P - """ - if isinstance(tp, _AnnotatedAlias): - return Annotated - if isinstance( - tp, (_GenericAlias, GenericAlias, _BaseGenericAlias, ParamSpecArgs, ParamSpecKwargs) - ): - return tp.__origin__ - if tp is Generic: - return Generic - return None - - def get_args(tp): - """Get type arguments with all substitutions performed. - - For unions, basic simplifications used by Union constructor are performed. - Examples:: - get_args(Dict[str, int]) == (str, int) - get_args(int) == () - get_args(Union[int, Union[T, int], str][int]) == (int, str) - get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) - get_args(Callable[[], T][int]) == ([], int) - """ - if isinstance(tp, _AnnotatedAlias): - return (tp.__origin__,) + tp.__metadata__ - if isinstance(tp, (_GenericAlias, GenericAlias)): - if getattr(tp, "_special", False): - return () - res = tp.__args__ - if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: - res = (list(res[:-1]), res[-1]) - return res - return () - - -if hasattr(typing, 'TypeAlias'): - TypeAlias = typing.TypeAlias -elif sys.version_info[:2] >= (3, 9): - - class _TypeAliasForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - - @_TypeAliasForm - def TypeAlias(self, parameters): - """Special marker indicating that an assignment should - be recognized as a proper type alias definition by type - checkers. - - For example:: - - Predicate: TypeAlias = Callable[..., bool] - - It's invalid when used anywhere except as in the example above. - """ - raise TypeError("{} is not subscriptable".format(self)) - -elif sys.version_info[:2] >= (3, 7): - - class _TypeAliasForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - - TypeAlias = _TypeAliasForm( - 'TypeAlias', - doc="""Special marker indicating that an assignment should - be recognized as a proper type alias definition by type - checkers. - - For example:: - - Predicate: TypeAlias = Callable[..., bool] - - It's invalid when used anywhere except as in the example - above.""", - ) - -elif hasattr(typing, '_FinalTypingBase'): - - class _TypeAliasMeta(typing.TypingMeta): - """Metaclass for TypeAlias""" - - def __repr__(self): - return 'typing_extensions.TypeAlias' - - class _TypeAliasBase(typing._FinalTypingBase, metaclass=_TypeAliasMeta, _root=True): - """Special marker indicating that an assignment should - be recognized as a proper type alias definition by type - checkers. - - For example:: - - Predicate: TypeAlias = Callable[..., bool] - - It's invalid when used anywhere except as in the example above. - """ - - __slots__ = () - - def __instancecheck__(self, obj): - raise TypeError("TypeAlias cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("TypeAlias cannot be used with issubclass().") - - def __repr__(self): - return 'typing_extensions.TypeAlias' - - TypeAlias = _TypeAliasBase(_root=True) -else: - - class _TypeAliasMeta(typing.TypingMeta): - """Metaclass for TypeAlias""" - - def __instancecheck__(self, obj): - raise TypeError("TypeAlias cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("TypeAlias cannot be used with issubclass().") - - def __call__(self, *args, **kwargs): - raise TypeError("Cannot instantiate TypeAlias") - - class TypeAlias(metaclass=_TypeAliasMeta, _root=True): - """Special marker indicating that an assignment should - be recognized as a proper type alias definition by type - checkers. - - For example:: - - Predicate: TypeAlias = Callable[..., bool] - - It's invalid when used anywhere except as in the example above. - """ - - __slots__ = () - - -# Python 3.10+ has PEP 612 -if hasattr(typing, 'ParamSpecArgs'): - ParamSpecArgs = typing.ParamSpecArgs - ParamSpecKwargs = typing.ParamSpecKwargs -else: - - class _Immutable: - """Mixin to indicate that object should not be copied.""" - - __slots__ = () - - def __copy__(self): - return self - - def __deepcopy__(self, memo): - return self - - class ParamSpecArgs(_Immutable): - """The args for a ParamSpec object. - - Given a ParamSpec object P, P.args is an instance of ParamSpecArgs. - - ParamSpecArgs objects have a reference back to their ParamSpec: - - P.args.__origin__ is P - - This type is meant for runtime introspection and has no special meaning to - static type checkers. - """ - - def __init__(self, origin): - self.__origin__ = origin - - def __repr__(self): - return "{}.args".format(self.__origin__.__name__) - - class ParamSpecKwargs(_Immutable): - """The kwargs for a ParamSpec object. - - Given a ParamSpec object P, P.kwargs is an instance of ParamSpecKwargs. - - ParamSpecKwargs objects have a reference back to their ParamSpec: - - P.kwargs.__origin__ is P - - This type is meant for runtime introspection and has no special meaning to - static type checkers. - """ - - def __init__(self, origin): - self.__origin__ = origin - - def __repr__(self): - return "{}.kwargs".format(self.__origin__.__name__) - - -if hasattr(typing, 'ParamSpec'): - ParamSpec = typing.ParamSpec -else: - # Inherits from list as a workaround for Callable checks in Python < 3.9.2. - class ParamSpec(list): - """Parameter specification variable. - - Usage:: - - P = ParamSpec('P') - - Parameter specification variables exist primarily for the benefit of static - type checkers. They are used to forward the parameter types of one - callable to another callable, a pattern commonly found in higher order - functions and decorators. They are only valid when used in ``Concatenate``, - or s the first argument to ``Callable``. In Python 3.10 and higher, - they are also supported in user-defined Generics at runtime. - See class Generic for more information on generic types. An - example for annotating a decorator:: - - T = TypeVar('T') - P = ParamSpec('P') - - def add_logging(f: Callable[P, T]) -> Callable[P, T]: - '''A type-safe decorator to add logging to a function.''' - def inner(*args: P.args, **kwargs: P.kwargs) -> T: - logging.info(f'{f.__name__} was called') - return f(*args, **kwargs) - return inner - - @add_logging - def add_two(x: float, y: float) -> float: - '''Add two numbers together.''' - return x + y - - Parameter specification variables defined with covariant=True or - contravariant=True can be used to declare covariant or contravariant - generic types. These keyword arguments are valid, but their actual semantics - are yet to be decided. See PEP 612 for details. - - Parameter specification variables can be introspected. e.g.: - - P.__name__ == 'T' - P.__bound__ == None - P.__covariant__ == False - P.__contravariant__ == False - - Note that only parameter specification variables defined in global scope can - be pickled. - """ - - # Trick Generic __parameters__. - __class__ = TypeVar - - @property - def args(self): - return ParamSpecArgs(self) - - @property - def kwargs(self): - return ParamSpecKwargs(self) - - def __init__(self, name, *, bound=None, covariant=False, contravariant=False): - super().__init__([self]) - self.__name__ = name - self.__covariant__ = bool(covariant) - self.__contravariant__ = bool(contravariant) - if bound: - self.__bound__ = typing._type_check(bound, 'Bound must be a type.') - else: - self.__bound__ = None - - # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None - if def_mod != 'typing_extensions': - self.__module__ = def_mod - - def __repr__(self): - if self.__covariant__: - prefix = '+' - elif self.__contravariant__: - prefix = '-' - else: - prefix = '~' - return prefix + self.__name__ - - def __hash__(self): - return object.__hash__(self) - - def __eq__(self, other): - return self is other - - def __reduce__(self): - return self.__name__ - - # Hack to get typing._type_check to pass. - def __call__(self, *args, **kwargs): - pass - - if not PEP_560: - # Only needed in 3.6 and lower. - def _get_type_vars(self, tvars): - if self not in tvars: - tvars.append(self) - - -# Inherits from list as a workaround for Callable checks in Python < 3.9.2. -class _ConcatenateGenericAlias(list): - # Trick Generic into looking into this for __parameters__. - if PEP_560: - __class__ = _GenericAlias - elif sys.version_info[:3] == (3, 5, 2): - __class__ = typing.TypingMeta - else: - __class__ = typing._TypingBase - - # Flag in 3.8. - _special = False - # Attribute in 3.6 and earlier. - if sys.version_info[:3] == (3, 5, 2): - _gorg = typing.GenericMeta - else: - _gorg = typing.Generic - - def __init__(self, origin, args): - super().__init__(args) - self.__origin__ = origin - self.__args__ = args - - def __repr__(self): - _type_repr = typing._type_repr - return '{origin}[{args}]'.format( - origin=_type_repr(self.__origin__), - args=', '.join(_type_repr(arg) for arg in self.__args__), - ) - - def __hash__(self): - return hash((self.__origin__, self.__args__)) - - # Hack to get typing._type_check to pass in Generic. - def __call__(self, *args, **kwargs): - pass - - @property - def __parameters__(self): - return tuple(tp for tp in self.__args__ if isinstance(tp, (TypeVar, ParamSpec))) - - if not PEP_560: - # Only required in 3.6 and lower. - def _get_type_vars(self, tvars): - if self.__origin__ and self.__parameters__: - typing._get_type_vars(self.__parameters__, tvars) - - -@_tp_cache -def _concatenate_getitem(self, parameters): - if parameters == (): - raise TypeError("Cannot take a Concatenate of no types.") - if not isinstance(parameters, tuple): - parameters = (parameters,) - if not isinstance(parameters[-1], ParamSpec): - raise TypeError("The last parameter to Concatenate should be a ParamSpec variable.") - msg = "Concatenate[arg, ...]: each arg must be a type." - parameters = tuple(typing._type_check(p, msg) for p in parameters) - return _ConcatenateGenericAlias(self, parameters) - - -if hasattr(typing, 'Concatenate'): - Concatenate = typing.Concatenate - _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa -elif sys.version_info[:2] >= (3, 9): - - @_TypeAliasForm - def Concatenate(self, parameters): - """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a - higher order function which adds, removes or transforms parameters of a - callable. - - For example:: - - Callable[Concatenate[int, P], int] - - See PEP 612 for detailed information. - """ - return _concatenate_getitem(self, parameters) - -elif sys.version_info[:2] >= (3, 7): - - class _ConcatenateForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - - def __getitem__(self, parameters): - return _concatenate_getitem(self, parameters) - - Concatenate = _ConcatenateForm( - 'Concatenate', - doc="""Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a - higher order function which adds, removes or transforms parameters of a - callable. - - For example:: - - Callable[Concatenate[int, P], int] - - See PEP 612 for detailed information. - """, - ) - -elif hasattr(typing, '_FinalTypingBase'): - - class _ConcatenateAliasMeta(typing.TypingMeta): - """Metaclass for Concatenate.""" - - def __repr__(self): - return 'typing_extensions.Concatenate' - - class _ConcatenateAliasBase( - typing._FinalTypingBase, metaclass=_ConcatenateAliasMeta, _root=True - ): - """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a - higher order function which adds, removes or transforms parameters of a - callable. - - For example:: - - Callable[Concatenate[int, P], int] - - See PEP 612 for detailed information. - """ - - __slots__ = () - - def __instancecheck__(self, obj): - raise TypeError("Concatenate cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("Concatenate cannot be used with issubclass().") - - def __repr__(self): - return 'typing_extensions.Concatenate' - - def __getitem__(self, parameters): - return _concatenate_getitem(self, parameters) - - Concatenate = _ConcatenateAliasBase(_root=True) -# For 3.5.0 - 3.5.2 -else: - - class _ConcatenateAliasMeta(typing.TypingMeta): - """Metaclass for Concatenate.""" - - def __instancecheck__(self, obj): - raise TypeError("TypeAlias cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("TypeAlias cannot be used with issubclass().") - - def __call__(self, *args, **kwargs): - raise TypeError("Cannot instantiate TypeAlias") - - def __getitem__(self, parameters): - return _concatenate_getitem(self, parameters) - - class Concatenate(metaclass=_ConcatenateAliasMeta, _root=True): - """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a - higher order function which adds, removes or transforms parameters of a - callable. - - For example:: - - Callable[Concatenate[int, P], int] - - See PEP 612 for detailed information. - """ - - __slots__ = () - - -if hasattr(typing, 'TypeGuard'): - TypeGuard = typing.TypeGuard -elif sys.version_info[:2] >= (3, 9): - - class _TypeGuardForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - - @_TypeGuardForm - def TypeGuard(self, parameters): - """Special typing form used to annotate the return type of a user-defined - type guard function. ``TypeGuard`` only accepts a single type argument. - At runtime, functions marked this way should return a boolean. - - ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static - type checkers to determine a more precise type of an expression within a - program's code flow. Usually type narrowing is done by analyzing - conditional code flow and applying the narrowing to a block of code. The - conditional expression here is sometimes referred to as a "type guard". - - Sometimes it would be convenient to use a user-defined boolean function - as a type guard. Such a function should use ``TypeGuard[...]`` as its - return type to alert static type checkers to this intention. - - Using ``-> TypeGuard`` tells the static type checker that for a given - function: - - 1. The return value is a boolean. - 2. If the return value is ``True``, the type of its argument - is the type inside ``TypeGuard``. - - For example:: - - def is_str(val: Union[str, float]): - # "isinstance" type guard - if isinstance(val, str): - # Type of ``val`` is narrowed to ``str`` - ... - else: - # Else, type of ``val`` is narrowed to ``float``. - ... - - Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower - form of ``TypeA`` (it can even be a wider form) and this may lead to - type-unsafe results. The main reason is to allow for things like - narrowing ``List[object]`` to ``List[str]`` even though the latter is not - a subtype of the former, since ``List`` is invariant. The responsibility of - writing type-safe type guards is left to the user. - - ``TypeGuard`` also works with type variables. For more information, see - PEP 647 (User-Defined Type Guards). - """ - item = typing._type_check(parameters, '{} accepts only single type.'.format(self)) - return _GenericAlias(self, (item,)) - -elif sys.version_info[:2] >= (3, 7): - - class _TypeGuardForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - - def __getitem__(self, parameters): - item = typing._type_check( - parameters, '{} accepts only a single type'.format(self._name) - ) - return _GenericAlias(self, (item,)) - - TypeGuard = _TypeGuardForm( - 'TypeGuard', - doc="""Special typing form used to annotate the return type of a user-defined - type guard function. ``TypeGuard`` only accepts a single type argument. - At runtime, functions marked this way should return a boolean. - - ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static - type checkers to determine a more precise type of an expression within a - program's code flow. Usually type narrowing is done by analyzing - conditional code flow and applying the narrowing to a block of code. The - conditional expression here is sometimes referred to as a "type guard". - - Sometimes it would be convenient to use a user-defined boolean function - as a type guard. Such a function should use ``TypeGuard[...]`` as its - return type to alert static type checkers to this intention. - - Using ``-> TypeGuard`` tells the static type checker that for a given - function: - - 1. The return value is a boolean. - 2. If the return value is ``True``, the type of its argument - is the type inside ``TypeGuard``. - - For example:: - - def is_str(val: Union[str, float]): - # "isinstance" type guard - if isinstance(val, str): - # Type of ``val`` is narrowed to ``str`` - ... - else: - # Else, type of ``val`` is narrowed to ``float``. - ... - - Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower - form of ``TypeA`` (it can even be a wider form) and this may lead to - type-unsafe results. The main reason is to allow for things like - narrowing ``List[object]`` to ``List[str]`` even though the latter is not - a subtype of the former, since ``List`` is invariant. The responsibility of - writing type-safe type guards is left to the user. - - ``TypeGuard`` also works with type variables. For more information, see - PEP 647 (User-Defined Type Guards). - """, - ) -elif hasattr(typing, '_FinalTypingBase'): - - class _TypeGuard(typing._FinalTypingBase, _root=True): - """Special typing form used to annotate the return type of a user-defined - type guard function. ``TypeGuard`` only accepts a single type argument. - At runtime, functions marked this way should return a boolean. - - ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static - type checkers to determine a more precise type of an expression within a - program's code flow. Usually type narrowing is done by analyzing - conditional code flow and applying the narrowing to a block of code. The - conditional expression here is sometimes referred to as a "type guard". - - Sometimes it would be convenient to use a user-defined boolean function - as a type guard. Such a function should use ``TypeGuard[...]`` as its - return type to alert static type checkers to this intention. - - Using ``-> TypeGuard`` tells the static type checker that for a given - function: - - 1. The return value is a boolean. - 2. If the return value is ``True``, the type of its argument - is the type inside ``TypeGuard``. - - For example:: - - def is_str(val: Union[str, float]): - # "isinstance" type guard - if isinstance(val, str): - # Type of ``val`` is narrowed to ``str`` - ... - else: - # Else, type of ``val`` is narrowed to ``float``. - ... - - Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower - form of ``TypeA`` (it can even be a wider form) and this may lead to - type-unsafe results. The main reason is to allow for things like - narrowing ``List[object]`` to ``List[str]`` even though the latter is not - a subtype of the former, since ``List`` is invariant. The responsibility of - writing type-safe type guards is left to the user. - - ``TypeGuard`` also works with type variables. For more information, see - PEP 647 (User-Defined Type Guards). - """ - - __slots__ = ('__type__',) - - def __init__(self, tp=None, **kwds): - self.__type__ = tp - - def __getitem__(self, item): - cls = type(self) - if self.__type__ is None: - return cls( - typing._type_check( - item, '{} accepts only a single type.'.format(cls.__name__[1:]) - ), - _root=True, - ) - raise TypeError('{} cannot be further subscripted'.format(cls.__name__[1:])) - - def _eval_type(self, globalns, localns): - new_tp = typing._eval_type(self.__type__, globalns, localns) - if new_tp == self.__type__: - return self - return type(self)(new_tp, _root=True) - - def __repr__(self): - r = super().__repr__() - if self.__type__ is not None: - r += '[{}]'.format(typing._type_repr(self.__type__)) - return r - - def __hash__(self): - return hash((type(self).__name__, self.__type__)) - - def __eq__(self, other): - if not isinstance(other, _TypeGuard): - return NotImplemented - if self.__type__ is not None: - return self.__type__ == other.__type__ - return self is other - - TypeGuard = _TypeGuard(_root=True) -else: - - class _TypeGuardMeta(typing.TypingMeta): - """Metaclass for TypeGuard""" - - def __new__(cls, name, bases, namespace, tp=None, _root=False): - self = super().__new__(cls, name, bases, namespace, _root=_root) - if tp is not None: - self.__type__ = tp - return self - - def __instancecheck__(self, obj): - raise TypeError("TypeGuard cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("TypeGuard cannot be used with issubclass().") - - def __getitem__(self, item): - cls = type(self) - if self.__type__ is not None: - raise TypeError('{} cannot be further subscripted'.format(cls.__name__[1:])) - - param = typing._type_check( - item, '{} accepts only single type.'.format(cls.__name__[1:]) - ) - return cls(self.__name__, self.__bases__, dict(self.__dict__), tp=param, _root=True) - - def _eval_type(self, globalns, localns): - new_tp = typing._eval_type(self.__type__, globalns, localns) - if new_tp == self.__type__: - return self - return type(self)( - self.__name__, self.__bases__, dict(self.__dict__), tp=self.__type__, _root=True - ) - - def __repr__(self): - r = super().__repr__() - if self.__type__ is not None: - r += '[{}]'.format(typing._type_repr(self.__type__)) - return r - - def __hash__(self): - return hash((type(self).__name__, self.__type__)) - - def __eq__(self, other): - if not hasattr(other, "__type__"): - return NotImplemented - if self.__type__ is not None: - return self.__type__ == other.__type__ - return self is other - - class TypeGuard(typing.Final, metaclass=_TypeGuardMeta, _root=True): - """Special typing form used to annotate the return type of a user-defined - type guard function. ``TypeGuard`` only accepts a single type argument. - At runtime, functions marked this way should return a boolean. - - ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static - type checkers to determine a more precise type of an expression within a - program's code flow. Usually type narrowing is done by analyzing - conditional code flow and applying the narrowing to a block of code. The - conditional expression here is sometimes referred to as a "type guard". - - Sometimes it would be convenient to use a user-defined boolean function - as a type guard. Such a function should use ``TypeGuard[...]`` as its - return type to alert static type checkers to this intention. - - Using ``-> TypeGuard`` tells the static type checker that for a given - function: - - 1. The return value is a boolean. - 2. If the return value is ``True``, the type of its argument - is the type inside ``TypeGuard``. - - For example:: - - def is_str(val: Union[str, float]): - # "isinstance" type guard - if isinstance(val, str): - # Type of ``val`` is narrowed to ``str`` - ... - else: - # Else, type of ``val`` is narrowed to ``float``. - ... - - Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower - form of ``TypeA`` (it can even be a wider form) and this may lead to - type-unsafe results. The main reason is to allow for things like - narrowing ``List[object]`` to ``List[str]`` even though the latter is not - a subtype of the former, since ``List`` is invariant. The responsibility of - writing type-safe type guards is left to the user. - - ``TypeGuard`` also works with type variables. For more information, see - PEP 647 (User-Defined Type Guards). - """ - - __type__ = None diff --git a/podman/tests/integration/test_containers.py b/podman/tests/integration/test_containers.py index dd596693..dbb63aeb 100644 --- a/podman/tests/integration/test_containers.py +++ b/podman/tests/integration/test_containers.py @@ -9,7 +9,7 @@ from collections.abc import Iterator except ImportError: # Python < 3.10 - from collections import Iterator + from collections.abc import Iterator import podman.tests.integration.base as base from podman import PodmanClient diff --git a/podman/tests/integration/test_images.py b/podman/tests/integration/test_images.py index 9f712f14..cbfcd454 100644 --- a/podman/tests/integration/test_images.py +++ b/podman/tests/integration/test_images.py @@ -133,7 +133,7 @@ def test_search(self): @unittest.skip("Needs Podman 3.1.0") def test_corrupt_load(self): with self.assertRaises(APIError) as e: - next(self.client.images.load("This is a corrupt tarball".encode("utf-8"))) + next(self.client.images.load(b"This is a corrupt tarball")) self.assertIn("payload does not match", e.exception.explanation) def test_build(self): diff --git a/podman/tests/unit/test_build.py b/podman/tests/unit/test_build.py index b29371f8..6adf0fe8 100644 --- a/podman/tests/unit/test_build.py +++ b/podman/tests/unit/test_build.py @@ -7,7 +7,7 @@ from collections.abc import Iterable except ImportError: # Python < 3.10 - from collections import Iterable + from collections.abc import Iterable from unittest.mock import patch import requests_mock diff --git a/podman/tests/unit/test_container.py b/podman/tests/unit/test_container.py index 90708d95..b38ea483 100644 --- a/podman/tests/unit/test_container.py +++ b/podman/tests/unit/test_container.py @@ -8,7 +8,7 @@ from collections.abc import Iterable except ImportError: # Python < 3.10 - from collections import Iterable + from collections.abc import Iterable import requests_mock diff --git a/podman/tests/unit/test_containersmanager.py b/podman/tests/unit/test_containersmanager.py index 76e06dc0..f72ac226 100644 --- a/podman/tests/unit/test_containersmanager.py +++ b/podman/tests/unit/test_containersmanager.py @@ -6,7 +6,7 @@ from collections.abc import Iterator except ImportError: # Python < 3.10 - from collections import Iterator + from collections.abc import Iterator from unittest.mock import DEFAULT, patch, MagicMock diff --git a/podman/tests/unit/test_imagesmanager.py b/podman/tests/unit/test_imagesmanager.py index 008228d6..22214d15 100644 --- a/podman/tests/unit/test_imagesmanager.py +++ b/podman/tests/unit/test_imagesmanager.py @@ -7,7 +7,7 @@ from collections.abc import Iterable except ImportError: # Python < 3.10 - from collections import Iterable + from collections.abc import Iterable import requests_mock diff --git a/rpm/python-podman.spec b/rpm/python-podman.spec index 75ff19e1..193c3fcd 100644 --- a/rpm/python-podman.spec +++ b/rpm/python-podman.spec @@ -81,10 +81,7 @@ export PBR_VERSION="0.0.0" %pyproject_save_files %{pypi_name} %endif -%if !%{defined rhel8_py} %check -%pyproject_check_import -e podman.api.typing_extensions -%endif %if %{defined rhel8_py} %files -n python%{python3_pkgversion}-%{pypi_name} diff --git a/ruff.toml b/ruff.toml index 6f522409..3eb9a1e6 100644 --- a/ruff.toml +++ b/ruff.toml @@ -18,7 +18,7 @@ select = [ "E", # Pycodestyle Error "W", # Pycodestyle Warning "N", # PEP8 Naming - # TODO "UP", # Pyupgrade + "UP", # Pyupgrade # TODO "ANN", # TODO "S", # Bandit # "B", # Bugbear From 896fc53c97788d08cbcd043421ddcab4f65118c1 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 17:06:42 +0100 Subject: [PATCH 36/48] Fix Bugbear B028: no-explicit-stacklevel It is recommended to use 2 or more to give the caller more context about warning Signed-off-by: Nicola Sella --- podman/errors/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/podman/errors/__init__.py b/podman/errors/__init__.py index 9a339112..ae8d9fa0 100644 --- a/podman/errors/__init__.py +++ b/podman/errors/__init__.py @@ -48,7 +48,9 @@ class NotFoundError(HTTPException): def __init__(self, message, response=None): super().__init__(message) self.response = response - warnings.warn("APIConnection() and supporting classes.", PendingDeprecationWarning) + warnings.warn( + "APIConnection() and supporting classes.", PendingDeprecationWarning, stacklevel=2 + ) # If found, use new ImageNotFound otherwise old class @@ -100,7 +102,9 @@ class RequestError(HTTPException): def __init__(self, message, response=None): super().__init__(message) self.response = response - warnings.warn("APIConnection() and supporting classes.", PendingDeprecationWarning) + warnings.warn( + "APIConnection() and supporting classes.", PendingDeprecationWarning, stacklevel=2 + ) class InternalServerError(HTTPException): @@ -112,4 +116,6 @@ class InternalServerError(HTTPException): def __init__(self, message, response=None): super().__init__(message) self.response = response - warnings.warn("APIConnection() and supporting classes.", PendingDeprecationWarning) + warnings.warn( + "APIConnection() and supporting classes.", PendingDeprecationWarning, stacklevel=2 + ) From 11944f9af4918b702690555f081c21a932dc59b2 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 13 Dec 2024 13:49:56 +0100 Subject: [PATCH 37/48] Silence Bugbear B024 B024: abstract-base-class-without-abstract-method Usually, abstract classes with no abstract methods are flagged as incorrectly implemented because @abstract might have been forgotten. PodmanResource should be kept an abstract class to prevent the initialization of an instance of it Signed-off-by: Nicola Sella --- podman/domain/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podman/domain/manager.py b/podman/domain/manager.py index 9be479d9..a9bda9f1 100644 --- a/podman/domain/manager.py +++ b/podman/domain/manager.py @@ -10,7 +10,7 @@ PodmanResourceType: TypeVar = TypeVar("PodmanResourceType", bound="PodmanResource") -class PodmanResource(ABC): +class PodmanResource(ABC): # noqa: B024 """Base class for representing resource of a Podman service. Attributes: From adf01fe148dd3ce6bacb7b898159572dbe8ebc99 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 17:16:30 +0100 Subject: [PATCH 38/48] Enable Bugbear Checks Bugbear checks usually check for design problems within the code. https://pypi.org/project/flake8-bugbear/ Signed-off-by: Nicola Sella --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 3eb9a1e6..60bfce3c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -21,7 +21,7 @@ select = [ "UP", # Pyupgrade # TODO "ANN", # TODO "S", # Bandit - # "B", # Bugbear + "B", # Bugbear "A", # flake-8-builtins "YTT", # flake-8-2020 "PLC", # Pylint Convention From 7e20dc7cc59ca6dca8e325beef97034f3cc1e628 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 17:23:11 +0100 Subject: [PATCH 39/48] Suppress Bandit S108: hardcoded-temp-file This could be an exception and should be checked in the future but it is suppressed at the moment. Signed-off-by: Nicola Sella --- ruff.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruff.toml b/ruff.toml index 60bfce3c..6c96389b 100644 --- a/ruff.toml +++ b/ruff.toml @@ -37,6 +37,9 @@ ignore = [ "N818", # TODO Error Suffix in exception name # This can lead to API breaking changes so it's disabled for now "N80", # TODO Invalid Name + # TODO this error fails on one file and it's necessary to address + # the issue properly on a specific PR + "S108" ] [lint.per-file-ignores] "podman/tests/*.py" = ["S"] From 031a277974532c451ceef52d797073dd4c34d85c Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 17:27:29 +0100 Subject: [PATCH 40/48] Suppress Bandit S603 S603: subprocess-without-shell-equals-true This could be an exception or a false positive and since it's used on one single piece of code it is ok to ignore from now. Signed-off-by: Nicola Sella --- ruff.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 6c96389b..3695cb26 100644 --- a/ruff.toml +++ b/ruff.toml @@ -39,7 +39,9 @@ ignore = [ "N80", # TODO Invalid Name # TODO this error fails on one file and it's necessary to address # the issue properly on a specific PR - "S108" + "S108", + # TODO This is probably a false positive + "S603", ] [lint.per-file-ignores] "podman/tests/*.py" = ["S"] From 83e3abdf8330d8fc4195ea71255d839eb43d3938 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 29 Nov 2024 17:27:45 +0100 Subject: [PATCH 41/48] Enable Flake Bandit checks Bandit provides security checks and good practices suggestions for the codebase. https://pypi.org/project/flake8-bandit/ Signed-off-by: Nicola Sella --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 3695cb26..84bceb20 100644 --- a/ruff.toml +++ b/ruff.toml @@ -20,7 +20,7 @@ select = [ "N", # PEP8 Naming "UP", # Pyupgrade # TODO "ANN", - # TODO "S", # Bandit + "S", # Bandit "B", # Bugbear "A", # flake-8-builtins "YTT", # flake-8-2020 From eff688ec3a9c63edbbbf0b0e4b9da4e290b67467 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Mon, 2 Dec 2024 15:28:16 +0100 Subject: [PATCH 42/48] Update Ruff version Signed-off-by: Nicola Sella --- .pre-commit-config.yaml | 2 +- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67fc4d6c..d9f5e2bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.6.0 + rev: v0.8.1 hooks: # Run the linter. - id: ruff diff --git a/tox.ini b/tox.ini index 66e6288a..25169f8c 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ setenv = commands = {posargs} [testenv:lint] -depends = ruff +deps = ruff==0.8.1 allowlist_externals = ruff commands = ruff check --diff @@ -30,7 +30,7 @@ commands = coverage report -m --skip-covered --fail-under=80 --omit=podman/tests/* --omit=.tox/* [testenv:format] -deps = ruff +deps = ruff==0.8.1 allowlist_externals = ruff commands = ruff format --diff From feaf5fc8bfe8f01b5ade284985b6ddda8fed9fea Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Mon, 2 Dec 2024 15:29:27 +0100 Subject: [PATCH 43/48] Fix Code based on ruff 0.3->0.8.1 Fix errors for tyope annotation for list, dict, type and tuple Example: UP006: Use `list` instead of `List` for type annotation Signed-off-by: Nicola Sella --- podman/api/adapter_utils.py | 3 +- podman/api/client.py | 19 +++++-- podman/api/http_utils.py | 11 ++-- podman/api/parse_utils.py | 11 ++-- podman/api/tar_utils.py | 8 +-- podman/client.py | 8 +-- podman/domain/config.py | 8 +-- podman/domain/containers.py | 28 +++++----- podman/domain/containers_create.py | 79 ++++++++++++++------------- podman/domain/containers_manager.py | 13 +++-- podman/domain/containers_run.py | 5 +- podman/domain/events.py | 9 +-- podman/domain/images.py | 7 ++- podman/domain/images_build.py | 13 +++-- podman/domain/images_manager.py | 47 +++++++++------- podman/domain/ipam.py | 5 +- podman/domain/manager.py | 5 +- podman/domain/manifests.py | 18 +++--- podman/domain/networks_manager.py | 24 ++++---- podman/domain/pods.py | 6 +- podman/domain/pods_manager.py | 28 +++++----- podman/domain/registry_data.py | 3 +- podman/domain/secrets.py | 5 +- podman/domain/system.py | 10 ++-- podman/domain/volumes.py | 14 ++--- podman/errors/exceptions.py | 5 +- podman/tests/integration/utils.py | 4 +- podman/tests/unit/test_parse_utils.py | 5 +- podman/tests/unit/test_podsmanager.py | 2 +- 29 files changed, 218 insertions(+), 185 deletions(-) diff --git a/podman/api/adapter_utils.py b/podman/api/adapter_utils.py index 2ec7cf15..b5a92f48 100644 --- a/podman/api/adapter_utils.py +++ b/podman/api/adapter_utils.py @@ -1,6 +1,7 @@ """Utility functions for working with Adapters.""" -from typing import NamedTuple, Mapping +from typing import NamedTuple +from collections.abc import Mapping def _key_normalizer(key_class: NamedTuple, request_context: Mapping) -> Mapping: diff --git a/podman/api/client.py b/podman/api/client.py index f38e83f8..3fa26ff3 100644 --- a/podman/api/client.py +++ b/podman/api/client.py @@ -3,7 +3,14 @@ import json import warnings import urllib.parse -from typing import Any, ClassVar, IO, Iterable, List, Mapping, Optional, Tuple, Type, Union +from typing import ( + Any, + ClassVar, + IO, + Optional, + Union, +) +from collections.abc import Iterable, Mapping import requests from requests.adapters import HTTPAdapter @@ -20,12 +27,12 @@ str, bytes, Mapping[str, Any], - Iterable[Tuple[str, Optional[str]]], + Iterable[tuple[str, Optional[str]]], IO, ] """Type alias for request data parameter.""" -_Timeout = Union[None, float, Tuple[float, float], Tuple[float, None]] +_Timeout = Union[None, float, tuple[float, float], tuple[float, None]] """Type alias for request timeout parameter.""" @@ -58,7 +65,7 @@ def __getattr__(self, item: str): """Forward any query for an attribute not defined in this proxy class to wrapped class.""" return getattr(self._response, item) - def raise_for_status(self, not_found: Type[APIError] = NotFound) -> None: + def raise_for_status(self, not_found: type[APIError] = NotFound) -> None: """Raises exception when Podman service reports one.""" if self.status_code < 400: return @@ -81,7 +88,7 @@ class APIClient(requests.Session): # Abstract methods (delete,get,head,post) are specialized and pylint cannot walk hierarchy. # pylint: disable=too-many-instance-attributes,arguments-differ,arguments-renamed - supported_schemes: ClassVar[List[str]] = ( + supported_schemes: ClassVar[list[str]] = ( "unix", "http+unix", "ssh", @@ -235,7 +242,7 @@ def get( self, path: Union[str, bytes], *, - params: Union[None, bytes, Mapping[str, List[str]]] = None, + params: Union[None, bytes, Mapping[str, list[str]]] = None, headers: Optional[Mapping[str, str]] = None, timeout: _Timeout = None, stream: Optional[bool] = False, diff --git a/podman/api/http_utils.py b/podman/api/http_utils.py index 5fb8599c..d68179d1 100644 --- a/podman/api/http_utils.py +++ b/podman/api/http_utils.py @@ -3,16 +3,17 @@ import base64 import collections.abc import json -from typing import Dict, List, Mapping, Optional, Union, Any +from typing import Optional, Union, Any +from collections.abc import Mapping -def prepare_filters(filters: Union[str, List[str], Mapping[str, str]]) -> Optional[str]: - """Return filters as an URL quoted JSON Dict[str, List[Any]].""" +def prepare_filters(filters: Union[str, list[str], Mapping[str, str]]) -> Optional[str]: + """Return filters as an URL quoted JSON dict[str, list[Any]].""" if filters is None or len(filters) == 0: return None - criteria: Dict[str, List[str]] = {} + criteria: dict[str, list[str]] = {} if isinstance(filters, str): _format_string(filters, criteria) elif isinstance(filters, collections.abc.Mapping): @@ -67,7 +68,7 @@ def prepare_body(body: Mapping[str, Any]) -> str: return json.dumps(body, sort_keys=True) -def _filter_values(mapping: Mapping[str, Any], recursion=False) -> Dict[str, Any]: +def _filter_values(mapping: Mapping[str, Any], recursion=False) -> dict[str, Any]: """Returns a canonical dictionary with values == None or empty Iterables removed. Dictionary is walked using recursion. diff --git a/podman/api/parse_utils.py b/podman/api/parse_utils.py index 6259ec13..1f9ec3ca 100644 --- a/podman/api/parse_utils.py +++ b/podman/api/parse_utils.py @@ -5,13 +5,14 @@ import json import struct from datetime import datetime -from typing import Any, Dict, Iterator, Optional, Tuple, Union +from typing import Any, Optional, Union +from collections.abc import Iterator from requests import Response from .output_utils import demux_output -def parse_repository(name: str) -> Tuple[str, Optional[str]]: +def parse_repository(name: str) -> tuple[str, Optional[str]]: """Parse repository image name from tag or digest Returns: @@ -33,7 +34,7 @@ def parse_repository(name: str) -> Tuple[str, Optional[str]]: return name, None -def decode_header(value: Optional[str]) -> Dict[str, Any]: +def decode_header(value: Optional[str]) -> dict[str, Any]: """Decode a base64 JSON header value.""" if value is None: return {} @@ -84,7 +85,7 @@ def frames(response: Response) -> Iterator[bytes]: def stream_frames( response: Response, demux: bool = False -) -> Iterator[Union[bytes, Tuple[bytes, bytes]]]: +) -> Iterator[Union[bytes, tuple[bytes, bytes]]]: """Returns each frame from multiplexed streamed payload. If ``demux`` then output will be tuples where the first position is ``STDOUT`` and the second @@ -111,7 +112,7 @@ def stream_frames( def stream_helper( response: Response, decode_to_json: bool = False -) -> Union[Iterator[bytes], Iterator[Dict[str, Any]]]: +) -> Union[Iterator[bytes], Iterator[dict[str, Any]]]: """Helper to stream results and optionally decode to json""" for value in response.iter_lines(): if decode_to_json: diff --git a/podman/api/tar_utils.py b/podman/api/tar_utils.py index b77a2353..7470e19a 100644 --- a/podman/api/tar_utils.py +++ b/podman/api/tar_utils.py @@ -6,12 +6,12 @@ import tarfile import tempfile from fnmatch import fnmatch -from typing import BinaryIO, List, Optional +from typing import BinaryIO, Optional import sys -def prepare_containerignore(anchor: str) -> List[str]: +def prepare_containerignore(anchor: str) -> list[str]: """Return the list of patterns for filenames to exclude. .containerignore takes precedence over .dockerignore. @@ -53,7 +53,7 @@ def prepare_containerfile(anchor: str, dockerfile: str) -> str: def create_tar( - anchor: str, name: str = None, exclude: List[str] = None, gzip: bool = False + anchor: str, name: str = None, exclude: list[str] = None, gzip: bool = False ) -> BinaryIO: """Create a tarfile from context_dir to send to Podman service. @@ -119,7 +119,7 @@ def add_filter(info: tarfile.TarInfo) -> Optional[tarfile.TarInfo]: return open(name.name, "rb") # pylint: disable=consider-using-with -def _exclude_matcher(path: str, exclude: List[str]) -> bool: +def _exclude_matcher(path: str, exclude: list[str]) -> bool: """Returns True if path matches an entry in exclude. Note: diff --git a/podman/client.py b/podman/client.py index 5785f6b9..f9a023e7 100644 --- a/podman/client.py +++ b/podman/client.py @@ -4,7 +4,7 @@ import os from contextlib import AbstractContextManager from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Optional from podman.api import cached_property from podman.api.client import APIClient @@ -88,8 +88,8 @@ def from_env( max_pool_size: Optional[int] = None, ssl_version: Optional[int] = None, # pylint: disable=unused-argument assert_hostname: bool = False, # pylint: disable=unused-argument - environment: Optional[Dict[str, str]] = None, - credstore_env: Optional[Dict[str, str]] = None, + environment: Optional[dict[str, str]] = None, + credstore_env: Optional[dict[str, str]] = None, use_ssh_client: bool = True, # pylint: disable=unused-argument ) -> "PodmanClient": """Returns connection to service using environment variables and parameters. @@ -175,7 +175,7 @@ def secrets(self): def system(self): return SystemManager(client=self.api) - def df(self) -> Dict[str, Any]: # pylint: disable=missing-function-docstring,invalid-name + def df(self) -> dict[str, Any]: # pylint: disable=missing-function-docstring,invalid-name return self.system.df() df.__doc__ = SystemManager.df.__doc__ diff --git a/podman/domain/config.py b/podman/domain/config.py index c62d2067..824dfd3e 100644 --- a/podman/domain/config.py +++ b/podman/domain/config.py @@ -3,7 +3,7 @@ import sys import urllib from pathlib import Path -from typing import Dict, Optional +from typing import Optional import json from podman.api import cached_property @@ -24,7 +24,7 @@ class ServiceConnection: """ServiceConnection defines a connection to the Podman service.""" - def __init__(self, name: str, attrs: Dict[str, str]): + def __init__(self, name: str, attrs: dict[str, str]): """Create a Podman ServiceConnection.""" self.name = name self.attrs = attrs @@ -122,14 +122,14 @@ def id(self): # pylint: disable=invalid-name @cached_property def services(self): - """Dict[str, ServiceConnection]: Returns list of service connections. + """dict[str, ServiceConnection]: Returns list of service connections. Examples: podman_config = PodmanConfig() address = podman_config.services["testing"] print(f"Testing service address {address}") """ - services: Dict[str, ServiceConnection] = {} + services: dict[str, ServiceConnection] = {} # read the keys of the toml file first engine = self.attrs.get("engine") diff --git a/podman/domain/containers.py b/podman/domain/containers.py index 61930c9a..2890698f 100644 --- a/podman/domain/containers.py +++ b/podman/domain/containers.py @@ -4,7 +4,8 @@ import logging import shlex from contextlib import suppress -from typing import Any, Dict, Iterable, Iterator, List, Mapping, Optional, Tuple, Union +from typing import Any, Optional, Union +from collections.abc import Iterable, Iterator, Mapping import requests @@ -98,7 +99,7 @@ def commit(self, repository: str = None, tag: str = None, **kwargs) -> Image: Keyword Args: author (str): Name of commit author - changes (List[str]): Instructions to apply during commit + changes (list[str]): Instructions to apply during commit comment (str): Commit message to include with Image, overrides keyword message conf (dict[str, Any]): Ignored. format (str): Format of the image manifest and metadata @@ -121,7 +122,7 @@ def commit(self, repository: str = None, tag: str = None, **kwargs) -> Image: body = response.json() return ImagesManager(client=self.client).get(body["Id"]) - def diff(self) -> List[Dict[str, int]]: + def diff(self) -> list[dict[str, int]]: """Report changes of a container's filesystem. Raises: @@ -134,7 +135,7 @@ def diff(self) -> List[Dict[str, int]]: # pylint: disable=too-many-arguments def exec_run( self, - cmd: Union[str, List[str]], + cmd: Union[str, list[str]], *, stdout: bool = True, stderr: bool = True, @@ -145,11 +146,12 @@ def exec_run( detach: bool = False, stream: bool = False, socket: bool = False, # pylint: disable=unused-argument - environment: Union[Mapping[str, str], List[str]] = None, + environment: Union[Mapping[str, str], list[str]] = None, workdir: str = None, demux: bool = False, - ) -> Tuple[ - Optional[int], Union[Iterator[Union[bytes, Tuple[bytes, bytes]]], Any, Tuple[bytes, bytes]] + ) -> tuple[ + Optional[int], + Union[Iterator[Union[bytes, tuple[bytes, bytes]]], Any, tuple[bytes, bytes]], ]: """Run given command inside container and return results. @@ -166,7 +168,7 @@ def exec_run( stream: Stream response data. Ignored if ``detach`` is ``True``. Default: False socket: Return the connection socket to allow custom read/write operations. Default: False - environment: A dictionary or a List[str] in + environment: A dictionary or a list[str] in the following format ["PASSWORD=xxx"] or {"PASSWORD": "xxx"}. workdir: Path to working directory for this exec session @@ -245,7 +247,7 @@ def export(self, chunk_size: int = api.DEFAULT_CHUNK_SIZE) -> Iterator[bytes]: def get_archive( self, path: str, chunk_size: int = api.DEFAULT_CHUNK_SIZE - ) -> Tuple[Iterable, Dict[str, Any]]: + ) -> tuple[Iterable, dict[str, Any]]: """Download a file or folder from the container's filesystem. Args: @@ -268,7 +270,7 @@ def init(self) -> None: response = self.client.post(f"/containers/{self.id}/init") response.raise_for_status() - def inspect(self) -> Dict: + def inspect(self) -> dict: """Inspect a container. Raises: @@ -419,7 +421,7 @@ def start(self, **kwargs) -> None: def stats( self, **kwargs - ) -> Union[bytes, Dict[str, Any], Iterator[bytes], Iterator[Dict[str, Any]]]: + ) -> Union[bytes, dict[str, Any], Iterator[bytes], Iterator[dict[str, Any]]]: """Return statistics for container. Keyword Args: @@ -474,7 +476,7 @@ def stop(self, **kwargs) -> None: body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"]) - def top(self, **kwargs) -> Union[Iterator[Dict[str, Any]], Dict[str, Any]]: + def top(self, **kwargs) -> Union[Iterator[dict[str, Any]], dict[str, Any]]: """Report on running processes in the container. Keyword Args: @@ -516,7 +518,7 @@ def wait(self, **kwargs) -> int: """Block until the container enters given state. Keyword Args: - condition (Union[str, List[str]]): Container state on which to release. + condition (Union[str, list[str]]): Container state on which to release. One or more of: "configured", "created", "running", "stopped", "paused", "exited", "removing", "stopping". interval (int): Time interval to wait before polling for completion. diff --git a/podman/domain/containers_create.py b/podman/domain/containers_create.py index 09ebd070..523a9023 100644 --- a/podman/domain/containers_create.py +++ b/podman/domain/containers_create.py @@ -5,7 +5,8 @@ import logging import re from contextlib import suppress -from typing import Any, Dict, List, MutableMapping, Union +from typing import Any, Union +from collections.abc import MutableMapping from podman import api from podman.domain.containers import Container @@ -23,7 +24,10 @@ class CreateMixin: # pylint: disable=too-few-public-methods """Class providing create method for ContainersManager.""" def create( - self, image: Union[Image, str], command: Union[str, List[str], None] = None, **kwargs + self, + image: Union[Image, str], + command: Union[str, list[str], None] = None, + **kwargs, ) -> Container: """Create a container. @@ -34,12 +38,12 @@ def create( Keyword Args: auto_remove (bool): Enable auto-removal of the container on daemon side when the container's process exits. - blkio_weight_device (Dict[str, Any]): Block IO weight (relative device weight) + blkio_weight_device (dict[str, Any]): Block IO weight (relative device weight) in the form of: [{"Path": "device_path", "Weight": weight}]. blkio_weight (int): Block IO weight (relative weight), accepts a weight value between 10 and 1000. - cap_add (List[str]): Add kernel capabilities. For example: ["SYS_ADMIN", "MKNOD"] - cap_drop (List[str]): Drop kernel capabilities. + cap_add (list[str]): Add kernel capabilities. For example: ["SYS_ADMIN", "MKNOD"] + cap_drop (list[str]): Drop kernel capabilities. cgroup_parent (str): Override the default parent cgroup. cpu_count (int): Number of usable CPUs (Windows only). cpu_percent (int): Usable percentage of the available CPUs (Windows only). @@ -52,32 +56,32 @@ def create( cpuset_mems (str): Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. detach (bool): Run container in the background and return a Container object. - device_cgroup_rules (List[str]): A list of cgroup rules to apply to the container. + device_cgroup_rules (list[str]): A list of cgroup rules to apply to the container. device_read_bps: Limit read rate (bytes per second) from a device in the form of: `[{"Path": "device_path", "Rate": rate}]` device_read_iops: Limit read rate (IO per second) from a device. device_write_bps: Limit write rate (bytes per second) from a device. device_write_iops: Limit write rate (IO per second) from a device. - devices (List[str]): Expose host devices to the container, as a List[str] in the form + devices (list[str]): Expose host devices to the container, as a list[str] in the form ::. For example: /dev/sda:/dev/xvda:rwm allows the container to have read-write access to the host's /dev/sda via a node named /dev/xvda inside the container. - dns (List[str]): Set custom DNS servers. - dns_opt (List[str]): Additional options to be added to the container's resolv.conf file. - dns_search (List[str]): DNS search domains. - domainname (Union[str, List[str]]): Set custom DNS search domains. - entrypoint (Union[str, List[str]]): The entrypoint for the container. - environment (Union[Dict[str, str], List[str]): Environment variables to set inside - the container, as a dictionary or a List[str] in the format + dns (list[str]): Set custom DNS servers. + dns_opt (list[str]): Additional options to be added to the container's resolv.conf file. + dns_search (list[str]): DNS search domains. + domainname (Union[str, list[str]]): Set custom DNS search domains. + entrypoint (Union[str, list[str]]): The entrypoint for the container. + environment (Union[dict[str, str], list[str]): Environment variables to set inside + the container, as a dictionary or a list[str] in the format ["SOMEVARIABLE=xxx", "SOMEOTHERVARIABLE=xyz"]. - extra_hosts (Dict[str, str]): Additional hostnames to resolve inside the container, + extra_hosts (dict[str, str]): Additional hostnames to resolve inside the container, as a mapping of hostname to IP address. - group_add (List[str]): List of additional group names and/or IDs that the container + group_add (list[str]): List of additional group names and/or IDs that the container process will run as. - healthcheck (Dict[str,Any]): Specify a test to perform to check that the + healthcheck (dict[str,Any]): Specify a test to perform to check that the container is healthy. health_check_on_failure_action (int): Specify an action if a healthcheck fails. hostname (str): Optional hostname for the container. @@ -86,14 +90,14 @@ def create( ipc_mode (str): Set the IPC mode for the container. isolation (str): Isolation technology to use. Default: `None`. kernel_memory (int or str): Kernel memory limit - labels (Union[Dict[str, str], List[str]): A dictionary of name-value labels (e.g. + labels (Union[dict[str, str], list[str]): A dictionary of name-value labels (e.g. {"label1": "value1", "label2": "value2"}) or a list of names of labels to set with empty values (e.g. ["label1", "label2"]) - links (Optional[Dict[str, str]]): Mapping of links using the {'container': 'alias'} + links (Optional[dict[str, str]]): Mapping of links using the {'container': 'alias'} format. The alias is optional. Containers declared in this dict will be linked to the new container using the provided alias. Default: None. log_config (LogConfig): Logging configuration. - lxc_config (Dict[str, str]): LXC config. + lxc_config (dict[str, str]): LXC config. mac_address (str): MAC address to assign to the container. mem_limit (Union[int, str]): Memory limit. Accepts float values (which represent the memory limit of the created container in bytes) or a string with a units @@ -104,7 +108,7 @@ def create( between 0 and 100. memswap_limit (Union[int, str]): Maximum amount of memory + swap a container is allowed to consume. - mounts (List[Mount]): Specification for mounts to be added to the container. More + mounts (list[Mount]): Specification for mounts to be added to the container. More powerful alternative to volumes. Each item in the list is expected to be a Mount object. For example: @@ -150,7 +154,7 @@ def create( ] name (str): The name for this container. nano_cpus (int): CPU quota in units of 1e-9 CPUs. - networks (Dict[str, Dict[str, Union[str, List[str]]): + networks (dict[str, dict[str, Union[str, list[str]]): Networks which will be connected to container during container creation Values of the network configuration can be : @@ -177,18 +181,18 @@ def create( platform (str): Platform in the format os[/arch[/variant]]. Only used if the method needs to pull the requested image. ports ( - Dict[ + dict[ Union[int, str], Union[ int, Tuple[str, int], - List[int], - Dict[ + list[int], + dict[ str, Union[ int, Tuple[str, int], - List[int] + list[int] ] ] ] @@ -241,7 +245,8 @@ def create( read_only (bool): Mount the container's root filesystem as read only. read_write_tmpfs (bool): Mount temporary file systems as read write, in case of read_only options set to True. Default: False - restart_policy (Dict[str, Union[str, int]]): Restart the container when it exits. + restart_policy (dict[str, Union[str, int]]): Restart the container when it exits. + remove (bool): Remove the container when it has finished running. Default: False. Configured as a dictionary with keys: - Name: One of on-failure, or always. @@ -249,7 +254,7 @@ def create( For example: {"Name": "on-failure", "MaximumRetryCount": 5} runtime (str): Runtime to use with this container. - secrets (List[Union[str, Secret, Dict[str, Union[str, int]]]]): Secrets to + secrets (list[Union[str, Secret, dict[str, Union[str, int]]]]): Secrets to mount to this container. For example: @@ -283,30 +288,30 @@ def create( }, ] - secret_env (Dict[str, str]): Secrets to add as environment variables available in the + secret_env (dict[str, str]): Secrets to add as environment variables available in the container. For example: {"VARIABLE1": "NameOfSecret", "VARIABLE2": "NameOfAnotherSecret"} - security_opt (List[str]): A List[str]ing values to customize labels for MLS systems, + security_opt (list[str]): A list[str]ing values to customize labels for MLS systems, such as SELinux. shm_size (Union[str, int]): Size of /dev/shm (e.g. 1G). stdin_open (bool): Keep STDIN open even if not attached. stdout (bool): Return logs from STDOUT when detach=False. Default: True. stderr (bool): Return logs from STDERR when detach=False. Default: False. stop_signal (str): The stop signal to use to stop the container (e.g. SIGINT). - storage_opt (Dict[str, str]): Storage driver options per container as a + storage_opt (dict[str, str]): Storage driver options per container as a key-value mapping. stream (bool): If true and detach is false, return a log generator instead of a string. Ignored if detach is true. Default: False. - sysctls (Dict[str, str]): Kernel parameters to set in the container. - tmpfs (Dict[str, str]): Temporary filesystems to mount, as a dictionary mapping a + sysctls (dict[str, str]): Kernel parameters to set in the container. + tmpfs (dict[str, str]): Temporary filesystems to mount, as a dictionary mapping a path inside the container to options for that path. For example: {'/mnt/vol2': '', '/mnt/vol1': 'size=3G,uid=1000'} tty (bool): Allocate a pseudo-TTY. - ulimits (List[Ulimit]): Ulimits to set inside the container. + ulimits (list[Ulimit]): Ulimits to set inside the container. use_config_proxy (bool): If True, and if the docker client configuration file (~/.config/containers/config.json by default) contains a proxy configuration, the corresponding environment variables will be set in the container being built. @@ -320,7 +325,7 @@ def create( version (str): The version of the API to use. Set to auto to automatically detect the server's version. Default: 3.0.0 volume_driver (str): The name of a volume driver/plugin. - volumes (Dict[str, Dict[str, Union[str, list]]]): A dictionary to configure + volumes (dict[str, dict[str, Union[str, list]]]): A dictionary to configure volumes mounted inside the container. The key is either the host path or a volume name, and the value is a dictionary with the keys: @@ -348,7 +353,7 @@ def create( } - volumes_from (List[str]): List of container names or IDs to get volumes from. + volumes_from (list[str]): List of container names or IDs to get volumes from. working_dir (str): Path to the working directory. workdir (str): Alias of working_dir - Path to the working directory. @@ -380,7 +385,7 @@ def create( # pylint: disable=too-many-locals,too-many-statements,too-many-branches @staticmethod - def _render_payload(kwargs: MutableMapping[str, Any]) -> Dict[str, Any]: + def _render_payload(kwargs: MutableMapping[str, Any]) -> dict[str, Any]: """Map create/run kwargs into body parameters.""" args = copy.copy(kwargs) diff --git a/podman/domain/containers_manager.py b/podman/domain/containers_manager.py index b204e877..b6318d76 100644 --- a/podman/domain/containers_manager.py +++ b/podman/domain/containers_manager.py @@ -2,7 +2,8 @@ import logging import urllib -from typing import Any, Dict, List, Mapping, Union +from typing import Any, Union +from collections.abc import Mapping from podman import api from podman.domain.containers import Container @@ -44,7 +45,7 @@ def get(self, key: str) -> Container: response.raise_for_status() return self.prepare_model(attrs=response.json()) - def list(self, **kwargs) -> List[Container]: + def list(self, **kwargs) -> list[Container]: """Report on containers. Keyword Args: @@ -57,7 +58,7 @@ def list(self, **kwargs) -> List[Container]: - exited (int): Only containers with specified exit code - status (str): One of restarting, running, paused, exited - - label (Union[str, List[str]]): Format either "key", "key=value" or a list of such. + - label (Union[str, list[str]]): Format either "key", "key=value" or a list of such. - id (str): The id of the container. - name (str): The name of the container. - ancestor (str): Filter by container ancestor. Format of @@ -90,17 +91,17 @@ def list(self, **kwargs) -> List[Container]: return [self.prepare_model(attrs=i) for i in response.json()] - def prune(self, filters: Mapping[str, str] = None) -> Dict[str, Any]: + def prune(self, filters: Mapping[str, str] = None) -> dict[str, Any]: """Delete stopped containers. Args: filters: Criteria for determining containers to remove. Available keys are: - until (str): Delete containers before this time - - label (List[str]): Labels associated with containers + - label (list[str]): Labels associated with containers Returns: Keys: - - ContainersDeleted (List[str]): Identifiers of deleted containers. + - ContainersDeleted (list[str]): Identifiers of deleted containers. - SpaceReclaimed (int): Amount of disk space reclaimed in bytes. Raises: diff --git a/podman/domain/containers_run.py b/podman/domain/containers_run.py index c20d807a..c393268e 100644 --- a/podman/domain/containers_run.py +++ b/podman/domain/containers_run.py @@ -3,7 +3,8 @@ import logging import threading from contextlib import suppress -from typing import Generator, Iterator, List, Union +from typing import Union +from collections.abc import Generator, Iterator from podman.domain.containers import Container from podman.domain.images import Image @@ -18,7 +19,7 @@ class RunMixin: # pylint: disable=too-few-public-methods def run( self, image: Union[str, Image], - command: Union[str, List[str], None] = None, + command: Union[str, list[str], None] = None, *, stdout=True, stderr=False, diff --git a/podman/domain/events.py b/podman/domain/events.py index 34972ec2..2fe68061 100644 --- a/podman/domain/events.py +++ b/podman/domain/events.py @@ -3,7 +3,8 @@ import json import logging from datetime import datetime -from typing import Any, Dict, Optional, Union, Iterator +from typing import Any, Optional, Union +from collections.abc import Iterator from podman import api from podman.api.client import APIClient @@ -26,9 +27,9 @@ def list( self, since: Union[datetime, int, None] = None, until: Union[datetime, int, None] = None, - filters: Optional[Dict[str, Any]] = None, + filters: Optional[dict[str, Any]] = None, decode: bool = False, - ) -> Iterator[Union[str, Dict[str, Any]]]: + ) -> Iterator[Union[str, dict[str, Any]]]: """Report on networks. Args: @@ -38,7 +39,7 @@ def list( until: Get events older than this time. Yields: - When decode is True, Iterator[Dict[str, Any]] + When decode is True, Iterator[dict[str, Any]] When decode is False, Iterator[str] """ diff --git a/podman/domain/images.py b/podman/domain/images.py index 63d7c78b..a02a04f0 100644 --- a/podman/domain/images.py +++ b/podman/domain/images.py @@ -1,7 +1,8 @@ """Model and Manager for Image resources.""" import logging -from typing import Any, Dict, Iterator, List, Optional, Union +from typing import Any, Optional, Union +from collections.abc import Iterator import urllib.parse @@ -36,7 +37,7 @@ def tags(self): return [tag for tag in repo_tags if tag != ":"] - def history(self) -> List[Dict[str, Any]]: + def history(self) -> list[dict[str, Any]]: """Returns history of the Image. Raises: @@ -49,7 +50,7 @@ def history(self) -> List[Dict[str, Any]]: def remove( self, **kwargs - ) -> List[Dict[api.Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]: + ) -> list[dict[api.Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]: """Delete image from Podman service. Podman only diff --git a/podman/domain/images_build.py b/podman/domain/images_build.py index 84de5d5b..f1fc9c38 100644 --- a/podman/domain/images_build.py +++ b/podman/domain/images_build.py @@ -7,7 +7,8 @@ import re import shutil import tempfile -from typing import Any, Dict, Iterator, List, Tuple +from typing import Any +from collections.abc import Iterator import itertools @@ -22,7 +23,7 @@ class BuildMixin: """Class providing build method for ImagesManager.""" # pylint: disable=too-many-locals,too-many-branches,too-few-public-methods,too-many-statements - def build(self, **kwargs) -> Tuple[Image, Iterator[bytes]]: + def build(self, **kwargs) -> tuple[Image, Iterator[bytes]]: """Returns built image. Keyword Args: @@ -39,7 +40,7 @@ def build(self, **kwargs) -> Tuple[Image, Iterator[bytes]]: forcerm (bool) – Always remove intermediate containers, even after unsuccessful builds dockerfile (str) – full path to the Dockerfile / Containerfile buildargs (Mapping[str,str) – A dictionary of build arguments - container_limits (Dict[str, Union[int,str]]) – + container_limits (dict[str, Union[int,str]]) – A dictionary of limits applied to each container created by the build process. Valid keys: @@ -52,11 +53,11 @@ def build(self, **kwargs) -> Tuple[Image, Iterator[bytes]]: shmsize (int) – Size of /dev/shm in bytes. The size must be greater than 0. If omitted the system uses 64MB labels (Mapping[str,str]) – A dictionary of labels to set on the image - cache_from (List[str]) – A list of image's identifier used for build cache resolution + cache_from (list[str]) – A list of image's identifier used for build cache resolution target (str) – Name of the build-stage to build in a multi-stage Dockerfile network_mode (str) – networking mode for the run commands during build squash (bool) – Squash the resulting images layers into a single layer. - extra_hosts (Dict[str,str]) – Extra hosts to add to /etc/hosts in building + extra_hosts (dict[str,str]) – Extra hosts to add to /etc/hosts in building containers, as a mapping of hostname to IP address. platform (str) – Platform in the format os[/arch[/variant]]. isolation (str) – Isolation technology used during build. (ignored) @@ -140,7 +141,7 @@ def build(self, **kwargs) -> Tuple[Image, Iterator[bytes]]: raise BuildError(unknown or "Unknown", report_stream) @staticmethod - def _render_params(kwargs) -> Dict[str, List[Any]]: + def _render_params(kwargs) -> dict[str, list[Any]]: """Map kwargs to query parameters. All unsupported kwargs are silently ignored. diff --git a/podman/domain/images_manager.py b/podman/domain/images_manager.py index b7fcdf74..39130fc3 100644 --- a/podman/domain/images_manager.py +++ b/podman/domain/images_manager.py @@ -5,7 +5,8 @@ import logging import os import urllib.parse -from typing import Any, Dict, Iterator, List, Mapping, Optional, Union, Generator +from typing import Any, Optional, Union +from collections.abc import Iterator, Mapping, Generator from pathlib import Path import requests @@ -48,17 +49,17 @@ def exists(self, key: str) -> bool: response = self.client.get(f"/images/{key}/exists") return response.ok - def list(self, **kwargs) -> List[Image]: + def list(self, **kwargs) -> list[Image]: """Report on images. Keyword Args: name (str) – Only show images belonging to the repository name all (bool) – Show intermediate image layers. By default, these are filtered out. - filters (Mapping[str, Union[str, List[str]]) – Filters to be used on the image list. + filters (Mapping[str, Union[str, list[str]]) – Filters to be used on the image list. Available filters: - dangling (bool) - - label (Union[str, List[str]]): format either "key" or "key=value" + - label (Union[str, list[str]]): format either "key" or "key=value" Raises: APIError: when service returns an error @@ -171,7 +172,7 @@ def prune( all: Optional[bool] = False, # pylint: disable=redefined-builtin external: Optional[bool] = False, filters: Optional[Mapping[str, Any]] = None, - ) -> Dict[Literal["ImagesDeleted", "SpaceReclaimed"], Any]: + ) -> dict[Literal["ImagesDeleted", "SpaceReclaimed"], Any]: """Delete unused images. The Untagged keys will always be "". @@ -202,8 +203,8 @@ def prune( response = self.client.post("/images/prune", params=params) response.raise_for_status() - deleted: List[Dict[str, str]] = [] - error: List[str] = [] + deleted: list[dict[str, str]] = [] + error: list[str] = [] reclaimed: int = 0 # If the prune doesn't remove images, the API returns "null" # and it's interpreted as None (NoneType) @@ -229,7 +230,7 @@ def prune( "SpaceReclaimed": reclaimed, } - def prune_builds(self) -> Dict[Literal["CachesDeleted", "SpaceReclaimed"], Any]: + def prune_builds(self) -> dict[Literal["CachesDeleted", "SpaceReclaimed"], Any]: """Delete builder cache. Method included to complete API, the operation always returns empty @@ -239,7 +240,7 @@ def prune_builds(self) -> Dict[Literal["CachesDeleted", "SpaceReclaimed"], Any]: def push( self, repository: str, tag: Optional[str] = None, **kwargs - ) -> Union[str, Iterator[Union[str, Dict[str, Any]]]]: + ) -> Union[str, Iterator[Union[str, dict[str, Any]]]]: """Push Image or repository to the registry. Args: @@ -249,7 +250,7 @@ def push( Keyword Args: auth_config (Mapping[str, str]: Override configured credentials. Must include username and password keys. - decode (bool): return data from server as Dict[str, Any]. Ignored unless stream=True. + decode (bool): return data from server as dict[str, Any]. Ignored unless stream=True. destination (str): alternate destination for image. (Podman only) stream (bool): return output as blocking generator. Default: False. tlsVerify (bool): Require TLS verification. @@ -259,7 +260,7 @@ def push( Raises: APIError: when service returns an error """ - auth_config: Optional[Dict[str, str]] = kwargs.get("auth_config") + auth_config: Optional[dict[str, str]] = kwargs.get("auth_config") headers = { # A base64url-encoded auth configuration @@ -301,8 +302,8 @@ def push( @staticmethod def _push_helper( - decode: bool, body: List[Dict[str, Any]] - ) -> Iterator[Union[str, Dict[str, Any]]]: + decode: bool, body: list[dict[str, Any]] + ) -> Iterator[Union[str, dict[str, Any]]]: """Helper needed to allow push() to return either a generator or a str.""" for entry in body: if decode: @@ -312,8 +313,12 @@ def _push_helper( # pylint: disable=too-many-locals,too-many-branches def pull( - self, repository: str, tag: Optional[str] = None, all_tags: bool = False, **kwargs - ) -> Union[Image, List[Image], Iterator[str]]: + self, + repository: str, + tag: Optional[str] = None, + all_tags: bool = False, + **kwargs, + ) -> Union[Image, list[Image], Iterator[str]]: """Request Podman service to pull image(s) from repository. Args: @@ -350,7 +355,7 @@ def pull( else: tag = "latest" - auth_config: Optional[Dict[str, str]] = kwargs.get("auth_config") + auth_config: Optional[dict[str, str]] = kwargs.get("auth_config") headers = { # A base64url-encoded auth configuration @@ -415,7 +420,7 @@ def pull( for item in reversed(list(response.iter_lines())): obj = json.loads(item) if all_tags and "images" in obj: - images: List[Image] = [] + images: list[Image] = [] for name in obj["images"]: images.append(self.get(name)) return images @@ -460,7 +465,7 @@ def remove( image: Union[Image, str], force: Optional[bool] = None, noprune: bool = False, # pylint: disable=unused-argument - ) -> List[Dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]: + ) -> list[dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]: """Delete image from Podman service. Args: @@ -479,7 +484,7 @@ def remove( response.raise_for_status(not_found=ImageNotFound) body = response.json() - results: List[Dict[str, Union[int, str]]] = [] + results: list[dict[str, Union[int, str]]] = [] for key in ("Deleted", "Untagged", "Errors"): if key in body: for element in body[key]: @@ -487,14 +492,14 @@ def remove( results.append({"ExitCode": body["ExitCode"]}) return results - def search(self, term: str, **kwargs) -> List[Dict[str, Any]]: + def search(self, term: str, **kwargs) -> list[dict[str, Any]]: """Search Images on registries. Args: term: Used to target Image results. Keyword Args: - filters (Mapping[str, List[str]): Refine results of search. Available filters: + filters (Mapping[str, list[str]): Refine results of search. Available filters: - is-automated (bool): Image build is automated. - is-official (bool): Image build is owned by product provider. diff --git a/podman/domain/ipam.py b/podman/domain/ipam.py index 2cf445e4..f446841c 100644 --- a/podman/domain/ipam.py +++ b/podman/domain/ipam.py @@ -3,7 +3,8 @@ Provided for compatibility """ -from typing import Any, List, Mapping, Optional +from typing import Any, Optional +from collections.abc import Mapping class IPAMPool(dict): @@ -41,7 +42,7 @@ class IPAMConfig(dict): def __init__( self, driver: Optional[str] = "host-local", - pool_configs: Optional[List[IPAMPool]] = None, + pool_configs: Optional[list[IPAMPool]] = None, options: Optional[Mapping[str, Any]] = None, ): """Create IPAMConfig. diff --git a/podman/domain/manager.py b/podman/domain/manager.py index a9bda9f1..ffbad3c4 100644 --- a/podman/domain/manager.py +++ b/podman/domain/manager.py @@ -2,7 +2,8 @@ from abc import ABC, abstractmethod from collections import abc -from typing import Any, List, Mapping, Optional, TypeVar, Union +from typing import Any, Optional, TypeVar, Union +from collections.abc import Mapping from podman.api.client import APIClient @@ -108,7 +109,7 @@ def get(self, key: str) -> PodmanResourceType: """Returns representation of resource.""" @abstractmethod - def list(self, **kwargs) -> List[PodmanResourceType]: + def list(self, **kwargs) -> list[PodmanResourceType]: """Returns list of resources.""" def prepare_model(self, attrs: Union[PodmanResource, Mapping[str, Any]]) -> PodmanResourceType: diff --git a/podman/domain/manifests.py b/podman/domain/manifests.py index b150f8ad..adc82a57 100644 --- a/podman/domain/manifests.py +++ b/podman/domain/manifests.py @@ -3,7 +3,7 @@ import logging import urllib.parse from contextlib import suppress -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union from podman import api from podman.domain.images import Image @@ -38,7 +38,7 @@ def quoted_name(self): @property def names(self): - """List[str]: Returns the identifier of the manifest.""" + """list[str]: Returns the identifier of the manifest.""" return self.name @property @@ -51,7 +51,7 @@ def version(self): """int: Returns the schema version type for this manifest.""" return self.attrs.get("schemaVersion") - def add(self, images: List[Union[Image, str]], **kwargs) -> None: + def add(self, images: list[Union[Image, str]], **kwargs) -> None: """Add Image to manifest list. Args: @@ -59,9 +59,9 @@ def add(self, images: List[Union[Image, str]], **kwargs) -> None: Keyword Args: all (bool): - annotation (Dict[str, str]): + annotation (dict[str, str]): arch (str): - features (List[str]): + features (list[str]): os (str): os_version (str): variant (str): @@ -153,7 +153,7 @@ def resource(self): def create( self, name: str, - images: Optional[List[Union[Image, str]]] = None, + images: Optional[list[Union[Image, str]]] = None, all: Optional[bool] = None, # pylint: disable=redefined-builtin ) -> Manifest: """Create a Manifest. @@ -167,7 +167,7 @@ def create( ValueError: when no names are provided NotFoundImage: when a given image does not exist """ - params: Dict[str, Any] = {} + params: dict[str, Any] = {} if images is not None: params["images"] = [] for item in images: @@ -219,12 +219,12 @@ def get(self, key: str) -> Manifest: body["names"] = key return self.prepare_model(attrs=body) - def list(self, **kwargs) -> List[Manifest]: + def list(self, **kwargs) -> list[Manifest]: """Not Implemented.""" raise NotImplementedError("Podman service currently does not support listing manifests.") - def remove(self, name: Union[Manifest, str]) -> Dict[str, Any]: + def remove(self, name: Union[Manifest, str]) -> dict[str, Any]: """Delete the manifest list from the Podman service.""" if isinstance(name, Manifest): name = name.name diff --git a/podman/domain/networks_manager.py b/podman/domain/networks_manager.py index 2deb175c..642972a4 100644 --- a/podman/domain/networks_manager.py +++ b/podman/domain/networks_manager.py @@ -12,7 +12,7 @@ import ipaddress import logging from contextlib import suppress -from typing import Any, Dict, List, Optional +from typing import Any, Optional from podman import api from podman.api import http_utils @@ -46,8 +46,8 @@ def create(self, name: str, **kwargs) -> Network: ingress (bool): Ignored, always False. internal (bool): Restrict external access to the network. ipam (IPAMConfig): Optional custom IP scheme for the network. - labels (Dict[str, str]): Map of labels to set on the network. - options (Dict[str, Any]): Driver options. + labels (dict[str, str]): Map of labels to set on the network. + options (dict[str, Any]): Driver options. scope (str): Ignored, always "local". Raises: @@ -75,7 +75,7 @@ def create(self, name: str, **kwargs) -> Network: response.raise_for_status() return self.prepare_model(attrs=response.json()) - def _prepare_ipam(self, data: Dict[str, Any], ipam: Dict[str, Any]): + def _prepare_ipam(self, data: dict[str, Any], ipam: dict[str, Any]): if "Driver" in ipam: data["ipam_options"] = {"driver": ipam["Driver"]} @@ -117,23 +117,23 @@ def get(self, key: str) -> Network: return self.prepare_model(attrs=response.json()) - def list(self, **kwargs) -> List[Network]: + def list(self, **kwargs) -> list[Network]: """Report on networks. Keyword Args: - names (List[str]): List of names to filter by. - ids (List[str]): List of identifiers to filter by. + names (list[str]): List of names to filter by. + ids (list[str]): List of identifiers to filter by. filters (Mapping[str,str]): Criteria for listing networks. Available filters: - driver="bridge": Matches a network's driver. Only "bridge" is supported. - - label=(Union[str, List[str]]): format either "key", "key=value" + - label=(Union[str, list[str]]): format either "key", "key=value" or a list of such. - type=(str): Filters networks by type, legal values are: - "custom" - "builtin" - - plugin=(List[str]]): Matches CNI plugins included in a network, legal + - plugin=(list[str]]): Matches CNI plugins included in a network, legal values are (Podman only): - bridge @@ -161,8 +161,8 @@ def list(self, **kwargs) -> List[Network]: return [self.prepare_model(i) for i in response.json()] def prune( - self, filters: Optional[Dict[str, Any]] = None - ) -> Dict[api.Literal["NetworksDeleted", "SpaceReclaimed"], Any]: + self, filters: Optional[dict[str, Any]] = None + ) -> dict[api.Literal["NetworksDeleted", "SpaceReclaimed"], Any]: """Delete unused Networks. SpaceReclaimed always reported as 0 @@ -177,7 +177,7 @@ def prune( response = self.client.post("/networks/prune", params=params) response.raise_for_status() - deleted: List[str] = [] + deleted: list[str] = [] for item in response.json(): if item["Error"] is not None: raise APIError( diff --git a/podman/domain/pods.py b/podman/domain/pods.py index 30a0d19c..56cd905c 100644 --- a/podman/domain/pods.py +++ b/podman/domain/pods.py @@ -1,11 +1,11 @@ """Model and Manager for Pod resources.""" import logging -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Optional, Union from podman.domain.manager import PodmanResource -_Timeout = Union[None, float, Tuple[float, float], Tuple[float, None]] +_Timeout = Union[None, float, tuple[float, float], tuple[float, None]] logger = logging.getLogger("podman.pods") @@ -88,7 +88,7 @@ def stop(self, timeout: _Timeout = None) -> None: response = self.client.post(f"/pods/{self.id}/stop", params=params) response.raise_for_status() - def top(self, **kwargs) -> Dict[str, Any]: + def top(self, **kwargs) -> dict[str, Any]: """Report on running processes in pod. Keyword Args: diff --git a/podman/domain/pods_manager.py b/podman/domain/pods_manager.py index 5918f2bd..4f2f3ca9 100644 --- a/podman/domain/pods_manager.py +++ b/podman/domain/pods_manager.py @@ -2,7 +2,7 @@ import json import logging -from typing import Any, Dict, List, Optional, Union, Iterator +from typing import Any, Optional, Union from podman import api from podman.domain.manager import Manager @@ -57,24 +57,24 @@ def get(self, pod_id: str) -> Pod: # pylint: disable=arguments-differ,arguments response.raise_for_status() return self.prepare_model(attrs=response.json()) - def list(self, **kwargs) -> List[Pod]: + def list(self, **kwargs) -> list[Pod]: """Report on pods. Keyword Args: filters (Mapping[str, str]): Criteria for listing pods. Available filters: - - ctr-ids (List[str]): List of container ids to filter by. - - ctr-names (List[str]): List of container names to filter by. - - ctr-number (List[int]): list pods with given number of containers. - - ctr-status (List[str]): List pods with containers in given state. + - ctr-ids (list[str]): list of container ids to filter by. + - ctr-names (list[str]): list of container names to filter by. + - ctr-number (list[int]): list pods with given number of containers. + - ctr-status (list[str]): list pods with containers in given state. Legal values are: "created", "running", "paused", "stopped", "exited", or "unknown" - id (str) - List pod with this id. - name (str) - List pod with this name. - - status (List[str]): List pods in given state. Legal values are: + - status (list[str]): List pods in given state. Legal values are: "created", "running", "paused", "stopped", "exited", or "unknown" - - label (List[str]): List pods with given labels. - - network (List[str]): List pods associated with given Network Ids (not Names). + - label (list[str]): List pods with given labels. + - network (list[str]): List pods associated with given Network Ids (not Names). Raises: APIError: when an error returned by service @@ -84,12 +84,12 @@ def list(self, **kwargs) -> List[Pod]: response.raise_for_status() return [self.prepare_model(attrs=i) for i in response.json()] - def prune(self, filters: Optional[Dict[str, str]] = None) -> Dict[str, Any]: + def prune(self, filters: Optional[dict[str, str]] = None) -> dict[str, Any]: """Delete unused Pods. Returns: Dictionary Keys: - - PodsDeleted (List[str]): List of pod ids deleted. + - PodsDeleted (list[str]): List of pod ids deleted. - SpaceReclaimed (int): Always zero. Raises: @@ -98,7 +98,7 @@ def prune(self, filters: Optional[Dict[str, str]] = None) -> Dict[str, Any]: response = self.client.post("/pods/prune", params={"filters": api.prepare_filters(filters)}) response.raise_for_status() - deleted: List[str] = [] + deleted: list[str] = [] for item in response.json(): if item["Err"] is not None: raise APIError( @@ -129,12 +129,12 @@ def remove(self, pod_id: Union[Pod, str], force: Optional[bool] = None) -> None: response = self.client.delete(f"/pods/{pod_id}", params={"force": force}) response.raise_for_status() - def stats(self, **kwargs) -> Union[List[Dict[str, Any]], Iterator[List[Dict[str, Any]]]]: + def stats(self, **kwargs) -> Union[list[dict[str, Any]], [list[dict[str, Any]]]]: """Resource usage statistics for the containers in pods. Keyword Args: all (bool): Provide statistics for all running pods. - name (Union[str, List[str]]): Pods to include in report. + name (Union[str, list[str]]): Pods to include in report. stream (bool): Stream statistics until cancelled. Default: False. decode (bool): If True, response will be decoded into dict. Default: False. diff --git a/podman/domain/registry_data.py b/podman/domain/registry_data.py index a792824d..5c0ad1ae 100644 --- a/podman/domain/registry_data.py +++ b/podman/domain/registry_data.py @@ -1,7 +1,8 @@ """Module for tracking registry metadata.""" import logging -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union +from collections.abc import Mapping from podman import api from podman.domain.images import Image diff --git a/podman/domain/secrets.py b/podman/domain/secrets.py index 77093ab0..d56432b9 100644 --- a/podman/domain/secrets.py +++ b/podman/domain/secrets.py @@ -1,7 +1,8 @@ """Model and Manager for Secrets resources.""" from contextlib import suppress -from typing import Any, List, Mapping, Optional, Union +from typing import Any, Optional, Union +from collections.abc import Mapping from podman.api import APIClient from podman.domain.manager import Manager, PodmanResource @@ -75,7 +76,7 @@ def get(self, secret_id: str) -> Secret: # pylint: disable=arguments-differ,arg response.raise_for_status() return self.prepare_model(attrs=response.json()) - def list(self, **kwargs) -> List[Secret]: + def list(self, **kwargs) -> list[Secret]: """Report on Secrets. Keyword Args: diff --git a/podman/domain/system.py b/podman/domain/system.py index 336421d7..88d66992 100644 --- a/podman/domain/system.py +++ b/podman/domain/system.py @@ -1,7 +1,7 @@ """SystemManager to provide system level information from Podman service.""" import logging -from typing import Any, Dict, Optional, Union +from typing import Any, Optional, Union from podman.api.client import APIClient from podman import api @@ -20,7 +20,7 @@ def __init__(self, client: APIClient) -> None: """ self.client = client - def df(self) -> Dict[str, Any]: # pylint: disable=invalid-name + def df(self) -> dict[str, Any]: # pylint: disable=invalid-name """Disk usage by Podman resources. Returns: @@ -30,7 +30,7 @@ def df(self) -> Dict[str, Any]: # pylint: disable=invalid-name response.raise_for_status() return response.json() - def info(self, *_, **__) -> Dict[str, Any]: + def info(self, *_, **__) -> dict[str, Any]: """Returns information on Podman service.""" response = self.client.get("/info") response.raise_for_status() @@ -48,7 +48,7 @@ def login( # pylint: disable=too-many-arguments,too-many-positional-arguments,u identitytoken: Optional[str] = None, registrytoken: Optional[str] = None, tls_verify: Optional[Union[bool, str]] = None, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Log into Podman service. Args: @@ -91,7 +91,7 @@ def ping(self) -> bool: response = self.client.head("/_ping") return response.ok - def version(self, **kwargs) -> Dict[str, Any]: + def version(self, **kwargs) -> dict[str, Any]: """Get version information from service. Keyword Args: diff --git a/podman/domain/volumes.py b/podman/domain/volumes.py index 6867d5c8..008a3c8c 100644 --- a/podman/domain/volumes.py +++ b/podman/domain/volumes.py @@ -1,7 +1,7 @@ """Model and Manager for Volume resources.""" import logging -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union import requests @@ -92,14 +92,14 @@ def get(self, volume_id: str) -> Volume: # pylint: disable=arguments-differ,arg response.raise_for_status() return self.prepare_model(attrs=response.json()) - def list(self, *_, **kwargs) -> List[Volume]: + def list(self, *_, **kwargs) -> list[Volume]: """Report on volumes. Keyword Args: - filters (Dict[str, str]): criteria to filter Volume list + filters (dict[str, str]): criteria to filter Volume list - driver (str): filter volumes by their driver - - label (Dict[str, str]): filter by label and/or value + - label (dict[str, str]): filter by label and/or value - name (str): filter by volume's name """ filters = api.prepare_filters(kwargs.get("filters")) @@ -113,8 +113,8 @@ def list(self, *_, **kwargs) -> List[Volume]: def prune( self, - filters: Optional[Dict[str, str]] = None, # pylint: disable=unused-argument - ) -> Dict[Literal["VolumesDeleted", "SpaceReclaimed"], Any]: + filters: Optional[dict[str, str]] = None, # pylint: disable=unused-argument + ) -> dict[Literal["VolumesDeleted", "SpaceReclaimed"], Any]: """Delete unused volumes. Args: @@ -127,7 +127,7 @@ def prune( data = response.json() response.raise_for_status() - volumes: List[str] = [] + volumes: list[str] = [] space_reclaimed = 0 for item in data: if "Err" in item: diff --git a/podman/errors/exceptions.py b/podman/errors/exceptions.py index ef3af2a0..f92d886c 100644 --- a/podman/errors/exceptions.py +++ b/podman/errors/exceptions.py @@ -1,6 +1,7 @@ """Podman API Errors.""" -from typing import Iterable, List, Optional, Union, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING +from collections.abc import Iterable from requests import Response from requests.exceptions import HTTPError @@ -112,7 +113,7 @@ def __init__( self, container: "Container", exit_status: int, - command: Union[str, List[str]], + command: Union[str, list[str]], image: str, stderr: Optional[Iterable[str]] = None, ): # pylint: disable=too-many-positional-arguments diff --git a/podman/tests/integration/utils.py b/podman/tests/integration/utils.py index 262bf86e..05f7c6d7 100644 --- a/podman/tests/integration/utils.py +++ b/podman/tests/integration/utils.py @@ -20,7 +20,7 @@ import subprocess import threading from contextlib import suppress -from typing import List, Optional +from typing import Optional import time @@ -53,7 +53,7 @@ def __init__( self.proc = None self.reference_id = hash(time.monotonic()) - self.cmd: List[str] = [] + self.cmd: list[str] = [] if privileged: self.cmd.append('sudo') diff --git a/podman/tests/unit/test_parse_utils.py b/podman/tests/unit/test_parse_utils.py index 5468272a..b76e83f9 100644 --- a/podman/tests/unit/test_parse_utils.py +++ b/podman/tests/unit/test_parse_utils.py @@ -3,7 +3,8 @@ import json import unittest from dataclasses import dataclass -from typing import Any, Iterable, Optional, Tuple +from typing import Any, Optional +from collections.abc import Iterable from unittest import mock from requests import Response @@ -17,7 +18,7 @@ def test_parse_repository(self): class TestCase: name: str input: Any - expected: Tuple[str, Optional[str]] + expected: tuple[str, Optional[str]] cases = [ TestCase(name="empty str", input="", expected=("", None)), diff --git a/podman/tests/unit/test_podsmanager.py b/podman/tests/unit/test_podsmanager.py index 4512f8e6..fd919cb8 100644 --- a/podman/tests/unit/test_podsmanager.py +++ b/podman/tests/unit/test_podsmanager.py @@ -1,7 +1,7 @@ import io import json import unittest -from typing import Iterable +from collections.abc import Iterable import requests_mock From b5718c7545067d6ed6df5d7bcf1d5167d7a03fdc Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Mon, 2 Dec 2024 16:58:54 +0100 Subject: [PATCH 44/48] Remove code that is never executed Python Version is already set to be < 3.9 so this code will never run Signed-off-by: Nicola Sella --- podman/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/podman/__init__.py b/podman/__init__.py index 826405e1..8e342bd9 100644 --- a/podman/__init__.py +++ b/podman/__init__.py @@ -1,12 +1,7 @@ """Podman client module.""" -import sys - from podman.client import PodmanClient, from_env from podman.version import __version__ -if sys.version_info < (3, 9): - raise ImportError("Python 3.6 or greater is required.") - # isort: unique-list __all__ = ['PodmanClient', '__version__', 'from_env'] From 29d122c1f97daec233e228c2132daf34ae3b9a2a Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Mon, 2 Dec 2024 20:05:35 +0100 Subject: [PATCH 45/48] Remove pylint bare-except comment Signed-off-by: Nicola Sella --- podman/domain/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podman/domain/config.py b/podman/domain/config.py index 824dfd3e..1d4e8bf7 100644 --- a/podman/domain/config.py +++ b/podman/domain/config.py @@ -87,7 +87,7 @@ def __init__(self, path: Optional[str] = None): try: with open(self.path, encoding='utf-8') as file: self.attrs = json.load(file) - except Exception: # pylint: disable=bare-except + except Exception: # if the user specifies a path, it can either be a JSON file # or a TOML file - so try TOML next try: From 67c5392b1df555c31fdd6aaffb75399f272d1e89 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Mon, 2 Dec 2024 19:53:31 +0100 Subject: [PATCH 46/48] Disambiguate shadowed builtins without API break This suppresses A003 for `list` builtin. `typing.List`, `typing.Dict`, `typing.Tuple` and `typing.Type` are deprecated. Removing these annotations breaks the calls to `list` when they are done within the same class scope, which makes them ambiguous. Typed returns `list` resolve to the function `list` defined in the class, shadowing the builtin function. This change is not great but a proper one would require changing the name of the class function `list` and breaking the API to be fixed. Example of where it breaks: podman/domains/images_manager.py class ImagesManager(...): def list(...): ... def pull( self, ... ) -> Image | list[Image], [[str]]: ... Here, the typed annotation of `pull` would resolve to the `list` method, rather than the builtin. For the sake of readability, all builtin `list` calls are replaced in the class as `builtins.list`. Signed-off-by: Nicola Sella --- podman/api/http_utils.py | 2 +- podman/domain/images_manager.py | 19 ++++++++++--------- podman/domain/networks.py | 12 ++++++------ podman/domain/pods_manager.py | 10 +++++++--- podman/domain/secrets.py | 2 +- podman/domain/volumes.py | 4 ++-- podman/tests/unit/test_api_utils.py | 4 ++-- 7 files changed, 29 insertions(+), 24 deletions(-) diff --git a/podman/api/http_utils.py b/podman/api/http_utils.py index d68179d1..e0bb062e 100644 --- a/podman/api/http_utils.py +++ b/podman/api/http_utils.py @@ -100,5 +100,5 @@ def _filter_values(mapping: Mapping[str, Any], recursion=False) -> dict[str, Any return canonical -def encode_auth_header(auth_config: Dict[str, str]) -> str: +def encode_auth_header(auth_config: dict[str, str]) -> str: return base64.urlsafe_b64encode(json.dumps(auth_config).encode('utf-8')) diff --git a/podman/domain/images_manager.py b/podman/domain/images_manager.py index 39130fc3..15f8d4f4 100644 --- a/podman/domain/images_manager.py +++ b/podman/domain/images_manager.py @@ -1,5 +1,6 @@ """PodmanResource manager subclassed for Images.""" +import builtins import io import json import logging @@ -49,7 +50,7 @@ def exists(self, key: str) -> bool: response = self.client.get(f"/images/{key}/exists") return response.ok - def list(self, **kwargs) -> list[Image]: + def list(self, **kwargs) -> builtins.list[Image]: """Report on images. Keyword Args: @@ -203,8 +204,8 @@ def prune( response = self.client.post("/images/prune", params=params) response.raise_for_status() - deleted: list[dict[str, str]] = [] - error: list[str] = [] + deleted: builtins.list[dict[str, str]] = [] + error: builtins.list[str] = [] reclaimed: int = 0 # If the prune doesn't remove images, the API returns "null" # and it's interpreted as None (NoneType) @@ -302,7 +303,7 @@ def push( @staticmethod def _push_helper( - decode: bool, body: list[dict[str, Any]] + decode: bool, body: builtins.list[dict[str, Any]] ) -> Iterator[Union[str, dict[str, Any]]]: """Helper needed to allow push() to return either a generator or a str.""" for entry in body: @@ -318,7 +319,7 @@ def pull( tag: Optional[str] = None, all_tags: bool = False, **kwargs, - ) -> Union[Image, list[Image], Iterator[str]]: + ) -> Union[Image, builtins.list[Image], Iterator[str]]: """Request Podman service to pull image(s) from repository. Args: @@ -420,7 +421,7 @@ def pull( for item in reversed(list(response.iter_lines())): obj = json.loads(item) if all_tags and "images" in obj: - images: list[Image] = [] + images: builtins.list[Image] = [] for name in obj["images"]: images.append(self.get(name)) return images @@ -465,7 +466,7 @@ def remove( image: Union[Image, str], force: Optional[bool] = None, noprune: bool = False, # pylint: disable=unused-argument - ) -> list[dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]: + ) -> builtins.list[dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]: """Delete image from Podman service. Args: @@ -484,7 +485,7 @@ def remove( response.raise_for_status(not_found=ImageNotFound) body = response.json() - results: list[dict[str, Union[int, str]]] = [] + results: builtins.list[dict[str, Union[int, str]]] = [] for key in ("Deleted", "Untagged", "Errors"): if key in body: for element in body[key]: @@ -492,7 +493,7 @@ def remove( results.append({"ExitCode": body["ExitCode"]}) return results - def search(self, term: str, **kwargs) -> list[dict[str, Any]]: + def search(self, term: str, **kwargs) -> builtins.list[dict[str, Any]]: """Search Images on registries. Args: diff --git a/podman/domain/networks.py b/podman/domain/networks.py index b509132a..bbc89539 100644 --- a/podman/domain/networks.py +++ b/podman/domain/networks.py @@ -24,7 +24,7 @@ class Network(PodmanResource): """Details and configuration for a networks managed by the Podman service. Attributes: - attrs (Dict[str, Any]): Attributes of Network reported from Podman service + attrs (dict[str, Any]): Attributes of Network reported from Podman service """ @property @@ -41,7 +41,7 @@ def id(self): # pylint: disable=invalid-name @property def containers(self): - """List[Container]: Returns list of Containers connected to network.""" + """list[Container]: Returns list of Containers connected to network.""" with suppress(KeyError): container_manager = ContainersManager(client=self.client) return [container_manager.get(ident) for ident in self.attrs["Containers"].keys()] @@ -71,12 +71,12 @@ def connect(self, container: Union[str, Container], *_, **kwargs) -> None: container: To add to this Network Keyword Args: - aliases (List[str]): Aliases to add for this endpoint - driver_opt (Dict[str, Any]): Options to provide to network driver + aliases (list[str]): Aliases to add for this endpoint + driver_opt (dict[str, Any]): Options to provide to network driver ipv4_address (str): IPv4 address for given Container on this network ipv6_address (str): IPv6 address for given Container on this network - link_local_ips (List[str]): list of link-local addresses - links (List[Union[str, Containers]]): Ignored + link_local_ips (list[str]): list of link-local addresses + links (list[Union[str, Containers]]): Ignored Raises: APIError: when Podman service reports an error diff --git a/podman/domain/pods_manager.py b/podman/domain/pods_manager.py index 4f2f3ca9..77828606 100644 --- a/podman/domain/pods_manager.py +++ b/podman/domain/pods_manager.py @@ -1,8 +1,10 @@ """PodmanResource manager subclassed for Networks.""" +import builtins import json import logging from typing import Any, Optional, Union +from collections.abc import Iterator from podman import api from podman.domain.manager import Manager @@ -57,7 +59,7 @@ def get(self, pod_id: str) -> Pod: # pylint: disable=arguments-differ,arguments response.raise_for_status() return self.prepare_model(attrs=response.json()) - def list(self, **kwargs) -> list[Pod]: + def list(self, **kwargs) -> builtins.list[Pod]: """Report on pods. Keyword Args: @@ -98,7 +100,7 @@ def prune(self, filters: Optional[dict[str, str]] = None) -> dict[str, Any]: response = self.client.post("/pods/prune", params={"filters": api.prepare_filters(filters)}) response.raise_for_status() - deleted: list[str] = [] + deleted: builtins.list[str] = [] for item in response.json(): if item["Err"] is not None: raise APIError( @@ -129,7 +131,9 @@ def remove(self, pod_id: Union[Pod, str], force: Optional[bool] = None) -> None: response = self.client.delete(f"/pods/{pod_id}", params={"force": force}) response.raise_for_status() - def stats(self, **kwargs) -> Union[list[dict[str, Any]], [list[dict[str, Any]]]]: + def stats( + self, **kwargs + ) -> Union[builtins.list[dict[str, Any]], Iterator[builtins.list[dict[str, Any]]]]: """Resource usage statistics for the containers in pods. Keyword Args: diff --git a/podman/domain/secrets.py b/podman/domain/secrets.py index d56432b9..20c81ac8 100644 --- a/podman/domain/secrets.py +++ b/podman/domain/secrets.py @@ -80,7 +80,7 @@ def list(self, **kwargs) -> list[Secret]: """Report on Secrets. Keyword Args: - filters (Dict[str, Any]): Ignored. + filters (dict[str, Any]): Ignored. Raises: APIError: when error returned by service diff --git a/podman/domain/volumes.py b/podman/domain/volumes.py index 008a3c8c..213d15e8 100644 --- a/podman/domain/volumes.py +++ b/podman/domain/volumes.py @@ -53,8 +53,8 @@ def create(self, name: Optional[str] = None, **kwargs) -> Volume: Keyword Args: driver (str): Volume driver to use - driver_opts (Dict[str, str]): Options to use with driver - labels (Dict[str, str]): Labels to apply to volume + driver_opts (dict[str, str]): Options to use with driver + labels (dict[str, str]): Labels to apply to volume Raises: APIError: when service reports error diff --git a/podman/tests/unit/test_api_utils.py b/podman/tests/unit/test_api_utils.py index ea389143..0635bcd3 100644 --- a/podman/tests/unit/test_api_utils.py +++ b/podman/tests/unit/test_api_utils.py @@ -22,10 +22,10 @@ class TestCase: TestCase(name="empty str", input="", expected=None), TestCase(name="str", input="reference=fedora", expected='{"reference": ["fedora"]}'), TestCase( - name="List[str]", input=["reference=fedora"], expected='{"reference": ["fedora"]}' + name="list[str]", input=["reference=fedora"], expected='{"reference": ["fedora"]}' ), TestCase( - name="Dict[str,str]", + name="dict[str,str]", input={"reference": "fedora"}, expected='{"reference": ["fedora"]}', ), From 4eab05d084154eda44ae54b777ac078620980d90 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Fri, 13 Dec 2024 14:08:48 +0100 Subject: [PATCH 47/48] Remove api.Literal Signed-off-by: Nicola Sella --- podman/api/__init__.py | 3 --- podman/domain/images.py | 8 ++++---- podman/domain/images_manager.py | 3 +-- podman/domain/networks_manager.py | 11 +++++------ podman/domain/volumes.py | 3 +-- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/podman/api/__init__.py b/podman/api/__init__.py index 6b197bad..0079d0d2 100644 --- a/podman/api/__init__.py +++ b/podman/api/__init__.py @@ -15,8 +15,6 @@ ) from podman.api.tar_utils import create_tar, prepare_containerfile, prepare_containerignore -from typing import Literal - DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024 @@ -25,7 +23,6 @@ 'APIClient', 'COMPATIBLE_VERSION', 'DEFAULT_CHUNK_SIZE', - 'Literal', 'VERSION', 'cached_property', 'create_tar', diff --git a/podman/domain/images.py b/podman/domain/images.py index a02a04f0..6e62acf9 100644 --- a/podman/domain/images.py +++ b/podman/domain/images.py @@ -1,12 +1,12 @@ """Model and Manager for Image resources.""" import logging -from typing import Any, Optional, Union +from typing import Any, Optional, Literal, Union from collections.abc import Iterator import urllib.parse -from podman import api +from podman.api import DEFAULT_CHUNK_SIZE from podman.domain.manager import PodmanResource from podman.errors import ImageNotFound, InvalidArgument @@ -50,7 +50,7 @@ def history(self) -> list[dict[str, Any]]: def remove( self, **kwargs - ) -> list[dict[api.Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]: + ) -> list[dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]: """Delete image from Podman service. Podman only @@ -70,7 +70,7 @@ def remove( def save( self, - chunk_size: Optional[int] = api.DEFAULT_CHUNK_SIZE, + chunk_size: Optional[int] = DEFAULT_CHUNK_SIZE, named: Union[str, bool] = False, ) -> Iterator[bytes]: """Returns Image as tarball. diff --git a/podman/domain/images_manager.py b/podman/domain/images_manager.py index 15f8d4f4..8d2cb56e 100644 --- a/podman/domain/images_manager.py +++ b/podman/domain/images_manager.py @@ -6,13 +6,12 @@ import logging import os import urllib.parse -from typing import Any, Optional, Union +from typing import Any, Literal, Optional, Union from collections.abc import Iterator, Mapping, Generator from pathlib import Path import requests from podman import api -from podman.api import Literal from podman.api.parse_utils import parse_repository from podman.api.http_utils import encode_auth_header from podman.domain.images import Image diff --git a/podman/domain/networks_manager.py b/podman/domain/networks_manager.py index 642972a4..c7c92f79 100644 --- a/podman/domain/networks_manager.py +++ b/podman/domain/networks_manager.py @@ -12,10 +12,9 @@ import ipaddress import logging from contextlib import suppress -from typing import Any, Optional +from typing import Any, Optional, Literal -from podman import api -from podman.api import http_utils +from podman.api import http_utils, prepare_filters from podman.domain.manager import Manager from podman.domain.networks import Network from podman.errors import APIError @@ -152,7 +151,7 @@ def list(self, **kwargs) -> list[Network]: filters = kwargs.get("filters", {}) filters["name"] = kwargs.get("names") filters["id"] = kwargs.get("ids") - filters = api.prepare_filters(filters) + filters = prepare_filters(filters) params = {"filters": filters} response = self.client.get("/networks/json", params=params) @@ -162,7 +161,7 @@ def list(self, **kwargs) -> list[Network]: def prune( self, filters: Optional[dict[str, Any]] = None - ) -> dict[api.Literal["NetworksDeleted", "SpaceReclaimed"], Any]: + ) -> dict[Literal["NetworksDeleted", "SpaceReclaimed"], Any]: """Delete unused Networks. SpaceReclaimed always reported as 0 @@ -173,7 +172,7 @@ def prune( Raises: APIError: when service reports error """ - params = {"filters": api.prepare_filters(filters)} + params = {"filters": prepare_filters(filters)} response = self.client.post("/networks/prune", params=params) response.raise_for_status() diff --git a/podman/domain/volumes.py b/podman/domain/volumes.py index 213d15e8..717db37d 100644 --- a/podman/domain/volumes.py +++ b/podman/domain/volumes.py @@ -1,12 +1,11 @@ """Model and Manager for Volume resources.""" import logging -from typing import Any, Optional, Union +from typing import Any, Literal, Optional, Union import requests from podman import api -from podman.api import Literal from podman.domain.manager import Manager, PodmanResource from podman.errors import APIError From 91d96aa080bbd6d7a8b2e92373cfe01514e2c199 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Thu, 5 Dec 2024 16:36:18 +0100 Subject: [PATCH 48/48] Onboard TMT Signed-off-by: Nicola Sella --- .fmf/version | 1 + .packit.yaml | 10 ++++++++ .pre-commit-config.yaml | 4 +++ plans/main.fmf | 56 +++++++++++++++++++++++++++++++++++++++++ tests/main.fmf | 23 +++++++++++++++++ 5 files changed, 94 insertions(+) create mode 100644 .fmf/version create mode 100644 plans/main.fmf create mode 100644 tests/main.fmf diff --git a/.fmf/version b/.fmf/version new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/.fmf/version @@ -0,0 +1 @@ +1 diff --git a/.packit.yaml b/.packit.yaml index a7a14509..40b2fe50 100644 --- a/.packit.yaml +++ b/.packit.yaml @@ -77,3 +77,13 @@ jobs: packages: [python-podman-fedora] dist_git_branches: - fedora-branched # rawhide updates are created automatically + + # Test linting on the codebase + # This test might break based on the OS and lint used, so we follow fedora-latest as a reference + - job: tests + trigger: pull_request + tmt_plan: /upstream/sanity + packages: [python-podman-fedora] + targets: + - fedora-latest-stable + skip_build: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67fc4d6c..80fce52a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,3 +14,7 @@ repos: args: [ --fix ] # Run the formatter. - id: ruff-format +- repo: https://github.com/teemtee/tmt.git + rev: 1.39.0 + hooks: + - id: tmt-lint diff --git a/plans/main.fmf b/plans/main.fmf new file mode 100644 index 00000000..06e46b52 --- /dev/null +++ b/plans/main.fmf @@ -0,0 +1,56 @@ +summary: Run Python Podman Tests + +discover: + how: fmf +execute: + how: tmt +prepare: + - name: pkg dependencies + how: install + package: + - make + - podman + - python3-pip + - python3.9 + - python3.10 + - python3.11 + - python3.12 + - python3.13 + + - name: pip dependencies + how: shell + script: + - pip3 install -r test-requirements.txt + + - name: ssh configuration + how: shell + script: + - ssh-keygen -t ecdsa -b 521 -f /root/.ssh/id_ecdsa -P "" + - cp /root/.ssh/authorized_keys /root/.ssh/authorized_keys% + - cat /root/.ssh/id_ecdsa.pub >>/root/.ssh/authorized_keys + +/upstream: + /sanity: + summary: Run Sanity and Coverage checks on Python Podman + discover+: + # we want to change this to tag:stable once all the coverage tests are fixed + filter: tag:lint + + /tests: + summary: Run Python Podman Tests on upstream PRs + discover+: + filter: tag:upstream + + adjust+: + enabled: false + when: initiator is not defined or initiator != packit + +/downstream: + /tests: + summary: Run Python Podman Tests on bodhi / errata and dist-git PRs + discover+: + filter: tag:downstream + + adjust+: + enabled: false + when: initiator == packit diff --git a/tests/main.fmf b/tests/main.fmf new file mode 100644 index 00000000..8aa9767e --- /dev/null +++ b/tests/main.fmf @@ -0,0 +1,23 @@ +require: + - make + - python3-pip + +/lint: + tag: [ stable, lint ] + summary: Run linting on the whole codebase + test: cd .. && make lint + +/coverage_integration: + tag: [ stable, coverage ] + summary: Run integration tests coverage check + test: cd .. && make integration + +/coverage_unittest: + tag: [ stable, coverage ] + summary: Run unit tests coverage check + test: cd .. && make unittest + +/tests: + tag: [ upstream, downstream ] + summary: Run all tests + test: cd .. && make tests