diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51bb5a1..1ba45c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,56 +2,41 @@ name: Python CI on: push: - branches: - - master + branches: [ master ] pull_request: branches: - '**' + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true jobs: - run_tests: - name: Tests + tests: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-20.04] python-version: [3.8] - toxenv: [ quality, unit, integration ] - steps: - - name: Checkout Repo - uses: actions/checkout@v2 + toxenv: [django32, django42, quality, package] - - name: Install Required System Packages - if: matrix.toxenv == 'integration' - run: sudo apt-get update && sudo apt-get install libxmlsec1-dev ubuntu-restricted-extras xvfb - - - name: Use Gecko Driver - if: matrix.toxenv == 'integration' - uses: browser-actions/setup-geckodriver@latest + steps: + - name: checkout repo + uses: actions/checkout@v3 + with: + submodules: recursive - name: setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - pip install -U pip wheel - pip install tox pylint + - name: Install Dependencies + run: make requirements - - name: Run Quality - env: - TOXENV: ${{ matrix.toxenv }} - if: matrix.toxenv == 'quality' - run: | - tox -e quality - - name: Run Unit Tests - env: - TOXENV: ${{ matrix.toxenv }} - if: matrix.toxenv == 'unit' - run: tox -e unit - - name: Run Integration Tests + - name: Run Tests env: TOXENV: ${{ matrix.toxenv }} - if: matrix.toxenv == 'integration' - run: xvfb-run --auto-servernum tox -e integration + run: tox diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 0000000..527dc58 --- /dev/null +++ b/.github/workflows/pypi-publish.yml @@ -0,0 +1,30 @@ +name: Publish package to PyPI + +on: + release: + types: [published] + +jobs: + push: + runs-on: ubuntu-20.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: setup python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Install Dependencies + run: pip install -r requirements/pip.txt + + - name: Build package + run: python setup.py sdist bdist_wheel + + - name: Publish to PyPi + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_UPLOAD_TOKEN }} diff --git a/.github/workflows/upgrade-python-requirements.yml b/.github/workflows/upgrade-python-requirements.yml new file mode 100644 index 0000000..dc92eef --- /dev/null +++ b/.github/workflows/upgrade-python-requirements.yml @@ -0,0 +1,29 @@ +name: Upgrade Python Requirements + +on: + schedule: + - cron: "0 0 * * 1" + workflow_dispatch: + inputs: + branch: + description: Target branch against which to create requirements PR + required: true + default: master + +jobs: + call-upgrade-python-requirements-workflow: + uses: openedx/.github/.github/workflows/upgrade-python-requirements.yml@master + # Do not run on forks + if: github.repository_owner == 'openedx' + with: + branch: ${{ github.event.inputs.branch || 'master' }} + # optional parameters below; fill in if you'd like github or email notifications + # user_reviewers: "" + # team_reviewers: "" + # email_address: "" + # send_success_notification: false + secrets: + requirements_bot_github_token: ${{ secrets.REQUIREMENTS_BOT_GITHUB_TOKEN }} + requirements_bot_github_email: ${{ secrets.REQUIREMENTS_BOT_GITHUB_EMAIL }} + edx_smtp_username: ${{ secrets.EDX_SMTP_USERNAME }} + edx_smtp_password: ${{ secrets.EDX_SMTP_PASSWORD }} diff --git a/.gitignore b/.gitignore index 8a50c92..ef8a3d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,70 @@ -xblock_image_explorer.egg-info -*.pyc -*~ -var/workbench.log* -geckodriver.log +*.py[cod] +__pycache__ + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +env +venv +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.cache/ +.coverage +.coverage.* +.pytest_cache .tox +coverage.xml +htmlcov/ +diff-cover.html +pii_report + +# Translations +*.mo + +# IDEs and text editors +*~ +*.swp +.idea/ +.project +.pycharm_helpers/ +.pydevproject + +# The Silver Searcher +.agignore + +# OS X artifacts +*.DS_Store + +# Logging +log/ +logs/ +chromedriver.log +ghostdriver.log + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +docs/_build + +# Cookiecutter +output/ +dj-package/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a1a2b11 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE +include README.md +include requirements/base.in +include requirements/constraints.txt diff --git a/Makefile b/Makefile index 86391f5..545958b 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,32 @@ clean: ## remove generated byte code, coverage reports, and build artifacts rm -fr dist/ rm -fr *.egg-info +piptools: ## install pinned version of pip-compile and pip-sync + pip install -r requirements/pip.txt + pip install -r requirements/pip-tools.txt + +requirements: piptools ## install test requirements locally + pip-sync requirements/ci.txt + +requirements_python: piptools ## install all requirements locally + pip-sync requirements/base.txt requirements/ci.txt requirements/test.txt requirements/private.* + +# Define PIP_COMPILE_OPTS=-v to get more information during make upgrade. +PIP_COMPILE = pip-compile --upgrade $(PIP_COMPILE_OPTS) + +upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade +upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in + pip install -qr requirements/pip-tools.txt + # Make sure to compile files after any other files they include! + $(PIP_COMPILE) --allow-unsafe -o requirements/pip.txt requirements/pip.in + $(PIP_COMPILE) -o requirements/pip-tools.txt requirements/pip-tools.in + pip install -qr requirements/pip.txt + pip install -qr requirements/pip-tools.txt + $(PIP_COMPILE) -o requirements/base.txt requirements/base.in + $(PIP_COMPILE) -o requirements/test.txt requirements/test.in + $(PIP_COMPILE) -o requirements/ci.txt requirements/ci.in + sed -i '/^[dD]jango==/d' requirements/test.txt + ## Localization targets extract_translations: ## extract strings to be translated, outputting .po files @@ -49,7 +75,7 @@ build_dummy_translations: dummy_translations compile_translations ## generate an validate_translations: build_dummy_translations detect_changed_source_translations ## validate translations pull_translations: ## pull translations from transifex - cd $(WORKING_DIR) && i18n_tool transifex pull + tx pull -t -a -f --mode reviewed --minimum-perc=1 push_translations: ## push translations to transifex - cd $(WORKING_DIR) && i18n_tool transifex push + tx push -s diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..29992e9 --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,30 @@ +# This file records information about this repo. Its use is described in OEP-55: +# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html + +apiVersion: backstage.io/v1alpha1 +# (Required) Acceptable Values: Component, Resource, System +# A repo will almost certainly be a Component. +kind: Component +metadata: + name: xblock-image-explorer + description: This XBlock allows you to use an image with hotspots in a course. When the student clicks a hotspot icon, a tooltip containing custom content is displayed. + annotations: + # (Optional) Annotation keys and values can be whatever you want. + # We use it in Open edX repos to have a comma-separated list of GitHub user + # names that might be interested in changes to the architecture of this + # component. + openedx.org/component-type: XBlock + openedx.org/arch-interest-groups: '' +spec: + + # (Required) This can be a group (`group:`) or a user (`user:`). + # Don't forget the "user:" or "group:" prefix. Groups must be GitHub team + # names in the openedx GitHub organization: https://github.com/orgs/openedx/teams + + owner: user:Agrendalath + + # (Required) Acceptable Type Values: service, website, library + type: library + + # (Required) Acceptable Lifecycle Values: experimental, production, deprecated + lifecycle: production diff --git a/image_explorer/__init__.py b/image_explorer/__init__.py index 3f66b9a..51a5d59 100644 --- a/image_explorer/__init__.py +++ b/image_explorer/__init__.py @@ -20,6 +20,6 @@ """ Image Explorer XBlock """ -from __future__ import absolute_import - from .image_explorer import ImageExplorerBlock + +__version__ = '2.2.0' diff --git a/image_explorer/image_explorer.py b/image_explorer/image_explorer.py index 0eb6fc2..a3075a2 100644 --- a/image_explorer/image_explorer.py +++ b/image_explorer/image_explorer.py @@ -28,14 +28,13 @@ import uuid import logging import textwrap +from io import StringIO +from urllib.parse import urljoin import pkg_resources -from six.moves import urllib -from six import StringIO -from parsel import Selector -from lxml import etree, html from django.conf import settings - +from lxml import etree, html +from parsel import Selector from xblock.completable import XBlockCompletionMode from xblock.core import XBlock from xblock.fragment import Fragment @@ -44,11 +43,11 @@ from .utils import loader, AttrDict, _ -log = logging.getLogger(__name__) # pylint: disable=invalid-name +log = logging.getLogger(__name__) @XBlock.needs('i18n') -class ImageExplorerBlock(XBlock): # pylint: disable=no-init +class ImageExplorerBlock(XBlock): """ XBlock that renders an image with tooltips """ @@ -114,7 +113,7 @@ class ImageExplorerBlock(XBlock): # pylint: disable=no-init """)) - def max_score(self): # pylint: disable=no-self-use + def max_score(self): """ Returns the maximum score that can be achieved (always 1.0 on this XBlock) """ @@ -254,7 +253,7 @@ def register_progress(self, hotspot_id): self.runtime.publish(self, 'progress', {}) self.opened_hotspots.append(hotspot_id) - log.debug(u'Opened hotspots so far for %s: %s', self._get_unique_id(), self.opened_hotspots) + log.debug('Opened hotspots so far for %s: %s', self._get_unique_id(), self.opened_hotspots) opened_hotspots = [h for h in hotspots_ids if h in self.opened_hotspots] percent_completion = float(len(opened_hotspots)) / len(hotspots_ids) @@ -262,7 +261,7 @@ def register_progress(self, hotspot_id): 'value': percent_completion, 'max_value': 1, }) - log.debug(u'Sending grade for %s: %s', self._get_unique_id(), percent_completion) + log.debug('Sending grade for %s: %s', self._get_unique_id(), percent_completion) def _get_unique_id(self): try: @@ -304,7 +303,7 @@ def studio_submit(self, submissions, suffix=''): # Python 2 and 3 compatibility fix # Switch to _, error_message = e.args try: - error_message = err.message # pylint: disable=exception-message-attribute + error_message = err.message except: # pylint: disable=bare-except _, error_message = err.args @@ -347,7 +346,7 @@ def _make_url_absolute(url): lms_base = settings.ENV_TOKENS.get('LMS_BASE') scheme = 'https' if settings.HTTPS == 'on' else 'http' lms_base = '{}://{}'.format(scheme, lms_base) - return urllib.parse.urljoin(lms_base, url) + return urljoin(lms_base, url) def _inner_content(self, tag, absolute_urls=False): """ @@ -464,7 +463,7 @@ def workbench_scenarios(): """A canned scenario for display in the workbench.""" return [("Image explorer scenario", "")] - def resource_string(self, path): # pylint: disable=no-self-use + def resource_string(self, path): """Handy helper for getting resources from our kit.""" data = pkg_resources.resource_string(__name__, path) return data.decode("utf8") diff --git a/image_explorer/utils.py b/image_explorer/utils.py index 97f75a8..210b0c8 100644 --- a/image_explorer/utils.py +++ b/image_explorer/utils.py @@ -49,5 +49,5 @@ class AttrDict(dict): Attribute Dictionary for storing properties """ def __init__(self, *args, **kwargs): - super(AttrDict, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.__dict__ = self diff --git a/pylintrc b/pylintrc index 65b664a..5acb516 100644 --- a/pylintrc +++ b/pylintrc @@ -22,7 +22,5 @@ min-similarity-lines=4 good-names=_,__,log,loader,x,y,id method-rgx=_?[a-z_][a-z0-9_]{2,40}$ function-rgx=_?[a-z_][a-z0-9_]{2,40}$ -method-name-hint=_?[a-z_][a-z0-9_]{2,40}$ -function-name-hint=_?[a-z_][a-z0-9_]{2,40}$ extension-pkg-whitelist=lxml diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index dcd2737..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ --e . --e git+https://github.com/openedx/xblock-utils.git@2.1.1#egg=xblock-utils==2.1.1 -edx-i18n-tools==0.5.3 -lxml==4.5.2 -mako==1.1.3 -parsel==1.6.0 -transifex-client==0.13.11 diff --git a/requirements/base.in b/requirements/base.in new file mode 100644 index 0000000..99aae93 --- /dev/null +++ b/requirements/base.in @@ -0,0 +1,5 @@ +# Core requirements for using this application +-c constraints.txt + +xblock[django] +parsel diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..d03d435 --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,88 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +appdirs==1.4.4 + # via fs +asgiref==3.7.2 + # via django +boto3==1.34.20 + # via fs-s3fs +botocore==1.34.20 + # via + # boto3 + # s3transfer +cssselect==1.2.0 + # via parsel +django==3.2.23 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # openedx-django-pyfs +fs==2.4.16 + # via + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via openedx-django-pyfs +jmespath==1.0.1 + # via + # boto3 + # botocore + # parsel +lazy==1.6 + # via xblock +lxml==5.1.0 + # via + # parsel + # xblock +mako==1.3.0 + # via xblock +markupsafe==2.1.3 + # via + # mako + # xblock +openedx-django-pyfs==3.4.1 + # via xblock +packaging==23.2 + # via parsel +parsel==1.8.1 + # via -r requirements/base.in +python-dateutil==2.8.2 + # via + # botocore + # xblock +pytz==2023.3.post1 + # via + # django + # xblock +pyyaml==6.0.1 + # via xblock +s3transfer==0.10.0 + # via boto3 +simplejson==3.19.2 + # via xblock +six==1.16.0 + # via + # fs + # fs-s3fs + # python-dateutil +sqlparse==0.4.4 + # via django +typing-extensions==4.9.0 + # via asgiref +urllib3==1.26.18 + # via botocore +w3lib==2.1.2 + # via parsel +web-fragments==2.1.0 + # via xblock +webob==1.8.7 + # via xblock +xblock[django]==1.9.1 + # via -r requirements/base.in + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/ci.in b/requirements/ci.in new file mode 100644 index 0000000..3c1c718 --- /dev/null +++ b/requirements/ci.in @@ -0,0 +1,4 @@ +# Requirements for running tests in CI +-c constraints.txt + +tox # Virtualenv management for tests diff --git a/requirements/ci.txt b/requirements/ci.txt new file mode 100644 index 0000000..3b6356c --- /dev/null +++ b/requirements/ci.txt @@ -0,0 +1,38 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +cachetools==5.3.2 + # via tox +chardet==5.2.0 + # via tox +colorama==0.4.6 + # via tox +distlib==0.3.8 + # via virtualenv +filelock==3.13.1 + # via + # tox + # virtualenv +packaging==23.2 + # via + # pyproject-api + # tox +platformdirs==4.1.0 + # via + # tox + # virtualenv +pluggy==1.3.0 + # via tox +pyproject-api==1.6.1 + # via tox +tomli==2.0.1 + # via + # pyproject-api + # tox +tox==4.12.0 + # via -r requirements/ci.in +virtualenv==20.25.0 + # via tox diff --git a/requirements/constraints.txt b/requirements/constraints.txt new file mode 100644 index 0000000..a737d6f --- /dev/null +++ b/requirements/constraints.txt @@ -0,0 +1,12 @@ +# Version constraints for pip-installation. +# +# This file doesn't install any packages. It specifies version constraints +# that will be applied if a package is needed. +# +# When pinning something here, please provide an explanation of why. Ideally, +# link to other information that will help people in the future to remove the +# pin when possible. Writing an issue against the offending project and +# linking to it here is good. + +# Common constraints for edx repos +-c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt diff --git a/requirements/pip-tools.in b/requirements/pip-tools.in new file mode 100644 index 0000000..0e88226 --- /dev/null +++ b/requirements/pip-tools.in @@ -0,0 +1,31 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +build==1.0.3 + # via pip-tools +click==8.1.7 + # via pip-tools +importlib-metadata==7.0.1 + # via build +packaging==23.2 + # via build +pip-tools==7.3.0 + # via -r requirements/pip-tools.in +pyproject-hooks==1.0.0 + # via build +tomli==2.0.1 + # via + # build + # pip-tools + # pyproject-hooks +wheel==0.42.0 + # via pip-tools +zipp==3.17.0 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt new file mode 100644 index 0000000..f4e8140 --- /dev/null +++ b/requirements/pip-tools.txt @@ -0,0 +1,46 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +build==1.0.3 + # via + # -r requirements/pip-tools.in + # pip-tools +click==8.1.7 + # via + # -r requirements/pip-tools.in + # pip-tools +importlib-metadata==7.0.1 + # via + # -r requirements/pip-tools.in + # build +packaging==23.2 + # via + # -r requirements/pip-tools.in + # build +pip-tools==7.3.0 + # via -r requirements/pip-tools.in +pyproject-hooks==1.0.0 + # via + # -r requirements/pip-tools.in + # build +tomli==2.0.1 + # via + # -r requirements/pip-tools.in + # build + # pip-tools + # pyproject-hooks +wheel==0.42.0 + # via + # -r requirements/pip-tools.in + # pip-tools +zipp==3.17.0 + # via + # -r requirements/pip-tools.in + # importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements/pip.in b/requirements/pip.in new file mode 100644 index 0000000..716c6f2 --- /dev/null +++ b/requirements/pip.in @@ -0,0 +1,6 @@ +# Core dependencies for installing other packages +-c constraints.txt + +pip +setuptools +wheel diff --git a/requirements/pip.txt b/requirements/pip.txt new file mode 100644 index 0000000..a4cf530 --- /dev/null +++ b/requirements/pip.txt @@ -0,0 +1,14 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +wheel==0.42.0 + # via -r requirements/pip.in + +# The following packages are considered to be unsafe in a requirements file: +pip==23.3.2 + # via -r requirements/pip.in +setuptools==69.0.3 + # via -r requirements/pip.in diff --git a/requirements/private.readme b/requirements/private.readme new file mode 100644 index 0000000..5600a10 --- /dev/null +++ b/requirements/private.readme @@ -0,0 +1,15 @@ +# If there are any Python packages you want to keep in your virtualenv beyond +# those listed in the official requirements files, create a "private.in" file +# and list them there. Generate the corresponding "private.txt" file pinning +# all of their indirect dependencies to specific versions as follows: + +# pip-compile private.in + +# This allows you to use "pip-sync" without removing these packages: + +# pip-sync requirements/*.txt + +# "private.in" and "private.txt" aren't checked into git to avoid merge +# conflicts, and the presence of this file allows "private.*" to be +# included in scripted pip-sync usage without requiring that those files be +# created first. diff --git a/requirements/test.in b/requirements/test.in new file mode 100644 index 0000000..da2c669 --- /dev/null +++ b/requirements/test.in @@ -0,0 +1,11 @@ +# Requirements for test runs. +-c constraints.txt + +-r base.txt # Core dependencies for this package + +mock # required by the workbench +pytest-cov # pytest extension for code coverage statistics +pytest-django # pytest extension for better Django support +xblock-sdk # workbench +edx-lint # edX pylint rules and plugins +pycodestyle # PEP 8 compliance validation diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 0000000..78b3375 --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,264 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +appdirs==1.4.4 + # via + # -r requirements/base.txt + # fs +arrow==1.3.0 + # via cookiecutter +asgiref==3.7.2 + # via + # -r requirements/base.txt + # django +astroid==3.0.2 + # via + # pylint + # pylint-celery +binaryornot==0.4.4 + # via cookiecutter +boto3==1.34.20 + # via + # -r requirements/base.txt + # fs-s3fs +botocore==1.34.20 + # via + # -r requirements/base.txt + # boto3 + # s3transfer +certifi==2023.11.17 + # via requests +chardet==5.2.0 + # via binaryornot +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via + # click-log + # code-annotations + # cookiecutter + # edx-lint +click-log==0.4.0 + # via edx-lint +code-annotations==1.5.0 + # via edx-lint +cookiecutter==2.5.0 + # via xblock-sdk +coverage[toml]==7.4.0 + # via + # coverage + # pytest-cov +cssselect==1.2.0 + # via + # -r requirements/base.txt + # parsel +dill==0.3.7 + # via pylint + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # -r requirements/base.txt + # openedx-django-pyfs + # xblock-sdk +edx-lint==5.3.6 + # via -r requirements/test.in +exceptiongroup==1.2.0 + # via pytest +fs==2.4.16 + # via + # -r requirements/base.txt + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via + # -r requirements/base.txt + # openedx-django-pyfs + # xblock-sdk +idna==3.6 + # via requests +iniconfig==2.0.0 + # via pytest +isort==5.13.2 + # via pylint +jinja2==3.1.3 + # via + # code-annotations + # cookiecutter +jmespath==1.0.1 + # via + # -r requirements/base.txt + # boto3 + # botocore + # parsel +lazy==1.6 + # via + # -r requirements/base.txt + # xblock +lxml==5.1.0 + # via + # -r requirements/base.txt + # parsel + # xblock + # xblock-sdk +mako==1.3.0 + # via + # -r requirements/base.txt + # xblock +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.3 + # via + # -r requirements/base.txt + # jinja2 + # mako + # xblock +mccabe==0.7.0 + # via pylint +mdurl==0.1.2 + # via markdown-it-py +mock==5.1.0 + # via -r requirements/test.in +openedx-django-pyfs==3.4.1 + # via + # -r requirements/base.txt + # xblock +packaging==23.2 + # via + # -r requirements/base.txt + # parsel + # pytest +parsel==1.8.1 + # via -r requirements/base.txt +pbr==6.0.0 + # via stevedore +platformdirs==4.1.0 + # via pylint +pluggy==1.3.0 + # via pytest +pycodestyle==2.11.1 + # via -r requirements/test.in +pygments==2.17.2 + # via rich +pylint==3.0.3 + # via + # edx-lint + # pylint-celery + # pylint-django + # pylint-plugin-utils +pylint-celery==0.3 + # via edx-lint +pylint-django==2.5.5 + # via edx-lint +pylint-plugin-utils==0.8.2 + # via + # pylint-celery + # pylint-django +pypng==0.20220715.0 + # via xblock-sdk +pytest==7.4.4 + # via + # pytest-cov + # pytest-django +pytest-cov==4.1.0 + # via -r requirements/test.in +pytest-django==4.7.0 + # via -r requirements/test.in +python-dateutil==2.8.2 + # via + # -r requirements/base.txt + # arrow + # botocore + # xblock +python-slugify==8.0.1 + # via + # code-annotations + # cookiecutter +pytz==2023.3.post1 + # via + # -r requirements/base.txt + # django + # xblock +pyyaml==6.0.1 + # via + # -r requirements/base.txt + # code-annotations + # cookiecutter + # xblock +requests==2.31.0 + # via + # cookiecutter + # xblock-sdk +rich==13.7.0 + # via cookiecutter +s3transfer==0.10.0 + # via + # -r requirements/base.txt + # boto3 +simplejson==3.19.2 + # via + # -r requirements/base.txt + # xblock + # xblock-sdk +six==1.16.0 + # via + # -r requirements/base.txt + # edx-lint + # fs + # fs-s3fs + # python-dateutil +sqlparse==0.4.4 + # via + # -r requirements/base.txt + # django +stevedore==5.1.0 + # via code-annotations +text-unidecode==1.3 + # via python-slugify +tomli==2.0.1 + # via + # coverage + # pylint + # pytest +tomlkit==0.12.3 + # via pylint +types-python-dateutil==2.8.19.20240106 + # via arrow +typing-extensions==4.9.0 + # via + # -r requirements/base.txt + # asgiref + # astroid + # pylint + # rich +urllib3==1.26.18 + # via + # -r requirements/base.txt + # botocore + # requests +w3lib==2.1.2 + # via + # -r requirements/base.txt + # parsel +web-fragments==2.1.0 + # via + # -r requirements/base.txt + # xblock + # xblock-sdk +webob==1.8.7 + # via + # -r requirements/base.txt + # xblock + # xblock-sdk +xblock[django]==1.9.1 + # via + # -r requirements/base.txt + # xblock + # xblock-sdk +xblock-sdk==0.7.0 + # via -r requirements/test.in + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/run_tests.py b/run_tests.py deleted file mode 100644 index 7cdae9f..0000000 --- a/run_tests.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Run tests for the Image Explorer XBlock - -This script is required to run our selenium tests inside the xblock-sdk workbench -because the workbench SDK's settings file is not inside any python module. -""" - -import os -import sys -import logging - -logging_level_overrides = { - 'workbench.views': logging.ERROR, - 'django.request': logging.ERROR, - 'workbench.runtime': logging.ERROR, -} - -if __name__ == "__main__": - # Use the workbench settings file: - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "workbench.settings") - # Configure a range of ports in case the default port of 8081 is in use - os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099") - - try: - os.mkdir('var') - except OSError: - # May already exist. - pass - - from django.conf import settings - settings.INSTALLED_APPS += ("image_explorer", ) - - for noisy_logger, log_level in logging_level_overrides.items(): - logging.getLogger(noisy_logger).setLevel(log_level) - - from django.core.management import execute_from_command_line - args = sys.argv[1:] - paths = [arg for arg in args if arg[0] != '-'] - if not paths: - paths = ["tests"] - options = [arg for arg in args if arg not in paths] - execute_from_command_line([sys.argv[0], "test"] + paths + options) diff --git a/setup.py b/setup.py index 7128052..28d9ac8 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,89 @@ # -*- coding: utf-8 -*- -# Imports ########################################################### - import os +import re +import sys + from setuptools import setup -# Functions ######################################################### +def load_requirements(*requirements_paths): + """ + Load all requirements from the specified requirements files. + Requirements will include any constraints from files specified + with -c in the requirements files. + Returns a list of requirement strings. + """ + requirements = {} + constraint_files = set() + + # groups "pkg<=x.y.z,..." into ("pkg", "<=x.y.z,...") + requirement_line_regex = re.compile(r"([a-zA-Z0-9-_.\[\]]+)([<>=][^#\s]+)?") + + def add_version_constraint_or_raise(current_line, current_requirements, add_if_not_present): + regex_match = requirement_line_regex.match(current_line) + if regex_match: + package = regex_match.group(1) + version_constraints = regex_match.group(2) + existing_version_constraints = current_requirements.get(package, None) + # fine to add constraints to an unconstrained package, + # raise an error if there are already constraints in place + if existing_version_constraints and existing_version_constraints != version_constraints: + raise BaseException( + f'Multiple constraint definitions found for {package}:' + f' "{existing_version_constraints}" and "{version_constraints}".' + f'Combine constraints into one location with {package}' + f'{existing_version_constraints},{version_constraints}.' + ) + if add_if_not_present or package in current_requirements: + current_requirements[package] = version_constraints + + # read requirements from .in + # store the path to any constraint files that are pulled in + for path in requirements_paths: + with open(path) as reqs: + for line in reqs: + if is_requirement(line): + add_version_constraint_or_raise(line, requirements, True) + if line and line.startswith('-c') and not line.startswith('-c http'): + constraint_files.add(os.path.dirname(path) + '/' + line.split('#')[0].replace('-c', '').strip()) + + # process constraint files: add constraints to existing requirements + for constraint_file in constraint_files: + with open(constraint_file) as reader: + for line in reader: + if is_requirement(line): + add_version_constraint_or_raise(line, requirements, False) + + # process back into list of pkg><=constraints strings + constrained_requirements = [f'{pkg}{version or ""}' for (pkg, version) in sorted(requirements.items())] + return constrained_requirements + + +def is_requirement(line): + """ + Return True if the requirement line is a package requirement. + Returns: + bool: True if the line is not blank, a comment, + a URL, or an included file + """ + return line and line.strip() and not line.startswith(("-r", "#", "-e", "git+", "-c")) + + +def get_version(*file_paths): + """ + Extract the version string from the file. + Input: + - file_paths: relative path fragments to file with + version string + """ + filename = os.path.join(os.path.dirname(__file__), *file_paths) + version_file = open(filename, encoding="utf8").read() + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError('Unable to find version string.') + def package_data(pkg, root_list): """Generic function to find package_data for `pkg` under `root`.""" @@ -19,19 +96,35 @@ def package_data(pkg, root_list): return {pkg: data} -# Main ############################################################## +VERSION = get_version('image_explorer', '__init__.py') + +if sys.argv[-1] == 'tag': + print("Tagging the version on GitHub:") + os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION)) + os.system("git push --tags") + sys.exit() + +README = open(os.path.join(os.path.dirname(__file__), 'README.md'), encoding="utf8").read() setup( name='xblock-image-explorer', - version='2.1.0', + version=VERSION, description='XBlock - Image Explorer', - packages=['image_explorer'], - install_requires=[ - 'XBlock>=1.2', - 'parsel>=1.6.0,<=1.6.0', + long_description=README, + long_description_content_type='text/markdown', + classifiers=[ + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.8', + 'Framework :: Django', + 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.2', ], + url='https://github.com/openedx/xblock-image-explorer', + install_requires=load_requirements('requirements/base.in'), entry_points={ 'xblock.v1': 'image-explorer = image_explorer:ImageExplorerBlock', }, + packages=['image_explorer'], package_data=package_data("image_explorer", ["static", "templates", "public", "translations"]), + python_requires=">=3.8", ) diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index 55cc6e3..0000000 --- a/test_requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -edx-lint==5.3.2 -mock==5.0.1 -pytest==7.2.1 -selenium==3.141.0 -xblock-sdk==0.5.4 diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/xml/default_image_explorer_scenario.xml b/tests/integration/xml/default_image_explorer_scenario.xml deleted file mode 100644 index 4f82fd0..0000000 --- a/tests/integration/xml/default_image_explorer_scenario.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/unit/test_image_explorer.py b/tests/test_image_explorer.py similarity index 99% rename from tests/unit/test_image_explorer.py rename to tests/test_image_explorer.py index f1e56f2..1d64556 100644 --- a/tests/unit/test_image_explorer.py +++ b/tests/test_image_explorer.py @@ -8,7 +8,7 @@ from xblock.field_data import DictFieldData from image_explorer.image_explorer import ImageExplorerBlock -from ..utils import MockRuntime, patch_static_replace_module +from .utils import MockRuntime, patch_static_replace_module from image_explorer.utils import AttrDict diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tox.ini b/tox.ini index d9bcf24..4c2c6db 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,30 @@ [tox] -envlist = unit,integration,quality +envlist = django{32,42},quality,package + +[pytest] +# Use the workbench settings file. +DJANGO_SETTINGS_MODULE = workbench.settings +addopts = --cov-report term-missing --cov-report xml [testenv] allowlist_externals = - make -commands_pre = - pip install -r requirements.txt - pip install -r test_requirements.txt - -[testenv:unit] + mkdir +deps = + django32: Django>=3.2,<4.0 + django42: Django>=4.2,<4.3 + -r{toxinidir}/requirements/test.txt commands = - python run_tests.py tests/unit + mkdir -p var + pytest {posargs:tests --cov image_explorer} -[testenv:integration] -passenv = * +[testenv:quality] commands = - python run_tests.py tests/integration + pylint --fail-under=9.0 image_explorer -[testenv:quality] +[testenv:package] +deps = + build + twine commands = - pylint --fail-under=9.0 image_explorer + python -m build + twine check dist/*