Skip to content

Commit a63390b

Browse files
authored
Drop py3.8 support | Replace pkg_resources lib with importlib.resources (#716)
* chore: transitioned from pkg_resources api to importlib-resources api * feat!: drop support for python 3.8
1 parent 52c17a5 commit a63390b

File tree

14 files changed

+59
-38
lines changed

14 files changed

+59
-38
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
python-version: ['3.8', '3.11', '3.12']
17+
python-version: ['3.11', '3.12']
1818
toxenv: [quality, django42]
1919

2020
steps:
@@ -34,7 +34,7 @@ jobs:
3434
run: tox -e ${{ matrix.toxenv }}
3535

3636
- name: Run Coverage
37-
if: matrix.python-version == '3.8' && matrix.toxenv == 'django42'
37+
if: matrix.python-version == '3.11' && matrix.toxenv == 'django42'
3838
uses: codecov/codecov-action@v4
3939
with:
4040
flags: unittests

.github/workflows/pypi-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- name: setup python
1616
uses: actions/setup-python@v2
1717
with:
18-
python-version: 3.8
18+
python-version: 3.11
1919

2020
- name: Install pip
2121
run: pip install wheel setuptools

.readthedocs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ formats:
1919
build:
2020
os: "ubuntu-22.04"
2121
tools:
22-
python: "3.8"
22+
python: "3.11"
2323

2424
# Optionally set the version of Python and requirements required to build your docs
2525
python:

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ Change history for XBlock
55
Unreleased
66
----------
77

8+
5.0.0 - 2024-05-30
9+
------------------
10+
11+
* dropped python 3.8 support
12+
* transitioned from deprecated pkg_resources lib to importlib.resources
13+
14+
815
4.1.0 - 2024-05-16
916
------------------
1017

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ One Time Setup
4545
cd XBlock
4646
4747
# Set up a virtualenv using virtualenvwrapper with the same name as the repo and activate it
48-
mkvirtualenv -p python3.8 XBlock
48+
mkvirtualenv -p python3.11 XBlock
4949
5050
Every time you develop something in this repo
5151
---------------------------------------------

docs/xblock-tutorial/getting_started/prereqs.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ To build an XBlock, you must have the following tools on your computer.
1111
:depth: 1
1212

1313

14-
**********
15-
Python 3.8
16-
**********
14+
***********
15+
Python 3.11
16+
***********
1717

1818
To run the a virtual environment and the XBlock SDK, and to build an XBlock,
19-
you must have Python 3.8 installed on your computer.
19+
you must have Python 3.11 installed on your computer.
2020

2121
`Download Python`_ for your operating system and follow the installation
2222
instructions.
@@ -48,7 +48,7 @@ applications you might need.
4848
The instructions and examples in this tutorial use `VirtualEnv`_ and
4949
`VirtualEnvWrapper`_ to build XBlocks. You can also use `PyEnv`_.
5050

51-
After you have installed Python 3.8, follow the `VirtualEnv Installation`_
51+
After you have installed Python 3.11, follow the `VirtualEnv Installation`_
5252
instructions.
5353

5454
For information on creating the virtual environment for your XBlock, see

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ def get_version(*file_paths):
7373
'License :: OSI Approved :: Apache Software License',
7474
'Natural Language :: English',
7575
"Programming Language :: Python :: 3",
76-
"Programming Language :: Python :: 3.8",
7776
"Programming Language :: Python :: 3.11",
7877
"Programming Language :: Python :: 3.12",
7978
]

tox.ini

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py{38,311,312}-django{42}, quality, docs
2+
envlist = py{311,312}-django{42}, quality, docs
33

44
[pytest]
55
DJANGO_SETTINGS_MODULE = xblock.test.settings
@@ -22,7 +22,7 @@ allowlist_externals =
2222

2323
[testenv:docs]
2424
basepython =
25-
python3.8
25+
python3.11
2626
changedir =
2727
{toxinidir}/docs
2828
deps =

