diff --git a/moto/config/models.py b/moto/config/models.py index 4f1da285f..04499d8a3 100644 --- a/moto/config/models.py +++ b/moto/config/models.py @@ -1,11 +1,9 @@ -import json import re import time import random import string from datetime import datetime -import pkg_resources from boto3 import Session @@ -49,6 +47,7 @@ from moto.config.exceptions import ( from moto.core import BaseBackend, BaseModel from moto.s3.config import s3_account_public_access_block_query, s3_config_query from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID +from moto.core.responses import AWSServiceSpec from moto.iam.config import role_config_query, policy_config_query @@ -419,15 +418,13 @@ class ConfigBackend(BaseBackend): self.config_aggregators = {} self.aggregation_authorizations = {} self.organization_conformance_packs = {} + self.config_schema = None - @staticmethod - def _validate_resource_types(resource_list): - # Load the service file: - resource_package = "botocore" - resource_path = "/".join(("data", "config", "2014-11-12", "service-2.json")) - config_schema = json.loads( - pkg_resources.resource_string(resource_package, resource_path) - ) + def _validate_resource_types(self, resource_list): + if not self.config_schema: + self.config_schema = AWSServiceSpec( + path="data/config/2014-11-12/service-2.json" + ) # Verify that each entry exists in the supported list: bad_list = [] @@ -435,31 +432,28 @@ class ConfigBackend(BaseBackend): # For PY2: r_str = str(resource) - if r_str not in config_schema["shapes"]["ResourceType"]["enum"]: + if r_str not in self.config_schema.shapes["ResourceType"]["enum"]: bad_list.append(r_str) if bad_list: raise InvalidResourceTypeException( - bad_list, config_schema["shapes"]["ResourceType"]["enum"] + bad_list, self.config_schema.shapes["ResourceType"]["enum"] ) - @staticmethod - def _validate_delivery_snapshot_properties(properties): - # Load the service file: - resource_package = "botocore" - resource_path = "/".join(("data", "config", "2014-11-12", "service-2.json")) - conifg_schema = json.loads( - pkg_resources.resource_string(resource_package, resource_path) - ) + def _validate_delivery_snapshot_properties(self, properties): + if not self.config_schema: + self.config_schema = AWSServiceSpec( + path="data/config/2014-11-12/service-2.json" + ) # Verify that the deliveryFrequency is set to an acceptable value: if ( properties.get("deliveryFrequency", None) - not in conifg_schema["shapes"]["MaximumExecutionFrequency"]["enum"] + not in self.config_schema.shapes["MaximumExecutionFrequency"]["enum"] ): raise InvalidDeliveryFrequency( properties.get("deliveryFrequency", None), - conifg_schema["shapes"]["MaximumExecutionFrequency"]["enum"], + self.config_schema.shapes["MaximumExecutionFrequency"]["enum"], ) def put_configuration_aggregator(self, config_aggregator, region): diff --git a/moto/core/models.py b/moto/core/models.py index 06ec14d51..8ce0f8e49 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -5,17 +5,18 @@ from __future__ import absolute_import import functools import inspect import os -import pkg_resources import re import types from abc import abstractmethod from io import BytesIO from collections import defaultdict + from botocore.config import Config from botocore.handlers import BUILTIN_HANDLERS from botocore.awsrequest import AWSResponse from distutils.version import LooseVersion from http.client import responses as http_responses +from importlib_metadata import version from urllib.parse import urlparse from werkzeug.wrappers import Request @@ -30,7 +31,6 @@ from .utils import ( ) ACCOUNT_ID = os.environ.get("MOTO_ACCOUNT_ID", "123456789012") -RESPONSES_VERSION = pkg_resources.get_distribution("responses").version class BaseMockAWS: @@ -306,6 +306,7 @@ def _find_first_match(self, request): # - First request matches on the appropriate S3 URL # - Same request, executed again, will be matched on the subsequent match, which happens to be the catch-all, not-yet-implemented, callback # Fix: Always return the first match +RESPONSES_VERSION = version("responses") if LooseVersion(RESPONSES_VERSION) < LooseVersion("0.12.1"): responses_mock._find_match = types.MethodType( _find_first_match_legacy, responses_mock diff --git a/moto/core/responses.py b/moto/core/responses.py index 9765b04b4..a53611dfb 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -6,7 +6,6 @@ import datetime import json import logging import re -import io import requests import pytz @@ -23,6 +22,7 @@ from werkzeug.exceptions import HTTPException import boto3 from collections import OrderedDict from moto.core.utils import camelcase_to_underscores, method_names_from_class +from moto.utilities.utils import load_resource from moto import settings log = logging.getLogger(__name__) @@ -903,12 +903,8 @@ class AWSServiceSpec(object): """ def __init__(self, path): - # Importing pkg_resources takes ~60ms; keep it local - from pkg_resources import resource_filename # noqa + spec = load_resource("botocore", path) - self.path = resource_filename("botocore", path) - with io.open(self.path, "r", encoding="utf-8") as f: - spec = json.load(f) self.metadata = spec["metadata"] self.operations = spec["operations"] self.shapes = spec["shapes"] diff --git a/moto/dynamodb2/parsing/reserved_keywords.py b/moto/dynamodb2/parsing/reserved_keywords.py index 6a14baacc..7fa8ddb15 100644 --- a/moto/dynamodb2/parsing/reserved_keywords.py +++ b/moto/dynamodb2/parsing/reserved_keywords.py @@ -1,5 +1,4 @@ from moto.utilities.utils import load_resource -from pkg_resources import resource_filename class ReservedKeywords(list): @@ -23,6 +22,6 @@ class ReservedKeywords(list): Get a list of reserved keywords of DynamoDB """ reserved_keywords = load_resource( - resource_filename(__name__, "reserved_keywords.txt"), as_json=False + __name__, "reserved_keywords.txt", as_json=False ) return reserved_keywords.split() diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 0546f790a..1454e1a1c 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -5,11 +5,11 @@ import itertools import ipaddress import json import os +import pathlib import re import warnings from boto3 import Session -from pkg_resources import resource_filename from collections import defaultdict import weakref @@ -173,30 +173,25 @@ from .utils import ( describe_tag_filter, ) +INSTANCE_TYPES = load_resource(__name__, "resources/instance_types.json") -INSTANCE_TYPES = load_resource( - resource_filename(__name__, "resources/instance_types.json") -) - +root = pathlib.Path(__file__).parent offerings_path = "resources/instance_type_offerings" INSTANCE_TYPE_OFFERINGS = {} -for location_type in listdir(resource_filename(__name__, offerings_path)): +for location_type in listdir(root / offerings_path): INSTANCE_TYPE_OFFERINGS[location_type] = {} - for region in listdir( - resource_filename(__name__, offerings_path + "/" + location_type) - ): - full_path = resource_filename( - __name__, offerings_path + "/" + location_type + "/" + region - ) + for region in listdir(root / offerings_path / location_type): + full_path = offerings_path + "/" + location_type + "/" + region INSTANCE_TYPE_OFFERINGS[location_type][ region.replace(".json", "") - ] = load_resource(full_path) + ] = load_resource(__name__, full_path) -AMIS = load_resource( - os.environ.get("MOTO_AMIS_PATH") - or resource_filename(__name__, "resources/amis.json"), -) +if "MOTO_AMIS_PATH" in os.environ: + with open(os.environ.get("MOTO_AMIS_PATH"), "r", encoding="utf-8") as f: + AMIS = json.load(f) +else: + AMIS = load_resource(__name__, "resources/amis.json") OWNER_ID = ACCOUNT_ID diff --git a/moto/support/models.py b/moto/support/models.py index 34aafdc28..4e0bd85d5 100644 --- a/moto/support/models.py +++ b/moto/support/models.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals from boto3 import Session -from pkg_resources import resource_filename from moto.core import BaseBackend from moto.utilities.utils import load_resource import datetime @@ -8,7 +7,7 @@ import random checks_json = "resources/describe_trusted_advisor_checks.json" -ADVISOR_CHECKS = load_resource(resource_filename(__name__, checks_json)) +ADVISOR_CHECKS = load_resource(__name__, checks_json) class SupportCase(object): diff --git a/moto/utilities/utils.py b/moto/utilities/utils.py index fd5c271c4..5a8bc9d7e 100644 --- a/moto/utilities/utils.py +++ b/moto/utilities/utils.py @@ -1,6 +1,7 @@ import json import random import string +import pkgutil def str2bool(v): @@ -18,15 +19,14 @@ def random_string(length=None): return random_str -def load_resource(filename, as_json=True): +def load_resource(package, resource, as_json=True): """ Open a file, and return the contents as JSON. Usage: - from pkg_resources import resource_filename - load_resource(resource_filename(__name__, "resources/file.json")) + load_resource(__name__, "resources/file.json") """ - with open(filename, "r", encoding="utf-8") as f: - return json.load(f) if as_json else f.read() + resource = pkgutil.get_data(package, resource) + return json.loads(resource) if as_json else resource.decode("utf-8") def merge_multiple_dicts(*args): diff --git a/scripts/dependency_test.sh b/scripts/dependency_test.sh index f5cdae4dc..1487fd26f 100755 --- a/scripts/dependency_test.sh +++ b/scripts/dependency_test.sh @@ -52,6 +52,9 @@ test_service() { pip install -r requirements-tests.txt > /dev/null pip install .[$service] > /dev/null 2>&1 pip install boto > /dev/null 2>&1 + if [[ $service != "xray" ]]; then + pip uninstall setuptools pkg_resources -y > /dev/null 2>&1 + fi # Restart venv - ensure these deps are loaded deactivate source ${venv_path}/bin/activate > /dev/null diff --git a/setup.py b/setup.py index 708787c9b..5593b4d12 100755 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ install_requires = [ "MarkupSafe!=2.0.0a1", # This is a Jinja2 dependency, 2.0.0a1 currently seems broken "Jinja2>=2.10.1", "more-itertools", + "importlib_metadata" ] _dep_PyYAML = "PyYAML>=5.1" @@ -52,6 +53,7 @@ _dep_aws_xray_sdk = "aws-xray-sdk!=0.96,>=0.93" _dep_idna = "idna<3,>=2.5" _dep_cfn_lint = "cfn-lint>=0.4.0" _dep_sshpubkeys = "sshpubkeys>=3.1.0" +_setuptools = "setuptools" all_extra_deps = [ _dep_PyYAML, @@ -63,6 +65,7 @@ all_extra_deps = [ _dep_idna, _dep_cfn_lint, _dep_sshpubkeys, + _setuptools, ] all_server_deps = all_extra_deps + ["flask", "flask-cors"] @@ -82,7 +85,9 @@ extras_per_service = { "sns": [], "sqs": [], "ssm": [_dep_PyYAML], - "xray": [_dep_aws_xray_sdk], + # XRay module uses pkg_resources, but doesn't have an explicit dependency listed + # This should be fixed in the next version: https://github.com/aws/aws-xray-sdk-python/issues/305 + "xray": [_dep_aws_xray_sdk, _setuptools], } # When a Table has a Stream, we'll always need to import AWSLambda to search for a corresponding function to send the table data to extras_per_service["dynamodb2"] = extras_per_service["awslambda"] diff --git a/tests/test_ssm/test_ssm_docs.py b/tests/test_ssm/test_ssm_docs.py index 9a1fb7cf4..aa0118820 100644 --- a/tests/test_ssm/test_ssm_docs.py +++ b/tests/test_ssm/test_ssm_docs.py @@ -5,19 +5,18 @@ import botocore.exceptions import sure # noqa import datetime import json -import pkg_resources import yaml import hashlib import copy +import pkgutil + from moto.core import ACCOUNT_ID from moto import mock_ssm def _get_yaml_template(): - template_path = "/".join(["test_ssm", "test_templates", "good.yaml"]) - resource_path = pkg_resources.resource_string("tests", template_path) - return resource_path + return pkgutil.get_data(__name__, "test_templates/good.yaml") def _validate_document_description(