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..b1bcda5bf 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(inspect.getmodule(cls).__package__).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..bc10c3347 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(group=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(group=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..d0b8a35a5 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(__package__) / locale_dir) domain = 'text' self.mock_translator = gettext.translation( domain, @@ -154,7 +154,7 @@ class TestResourceLoader(unittest.TestCase): """ def test_load_unicode(self): - s = ResourceLoader(__name__).load_unicode("data/simple_django_template.txt") + s = ResourceLoader(__package__).load_unicode("data/simple_django_template.txt") self.assertEqual(s, expected_string) def test_load_unicode_from_another_module(self): @@ -162,12 +162,12 @@ def test_load_unicode_from_another_module(self): self.assertEqual(s, expected_string) def test_render_django_template(self): - loader = ResourceLoader(__name__) + loader = ResourceLoader(__package__) s = loader.render_django_template("data/simple_django_template.txt", example_context) self.assertEqual(s, expected_filled_template) def test_render_django_template_translated(self): - loader = ResourceLoader(__name__) + loader = ResourceLoader(__package__) s = loader.render_django_template("data/trans_django_template.txt", context=example_context, i18n_service=MockI18nService()) @@ -179,31 +179,31 @@ def test_render_django_template_translated(self): def test_render_django_template_localized(self): # Test that default template tags like l10n are loaded - loader = ResourceLoader(__name__) + loader = ResourceLoader(__package__) s = loader.render_django_template("data/l10n_django_template.txt", context=example_context, i18n_service=MockI18nService()) self.assertEqual(s, expected_localized_template) def test_render_mako_template(self): - loader = ResourceLoader(__name__) + loader = ResourceLoader(__package__) s = loader.render_mako_template("data/simple_mako_template.txt", example_context) self.assertEqual(s, expected_filled_template) @patch('warnings.warn', DEFAULT) def test_render_template_deprecated(self, mock_warn): - loader = ResourceLoader(__name__) + loader = ResourceLoader(__package__) s = loader.render_template("data/simple_django_template.txt", example_context) self.assertTrue(mock_warn.called) self.assertEqual(s, expected_filled_template) def test_render_js_template(self): - loader = ResourceLoader(__name__) + loader = ResourceLoader(__package__) s = loader.render_js_template("data/simple_django_template.txt", example_id, example_context) self.assertEqual(s, expected_filled_js_template) def test_render_js_template_translated(self): - loader = ResourceLoader(__name__) + loader = ResourceLoader(__package__) s = loader.render_js_template("data/trans_django_template.txt", example_id, context=example_context, @@ -216,7 +216,7 @@ def test_render_js_template_translated(self): def test_render_js_template_localized(self): # Test that default template tags like l10n are loaded - loader = ResourceLoader(__name__) + loader = ResourceLoader(__package__) s = loader.render_js_template("data/l10n_django_template.txt", example_id, context=example_context, @@ -224,11 +224,11 @@ def test_render_js_template_localized(self): self.assertEqual(s, expected_filled_localized_js_template) def test_load_scenarios(self): - loader = ResourceLoader(__name__) + loader = ResourceLoader(__package__) scenarios = loader.load_scenarios_from_path("data") self.assertEqual(scenarios, expected_scenarios) def test_load_scenarios_with_identifiers(self): - loader = ResourceLoader(__name__) + loader = ResourceLoader(__package__) scenarios = loader.load_scenarios_from_path("data", include_identifier=True) self.assertEqual(scenarios, expected_scenarios_with_identifiers) 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) diff --git a/xblock/utils/studio_editable.py b/xblock/utils/studio_editable.py index b705854cb..8a12400eb 100644 --- a/xblock/utils/studio_editable.py +++ b/xblock/utils/studio_editable.py @@ -21,7 +21,7 @@ # Globals ########################################################### log = logging.getLogger(__name__) -loader = ResourceLoader(__name__) +loader = ResourceLoader(__package__) # Classes ###########################################################