xblock/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
XBlock Courseware Components
33
"""
44

5-
__version__ = '4.1.0'
5+
__version__ = '5.0.0'

xblock/core.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
import inspect
77
import json
88
import logging
9-
import os
109
import warnings
1110
from collections import OrderedDict, defaultdict
1211

13-
import pkg_resources
12+
import importlib.resources
1413
from lxml import etree
1514
from webob import Response
1615

@@ -157,7 +156,17 @@ def open_local_resource(cls, uri):
157156
if "/." in uri:
158157
raise DisallowedFileError("Only safe file names are allowed: %r" % uri)
159158

160-
return pkg_resources.resource_stream(cls.__module__, os.path.join(cls.resources_dir, uri))
159+
return cls._open_resource(uri)
160+
161+
@classmethod
162+
def _open_resource(cls, uri):
163+
return importlib.resources.files(
164+
inspect.getmodule(cls).__package__
165+
).joinpath(
166+
cls.resources_dir
167+
).joinpath(
168+
uri
169+
).open('rb')
161170

162171
@classmethod
163172
def json_handler(cls, func):

xblock/plugin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
This code is in the Runtime layer.
55
"""
66
import functools
7+
import importlib.metadata
78
import itertools
89
import logging
9-
import pkg_resources
1010

1111
from xblock.internal import class_lazy
1212

@@ -100,7 +100,7 @@ def select(identifier, all_entry_points):
100100
if select is None:
101101
select = default_select
102102

