From 59c8f8c35bd671d9f5736d7185b9161224a1f8a3 Mon Sep 17 00:00:00 2001 From: Jaye Doepke Date: Wed, 2 Jun 2021 11:27:03 -0500 Subject: [PATCH 1/6] Add type hinting to __init__.py --- pytest_localstack/__init__.py | 50 ++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/pytest_localstack/__init__.py b/pytest_localstack/__init__.py index 5356eed..7421456 100644 --- a/pytest_localstack/__init__.py +++ b/pytest_localstack/__init__.py @@ -1,6 +1,7 @@ import contextlib import logging import sys +from typing import TYPE_CHECKING, Dict, List, Optional, Union import docker @@ -9,48 +10,53 @@ from pytest_localstack import plugin, session, utils -_start_timeout = None -_stop_timeout = None +if TYPE_CHECKING: + import _pytest.config + import _pytest.config.argparsing -def pytest_configure(config): +_start_timeout: Optional[float] = None +_stop_timeout: Optional[float] = None + + +def pytest_configure(config: "_pytest.config.Config"): global _start_timeout, _stop_timeout _start_timeout = config.getoption("--localstack-start-timeout") _stop_timeout = config.getoption("--localstack-stop-timeout") -def pytest_addoption(parser): +def pytest_addoption(parser: "_pytest.config.argparsing.Parser"): """Hook to add pytest_localstack command line options to pytest.""" group = parser.getgroup("localstack") group.addoption( "--localstack-start-timeout", action="store", - type=int, + type=float, default=60, help="max seconds for starting a localstack container", ) group.addoption( "--localstack-stop-timeout", action="store", - type=int, + type=float, default=5, help="max seconds for stopping a localstack container", ) def session_fixture( - scope="function", - services=None, - autouse=False, - docker_client=None, - region_name=None, - kinesis_error_probability=0.0, - dynamodb_error_probability=0.0, - container_log_level=logging.DEBUG, - localstack_version="latest", - auto_remove=True, - pull_image=True, - container_name=None, + scope: str = "function", + services: Optional[Union[List[str], Dict[str, int]]] = None, + autouse: bool = False, + docker_client: Optional[docker.DockerClient] = None, + region_name: Optional[str] = None, + kinesis_error_probability: float = 0.0, + dynamodb_error_probability: float = 0.0, + container_log_level: int = logging.DEBUG, + localstack_version: str = "latest", + auto_remove: bool = True, + pull_image: bool = True, + container_name: Optional[str] = None, **kwargs ): """Create a pytest fixture that provides a LocalstackSession. @@ -66,7 +72,7 @@ def session_fixture( Args: scope (str, optional): The pytest scope which this fixture will use. Defaults to :const:`"function"`. - services (list, dict, optional): One of: + services (list, optional): One of: - A :class:`list` of AWS service names to start in the Localstack container. @@ -108,8 +114,8 @@ def session_fixture( """ - @pytest.fixture(scope=scope, autouse=autouse) - def _fixture(pytestconfig): + @pytest.fixture(scope=scope, autouse=autouse) # type: ignore + def _fixture(pytestconfig: "_pytest.config.Config"): if not pytestconfig.pluginmanager.hasplugin("localstack"): pytest.skip("skipping because localstack plugin isn't loaded") with _make_session( @@ -131,7 +137,7 @@ def _fixture(pytestconfig): @contextlib.contextmanager -def _make_session(docker_client, *args, **kwargs): +def _make_session(docker_client: Optional[docker.DockerClient], *args, **kwargs): utils.check_proxy_env_vars() if docker_client is None: From 974d8a0181ae828d6e98641c44ce2bb85155bcf2 Mon Sep 17 00:00:00 2001 From: Jaye Doepke Date: Wed, 2 Jun 2021 11:27:40 -0500 Subject: [PATCH 2/6] Use bare super()s --- pytest_localstack/container.py | 2 +- pytest_localstack/contrib/botocore.py | 4 ++-- pytest_localstack/exceptions.py | 10 +++++----- pytest_localstack/session.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pytest_localstack/container.py b/pytest_localstack/container.py index a4df342..713595d 100644 --- a/pytest_localstack/container.py +++ b/pytest_localstack/container.py @@ -31,7 +31,7 @@ def __init__( self.stdout = stdout self.stderr = stderr self.encoding = encoding - super(DockerLogTailer, self).__init__() + super().__init__() self.daemon = True def run(self): diff --git a/pytest_localstack/contrib/botocore.py b/pytest_localstack/contrib/botocore.py index b495f39..018aa48 100644 --- a/pytest_localstack/contrib/botocore.py +++ b/pytest_localstack/contrib/botocore.py @@ -430,7 +430,7 @@ class Session(botocore.session.Session): def __init__(self, localstack_session, *args, **kwargs): self.localstack_session = localstack_session - super(Session, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _register_endpoint_resolver(self): def create_default_resolver(): @@ -499,7 +499,7 @@ class LocalstackEndpointResolver(botocore.regions.EndpointResolver): def __init__(self, localstack_session, endpoints): self.localstack_session = localstack_session - super(LocalstackEndpointResolver, self).__init__(endpoints) + super().__init__(endpoints) @property def valid_regions(self): diff --git a/pytest_localstack/exceptions.py b/pytest_localstack/exceptions.py index 8584c57..0bbbb10 100644 --- a/pytest_localstack/exceptions.py +++ b/pytest_localstack/exceptions.py @@ -10,7 +10,7 @@ class ContainerNotStartedError(Error): def __init__(self, session, *args, **kwargs): msg = f"{session!r} isn't started yet" - super(ContainerNotStartedError, self).__init__(msg, *args, **kwargs) + super().__init__(msg, *args, **kwargs) class ServiceError(Error): @@ -22,7 +22,7 @@ def __init__(self, msg=None, service_name=None, *args, **kwargs): msg = f"{service_name} isn't responding" else: msg = "Service error" - super(ServiceError, self).__init__(msg, *args, **kwargs) + super().__init__(msg, *args, **kwargs) class ContainerAlreadyStartedError(Error): @@ -30,7 +30,7 @@ class ContainerAlreadyStartedError(Error): def __init__(self, session, *args, **kwargs): msg = f"{session!r} is already started" - super(ContainerAlreadyStartedError, self).__init__(msg, *args, **kwargs) + super().__init__(msg, *args, **kwargs) class TimeoutError(Error): @@ -41,7 +41,7 @@ class UnsupportedPartitionError(Error): """Raised when asking for an AWS partition that isn't 'aws'.""" def __init__(self, partition_name): - super(UnsupportedPartitionError, self).__init__( + super().__init__( "LocalstackEndpointResolver only supports the 'aws' partition, " "not '%s'" % (partition_name,) ) @@ -56,7 +56,7 @@ class RegionError(Error): """ def __init__(self, region_name, should_be_region): - super(RegionError, self).__init__( + super().__init__( "This LocalstackSession is configured for region %s, not %s" % (should_be_region, region_name) ) diff --git a/pytest_localstack/session.py b/pytest_localstack/session.py index d7869be..6b782b0 100644 --- a/pytest_localstack/session.py +++ b/pytest_localstack/session.py @@ -259,7 +259,7 @@ def __init__( self.auto_remove = bool(auto_remove) self.pull_image = bool(pull_image) - super(LocalstackSession, self).__init__( + super().__init__( hostname=hostname if hostname else constants.LOCALHOST, services=services, region_name=region_name, From e87d634068579f8f45ac17005f1e8213e5c946da Mon Sep 17 00:00:00 2001 From: Jaye Doepke Date: Wed, 2 Jun 2021 11:42:21 -0500 Subject: [PATCH 3/6] Move patch_fixture() to pytest_localstack --- pytest_localstack/__init__.py | 98 +++++++++++++++- pytest_localstack/contrib/botocore.py | 105 +----------------- .../test_patch_fixture/test_README.py | 2 +- .../test_services_available.py | 2 +- 4 files changed, 99 insertions(+), 108 deletions(-) diff --git a/pytest_localstack/__init__.py b/pytest_localstack/__init__.py index 7421456..4572be1 100644 --- a/pytest_localstack/__init__.py +++ b/pytest_localstack/__init__.py @@ -44,6 +44,100 @@ def pytest_addoption(parser: "_pytest.config.argparsing.Parser"): ) +def patch_fixture( + scope: str = "function", + services: Optional[Union[List[str], Dict[str, int]]] = None, + autouse: bool = False, + docker_client: Optional[docker.DockerClient] = None, + region_name: Optional[str] = None, + kinesis_error_probability: float = 0.0, + dynamodb_error_probability: float = 0.0, + container_log_level: int = logging.DEBUG, + localstack_version: str = "latest", + auto_remove: bool = True, + pull_image: bool = True, + container_name: Optional[str] = None, + **kwargs, +): + """Create a pytest fixture that temporarily redirects all botocore + sessions and clients to a Localstack container. + + This is not a fixture! It is a factory to create them. + + The fixtures that are created by this function will run a Localstack + container and patch botocore to direct traffic there for the duration + of the tests. + + Since boto3 uses botocore to send requests, boto3 will also be redirected. + + Args: + scope (str, optional): The pytest scope which this fixture will use. + Defaults to :const:`"function"`. + services (list, dict, optional): One of + + - A :class:`list` of AWS service names to start in the + Localstack container. + - A :class:`dict` of service names to the port they should run on. + + Defaults to all services. Setting this + can reduce container startup time and therefore test time. + autouse (bool, optional): If :obj:`True`, automatically use this + fixture in applicable tests. Default: :obj:`False` + docker_client (:class:`~docker.client.DockerClient`, optional): + Docker client to run the Localstack container with. + Defaults to :func:`docker.client.from_env`. + region_name (str, optional): Region name to assume. + Each Localstack container acts like a single AWS region. + Defaults to :const:`"us-east-1"`. + kinesis_error_probability (float, optional): Decimal value between + 0.0 (default) and 1.0 to randomly inject + ProvisionedThroughputExceededException errors + into Kinesis API responses. + dynamodb_error_probability (float, optional): Decimal value + between 0.0 (default) and 1.0 to randomly inject + ProvisionedThroughputExceededException errors into + DynamoDB API responses. + container_log_level (int, optional): The logging level to use + for Localstack container logs. Defaults to :data:`logging.DEBUG`. + localstack_version (str, optional): The version of the Localstack + image to use. Defaults to :const:`"latest"`. + auto_remove (bool, optional): If :obj:`True`, delete the Localstack + container when it stops. Default: :obj:`True` + pull_image (bool, optional): If :obj:`True`, pull the Localstack + image before running it. Default: :obj:`True` + container_name (str, optional): The name for the Localstack + container. Defaults to a randomly generated id. + **kwargs: Additional kwargs will be passed to the + :class:`.LocalstackSession`. + + Returns: + A :func:`pytest fixture <_pytest.fixtures.fixture>`. + + """ + + @pytest.fixture(scope=scope, autouse=autouse) # type: ignore + def _fixture(pytestconfig): + if not pytestconfig.pluginmanager.hasplugin("localstack"): + pytest.skip("skipping because localstack plugin isn't loaded") + with _make_session( + docker_client=docker_client, + services=services, + region_name=region_name, + kinesis_error_probability=kinesis_error_probability, + dynamodb_error_probability=dynamodb_error_probability, + container_log_level=container_log_level, + localstack_version=localstack_version, + auto_remove=auto_remove, + pull_image=pull_image, + container_name=container_name, + **kwargs, + ) as session: + with session.botocore.patch_botocore(): + yield session + + return _fixture + + def session_fixture( scope: str = "function", services: Optional[Union[List[str], Dict[str, int]]] = None, @@ -57,7 +151,7 @@ def session_fixture( auto_remove: bool = True, pull_image: bool = True, container_name: Optional[str] = None, - **kwargs + **kwargs, ): """Create a pytest fixture that provides a LocalstackSession. @@ -129,7 +223,7 @@ def _fixture(pytestconfig: "_pytest.config.Config"): auto_remove=auto_remove, pull_image=pull_image, container_name=container_name, - **kwargs + **kwargs, ) as session: yield session diff --git a/pytest_localstack/contrib/botocore.py b/pytest_localstack/contrib/botocore.py index 018aa48..980509b 100644 --- a/pytest_localstack/contrib/botocore.py +++ b/pytest_localstack/contrib/botocore.py @@ -14,9 +14,7 @@ import botocore.regions import botocore.session -import pytest - -from pytest_localstack import _make_session, constants, exceptions, hookspecs, utils +from pytest_localstack import constants, exceptions, hookspecs, utils from pytest_localstack.session import RunningSession @@ -35,13 +33,6 @@ def contribute_to_session(session): session.botocore = BotocoreTestResourceFactory(session) -@hookspecs.pytest_localstack_hookimpl -def contribute_to_module(pytest_localstack): - """Add :func:`patch_fixture` to :mod:`pytest_localstack`.""" - logger.debug("patching module %r", pytest_localstack) - pytest_localstack.patch_fixture = patch_fixture - - class BotocoreTestResourceFactory: """Create botocore clients to interact with a :class:`.LocalstackSession`. @@ -327,100 +318,6 @@ def new_getattribute(self, key): boto3.DEFAULT_SESSION = preexisting_boto3_session -def patch_fixture( - scope="function", - services=None, - autouse=False, - docker_client=None, - region_name=None, - kinesis_error_probability=0.0, - dynamodb_error_probability=0.0, - container_log_level=logging.DEBUG, - localstack_version="latest", - auto_remove=True, - pull_image=True, - container_name=None, - **kwargs, -): - """Create a pytest fixture that temporarially redirects all botocore - sessions and clients to a Localstack container. - - This is not a fixture! It is a factory to create them. - - The fixtures that are created by this function will run a Localstack - container and patch botocore to direct traffic there for the duration - of the tests. - - Since boto3 uses botocore to send requests, boto3 will also be redirected. - - Args: - scope (str, optional): The pytest scope which this fixture will use. - Defaults to :const:`"function"`. - services (list, dict, optional): One of - - - A :class:`list` of AWS service names to start in the - Localstack container. - - A :class:`dict` of service names to the port they should run on. - - Defaults to all services. Setting this - can reduce container startup time and therefore test time. - autouse (bool, optional): If :obj:`True`, automatically use this - fixture in applicable tests. Default: :obj:`False` - docker_client (:class:`~docker.client.DockerClient`, optional): - Docker client to run the Localstack container with. - Defaults to :func:`docker.client.from_env`. - region_name (str, optional): Region name to assume. - Each Localstack container acts like a single AWS region. - Defaults to :const:`"us-east-1"`. - kinesis_error_probability (float, optional): Decimal value between - 0.0 (default) and 1.0 to randomly inject - ProvisionedThroughputExceededException errors - into Kinesis API responses. - dynamodb_error_probability (float, optional): Decimal value - between 0.0 (default) and 1.0 to randomly inject - ProvisionedThroughputExceededException errors into - DynamoDB API responses. - container_log_level (int, optional): The logging level to use - for Localstack container logs. Defaults to :data:`logging.DEBUG`. - localstack_version (str, optional): The version of the Localstack - image to use. Defaults to :const:`"latest"`. - auto_remove (bool, optional): If :obj:`True`, delete the Localstack - container when it stops. Default: :obj:`True` - pull_image (bool, optional): If :obj:`True`, pull the Localstack - image before running it. Default: :obj:`True` - container_name (str, optional): The name for the Localstack - container. Defaults to a randomly generated id. - **kwargs: Additional kwargs will be passed to the - :class:`.LocalstackSession`. - - Returns: - A :func:`pytest fixture <_pytest.fixtures.fixture>`. - - """ - - @pytest.fixture(scope=scope, autouse=autouse) - def _fixture(pytestconfig): - if not pytestconfig.pluginmanager.hasplugin("localstack"): - pytest.skip("skipping because localstack plugin isn't loaded") - with _make_session( - docker_client=docker_client, - services=services, - region_name=region_name, - kinesis_error_probability=kinesis_error_probability, - dynamodb_error_probability=dynamodb_error_probability, - container_log_level=container_log_level, - localstack_version=localstack_version, - auto_remove=auto_remove, - pull_image=pull_image, - container_name=container_name, - **kwargs, - ) as session: - with session.botocore.patch_botocore(): - yield session - - return _fixture - - # Grab a reference here to avoid breaking things during patching. _original_create_client = utils.unbind(botocore.session.Session.create_client) diff --git a/tests/functional/test_patch_fixture/test_README.py b/tests/functional/test_patch_fixture/test_README.py index e25e64d..f32933c 100644 --- a/tests/functional/test_patch_fixture/test_README.py +++ b/tests/functional/test_patch_fixture/test_README.py @@ -6,7 +6,7 @@ import pytest_localstack -patch_s3 = pytest_localstack.patch_fixture(services=["s3"]) # type: ignore +patch_s3 = pytest_localstack.patch_fixture(services=["s3"]) @pytest.mark.usefixtures("patch_s3") diff --git a/tests/functional/test_patch_fixture/test_services_available.py b/tests/functional/test_patch_fixture/test_services_available.py index 22e7476..3e3175b 100644 --- a/tests/functional/test_patch_fixture/test_services_available.py +++ b/tests/functional/test_patch_fixture/test_services_available.py @@ -5,7 +5,7 @@ import pytest_localstack -localstack = pytest_localstack.patch_fixture(scope="module", autouse=True) # type: ignore +localstack = pytest_localstack.patch_fixture(scope="module", autouse=True) def _assert_key_isinstance(result, key, type): From 85349bcf88a76849153fea7ad9f01d4c762830b7 Mon Sep 17 00:00:00 2001 From: Jaye Doepke Date: Wed, 2 Jun 2021 12:06:14 -0500 Subject: [PATCH 4/6] Move botocore to pytest_localstack --- pytest_localstack/__init__.py | 1 - pytest_localstack/{contrib => }/botocore.py | 15 ++++++--------- pytest_localstack/session.py | 3 +++ tests/integration/test_contrib/test_botocore.py | 17 ++--------------- 4 files changed, 11 insertions(+), 25 deletions(-) rename pytest_localstack/{contrib => }/botocore.py (97%) diff --git a/pytest_localstack/__init__.py b/pytest_localstack/__init__.py index 4572be1..fa3b1c0 100644 --- a/pytest_localstack/__init__.py +++ b/pytest_localstack/__init__.py @@ -252,7 +252,6 @@ def _make_session(docker_client: Optional[docker.DockerClient], *args, **kwargs) # Register contrib modules -plugin.register_plugin_module("pytest_localstack.contrib.botocore") plugin.register_plugin_module("pytest_localstack.contrib.boto3", False) # Register 3rd-party modules diff --git a/pytest_localstack/contrib/botocore.py b/pytest_localstack/botocore.py similarity index 97% rename from pytest_localstack/contrib/botocore.py rename to pytest_localstack/botocore.py index 980509b..5485fd0 100644 --- a/pytest_localstack/contrib/botocore.py +++ b/pytest_localstack/botocore.py @@ -5,6 +5,7 @@ import logging import socket import weakref +from typing import TYPE_CHECKING from unittest import mock import botocore @@ -14,8 +15,11 @@ import botocore.regions import botocore.session -from pytest_localstack import constants, exceptions, hookspecs, utils -from pytest_localstack.session import RunningSession +from pytest_localstack import constants, exceptions, utils + + +if TYPE_CHECKING: + from pytest_localstack.session import RunningSession try: @@ -26,13 +30,6 @@ logger = logging.getLogger(__name__) -@hookspecs.pytest_localstack_hookimpl -def contribute_to_session(session): - """Add :class:`BotocoreTestResourceFactory` to :class:`.LocalstackSession`.""" - logger.debug("patching session %r", session) - session.botocore = BotocoreTestResourceFactory(session) - - class BotocoreTestResourceFactory: """Create botocore clients to interact with a :class:`.LocalstackSession`. diff --git a/pytest_localstack/session.py b/pytest_localstack/session.py index 6b782b0..6c7f183 100644 --- a/pytest_localstack/session.py +++ b/pytest_localstack/session.py @@ -13,6 +13,7 @@ service_checks, utils, ) +from pytest_localstack.botocore import BotocoreTestResourceFactory logger = logging.getLogger(__name__) @@ -44,6 +45,8 @@ def __init__( else: self.service_ports = constants.SERVICE_PORTS + self.botocore = BotocoreTestResourceFactory(self) + plugin.manager.hook.contribute_to_session(session=self) # If no region was provided, use what botocore defaulted to. if not region_name: diff --git a/tests/integration/test_contrib/test_botocore.py b/tests/integration/test_contrib/test_botocore.py index e7333fa..62fa3a8 100644 --- a/tests/integration/test_contrib/test_botocore.py +++ b/tests/integration/test_contrib/test_botocore.py @@ -4,21 +4,8 @@ import pytest from tests import utils as test_utils -import pytest_localstack -from pytest_localstack import constants, exceptions, plugin -from pytest_localstack.contrib import botocore as localstack_botocore - - -def test_patch_fixture_contributed_to_module(): - assert pytest_localstack.patch_fixture is localstack_botocore.patch_fixture - - -def test_session_contribution(): - dummy_session = type("DummySession", (object,), {})() - plugin.manager.hook.contribute_to_session(session=dummy_session) - assert isinstance( - dummy_session.botocore, localstack_botocore.BotocoreTestResourceFactory - ) +from pytest_localstack import botocore as localstack_botocore +from pytest_localstack import constants, exceptions def test_create_credential_resolver(): From 1780603e40dce0832eb07683bb1306bf667bceb9 Mon Sep 17 00:00:00 2001 From: Jaye Doepke Date: Wed, 2 Jun 2021 12:07:11 -0500 Subject: [PATCH 5/6] Reorder test imports --- pyproject.toml | 4 ++-- tests/integration/test_contrib/test_boto3.py | 2 +- tests/integration/test_contrib/test_botocore.py | 2 +- tests/unit/test_container.py | 3 +-- tests/unit/test_session.py | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 580e25a..9ce03cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,8 +69,8 @@ lines_after_imports = 2 # Structure sections = ["FUTURE", "STDLIB", "THIRDPARTY", "TESTING", "FIRSTPARTY", "LOCALFOLDER"] default_section = "THIRDPARTY" -known_testing = ["hypothesis", "pytest", "tests"] -known_first_party = ["pytest_localstack"] +known_testing = ["hypothesis", "pytest"] +known_first_party = ["pytest_localstack", "tests"] [tool.flake8] max-line-length = 88 # Black line length. diff --git a/tests/integration/test_contrib/test_boto3.py b/tests/integration/test_contrib/test_boto3.py index 4bc9d3b..275e0e3 100644 --- a/tests/integration/test_contrib/test_boto3.py +++ b/tests/integration/test_contrib/test_boto3.py @@ -5,10 +5,10 @@ import botocore import pytest -from tests import utils as test_utils from pytest_localstack import constants, exceptions, plugin from pytest_localstack.contrib import boto3 as ptls_boto3 +from tests import utils as test_utils def test_session_contribution(): diff --git a/tests/integration/test_contrib/test_botocore.py b/tests/integration/test_contrib/test_botocore.py index 62fa3a8..c3db5ab 100644 --- a/tests/integration/test_contrib/test_botocore.py +++ b/tests/integration/test_contrib/test_botocore.py @@ -2,10 +2,10 @@ import botocore.session import pytest -from tests import utils as test_utils from pytest_localstack import botocore as localstack_botocore from pytest_localstack import constants, exceptions +from tests import utils as test_utils def test_create_credential_resolver(): diff --git a/tests/unit/test_container.py b/tests/unit/test_container.py index b70a891..afac319 100644 --- a/tests/unit/test_container.py +++ b/tests/unit/test_container.py @@ -1,10 +1,9 @@ import logging from unittest import mock -from tests import utils as test_utils - from pytest_localstack import container as ptls_container from pytest_localstack import session +from tests import utils as test_utils def test_DockerLogTailer(caplog): diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index 9c0656b..07e979a 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -4,9 +4,9 @@ import pytest from hypothesis import given from hypothesis import strategies as st -from tests import utils as test_utils from pytest_localstack import constants, exceptions, session +from tests import utils as test_utils @given(random=st.random_module()) From f66970256af9d99369b5e0853c3ee4764b10066f Mon Sep 17 00:00:00 2001 From: Jaye Doepke Date: Fri, 4 Jun 2021 20:15:40 -0500 Subject: [PATCH 6/6] wip --- .travis.yml | 1 - Makefile | 29 -- docs/Makefile | 20 - docs/_build/.gitignore | 4 - docs/_static/.gitignore | 4 - docs/conf.py | 201 -------- docs/index.rst | 24 - docs/internals/contrib/boto3.rst | 5 - docs/internals/contrib/botocore.rst | 5 - docs/internals/contrib/index.rst | 10 - docs/internals/hooks.rst | 8 - docs/internals/index.rst | 9 - docs/internals/session.rst | 4 - docs/make.bat | 36 -- docs/requirements.txt | 2 - docs/using.rst | 5 - examples/sync_buckets/test_sync_buckets.py | 72 +-- poetry.lock | 315 +---------- pyproject.toml | 9 +- pytest_localstack/__init__.py | 487 ++++++++++-------- pytest_localstack/botocore.py | 447 ---------------- pytest_localstack/constants.py | 48 +- pytest_localstack/container.py | 327 +++++++++++- pytest_localstack/contrib/__init__.py | 0 pytest_localstack/contrib/boto3.py | 71 --- pytest_localstack/exceptions.py | 15 +- pytest_localstack/hookspecs.py | 52 -- pytest_localstack/patch.py | 112 ++++ pytest_localstack/plugin.py | 39 -- pytest_localstack/service_checks.py | 162 ------ pytest_localstack/session.py | 410 --------------- pytest_localstack/utils.py | 76 ++- tests/conftest.py | 22 + .../test_patch_fixture/test_README.py | 24 +- .../test_services_available.py | 252 --------- .../test_service_fixture/test_README.py | 98 ++-- .../test_services_available.py | 240 --------- tests/functional/test_session.py | 78 +-- tests/integration/test_contrib/__init__.py | 0 tests/integration/test_contrib/test_boto3.py | 162 ------ .../integration/test_contrib/test_botocore.py | 203 -------- tests/integration/test_plugin.py | 20 +- tests/unit/test_container.py | 3 +- tests/unit/test_patch.py | 58 +++ tests/unit/test_session.py | 220 ++++---- tests/unit/test_utils.py | 57 +- tests/utils.py | 6 +- 47 files changed, 1105 insertions(+), 3347 deletions(-) delete mode 100644 docs/Makefile delete mode 100644 docs/_build/.gitignore delete mode 100644 docs/_static/.gitignore delete mode 100644 docs/conf.py delete mode 100644 docs/index.rst delete mode 100644 docs/internals/contrib/boto3.rst delete mode 100644 docs/internals/contrib/botocore.rst delete mode 100644 docs/internals/contrib/index.rst delete mode 100644 docs/internals/hooks.rst delete mode 100644 docs/internals/index.rst delete mode 100644 docs/internals/session.rst delete mode 100644 docs/make.bat delete mode 100644 docs/requirements.txt delete mode 100644 docs/using.rst delete mode 100644 pytest_localstack/botocore.py delete mode 100644 pytest_localstack/contrib/__init__.py delete mode 100644 pytest_localstack/contrib/boto3.py delete mode 100644 pytest_localstack/hookspecs.py create mode 100644 pytest_localstack/patch.py delete mode 100644 pytest_localstack/plugin.py delete mode 100644 pytest_localstack/service_checks.py delete mode 100644 pytest_localstack/session.py create mode 100644 tests/conftest.py delete mode 100644 tests/functional/test_patch_fixture/test_services_available.py delete mode 100644 tests/functional/test_service_fixture/test_services_available.py delete mode 100644 tests/integration/test_contrib/__init__.py delete mode 100644 tests/integration/test_contrib/test_boto3.py delete mode 100644 tests/integration/test_contrib/test_botocore.py create mode 100644 tests/unit/test_patch.py diff --git a/.travis.yml b/.travis.yml index 1f4263e..2e67947 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,6 @@ jobs: install: make install script: - make lint - - make docs after_success: [] deploy: [] - stage: deploy diff --git a/Makefile b/Makefile index 2a6adab..b8afc21 100644 --- a/Makefile +++ b/Makefile @@ -61,32 +61,3 @@ fmt: $(INSTALL_STAMP) ## apply code style formatting .PHONY: test test: $(INSTALL_STAMP) ## run tests $(POETRY) run pytest - -.PHONY: docs -docs: $(INSTALL_STAMP) ## build documentation - $(POETRY) run $(MAKE) -C docs html - - -.PHONY: docs-live -docs-live: $(INSTALL_STAMP) ## build and view docs in real-time - $(POETRY) run sphinx-autobuild -b html \ - -p 0 \ - --open-browser \ - --watch ./ \ - --ignore ".git/*" \ - --ignore ".venv/*" \ - --ignore "*.swp" \ - --ignore "*.pdf" \ - --ignore "*.log" \ - --ignore "*.out" \ - --ignore "*.toc" \ - --ignore "*.aux" \ - --ignore "*.idx" \ - --ignore "*.ind" \ - --ignore "*.ilg" \ - --ignore "*.tex" \ - --ignore "Makefile" \ - --ignore "setup.py" \ - --ignore "setup.cfg" \ - --ignore "Pipfile*" \ - docs docs/_build/html diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 9424f0c..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = pytest-localstack -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_build/.gitignore b/docs/_build/.gitignore deleted file mode 100644 index 5e7d273..0000000 --- a/docs/_build/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore deleted file mode 100644 index 5e7d273..0000000 --- a/docs/_static/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 11c558b..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- -# -# pytest-localstack documentation build configuration file, created by -# sphinx-quickstart on Tue Mar 6 09:13:05 2018. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. - -import os -import sys -try: - import importlib.metadata as importlib_metadata -except ModuleNotFoundError: - import importlib_metadata - -DOCS = os.path.dirname(os.path.abspath(__file__)) -ROOT = os.path.dirname(DOCS) -sys.path.insert(0, ROOT) - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. - -needs_sphinx = "1.3" - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.coverage", - "sphinx.ext.intersphinx", - "sphinx.ext.napoleon", - "sphinx.ext.viewcode", -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -source_suffix = [".rst", ".md"] - -# The master toctree document. -master_doc = "index" - -# General information about the project. -project = u"pytest-localstack" -copyright = u"2018, Mintel" -author = u"Mintel Group Ltd." - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# The short X.Y version. -version = ".".join(importlib_metadata.version("pytest_localstack").split(".")[:2]) -# The full version, including alpha/beta/rc tags. -release = importlib_metadata.version("pytest_localstack") - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "alabaster" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. - -html_theme_options = { - "github_user": "mintel", - "github_repo": "pytest-localstack", - "description": "AWS integration tests via Localstack Docker container.", - "github_banner": True, -} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - "**": [ - "about.html", - "navigation.html", - "relations.html", # needs 'show_related': True theme option to display - "searchbox.html", - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = "pytest-localstackdoc" - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ( - master_doc, - "pytest-localstack.tex", - u"pytest-localstack Documentation", - u"Jaye Doepke", - "manual", - ) -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, "pytest-localstack", u"pytest-localstack Documentation", [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "pytest-localstack", - u"pytest-localstack Documentation", - author, - "pytest-localstack", - "One line description of project.", - "Miscellaneous", - ) -] - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - "boto3": ("https://boto3.readthedocs.io/en/stable/", None), - "botocore": ("https://botocore.readthedocs.io/en/stable/", None), - "docker-py": ("https://docker-py.readthedocs.io/en/stable/", None), - "pytest": ("https://docs.pytest.org/en/stable/", None), - "python": ("https://docs.python.org/", None), -} - -# Add parser for .md files -source_parsers = {".md": "recommonmark.parser.CommonMarkParser"} diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index f40e894..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. pytest-localstack documentation master file, created by - sphinx-quickstart on Tue Mar 6 09:13:05 2018. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -.. include:: ../README.rst - -Table of Contents -================= -.. toctree:: - :maxdepth: 2 - - using - internals/index - -.. include:: ../CHANGELOG.rst - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/internals/contrib/boto3.rst b/docs/internals/contrib/boto3.rst deleted file mode 100644 index 0a56dd8..0000000 --- a/docs/internals/contrib/boto3.rst +++ /dev/null @@ -1,5 +0,0 @@ -boto3 -===== - -.. automodule:: pytest_localstack.contrib.boto3 - :members: diff --git a/docs/internals/contrib/botocore.rst b/docs/internals/contrib/botocore.rst deleted file mode 100644 index f923fe2..0000000 --- a/docs/internals/contrib/botocore.rst +++ /dev/null @@ -1,5 +0,0 @@ -botocore -======== - -.. automodule:: pytest_localstack.contrib.botocore - :members: diff --git a/docs/internals/contrib/index.rst b/docs/internals/contrib/index.rst deleted file mode 100644 index a85cce2..0000000 --- a/docs/internals/contrib/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Contrib -======= - -.. automodule:: pytest_localstack.contrib - :members: - -.. toctree:: - - botocore - boto3 diff --git a/docs/internals/hooks.rst b/docs/internals/hooks.rst deleted file mode 100644 index 129c4e8..0000000 --- a/docs/internals/hooks.rst +++ /dev/null @@ -1,8 +0,0 @@ -Plugins/Hooks -============= - -.. automodule:: pytest_localstack.hookspecs - :members: - -.. automodule:: pytest_localstack.plugin - :members: diff --git a/docs/internals/index.rst b/docs/internals/index.rst deleted file mode 100644 index e10a1c8..0000000 --- a/docs/internals/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Internals -========= - -.. toctree:: - :maxdepth: 2 - - session - hooks - contrib/index diff --git a/docs/internals/session.rst b/docs/internals/session.rst deleted file mode 100644 index 1a9d70a..0000000 --- a/docs/internals/session.rst +++ /dev/null @@ -1,4 +0,0 @@ -LocalstackSession -================= - -.. autoclass:: pytest_localstack.session.LocalstackSession diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 006070f..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=pytest-localstack - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 8f7add7..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -recommonmark -boto3 diff --git a/docs/using.rst b/docs/using.rst deleted file mode 100644 index 1eaf36b..0000000 --- a/docs/using.rst +++ /dev/null @@ -1,5 +0,0 @@ -Usage -===== - -.. autofunction:: pytest_localstack.patch_fixture -.. autofunction:: pytest_localstack.session_fixture diff --git a/examples/sync_buckets/test_sync_buckets.py b/examples/sync_buckets/test_sync_buckets.py index 2496516..a1cea23 100644 --- a/examples/sync_buckets/test_sync_buckets.py +++ b/examples/sync_buckets/test_sync_buckets.py @@ -1,51 +1,51 @@ -"""Test sync_buckets.py""" -import boto3 -from sync_buckets import sync_buckets +# """Test sync_buckets.py""" +# import boto3 +# from sync_buckets import sync_buckets -import pytest_localstack +# import pytest_localstack -patch = pytest_localstack.patch_fixture(services=["s3"]) -localstack_1 = pytest_localstack.session_fixture() -localstack_2 = pytest_localstack.session_fixture() +# patch = pytest_localstack.patch_fixture(services=["s3"]) +# localstack_1 = pytest_localstack.session_fixture() +# localstack_2 = pytest_localstack.session_fixture() -def test_sync_buckets_patch(patch): - """Test using patch_fixture.""" - s3 = boto3.resource("s3") - src_bucket = s3.Bucket("src-bucket") - src_bucket.create() - dest_bucket = s3.Bucket("dest-bucket") - dest_bucket.create() +# def test_sync_buckets_patch(patch): +# """Test using patch_fixture.""" +# s3 = boto3.resource("s3") +# src_bucket = s3.Bucket("src-bucket") +# src_bucket.create() +# dest_bucket = s3.Bucket("dest-bucket") +# dest_bucket.create() - src_bucket.put_object(Key="test", Body=b"foobar") +# src_bucket.put_object(Key="test", Body=b"foobar") - result = sync_buckets(src_bucket, dest_bucket) - assert result == 1 +# result = sync_buckets(src_bucket, dest_bucket) +# assert result == 1 - assert len(list(dest_bucket.objects.all())) == 1 +# assert len(list(dest_bucket.objects.all())) == 1 - response = dest_bucket.Object("test").get() - data = response["Body"].read() - assert data == b"foobar" +# response = dest_bucket.Object("test").get() +# data = response["Body"].read() +# assert data == b"foobar" -def test_sync_buckets_between_accounts(localstack_1, localstack_2): - """Test using session_fixture.""" - src_s3 = localstack_1.boto3.resource("s3") - src_bucket = src_s3.Bucket("src-bucket") - src_bucket.create() +# def test_sync_buckets_between_accounts(localstack_1, localstack_2): +# """Test using session_fixture.""" +# src_s3 = localstack_1.boto3.resource("s3") +# src_bucket = src_s3.Bucket("src-bucket") +# src_bucket.create() - dest_s3 = localstack_2.boto3.resource("s3") - dest_bucket = dest_s3.Bucket("dest-bucket") - dest_bucket.create() +# dest_s3 = localstack_2.boto3.resource("s3") +# dest_bucket = dest_s3.Bucket("dest-bucket") +# dest_bucket.create() - src_bucket.put_object(Key="test", Body=b"foobar") +# src_bucket.put_object(Key="test", Body=b"foobar") - result = sync_buckets(src_bucket, dest_bucket) - assert result == 1 +# result = sync_buckets(src_bucket, dest_bucket) +# assert result == 1 - assert len(list(dest_bucket.objects.all())) == 1 +# assert len(list(dest_bucket.objects.all())) == 1 - response = dest_bucket.Object("test").get() - data = response["Body"].read() - assert data == b"foobar" +# response = dest_bucket.Object("test").get() +# data = response["Body"].read() +# assert data == b"foobar" diff --git a/poetry.lock b/poetry.lock index 446d64e..a308e09 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,11 +1,3 @@ -[[package]] -name = "alabaster" -version = "0.7.12" -description = "A configurable sidebar-enabled Sphinx theme" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "apipkg" version = "1.5" @@ -44,17 +36,6 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] -[[package]] -name = "babel" -version = "2.9.1" -description = "Internationalization utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -pytz = ">=2015.7" - [[package]] name = "bandit" version = "1.7.0" @@ -97,20 +78,20 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.17.84" +version = "1.17.88" description = "The AWS SDK for Python" category = "dev" optional = false python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] -botocore = ">=1.20.84,<1.21.0" +botocore = ">=1.20.88,<1.21.0" jmespath = ">=0.7.1,<1.0.0" s3transfer = ">=0.4.0,<0.5.0" [[package]] name = "botocore" -version = "1.20.84" +version = "1.20.88" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -212,14 +193,6 @@ websocket-client = ">=0.32.0" ssh = ["paramiko (>=2.4.2)"] tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] -[[package]] -name = "docutils" -version = "0.17.1" -description = "Docutils -- Python Documentation Utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "execnet" version = "1.8.1" @@ -273,7 +246,7 @@ typing-extensions = {version = ">=3.7.4.0", markers = "python_version < \"3.8\"" [[package]] name = "hypothesis" -version = "6.13.10" +version = "6.13.14" description = "A library for property-based testing" category = "dev" optional = false @@ -307,17 +280,9 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[[package]] -name = "imagesize" -version = "1.2.0" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "importlib-metadata" -version = "4.4.0" +version = "4.5.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -352,20 +317,6 @@ pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] -[[package]] -name = "jinja2" -version = "3.0.1" -description = "A very fast and expressive template engine." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - [[package]] name = "jmespath" version = "0.10.0" @@ -374,14 +325,6 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "markupsafe" -version = "2.0.1" -description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" -optional = false -python-versions = ">=3.6" - [[package]] name = "mccabe" version = "0.6.1" @@ -443,14 +386,14 @@ python-versions = ">=2.6" [[package]] name = "pluggy" -version = "0.12.0" +version = "0.13.1" description = "plugin and hook calling mechanisms for python" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] -importlib-metadata = ">=0.12" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] @@ -479,14 +422,6 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[[package]] -name = "pygments" -version = "2.9.0" -description = "Pygments is a syntax highlighting package written in Python." -category = "dev" -optional = false -python-versions = ">=3.5" - [[package]] name = "pyparsing" version = "2.4.7" @@ -585,14 +520,6 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" -[[package]] -name = "pytz" -version = "2021.1" -description = "World timezone definitions, modern and historical" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "pywin32" version = "227" @@ -665,14 +592,6 @@ category = "dev" optional = false python-versions = ">=3.5" -[[package]] -name = "snowballstemmer" -version = "2.1.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "sortedcontainers" version = "2.4.0" @@ -681,108 +600,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "sphinx" -version = "4.0.2" -description = "Python documentation generator" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=1.3" -colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.18" -imagesize = "*" -Jinja2 = ">=2.3" -packaging = "*" -Pygments = ">=2.0" -requests = ">=2.5.0" -snowballstemmer = ">=1.1" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = "*" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = "*" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"] -test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.2" -description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest", "html5lib"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -test = ["pytest", "flake8", "mypy"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest"] - [[package]] name = "stevedore" version = "3.3.0" @@ -855,13 +672,9 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "17c49171489c470fe7df9d1ae532c962c695fd1b6679a5fc1db18951e1a48596" +content-hash = "730c61a56a686300e6eb82a44f82f082c8b413bf35ba7ab58f003706ff60cd3a" [metadata.files] -alabaster = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, -] apipkg = [ {file = "apipkg-1.5-py2.py3-none-any.whl", hash = "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"}, {file = "apipkg-1.5.tar.gz", hash = "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6"}, @@ -878,10 +691,6 @@ attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] -babel = [ - {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, - {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, -] bandit = [ {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, @@ -891,12 +700,12 @@ black = [ {file = "black-21.5b2.tar.gz", hash = "sha256:1fc0e0a2c8ae7d269dfcf0c60a89afa299664f3e811395d40b1922dff8f854b5"}, ] boto3 = [ - {file = "boto3-1.17.84-py2.py3-none-any.whl", hash = "sha256:1d24c6d1f5db4b52bb29f1dfe13fd3e9d95d9fa4634b0638a096f5a884173cde"}, - {file = "boto3-1.17.84.tar.gz", hash = "sha256:8ee8766813864796be6c87ad762c6da4bfef603977931854a38f49fe4db06495"}, + {file = "boto3-1.17.88-py2.py3-none-any.whl", hash = "sha256:13afcc5e2fcc5e4f9eab1ee46a769cf738a259dcd45f71ee79255f18973e4584"}, + {file = "boto3-1.17.88.tar.gz", hash = "sha256:a715ca6c4457d56ea3e3efde9bdc8be41c29b2f2a904fbd12befdb9cb5e289e4"}, ] botocore = [ - {file = "botocore-1.20.84-py2.py3-none-any.whl", hash = "sha256:75e1397b80aa8757a26636b949eebd20b3cf67e8f1ed80dc01170907e06ea45d"}, - {file = "botocore-1.20.84.tar.gz", hash = "sha256:bc59eb748fcb07835613ebea6dcc2600ae1a8be0fae30e40b9c1e81b73262296"}, + {file = "botocore-1.20.88-py2.py3-none-any.whl", hash = "sha256:be3cb73fab60a2349e2932bd0cbbe7e7736e3a2cd8c05b539d362ff3e406be76"}, + {file = "botocore-1.20.88.tar.gz", hash = "sha256:bc989edab52d4788aadd8d1aff925f5c6a7cbc68900bfdb8e379965aeac17317"}, ] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, @@ -981,10 +790,6 @@ docker = [ {file = "docker-4.4.4-py2.py3-none-any.whl", hash = "sha256:f3607d5695be025fa405a12aca2e5df702a57db63790c73b927eb6a94aac60af"}, {file = "docker-4.4.4.tar.gz", hash = "sha256:d3393c878f575d3a9ca3b94471a3c89a6d960b35feb92f033c0de36cc9d934db"}, ] -docutils = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, -] execnet = [ {file = "execnet-1.8.1-py2.py3-none-any.whl", hash = "sha256:e840ce25562e414ee5684864d510dbeeb0bce016bc89b22a6e5ce323b5e6552f"}, {file = "execnet-1.8.1.tar.gz", hash = "sha256:7e3c2cdb6389542a91e9855a9cc7545fbed679e96f8808bcbb1beb325345b189"}, @@ -1002,20 +807,16 @@ gitpython = [ {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"}, ] hypothesis = [ - {file = "hypothesis-6.13.10-py3-none-any.whl", hash = "sha256:d3038cb55263cc31e42cd79d98197d16b74cff28a3b8c0f41ccda30fdabf0752"}, - {file = "hypothesis-6.13.10.tar.gz", hash = "sha256:f431ec921d4d1eb1c0fd8e59e64452158e8587342c3841a39d24bb1e5788a7fd"}, + {file = "hypothesis-6.13.14-py3-none-any.whl", hash = "sha256:8f9346a60183d7e9213f6af4d0b3ce19130530884d9444087263775da327d81d"}, + {file = "hypothesis-6.13.14.tar.gz", hash = "sha256:36ef2d58f600be2973f694f45a55a5502de705d7594f9cf841276aec9082c414"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] -imagesize = [ - {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, - {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, -] importlib-metadata = [ - {file = "importlib_metadata-4.4.0-py3-none-any.whl", hash = "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786"}, - {file = "importlib_metadata-4.4.0.tar.gz", hash = "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5"}, + {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, + {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -1025,50 +826,10 @@ isort = [ {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, ] -jinja2 = [ - {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, - {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, -] jmespath = [ {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"}, {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"}, ] -markupsafe = [ - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, -] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, @@ -1114,8 +875,8 @@ pbr = [ {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, ] pluggy = [ - {file = "pluggy-0.12.0-py2.py3-none-any.whl", hash = "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"}, - {file = "pluggy-0.12.0.tar.gz", hash = "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc"}, + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, @@ -1129,10 +890,6 @@ pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] -pygments = [ - {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, - {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, -] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, @@ -1161,10 +918,6 @@ python-dateutil = [ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, ] -pytz = [ - {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, - {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, -] pywin32 = [ {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, @@ -1269,42 +1022,10 @@ smmap = [ {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, ] -snowballstemmer = [ - {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, - {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, -] sortedcontainers = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] -sphinx = [ - {file = "Sphinx-4.0.2-py3-none-any.whl", hash = "sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4"}, - {file = "Sphinx-4.0.2.tar.gz", hash = "sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, -] -sphinxcontrib-jsmath = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] -sphinxcontrib-qthelp = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] stevedore = [ {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, diff --git a/pyproject.toml b/pyproject.toml index 9ce03cc..50c7f6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,8 @@ localstack = "pytest_localstack" python = "^3.6.2" botocore = "^1.4.31,!=1.4.45" docker = "^4.0.0" -pluggy = "^0.12.0" -pytest = "^6.0.0" # need caplog (+ warnings for tests) +pytest = ">=3.6.0" +requests = "^2.25.1" [tool.poetry.dev-dependencies] boto3 = "*" @@ -47,9 +47,8 @@ bandit = "^1.7.0" pyproject-flake8 = "^0.0.1-alpha.2" coverage = {extras = ["toml"], version = "^5.5"} codecov = "^2.1.11" -pytest-cov = "^2.12.1" -pytest-xdist = "^2.2.1" -Sphinx = "^4.0.2" +pytest-cov = "*" +pytest-xdist = "*" [build-system] requires = ["setuptools", "wheel"] diff --git a/pytest_localstack/__init__.py b/pytest_localstack/__init__.py index fa3b1c0..d47b982 100644 --- a/pytest_localstack/__init__.py +++ b/pytest_localstack/__init__.py @@ -1,263 +1,306 @@ -import contextlib import logging -import sys -from typing import TYPE_CHECKING, Dict, List, Optional, Union +from enum import auto +from typing import TYPE_CHECKING, Union +from unittest import mock import docker +import docker.errors import pytest -from pytest_localstack import plugin, session, utils +from pytest_localstack import container, patch, utils + + +LOGGER = logging.getLogger(__name__) +# https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library +LOGGER.addHandler(logging.NullHandler()) if TYPE_CHECKING: import _pytest.config import _pytest.config.argparsing - -_start_timeout: Optional[float] = None -_stop_timeout: Optional[float] = None - - -def pytest_configure(config: "_pytest.config.Config"): - global _start_timeout, _stop_timeout - _start_timeout = config.getoption("--localstack-start-timeout") - _stop_timeout = config.getoption("--localstack-stop-timeout") +_start_timeout: float +_stop_timeout: float +_image: str +_pull: bool def pytest_addoption(parser: "_pytest.config.argparsing.Parser"): """Hook to add pytest_localstack command line options to pytest.""" group = parser.getgroup("localstack") + group.addoption( + "--localstack-image", + action="store", + type=str, + default="localstack/localstack:latest", + help="The Localstack Docker image to use for mocking AWS in tests.", + ) + group.addoption( + "--localstack-pull-image", + action="store_true", + help="If set, always pull the latest Localstack Docker image.", + ) group.addoption( "--localstack-start-timeout", action="store", type=float, default=60, - help="max seconds for starting a localstack container", + help="The default max seconds for starting a localstack container.", ) group.addoption( "--localstack-stop-timeout", action="store", type=float, - default=5, - help="max seconds for stopping a localstack container", + default=10, + help="The default max seconds for stopping a localstack container.", ) -def patch_fixture( - scope: str = "function", - services: Optional[Union[List[str], Dict[str, int]]] = None, - autouse: bool = False, - docker_client: Optional[docker.DockerClient] = None, - region_name: Optional[str] = None, - kinesis_error_probability: float = 0.0, - dynamodb_error_probability: float = 0.0, - container_log_level: int = logging.DEBUG, - localstack_version: str = "latest", - auto_remove: bool = True, - pull_image: bool = True, - container_name: Optional[str] = None, - **kwargs, -): - """Create a pytest fixture that temporarily redirects all botocore - sessions and clients to a Localstack container. - - This is not a fixture! It is a factory to create them. - - The fixtures that are created by this function will run a Localstack - container and patch botocore to direct traffic there for the duration - of the tests. - - Since boto3 uses botocore to send requests, boto3 will also be redirected. - - Args: - scope (str, optional): The pytest scope which this fixture will use. - Defaults to :const:`"function"`. - services (list, dict, optional): One of - - - A :class:`list` of AWS service names to start in the - Localstack container. - - A :class:`dict` of service names to the port they should run on. - - Defaults to all services. Setting this - can reduce container startup time and therefore test time. - autouse (bool, optional): If :obj:`True`, automatically use this - fixture in applicable tests. Default: :obj:`False` - docker_client (:class:`~docker.client.DockerClient`, optional): - Docker client to run the Localstack container with. - Defaults to :func:`docker.client.from_env`. - region_name (str, optional): Region name to assume. - Each Localstack container acts like a single AWS region. - Defaults to :const:`"us-east-1"`. - kinesis_error_probability (float, optional): Decimal value between - 0.0 (default) and 1.0 to randomly inject - ProvisionedThroughputExceededException errors - into Kinesis API responses. - dynamodb_error_probability (float, optional): Decimal value - between 0.0 (default) and 1.0 to randomly inject - ProvisionedThroughputExceededException errors into - DynamoDB API responses. - container_log_level (int, optional): The logging level to use - for Localstack container logs. Defaults to :data:`logging.DEBUG`. - localstack_version (str, optional): The version of the Localstack - image to use. Defaults to :const:`"latest"`. - auto_remove (bool, optional): If :obj:`True`, delete the Localstack - container when it stops. Default: :obj:`True` - pull_image (bool, optional): If :obj:`True`, pull the Localstack - image before running it. Default: :obj:`True` - container_name (str, optional): The name for the Localstack - container. Defaults to a randomly generated id. - **kwargs: Additional kwargs will be passed to the - :class:`.LocalstackSession`. - - Returns: - A :func:`pytest fixture <_pytest.fixtures.fixture>`. - - """ - - @pytest.fixture(scope=scope, autouse=autouse) # type: ignore - def _fixture(pytestconfig): - if not pytestconfig.pluginmanager.hasplugin("localstack"): - pytest.skip("skipping because localstack plugin isn't loaded") - with _make_session( - docker_client=docker_client, - services=services, - region_name=region_name, - kinesis_error_probability=kinesis_error_probability, - dynamodb_error_probability=dynamodb_error_probability, - container_log_level=container_log_level, - localstack_version=localstack_version, - auto_remove=auto_remove, - pull_image=pull_image, - container_name=container_name, - **kwargs, - ) as session: - with session.botocore.patch_botocore(): - yield session - - return _fixture - - -def session_fixture( - scope: str = "function", - services: Optional[Union[List[str], Dict[str, int]]] = None, - autouse: bool = False, - docker_client: Optional[docker.DockerClient] = None, - region_name: Optional[str] = None, - kinesis_error_probability: float = 0.0, - dynamodb_error_probability: float = 0.0, - container_log_level: int = logging.DEBUG, - localstack_version: str = "latest", - auto_remove: bool = True, - pull_image: bool = True, - container_name: Optional[str] = None, - **kwargs, -): - """Create a pytest fixture that provides a LocalstackSession. - - This is not a fixture! It is a factory to create them. - - The fixtures that are created by this function will yield - a :class:`.LocalstackSession` instance. - This is useful for simulating multiple AWS accounts. - It does not automatically redirect botocore/boto3 traffic to Localstack - (although :class:`.LocalstackSession` has a method to do that.) - - Args: - scope (str, optional): The pytest scope which this fixture will use. - Defaults to :const:`"function"`. - services (list, optional): One of: - - - A :class:`list` of AWS service names to start in the - Localstack container. - - A :class:`dict` of service names to the port they should run on. - - Defaults to all services. Setting this can reduce container - startup time and therefore test time. - autouse (bool, optional): If :obj:`True`, automatically use this - fixture in applicable tests. Default: :obj:`False` - docker_client (:class:`~docker.client.DockerClient`, optional): - Docker client to run the Localstack container with. - Defaults to :func:`docker.client.from_env`. - region_name (str, optional): Region name to assume. - Each Localstack container acts like a single AWS region. - Defaults to :const:`"us-east-1"`. - kinesis_error_probability (float, optional): Decimal value between - 0.0 (default) and 1.0 to randomly inject - ProvisionedThroughputExceededException errors - into Kinesis API responses. - dynamodb_error_probability (float, optional): Decimal value - between 0.0 (default) and 1.0 to randomly inject - ProvisionedThroughputExceededException errors into - DynamoDB API responses. - container_log_level (int, optional): The logging level to use - for Localstack container logs. Defaults to :data:`logging.DEBUG`. - localstack_version (str, optional): The version of the Localstack - image to use. Defaults to :const:`"latest"`. - auto_remove (bool, optional): If :obj:`True`, delete the Localstack - container when it stops. Default: :obj:`True` - pull_image (bool, optional): If :obj:`True`, pull the Localstack - image before running it. Default: :obj:`True`. - container_name (str, optional): The name for the Localstack - container. Defaults to a randomly generated id. - **kwargs: Additional kwargs will be passed to the - :class:`.LocalstackSession`. - - Returns: - A :func:`pytest fixture <_pytest.fixtures.fixture>`. - - """ - - @pytest.fixture(scope=scope, autouse=autouse) # type: ignore - def _fixture(pytestconfig: "_pytest.config.Config"): - if not pytestconfig.pluginmanager.hasplugin("localstack"): - pytest.skip("skipping because localstack plugin isn't loaded") - with _make_session( - docker_client=docker_client, - services=services, - region_name=region_name, - kinesis_error_probability=kinesis_error_probability, - dynamodb_error_probability=dynamodb_error_probability, - container_log_level=container_log_level, - localstack_version=localstack_version, - auto_remove=auto_remove, - pull_image=pull_image, - container_name=container_name, - **kwargs, - ) as session: - yield session - - return _fixture +def pytest_configure(config: "_pytest.config.Config"): + global _start_timeout, _stop_timeout, _image, _pull + _start_timeout = config.getoption("--localstack-start-timeout") + _stop_timeout = config.getoption("--localstack-stop-timeout") + _image = config.getoption("--localstack-image") + _pull = config.getoption("--localstack-pull-image") -@contextlib.contextmanager -def _make_session(docker_client: Optional[docker.DockerClient], *args, **kwargs): - utils.check_proxy_env_vars() +@pytest.fixture(scope="session") +def docker_client(): + return docker.from_env() - if docker_client is None: - docker_client = docker.from_env() +@pytest.fixture(scope="session") +def localstack_image(docker_client: docker.DockerClient): + image = None try: - docker_client.ping() # Check connectivity - except docker.errors.APIError: - pytest.fail("Could not connect to Docker.") + image = docker_client.images.get(_image) + except docker.errors.ImageNotFound: + pass + if _pull or image is None: + image = docker_client.images.pull(*(_image.split(":", 1))) + return image - _session = session.LocalstackSession(docker_client, *args, **kwargs) - _session.start(timeout=_start_timeout) - try: - yield _session - finally: - _session.stop(timeout=_stop_timeout) +@pytest.fixture(scope="session", autouse=True) +def patch_aws_env_vars(): + with patch.aws_credential_env_vars(): + yield -# Register contrib modules -plugin.register_plugin_module("pytest_localstack.contrib.boto3", False) +def patch_aws_clients_fixture( + endpoint_url: str, + scope: str = "session", + autouse: bool = False, + verify_ssl: Union[bool, str] = False, +) -> pytest.fixture: + @pytest.fixture(scope=scope, autouse=autouse) # type: ignore + def _fixture(): + with patch.aws_clients(endpoint_url, verify_ssl): + yield + + return _fixture -# Register 3rd-party modules -plugin.manager.load_setuptools_entrypoints("localstack") -# Trigger pytest_localstack_contribute_to_module hook -plugin.manager.hook.contribute_to_module.call_historic( - kwargs={"pytest_localstack": sys.modules[__name__]} -) +# def patch_fixture( +# scope: str = "function", +# autouse: bool = False, +# image: str = "localstack/localstack:latest", +# container: Optional[str] = None, +# auto_remove: bool = True, +# pull_image: bool = True, +# container_name: Optional[str] = None, +# docker_client: Optional[docker.DockerClient] = None, +# container_log_level: int = logging.DEBUG, +# **kwargs, +# ): +# """Create a pytest fixture that temporarily redirects all botocore +# sessions and clients to a Localstack container. + +# This is not a fixture! It is a factory to create them. + +# The fixtures that are created by this function will run a Localstack +# container and patch botocore to direct traffic there for the duration +# of the tests. + +# Since boto3 uses botocore to send requests, boto3 will also be redirected. + +# Args: +# scope (str, optional): The pytest scope which this fixture will use. +# Defaults to :const:`"function"`. +# services (list, dict, optional): One of + +# - A :class:`list` of AWS service names to start in the +# Localstack container. +# - A :class:`dict` of service names to the port they should run on. + +# Defaults to all services. Setting this +# can reduce container startup time and therefore test time. +# autouse (bool, optional): If :obj:`True`, automatically use this +# fixture in applicable tests. Default: :obj:`False` +# docker_client (:class:`~docker.client.DockerClient`, optional): +# Docker client to run the Localstack container with. +# Defaults to :func:`docker.client.from_env`. +# region_name (str, optional): Region name to assume. +# Each Localstack container acts like a single AWS region. +# Defaults to :const:`"us-east-1"`. +# kinesis_error_probability (float, optional): Decimal value between +# 0.0 (default) and 1.0 to randomly inject +# ProvisionedThroughputExceededException errors +# into Kinesis API responses. +# dynamodb_error_probability (float, optional): Decimal value +# between 0.0 (default) and 1.0 to randomly inject +# ProvisionedThroughputExceededException errors into +# DynamoDB API responses. +# container_log_level (int, optional): The logging level to use +# for Localstack container logs. Defaults to :data:`logging.DEBUG`. +# localstack_version (str, optional): The version of the Localstack +# image to use. Defaults to :const:`"latest"`. +# auto_remove (bool, optional): If :obj:`True`, delete the Localstack +# container when it stops. Default: :obj:`True` +# pull_image (bool, optional): If :obj:`True`, pull the Localstack +# image before running it. Default: :obj:`True` +# container_name (str, optional): The name for the Localstack +# container. Defaults to a randomly generated id. +# **kwargs: Additional kwargs will be passed to the +# :class:`.LocalstackSession`. + +# Returns: +# A :func:`pytest fixture <_pytest.fixtures.fixture>`. + +# """ + +# @pytest.fixture(scope=scope, autouse=autouse) # type: ignore +# def _fixture(pytestconfig): +# if not pytestconfig.pluginmanager.hasplugin("localstack"): +# pytest.skip("skipping because localstack plugin isn't loaded") +# with _make_session( +# docker_client=docker_client, +# services=services, +# region_name=region_name, +# kinesis_error_probability=kinesis_error_probability, +# dynamodb_error_probability=dynamodb_error_probability, +# container_log_level=container_log_level, +# localstack_version=localstack_version, +# auto_remove=auto_remove, +# pull_image=pull_image, +# container_name=container_name, +# **kwargs, +# ) as session: +# with session.botocore.patch_botocore(): +# yield session + +# return _fixture + + +# def session_fixture( +# scope: str = "function", +# services: Optional[Union[List[str], Dict[str, int]]] = None, +# autouse: bool = False, +# docker_client: Optional[docker.DockerClient] = None, +# region_name: Optional[str] = None, +# kinesis_error_probability: float = 0.0, +# dynamodb_error_probability: float = 0.0, +# container_log_level: int = logging.DEBUG, +# localstack_version: str = "latest", +# auto_remove: bool = True, +# pull_image: bool = True, +# container_name: Optional[str] = None, +# **kwargs, +# ): +# """Create a pytest fixture that provides a LocalstackSession. + +# This is not a fixture! It is a factory to create them. + +# The fixtures that are created by this function will yield +# a :class:`.LocalstackSession` instance. +# This is useful for simulating multiple AWS accounts. +# It does not automatically redirect botocore/boto3 traffic to Localstack +# (although :class:`.LocalstackSession` has a method to do that.) + +# Args: +# scope (str, optional): The pytest scope which this fixture will use. +# Defaults to :const:`"function"`. +# services (list, optional): One of: + +# - A :class:`list` of AWS service names to start in the +# Localstack container. +# - A :class:`dict` of service names to the port they should run on. + +# Defaults to all services. Setting this can reduce container +# startup time and therefore test time. +# autouse (bool, optional): If :obj:`True`, automatically use this +# fixture in applicable tests. Default: :obj:`False` +# docker_client (:class:`~docker.client.DockerClient`, optional): +# Docker client to run the Localstack container with. +# Defaults to :func:`docker.client.from_env`. +# region_name (str, optional): Region name to assume. +# Each Localstack container acts like a single AWS region. +# Defaults to :const:`"us-east-1"`. +# kinesis_error_probability (float, optional): Decimal value between +# 0.0 (default) and 1.0 to randomly inject +# ProvisionedThroughputExceededException errors +# into Kinesis API responses. +# dynamodb_error_probability (float, optional): Decimal value +# between 0.0 (default) and 1.0 to randomly inject +# ProvisionedThroughputExceededException errors into +# DynamoDB API responses. +# container_log_level (int, optional): The logging level to use +# for Localstack container logs. Defaults to :data:`logging.DEBUG`. +# localstack_version (str, optional): The version of the Localstack +# image to use. Defaults to :const:`"latest"`. +# auto_remove (bool, optional): If :obj:`True`, delete the Localstack +# container when it stops. Default: :obj:`True` +# pull_image (bool, optional): If :obj:`True`, pull the Localstack +# image before running it. Default: :obj:`True`. +# container_name (str, optional): The name for the Localstack +# container. Defaults to a randomly generated id. +# **kwargs: Additional kwargs will be passed to the +# :class:`.LocalstackSession`. + +# Returns: +# A :func:`pytest fixture <_pytest.fixtures.fixture>`. + +# """ + +# @pytest.fixture(scope=scope, autouse=autouse) # type: ignore +# def _fixture(pytestconfig: "_pytest.config.Config"): +# if not pytestconfig.pluginmanager.hasplugin("localstack"): +# pytest.skip("skipping because localstack plugin isn't loaded") +# with _make_session( +# docker_client=docker_client, +# services=services, +# region_name=region_name, +# kinesis_error_probability=kinesis_error_probability, +# dynamodb_error_probability=dynamodb_error_probability, +# container_log_level=container_log_level, +# localstack_version=localstack_version, +# auto_remove=auto_remove, +# pull_image=pull_image, +# container_name=container_name, +# **kwargs, +# ) as session: +# yield session + +# return _fixture + + +# @contextlib.contextmanager +# def _make_session(docker_client: Optional[docker.DockerClient], *args, **kwargs): +# utils.check_proxy_env_vars() + +# if docker_client is None: +# docker_client = docker.from_env() + +# try: +# docker_client.ping() # Check connectivity +# except docker.errors.APIError: +# pytest.fail("Could not connect to Docker.") + +# _session = session.LocalstackSession(docker_client, *args, **kwargs) + +# _session.start(timeout=_start_timeout) +# try: +# yield _session +# finally: +# _session.stop(timeout=_stop_timeout) diff --git a/pytest_localstack/botocore.py b/pytest_localstack/botocore.py deleted file mode 100644 index 5485fd0..0000000 --- a/pytest_localstack/botocore.py +++ /dev/null @@ -1,447 +0,0 @@ -"""Test resource factory for the botocore library.""" -import contextlib -import functools -import inspect -import logging -import socket -import weakref -from typing import TYPE_CHECKING -from unittest import mock - -import botocore -import botocore.client -import botocore.config -import botocore.credentials -import botocore.regions -import botocore.session - -from pytest_localstack import constants, exceptions, utils - - -if TYPE_CHECKING: - from pytest_localstack.session import RunningSession - - -try: - import boto3 -except ImportError: - boto3 = None - -logger = logging.getLogger(__name__) - - -class BotocoreTestResourceFactory: - """Create botocore clients to interact with a :class:`.LocalstackSession`. - - Args: - localstack_session (:class:`.LocalstackSession`): - The session that this factory should create test resources for. - - """ - - def __init__(self, localstack_session): - logger.debug("BotocoreTestResourceFactory.__init__") - self.localstack_session = localstack_session - self._default_session = None - - def session(self, *args, **kwargs): - """Create a botocore Session that will use Localstack. - - Arguments are the same as :class:`botocore.session.Session`. - """ - return Session(self.localstack_session, *args, **kwargs) - - def client(self, service_name, *args, **kwargs): - """Create a botocore client that will use Localstack. - - Arguments are the same as - :meth:`botocore.session.Session.create_client`. - """ - return self.default_session.create_client(service_name, *args, **kwargs) - - @property - def default_session(self): - """Return a default botocore Localstack Session. - - Most applications only need one Session. - """ - if self._default_session is None: - self._default_session = self.session() - return self._default_session - - @contextlib.contextmanager - def patch_botocore(self): - """Context manager that will patch botocore to use Localstack. - - Since boto3 relies on botocore to perform API calls, this method - also effectively patches boto3. - """ - # Q: Why is this method so complicated? - # A: Because the most common usecase is something like this:: - # - # >>> import boto3 - # >>> - # >>> S3 = boto3.resource('s3') - # >>> - # >>> def do_stuff(): - # >>> bucket = S3.Bucket('foobar') - # >>> bucket.create() - # ... - # - # The `S3` resource creates a botocore Client when the module - # is loaded. It's hard to patch existing Client instances since - # there isn't a good way to find them. - # You must add a descriptor to the Client class - # that overrides specific properties of the Client instances. - # TODO: Could we use use `gc.get_referrers()` to find instances? - logger.debug("enter patch") - if boto3 is not None: - preexisting_boto3_session = boto3.DEFAULT_SESSION - - try: - factory = self - patches = [] - - # Step 1: patch botocore Session to use Localstack. - attr = {} - - @property - def localstack_session(self): - # Simlate the 'localstack_session' attr from Session class below. - # Patch this into the botocore Session class. - if "localstack_session" in self.__dict__: - # We're patching this into the base botocore Session, - # but we don't want to override things for the Session - # subclass below. - return self.__dict__["localstack_session"] - return factory.localstack_session - - @localstack_session.setter - def localstack_session(self, value): - if not isinstance(value, RunningSession): - raise TypeError( - f"localstack_session value is type {value.__class__.__name__}, must be a LocalstackSession" - ) - self.__dict__["localstack_session"] = value - - attr["localstack_session"] = localstack_session - - @property - def _components(self): - if isinstance(self, Session): - try: - return self.__dict__["_components"] - except KeyError: - raise AttributeError("_components") - proxy_components = botocore.session.Session._proxy_components - if self not in proxy_components: - proxy_components[self] = botocore.session.ComponentLocator() - self._register_components() - return proxy_components[self] - - @_components.setter - def _components(self, value): - self.__dict__["_components"] = value - - @property - def _internal_components(self): - if isinstance(self, Session): - try: - return self.__dict__["_internal_components"] - except KeyError: - raise AttributeError("_internal_components") - proxy_components = botocore.session.Session._proxy_components - if self not in proxy_components: - proxy_components[self] = DebugComponentLocator() # noqa - self._register_components() - return proxy_components[self] - - @_internal_components.setter - def _internal_components(self, value): - self.__dict__["_internal_components"] = value - - attr.update( - { - "_components": _components, - "_internal_components": _internal_components, - "_proxy_components": weakref.WeakKeyDictionary(), - } - ) - - @property - def _credentials(self): - return self._proxy_credentials.get(self) - - @_credentials.setter - def _credentials(self, value): - self._proxy_credentials[self] = value - - attr.update( - { - "_credentials": _credentials, - "_proxy_credentials": weakref.WeakKeyDictionary(), - } - ) - - patches.append( - mock.patch.multiple("botocore.session.Session", create=True, **attr) - ) - patches.append( - mock.patch.multiple( - botocore.session.Session, - _register_endpoint_resolver=utils.unbind( - Session._register_endpoint_resolver - ), - _register_credential_provider=utils.unbind( - Session._register_credential_provider - ), - create_client=utils.unbind(Session.create_client), - ) - ) - - # Step 2: Safety checks - # Make absolutly sure we use Localstack and not AWS. - _original_convert_to_request_dict = ( - botocore.client.BaseClient._convert_to_request_dict - ) - - @functools.wraps(_original_convert_to_request_dict) - def _convert_to_request_dict(self, *args, **kwargs): - request_dict = _original_convert_to_request_dict(self, *args, **kwargs) - if not ( - factory.localstack_session.hostname in request_dict["url"] - or socket.gethostname() in request_dict["url"] - ): - # The URL of the request points to something other than localstack. - raise Exception("request dict is not patched") - return request_dict - - patches.append( - mock.patch( - "botocore.client.BaseClient._convert_to_request_dict", - _convert_to_request_dict, - ) - ) - - # Step 3: Patch existing clients - # Patching botocore Session doesn't help with an existing - # botocore Clients objects. They will have already been created with - # endpoints aimed at AWS. We need to patch botocore.client.BaseClient - # to temporarially act like a Localstack. - original_init = botocore.client.BaseClient.__init__ - - @functools.wraps(original_init) - def new_init(self, *args, **kwargs): - # Every client created during the patch is a Localstack client. - # Set this flag so that the proxy_client_attr() stuff below - # won't break during original_init(). - self._is_pytest_localstack = True - original_init(self, *args, **kwargs) - - patches.append( - mock.patch.multiple(botocore.client.BaseClient, __init__=new_init) - ) - - # Create a place to store proxy clients. - patches.append( - mock.patch( - "botocore.client.BaseClient._proxy_clients", - weakref.WeakKeyDictionary(), - create=True, - ) - ) - - def new_getattribute(self, key): - if key.startswith("__"): - return object.__getattribute__(self, key) - proxied_keys = [ - "_cache", - "_client_config", - "_endpoint", - "_exceptions_factory", - "_exceptions", - "exceptions", - "_loader", - "_request_signer", - "_response_parser", - "_serializer", - "meta", - ] - __dict__ = object.__getattribute__(self, "__dict__") - if ( - __dict__.get("_is_pytest_localstack", False) - or key not in proxied_keys - ): - # Don't proxy clients that are already Localstack clients - return object.__getattribute__(self, key) - if self not in botocore.client.BaseClient._proxy_clients: - try: - meta = __dict__["meta"] - except KeyError: - raise AttributeError("meta") - proxy = factory.default_session.create_client( - meta.service_model.service_name, - # config=config, - config=__dict__["_client_config"], - ) - botocore.client.BaseClient._proxy_clients[self] = proxy - return object.__getattribute__( - botocore.client.BaseClient._proxy_clients[self], key - ) - - patches.append( - mock.patch( - "botocore.client.BaseClient.__getattribute__", - new_getattribute, - create=True, - ) - ) - - # STS is sneaky and even after patching the endpoint it has a final custom check - # to see whether it should override with the global endpoint url... patch that too - - patches.append( - mock.patch( - "botocore.args.ClientArgsCreator._should_set_global_sts_endpoint", - lambda *args, **kwargs: False, - ) - ) - - with utils.nested(*patches): - yield - finally: - logger.debug("exit patch") - if boto3 is not None: - boto3.DEFAULT_SESSION = preexisting_boto3_session - - -# Grab a reference here to avoid breaking things during patching. -_original_create_client = utils.unbind(botocore.session.Session.create_client) - - -class Session(botocore.session.Session): - """A botocore Session subclass that talks to Localstack.""" - - def __init__(self, localstack_session, *args, **kwargs): - self.localstack_session = localstack_session - super().__init__(*args, **kwargs) - - def _register_endpoint_resolver(self): - def create_default_resolver(): - loader = self.get_component("data_loader") - endpoints = loader.load_data("endpoints") - return LocalstackEndpointResolver(self.localstack_session, endpoints) - - if constants.BOTOCORE_VERSION >= (1, 10, 58): - self._internal_components.lazy_register_component( - "endpoint_resolver", create_default_resolver - ) - else: - self._components.lazy_register_component( - "endpoint_resolver", create_default_resolver - ) - - def _register_credential_provider(self): - self._components.lazy_register_component( - "credential_provider", create_credential_resolver - ) - - def create_client(self, *args, **kwargs): - """Create a botocore client.""" - # Localstack doesn't use the virtual host addressing style. - config = botocore.config.Config(s3={"addressing_style": "path"}) - callargs = inspect.getcallargs(_original_create_client, self, *args, **kwargs) - if callargs.get("config"): - config = callargs["config"].merge(config) - callargs["config"] = config - with mock.patch( - "botocore.args.ClientArgsCreator._should_set_global_sts_endpoint", - lambda *args, **kwargs: False, - ): - client = _original_create_client(**callargs) - client._is_pytest_localstack = True - return client - - -def create_credential_resolver(): - """Create a credentials resolver for Localstack.""" - env_provider = botocore.credentials.EnvProvider() - default = DefaultCredentialProvider() - resolver = botocore.credentials.CredentialResolver( - providers=[env_provider, default] - ) - return resolver - - -class DefaultCredentialProvider(botocore.credentials.CredentialProvider): - """Provide some default credentials for Localstack clients.""" - - METHOD = "localstack-default" - - def load(self): - """Return credentials.""" - return botocore.credentials.Credentials( - access_key=constants.DEFAULT_AWS_ACCESS_KEY_ID, - secret_key=constants.DEFAULT_AWS_SECRET_ACCESS_KEY, - token=constants.DEFAULT_AWS_SESSION_TOKEN, - method=self.METHOD, - ) - - -class LocalstackEndpointResolver(botocore.regions.EndpointResolver): - """Resolve AWS service endpoints based on a LocalstackSession.""" - - def __init__(self, localstack_session, endpoints): - self.localstack_session = localstack_session - super().__init__(endpoints) - - @property - def valid_regions(self): - """Return a list of regions we can resolve endpoints for.""" - return set([self.localstack_session.region_name, "aws-global"]) - - def get_available_partitions(self): - """List the partitions available to the endpoint resolver.""" - return ["aws"] - - def get_available_endpoints( - self, service_name, partition_name="aws", allow_non_regional=False - ): - """List the endpoint names of a particular partition.""" - if partition_name != "aws": - raise exceptions.UnsupportedPartitionError(partition_name) - result = [] - for partition in self._endpoint_data["partitions"]: - if partition["partition"] != "aws": - continue - services = partition["services"] - if service_name not in services: - continue - for endpoint_name in services[service_name]["endpoints"]: - if allow_non_regional or endpoint_name in self.valid_regions: - result.append(endpoint_name) - return result - - def construct_endpoint(self, service_name, region_name=None): - """Resolve an endpoint for a service and region combination.""" - if region_name is None: - region_name = self.localstack_session.region_name - elif region_name not in self.valid_regions: - raise exceptions.RegionError( - region_name, self.localstack_session.region_name - ) - for partition in self._endpoint_data["partitions"]: - if partition["partition"] != "aws": - continue - result = self._endpoint_for_partition(partition, service_name, region_name) - if result: - result["hostname"] = self.localstack_session.service_hostname( - service_name - ) - result["protocols"] = ( - result["protocols"] if self.localstack_session.use_ssl else ["http"] - ) - if not self.localstack_session.use_ssl: - result.pop("sslCommonName", None) - result["dnsSuffix"] = self.localstack_session.hostname - return result diff --git a/pytest_localstack/constants.py b/pytest_localstack/constants.py index 3a8f175..7b22973 100644 --- a/pytest_localstack/constants.py +++ b/pytest_localstack/constants.py @@ -1,12 +1,17 @@ """pytest-localstack constants.""" -import botocore +# The container port Localstack accepts traffic on. +EDGE_PORT = 4566 -from pytest_localstack import utils +# The container port the Elasticsearch service runs on. +ELASTICSEARCH_PORT = 4571 +# A Localstack container port that exposes debugpy if DEVELOP=true. +# https://github.com/microsoft/debugpy +DEVELOP_PORT = 5678 -# IP for localhost -LOCALHOST = "127.0.0.1" +# The container port the Localstack web UI is exposed at. +WEB_UI_PORT = 8080 # The default AWS region. DEFAULT_AWS_REGION = "us-east-1" @@ -20,34 +25,8 @@ # The default AWS session token. DEFAULT_AWS_SESSION_TOKEN = "token" # nosec -# Mapping AWS service name to default Localstack port. -LEGACY_SERVICE_PORTS = { - "apigateway": 4567, - "cloudformation": 4581, - "cloudwatch": 4582, - "dynamodb": 4569, - "dynamodbstreams": 4570, - "ec2": 4597, - "es": 4578, - "events": 4587, - "firehose": 4573, - "iam": 4593, - "kinesis": 4568, - "lambda": 4574, - "logs": 4586, - "redshift": 4577, - "route53": 4580, - "s3": 4572, - "secretsmanager": 4584, - "ses": 4579, - "sns": 4575, - "sqs": 4576, - "ssm": 4583, - "stepfunctions": 4585, - "sts": 4592, -} - -SERVICE_PORTS = {k: 4566 for k in LEGACY_SERVICE_PORTS} +# The default Localstack Docker image to run. +DEFAULT_IMAGE = "localstack/localstack:latest" # AWS uses multiple names for some services. Map alias to service name. SERVICE_ALIASES = { @@ -56,8 +35,3 @@ "states": "stepfunctions", "streams.dynamodb": "dynamodbstreams", } - -DEFAULT_CONTAINER_START_TIMEOUT = 60 -DEFAULT_CONTAINER_STOP_TIMEOUT = 10 - -BOTOCORE_VERSION = utils.get_version_tuple(botocore.__version__) diff --git a/pytest_localstack/container.py b/pytest_localstack/container.py index 713595d..c94bee0 100644 --- a/pytest_localstack/container.py +++ b/pytest_localstack/container.py @@ -1,29 +1,308 @@ -"""Docker container tools.""" +import logging +import os import threading +from typing import Dict, Optional, Union -from pytest_localstack import utils +import docker +import requests +from docker.models.containers import Container +from docker.types import CancellableStream + +from pytest_localstack import constants, utils + + +LOGGER = logging.getLogger(__name__) + + +class LocalstackContainer: + """Manages a Docker container running Localstack.""" + + @classmethod + def start( + cls, + image: str = "localstack/localstack:latest", + docker_client: Optional[docker.DockerClient] = None, + auto_remove: bool = True, + environment: Optional[Dict[str, str]] = None, + container_name: Optional[str] = None, + ): + """Run a Localstack container. + + Args: + image (str, optional): The Localstack Docker image to run. + Defaults to "localstack/localstack:latest". + pull_image (bool, optional): If the image should be pulled to get the latest version + before being run. Defaults to True. + docker_client (docker.DockerClient, optional): The Docker API client. + If not specified, one is created with default settings. + Defaults to creating a new client from the Docker defaults. + auto_remove (bool, optional): If true, delete the Localstack container after it exits. + Defaults to True. + environment (Dict[str, str], optional): A dict of additional + configuration to pass as environment variables to the container. + See: https://github.com/localstack/localstack#configurations + Defaults to None. + container_name (str, optional): A name for the Localstack container. + Defaults to a randomly generate name like "pytest-localstack-abc123". + Returns: + LocalstackContainer: The running Localstack container. + """ + + utils.check_supported_localstack_image(image) + + if docker_client is None: + docker_client = docker.from_env() + + if container_name is None: + container_name = f"pytest-localstack-{utils.generate_random_string()}" + + if environment is not None and "AWS_DEFAULT_REGION" in os.environ: + # Honor the AWS_DEFAULT_REGION env var by running the + # Localstack container as the same region, unless + # the desired region was passed in explicitly. + environment.setdefault("DEFAULT_REGION", os.environ["AWS_DEFAULT_REGION"]) + + LOGGER.debug("Starting container '%s'", container_name) + container = docker_client.containers.run( + image, + name=container_name, + detach=True, + auto_remove=auto_remove, + environment=environment, + ports={ + f"{constants.EDGE_PORT}/tcp": None, # 4566 + f"{constants.ELASTICSEARCH_PORT}/tcp": None, # 4571 + f"{constants.WEB_UI_PORT}/tcp": None, # 8080 + f"{constants.DEVELOP_PORT}/tcp": None, # 5678 + }, + ) + return cls(container) + + @classmethod + def from_existing( + cls, container_id: str, docker_client: Optional[docker.DockerClient] = None + ): + """Return a LocalstackContainer for an already-running container. + + Args: + container_id (str): The ID or name of an already-running Localstack container. + docker_client (docker.DockerClient, optional): The Docker API client. + If not specified, one is created with default settings. + Defaults to creating a new client from the Docker defaults. + Returns: + LocalstackContainer: The running Localstack container. + """ + if docker_client is None: + docker_client = docker.from_env() + return LocalstackContainer(docker_client.containers.get(container_id)) + + def __init__(self, container: Container): + self.container = container + self._logger = LOGGER.getChild(f"containers.{self.container.name}") + + def is_ready(self) -> bool: + """Returns True if all Localstack services and features are ready.""" + try: + resp = requests.get(f"http://localhost:{self.get_edge_host_port()}/health") + except requests.ConnectionError: + self._logger.debug( + "Localstack not ready; healthcheck endpoint connection error." + ) + return False + body = resp.json() + + if "services" not in body: + self._logger.debug( + "Localstack not ready; service status isn't populated yet." + ) + return False + + if "features" not in body: + self._logger.debug( + "Localstack not ready; feature status isn't populated yet." + ) + return False + + if "initScripts" not in body["features"]: + self._logger.debug( + "Localstack not ready; initScripts status isn't populated yet." + ) + return False + + if "persistence" not in body["features"]: + self._logger.debug( + "Localstack not ready; persistence status isn't populated yet." + ) + return False + + for feature, status in body["features"].items(): + if status not in ("initialized", "disabled"): + self._logger.debug( + "Localstack not ready; feature '%s' is '%s', must be 'initialized' or 'disabled'.", + feature, + status, + ) + return False + + for service, status in body["services"].items(): + if status != "running": + self._logger.debug( + "Localstack not ready; service '%s' is '%s', not 'running'.", + service, + status, + ) + return False + + return True + + def wait_for_ready(self, timeout: float = 60, poll_interval: float = 0.25) -> None: + """Wait until all Localstack services and features are ready or timeout is reached. + + Args: + timeout (float, optional): [description]. Defaults to 60. + poll_interval (float, optional): [description]. Defaults to 0.25. + Raises: + TimeoutError: if the timeout is reached before Localstack is ready. + """ + done = threading.Event() + + def _poll(): + while not done.is_set(): + ready = self.is_ready() + if ready: + done.set() + return + done.wait(poll_interval) + + t = threading.Thread(target=_poll, daemon=True) + t.start() + try: + done.wait(timeout) + finally: + done.set() + + def stop(self, timeout: Optional[int] = 10): + """ + Stop the Localstack container. + + Will wait `timeout` seconds for the container to exit + gracefully before SIGKILLing it. + """ + self.container.stop(timeout=timeout) + + def tail_logs( + self, logger: logging.Logger = LOGGER, level: int = logging.INFO + ) -> None: + """Tail the container's logs, sending each line to `logger`.""" + container_logger = logger.getChild("containers.%s" % self.container.short_id) + + stdout_tailer = DockerLogTailer( + self.container, + container_logger.getChild("stdout"), + level, + stdout=True, + stderr=False, + ) + stdout_tailer.start() + + stderr_tailer = DockerLogTailer( + self.container, + container_logger.getChild("stderr"), + level, + stdout=False, + stderr=True, + ) + stderr_tailer.start() + + @property + def _docker_client(self) -> docker.DockerClient: + return self.container.client + + def get_environment(self) -> Dict[str, str]: + """Return a dict of the container environment variables.""" + env = {} + for env_pair in self.container.attrs["Config"]["Env"]: + key, value = env_pair.split("=", 1) + env[key] = value + return env + + def get_host_port(self, port: int) -> Optional[int]: + """Gets a host port number from a container port number.""" + result = self._docker_client.api.port(self.container.id, port) + if not result: + return None + return int(result[0]["HostPort"]) + + def get_edge_host_port(self) -> int: + """The port on the host machine where AWS services can be accessed.""" + return self.get_host_port(constants.EDGE_PORT) + + def get_elasticsearch_host_port(self) -> int: + """The port on the host machine where Elasticsearch can be accessed, if it's running.""" + return self.get_host_port(constants.ELASTICSEARCH_PORT) + + def get_web_ui_host_port(self) -> int: + """The port on the host machine where the Localstack web UI can be accessed.""" + return self.get_host_port(constants.WEB_UI_PORT) + + def get_develop_host_port(self) -> int: + """The port on the host machine where debugpy can be access, if the DEVELOP=true env var was set.""" + return self.get_host_port(constants.DEVELOP_PORT) + + def get_edge_url(self) -> str: + """Get a full URL to the Localstack edge endpoint. + This is the URL that would be use as the endpoint URL + of test AWS clients. + """ + host = "localhost" + port = self.get_edge_host_port() + if "USE_SSL" in self.get_environment(): + proto = "https" + else: + proto = "http" + return f"{proto}://{host}:{port}" class DockerLogTailer(threading.Thread): - """Write Docker container logs to a Python standard logger. + """Write Docker container logs to a Python logger, as a separate thread. + + If there's an exception that causes the thread to exit, it will + be stored as an `exception` attribute. Args: - container (:class:`docker.models.containers.Container`): - A container object returned by docker-py's - `run(detach=True)` method. - logger (:class:`logging.Logger`): A standard Python logger. - log_level (int): The log level to use. - stdout (bool, optional): Capture the containers stdout logs. - Default is True. - stderr (bool, optional): Capture the containers stderr logs. - Default is True. - encoding (str, optional): Read container logs bytes using - this encoding. Default is utf-8. Set to None to log raw bytes. + container: A container object returned by docker-py's `run(detach=True)` + method. + logger: A standard Python logger. + log_level: The log level to use. + stdout: Capture the containers stdout logs. Default is True. + stderr: Capture the containers stderr logs. Default is True. + encoding: Read container logs bytes using this encoding. Default is utf-8. Set + to None to log raw bytes. + tail: The number of line to to emit from the end of the existing container logs + when start() is called. Can be any int number of log lines or the string + `all` to emit all existing lines. """ + container: Container + logger: logging.Logger + log_level: int + stdout: bool + stderr: bool + encoding: str + tail: Union[int, str] + _logs_generator: Optional[CancellableStream] + exception: Optional[Exception] + def __init__( - self, container, logger, log_level, stdout=True, stderr=True, encoding="utf-8" + self, + container: Container, + logger: logging.Logger, + log_level: int, + stdout: bool = True, + stderr: bool = True, + encoding: str = "utf-8", + tail: Union[int, str] = 0, ): self.container = container self.logger = logger @@ -31,16 +310,20 @@ def __init__( self.stdout = stdout self.stderr = stderr self.encoding = encoding + self.tail = tail super().__init__() self.daemon = True def run(self): - """Tail the container logs as a separate thread.""" try: - logs_generator = self.container.logs( - stream=True, stdout=self.stdout, stderr=self.stderr + self._logs_generator = self.container.logs( + stream=True, + stdout=self.stdout, + stderr=self.stderr, + follow=True, + tail=self.tail, ) - for line in logs_generator: + for line in self._logs_generator: if self.encoding is not None and isinstance(line, bytes): line = line.decode(self.encoding) line = utils.remove_newline(line) @@ -48,3 +331,9 @@ def run(self): except Exception as e: self.exception = e raise + + def stop(self): + """Stop tailing the logs.""" + if self._logs_generator is None: + raise RuntimeError("DockerLogTailer isn't started yet") + self._logs_generator.close() diff --git a/pytest_localstack/contrib/__init__.py b/pytest_localstack/contrib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pytest_localstack/contrib/boto3.py b/pytest_localstack/contrib/boto3.py deleted file mode 100644 index 2493ada..0000000 --- a/pytest_localstack/contrib/boto3.py +++ /dev/null @@ -1,71 +0,0 @@ -"""pytest-localstack extensions for boto3.""" -import logging - -import boto3.session - -from pytest_localstack import constants, hookspecs - - -logger = logging.getLogger(__name__) - - -@hookspecs.pytest_localstack_hookimpl -def contribute_to_session(session): - """Add :class:`Boto3TestResourceFactory` to :class:`~.LocalstackSession`.""" - logger.debug("patching session %r", session) - session.boto3 = Boto3TestResourceFactory(session) - - -class Boto3TestResourceFactory: - """Create boto3 clients and resources to interact with a :class:`~.LocalstackSession`. - - Args: - localstack_session (:class:`.LocalstackSession`): - The session that this factory should create test resources for. - - """ - - def __init__(self, localstack_session): - logger.debug("Boto3TestResourceFactory.__init__") - self.localstack_session = localstack_session - self._default_session = None - - def session(self, *args, **kwargs): - """Return a boto3 Session object that will use localstack. - - Arguments are the same as :class:`boto3.session.Session`. - """ - kwargs["botocore_session"] = self.localstack_session.botocore.default_session - kwargs.setdefault("aws_access_key_id", constants.DEFAULT_AWS_ACCESS_KEY_ID) - kwargs.setdefault( - "aws_secret_access_key", constants.DEFAULT_AWS_SECRET_ACCESS_KEY - ) - kwargs.setdefault("aws_session_token", constants.DEFAULT_AWS_SESSION_TOKEN) - return boto3.session.Session(*args, **kwargs) - - @property - def default_session(self): - """Return a default boto3 Localstack Session. - - Most applications only need one Session. - """ - if self._default_session is None: - self._default_session = self.session() - return self._default_session - - def client(self, service_name): - """Return a patched boto3 Client object that will use localstack. - - Arguments are the same as :func:`boto3.client`. - """ - return self.default_session.client(service_name) - - def resource(self, service_name): - """Return a patched boto3 Resource object that will use localstack. - - Arguments are the same as :func:`boto3.resource`. - """ - return self.default_session.resource(service_name) - - # No need for a patch method. - # Running the botocore patch will also patch boto3. diff --git a/pytest_localstack/exceptions.py b/pytest_localstack/exceptions.py index 0bbbb10..9ef4a36 100644 --- a/pytest_localstack/exceptions.py +++ b/pytest_localstack/exceptions.py @@ -42,8 +42,16 @@ class UnsupportedPartitionError(Error): def __init__(self, partition_name): super().__init__( - "LocalstackEndpointResolver only supports the 'aws' partition, " - "not '%s'" % (partition_name,) + f"LocalstackEndpointResolver only supports the 'aws' partition, not '{partition_name}'" + ) + + +class UnsupportedLocalstackVersionError(Error): + """Raised when""" + + def __init__(self, image): + super().__init__( + f"Localstack Docker image '{image}' isn't supported (must be >= v0.11.6)" ) @@ -57,6 +65,5 @@ class RegionError(Error): def __init__(self, region_name, should_be_region): super().__init__( - "This LocalstackSession is configured for region %s, not %s" - % (should_be_region, region_name) + f"This LocalstackSession is configured for region {should_be_region}, not {region_name}" ) diff --git a/pytest_localstack/hookspecs.py b/pytest_localstack/hookspecs.py deleted file mode 100644 index 4d66d8f..0000000 --- a/pytest_localstack/hookspecs.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Much like `pytest `_, -itself, pytest-localstack uses `pluggy `_ -to implement a plugin system. These plugins can be used to add additional -functionality to pytest-localstack and to trigger callbacks when the -Localstack container is started and stopped. - -""" -import pluggy - - -pytest_localstack_hookspec = pluggy.HookspecMarker("pytest-localstack") -pytest_localstack_hookimpl = pluggy.HookimplMarker("pytest-localstack") - - -@pytest_localstack_hookspec(historic=True) -def contribute_to_module(pytest_localstack): - """ - Hook to add additional functionality to the :mod:`pytest_localstack` - module. - - Primarily used to add importable fixture factories at a top level. - """ - - -@pytest_localstack_hookspec -def contribute_to_session(session): - """Hook to add additional functionality to :class:`LocalstackSession`. - - Primarily used to add test resource factories to sessions. - See :mod:`pytest_localstack.contrib.botocore` for an example of that. - """ - - -@pytest_localstack_hookspec -def session_starting(session): - """Hook fired when :class:`LocalstackSession` is starting.""" - - -@pytest_localstack_hookspec -def session_started(session): - """Hook fired when :class:`LocalstackSession` has started.""" - - -@pytest_localstack_hookspec -def session_stopping(session): - """Hook fired when :class:`LocalstackSession` is stopping.""" - - -@pytest_localstack_hookspec -def session_stopped(session): - """Hook fired when :class:`LocalstackSession` has stopped.""" diff --git a/pytest_localstack/patch.py b/pytest_localstack/patch.py new file mode 100644 index 0000000..593ac66 --- /dev/null +++ b/pytest_localstack/patch.py @@ -0,0 +1,112 @@ +import contextlib +import functools +import logging +from typing import Union +from unittest import mock + +import botocore.endpoint +import botocore.session + +from pytest_localstack import constants + + +LOGGER = logging.getLogger(__name__) + + +@contextlib.contextmanager +def aws_clients(endpoint_url: str, verify: Union[bool, str] = False): + """ + A context manager function that will patch all botocore clients + created after the patch is applied (and consequently boto3 clients) + to always use the given endpoint URL. + + A patch will also be applied to botocore to raise an exception if it + attempts to send any requests to AWS instead of endpoint_url. + + Args: + endpoint_url (string): The URL that AWS clients should send requests to. + verify (bool, str): Whether or not to verify SSL certificates. + Defaults to False, which is not to verify. + Can also be set to a path toa CA cert bundle file to use + rather than the one botocore uses by default. + Both options are useful when running Localstack with + USE_SSL=true. This can be overridden on a per-client basis. + + Example: + + import boto3 + from pytest_localstack import patch + with patch.aws_clients("http://localstack:4566"): + s3 = boto3.client("s3") # <- This will send requests to http://localstack:4566. + + """ + original_create_client = botocore.session.Session.create_client + + @functools.wraps(original_create_client) + def create_client(self, *args, **kwargs): + optional_kwargs = { + "verify": verify, + } + required_kwargs = { + "endpoint_url": endpoint_url, + } + kwargs = {**optional_kwargs, **kwargs, **required_kwargs} + return original_create_client(self, *args, **kwargs) + + patch_clients = mock.patch( + "botocore.session.Session.create_client", new=create_client + ) + patch_clients.start() + + original_make_request = botocore.endpoint.Endpoint.make_request + + @functools.wraps(original_make_request) + def make_request(self, *args, **kwargs): + if not self.host.startswith(endpoint_url): + raise Exception() + return original_make_request(self, *args, **kwargs) + + patch_requests = mock.patch( + "botocore.endpoint.Endpoint.make_request", new=make_request + ) + patch_requests.start() + + patch_status = mock.patch.object(aws_clients, "active", new=True) + patch_status.start() + try: + yield + finally: + patch_clients.stop() + patch_requests.stop() + patch_status.stop() + + +aws_clients.active = False + + +@contextlib.contextmanager +def aws_credential_env_vars(): + """ + A context manager function that patches environment variables to set + AWS credential env vars to dummy values. + + This should be done at the beginning of any tests, before the code to be tested has + even been imported. That way there is no possibility of real calls being made to AWS. + """ + new_env = { + "AWS_ACCESS_KEY_ID": constants.DEFAULT_AWS_ACCESS_KEY_ID, + "AWS_SECRET_ACCESS_KEY": constants.DEFAULT_AWS_SECRET_ACCESS_KEY, + "AWS_SESSION_TOKEN": constants.DEFAULT_AWS_SESSION_TOKEN, + } + patch_env_vars = mock.patch.dict("os.environ", new_env) + patch_env_vars.start() + patch_status = mock.patch.object(aws_credential_env_vars, "active", new=True) + patch_status.start() + try: + yield + finally: + patch_env_vars.stop() + patch_status.stop() + + +aws_credential_env_vars.active = False diff --git a/pytest_localstack/plugin.py b/pytest_localstack/plugin.py deleted file mode 100644 index 8c23da9..0000000 --- a/pytest_localstack/plugin.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Plugins manager. - -.. seealso:: :mod:`~pytest_localstack.hookspecs` - -""" -import importlib - -import pluggy - -import pytest_localstack.hookspecs - - -manager = pluggy.PluginManager("pytest-localstack") -manager.add_hookspecs(pytest_localstack.hookspecs) - - -def register_plugin_module(module_path, required=True): - """Register hooks in a module with the PluginManager by Python path. - - Args: - module_path (str): A Python dotted import path. - required (bool, optional): If False, ignore ImportError. - Default: True. - - Returns: - The imported module. - - Raises: - ImportError: If `required` is True and the module cannot be imported. - - """ - try: - module = importlib.import_module(module_path) - except ImportError: - if required: - raise - else: - manager.register(module) - return module diff --git a/pytest_localstack/service_checks.py b/pytest_localstack/service_checks.py deleted file mode 100644 index 3eeacce..0000000 --- a/pytest_localstack/service_checks.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Checks to see if Localstack service is running. - -Each check takes a :class:`.LocalstackSession` and -raises :class:`~pytest_localstack.exceptions.ServiceError` -if the service is not available. -""" -import contextlib -import functools -import socket -import urllib.parse - -import botocore.config - -from pytest_localstack import constants, exceptions - - -def is_port_open(port_or_url, timeout=1): - """Check if TCP port is open.""" - if isinstance(port_or_url, (str, bytes)): - url = urllib.parse.urlparse(port_or_url) - port = url.port - host = url.hostname - else: - port = port_or_url - host = "127.0.0.1" - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - with contextlib.closing(sock): - sock.settimeout(timeout) - result = sock.connect_ex((host, port)) - return result == 0 - - -def port_check(service_name): - """Check that a service port is open.""" - - def _check(localstack_session): - url = localstack_session.endpoint_url(service_name) - if not is_port_open(url): - raise exceptions.ServiceError(service_name=service_name) - - return _check - - -def botocore_check(service_name, client_func_name): - """Decorator to check service via botocore Client. - - `client_func_name` should be the name of a harmless client - method to call that has no required arguements. - `list_*` methods are usually good candidates. - """ - - def _decorator(check_results_func): - @functools.wraps(check_results_func) - def _wrapped(localstack_session): - url = localstack_session.endpoint_url(service_name) - if not is_port_open(url): - raise exceptions.ServiceError(service_name=service_name) - config_kwargs = { - "connect_timeout": 1, - "read_timeout": 1, - "s3": {"addressing_style": "path"}, - } - if constants.BOTOCORE_VERSION >= (1, 6, 0): - config_kwargs["retries"] = {"max_attempts": 1} - client = localstack_session.botocore.client( - service_name, - # Handle retries at a higher level - config=botocore.config.Config(**config_kwargs), - ) - client_func = getattr(client, client_func_name) - try: - response = client_func() - check_results_func(response) - except Exception as e: - raise exceptions.ServiceError(service_name=service_name) from e - - return _wrapped - - return _decorator - - -def botocore_check_response_type( - service_name, client_func_name, expected_type, *response_keys -): - """Generate a service check function that tests that the response is a specific type. - - Optionally pass response_keys to check the type of something nested in a - response dict. - """ - - @botocore_check(service_name, client_func_name) - def _f(client_response): - for key in response_keys: - client_response = client_response[key] - if not isinstance(client_response, expected_type): - raise TypeError( - f"Client response type {client_response.__class__.__name__} is not a subtype of {expected_type.__name__}" - ) - - return _f - - -SERVICE_CHECKS = { - "events": port_check("events"), - "apigateway": port_check( - "apigateway" # moto doesn't implement a good apigateway endpoint for checks yet. - ), - "cloudformation": botocore_check_response_type( - "cloudformation", "list_stacks", list, "StackSummaries" - ), - "cloudwatch": botocore_check_response_type( - "cloudwatch", "list_dashboards", list, "DashboardEntries" - ), - "dynamodb": botocore_check_response_type( - "dynamodb", "list_tables", list, "TableNames" - ), - "dynamodbstreams": botocore_check_response_type( - "dynamodbstreams", "list_streams", list, "Streams" - ), - "ec2": botocore_check_response_type("ec2", "describe_regions", list, "Regions"), - "es": botocore_check_response_type("es", "list_domain_names", list, "DomainNames"), - "firehose": botocore_check_response_type( - "firehose", "list_delivery_streams", list, "DeliveryStreamNames" - ), - "iam": botocore_check_response_type("iam", "list_roles", list, "Roles"), - "kinesis": botocore_check_response_type( - "kinesis", "list_streams", list, "StreamNames" - ), - "lambda": botocore_check_response_type( - "lambda", "list_functions", list, "Functions" - ), - "logs": botocore_check_response_type( - "logs", "describe_log_groups", list, "logGroups" - ), - "redshift": botocore_check_response_type( - "redshift", "describe_clusters", list, "Clusters" - ), - "route53": port_check( - "route53" # moto doesn't implement a good route53 endpoint for checks yet. - ), - "s3": botocore_check_response_type("s3", "list_buckets", list, "Buckets"), - "secretsmanager": botocore_check_response_type( - "secretsmanager", "list_secrets", list, "SecretList" - ), - "ses": botocore_check_response_type("ses", "list_identities", list, "Identities"), - "sns": botocore_check_response_type("sns", "list_topics", list, "Topics"), - "sqs": botocore_check_response_type( - "sqs", "list_queues", dict # https://github.com/boto/boto3/issues/1813 - ), - "ssm": botocore_check_response_type( - "ssm", "describe_parameters", list, "Parameters" - ), - "stepfunctions": botocore_check_response_type( - "stepfunctions", "list_activities", list, "activities" - ), - "sts": port_check( - "sts" # moto doesn't implement a good sts endpoint for checks yet. - ), -} - -# All services should have a check. -assert set(SERVICE_CHECKS) == set(constants.SERVICE_PORTS) # nosec diff --git a/pytest_localstack/session.py b/pytest_localstack/session.py deleted file mode 100644 index 6c7f183..0000000 --- a/pytest_localstack/session.py +++ /dev/null @@ -1,410 +0,0 @@ -"""Run and interact with a Localstack container.""" -import logging -import os -import string -import time -from copy import copy - -from pytest_localstack import ( - constants, - container, - exceptions, - plugin, - service_checks, - utils, -) -from pytest_localstack.botocore import BotocoreTestResourceFactory - - -logger = logging.getLogger(__name__) - - -class RunningSession: - """Connects to an already running localstack server""" - - def __init__( - self, - hostname, - services=None, - region_name=None, - use_ssl=False, - localstack_version="latest", - **kwargs, - ): - - self.kwargs = kwargs - self.use_ssl = use_ssl - self.region_name = region_name - self._hostname = hostname - self.localstack_version = localstack_version - - if self.localstack_version != "latest" and utils.get_version_tuple( - localstack_version - ) < utils.get_version_tuple("0.11"): - self.service_ports = constants.LEGACY_SERVICE_PORTS - else: - self.service_ports = constants.SERVICE_PORTS - - self.botocore = BotocoreTestResourceFactory(self) - - plugin.manager.hook.contribute_to_session(session=self) - # If no region was provided, use what botocore defaulted to. - if not region_name: - self.region_name = ( - self.botocore.session().get_config_variable("region") - or constants.DEFAULT_AWS_REGION - ) - - if services is None: - self.services = copy(self.service_ports) - elif isinstance(services, (list, tuple, set)): - self.services = {} - for service_name in services: - try: - port = self.service_ports[service_name] - except KeyError: - raise exceptions.ServiceError("unknown service " + service_name) - self.services[service_name] = port - elif isinstance(services, dict): - self.services = {} - for service_name, port in services.items(): - if service_name not in self.service_ports: - raise exceptions.ServiceError("unknown service " + service_name) - if port is None: - port = self.service_ports[service_name] - self.services[service_name] = port - else: - raise TypeError("unsupported services type: %r" % (services,)) - - @property - def hostname(self): - """Return hostname of Localstack.""" - return self._hostname - - @property - def service_aliases(self): - """Return a full list of possible names supported.""" - services = set(self.services) - result = set() - for alias, service_name in constants.SERVICE_ALIASES.items(): - if service_name in services: - result.add(service_name) - result.add(alias) - return result - - def start(self, timeout=60): - """Starts Localstack if needed.""" - plugin.manager.hook.session_starting(session=self) - - self._check_services(timeout) - plugin.manager.hook.session_started(session=self) - - def _check_services(self, timeout, initial_retry_delay=0.01, max_delay=1): - """Check that all Localstack services are running and accessible. - - Does exponential backoff up to `max_delay`. - - Args: - timeout (float): Number of seconds to wait for services to - be available. - initial_retry_delay (float, optional): Initial retry delay value - in seconds. Will be multiplied by `2^n` for each retry. - Default: 0.01 - max_delay (float, optional): Max time in seconds to wait between - checking service availability. Default: 1 - - Returns: - None - - Raises: - pytest_localstack.exceptions.TimeoutError: If not all services - started before `timeout` was reached. - - """ - services = set(self.services) - num_retries = 0 - start_time = time.time() - while services and (time.time() - start_time) < timeout: - for service_name in list( - services - ): # list() because set may change during iteration - try: - service_checks.SERVICE_CHECKS[service_name](self) - services.discard(service_name) - except exceptions.ServiceError as e: - if (time.time() - start_time) >= timeout: - raise exceptions.TimeoutError( - f"Localstack service not started: {service_name}" - ) from e - if services: - delay = (2 ** num_retries) * initial_retry_delay - if delay > max_delay: - delay = max_delay - time.sleep(delay) - num_retries += 1 - - def stop(self, timeout=10): - """Stops Localstack.""" - plugin.manager.hook.session_stopping(session=self) - plugin.manager.hook.session_stopped(session=self) - - def __enter__( - self, - start_timeout=constants.DEFAULT_CONTAINER_START_TIMEOUT, - stop_timeout=constants.DEFAULT_CONTAINER_STOP_TIMEOUT, - ): - self.__stop_timeout = stop_timeout - self.start(timeout=start_timeout) - return self - - def __exit__(self, exc_type, exc, tb): - timeout = getattr( - self, "__stop_timeout", constants.DEFAULT_CONTAINER_STOP_TIMEOUT - ) - self.stop(timeout=timeout) - - def map_port(self, port): - """Return host port based on Localstack port.""" - return port - - def service_hostname(self, service_name): - """Get hostname and port for an AWS service.""" - service_name = constants.SERVICE_ALIASES.get(service_name, service_name) - if service_name not in self.services: - raise exceptions.ServiceError( - f"{self!r} does not have {service_name} enabled" - ) - port = self.map_port(self.services[service_name]) - return "%s:%i" % (self.hostname, port) - - def endpoint_url(self, service_name): - """Get the URL for a service endpoint.""" - url = ("https" if self.use_ssl else "http") + "://" - url += self.service_hostname(service_name) - return url - - -class LocalstackSession(RunningSession): - """Run a localstack Docker container. - - This class can start and stop a Localstack container, as well as capture - its logs. - - Can be used as a context manager: - - >>> import docker - >>> client = docker.from_env() - >>> with LocalstackSession(client) as session: - ... s3 = session.boto3.resource('s3') - - Args: - docker_client: A docker-py Client object that will be used - to talk to Docker. - services (list|dict, optional): One of - - - A list of AWS service names to start in the - Localstack container. - - A dict of service names to the port they should run on. - - Defaults to all services. Setting this - can reduce container startup time and therefore test time. - region_name (str, optional): Region name to assume. - Each Localstack container acts like a single AWS region. - Defaults to 'us-east-1'. - kinesis_error_probability (float, optional): Decimal value between - 0.0 (default) and 1.0 to randomly inject - ProvisionedThroughputExceededException errors - into Kinesis API responses. - dynamodb_error_probability (float, optional): Decimal value - between 0.0 (default) and 1.0 to randomly inject - ProvisionedThroughputExceededException errors into - DynamoDB API responses. - container_log_level (int, optional): The logging level to use - for Localstack container logs. Defaults to :attr:`logging.DEBUG`. - localstack_version (str, optional): The version of the Localstack - image to use. Defaults to `latest`. - auto_remove (bool, optional): If True, delete the Localstack - container when it stops. - container_name (str, optional): The name for the Localstack - container. Defaults to a randomly generated id. - use_ssl (bool, optional): If True use SSL to connect to Localstack. - Default is False. - **kwargs: Additional kwargs will be stored in a `kwargs` attribute - in case test resource factories want to access them. - - """ - - image_name = "localstack/localstack" - - def __init__( - self, - docker_client, - services=None, - region_name=None, - kinesis_error_probability=0.0, - dynamodb_error_probability=0.0, - container_log_level=logging.DEBUG, - localstack_version="latest", - auto_remove=True, - pull_image=True, - container_name=None, - use_ssl=False, - hostname=None, - **kwargs, - ): - self._container = None - self._factory_cache = {} - - self.docker_client = docker_client - self.region_name = region_name - self.kinesis_error_probability = kinesis_error_probability - self.dynamodb_error_probability = dynamodb_error_probability - self.auto_remove = bool(auto_remove) - self.pull_image = bool(pull_image) - - super().__init__( - hostname=hostname if hostname else constants.LOCALHOST, - services=services, - region_name=region_name, - use_ssl=use_ssl, - localstack_version=localstack_version, - **kwargs, - ) - - self.container_log_level = container_log_level - self.localstack_version = localstack_version - self.container_name = container_name or generate_container_name() - - def start(self, timeout=60): - """Start the Localstack container. - - Args: - timeout (float, optional): Wait at most this many seconds - for the Localstack services to start. Default is 1 minute. - - Raises: - pytest_localstack.exceptions.TimeoutError: - If *timeout* was reached before all Localstack - services were available. - docker.errors.APIError: If the Docker daemon returns an error. - - """ - if self._container is not None: - raise exceptions.ContainerAlreadyStartedError(self) - - logger.debug("Starting Localstack container %s", self.container_name) - logger.debug("%r running starting hooks", self) - plugin.manager.hook.session_starting(session=self) - - image_name = self.image_name + ":" + self.localstack_version - if self.pull_image: - logger.debug("Pulling docker image %r", image_name) - self.docker_client.images.pull(image_name) - - start_time = time.time() - - services = ",".join("%s:%s" % pair for pair in self.services.items()) - kinesis_error_probability = "%f" % self.kinesis_error_probability - dynamodb_error_probability = "%f" % self.dynamodb_error_probability - use_ssl = str(self.use_ssl).lower() - self._container = self.docker_client.containers.run( - image_name, - name=self.container_name, - detach=True, - auto_remove=self.auto_remove, - environment={ - "DEFAULT_REGION": self.region_name, - "SERVICES": services, - "KINESIS_ERROR_PROBABILITY": kinesis_error_probability, - "DYNAMODB_ERROR_PROBABILITY": dynamodb_error_probability, - "USE_SSL": use_ssl, - }, - ports={port: None for port in self.services.values()}, - ) - logger.debug( - "Started Localstack container %s (id: %s)", - self.container_name, - self._container.short_id, - ) - - # Tail container logs - container_logger = logger.getChild("containers.%s" % self._container.short_id) - self._stdout_tailer = container.DockerLogTailer( - self._container, - container_logger.getChild("stdout"), - self.container_log_level, - stdout=True, - stderr=False, - ) - self._stdout_tailer.start() - self._stderr_tailer = container.DockerLogTailer( - self._container, - container_logger.getChild("stderr"), - self.container_log_level, - stdout=False, - stderr=True, - ) - self._stderr_tailer.start() - - try: - timeout_remaining = timeout - (time.time() - start_time) - if timeout_remaining <= 0: - raise exceptions.TimeoutError("Container took too long to start.") - - self._check_services(timeout_remaining) - - logger.debug("%r running started hooks", self) - plugin.manager.hook.session_started(session=self) - logger.debug("%r finished started hooks", self) - except exceptions.TimeoutError: - if self._container is not None: - self.stop(0.1) - raise - - def stop(self, timeout=10): - """Stop the Localstack container. - - Args: - timeout (float, optional): Timeout in seconds to wait for the - container to stop before sending a SIGKILL. Default: 10 - - Raises: - docker.errors.APIError: If the Docker daemon returns an error. - - """ - if self._container is not None: - logger.debug("Stopping %r", self) - logger.debug("Running stopping hooks for %r", self) - plugin.manager.hook.session_stopping(session=self) - logger.debug("Finished stopping hooks for %r", self) - self._container.stop(timeout=10) - self._container = None - self._stdout_tailer = None - self._stderr_tailer = None - logger.debug("Stopped %r", self) - logger.debug("Running stopped hooks for %r", self) - plugin.manager.hook.session_stopped(session=self) - logger.debug("Finished stopped hooks for %r", self) - - def __del__(self): - """Stop container on garbage collection.""" - self.stop(0.1) - - def map_port(self, port): - """Return host port based on Localstack container port.""" - if self._container is None: - raise exceptions.ContainerNotStartedError(self) - result = self.docker_client.api.port(self._container.id, int(port)) - if not result: - return None - return int(result[0]["HostPort"]) - - -def generate_container_name(): - """Generate a random name for a Localstack container.""" - valid_chars = set(string.ascii_letters) - chars = [] - while len(chars) < 6: - new_chars = [chr(c) for c in os.urandom(6 - len(chars))] - chars += [c for c in new_chars if c in valid_chars] - return "pytest-localstack-" + "".join(chars) diff --git a/pytest_localstack/utils.py b/pytest_localstack/utils.py index f3fe87d..0e9e898 100644 --- a/pytest_localstack/utils.py +++ b/pytest_localstack/utils.py @@ -1,47 +1,10 @@ """Misc utilities.""" -import contextlib import os +import string import types -import urllib.request +from typing import Tuple - -def check_proxy_env_vars(): - """Raise warnings about improperly-set proxy environment variables.""" - proxy_settings = urllib.request.getproxies() - if "http" not in proxy_settings and "https" not in proxy_settings: - return - for var in ["http_proxy", "https_proxy", "no_proxy"]: - try: - if os.environ[var.lower()] != os.environ[var.upper()]: - raise UserWarning( - f"Your {var.lower()} and {var.upper()} environment variables are set to different values." - ) - except KeyError: - pass - if "no" not in proxy_settings: - raise UserWarning( - "You have proxy settings, but no_proxy isn't set. " - "If you try to connect to localhost (i.e. like pytest-localstack does) " - "it's going to try to go through the proxy and fail. " - "Set the no_proxy environment variable to something like " - "'localhost,127.0.0.1' (and maybe add your local network as well? ;D )" - ) - if "127.0.0.1" not in proxy_settings["no"]: - raise UserWarning( - "You have proxy settings (including no_proxy) set, " - "but no_proxy doens't contain '127.0.0.1'. " - "This is needed for Localstack. " - "Please set the no_proxy environment variable to something like " - "'localhost,127.0.0.1' (and maybe add your local network as well? ;D )" - ) - - -@contextlib.contextmanager -def nested(*mgrs): - """Combine multiple context managers.""" - with contextlib.ExitStack() as stack: - outputs = [stack.enter_context(cm) for cm in mgrs] - yield outputs +from pytest_localstack import exceptions def unbind(func): @@ -51,7 +14,7 @@ def unbind(func): return func -def remove_newline(string, n=1): +def remove_newline(string: str, n: int = 1) -> str: """Remove up to `n` trailing newlines from `string`.""" # Regex returns some weird results when dealing with only newlines, # so we do this manually. @@ -68,7 +31,7 @@ def remove_newline(string, n=1): return string -def get_version_tuple(version): +def get_version_tuple(version: str) -> Tuple[int]: """ Return a tuple of version numbers (e.g. (1, 2, 3)) from the version string (e.g. '1.2.3'). @@ -77,3 +40,32 @@ def get_version_tuple(version): version = version[1:] parts = version.split(".") return tuple(int(p) for p in parts) + + +def check_supported_localstack_image(image: str): + """ + Checks a Localstack Docker image ref to see if the version + tag is one that pytest-localstack supports. + """ + image_parts = image.split(":", 1) + if len(image_parts) < 2: + return # No tag, so latest image. + tag = image_parts[1] + if tag[0] != "v": + # Not a version tag. Assume it's ok. + return + version = get_version_tuple(tag) + if version >= (0, 11, 6): + # This is a + return + raise exceptions.UnsupportedLocalstackVersionError(image) + + +def generate_random_string(n: int = 6) -> str: + """Generate a random string of ascii chars, n chars long.""" + valid_chars = set(string.ascii_letters) + chars = [] + while len(chars) < 6: + new_chars = [chr(c) for c in os.urandom(6 - len(chars))] + chars += [c for c in new_chars if c in valid_chars] + return "".join(chars) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a809e4e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,22 @@ +import os +from unittest import mock + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def patch_aws_env_vars(): + """Prevent any accidental calls to real AWS by setting these env vars.""" + with mock.patch.dict( + "os.environ", + **{ + **os.environ, + **{ + "AWS_ACCESS_KEY_ID": "accesskey", + "AWS_SECRET_ACCESS_KEY": "secretkey", + "AWS_SESSION_TOKEN": "token", + "AWS_DEFAULT_REGION": "us-east-1", + }, + } + ): + yield diff --git a/tests/functional/test_patch_fixture/test_README.py b/tests/functional/test_patch_fixture/test_README.py index f32933c..7e64acd 100644 --- a/tests/functional/test_patch_fixture/test_README.py +++ b/tests/functional/test_patch_fixture/test_README.py @@ -1,18 +1,18 @@ -"""Test examples from the README.""" -import boto3 +# """Test examples from the README.""" +# import boto3 -import pytest +# import pytest -import pytest_localstack +# import pytest_localstack -patch_s3 = pytest_localstack.patch_fixture(services=["s3"]) +# patch_s3 = pytest_localstack.patch_fixture(services=["s3"]) -@pytest.mark.usefixtures("patch_s3") -def test_s3_bucket_creation(): - """Test S3 bucket creation with patch fixture.""" - s3 = boto3.resource("s3") # Will use Localstack - assert len(list(s3.buckets.all())) == 0 - bucket = s3.Bucket("foobar") - bucket.create() +# @pytest.mark.usefixtures("patch_s3") +# def test_s3_bucket_creation(): +# """Test S3 bucket creation with patch fixture.""" +# s3 = boto3.resource("s3") # Will use Localstack +# assert len(list(s3.buckets.all())) == 0 +# bucket = s3.Bucket("foobar") +# bucket.create() diff --git a/tests/functional/test_patch_fixture/test_services_available.py b/tests/functional/test_patch_fixture/test_services_available.py deleted file mode 100644 index 3e3175b..0000000 --- a/tests/functional/test_patch_fixture/test_services_available.py +++ /dev/null @@ -1,252 +0,0 @@ -"""Test all services accessible for pytest_localstack.patch_fixture.""" -import boto3 -import botocore - -import pytest_localstack - - -localstack = pytest_localstack.patch_fixture(scope="module", autouse=True) - - -def _assert_key_isinstance(result, key, type): - assert key in result - assert isinstance(result[key], type) - - -class TestBotocore: - """Test service accessibility via botocore.""" - - @property - def session(self): - if not hasattr(self, "_session"): - self._session = botocore.session.get_session() - return self._session - - def client(self, *args, **kwargs): - return self.session.create_client(*args, **kwargs) - - def test_apigateway_available(self): - client = self.client("apigateway") - result = client.get_rest_apis() - assert result["items"] == [] - - def test_cloudformation_available(self): - client = self.client("cloudformation") - result = client.list_stacks( - StackStatusFilter=[ - "CREATE_IN_PROGRESS", - "CREATE_FAILED", - "CREATE_COMPLETE", - "ROLLBACK_IN_PROGRESS", - "ROLLBACK_FAILED", - "ROLLBACK_COMPLETE", - "DELETE_IN_PROGRESS", - "DELETE_FAILED", - "DELETE_COMPLETE", - "UPDATE_IN_PROGRESS", - "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "UPDATE_COMPLETE", - "UPDATE_ROLLBACK_IN_PROGRESS", - "UPDATE_ROLLBACK_FAILED", - "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", - "UPDATE_ROLLBACK_COMPLETE", - "REVIEW_IN_PROGRESS", - ] - ) - _assert_key_isinstance(result, "StackSummaries", list) - - def test_cloudwatch_available(self): - client = self.client("cloudwatch") - result = client.list_metrics() - _assert_key_isinstance(result, "Metrics", list) - - def test_dynamodb_available(self): - client = self.client("dynamodb") - result = client.list_tables() - _assert_key_isinstance(result, "TableNames", list) - - def test_dynamodbstreams_available(self): - client = self.client("dynamodbstreams") - result = client.list_streams() - _assert_key_isinstance(result, "Streams", list) - - def test_es_available(self): - client = self.client("es") - result = client.list_domain_names() - _assert_key_isinstance(result, "DomainNames", list) - - def test_firehose_available(self): - client = self.client("firehose") - result = client.list_delivery_streams() - _assert_key_isinstance(result, "DeliveryStreamNames", list) - - def test_kinesis_available(self): - client = self.client("kinesis") - result = client.list_streams() - _assert_key_isinstance(result, "StreamNames", list) - - def test_lambda_available(self): - client = self.client("lambda") - result = client.list_functions() - _assert_key_isinstance(result, "Functions", list) - - def test_redshift_available(self): - client = self.client("redshift") - result = client.describe_clusters() - _assert_key_isinstance(result, "Clusters", list) - - def test_route53_available(self): - client = self.client("route53") - result = client.list_hosted_zones() - _assert_key_isinstance(result, "HostedZones", list) - - def test_s3_available(self): - client = self.client("s3") - result = client.list_buckets() - _assert_key_isinstance(result, "Buckets", list) - - def test_ses_available(self): - client = self.client("ses") - result = client.list_identities() - _assert_key_isinstance(result, "Identities", list) - - def test_sns_available(self): - client = self.client("sns") - result = client.list_topics() - _assert_key_isinstance(result, "Topics", list) - - def test_sqs_available(self): - client = self.client("sqs") - result = client.list_queues() - assert "ResponseMetadata" in result - assert "HTTPStatusCode" in result["ResponseMetadata"] - assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 - - -class TestBoto3Clients: - """Test service accessibility via boto3 clients.""" - - def test_apigateway_available(self): - client = boto3.client("apigateway") - result = client.get_rest_apis() - assert result["items"] == [] - - def test_cloudformation_available(self): - client = boto3.client("cloudformation") - result = client.list_stacks( - StackStatusFilter=[ - "CREATE_IN_PROGRESS", - "CREATE_FAILED", - "CREATE_COMPLETE", - "ROLLBACK_IN_PROGRESS", - "ROLLBACK_FAILED", - "ROLLBACK_COMPLETE", - "DELETE_IN_PROGRESS", - "DELETE_FAILED", - "DELETE_COMPLETE", - "UPDATE_IN_PROGRESS", - "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "UPDATE_COMPLETE", - "UPDATE_ROLLBACK_IN_PROGRESS", - "UPDATE_ROLLBACK_FAILED", - "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", - "UPDATE_ROLLBACK_COMPLETE", - "REVIEW_IN_PROGRESS", - ] - ) - _assert_key_isinstance(result, "StackSummaries", list) - - def test_cloudwatch_available(self): - client = boto3.client("cloudwatch") - result = client.list_metrics() - _assert_key_isinstance(result, "Metrics", list) - - def test_dynamodb_available(self): - client = boto3.client("dynamodb") - result = client.list_tables() - _assert_key_isinstance(result, "TableNames", list) - - def test_dynamodbstreams_available(self): - client = boto3.client("dynamodbstreams") - result = client.list_streams() - _assert_key_isinstance(result, "Streams", list) - - def test_es_available(self): - client = boto3.client("es") - result = client.list_domain_names() - _assert_key_isinstance(result, "DomainNames", list) - - def test_firehose_available(self): - client = boto3.client("firehose") - result = client.list_delivery_streams() - _assert_key_isinstance(result, "DeliveryStreamNames", list) - - def test_kinesis_available(self): - client = boto3.client("kinesis") - result = client.list_streams() - _assert_key_isinstance(result, "StreamNames", list) - - def test_lambda_available(self): - client = boto3.client("lambda") - result = client.list_functions() - _assert_key_isinstance(result, "Functions", list) - - def test_redshift_available(self): - client = boto3.client("redshift") - result = client.describe_clusters() - _assert_key_isinstance(result, "Clusters", list) - - def test_route53_available(self): - client = boto3.client("route53") - result = client.list_hosted_zones() - _assert_key_isinstance(result, "HostedZones", list) - - def test_s3_available(self): - client = boto3.client("s3") - result = client.list_buckets() - _assert_key_isinstance(result, "Buckets", list) - - def test_ses_available(self): - client = boto3.client("ses") - result = client.list_identities() - _assert_key_isinstance(result, "Identities", list) - - def test_sns_available(self): - client = boto3.client("sns") - result = client.list_topics() - _assert_key_isinstance(result, "Topics", list) - - def test_sqs_available(self): - client = boto3.client("sqs") - result = client.list_queues() - assert "ResponseMetadata" in result - assert "HTTPStatusCode" in result["ResponseMetadata"] - assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 - - -class TestBoto3Resources: - """Test service accessibility via boto3 resources.""" - - def test_cloudformation_available(self): - cloudformation = boto3.resource("cloudformation") - assert isinstance(list(cloudformation.stacks.all()), list) - - def test_cloudwatch_available(self): - cloudwatch = boto3.resource("cloudwatch") - assert isinstance(list(cloudwatch.alarms.all()), list) - - def test_dynamodb_available(self): - dynamodb = boto3.resource("dynamodb") - assert isinstance(list(dynamodb.tables.all()), list) - - def test_s3_available(self): - s3 = boto3.resource("s3") - assert isinstance(list(s3.buckets.all()), list) - - def test_sns_available(self): - sns = boto3.resource("sns") - assert isinstance(list(sns.topics.all()), list) - - def test_sqs_available(self): - sqs = boto3.resource("sqs") - assert isinstance(list(sqs.queues.all()), list) diff --git a/tests/functional/test_service_fixture/test_README.py b/tests/functional/test_service_fixture/test_README.py index 8db0135..3fea70e 100644 --- a/tests/functional/test_service_fixture/test_README.py +++ b/tests/functional/test_service_fixture/test_README.py @@ -1,49 +1,49 @@ -"""Test examples from the README.""" -import pytest_localstack - - -s3_service_1 = pytest_localstack.session_fixture(services=["s3"]) -s3_service_2 = pytest_localstack.session_fixture(services=["s3"]) - - -def test_sync_buckets(s3_service_1, s3_service_2): - """Test using multple sessions in one test.""" - s3_1 = s3_service_1.boto3.resource("s3") - s3_2 = s3_service_2.boto3.resource("s3") - - src_bucket = s3_1.Bucket("src-bucket") - src_bucket.create() - src_object = src_bucket.Object("foobar") - src_object.put(Body=b"Hello world!") - - dest_bucket = s3_2.Bucket("dest-bucket") - dest_bucket.create() - - _sync_buckets(src_bucket, dest_bucket) - - response = dest_bucket.Object("foobar").get() - assert response["Body"].read() == b"Hello world!" - - -def _sync_buckets(src_bucket, dest_bucket): - for src_obj in src_bucket.objects.all(): - dest_obj = dest_bucket.Object(src_obj.key) - response = src_obj.get() - kwargs = {"Body": response["Body"].read()} - for key in [ - "CacheControl", - "ContentDisposition", - "ContentEncoding", - "ContentType", - "Expires", - "WebsiteRedirectLocation", - "ServerSideEncryption", - "Metadata", - "SSECustomerAlgorithm", - "SSECustomerKeyMD5", - "SSEKMSKeyId", - "StorageClass", - ]: - if key in response: - kwargs[key] = response[key] - dest_obj.put(**kwargs) +# """Test examples from the README.""" +# import pytest_localstack + + +# s3_service_1 = pytest_localstack.session_fixture(services=["s3"]) +# s3_service_2 = pytest_localstack.session_fixture(services=["s3"]) + + +# def test_sync_buckets(s3_service_1, s3_service_2): +# """Test using multple sessions in one test.""" +# s3_1 = s3_service_1.boto3.resource("s3") +# s3_2 = s3_service_2.boto3.resource("s3") + +# src_bucket = s3_1.Bucket("src-bucket") +# src_bucket.create() +# src_object = src_bucket.Object("foobar") +# src_object.put(Body=b"Hello world!") + +# dest_bucket = s3_2.Bucket("dest-bucket") +# dest_bucket.create() + +# _sync_buckets(src_bucket, dest_bucket) + +# response = dest_bucket.Object("foobar").get() +# assert response["Body"].read() == b"Hello world!" + + +# def _sync_buckets(src_bucket, dest_bucket): +# for src_obj in src_bucket.objects.all(): +# dest_obj = dest_bucket.Object(src_obj.key) +# response = src_obj.get() +# kwargs = {"Body": response["Body"].read()} +# for key in [ +# "CacheControl", +# "ContentDisposition", +# "ContentEncoding", +# "ContentType", +# "Expires", +# "WebsiteRedirectLocation", +# "ServerSideEncryption", +# "Metadata", +# "SSECustomerAlgorithm", +# "SSECustomerKeyMD5", +# "SSEKMSKeyId", +# "StorageClass", +# ]: +# if key in response: +# kwargs[key] = response[key] +# dest_obj.put(**kwargs) diff --git a/tests/functional/test_service_fixture/test_services_available.py b/tests/functional/test_service_fixture/test_services_available.py deleted file mode 100644 index 1eb866e..0000000 --- a/tests/functional/test_service_fixture/test_services_available.py +++ /dev/null @@ -1,240 +0,0 @@ -"""Test all services accessible for pytest_localstack.session_fixture.""" -import pytest_localstack - - -localstack = pytest_localstack.session_fixture(scope="module", autouse=True) - - -def _assert_key_isinstance(result, key, type): - assert key in result - assert isinstance(result[key], type) - - -class TestBotocore: - """Test service accessibility via botocore.""" - - def test_apigateway_available(self, localstack): - client = localstack.botocore.client("apigateway") - result = client.get_rest_apis() - assert result["items"] == [] - - def test_cloudformation_available(self, localstack): - client = localstack.botocore.client("cloudformation") - result = client.list_stacks( - StackStatusFilter=[ - "CREATE_IN_PROGRESS", - "CREATE_FAILED", - "CREATE_COMPLETE", - "ROLLBACK_IN_PROGRESS", - "ROLLBACK_FAILED", - "ROLLBACK_COMPLETE", - "DELETE_IN_PROGRESS", - "DELETE_FAILED", - "DELETE_COMPLETE", - "UPDATE_IN_PROGRESS", - "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "UPDATE_COMPLETE", - "UPDATE_ROLLBACK_IN_PROGRESS", - "UPDATE_ROLLBACK_FAILED", - "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", - "UPDATE_ROLLBACK_COMPLETE", - "REVIEW_IN_PROGRESS", - ] - ) - _assert_key_isinstance(result, "StackSummaries", list) - - def test_cloudwatch_available(self, localstack): - client = localstack.botocore.client("cloudwatch") - result = client.list_metrics() - _assert_key_isinstance(result, "Metrics", list) - - def test_dynamodb_available(self, localstack): - client = localstack.botocore.client("dynamodb") - result = client.list_tables() - _assert_key_isinstance(result, "TableNames", list) - - def test_dynamodbstreams_available(self, localstack): - client = localstack.botocore.client("dynamodbstreams") - result = client.list_streams() - _assert_key_isinstance(result, "Streams", list) - - def test_es_available(self, localstack): - client = localstack.botocore.client("es") - result = client.list_domain_names() - _assert_key_isinstance(result, "DomainNames", list) - - def test_firehose_available(self, localstack): - client = localstack.botocore.client("firehose") - result = client.list_delivery_streams() - _assert_key_isinstance(result, "DeliveryStreamNames", list) - - def test_kinesis_available(self, localstack): - client = localstack.botocore.client("kinesis") - result = client.list_streams() - _assert_key_isinstance(result, "StreamNames", list) - - def test_lambda_available(self, localstack): - client = localstack.botocore.client("lambda") - result = client.list_functions() - _assert_key_isinstance(result, "Functions", list) - - def test_redshift_available(self, localstack): - client = localstack.botocore.client("redshift") - result = client.describe_clusters() - _assert_key_isinstance(result, "Clusters", list) - - def test_route53_available(self, localstack): - client = localstack.botocore.client("route53") - result = client.list_hosted_zones() - _assert_key_isinstance(result, "HostedZones", list) - - def test_s3_available(self, localstack): - client = localstack.botocore.client("s3") - result = client.list_buckets() - _assert_key_isinstance(result, "Buckets", list) - - def test_ses_available(self, localstack): - client = localstack.botocore.client("ses") - result = client.list_identities() - _assert_key_isinstance(result, "Identities", list) - - def test_sns_available(self, localstack): - client = localstack.botocore.client("sns") - result = client.list_topics() - _assert_key_isinstance(result, "Topics", list) - - def test_sqs_available(self, localstack): - client = localstack.botocore.client("sqs") - result = client.list_queues() - assert "ResponseMetadata" in result - assert "HTTPStatusCode" in result["ResponseMetadata"] - assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 - - -class TestBoto3Clients: - """Test service accessibility via boto3 clients.""" - - def test_apigateway_available(self, localstack): - client = localstack.boto3.client("apigateway") - result = client.get_rest_apis() - assert result["items"] == [] - - def test_cloudformation_available(self, localstack): - client = localstack.boto3.client("cloudformation") - result = client.list_stacks( - StackStatusFilter=[ - "CREATE_IN_PROGRESS", - "CREATE_FAILED", - "CREATE_COMPLETE", - "ROLLBACK_IN_PROGRESS", - "ROLLBACK_FAILED", - "ROLLBACK_COMPLETE", - "DELETE_IN_PROGRESS", - "DELETE_FAILED", - "DELETE_COMPLETE", - "UPDATE_IN_PROGRESS", - "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "UPDATE_COMPLETE", - "UPDATE_ROLLBACK_IN_PROGRESS", - "UPDATE_ROLLBACK_FAILED", - "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", - "UPDATE_ROLLBACK_COMPLETE", - "REVIEW_IN_PROGRESS", - ] - ) - _assert_key_isinstance(result, "StackSummaries", list) - - def test_cloudwatch_available(self, localstack): - client = localstack.boto3.client("cloudwatch") - result = client.list_metrics() - _assert_key_isinstance(result, "Metrics", list) - - def test_dynamodb_available(self, localstack): - client = localstack.boto3.client("dynamodb") - result = client.list_tables() - _assert_key_isinstance(result, "TableNames", list) - - def test_dynamodbstreams_available(self, localstack): - client = localstack.boto3.client("dynamodbstreams") - result = client.list_streams() - _assert_key_isinstance(result, "Streams", list) - - def test_es_available(self, localstack): - client = localstack.boto3.client("es") - result = client.list_domain_names() - _assert_key_isinstance(result, "DomainNames", list) - - def test_firehose_available(self, localstack): - client = localstack.boto3.client("firehose") - result = client.list_delivery_streams() - _assert_key_isinstance(result, "DeliveryStreamNames", list) - - def test_kinesis_available(self, localstack): - client = localstack.boto3.client("kinesis") - result = client.list_streams() - _assert_key_isinstance(result, "StreamNames", list) - - def test_lambda_available(self, localstack): - client = localstack.boto3.client("lambda") - result = client.list_functions() - _assert_key_isinstance(result, "Functions", list) - - def test_redshift_available(self, localstack): - client = localstack.boto3.client("redshift") - result = client.describe_clusters() - _assert_key_isinstance(result, "Clusters", list) - - def test_route53_available(self, localstack): - client = localstack.boto3.client("route53") - result = client.list_hosted_zones() - _assert_key_isinstance(result, "HostedZones", list) - - def test_s3_available(self, localstack): - client = localstack.boto3.client("s3") - result = client.list_buckets() - _assert_key_isinstance(result, "Buckets", list) - - def test_ses_available(self, localstack): - client = localstack.boto3.client("ses") - result = client.list_identities() - _assert_key_isinstance(result, "Identities", list) - - def test_sns_available(self, localstack): - client = localstack.boto3.client("sns") - result = client.list_topics() - _assert_key_isinstance(result, "Topics", list) - - def test_sqs_available(self, localstack): - client = localstack.boto3.client("sqs") - result = client.list_queues() - assert "ResponseMetadata" in result - assert "HTTPStatusCode" in result["ResponseMetadata"] - assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 - - -class TestBoto3Resources: - """Test service accessibility via boto3 resources.""" - - def test_cloudformation_available(self, localstack): - cloudformation = localstack.boto3.resource("cloudformation") - assert isinstance(list(cloudformation.stacks.all()), list) - - def test_cloudwatch_available(self, localstack): - cloudwatch = localstack.boto3.resource("cloudwatch") - assert isinstance(list(cloudwatch.alarms.all()), list) - - def test_dynamodb_available(self, localstack): - dynamodb = localstack.boto3.resource("dynamodb") - assert isinstance(list(dynamodb.tables.all()), list) - - def test_s3_available(self, localstack): - s3 = localstack.boto3.resource("s3") - assert isinstance(list(s3.buckets.all()), list) - - def test_sns_available(self, localstack): - sns = localstack.boto3.resource("sns") - assert isinstance(list(sns.topics.all()), list) - - def test_sqs_available(self, localstack): - sqs = localstack.boto3.resource("sqs") - assert isinstance(list(sqs.queues.all()), list) diff --git a/tests/functional/test_session.py b/tests/functional/test_session.py index d359177..526f54d 100644 --- a/tests/functional/test_session.py +++ b/tests/functional/test_session.py @@ -1,45 +1,45 @@ -"""Functional tests for pytest_localstack.session.""" -import pytest +# """Functional tests for pytest_localstack.session.""" +# import pytest -from pytest_localstack import constants, exceptions, service_checks, session +# from pytest_localstack import constants, exceptions, service_checks, session -@pytest.mark.parametrize("test_service", sorted(constants.SERVICE_PORTS)) -def test_RunningSession_individual_services(test_service, docker_client): - localstack_imagename = "localstack/localstack:latest" +# @pytest.mark.parametrize("test_service", sorted(constants.SERVICE_PORTS)) +# def test_RunningSession_individual_services(test_service, docker_client): +# localstack_imagename = "localstack/localstack:latest" - docker_client.images.pull(localstack_imagename) - localstack_container = None - try: - port = constants.SERVICE_PORTS[test_service] - localstack_container = docker_client.containers.run( - localstack_imagename, - name="localstack_test", - detach=True, - auto_remove=True, - ports={port: port}, - ) - test_session = session.RunningSession("127.0.0.1", services=[test_service]) - with test_session: - for service_name, service_check in service_checks.SERVICE_CHECKS.items(): - if service_name == test_service: - service_check(test_session) - else: - with pytest.raises(exceptions.ServiceError): - test_session.service_hostname(test_session) - finally: - if localstack_container: - localstack_container.stop(timeout=10) +# docker_client.images.pull(localstack_imagename) +# localstack_container = None +# try: +# port = constants.SERVICE_PORTS[test_service] +# localstack_container = docker_client.containers.run( +# localstack_imagename, +# name="localstack_test", +# detach=True, +# auto_remove=True, +# ports={port: port}, +# ) +# test_session = session.RunningSession("127.0.0.1", services=[test_service]) +# with test_session: +# for service_name, service_check in service_checks.SERVICE_CHECKS.items(): +# if service_name == test_service: +# service_check(test_session) +# else: +# with pytest.raises(exceptions.ServiceError): +# test_session.service_hostname(test_session) +# finally: +# if localstack_container: +# localstack_container.stop(timeout=10) -@pytest.mark.parametrize("test_service", sorted(constants.SERVICE_PORTS)) -def test_LocalstackSession_individual_services(test_service, docker_client): - """Test that each service can run individually.""" - test_session = session.LocalstackSession(docker_client, services=[test_service]) - with test_session: - for service_name, service_check in service_checks.SERVICE_CHECKS.items(): - if service_name == test_service: - service_check(test_session) - else: - with pytest.raises(exceptions.ServiceError): - test_session.service_hostname(test_session) +# @pytest.mark.parametrize("test_service", sorted(constants.SERVICE_PORTS)) +# def test_LocalstackSession_individual_services(test_service, docker_client): +# """Test that each service can run individually.""" +# test_session = session.LocalstackSession(docker_client, services=[test_service]) +# with test_session: +# for service_name, service_check in service_checks.SERVICE_CHECKS.items(): +# if service_name == test_service: +# service_check(test_session) +# else: +# with pytest.raises(exceptions.ServiceError): +# test_session.service_hostname(test_session) diff --git a/tests/integration/test_contrib/__init__.py b/tests/integration/test_contrib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_contrib/test_boto3.py b/tests/integration/test_contrib/test_boto3.py deleted file mode 100644 index 275e0e3..0000000 --- a/tests/integration/test_contrib/test_boto3.py +++ /dev/null @@ -1,162 +0,0 @@ -import os -from unittest import mock - -import boto3 -import botocore - -import pytest - -from pytest_localstack import constants, exceptions, plugin -from pytest_localstack.contrib import boto3 as ptls_boto3 -from tests import utils as test_utils - - -def test_session_contribution(): - dummy_session = type("DummySession", (object,), {})() - plugin.manager.hook.contribute_to_session(session=dummy_session) - assert isinstance(dummy_session.boto3, ptls_boto3.Boto3TestResourceFactory) - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -def test_session(make_test_session): - """Test session creation.""" - localstack = make_test_session() - - ls_session = localstack.boto3.session() - assert isinstance(ls_session, boto3.session.Session) - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -def test_default_session(make_test_session): - """Test default session.""" - localstack = make_test_session() - session_1 = localstack.boto3.default_session - session_2 = localstack.boto3.default_session - assert session_1 is session_2 - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -@pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) -def test_client(service_name, make_test_session): - """Test client creation.""" - localstack = make_test_session() - if hasattr(localstack, "_container"): - with pytest.raises(exceptions.ContainerNotStartedError): - client = localstack.boto3.client(service_name) - - with localstack: - client = localstack.boto3.client(service_name) - assert isinstance(client, botocore.client.BaseClient) - assert "127.0.0.1" in client._endpoint.host - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -@pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) -def test_resource(service_name, make_test_session): - """Test resource creation.""" - if service_name not in [ - "cloudformation", - "cloudwatch", - "dynamodb", - "ec2", - "glacier", - "iam", - "opsworks", - "s3", - "sns", - "sqs", - ]: - pytest.skip("No boto3 resource available for this service.") - localstack = make_test_session() - - if hasattr(localstack, "_container"): - with pytest.raises(exceptions.ContainerNotStartedError): - resource = localstack.boto3.resource(service_name) - - with localstack: - resource = localstack.boto3.resource(service_name) - assert isinstance(resource, boto3.resources.base.ServiceResource) - assert "127.0.0.1" in resource.meta.client._endpoint.host - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -def test_patch_botocore_credentials(make_test_session): - """Test to the default boto3 session credentials get patched correctly.""" - session = boto3._get_default_session() - localstack = make_test_session() - - credentials = session.get_credentials() - initial_access_key = credentials.access_key if credentials else None - initial_secret_key = credentials.secret_key if credentials else None - initial_token = credentials.token if credentials else None - initial_method = credentials.method if credentials else None - - assert initial_access_key != constants.DEFAULT_AWS_ACCESS_KEY_ID - assert initial_secret_key != constants.DEFAULT_AWS_SECRET_ACCESS_KEY - assert initial_token != constants.DEFAULT_AWS_SESSION_TOKEN - assert initial_method != "localstack-default" - - with localstack: - # should prefer access credentials from environment variables. - with mock.patch.dict( - os.environ, - AWS_ACCESS_KEY_ID=str(mock.sentinel.AWS_ACCESS_KEY_ID), - AWS_SECRET_ACCESS_KEY=str(mock.sentinel.AWS_SECRET_ACCESS_KEY), - AWS_SESSION_TOKEN=str(mock.sentinel.AWS_SESSION_TOKEN), - ): - with localstack.botocore.patch_botocore(): - credentials = session.get_credentials() - assert credentials is not None - assert credentials.access_key == str(mock.sentinel.AWS_ACCESS_KEY_ID) - assert credentials.secret_key == str( - mock.sentinel.AWS_SECRET_ACCESS_KEY - ) - assert credentials.token == str(mock.sentinel.AWS_SESSION_TOKEN) - assert credentials.method == "env" - - # check credentials get unpatched correctly - credentials = session.get_credentials() - assert (credentials.access_key if credentials else None) == initial_access_key - assert (credentials.secret_key if credentials else None) == initial_secret_key - assert (credentials.token if credentials else None) == initial_token - assert (credentials.method if credentials else None) == initial_method - - # should fallback to default credentials if none in the environment - with mock.patch.dict( - os.environ, - AWS_ACCESS_KEY_ID="", - AWS_SECRET_ACCESS_KEY="", - AWS_SESSION_TOKEN="", - ): - os.environ.pop("AWS_ACCESS_KEY_ID", None) - os.environ.pop("AWS_SECRET_ACCESS_KEY", None) - os.environ.pop("AWS_SESSION_TOKEN", None) - with localstack.botocore.patch_botocore(): - credentials = session.get_credentials() - assert credentials is not None - assert credentials.access_key == constants.DEFAULT_AWS_ACCESS_KEY_ID - assert credentials.secret_key == constants.DEFAULT_AWS_SECRET_ACCESS_KEY - assert credentials.token == constants.DEFAULT_AWS_SESSION_TOKEN - assert credentials.method == "localstack-default" - - # check credentials get unpatched correctly - credentials = session.get_credentials() - assert (credentials.access_key if credentials else None) == initial_access_key - assert (credentials.secret_key if credentials else None) == initial_secret_key - assert (credentials.token if credentials else None) == initial_token - assert (credentials.method if credentials else None) == initial_method diff --git a/tests/integration/test_contrib/test_botocore.py b/tests/integration/test_contrib/test_botocore.py deleted file mode 100644 index c3db5ab..0000000 --- a/tests/integration/test_contrib/test_botocore.py +++ /dev/null @@ -1,203 +0,0 @@ -import botocore -import botocore.session - -import pytest - -from pytest_localstack import botocore as localstack_botocore -from pytest_localstack import constants, exceptions -from tests import utils as test_utils - - -def test_create_credential_resolver(): - """Test pytest_localstack.botocore.create_credential_resolver.""" - resolver = localstack_botocore.create_credential_resolver() - assert isinstance(resolver, botocore.credentials.CredentialResolver) - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -@pytest.mark.parametrize("region_name", test_utils.AWS_REGIONS) -@pytest.mark.parametrize("not_region_name", test_utils.AWS_REGIONS) -@pytest.mark.parametrize( - "service_alias", - sorted( - list(constants.SERVICE_ALIASES.keys()) + list(constants.SERVICE_PORTS.keys()) - ), -) -def test_LocalstackEndpointResolver( - region_name, not_region_name, service_alias, make_test_session -): - """Test pytest_localstack.botocore.LocalstackEndpointResolver.""" - if region_name == not_region_name: - pytest.skip("Should not be equal.") - service_name = constants.SERVICE_ALIASES.get(service_alias, service_alias) - localstack = make_test_session(region_name=region_name, use_ssl=False) - - # Is the correct type. - if constants.BOTOCORE_VERSION >= (1, 10, 58): - resolver = localstack.botocore.session()._get_internal_component( - "endpoint_resolver" - ) - else: - resolver = localstack.botocore.session().get_component("endpoint_resolver") - assert isinstance(resolver, localstack_botocore.LocalstackEndpointResolver) - assert isinstance(resolver, botocore.regions.EndpointResolver) - - # Only supports 'aws' partition. - result = resolver.get_available_partitions() - assert result == ["aws"] - with pytest.raises(exceptions.UnsupportedPartitionError): - resolver.get_available_endpoints(service_alias, partition_name="aws-cn") - - # Can only return the region LocalstackSession was configure with or aws-global. - result = resolver.get_available_endpoints(service_alias) - result = set(result) - result.discard(localstack.region_name) - result.discard("aws-global") - assert not result - - if hasattr(localstack, "_container"): - # Can't construct endpoints until the container is started. - with pytest.raises(exceptions.ContainerNotStartedError): - result = resolver.construct_endpoint(service_alias) - - with localstack: # Start container. - with pytest.raises(exceptions.RegionError): - # Can only get endpoints for the region LocalstackSession - # was configured with. - result = resolver.construct_endpoint( - service_alias, region_name=not_region_name - ) - - result = resolver.construct_endpoint(service_alias) - assert result["partition"] == "aws" - assert result["endpointName"] in (localstack.region_name, "aws-global") - assert ( - result["hostname"] == "127.0.0.1:%i" % constants.SERVICE_PORTS[service_name] - ) - assert result["protocols"] == ["http"] - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -@pytest.mark.parametrize("region_name", test_utils.AWS_REGIONS) -@pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) -def test_session(region_name, service_name, make_test_session): - """Test Session creation.""" - localstack = make_test_session(region_name=region_name) - - ls_session = localstack.botocore.session() - assert isinstance(ls_session, localstack_botocore.Session) - - if hasattr(localstack, "_container"): - with pytest.raises(exceptions.ContainerNotStartedError): - # Can't create clients until the container is started, - # because the client needs to know what port its - # target service is running on. - bc_client = ls_session.create_client(service_name, localstack.region_name) - - with localstack: # Start container. - bc_client = ls_session.create_client(service_name, localstack.region_name) - assert isinstance(bc_client, botocore.client.BaseClient) - assert "127.0.0.1" in bc_client._endpoint.host - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -@pytest.mark.parametrize("region_name", test_utils.AWS_REGIONS) -@pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) -def test_client(region_name, service_name, make_test_session): - """Test Client creation.""" - localstack = make_test_session(region_name=region_name) - - if hasattr(localstack, "_container"): - with pytest.raises(exceptions.ContainerNotStartedError): - bc_client = localstack.botocore.client(service_name, localstack.region_name) - - with localstack: # Start container. - bc_client = localstack.botocore.client(service_name, localstack.region_name) - assert isinstance(bc_client, botocore.client.BaseClient) - assert "127.0.0.1" in bc_client._endpoint.host - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -@pytest.mark.parametrize("region_name", test_utils.AWS_REGIONS) -def test_default_session(region_name, make_test_session): - """Test default session.""" - localstack = make_test_session(region_name=region_name) - session_1 = localstack.botocore.default_session - session_2 = localstack.botocore.default_session - assert session_1 is session_2 - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -@pytest.mark.parametrize("region_name", test_utils.AWS_REGIONS) -@pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) -def test_patch(region_name, service_name, make_test_session): - """Test patching.""" - localstack = make_test_session(region_name=region_name) - - with localstack: - # Haven't patched yet. - # A regular botocore client should point to AWS right now. - original_bc_session = botocore.session.get_session() - original_bc_client = original_bc_session.create_client( - service_name, localstack.region_name - ) - assert "127.0.0.1" not in original_bc_client._endpoint.host - - with localstack.botocore.patch_botocore(): - - # Original client should now point to Localstack - assert "127.0.0.1" in original_bc_client._endpoint.host - - # Original session should create Localstack clients - ls_client = original_bc_session.create_client( - service_name, localstack.region_name - ) - assert "127.0.0.1" in ls_client._endpoint.host - - # Original client back to AWS - assert "127.0.0.1" not in original_bc_client._endpoint.host - - # Original session should create AWS clients - bc_client = original_bc_session.create_client( - service_name, localstack.region_name - ) - assert "127.0.0.1" not in bc_client._endpoint.host - - # Localstack client create while patched still points to Localstack - assert "127.0.0.1" in ls_client._endpoint.host - - -@pytest.mark.parametrize( - "make_test_session", - [test_utils.make_test_LocalstackSession, test_utils.make_test_RunningSession], -) -def test_exceptions_populated(make_test_session): - """Patched botocore clients populated `exceptions` correctly.""" - botocore_session = botocore.session.get_session() - botocore_client = botocore_session.create_client("s3") - - localstack = make_test_session() - - assert botocore_client._exceptions is None - - with localstack, localstack.botocore.patch_botocore(): - result = botocore_client.exceptions - assert result is not None - assert botocore_client._exceptions is not None - - assert botocore_client._exceptions is None diff --git a/tests/integration/test_plugin.py b/tests/integration/test_plugin.py index b44b7a9..4fa836e 100644 --- a/tests/integration/test_plugin.py +++ b/tests/integration/test_plugin.py @@ -1,14 +1,14 @@ -import pytest_localstack -from pytest_localstack import hookspecs, plugin +# import pytest_localstack +# from pytest_localstack import hookspecs, plugin -@hookspecs.pytest_localstack_hookimpl -def contribute_to_module(pytest_localstack): - pytest_localstack._foo = "bar" +# @hookspecs.pytest_localstack_hookimpl +# def contribute_to_module(pytest_localstack): +# pytest_localstack._foo = "bar" -def test_register_plugin_module(): - assert not hasattr(pytest_localstack, "_foo") - plugin.register_plugin_module("tests.integration.test_plugin") - assert pytest_localstack._foo == "bar" - del pytest_localstack._foo +# def test_register_plugin_module(): +# assert not hasattr(pytest_localstack, "_foo") +# plugin.register_plugin_module("tests.integration.test_plugin") +# assert pytest_localstack._foo == "bar" +# del pytest_localstack._foo diff --git a/tests/unit/test_container.py b/tests/unit/test_container.py index afac319..9f81ad8 100644 --- a/tests/unit/test_container.py +++ b/tests/unit/test_container.py @@ -2,13 +2,12 @@ from unittest import mock from pytest_localstack import container as ptls_container -from pytest_localstack import session from tests import utils as test_utils def test_DockerLogTailer(caplog): """Test pytest_localstack.container.DockerLogTailer.""" - container = test_utils.make_mock_container(session.LocalstackSession.image_name) + container = test_utils.make_mock_container("localstack/localstack") logger_name = "test_logger.%s." % container.short_id logger = logging.getLogger(logger_name) log_level = logging.DEBUG diff --git a/tests/unit/test_patch.py b/tests/unit/test_patch.py new file mode 100644 index 0000000..9ecc959 --- /dev/null +++ b/tests/unit/test_patch.py @@ -0,0 +1,58 @@ +import boto3 +import botocore.session + +from pytest_localstack import patch + + +class TestPatchClients: + """Test to ensure the patch() function actually patches AWS clients.""" + + def test_botocore(self): + localstack_url = "http://example.org" + + session = botocore.session.Session() + + # Without patch. + s3_client = session.create_client("s3") + assert s3_client._endpoint.host != localstack_url + + # With patch. + with patch.aws_clients(localstack_url): + s3_client = session.create_client("s3") + assert s3_client._endpoint.host == localstack_url + + # With patch removed. + s3_client = session.create_client("s3") + assert s3_client._endpoint.host != localstack_url + + def test_boto3_client(self): + localstack_url = "http://example.org" + + # Without patch. + s3_client = boto3.client("s3") + assert s3_client._endpoint.host != localstack_url + + # With patch. + with patch.aws_clients(localstack_url): + s3_client = boto3.client("s3") + assert s3_client._endpoint.host == localstack_url + + # With patch removed. + s3_client = boto3.client("s3") + assert s3_client._endpoint.host != localstack_url + + def test_boto3_resource(self): + localstack_url = "http://example.org" + + # Without patch. + s3_resource = boto3.resource("s3") + assert s3_resource.meta.client._endpoint.host != localstack_url + + # With patch. + with patch.aws_clients(localstack_url): + s3_resource = boto3.resource("s3") + assert s3_resource.meta.client._endpoint.host == localstack_url + + # With patch removed. + s3_resource = boto3.resource("s3") + assert s3_resource.meta.client._endpoint.host != localstack_url diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index 07e979a..5b33c5b 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -1,152 +1,152 @@ -import re -import time +# import re +# import time -import pytest -from hypothesis import given -from hypothesis import strategies as st +# import pytest +# from hypothesis import given +# from hypothesis import strategies as st -from pytest_localstack import constants, exceptions, session -from tests import utils as test_utils +# from pytest_localstack import constants, exceptions, session +# from tests import utils as test_utils -@given(random=st.random_module()) -def test_generate_container_name(random): - """Test pytest_localstack.session.generate_container_name.""" - result = session.generate_container_name() - assert re.match(r"^pytest-localstack-[\w]{6}$", result) +# @given(random=st.random_module()) +# def test_generate_container_name(random): +# """Test pytest_localstack.session.generate_container_name.""" +# result = session.generate_container_name() +# assert re.match(r"^pytest-localstack-[\w]{6}$", result) -@pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) -def test_LocalstackSession_map_port(service_name): - """Test pytest_localstack.session.LocalstackSession.map_port.""" - test_session = test_utils.make_test_LocalstackSession() +# @pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) +# def test_LocalstackSession_map_port(service_name): +# """Test pytest_localstack.session.LocalstackSession.map_port.""" +# test_session = test_utils.make_test_LocalstackSession() - port = constants.SERVICE_PORTS[service_name] +# port = constants.SERVICE_PORTS[service_name] - with pytest.raises(exceptions.ContainerNotStartedError): - test_session.map_port(port) +# with pytest.raises(exceptions.ContainerNotStartedError): +# test_session.map_port(port) - test_session.start() - result = test_session.map_port(port) - assert result == port # see tests.utils.make_mock_docker_client() +# test_session.start() +# result = test_session.map_port(port) +# assert result == port # see tests.utils.make_mock_docker_client() -@pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) -@pytest.mark.parametrize("not_service_name", sorted(constants.SERVICE_PORTS.keys())) -def test_LocalstackSession_service_hostname(service_name, not_service_name): - """Test pytest_localstack.session.LocalstackSession.service_hostname.""" - if service_name == not_service_name: - pytest.skip("should not be equal") +# @pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) +# @pytest.mark.parametrize("not_service_name", sorted(constants.SERVICE_PORTS.keys())) +# def test_LocalstackSession_service_hostname(service_name, not_service_name): +# """Test pytest_localstack.session.LocalstackSession.service_hostname.""" +# if service_name == not_service_name: +# pytest.skip("should not be equal") - test_session = test_utils.make_test_LocalstackSession(services=[service_name]) +# test_session = test_utils.make_test_LocalstackSession(services=[service_name]) - with pytest.raises(exceptions.ContainerNotStartedError): - test_session.service_hostname(service_name) +# with pytest.raises(exceptions.ContainerNotStartedError): +# test_session.service_hostname(service_name) - test_session.start() +# test_session.start() - with pytest.raises(exceptions.ServiceError): - test_session.service_hostname(not_service_name) +# with pytest.raises(exceptions.ServiceError): +# test_session.service_hostname(not_service_name) - result = test_session.service_hostname(service_name) +# result = test_session.service_hostname(service_name) - # see tests.utils.make_mock_docker_client() - assert result == "127.0.0.1:%i" % (constants.SERVICE_PORTS[service_name]) +# # see tests.utils.make_mock_docker_client() +# assert result == "127.0.0.1:%i" % (constants.SERVICE_PORTS[service_name]) -@pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) -@pytest.mark.parametrize("not_service_name", sorted(constants.SERVICE_PORTS.keys())) -def test_RunningSession_service_hostname(service_name, not_service_name): - """Test pytest_localstack.session.RunningSession.service_hostname.""" - if service_name == not_service_name: - pytest.skip("should not be equal") +# @pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) +# @pytest.mark.parametrize("not_service_name", sorted(constants.SERVICE_PORTS.keys())) +# def test_RunningSession_service_hostname(service_name, not_service_name): +# """Test pytest_localstack.session.RunningSession.service_hostname.""" +# if service_name == not_service_name: +# pytest.skip("should not be equal") - test_session = test_utils.make_test_RunningSession(services=[service_name]) +# test_session = test_utils.make_test_RunningSession(services=[service_name]) - with pytest.raises(exceptions.ServiceError): - test_session.service_hostname(not_service_name) +# with pytest.raises(exceptions.ServiceError): +# test_session.service_hostname(not_service_name) - result = test_session.service_hostname(service_name) +# result = test_session.service_hostname(service_name) - # see tests.utils.make_mock_docker_client() - assert result == "127.0.0.1:%i" % (constants.SERVICE_PORTS[service_name]) +# # see tests.utils.make_mock_docker_client() +# assert result == "127.0.0.1:%i" % (constants.SERVICE_PORTS[service_name]) -@pytest.mark.parametrize("use_ssl", [(True,), (False,)]) -@pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) -@pytest.mark.parametrize("not_service_name", sorted(constants.SERVICE_PORTS.keys())) -def test_LocalstackSession_endpoint_url(use_ssl, service_name, not_service_name): - """Test pytest_localstack.session.LocalstackSession.endpoint_url.""" - if service_name == not_service_name: - pytest.skip("should not be equal") +# @pytest.mark.parametrize("use_ssl", [(True,), (False,)]) +# @pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) +# @pytest.mark.parametrize("not_service_name", sorted(constants.SERVICE_PORTS.keys())) +# def test_LocalstackSession_endpoint_url(use_ssl, service_name, not_service_name): +# """Test pytest_localstack.session.LocalstackSession.endpoint_url.""" +# if service_name == not_service_name: +# pytest.skip("should not be equal") - test_session = test_utils.make_test_LocalstackSession( - services=[service_name], use_ssl=use_ssl - ) +# test_session = test_utils.make_test_LocalstackSession( +# services=[service_name], use_ssl=use_ssl +# ) - with pytest.raises(exceptions.ContainerNotStartedError): - test_session.endpoint_url(service_name) +# with pytest.raises(exceptions.ContainerNotStartedError): +# test_session.endpoint_url(service_name) - test_session.start() +# test_session.start() - with pytest.raises(exceptions.ServiceError): - test_session.endpoint_url(not_service_name) +# with pytest.raises(exceptions.ServiceError): +# test_session.endpoint_url(not_service_name) - result = test_session.endpoint_url(service_name) - if test_session.use_ssl: - # see tests.utils.make_mock_docker_client() - assert result == "https://127.0.0.1:%i" % ( - constants.SERVICE_PORTS[service_name] - ) - else: - # see tests.utils.make_mock_docker_client() - assert result == "http://127.0.0.1:%i" % (constants.SERVICE_PORTS[service_name]) +# result = test_session.endpoint_url(service_name) +# if test_session.use_ssl: +# # see tests.utils.make_mock_docker_client() +# assert result == "https://127.0.0.1:%i" % ( +# constants.SERVICE_PORTS[service_name] +# ) +# else: +# # see tests.utils.make_mock_docker_client() +# assert result == "http://127.0.0.1:%i" % (constants.SERVICE_PORTS[service_name]) -@pytest.mark.parametrize("use_ssl", [(True,), (False,)]) -@pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) -@pytest.mark.parametrize("not_service_name", sorted(constants.SERVICE_PORTS.keys())) -def test_RunningSession_endpoint_url(use_ssl, service_name, not_service_name): - """Test pytest_localstack.session.RunningSession.endpoint_url.""" - if service_name == not_service_name: - pytest.skip("should not be equal") +# @pytest.mark.parametrize("use_ssl", [(True,), (False,)]) +# @pytest.mark.parametrize("service_name", sorted(constants.SERVICE_PORTS.keys())) +# @pytest.mark.parametrize("not_service_name", sorted(constants.SERVICE_PORTS.keys())) +# def test_RunningSession_endpoint_url(use_ssl, service_name, not_service_name): +# """Test pytest_localstack.session.RunningSession.endpoint_url.""" +# if service_name == not_service_name: +# pytest.skip("should not be equal") - test_session = test_utils.make_test_RunningSession( - services=[service_name], use_ssl=use_ssl - ) +# test_session = test_utils.make_test_RunningSession( +# services=[service_name], use_ssl=use_ssl +# ) - with pytest.raises(exceptions.ServiceError): - test_session.endpoint_url(not_service_name) +# with pytest.raises(exceptions.ServiceError): +# test_session.endpoint_url(not_service_name) - result = test_session.endpoint_url(service_name) - if test_session.use_ssl: - # see tests.utils.make_mock_docker_client() - assert result == "https://127.0.0.1:%i" % ( - constants.SERVICE_PORTS[service_name] - ) - else: - # see tests.utils.make_mock_docker_client() - assert result == "http://127.0.0.1:%i" % (constants.SERVICE_PORTS[service_name]) +# result = test_session.endpoint_url(service_name) +# if test_session.use_ssl: +# # see tests.utils.make_mock_docker_client() +# assert result == "https://127.0.0.1:%i" % ( +# constants.SERVICE_PORTS[service_name] +# ) +# else: +# # see tests.utils.make_mock_docker_client() +# assert result == "http://127.0.0.1:%i" % (constants.SERVICE_PORTS[service_name]) -def test_LocalstackSession_context(): - """Test pytest_localstack.session.LocalstackSession as a context manager.""" - test_session = test_utils.make_test_LocalstackSession() +# def test_LocalstackSession_context(): +# """Test pytest_localstack.session.LocalstackSession as a context manager.""" +# test_session = test_utils.make_test_LocalstackSession() - assert test_session._container is None - with test_session: - assert test_session._container is not None - time.sleep(1) # give fake like generator some time - assert test_session._container is None +# assert test_session._container is None +# with test_session: +# assert test_session._container is not None +# time.sleep(1) # give fake like generator some time +# assert test_session._container is None -def test_LocalstackSession_start_timeout(): - """Test that the LocalstackSession.start() timeout works.""" - test_session = test_utils.make_test_LocalstackSession() +# def test_LocalstackSession_start_timeout(): +# """Test that the LocalstackSession.start() timeout works.""" +# test_session = test_utils.make_test_LocalstackSession() - test_session._check_services.side_effect = exceptions.ContainerNotStartedError( - test_session - ) +# test_session._check_services.side_effect = exceptions.ContainerNotStartedError( +# test_session +# ) - with pytest.raises(exceptions.ContainerNotStartedError): - test_session.start(timeout=1) +# with pytest.raises(exceptions.ContainerNotStartedError): +# test_session.start(timeout=1) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 9374b86..c74ac69 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,66 +1,11 @@ """Unit tests for pytest_localstack.utils.""" -import os -from unittest import mock - import pytest -from hypothesis import assume, given, settings +from hypothesis import assume, given from hypothesis import strategies as st from pytest_localstack import utils -def _get_env_var(name): - return os.environ.get(name) or os.environ.get(name.upper(), "") - - -def _set_env_var(name, value): - if value is None: - os.environ.pop(name, None) - else: - os.environ[name] = value - - -@given( - http_proxy=st.sampled_from([None, "http://proxy:3128"]), - https_proxy=st.sampled_from([None, "http://proxy:3128"]), - HTTP_PROXY=st.sampled_from([None, "http://proxy:3128"]), - HTTPS_PROXY=st.sampled_from([None, "http://proxy:3128"]), - no_proxy=st.sampled_from([None, "localhost,127.0.0.1", "localhost", "foobar"]), - NO_PROXY=st.sampled_from([None, "localhost,127.0.0.1", "localhost", "foobar"]), -) -@settings(deadline=1000) -def test_check_proxy_env_vars( - http_proxy, https_proxy, HTTP_PROXY, HTTPS_PROXY, no_proxy, NO_PROXY -): - """Test pytest_localstack.utils.check_proxy_env_vars.""" - with mock.patch.dict(os.environ): - # mock.patch.dict can't delete keys. - # Patch os.environ manually. - _set_env_var("http_proxy", http_proxy) - _set_env_var("https_proxy", https_proxy) - _set_env_var("HTTP_PROXY", HTTP_PROXY) - _set_env_var("HTTPS_PROXY", HTTPS_PROXY) - _set_env_var("no_proxy", no_proxy) - _set_env_var("NO_PROXY", NO_PROXY) - - settings_match = ( - ((http_proxy or HTTP_PROXY) == (HTTP_PROXY or http_proxy)) - and ((https_proxy or HTTPS_PROXY) == (HTTPS_PROXY or https_proxy)) - and ((no_proxy or NO_PROXY) == (NO_PROXY or no_proxy)) - ) - has_http_proxy = bool(_get_env_var("http_proxy")) - has_https_proxy = bool(_get_env_var("https_proxy")) - good_no_proxy = "127.0.0.1" in _get_env_var("no_proxy") - - if (has_http_proxy or has_https_proxy) and not ( - settings_match and good_no_proxy - ): - with pytest.raises(UserWarning): - utils.check_proxy_env_vars() - else: - utils.check_proxy_env_vars() - - @given( string=st.text(), newline=st.sampled_from(["\n", "\r\n", "\r", "\n\r"]), diff --git a/tests/utils.py b/tests/utils.py index 0d44bc2..110d579 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,7 +4,7 @@ import docker -from pytest_localstack import session +from pytest_localstack import utils AWS_REGIONS = [ @@ -39,7 +39,9 @@ def make_mock_container( container = mock.Mock(spec=docker.models.containers.Container) container.labels = [] container.status = "running" - container.name = kwargs.get("name") or session.generate_container_name() + container.name = ( + kwargs.get("name") or "pytest-localstack-" + utils.generate_random_string() + ) container.id = ( "sha256:" + hashlib.sha256(container.name.encode("utf-8")).hexdigest() )