diff --git a/requirements/base.in b/requirements/base.in index 07b5047d2..7bb5fed24 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -3,6 +3,7 @@ edx-opaque-keys fs +importlib-resources lxml mako markupsafe diff --git a/setup.py b/setup.py index ec7ca0f77..7731c05df 100755 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ def get_version(*file_paths): }, install_requires=[ 'fs', + 'importlib-resources', 'lxml', 'mako', 'markupsafe', diff --git a/xblock/core.py b/xblock/core.py index 2c93195ac..cfe5f8114 100644 --- a/xblock/core.py +++ b/xblock/core.py @@ -10,7 +10,7 @@ import os import warnings -import pkg_resources +import importlib_resources import xblock.exceptions from xblock.exceptions import DisallowedFileError @@ -119,7 +119,9 @@ def open_local_resource(cls, uri): if "/." in uri: raise DisallowedFileError("Only safe file names are allowed: %r" % uri) - return pkg_resources.resource_stream(cls.__module__, os.path.join(cls.resources_dir, uri)) + return importlib_resources.files(cls.__module__).joinpath( + os.path.join(cls.resources_dir, uri) + ).open('rb') # -- Base Block diff --git a/xblock/plugin.py b/xblock/plugin.py index 42f1ca6ce..633785399 100644 --- a/xblock/plugin.py +++ b/xblock/plugin.py @@ -4,9 +4,9 @@ This code is in the Runtime layer. """ import functools +import importlib.metadata import itertools import logging -import pkg_resources from xblock.internal import class_lazy @@ -100,7 +100,11 @@ def select(identifier, all_entry_points): if select is None: select = default_select - all_entry_points = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier)) + all_entry_points = [ + entry_point + for entry_point in importlib.metadata.entry_points().get(cls.entry_point, []) + if entry_point.name == identifier + ] for extra_identifier, extra_entry_point in iter(cls.extra_entry_points): if identifier == extra_identifier: all_entry_points.append(extra_entry_point) @@ -133,7 +137,7 @@ def load_classes(cls, fail_silently=True): contexts. Hence, the flag. """ all_classes = itertools.chain( - pkg_resources.iter_entry_points(cls.entry_point), + importlib.metadata.entry_points().get(cls.entry_point, []), (entry_point for identifier, entry_point in iter(cls.extra_entry_points)), ) for class_ in all_classes: diff --git a/xblock/test/test_core.py b/xblock/test/test_core.py index 7f7cd4700..723a28636 100644 --- a/xblock/test/test_core.py +++ b/xblock/test/test_core.py @@ -958,11 +958,6 @@ class UnloadableXBlock(XBlock): """Just something to load resources from.""" resources_dir = None - def stub_resource_stream(self, module, name): - """Act like pkg_resources.resource_stream, for testing.""" - assert module == "xblock.test.test_core" - return "!" + name + "!" - @ddt.data( "public/hey.js", "public/sub/hey.js", @@ -973,7 +968,8 @@ def stub_resource_stream(self, module, name): ) def test_open_good_local_resource(self, uri): loadable = self.LoadableXBlock(None, scope_ids=None) - with patch('pkg_resources.resource_stream', self.stub_resource_stream): + with patch('importlib_resources.files') as mock_files: + mock_files.return_value.joinpath.return_value.open.return_value = "!" + uri + "!" assert loadable.open_local_resource(uri) == "!" + uri + "!" assert loadable.open_local_resource(uri.encode('utf-8')) == "!" + uri + "!" @@ -987,7 +983,8 @@ def test_open_good_local_resource(self, uri): ) def test_open_good_local_resource_binary(self, uri): loadable = self.LoadableXBlock(None, scope_ids=None) - with patch('pkg_resources.resource_stream', self.stub_resource_stream): + with patch('importlib_resources.files') as mock_files: + mock_files.return_value.joinpath.return_value.open.return_value = "!" + uri.decode('utf-8') + "!" assert loadable.open_local_resource(uri) == "!" + uri.decode('utf-8') + "!" @ddt.data( @@ -1001,7 +998,8 @@ def test_open_good_local_resource_binary(self, uri): ) def test_open_bad_local_resource(self, uri): loadable = self.LoadableXBlock(None, scope_ids=None) - with patch('pkg_resources.resource_stream', self.stub_resource_stream): + with patch('importlib_resources.files') as mock_files: + mock_files.return_value.joinpath.return_value.open.return_value = "!" + uri + "!" msg_pattern = ".*: %s" % re.escape(repr(uri)) with pytest.raises(DisallowedFileError, match=msg_pattern): loadable.open_local_resource(uri) @@ -1017,7 +1015,8 @@ def test_open_bad_local_resource(self, uri): ) def test_open_bad_local_resource_binary(self, uri): loadable = self.LoadableXBlock(None, scope_ids=None) - with patch('pkg_resources.resource_stream', self.stub_resource_stream): + with patch('importlib_resources.files') as mock_files: + mock_files.return_value.joinpath.return_value.open.return_value = "!" + str(uri) + "!" msg = ".*: %s" % re.escape(repr(uri.decode('utf-8'))) with pytest.raises(DisallowedFileError, match=msg): loadable.open_local_resource(uri) @@ -1040,7 +1039,8 @@ def test_open_bad_local_resource_binary(self, uri): def test_open_local_resource_with_no_resources_dir(self, uri): unloadable = self.UnloadableXBlock(None, scope_ids=None) - with patch('pkg_resources.resource_stream', self.stub_resource_stream): + with patch('importlib_resources.files') as mock_files: + mock_files.return_value.joinpath.return_value.open.return_value = "!" + uri + "!" msg = "not configured to serve local resources" with pytest.raises(DisallowedFileError, match=msg): unloadable.open_local_resource(uri) diff --git a/xblock/test/utils/test_resources.py b/xblock/test/utils/test_resources.py index d95b0114d..fa06c4c17 100644 --- a/xblock/test/utils/test_resources.py +++ b/xblock/test/utils/test_resources.py @@ -5,9 +5,9 @@ import gettext import unittest -from unittest.mock import patch, DEFAULT +from unittest.mock import DEFAULT, patch -from pkg_resources import resource_filename +import importlib_resources from xblock.utils.resources import ResourceLoader @@ -136,7 +136,7 @@ class MockI18nService: def __init__(self): locale_dir = 'data/translations' - locale_path = resource_filename(__name__, locale_dir) + locale_path = str(importlib_resources.files(__name__) / locale_dir) domain = 'text' self.mock_translator = gettext.translation( domain, diff --git a/xblock/utils/resources.py b/xblock/utils/resources.py index 1066ffd59..ed1d8c397 100644 --- a/xblock/utils/resources.py +++ b/xblock/utils/resources.py @@ -6,8 +6,8 @@ import sys import warnings -import pkg_resources -from django.template import Context, Template, Engine +import importlib_resources +from django.template import Context, Engine, Template from django.template.backends.django import get_installed_libraries from mako.lookup import TemplateLookup as MakoTemplateLookup from mako.template import Template as MakoTemplate @@ -22,8 +22,7 @@ def load_unicode(self, resource_path): """ Gets the content of a resource """ - resource_content = pkg_resources.resource_string(self.module_name, resource_path) - return resource_content.decode('utf-8') + return importlib_resources.files(self.module_name).joinpath(resource_path).read_text() def render_django_template(self, template_path, context=None, i18n_service=None): """ @@ -57,7 +56,8 @@ def render_mako_template(self, template_path, context=None): ) context = context or {} template_str = self.load_unicode(template_path) - lookup = MakoTemplateLookup(directories=[pkg_resources.resource_filename(self.module_name, '')]) + directory = str(importlib_resources.as_file(importlib_resources.files(self.module_name) / '')) + lookup = MakoTemplateLookup(directories=[directory]) template = MakoTemplate(template_str, lookup=lookup) return template.render(**context)