103-
all_entry_points = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier))
103+
all_entry_points = list(importlib.metadata.entry_points(group=cls.entry_point, name=identifier))
104104
for extra_identifier, extra_entry_point in iter(cls.extra_entry_points):
105105
if identifier == extra_identifier:
106106
all_entry_points.append(extra_entry_point)
@@ -133,7 +133,7 @@ def load_classes(cls, fail_silently=True):
133133
contexts. Hence, the flag.
134134
"""
135135
all_classes = itertools.chain(
136-
pkg_resources.iter_entry_points(cls.entry_point),
136+
importlib.metadata.entry_points(group=cls.entry_point),
137137
(entry_point for identifier, entry_point in iter(cls.extra_entry_points)),
138138
)
139139
for class_ in all_classes:

xblock/test/test_core.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -961,10 +961,9 @@ class UnloadableXBlock(XBlock):
961961
"""Just something to load resources from."""
962962
resources_dir = None
963963

964-
def stub_resource_stream(self, module, name):
965-
"""Act like pkg_resources.resource_stream, for testing."""
966-
assert module == "xblock.test.test_core"
967-
return "!" + name + "!"
964+
def stub_open_resource(self, uri):
965+
"""Act like xblock.core.Blocklike._open_resource, for testing."""
966+
return "!" + uri + "!"
968967

969968
@ddt.data(
970969
"public/hey.js",
@@ -976,7 +975,7 @@ def stub_resource_stream(self, module, name):
976975
)
977976
def test_open_good_local_resource(self, uri):
978977
loadable = self.LoadableXBlock(None, scope_ids=Mock())
979-
with patch('pkg_resources.resource_stream', self.stub_resource_stream):
978+
with patch('xblock.core.Blocklike._open_resource', self.stub_open_resource):
980979
assert loadable.open_local_resource(uri) == "!" + uri + "!"
981980
assert loadable.open_local_resource(uri.encode('utf-8')) == "!" + uri + "!"
982981

@@ -990,7 +989,7 @@ def test_open_good_local_resource(self, uri):
990989
)
991990
def test_open_good_local_resource_binary(self, uri):
992991
loadable = self.LoadableXBlock(None, scope_ids=Mock())
993-
with patch('pkg_resources.resource_stream', self.stub_resource_stream):
992+
with patch('xblock.core.Blocklike._open_resource', self.stub_open_resource):
994993
assert loadable.open_local_resource(uri) == "!" + uri.decode('utf-8') + "!"
995994

996995
@ddt.data(
@@ -1004,7 +1003,7 @@ def test_open_good_local_resource_binary(self, uri):
10041003
)
10051004
def test_open_bad_local_resource(self, uri):
10061005
loadable = self.LoadableXBlock(None, scope_ids=Mock())
1007-
with patch('pkg_resources.resource_stream', self.stub_resource_stream):
1006+
with patch('xblock.core.Blocklike._open_resource', self.stub_open_resource):
10081007
msg_pattern = ".*: %s" % re.escape(repr(uri))
10091008
with pytest.raises(DisallowedFileError, match=msg_pattern):
10101009
loadable.open_local_resource(uri)
@@ -1020,7 +1019,7 @@ def test_open_bad_local_resource(self, uri):
10201019
)
10211020
def test_open_bad_local_resource_binary(self, uri):
10221021
loadable = self.LoadableXBlock(None, scope_ids=Mock())
1023-
with patch('pkg_resources.resource_stream', self.stub_resource_stream):
1022+
with patch('xblock.core.Blocklike._open_resource', self.stub_open_resource):
10241023
msg = ".*: %s" % re.escape(repr(uri.decode('utf-8')))
10251024
with pytest.raises(DisallowedFileError, match=msg):
10261025
loadable.open_local_resource(uri)
@@ -1043,7 +1042,7 @@ def test_open_bad_local_resource_binary(self, uri):
10431042
def test_open_local_resource_with_no_resources_dir(self, uri):
10441043
unloadable = self.UnloadableXBlock(None, scope_ids=Mock())
10451044

1046-
with patch('pkg_resources.resource_stream', self.stub_resource_stream):
1045+
with patch('xblock.core.Blocklike._open_resource', self.stub_open_resource):
10471046
msg = "not configured to serve local resources"
10481047
with pytest.raises(DisallowedFileError, match=msg):
10491048
unloadable.open_local_resource(uri)

xblock/test/utils/test_resources.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
import gettext
77
import unittest
8-
from unittest.mock import patch, DEFAULT
8+
from unittest.mock import DEFAULT, patch
99

10-
from pkg_resources import resource_filename
10+
import importlib.resources
1111

1212
from xblock.utils.resources import ResourceLoader
1313

@@ -136,7 +136,7 @@ class MockI18nService:
136136
def __init__(self):
137137

138138
locale_dir = 'data/translations'
139-
locale_path = resource_filename(__name__, locale_dir)
139+
locale_path = str(importlib.resources.files(__package__) / locale_dir)
140140
domain = 'text'
141141
self.mock_translator = gettext.translation(
142142
domain,

xblock/utils/resources.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
"""
22
Helper class (ResourceLoader) for loading resources used by an XBlock
33
"""
4-
54
import os
65
import sys
76
import warnings
87

9-
import pkg_resources
10-
from django.template import Context, Template, Engine
8+
import importlib.resources
9+
from django.template import Context, Engine, Template
1110
from django.template.backends.django import get_installed_libraries
1211
from mako.lookup import TemplateLookup as MakoTemplateLookup
1312
from mako.template import Template as MakoTemplate
@@ -22,8 +21,13 @@ def load_unicode(self, resource_path):
2221
"""
2322
Gets the content of a resource
2423
"""
25-
resource_content = pkg_resources.resource_string(self.module_name, resource_path)
26-
return resource_content.decode('utf-8')
24+
package_name = importlib.import_module(self.module_name).__package__
25+
# TODO: Add encoding on other places as well
26+
# resource_path should be a relative path, but historically some callers passed it in
27+
# with a leading slash, which pkg_resources tolerated and ignored. importlib is less
28+
# forgiving, so in order to maintain backwards compatibility, we must strip off the
29+
# leading slash is there is one to ensure we actually have a relative path.
30+
return importlib.resources.files(package_name).joinpath(resource_path.lstrip('/')).read_text(encoding="utf-8")
2731

2832
def render_django_template(self, template_path, context=None, i18n_service=None):
2933
"""
@@ -57,7 +61,10 @@ def render_mako_template(self, template_path, context=None):
5761
)
5862
context = context or {}
5963
template_str = self.load_unicode(template_path)
60-
lookup = MakoTemplateLookup(directories=[pkg_resources.resource_filename(self.module_name, '')])
64+
65+
package_name = importlib.import_module(self.module_name).__package__
66+
directory = str(importlib.resources.files(package_name))
67+
lookup = MakoTemplateLookup(directories=[directory])
6168
template = MakoTemplate(template_str, lookup=lookup)
6269
return template.render(**context)
6370

0 commit comments

Comments
 (0)