Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop py3.8 support | Replace pkg_resources lib with importlib.resources #716

Merged
merged 6 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.8', '3.11', '3.12']
python-version: ['3.11', '3.12']
toxenv: [quality, django42]

steps:
Expand All @@ -34,7 +34,7 @@ jobs:
run: tox -e ${{ matrix.toxenv }}

- name: Run Coverage
if: matrix.python-version == '3.8' && matrix.toxenv == 'django42'
if: matrix.python-version == '3.11' && matrix.toxenv == 'django42'
uses: codecov/codecov-action@v4
with:
flags: unittests
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: setup python
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: 3.11

- name: Install pip
run: pip install wheel setuptools
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ formats:
build:
os: "ubuntu-22.04"
tools:
python: "3.8"
python: "3.11"

# Optionally set the version of Python and requirements required to build your docs
python:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Change history for XBlock
Unreleased
----------

5.0.0 - 2024-05-30
------------------

* dropped python 3.8 support
* transitioned from deprecated pkg_resources lib to importlib.resources


4.1.0 - 2024-05-16
------------------

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ One Time Setup
cd XBlock

# Set up a virtualenv using virtualenvwrapper with the same name as the repo and activate it
mkvirtualenv -p python3.8 XBlock
mkvirtualenv -p python3.11 XBlock

Every time you develop something in this repo
---------------------------------------------
Expand Down
10 changes: 5 additions & 5 deletions docs/xblock-tutorial/getting_started/prereqs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ To build an XBlock, you must have the following tools on your computer.
:depth: 1


**********
Python 3.8
**********
***********
Python 3.11
***********

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

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

After you have installed Python 3.8, follow the `VirtualEnv Installation`_
After you have installed Python 3.11, follow the `VirtualEnv Installation`_
instructions.

For information on creating the virtual environment for your XBlock, see
Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ def get_version(*file_paths):
'License :: OSI Approved :: Apache Software License',
'Natural Language :: English',
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
Expand Down
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py{38,311,312}-django{42}, quality, docs
envlist = py{311,312}-django{42}, quality, docs

[pytest]
DJANGO_SETTINGS_MODULE = xblock.test.settings
Expand All @@ -22,7 +22,7 @@ allowlist_externals =

[testenv:docs]
basepython =
python3.8
python3.11
changedir =
{toxinidir}/docs
deps =
Expand Down
2 changes: 1 addition & 1 deletion xblock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
XBlock Courseware Components
"""

__version__ = '4.1.0'
__version__ = '5.0.0'
15 changes: 12 additions & 3 deletions xblock/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
import inspect
import json
import logging
import os
import warnings
from collections import OrderedDict, defaultdict

import pkg_resources
import importlib.resources
from lxml import etree
from webob import Response

Expand Down Expand Up @@ -157,7 +156,17 @@
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 cls._open_resource(uri)

@classmethod
def _open_resource(cls, uri):
return importlib.resources.files(

Check warning on line 163 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L163

Added line #L163 was not covered by tests
inspect.getmodule(cls).__package__
).joinpath(
cls.resources_dir
).joinpath(
uri
).open('rb')

@classmethod
def json_handler(cls, func):
Expand Down
6 changes: 3 additions & 3 deletions xblock/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -100,7 +100,7 @@ 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 = list(importlib.metadata.entry_points(group=cls.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)
Expand Down Expand Up @@ -133,7 +133,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:
Expand Down
17 changes: 8 additions & 9 deletions xblock/test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,10 +961,9 @@ 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 + "!"
def stub_open_resource(self, uri):
"""Act like xblock.core.Blocklike._open_resource, for testing."""
return "!" + uri + "!"

@ddt.data(
"public/hey.js",
Expand All @@ -976,7 +975,7 @@ 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('xblock.core.Blocklike._open_resource', self.stub_open_resource):
assert loadable.open_local_resource(uri) == "!" + uri + "!"
assert loadable.open_local_resource(uri.encode('utf-8')) == "!" + uri + "!"

Expand All @@ -990,7 +989,7 @@ 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('xblock.core.Blocklike._open_resource', self.stub_open_resource):
assert loadable.open_local_resource(uri) == "!" + uri.decode('utf-8') + "!"

@ddt.data(
Expand All @@ -1004,7 +1003,7 @@ 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('xblock.core.Blocklike._open_resource', self.stub_open_resource):
msg_pattern = ".*: %s" % re.escape(repr(uri))
with pytest.raises(DisallowedFileError, match=msg_pattern):
loadable.open_local_resource(uri)
Expand All @@ -1020,7 +1019,7 @@ 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('xblock.core.Blocklike._open_resource', self.stub_open_resource):
msg = ".*: %s" % re.escape(repr(uri.decode('utf-8')))
with pytest.raises(DisallowedFileError, match=msg):
loadable.open_local_resource(uri)
Expand All @@ -1043,7 +1042,7 @@ 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('xblock.core.Blocklike._open_resource', self.stub_open_resource):
msg = "not configured to serve local resources"
with pytest.raises(DisallowedFileError, match=msg):
unloadable.open_local_resource(uri)
Expand Down
6 changes: 3 additions & 3 deletions xblock/test/utils/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
19 changes: 13 additions & 6 deletions xblock/utils/resources.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
"""
Helper class (ResourceLoader) for loading resources used by an XBlock
"""

import os
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
Expand All @@ -22,8 +21,13 @@ 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')
package_name = importlib.import_module(self.module_name).__package__
# TODO: Add encoding on other places as well
# resource_path should be a relative path, but historically some callers passed it in
# with a leading slash, which pkg_resources tolerated and ignored. importlib is less
# forgiving, so in order to maintain backwards compatibility, we must strip off the
# leading slash is there is one to ensure we actually have a relative path.
return importlib.resources.files(package_name).joinpath(resource_path.lstrip('/')).read_text(encoding="utf-8")

def render_django_template(self, template_path, context=None, i18n_service=None):
"""
Expand Down Expand Up @@ -57,7 +61,10 @@ 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, '')])

package_name = importlib.import_module(self.module_name).__package__
directory = str(importlib.resources.files(package_name))
lookup = MakoTemplateLookup(directories=[directory])
template = MakoTemplate(template_str, lookup=lookup)
return template.render(**context)

Expand Down
Loading