diff --git a/xblock/__init__.py b/xblock/__init__.py index e679b053b..6ac9bd16f 100644 --- a/xblock/__init__.py +++ b/xblock/__init__.py @@ -2,4 +2,4 @@ XBlock Courseware Components """ -__version__ = '4.1.0' +__version__ = '4.1.1' diff --git a/xblock/core.py b/xblock/core.py index fcea0f597..795a3d2ba 100644 --- a/xblock/core.py +++ b/xblock/core.py @@ -10,7 +10,7 @@ import warnings from collections import OrderedDict, defaultdict -import pkg_resources +import importlib.resources from lxml import etree from webob import Response @@ -157,7 +157,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') @classmethod def json_handler(cls, func): 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 c6a6fed10..e559b380a 100644 --- a/xblock/test/test_core.py +++ b/xblock/test/test_core.py @@ -961,11 +961,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", @@ -976,7 +971,8 @@ def stub_resource_stream(self, module, name): ) def test_open_good_local_resource(self, uri): loadable = self.LoadableXBlock(None, scope_ids=Mock()) - 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 + "!" @@ -990,7 +986,8 @@ def test_open_good_local_resource(self, uri): ) def test_open_good_local_resource_binary(self, uri): loadable = self.LoadableXBlock(None, scope_ids=Mock()) - 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( @@ -1004,7 +1001,8 @@ def test_open_good_local_resource_binary(self, uri): ) def test_open_bad_local_resource(self, uri): loadable = self.LoadableXBlock(None, scope_ids=Mock()) - 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) @@ -1020,7 +1018,8 @@ def test_open_bad_local_resource(self, uri): ) def test_open_bad_local_resource_binary(self, uri): loadable = self.LoadableXBlock(None, scope_ids=Mock()) - 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) @@ -1043,7 +1042,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=Mock()) - 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..b5199d7ba 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..b48bb26fb 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)