diff --git a/.github/workflows/build_pex.yml b/.github/workflows/build_pex.yml index 25b052d671a..877c123468a 100644 --- a/.github/workflows/build_pex.yml +++ b/.github/workflows/build_pex.yml @@ -15,12 +15,14 @@ jobs: build_pex: name: Build PEX runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster outputs: pex-file-name: ${{ steps.get-pex-filename.outputs.pex-file-name }} steps: - uses: actions/checkout@v4 + - name: Set up Python 3.6 + uses: actions/setup-python@v4 + with: + python-version: 3.6 - uses: actions/cache@v3 with: path: ~/.cache/pip diff --git a/.github/workflows/build_whl.yml b/.github/workflows/build_whl.yml index c8182263389..47fc4e02cb5 100644 --- a/.github/workflows/build_whl.yml +++ b/.github/workflows/build_whl.yml @@ -14,30 +14,26 @@ jobs: build_whl: name: Build WHL runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster outputs: whl-file-name: ${{ steps.get-whl-filename.outputs.whl-file-name }} tar-file-name: ${{ steps.get-tar-filename.outputs.tar-file-name }} steps: - - name: Install Git LFS - run: | - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash - apt-get install git-lfs - uses: actions/checkout@v4 with: fetch-depth: 0 lfs: true - name: Install Ubuntu dependencies run: | - apt-get -y -qq update - apt-get install -y gettext sudo + sudo apt-get -y -qq update + sudo apt-get install -y gettext + - name: Set up Python 3.6 + uses: actions/setup-python@v4 + with: + python-version: 3.6 - name: Use Node.js uses: actions/setup-node@v4 with: node-version: '16.x' - - name: Install Yarn - run: npm install -g yarn - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT diff --git a/.github/workflows/c_extensions.yml b/.github/workflows/c_extensions.yml index 3becb0dbd71..e4cc89424c7 100644 --- a/.github/workflows/c_extensions.yml +++ b/.github/workflows/c_extensions.yml @@ -28,13 +28,11 @@ jobs: needs: pre_job if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster - defaults: - run: - shell: bash steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: 3.6 - name: pip cache uses: actions/cache@v3 with: @@ -54,13 +52,13 @@ jobs: # in the kolibri directory python -m compileall -q kolibri -x py2only # Until we have staged builds, we will be running this in each and every - # environment even though builds should be done in Py 2.7 + # environment even though builds should be done in Py 3.6 make staticdeps make staticdeps-cext pip install . - # Start and stop kolibri - disabled until we can move out of a container - # kolibri start --port=8081 - # kolibri stop + # Start and stop kolibri + coverage run -p kolibri start --port=8081 + coverage run -p kolibri stop # Run just tests in test/ py.test --cov=kolibri --cov-report= --cov-append --color=no test/ no_c_ext: @@ -68,13 +66,11 @@ jobs: needs: pre_job if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster - defaults: - run: - shell: bash steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: 3.6 - name: pip cache uses: actions/cache@v3 with: @@ -94,11 +90,11 @@ jobs: # in the kolibri directory python -m compileall -q kolibri -x py2only # Until we have staged builds, we will be running this in each and every - # environment even though builds should be done in Py 2.7 + # environment even though builds should be done in Py 3.6 make staticdeps pip install . - # Start and stop kolibri - disabled until we can move out of a container - # kolibri start --port=8081 - # kolibri stop + # Start and stop kolibri + coverage run -p kolibri start --port=8081 + coverage run -p kolibri stop # Run just tests in test/ py.test --cov=kolibri --cov-report= --cov-append --color=no test/ diff --git a/.github/workflows/python2lint.yml b/.github/workflows/python2lint.yml deleted file mode 100644 index 3cb206ac9c9..00000000000 --- a/.github/workflows/python2lint.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Python 2 linting - -on: - push: - branches: - - develop - - 'release-v**' - pull_request: - branches: - - develop - - 'release-v**' - -jobs: - pre_job: - name: Path match check - runs-on: ubuntu-latest - # Map a step output to a job output - outputs: - should_skip: ${{ steps.skip_check.outputs.should_skip }} - steps: - - id: skip_check - uses: fkirc/skip-duplicate-actions@master - with: - github_token: ${{ github.token }} - paths: '["kolibri/**/*.py"]' - lint: - name: Python 2 syntax checking - needs: pre_job - if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster - steps: - - uses: actions/checkout@v4 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 - - name: Lint with flake8 - run: | - flake8 kolibri diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index e502bd5d682..e26d43fefda 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -53,28 +53,6 @@ jobs: - name: Test with tox if: ${{ needs.pre_job.outputs.should_skip != 'true' }} run: tox -e py${{ matrix.python-version }} - unit_test27: - name: Python unit tests (2.7) - needs: pre_job - runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster - steps: - - uses: actions/checkout@v4 - - name: Install tox - if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - run: | - python -m pip install --upgrade pip - pip install "tox<4" - - name: tox env cache - if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - uses: actions/cache@v3 - with: - path: ${{ github.workspace }}/.tox/py2.7 - key: ${{ runner.os }}-tox-py2.7-${{ hashFiles('requirements/*.txt') }} - - name: Test with tox - if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - run: tox -e py2.7 postgres: name: Python postgres unit tests needs: pre_job diff --git a/Makefile b/Makefile index df3aa492e5e..afc0b80b064 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ SHELL := /bin/bash # List most target names as 'PHONY' to prevent Make from thinking it will be creating a file of the same name -.PHONY: help clean clean-assets clean-build clean-pyc clean-docs lint test test-all assets coverage docs release test-namespaced-packages staticdeps staticdeps-cext writeversion setrequirements buildconfig pex i18n-extract-frontend i18n-extract-backend i18n-transfer-context i18n-extract i18n-django-compilemessages i18n-upload i18n-pretranslate i18n-pretranslate-approve-all i18n-download i18n-regenerate-fonts i18n-stats i18n-install-font i18n-download-translations i18n-download-glossary i18n-upload-glossary docker-whl docker-demoserver docker-devserver docker-envlist +.PHONY: help clean clean-assets clean-build clean-pyc clean-docs lint test test-all assets coverage docs release staticdeps staticdeps-cext writeversion setrequirements buildconfig pex i18n-extract-frontend i18n-extract-backend i18n-transfer-context i18n-extract i18n-django-compilemessages i18n-upload i18n-pretranslate i18n-pretranslate-approve-all i18n-download i18n-regenerate-fonts i18n-stats i18n-install-font i18n-download-translations i18n-download-glossary i18n-upload-glossary docker-whl docker-demoserver docker-devserver docker-envlist help: @@ -33,7 +33,6 @@ help: @echo "test: run tests quickly with the default Python" @echo "test-all: run tests on every Python version with Tox" @echo "test-with-postgres: run tests quickly with a temporary postgresql backend" - @echo "test-namespaced-packages: verify that we haven't fetched anything namespaced into kolibri/dist" @echo "coverage: run tests, recording and printing out Python code coverage" @echo "docs: generate developer documentation" @echo "start-foreground-with-postgres: run Kolibri in foreground mode with a temporary postgresql backend" @@ -152,30 +151,21 @@ release: @read __ twine upload -s dist/* -test-namespaced-packages: - # This expression checks that everything in kolibri/dist has an __init__.py - # To prevent namespaced packages from suddenly showing up - # https://github.com/learningequality/kolibri/pull/2972 - ! find kolibri/dist -mindepth 1 -maxdepth 1 -type d -not -name __pycache__ -not -name cext -not -name py2only -not -name *dist-info -exec ls {}/__init__.py \; 2>&1 | grep "No such file" - clean-staticdeps: rm -rf kolibri/dist/* || true # remove everything git checkout -- kolibri/dist # restore __init__.py staticdeps: clean-staticdeps - test "${SKIP_PY_CHECK}" = "1" || python2 --version 2>&1 | grep -q 2.7 || ( echo "Only intended to run on Python 2.7" && exit 1 ) - pip2 install -t kolibri/dist -r "requirements.txt" + test "${SKIP_PY_CHECK}" = "1" || python --version 2>&1 | grep -q 3.6 || ( echo "Only intended to run on Python 3.6" && exit 1 ) + pip install -t kolibri/dist -r "requirements.txt" rm -rf kolibri/dist/*.egg-info rm -r kolibri/dist/man kolibri/dist/bin || true # remove the two folders introduced by pip 10 - python2 build_tools/py2only.py # move `future` and `futures` packages to `kolibri/dist/py2only` - make test-namespaced-packages staticdeps-cext: rm -rf kolibri/dist/cext || true # remove everything python build_tools/install_cexts.py --file "requirements/cext.txt" # pip install c extensions pip install -t kolibri/dist/cext -r "requirements/cext_noarch.txt" --no-deps rm -rf kolibri/dist/*.egg-info - make test-namespaced-packages staticdeps-compileall: bash -c 'python --version' @@ -209,7 +199,7 @@ read-whl-file-version: python ./build_tools/read_whl_version.py ${whlfile} > kolibri/VERSION pex: - ls dist/*.whl | while read whlfile; do $(MAKE) read-whl-file-version whlfile=$$whlfile; pex $$whlfile --disable-cache -o dist/kolibri-`cat kolibri/VERSION | sed 's/+/_/g'`.pex -m kolibri --python-shebang=/usr/bin/python; done + ls dist/*.whl | while read whlfile; do $(MAKE) read-whl-file-version whlfile=$$whlfile; pex $$whlfile --disable-cache -o dist/kolibri-`cat kolibri/VERSION | sed 's/+/_/g'`.pex -m kolibri --python-shebang=/usr/bin/python3; done i18n-extract-backend: cd kolibri && python -m kolibri manage makemessages -- -l en --ignore 'node_modules/*' --ignore 'kolibri/dist/*' diff --git a/build_tools/install_cexts.py b/build_tools/install_cexts.py index 59aa3909b31..21ec8c4c5fc 100644 --- a/build_tools/install_cexts.py +++ b/build_tools/install_cexts.py @@ -2,7 +2,8 @@ This module defines functions to install c extensions for all the platforms into Kolibri. -It is required to have pip version greater than 19.3.1 to run this script. +See requirements/build.txt for the list of requirements that must be installed for this +script to run. Usage: > python build_tools/install_cexts.py --file "requirements/cext.txt" --cache-path "/cext_cache" @@ -170,8 +171,6 @@ def parse_package_page(files, pk_version, index_url, cache_path): * not the version specified in requirements.txt * not python versions that kolibri does not support * not macosx - * not win_x64 with python 3.8 - * not win32 with python 3.8 """ result = [] @@ -191,13 +190,10 @@ def parse_package_page(files, pk_version, index_url, cache_path): if package_version != pk_version: continue - if python_version != "27" and python_version not in supported_python3_versions: + if python_version not in supported_python3_versions: continue if "macosx" in platform: continue - if "win_amd64" in platform or "win32" in platform and python_version == "27": - # Don't install win_amd64 or win32 with python 2.7 - continue info = { "platform": platform, diff --git a/build_tools/py2only.py b/build_tools/py2only.py deleted file mode 100644 index 303fd994b18..00000000000 --- a/build_tools/py2only.py +++ /dev/null @@ -1,55 +0,0 @@ -import os -import shutil -import sys - -dest = "py2only" -futures_dirname = "concurrent" -scandir_module_name = "scandir.py" -typing_module_name = "typing.py" -DIST_DIR = os.path.join( - os.path.dirname(os.path.realpath(os.path.dirname(__file__))), "kolibri", "dist" -) - - -def hide_py2_modules(): - """ - Move the directory of 'futures' and python2-only modules of 'future' - inside the directory 'py2only' - """ - - # Move the directory of 'futures' inside the directory 'py2only' - _move_modules_to_py2only(futures_dirname) - _move_modules_to_py2only(scandir_module_name) - _move_modules_to_py2only(typing_module_name) - - # Future's submodules are not downloaded in Python 3 but only in Python 2 - if sys.version_info[0] == 2: - from future.standard_library import TOP_LEVEL_MODULES - - for module in TOP_LEVEL_MODULES: - if module == "test": - continue - - # Move the directory of submodules of 'future' inside 'py2only' - _move_modules_to_py2only(module) - - -def _move_modules_to_py2only(module_name): - module_src_path = os.path.join(DIST_DIR, module_name) - module_dst_path = os.path.join(DIST_DIR, dest, module_name) - shutil.move(module_src_path, module_dst_path) - - -if __name__ == "__main__": - # Temporarily add `kolibri/dist` to PYTHONPATH to import future - sys.path.append(DIST_DIR) - - try: - os.makedirs(os.path.join(DIST_DIR, dest)) - except OSError: - raise - - hide_py2_modules() - - # Remove `kolibri/dist` from PYTHONPATH - sys.path = sys.path[:-1] diff --git a/docker/build_whl.dockerfile b/docker/build_whl.dockerfile index deb7096324b..177f41421b3 100644 --- a/docker/build_whl.dockerfile +++ b/docker/build_whl.dockerfile @@ -10,7 +10,7 @@ RUN apt-get update && \ gettext \ git \ git-lfs \ - python2.7 \ + python3.6 \ python-pip \ python-sphinx diff --git a/docs/getting_started.rst b/docs/getting_started.rst index f464bd42d0c..20673f0939a 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -62,7 +62,7 @@ Python and Pip To develop on Kolibri, you'll need: -* Python 3.4+ or Python 2.7+ (Kolibri doesn't currently support Python 3.11.0 or higher) +* Python 3.6+ (Kolibri doesn't currently support Python 3.12.0 or higher) * `pip `__ Managing Python installations can be quite tricky. We *highly* recommend using `pyenv `__ or if you are more comfortable using a package manager, then package managers like `Homebrew `__ on Mac or ``apt`` on Debian for this. diff --git a/docs/stack.rst b/docs/stack.rst index 290cabc60be..4f69109c401 100644 --- a/docs/stack.rst +++ b/docs/stack.rst @@ -10,7 +10,7 @@ Note that since Kolibri is still in development, the APIs are subject to change, Server ------ -The server is a `Django `__ application, and contains only pure-Python (2.7+) dependencies at run-time. It is responsible for: +The server is a `Django `__ application, and contains only pure-Python (3.6+) dependencies at run-time. It is responsible for: - Interfacing with the database (either `SQLite `__ or `PostgreSQL `__) - Authentication and permission middleware diff --git a/kolibri/__init__.py b/kolibri/__init__.py index ba10d90a778..ce4b84a8534 100755 --- a/kolibri/__init__.py +++ b/kolibri/__init__.py @@ -3,10 +3,6 @@ This module is imported in setup.py, so you cannot for instance import a dependency. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.utils import env from kolibri.utils.version import get_version diff --git a/kolibri/__main__.py b/kolibri/__main__.py index 40500886e00..ec5f80a9d40 100644 --- a/kolibri/__main__.py +++ b/kolibri/__main__.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import sys if __name__ == "__main__": diff --git a/kolibri/core/__init__.py b/kolibri/core/__init__.py index 12d7cccb3a3..93bad367500 100644 --- a/kolibri/core/__init__.py +++ b/kolibri/core/__init__.py @@ -2,8 +2,4 @@ enters the docs) """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - default_app_config = "kolibri.core.apps.KolibriCoreConfig" diff --git a/kolibri/core/analytics/test/test_ping.py b/kolibri/core/analytics/test/test_ping.py index e04a724da3c..1cd87f73ea5 100644 --- a/kolibri/core/analytics/test/test_ping.py +++ b/kolibri/core/analytics/test/test_ping.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import json import zlib diff --git a/kolibri/core/analytics/test/test_utils.py b/kolibri/core/analytics/test/test_utils.py index b9275ec15df..714def16fbc 100644 --- a/kolibri/core/analytics/test/test_utils.py +++ b/kolibri/core/analytics/test/test_utils.py @@ -1,7 +1,4 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - +import base64 import csv import datetime import hashlib @@ -18,7 +15,6 @@ from kolibri.core.analytics.models import PingbackNotification from kolibri.core.analytics.utils import calculate_list_stats from kolibri.core.analytics.utils import create_and_update_notifications -from kolibri.core.analytics.utils import encodestring from kolibri.core.analytics.utils import extract_channel_statistics from kolibri.core.analytics.utils import extract_facility_statistics from kolibri.core.auth.constants import demographics @@ -338,9 +334,9 @@ def test_extract_facility_statistics__soud_hash(self): actual = extract_facility_statistics(facility) users = sorted(self.users, key=lambda u: u.id) user_ids = ":".join([user.id for user in users]) - expected_soud_hash = encodestring(hashlib.md5(user_ids.encode()).digest())[ - :10 - ].decode() + expected_soud_hash = base64.encodebytes( + hashlib.md5(user_ids.encode()).digest() + )[:10].decode() self.assertEqual(expected_soud_hash, actual.pop("sh")) diff --git a/kolibri/core/analytics/utils.py b/kolibri/core/analytics/utils.py index 0d59d13c8c0..d08d5be7298 100644 --- a/kolibri/core/analytics/utils.py +++ b/kolibri/core/analytics/utils.py @@ -4,7 +4,6 @@ import json import logging import math -import sys import requests from dateutil import parser @@ -65,15 +64,6 @@ "registered", ] -if sys.version_info[0] >= 3: - # encodestring is a deprecated alias for - # encodebytes, which was finally removed - # in Python 3.9 - encodestring = base64.encodebytes -else: - # encodebytes does not exist in Python 2.7 - encodestring = base64.encodestring - def calculate_list_stats(data): if data: @@ -236,7 +226,7 @@ def extract_facility_statistics(facility): # fmt: off data = { # facility_id - "fi": encodestring(hashlib.md5(facility.id.encode()).digest())[:10].decode(), + "fi": base64.encodebytes(hashlib.md5(facility.id.encode()).digest())[:10].decode(), # settings "s": settings, # learners_count @@ -300,7 +290,9 @@ def extract_facility_statistics(facility): facility.facilityuser_set.order_by("id").values_list("id", flat=True) ) # soud_hash - data["sh"] = encodestring(hashlib.md5(user_ids.encode()).digest())[:10].decode() + data["sh"] = base64.encodebytes(hashlib.md5(user_ids.encode()).digest())[ + :10 + ].decode() return data diff --git a/kolibri/core/api.py b/kolibri/core/api.py index f169f66c93b..49af5b60eb4 100644 --- a/kolibri/core/api.py +++ b/kolibri/core/api.py @@ -17,7 +17,6 @@ from rest_framework.serializers import ValidationError from rest_framework.status import HTTP_201_CREATED from rest_framework.status import HTTP_503_SERVICE_UNAVAILABLE -from six import string_types from .utils.portal import registerfacility from kolibri.core.auth.models import Facility @@ -76,9 +75,7 @@ def get_default_valid_fields(self, queryset, view, context=None): # All the fields that we have field maps defined for - this only allows for simple mapped fields # where the field is essentially a rename, as we have no good way of doing ordering on a field that # that is doing more complex function based mapping. - mapped_fields = { - v: k for k, v in view.field_map.items() if isinstance(v, string_types) - } + mapped_fields = {v: k for k, v in view.field_map.items() if isinstance(v, str)} # All the fields of the model model_fields = {f.name for f in queryset.model._meta.get_fields()} # Loop through every value in the view's values tuple @@ -109,9 +106,7 @@ def remove_invalid_fields(self, queryset, fields, view, request): to do filtering based on valuesviewset setup """ # We filter the mapped fields to ones that do simple string mappings here, any functional maps are excluded. - mapped_fields = { - k: v for k, v in view.field_map.items() if isinstance(v, string_types) - } + mapped_fields = {k: v for k, v in view.field_map.items() if isinstance(v, str)} valid_fields = [ item[0] for item in self.get_valid_fields(queryset, view, {"request": request}) @@ -167,9 +162,7 @@ def generate_serializer(self): model = getattr(queryset, "model", None) if model is None: return Serializer - mapped_fields = { - v: k for k, v in self.field_map.items() if isinstance(v, string_types) - } + mapped_fields = {v: k for k, v in self.field_map.items() if isinstance(v, str)} fields = [] extra_kwargs = {} for value in self.values: diff --git a/kolibri/core/apps.py b/kolibri/core/apps.py index cc3d5d129c1..387c2a2599c 100644 --- a/kolibri/core/apps.py +++ b/kolibri/core/apps.py @@ -10,7 +10,6 @@ from django.db.utils import DatabaseError from django_filters.filters import UUIDFilter from django_filters.rest_framework.filterset import FilterSet -from six import raise_from from kolibri.core.errors import RedisConnectionError from kolibri.core.sqlite.pragmas import CONNECTION_PRAGMAS @@ -170,9 +169,9 @@ def check_redis_settings(): # noqa C901 except ConnectionError as e: logger.warning("Unable to connect to Redis: {}".format(str(e))) - raise_from( - RedisConnectionError("Unable to connect to Redis: {}".format(str(e))), e - ) + raise RedisConnectionError( + "Unable to connect to Redis: {}".format(str(e)) + ) from e except Exception as e: logger.warning("Unable to check Redis settings") diff --git a/kolibri/core/assets/src/mixins/commonCoreStrings.js b/kolibri/core/assets/src/mixins/commonCoreStrings.js index 1aa24d3812a..5ec5a98b485 100644 --- a/kolibri/core/assets/src/mixins/commonCoreStrings.js +++ b/kolibri/core/assets/src/mixins/commonCoreStrings.js @@ -1305,12 +1305,6 @@ export const coreStrings = createTranslator('CommonCoreStrings', { context: 'Displayed to users of kolibri where one or more devices on the network are using Internet Explorer 11, as part of a message encouraging the user to upgrade.', }, - pythonSupportWillBeDropped: { - message: - 'Please note that support for Python 2.7 will be dropped in the upcoming version 0.17. Upgrade your Python version to Python 3.7+ to continue working with Kolibri. More recent versions of Python 3 are recommended.', - context: - 'Displayed to users of kolibri where one or more devices on the network are using Python 2.7, as part of a message encouraging the user to upgrade.', - }, // Content activity notStartedLabel: { diff --git a/kolibri/core/auth/api.py b/kolibri/core/auth/api.py index 5a3cb865582..8327ca2ee92 100644 --- a/kolibri/core/auth/api.py +++ b/kolibri/core/auth/api.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import time from datetime import datetime from datetime import timedelta diff --git a/kolibri/core/auth/apps.py b/kolibri/core/auth/apps.py index b41e49fa4af..0e41aa50aa4 100644 --- a/kolibri/core/auth/apps.py +++ b/kolibri/core/auth/apps.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/kolibri/core/auth/management/commands/bulkimportusers.py b/kolibri/core/auth/management/commands/bulkimportusers.py index 4a39845373a..9004ce8454e 100644 --- a/kolibri/core/auth/management/commands/bulkimportusers.py +++ b/kolibri/core/auth/management/commands/bulkimportusers.py @@ -2,7 +2,6 @@ import logging import ntpath import re -import sys from uuid import UUID from django.conf import settings @@ -782,11 +781,7 @@ def remove_memberships(self, users, enrolled, assigned): for user in users: # enrolled: to_remove = user.memberships.filter(collection__kind=CLASSROOM) - username = ( - user.username - if sys.version_info[0] >= 3 - else user.username.encode("utf-8") - ) + username = user.username if username in users_enrolled.keys(): to_remove.exclude( collection__name__in=users_enrolled[username] diff --git a/kolibri/core/auth/management/commands/fullfacilitysync.py b/kolibri/core/auth/management/commands/fullfacilitysync.py index f7e82e615ba..5d473ed9a21 100644 --- a/kolibri/core/auth/management/commands/fullfacilitysync.py +++ b/kolibri/core/auth/management/commands/fullfacilitysync.py @@ -5,7 +5,6 @@ from django.core.management import call_command from django.core.management.base import CommandError from django.core.validators import URLValidator -from django.utils.six.moves import input from morango.models import Certificate from morango.models import Filter from morango.models import InstanceIDModel diff --git a/kolibri/core/auth/management/utils.py b/kolibri/core/auth/management/utils.py index 05e7d25b6c2..bbe9c48fe9d 100644 --- a/kolibri/core/auth/management/utils.py +++ b/kolibri/core/auth/management/utils.py @@ -11,7 +11,6 @@ import requests from django.core.management.base import CommandError -from django.utils.six.moves import input from morango.models import Certificate from morango.models import InstanceIDModel from morango.models import ScopeDefinition diff --git a/kolibri/core/auth/models.py b/kolibri/core/auth/models.py index 95a78ef1ccb..47d8ff7dc31 100644 --- a/kolibri/core/auth/models.py +++ b/kolibri/core/auth/models.py @@ -18,14 +18,9 @@ object also stores the "kind" of the role (currently, one of "admin" or "coach"), which affects what permissions the user gains through the ``Role``. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging from threading import local -import six from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import UserManager @@ -745,7 +740,7 @@ def validate_birth_year(value): def validate_role_kinds(kinds): - if isinstance(kinds, six.string_types): + if isinstance(kinds, str): kinds = set([kinds]) else: try: diff --git a/kolibri/core/auth/serializers.py b/kolibri/core/auth/serializers.py index 1f9498f458d..c77919e9abd 100644 --- a/kolibri/core/auth/serializers.py +++ b/kolibri/core/auth/serializers.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging from django.core.exceptions import ValidationError as DjangoValidationError diff --git a/kolibri/core/auth/test/test_api.py b/kolibri/core/auth/test/test_api.py index 702a37e280c..04845cf0e3a 100644 --- a/kolibri/core/auth/test/test_api.py +++ b/kolibri/core/auth/test/test_api.py @@ -1,10 +1,5 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import base64 import collections -import sys import time import uuid from datetime import datetime @@ -19,7 +14,7 @@ from morango.models import SyncSession from morango.models import TransferSession from rest_framework import status -from rest_framework.test import APITestCase as BaseTestCase +from rest_framework.test import APITestCase from .. import models from ..constants import role_kinds @@ -33,19 +28,6 @@ from kolibri.core.auth.constants import demographics from kolibri.core.device.utils import set_device_settings -# A weird hack because of http://bugs.python.org/issue17866 -if sys.version_info >= (3,): - - class APITestCase(BaseTestCase): - def assertItemsEqual(self, *args, **kwargs): - self.assertCountEqual(*args, **kwargs) - - -else: - - class APITestCase(BaseTestCase): - pass - class FacilityFactory(factory.DjangoModelFactory): class Meta: @@ -116,11 +98,9 @@ def test_learnergroup_list(self): ) for group in self.learner_groups ] - # assertItemsEqual does not deal well with embedded objects, as it does - # not do a deepEqual, so check each individual list of user_ids for i, group in enumerate(response.data): - self.assertItemsEqual(group.pop("user_ids"), expected[i].pop("user_ids")) - self.assertItemsEqual(response.data, expected) + self.assertCountEqual(group.pop("user_ids"), expected[i].pop("user_ids")) + self.assertCountEqual(response.data, expected) def test_learnergroup_list_user(self): self.client.login( @@ -132,7 +112,7 @@ def test_learnergroup_list_user(self): reverse("kolibri:core:learnergroup-list"), format="json" ) expected = [] - self.assertItemsEqual(response.data, expected) + self.assertCountEqual(response.data, expected) def test_learnergroup_list_user_parent_filter(self): self.client.login( @@ -147,7 +127,7 @@ def test_learnergroup_list_user_parent_filter(self): format="json", ) expected = [] - self.assertItemsEqual(response.data, expected) + self.assertCountEqual(response.data, expected) def test_learnergroup_detail(self): self.login_superuser() @@ -164,7 +144,7 @@ def test_learnergroup_detail(self): "parent": self.learner_groups[0].parent.id, "user_ids": [member.id for member in self.learner_groups[0].get_members()], } - self.assertItemsEqual(response.data, expected) + self.assertCountEqual(response.data, expected) def test_learnergroup_detail_user(self): self.client.login( @@ -201,11 +181,11 @@ def test_parent_in_queryparam_with_one_id(self): for group in self.learner_groups if group.parent.id == classroom_id ] - # assertItemsEqual does not deal well with embedded objects, as it does + # assertCountEqual does not deal well with embedded objects, as it does # not do a deepEqual, so check each individual list of user_ids for i, group in enumerate(response.data): - self.assertItemsEqual(group.pop("user_ids"), expected[i].pop("user_ids")) - self.assertItemsEqual(response.data, expected) + self.assertCountEqual(group.pop("user_ids"), expected[i].pop("user_ids")) + self.assertCountEqual(response.data, expected) def test_cannot_create_learnergroup_same_name(self): self.login_superuser() @@ -271,7 +251,7 @@ def test_classroom_list(self): ) for classroom in sorted(self.classrooms, key=lambda x: x.id) ] - self.assertItemsEqual(response.data, expected) + self.assertCountEqual(response.data, expected) def test_classroom_list_user(self): self.client.login( @@ -282,7 +262,7 @@ def test_classroom_list_user(self): response = self.client.get( reverse("kolibri:core:classroom-list"), format="json" ) - self.assertItemsEqual(response.data, []) + self.assertCountEqual(response.data, []) def test_classroom_list_user_parent_filter(self): self.client.login( @@ -294,7 +274,7 @@ def test_classroom_list_user_parent_filter(self): reverse("kolibri:core:classroom-list") + "?parent=" + self.facility.id, format="json", ) - self.assertItemsEqual(response.data, []) + self.assertCountEqual(response.data, []) def test_classroom_detail(self): self.login_superuser() @@ -620,52 +600,32 @@ def test_public_facility_endpoint(self): self.assertEqual(models.Facility.objects.all().count(), len(response.data)) def test_public_facilityuser_endpoint(self): - if sys.version_info[0] == 2: - credentials = base64.b64encode( + credentials = base64.b64encode( + str.encode( "username={}&{}={}:{}".format( self.user1.username, FACILITY_CREDENTIAL_KEY, self.facility1.id, DUMMY_PASSWORD, - ).encode("utf-8") - ) - else: - credentials = base64.b64encode( - str.encode( - "username={}&{}={}:{}".format( - self.user1.username, - FACILITY_CREDENTIAL_KEY, - self.facility1.id, - DUMMY_PASSWORD, - ) ) - ).decode("ascii") + ) + ).decode("ascii") self.client.credentials(HTTP_AUTHORIZATION="Basic {}".format(credentials)) response = self.client.get( reverse("kolibri:core:publicuser-list"), format="json", ) self.assertEqual(len(response.data), 1) - if sys.version_info[0] == 2: - credentials = base64.b64encode( + credentials = base64.b64encode( + str.encode( "username={}&{}={}:{}".format( self.superuser.username, FACILITY_CREDENTIAL_KEY, self.facility1.id, DUMMY_PASSWORD, - ).encode("utf-8") - ) - else: - credentials = base64.b64encode( - str.encode( - "username={}&{}={}:{}".format( - self.superuser.username, - FACILITY_CREDENTIAL_KEY, - self.facility1.id, - DUMMY_PASSWORD, - ) ) - ).decode("ascii") + ) + ).decode("ascii") self.client.credentials(HTTP_AUTHORIZATION="Basic {}".format(credentials)) response = self.client.get( reverse("kolibri:core:publicuser-list"), @@ -1007,7 +967,7 @@ def test_user_list(self): ) response = self.client.get(reverse("kolibri:core:facilityuser-list")) self.assertEqual(response.status_code, 200) - self.assertItemsEqual( + self.assertCountEqual( response.data, [ { @@ -1049,7 +1009,7 @@ def test_user_list_self(self): ) response = self.client.get(reverse("kolibri:core:facilityuser-list")) self.assertEqual(response.status_code, 200) - self.assertItemsEqual( + self.assertCountEqual( response.data, [ { @@ -1069,7 +1029,7 @@ def test_user_list_self(self): def test_anonymous_user_list(self): response = self.client.get(reverse("kolibri:core:facilityuser-list")) self.assertEqual(response.status_code, 200) - self.assertItemsEqual( + self.assertCountEqual( response.data, [], ) diff --git a/kolibri/core/auth/test/test_backend.py b/kolibri/core/auth/test/test_backend.py index 60e0918fdf1..aa5c1530427 100644 --- a/kolibri/core/auth/test/test_backend.py +++ b/kolibri/core/auth/test/test_backend.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import mock from django.test import TestCase diff --git a/kolibri/core/auth/test/test_bulk_import.py b/kolibri/core/auth/test/test_bulk_import.py index e985b3c314a..68f6c231674 100644 --- a/kolibri/core/auth/test/test_bulk_import.py +++ b/kolibri/core/auth/test/test_bulk_import.py @@ -1,6 +1,6 @@ import csv -import sys import tempfile +from io import StringIO from uuid import uuid4 import pytest @@ -18,11 +18,6 @@ from kolibri.core.utils.csv import open_csv_for_reading from kolibri.core.utils.csv import open_csv_for_writing -if sys.version_info[0] < 3: - from cStringIO import StringIO -else: - from io import StringIO - CLASSROOMS = 2 diff --git a/kolibri/core/auth/test/test_deprovisioning.py b/kolibri/core/auth/test/test_deprovisioning.py index f40cb493734..52115a440d0 100644 --- a/kolibri/core/auth/test/test_deprovisioning.py +++ b/kolibri/core/auth/test/test_deprovisioning.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import uuid from django.core.management import call_command diff --git a/kolibri/core/auth/test/test_middleware.py b/kolibri/core/auth/test/test_middleware.py index 53e4ee8b01b..65dfadbb20f 100644 --- a/kolibri/core/auth/test/test_middleware.py +++ b/kolibri/core/auth/test/test_middleware.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.core.exceptions import ImproperlyConfigured from django.test import override_settings from django.test import TestCase diff --git a/kolibri/core/auth/test/test_models.py b/kolibri/core/auth/test/test_models.py index 549a691b54d..432ba5593d0 100644 --- a/kolibri/core/auth/test/test_models.py +++ b/kolibri/core/auth/test/test_models.py @@ -1,10 +1,6 @@ """ Tests of the core auth models (Role, Membership, Collection, FacilityUser, etc). """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.core.exceptions import ValidationError from django.db.utils import IntegrityError from django.test import TestCase diff --git a/kolibri/core/auth/test/test_permissions.py b/kolibri/core/auth/test/test_permissions.py index 1e8a52631cd..dbba02864aa 100644 --- a/kolibri/core/auth/test/test_permissions.py +++ b/kolibri/core/auth/test/test_permissions.py @@ -1,10 +1,6 @@ """ Tests of the permissions on specific models in the auth app. For tests of the permissions system itself, see test_permission_classes.py """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.test import TestCase from ..constants import role_kinds diff --git a/kolibri/core/auth/test/test_permissions_classes.py b/kolibri/core/auth/test/test_permissions_classes.py index ee80adc3851..a9c46e1d640 100644 --- a/kolibri/core/auth/test/test_permissions_classes.py +++ b/kolibri/core/auth/test/test_permissions_classes.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.test import TestCase from mock import Mock diff --git a/kolibri/core/auth/test/test_roles_and_membership.py b/kolibri/core/auth/test/test_roles_and_membership.py index 9390455a0d0..2ff5ae83e4f 100644 --- a/kolibri/core/auth/test/test_roles_and_membership.py +++ b/kolibri/core/auth/test/test_roles_and_membership.py @@ -1,10 +1,6 @@ """ Tests of role and membership calculations. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.test import TestCase from ..constants import role_kinds diff --git a/kolibri/core/auth/test/test_user_export.py b/kolibri/core/auth/test/test_user_export.py index b972886d45c..9195d83e1e4 100644 --- a/kolibri/core/auth/test/test_user_export.py +++ b/kolibri/core/auth/test/test_user_export.py @@ -7,7 +7,6 @@ from django.core.management import call_command from django.test import TestCase -from six.moves import filter from .helpers import setup_device from kolibri.core.auth.constants.demographics import DEFERRED diff --git a/kolibri/core/auth/test/test_user_import.py b/kolibri/core/auth/test/test_user_import.py index 14811d9d0bd..aee5fb59d62 100644 --- a/kolibri/core/auth/test/test_user_import.py +++ b/kolibri/core/auth/test/test_user_import.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import csv import os import tempfile diff --git a/kolibri/core/auth/test/test_users.py b/kolibri/core/auth/test/test_users.py index 333403ff820..de64879366f 100644 --- a/kolibri/core/auth/test/test_users.py +++ b/kolibri/core/auth/test/test_users.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.test import TestCase from ..models import Facility diff --git a/kolibri/core/auth/test/test_utils.py b/kolibri/core/auth/test/test_utils.py index 52eb89c978d..c4dd97247fb 100644 --- a/kolibri/core/auth/test/test_utils.py +++ b/kolibri/core/auth/test/test_utils.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import datetime import random import uuid diff --git a/kolibri/core/content/apps.py b/kolibri/core/content/apps.py index b17ffff5a84..a4a11f71483 100644 --- a/kolibri/core/content/apps.py +++ b/kolibri/core/content/apps.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/kolibri/core/content/hooks.py b/kolibri/core/content/hooks.py index 561efe349a0..bc31aa9139d 100644 --- a/kolibri/core/content/hooks.py +++ b/kolibri/core/content/hooks.py @@ -4,10 +4,6 @@ Hooks for managing the display and rendering of content. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import json from abc import abstractmethod from abc import abstractproperty diff --git a/kolibri/core/content/management/commands/content.py b/kolibri/core/content/management/commands/content.py index c891460d237..cd0c5a899ee 100644 --- a/kolibri/core/content/management/commands/content.py +++ b/kolibri/core/content/management/commands/content.py @@ -1,13 +1,8 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging import os import shutil from django.core.management.base import BaseCommand -from six.moves import input from kolibri.utils import server from kolibri.utils.conf import KOLIBRI_HOME diff --git a/kolibri/core/content/management/commands/generate_schema.py b/kolibri/core/content/management/commands/generate_schema.py index e580dc91e5d..9e437214f6c 100644 --- a/kolibri/core/content/management/commands/generate_schema.py +++ b/kolibri/core/content/management/commands/generate_schema.py @@ -2,7 +2,6 @@ import json import os import shutil -import sys from collections import defaultdict from collections import OrderedDict @@ -133,14 +132,8 @@ def handle(self, *args, **options): data[table_name] = [get_dict(r) for r in session.query(record).all()] data_path = DATA_PATH_TEMPLATE.format(name=version) - # Handle Python 2 unicode issue by opening the file in binary mode - # with no encoding as the data has already been encoded - if sys.version[0] == "2": - with io.open(data_path, mode="wb") as f: - json.dump(data, f) - else: - with io.open(data_path, mode="w", encoding="utf-8") as f: - json.dump(data, f) + with io.open(data_path, mode="w", encoding="utf-8") as f: + json.dump(data, f) shutil.rmtree( os.path.join( diff --git a/kolibri/core/content/tasks.py b/kolibri/core/content/tasks.py index 7cd4024192b..fa7be917d11 100644 --- a/kolibri/core/content/tasks.py +++ b/kolibri/core/content/tasks.py @@ -1,10 +1,10 @@ +from urllib.parse import urljoin + import requests from django.core.exceptions import ValidationError from django.core.management import call_command from django.db.models import Q from rest_framework import serializers -from six import with_metaclass -from six.moves.urllib.parse import urljoin from kolibri.core.auth.models import FacilityDataset from kolibri.core.content.models import ChannelMetadata @@ -131,7 +131,7 @@ def to_internal_value(self, drive_id): return drive_id -class LocalMixin(with_metaclass(serializers.SerializerMetaclass)): +class LocalMixin(metaclass=serializers.SerializerMetaclass): drive_id = DriveIdField() def validate(self, data): @@ -181,7 +181,7 @@ def diskcontentimport( manager.run() -class RemoteImportMixin(with_metaclass(serializers.SerializerMetaclass)): +class RemoteImportMixin(metaclass=serializers.SerializerMetaclass): peer = serializers.PrimaryKeyRelatedField( required=False, queryset=NetworkLocation.objects.all().values("base_url", "id") ) diff --git a/kolibri/core/content/templatetags/content_tags.py b/kolibri/core/content/templatetags/content_tags.py index 7e2aac3cb00..6cbc5d9d338 100644 --- a/kolibri/core/content/templatetags/content_tags.py +++ b/kolibri/core/content/templatetags/content_tags.py @@ -12,10 +12,6 @@ {% content_renderer_assets %} """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django import template from .. import hooks diff --git a/kolibri/core/content/test/test_channel_order.py b/kolibri/core/content/test/test_channel_order.py index 9939d21dfd4..ed409573e1f 100644 --- a/kolibri/core/content/test/test_channel_order.py +++ b/kolibri/core/content/test/test_channel_order.py @@ -1,8 +1,8 @@ import uuid +from io import StringIO from django.core.management import call_command from django.test import TestCase -from django.utils import six from kolibri.core.content import models as content @@ -13,7 +13,7 @@ def _refresh_data(self, *args): obj.refresh_from_db() def setUp(self): - self.out = six.StringIO() + self.out = StringIO() node = content.ContentNode.objects.create( id=uuid.uuid4(), title="test", diff --git a/kolibri/core/content/test/test_import_export.py b/kolibri/core/content/test/test_import_export.py index d1952cc9469..6053c651a5b 100644 --- a/kolibri/core/content/test/test_import_export.py +++ b/kolibri/core/content/test/test_import_export.py @@ -5,12 +5,12 @@ import tempfile import time import uuid +from io import StringIO from django.core.management import call_command from django.core.management import CommandError from django.db.models import Q from django.test import TestCase -from django.utils import six from le_utils.constants import content_kinds from mock import call from mock import MagicMock @@ -1538,7 +1538,7 @@ def test_local_import_with_detected_manifest_file( # according to channel_id in the detected manifest file. get_import_export_mock.assert_called_once_with( self.the_channel_id, - {six.text_type(self.c2c1_node_id)}, + {str(self.c2c1_node_id)}, None, False, renderable_only=True, @@ -1591,7 +1591,7 @@ def test_local_import_with_local_manifest_file_and_node_ids( get_import_export_mock.return_value = (0, [], 0) - manifest_file = six.StringIO( + manifest_file = StringIO( json.dumps( { "channels": [ @@ -1655,7 +1655,7 @@ def test_local_import_with_local_manifest_file_with_multiple_versions( manager = DiskChannelResourceImportManager.from_manifest( self.the_channel_id, path=import_source_dir, - manifest_file=six.StringIO( + manifest_file=StringIO( json.dumps( { "channels": [ @@ -1692,7 +1692,7 @@ def test_local_import_with_local_manifest_file_with_multiple_versions( # list of node_ids built from all versions of the channel_id channel. get_import_export_mock.assert_called_once_with( self.the_channel_id, - {six.text_type(self.c2c1_node_id), six.text_type(self.c2c2_node_id)}, + {str(self.c2c1_node_id), str(self.c2c2_node_id)}, None, False, renderable_only=True, @@ -1739,7 +1739,7 @@ def test_local_import_with_detected_manifest_file_and_node_ids( # of node_ids, ignoring the detected manifest file. get_import_export_mock.assert_called_once_with( self.the_channel_id, - {six.text_type(self.c2c2_node_id)}, + {str(self.c2c2_node_id)}, None, False, renderable_only=True, @@ -1795,7 +1795,7 @@ def test_local_import_with_detected_manifest_file_and_manifest_file( manager = DiskChannelResourceImportManager.from_manifest( self.the_channel_id, path=import_source_dir, - manifest_file=six.StringIO( + manifest_file=StringIO( json.dumps( { "channels": [ @@ -1818,7 +1818,7 @@ def test_local_import_with_detected_manifest_file_and_manifest_file( # node_ids according to channel_id in the provided manifest file. get_import_export_mock.assert_called_once_with( self.the_channel_id, - {six.text_type(self.c2c2_node_id)}, + {str(self.c2c2_node_id)}, None, False, renderable_only=True, @@ -1890,7 +1890,7 @@ def test_remote_import_with_local_manifest_file( manager = RemoteChannelResourceImportManager.from_manifest( self.the_channel_id, - manifest_file=six.StringIO( + manifest_file=StringIO( json.dumps( { "channels": [ @@ -1912,7 +1912,7 @@ def test_remote_import_with_local_manifest_file( # channel_id in the provided manifest file. get_import_export_mock.assert_called_once_with( self.the_channel_id, - {six.text_type(self.c2c1_node_id)}, + {str(self.c2c1_node_id)}, None, False, renderable_only=True, diff --git a/kolibri/core/content/test/test_redirectcontent.py b/kolibri/core/content/test/test_redirectcontent.py index 505defa45c1..de881ff5d58 100644 --- a/kolibri/core/content/test/test_redirectcontent.py +++ b/kolibri/core/content/test/test_redirectcontent.py @@ -1,9 +1,9 @@ import uuid +from urllib.parse import urlencode from django.test import TestCase from django.urls import reverse from mock import patch -from six.moves.urllib.parse import urlencode from kolibri.core.content.models import ContentNode diff --git a/kolibri/core/content/utils/channel_import.py b/kolibri/core/content/utils/channel_import.py index 80b4f8ab577..51ca0ddb712 100644 --- a/kolibri/core/content/utils/channel_import.py +++ b/kolibri/core/content/utils/channel_import.py @@ -6,7 +6,6 @@ from django.apps import apps from django.db.models.fields.related import ForeignKey -from six import string_types from sqlalchemy import and_ from sqlalchemy import or_ from sqlalchemy.dialects.postgresql import insert @@ -249,7 +248,7 @@ def __init__( self.partial = partial - if isinstance(source, string_types): + if isinstance(source, str): if self.partial: raise ValueError( "partial init argument to channel import class can only be used with dict imports" diff --git a/kolibri/core/content/utils/resource_import.py b/kolibri/core/content/utils/resource_import.py index 78759de2fba..66883fa6cd5 100644 --- a/kolibri/core/content/utils/resource_import.py +++ b/kolibri/core/content/utils/resource_import.py @@ -6,8 +6,6 @@ import requests from le_utils.constants import content_kinds -from six import string_types -from six import with_metaclass from kolibri.core.analytics.tasks import schedule_ping from kolibri.core.content.errors import InsufficientStorageSpaceError @@ -63,7 +61,7 @@ def lookup_channel_listing_status(channel_id, baseurl=None): return channel_info.get("public", None) -class ResourceImportManagerBase(with_metaclass(ABCMeta, JobProgressMixin)): +class ResourceImportManagerBase(JobProgressMixin, metaclass=ABCMeta): public = None def __init__( @@ -100,7 +98,7 @@ def from_manifest(cls, channel_id, manifest_file, **kwargs): raise TypeError("Unexpected keyword argument node_ids") if "exclude_node_ids" in kwargs: raise TypeError("Unexpected keyword argument exclude_node_ids") - if isinstance(manifest_file, string_types): + if isinstance(manifest_file, str): manifest_file = open(manifest_file, "r") content_manifest = ContentManifest() content_manifest.read_file(manifest_file) diff --git a/kolibri/core/content/views.py b/kolibri/core/content/views.py index 4ff31ace151..010e2fa98c9 100644 --- a/kolibri/core/content/views.py +++ b/kolibri/core/content/views.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging from django.http import Http404 diff --git a/kolibri/core/content/zip_wsgi.py b/kolibri/core/content/zip_wsgi.py index b7fdaf2aadf..7cf0da8145a 100644 --- a/kolibri/core/content/zip_wsgi.py +++ b/kolibri/core/content/zip_wsgi.py @@ -1,13 +1,10 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging import mimetypes import os import re import time import zipfile +from urllib.parse import unquote import html5lib from cheroot import wsgi @@ -23,7 +20,6 @@ from django.utils.cache import patch_response_headers from django.utils.encoding import force_str from django.utils.http import http_date -from six.moves.urllib.parse import unquote from kolibri.core.content.errors import InvalidStorageFilenameError from kolibri.core.content.utils.paths import get_content_storage_file_path diff --git a/kolibri/core/decorators.py b/kolibri/core/decorators.py index 833fc443efa..111ba1dbecb 100644 --- a/kolibri/core/decorators.py +++ b/kolibri/core/decorators.py @@ -1,12 +1,7 @@ """ Modified and extended from https://github.com/camsaul/django-rest-params/blob/master/django_rest_params/decorators.py """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import hashlib -import sys from threading import local from django.core.cache import cache @@ -14,7 +9,6 @@ from django.views.decorators.http import etag from rest_framework.exceptions import APIException from rest_framework.views import APIView -from six import string_types from kolibri import __version__ as kolibri_version @@ -39,10 +33,7 @@ class MissingRequiredParamsException(APIException): # Types that we'll all for as 'tuple' params TUPLE_TYPES = tuple, set, frozenset, list -if sys.version_info > (3, 0): - VALID_TYPES = int, float, str, bool -else: - VALID_TYPES = int, float, str, unicode, bool # noqa F821 +VALID_TYPES = int, float, str, bool class ParamValidator(object): @@ -89,7 +80,7 @@ def check_non_tuple_types(self, param): elif self.param_type == float: param = float(param) elif self.param_type == str: - if not isinstance(param, string_types): + if not isinstance(param, str): raise AssertionError elif self.param_type == bool: param = str(param).lower() # bool isn't case sensitive @@ -205,7 +196,7 @@ def set_constraints(self, suffix, value): self.default = value elif suffix == "field": - if not isinstance(suffix, string_types): + if not isinstance(suffix, str): raise AssertionError self.field = value else: diff --git a/kolibri/core/device/management/commands/provisiondevice.py b/kolibri/core/device/management/commands/provisiondevice.py index d71609d09ef..d94f15d1ec6 100644 --- a/kolibri/core/device/management/commands/provisiondevice.py +++ b/kolibri/core/device/management/commands/provisiondevice.py @@ -6,7 +6,6 @@ from django.conf import settings from django.core.management.base import BaseCommand from django.core.management.base import CommandError -from django.utils import six from kolibri.core.auth.constants.facility_presets import presets from kolibri.core.device.utils import get_facility_by_name @@ -22,7 +21,7 @@ def get_user_response(prompt, valid_answers=None, to_lower_case=True): while not answer or ( valid_answers is not None and answer.lower() not in valid_answers ): - answer = six.moves.input(prompt) + answer = input(prompt) if to_lower_case: return answer.lower() return answer diff --git a/kolibri/core/device/test/test_device_provision.py b/kolibri/core/device/test/test_device_provision.py index 3f680acce3d..6e5c937e68d 100644 --- a/kolibri/core/device/test/test_device_provision.py +++ b/kolibri/core/device/test/test_device_provision.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import json import os import tempfile diff --git a/kolibri/core/deviceadmin/management/commands/dbbackup.py b/kolibri/core/deviceadmin/management/commands/dbbackup.py index a9120bfaea3..e7cc38eec27 100644 --- a/kolibri/core/deviceadmin/management/commands/dbbackup.py +++ b/kolibri/core/deviceadmin/management/commands/dbbackup.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging from django.core.management.base import BaseCommand diff --git a/kolibri/core/deviceadmin/management/commands/dbrestore.py b/kolibri/core/deviceadmin/management/commands/dbrestore.py index b1aa1faa1dc..ee9063feba6 100644 --- a/kolibri/core/deviceadmin/management/commands/dbrestore.py +++ b/kolibri/core/deviceadmin/management/commands/dbrestore.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging import os diff --git a/kolibri/core/deviceadmin/tests/test_dbbackup.py b/kolibri/core/deviceadmin/tests/test_dbbackup.py index 60dfc5ee1cf..ddee7b5d220 100644 --- a/kolibri/core/deviceadmin/tests/test_dbbackup.py +++ b/kolibri/core/deviceadmin/tests/test_dbbackup.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import os import tempfile diff --git a/kolibri/core/deviceadmin/tests/test_dbrestore.py b/kolibri/core/deviceadmin/tests/test_dbrestore.py index 0a50dff0d42..ed6351fc66f 100644 --- a/kolibri/core/deviceadmin/tests/test_dbrestore.py +++ b/kolibri/core/deviceadmin/tests/test_dbrestore.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import os import random import tempfile diff --git a/kolibri/core/deviceadmin/utils.py b/kolibri/core/deviceadmin/utils.py index a83d08838ba..4c5e6b2bfc0 100644 --- a/kolibri/core/deviceadmin/utils.py +++ b/kolibri/core/deviceadmin/utils.py @@ -2,7 +2,6 @@ import logging import os import re -import sys from datetime import datetime from django import db @@ -19,15 +18,10 @@ logger = logging.getLogger(__name__) -# Use encoded text for Python 3 (doesn't work in Python 2!) +# Use encoded text KWARGS_IO_READ = {"mode": "r", "encoding": "utf-8"} KWARGS_IO_WRITE = {"mode": "w", "encoding": "utf-8"} -# Use binary file mode for Python 2 (doesn't work in Python 3!) -if sys.version_info < (3,): - KWARGS_IO_READ = {"mode": "rb"} - KWARGS_IO_WRITE = {"mode": "wb"} - def default_backup_folder(): return os.path.join(KOLIBRI_HOME, "backups") diff --git a/kolibri/core/discovery/serializers.py b/kolibri/core/discovery/serializers.py index 9486669c60c..cea9109d267 100644 --- a/kolibri/core/discovery/serializers.py +++ b/kolibri/core/discovery/serializers.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from rest_framework import serializers from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ValidationError diff --git a/kolibri/core/discovery/test/test_api.py b/kolibri/core/discovery/test/test_api.py index e34782514b9..97367c5591e 100644 --- a/kolibri/core/discovery/test/test_api.py +++ b/kolibri/core/discovery/test/test_api.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import uuid import mock diff --git a/kolibri/core/discovery/test/test_filesystem_utils.py b/kolibri/core/discovery/test/test_filesystem_utils.py index bfcb76d036a..0d8187fbc47 100644 --- a/kolibri/core/discovery/test/test_filesystem_utils.py +++ b/kolibri/core/discovery/test/test_filesystem_utils.py @@ -1,11 +1,6 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import ntpath import os import posixpath -import sys from django.test import TestCase from mock import patch @@ -70,9 +65,7 @@ def __init__(self, disk_sizes): self.mocked_disk_usage = _get_mocked_disk_usage(disk_sizes) def __call__(self, f): - if sys.version_info >= (3, 3): - return patch("shutil.disk_usage", self.mocked_disk_usage)(f) - return patch("os.statvfs", self.mocked_disk_usage)(f) + return patch("shutil.disk_usage", self.mocked_disk_usage)(f) def patch_os_access(readable, writable): diff --git a/kolibri/core/discovery/utils/filesystem/posix.py b/kolibri/core/discovery/utils/filesystem/posix.py index 64d1b91e2d7..fd323962649 100644 --- a/kolibri/core/discovery/utils/filesystem/posix.py +++ b/kolibri/core/discovery/utils/filesystem/posix.py @@ -150,27 +150,10 @@ def get_drive_list(): def _get_drive_usage(path): """ - Use Python libraries to get drive space/usage statistics. Prior to v3.3, use `os.statvfs`; - on v3.3+, use the more accurate `shutil.disk_usage`. + Use Python libraries to get drive space/usage statistics. """ - if sys.version_info >= (3, 3): - usage = shutil.disk_usage(path) - return {"total": usage.total, "used": usage.used, "free": usage.free} - if on_android(): - from jnius import autoclass - - StatFs = autoclass("android.os.StatFs") - AndroidString = autoclass("java.lang.String") - stats = StatFs(AndroidString(path)) - return { - "total": stats.getBlockCountLong() * stats.getBlockSizeLong(), - "free": stats.getAvailableBlocksLong() * stats.getBlockSizeLong(), - } - # with os.statvfs, we need to multiple block sizes by block counts to get bytes - stats = os.statvfs(path) - total = stats.f_frsize * stats.f_blocks - free = stats.f_frsize * stats.f_bavail - return {"total": total, "free": free, "used": total - free} + usage = shutil.disk_usage(path) + return {"total": usage.total, "used": usage.used, "free": usage.free} def _try_to_get_drive_info_from_dbus(device): diff --git a/kolibri/core/discovery/utils/network/broadcast.py b/kolibri/core/discovery/utils/network/broadcast.py index 09f5f95a21a..2047ac5c99f 100644 --- a/kolibri/core/discovery/utils/network/broadcast.py +++ b/kolibri/core/discovery/utils/network/broadcast.py @@ -6,8 +6,6 @@ from magicbus.base import Bus from magicbus.plugins import SimplePlugin -from six import integer_types -from six import string_types from zeroconf import get_all_addresses from zeroconf import InterfaceChoice from zeroconf import NonUniqueNameException @@ -89,7 +87,7 @@ def __init__( ): # Zeroconf wants socket.inet_aton() format, so make sure we have string with this class # which we convert when interfacing with Zeroconf - if ip is not None and not isinstance(ip, string_types): + if ip is not None and not isinstance(ip, str): raise TypeError("IP must be a string, not {}".format(type(ip))) self.id = instance_id @@ -163,9 +161,9 @@ def to_service_info(self, zeroconf_id=None): properties = {} for key, val in self.device_info.items(): - if not isinstance(key, string_types): + if not isinstance(key, str): raise TypeError("Keys for the service info properties must be strings") - if not isinstance(val, string_types + integer_types + (bool,)): + if not isinstance(val, (str, int, bool)): raise TypeError( "Values for the service info properties must be a string, an integer or a boolean" ) diff --git a/kolibri/core/discovery/utils/network/client.py b/kolibri/core/discovery/utils/network/client.py index 6c3e2d7e840..1b2c31e1abb 100644 --- a/kolibri/core/discovery/utils/network/client.py +++ b/kolibri/core/discovery/utils/network/client.py @@ -1,8 +1,7 @@ import logging +from urllib.parse import urlparse import requests -from six import raise_from -from six.moves.urllib.parse import urlparse import kolibri from . import errors @@ -145,34 +144,25 @@ def request(self, method, path, **kwargs): requests.exceptions.InvalidHeader, requests.exceptions.InvalidJSONError, ) as e: - raise_from( - errors.NetworkLocationConnectionFailure( - "Unable to connect: {}".format(url) - ), - e, - ) + raise errors.NetworkLocationConnectionFailure( + "Unable to connect: {}".format(url) + ) from e except ( requests.exceptions.ReadTimeout, requests.exceptions.TooManyRedirects, ) as e: - raise_from( - errors.NetworkLocationResponseTimeout( - "Response timeout: {}".format(url) - ), - e, - ) + raise errors.NetworkLocationResponseTimeout( + "Response timeout: {}".format(url) + ) from e except ( requests.exceptions.HTTPError, requests.exceptions.ContentDecodingError, requests.exceptions.ChunkedEncodingError, requests.exceptions.RequestException, ) as e: - raise_from( - errors.NetworkLocationResponseFailure( - "Response failure: {}".format(url), response=response - ), - e, - ) + raise errors.NetworkLocationResponseFailure( + "Response failure: {}".format(url), response=response + ) from e def connect(self, raise_if_unavailable=True): # noqa: C901 """ @@ -239,9 +229,9 @@ def connect(self, raise_if_unavailable=True): # noqa: C901 "Invalid JSON returned when attempting to connect to a remote server" ) if raise_if_unavailable: - raise_from( - errors.NetworkLocationInvalidResponse("Invalid JSON returned"), e - ) + raise errors.NetworkLocationInvalidResponse( + "Invalid JSON returned" + ) from e return False return True diff --git a/kolibri/core/discovery/utils/network/connections.py b/kolibri/core/discovery/utils/network/connections.py index 55fb8433a0a..e32b8edb278 100644 --- a/kolibri/core/discovery/utils/network/connections.py +++ b/kolibri/core/discovery/utils/network/connections.py @@ -1,13 +1,13 @@ import socket from contextlib import closing from contextlib import contextmanager +from ipaddress import ip_address from . import errors from .client import NetworkClient from .urls import parse_address_into_components from kolibri.core.discovery.models import ConnectionStatus from kolibri.core.discovery.models import NetworkLocation -from kolibri.core.discovery.utils.network.ipaddress import ip_address def check_if_port_open(base_url, timeout=1): diff --git a/kolibri/core/discovery/utils/network/ipaddress.py b/kolibri/core/discovery/utils/network/ipaddress.py deleted file mode 100644 index 9afd4d4779b..00000000000 --- a/kolibri/core/discovery/utils/network/ipaddress.py +++ /dev/null @@ -1,1822 +0,0 @@ -# Copyright 2007 Google Inc. -# Licensed to PSF under a Contributor Agreement. -# ToDo: Remove any other unneccesary functions/logic(https://github.com/learningequality/kolibri/issues/10363) -"""A fast, lightweight IPv4/IPv6 manipulation library in Python. - -This library is used to create/poke/manipulate IPv4 and IPv6 addresses -and networks. - -""" - -__version__ = "1.0" - -import functools -import re -import six -from django.utils.functional import cached_property -from kolibri.utils.lru_cache import lru_cache - -IPV4LENGTH = 32 -IPV6LENGTH = 128 - - -class AddressValueError(ValueError): - """A Value Error related to the address.""" - - -class NetmaskValueError(ValueError): - """A Value Error related to the netmask.""" - - -def ip_address(address): - """Take an IP string/int and return an object of the correct type. - - Args: - address: A string or integer, the IP address. Either IPv4 or - IPv6 addresses may be supplied; integers less than 2**32 will - be considered to be IPv4 by default. - - Returns: - An IPv4Address or IPv6Address object. - - Raises: - ValueError: if the *address* passed isn't either a v4 or a v6 - address - - """ - try: - return IPv4Address(address) - except (AddressValueError, NetmaskValueError): - pass - - try: - return IPv6Address(address) - except (AddressValueError, NetmaskValueError): - pass - - raise ValueError("%s does not appear to be an IPv4 or IPv6 address" % address) - - -def _is_ascii(string): - # Match any ASCII character - match = re.search(r"^[\x00-\x7F]+$", string) - return bool(match) - - -def _is_number(x): - return str(x).isdigit() if not isinstance(x, six.string_types) else False - - -def _to_bytes(n, length=1, byteorder="big"): - if byteorder == "little": - order = range(length) - elif byteorder == "big": - order = reversed(range(length)) - else: - raise ValueError("byteorder must be either 'little' or 'big'") - - return bytes((n >> i * 8) & 0xFF for i in order) - - -def _from_bytes(bytes, byteorder="big", signed=False): - if byteorder == "little": - little_ordered = list(bytes) - elif byteorder == "big": - little_ordered = list(reversed(bytes)) - else: - raise ValueError("byteorder must be either 'little' or 'big'") - - n = sum(b << i * 8 for i, b in enumerate(little_ordered)) - if signed and little_ordered and (little_ordered[-1] & 0x80): - n -= 1 << 8 * len(little_ordered) - - return n - - -def _split_optional_netmask(address): - """Helper to split the netmask and raise AddressValueError if needed""" - addr = str(address).split("/") - if len(addr) > 2: - raise AddressValueError("Only one '/' permitted in %s" % address) - return addr - - -def _find_address_range(addresses): - """Find a sequence of sorted deduplicated IPv#Address. - Args: - addresses: a list of IPv#Address objects. - Yields: - A tuple containing the first and last IP addresses in the sequence. - """ - it = iter(addresses) - first = last = next(it) - for ip in it: - if ip._ip != last._ip + 1: - yield first, last - first = ip - last = ip - yield first, last - - -def _count_righthand_zero_bits(number, bits): - """Count the number of zero bits on the right hand side. - Args: - number: an integer. - bits: maximum number of bits to count. - Returns: - The number of zero bits on the right hand side of the number. - """ - if number == 0: - return bits - return min(bits, (~number & (number - 1)).bit_length()) - - -def summarize_address_range(first, last): - """Summarize a network range given the first and last IP addresses. - Example: - >>> list(summarize_address_range(IPv4Address('192.0.2.0'), - ... IPv4Address('192.0.2.130'))) - ... #doctest: +NORMALIZE_WHITESPACE - [IPv4Network('192.0.2.0/25'), IPv4Network('192.0.2.128/31'), - IPv4Network('192.0.2.130/32')] - Args: - first: the first IPv4Address or IPv6Address in the range. - last: the last IPv4Address or IPv6Address in the range. - Returns: - An iterator of the summarized IPv(4|6) network objects. - Raise: - TypeError: - If the first and last objects are not IP addresses. - If the first and last objects are not the same version. - ValueError: - If the last object is not greater than the first. - If the version of the first address is not 4 or 6. - """ - if not (isinstance(first, _BaseAddress) and isinstance(last, _BaseAddress)): - raise TypeError("first and last must be IP addresses, not networks") - if first.version != last.version: - raise TypeError("%s and %s are not of the same version" % (first, last)) - if first > last: - raise ValueError("last IP address must be greater than first") - - if first.version == 4: - ip = IPv4Network - elif first.version == 6: - ip = IPv6Network - else: - raise ValueError("unknown IP version") - - ip_bits = first._max_prefixlen - first_int = first._ip - last_int = last._ip - while first_int <= last_int: - nbits = min( - _count_righthand_zero_bits(first_int, ip_bits), - (last_int - first_int + 1).bit_length() - 1, - ) - net = ip((first_int, ip_bits - nbits)) - yield net - first_int += 1 << nbits - if first_int - 1 == ip._ALL_ONES: - break - - -def _collapse_addresses_internal(addresses): - """Loops through the addresses, collapsing concurrent netblocks. - Example: - ip1 = IPv4Network('192.0.2.0/26') - ip2 = IPv4Network('192.0.2.64/26') - ip3 = IPv4Network('192.0.2.128/26') - ip4 = IPv4Network('192.0.2.192/26') - _collapse_addresses_internal([ip1, ip2, ip3, ip4]) -> - [IPv4Network('192.0.2.0/24')] - This shouldn't be called directly; it is called via - collapse_addresses([]). - Args: - addresses: A list of IPv4Network's or IPv6Network's - Returns: - A list of IPv4Network's or IPv6Network's depending on what we were - passed. - """ - # First merge - to_merge = list(addresses) - subnets = {} - while to_merge: - net = to_merge.pop() - supernet = net.supernet() - existing = subnets.get(supernet) - if existing is None: - subnets[supernet] = net - elif existing != net: - # Merge consecutive subnets - del subnets[supernet] - to_merge.append(supernet) - # Then iterate over resulting networks, skipping subsumed subnets - last = None - for net in sorted(subnets.values()): - if last is not None: - # Since they are sorted, last.network_address <= net.network_address - # is a given. - if last.broadcast_address >= net.broadcast_address: - continue - yield net - last = net - - -def collapse_addresses(addresses): # noqa C901 - """Collapse a list of IP objects. - Example: - collapse_addresses([IPv4Network('192.0.2.0/25'), - IPv4Network('192.0.2.128/25')]) -> - [IPv4Network('192.0.2.0/24')] - Args: - addresses: An iterator of IPv4Network or IPv6Network objects. - Returns: - An iterator of the collapsed IPv(4|6)Network objects. - Raises: - TypeError: If passed a list of mixed version objects. - """ - addrs = [] - ips = [] - nets = [] - - # split IP addresses and networks - for ip in addresses: - if isinstance(ip, _BaseAddress): - if ips and ips[-1]._version != ip._version: - raise TypeError("%s and %s are not of the same version" % (ip, ips[-1])) - ips.append(ip) - elif ip._prefixlen == ip._max_prefixlen: - if ips and ips[-1]._version != ip._version: - raise TypeError("%s and %s are not of the same version" % (ip, ips[-1])) - try: - ips.append(ip.ip) - except AttributeError: - ips.append(ip.network_address) - else: - if nets and nets[-1]._version != ip._version: - raise TypeError( - "%s and %s are not of the same version" % (ip, nets[-1]) - ) - nets.append(ip) - - # sort and dedup - ips = sorted(set(ips)) - - # find consecutive address ranges in the sorted sequence and summarize them - if ips: - for first, last in _find_address_range(ips): - addrs.extend(summarize_address_range(first, last)) - - return _collapse_addresses_internal(addrs + nets) - - -class _IPAddressBase: - - """The mother class.""" - - __slots__ = () - - @property - def exploded(self): - """Return the longhand version of the IP address as a string.""" - return self._explode_shorthand_ip_string() - - @property - def compressed(self): - """Return the shorthand version of the IP address as a string.""" - return str(self) - - @property - def reverse_pointer(self): - """The name of the reverse DNS pointer for the IP address, e.g.: - >>> ipaddress.ip_address("127.0.0.1").reverse_pointer - '1.0.0.127.in-addr.arpa' - >>> ipaddress.ip_address("2001:db8::1").reverse_pointer - '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa' - """ - return self._reverse_pointer() - - @property - def version(self): - msg = "%200s has no version specified" % (type(self),) - raise NotImplementedError(msg) - - def _check_int_address(self, address): - if address < 0: - msg = "%d (< 0) is not permitted as an IPv%d address" - raise AddressValueError(msg % (address, self._version)) - if address > self._ALL_ONES: - msg = "%d (>= 2**%d) is not permitted as an IPv%d address" - raise AddressValueError(msg % (address, self._max_prefixlen, self._version)) - - @classmethod - def _ip_int_from_prefix(cls, prefixlen): - """Turn the prefix length into a bitwise netmask - Args: - prefixlen: An integer, the prefix length. - Returns: - An integer. - """ - return cls._ALL_ONES ^ (cls._ALL_ONES >> prefixlen) - - @classmethod - def _prefix_from_ip_int(cls, ip_int): - """Return prefix length from the bitwise netmask. - Args: - ip_int: An integer, the netmask in expanded bitwise format - Returns: - An integer, the prefix length. - Raises: - ValueError: If the input intermingles zeroes & ones - """ - trailing_zeroes = _count_righthand_zero_bits(ip_int, cls._max_prefixlen) - prefixlen = cls._max_prefixlen - trailing_zeroes - leading_ones = ip_int >> trailing_zeroes - all_ones = (1 << prefixlen) - 1 - if leading_ones != all_ones: - byteslen = cls._max_prefixlen // 8 - details = _to_bytes(ip_int, byteslen, "big") - msg = "Netmask pattern %r mixes zeroes & ones" - raise ValueError(msg % details) - return prefixlen - - @classmethod - def _report_invalid_netmask(cls, netmask_str): - msg = "%r is not a valid netmask" % netmask_str - raise six.raise_from(NetmaskValueError(msg), None) - - @classmethod - def _prefix_from_prefix_string(cls, prefixlen_str): - """Return prefix length from a numeric string - Args: - prefixlen_str: The string to be converted - Returns: - An integer, the prefix length. - Raises: - NetmaskValueError: If the input is not a valid netmask - """ - # int allows a leading +/- as well as surrounding whitespace, - # so we ensure that isn't the case - if not (_is_ascii(prefixlen_str) and prefixlen_str.isdigit()): - cls._report_invalid_netmask(prefixlen_str) - try: - prefixlen = int(prefixlen_str) - except ValueError: - cls._report_invalid_netmask(prefixlen_str) - if not (0 <= prefixlen <= cls._max_prefixlen): - cls._report_invalid_netmask(prefixlen_str) - return prefixlen - - @classmethod - def _prefix_from_ip_string(cls, ip_str): - """Turn a netmask/hostmask string into a prefix length - Args: - ip_str: The netmask/hostmask to be converted - Returns: - An integer, the prefix length. - Raises: - NetmaskValueError: If the input is not a valid netmask/hostmask - """ - # Parse the netmask/hostmask like an IP address. - try: - ip_int = cls._ip_int_from_string(ip_str) - except AddressValueError: - cls._report_invalid_netmask(ip_str) - - # Try matching a netmask (this would be /1*0*/ as a bitwise regexp). - # Note that the two ambiguous cases (all-ones and all-zeroes) are - # treated as netmasks. - try: - return cls._prefix_from_ip_int(ip_int) - except ValueError: - pass - - # Invert the bits, and try matching a /0+1+/ hostmask instead. - ip_int ^= cls._ALL_ONES - try: - return cls._prefix_from_ip_int(ip_int) - except ValueError: - cls._report_invalid_netmask(ip_str) - - @classmethod - def _split_addr_prefix(cls, address): - """Helper function to parse address of Network/Interface. - Arg: - address: Argument of Network/Interface. - Returns: - (addr, prefix) tuple. - """ - - if _is_number(address): - return address, cls._max_prefixlen - - if not isinstance(address, tuple): - # Assume input argument to be string or any object representation - # which converts into a formatted IP prefix string. - address = _split_optional_netmask(address) - - # Constructing from a tuple (addr, [mask]) - if len(address) > 1: - return address - return address[0], cls._max_prefixlen - - def __reduce__(self): - return self.__class__, (str(self),) - - -_address_fmt_re = None - - -@functools.total_ordering -class _BaseAddress(_IPAddressBase): - - """A generic IP object. - This IP class contains the version independent methods which are - used by single IP addresses. - """ - - __slots__ = () - - def __int__(self): - return self._ip - - def __eq__(self, other): - try: - return self._ip == other._ip and self._version == other._version - except AttributeError: - return NotImplemented - - def __lt__(self, other): - if not isinstance(other, _BaseAddress): - return NotImplemented - if self._version != other._version: - raise TypeError("%s and %s are not of the same version" % (self, other)) - if self._ip != other._ip: - return self._ip < other._ip - return False - - # Shorthand for Integer addition and subtraction. This is not - # meant to ever support addition/subtraction of addresses. - def __add__(self, other): - if not _is_number(other): - return NotImplemented - return self.__class__(int(self) + other) - - def __sub__(self, other): - if not _is_number(other): - return NotImplemented - return self.__class__(int(self) - other) - - def __repr__(self): - return "%s(%r)" % (self.__class__.__name__, str(self)) - - def __str__(self): - return str(self._string_from_ip_int(self._ip)) - - def __hash__(self): - return hash(hex(int(self._ip))) - - def _get_address_key(self): - return (self._version, self) - - def __reduce__(self): - return self.__class__, (self._ip,) - - def __format__(self, fmt): - """Returns an IP address as a formatted string. - Supported presentation types are: - 's': returns the IP address as a string (default) - 'b': converts to binary and returns a zero-padded string - 'X' or 'x': converts to upper- or lower-case hex and returns a zero-padded string - 'n': the same as 'b' for IPv4 and 'x' for IPv6 - For binary and hex presentation types, the alternate form specifier - '#' and the grouping option '_' are supported. - """ - - # Support string formatting - if not fmt or fmt[-1] == "s": - return format(str(self), fmt) - - # From here on down, support for 'bnXx' - global _address_fmt_re - if _address_fmt_re is None: - import re - - _address_fmt_re = re.compile("(#?)(_?)([xbnX])") - - m = _address_fmt_re.fullmatch(fmt) - if not m: - return super().__format__(fmt) - - alternate, grouping, fmt_base = m.groups() - - # Set some defaults - if fmt_base == "n": - if self._version == 4: - fmt_base = "b" # Binary is default for ipv4 - else: - fmt_base = "x" # Hex is default for ipv6 - - if fmt_base == "b": - padlen = self._max_prefixlen - else: - padlen = self._max_prefixlen // 4 - - if grouping: - padlen += padlen // 4 - 1 - - if alternate: - padlen += 2 # 0b or 0x - - return format(int(self), "%s0%s%s%s" % (alternate, padlen, grouping, fmt_base)) - - -@functools.total_ordering -class _BaseNetwork(_IPAddressBase): - """A generic IP network object. - This IP class contains the version independent methods which are - used by networks. - """ - - def __repr__(self): - return "%s(%r)" % (self.__class__.__name__, str(self)) - - def __str__(self): - return "%s/%d" % (self.network_address, self.prefixlen) - - def __iter__(self): - network = int(self.network_address) - broadcast = int(self.broadcast_address) - for x in range(network, broadcast + 1): - yield self._address_class(x) - - def __getitem__(self, n): - network = int(self.network_address) - broadcast = int(self.broadcast_address) - if n >= 0: - if network + n > broadcast: - raise IndexError("address out of range") - return self._address_class(network + n) - else: - n += 1 - if broadcast + n < network: - raise IndexError("address out of range") - return self._address_class(broadcast + n) - - def __lt__(self, other): - if not isinstance(other, _BaseNetwork): - return NotImplemented - if self._version != other._version: - raise TypeError("%s and %s are not of the same version" % (self, other)) - if self.network_address != other.network_address: - return self.network_address < other.network_address - if self.netmask != other.netmask: - return self.netmask < other.netmask - return False - - def __eq__(self, other): - try: - return ( - self._version == other._version - and self.network_address == other.network_address - and int(self.netmask) == int(other.netmask) - ) - except AttributeError: - return NotImplemented - - def __hash__(self): - return hash(int(self.network_address) ^ int(self.netmask)) - - def __contains__(self, other): - # always false if one is v4 and the other is v6. - if self._version != other._version: - return False - # dealing with another network. - if isinstance(other, _BaseNetwork): - return False - # dealing with another address - else: - # address - return other._ip & self.netmask._ip == self.network_address._ip - - def overlaps(self, other): - """Tell if self is partly contained in other.""" - return self.network_address in other or ( - self.broadcast_address in other - or (other.network_address in self or (other.broadcast_address in self)) - ) - - @cached_property - def broadcast_address(self): - return self._address_class(int(self.network_address) | int(self.hostmask)) - - @cached_property - def hostmask(self): - return self._address_class(int(self.netmask) ^ self._ALL_ONES) - - @property - def with_prefixlen(self): - return "%s/%d" % (self.network_address, self._prefixlen) - - @property - def with_netmask(self): - return "%s/%s" % (self.network_address, self.netmask) - - @property - def with_hostmask(self): - return "%s/%s" % (self.network_address, self.hostmask) - - @property - def num_addresses(self): - """Number of hosts in the current subnet.""" - return int(self.broadcast_address) - int(self.network_address) + 1 - - @property - def _address_class(self): - # Returning bare address objects (rather than interfaces) allows for - # more consistent behaviour across the network address, broadcast - # address and individual host addresses. - msg = "%200s has no associated address class" % (type(self),) - raise NotImplementedError(msg) - - @property - def prefixlen(self): - return self._prefixlen - - def address_exclude(self, other): - """Remove an address from a larger block. - For example: - addr1 = ip_network('192.0.2.0/28') - addr2 = ip_network('192.0.2.1/32') - list(addr1.address_exclude(addr2)) = - [IPv4Network('192.0.2.0/32'), IPv4Network('192.0.2.2/31'), - IPv4Network('192.0.2.4/30'), IPv4Network('192.0.2.8/29')] - or IPv6: - addr1 = ip_network('2001:db8::1/32') - addr2 = ip_network('2001:db8::1/128') - list(addr1.address_exclude(addr2)) = - [ip_network('2001:db8::1/128'), - ip_network('2001:db8::2/127'), - ip_network('2001:db8::4/126'), - ip_network('2001:db8::8/125'), - ... - ip_network('2001:db8:8000::/33')] - Args: - other: An IPv4Network or IPv6Network object of the same type. - Returns: - An iterator of the IPv(4|6)Network objects which is self - minus other. - Raises: - TypeError: If self and other are of differing address - versions, or if other is not a network object. - ValueError: If other is not completely contained by self. - """ - if not self._version == other._version: - raise TypeError("%s and %s are not of the same version" % (self, other)) - - if not isinstance(other, _BaseNetwork): - raise TypeError("%s is not a network object" % other) - - if not other.subnet_of(self): - raise ValueError("%s not contained in %s" % (other, self)) - if other == self: - return - - # Make sure we're comparing the network of other. - other = other.__class__("%s/%s" % (other.network_address, other.prefixlen)) - - s1, s2 = self.subnets() - while s1 != other and s2 != other: - if other.subnet_of(s1): - yield s2 - s1, s2 = s1.subnets() - elif other.subnet_of(s2): - yield s1 - s1, s2 = s2.subnets() - else: - # If we got here, there's a bug somewhere. - raise AssertionError( - "Error performing exclusion: " - "s1: %s s2: %s other: %s" % (s1, s2, other) - ) - if s1 == other: - yield s2 - elif s2 == other: - yield s1 - else: - # If we got here, there's a bug somewhere. - raise AssertionError( - "Error performing exclusion: " - "s1: %s s2: %s other: %s" % (s1, s2, other) - ) - - def compare_networks(self, other): - """Compare two IP objects. - This is only concerned about the comparison of the integer - representation of the network addresses. This means that the - host bits aren't considered at all in this method. If you want - to compare host bits, you can easily enough do a - 'HostA._ip < HostB._ip' - Args: - other: An IP object. - Returns: - If the IP versions of self and other are the same, returns: - -1 if self < other: - eg: IPv4Network('192.0.2.0/25') < IPv4Network('192.0.2.128/25') - IPv6Network('2001:db8::1000/124') < - IPv6Network('2001:db8::2000/124') - 0 if self == other - eg: IPv4Network('192.0.2.0/24') == IPv4Network('192.0.2.0/24') - IPv6Network('2001:db8::1000/124') == - IPv6Network('2001:db8::1000/124') - 1 if self > other - eg: IPv4Network('192.0.2.128/25') > IPv4Network('192.0.2.0/25') - IPv6Network('2001:db8::2000/124') > - IPv6Network('2001:db8::1000/124') - Raises: - TypeError if the IP versions are different. - """ - # does this need to raise a ValueError? - if self._version != other._version: - raise TypeError("%s and %s are not of the same type" % (self, other)) - # self._version == other._version below here: - if self.network_address < other.network_address: - return -1 - if self.network_address > other.network_address: - return 1 - # self.network_address == other.network_address below here: - if self.netmask < other.netmask: - return -1 - if self.netmask > other.netmask: - return 1 - return 0 - - def _get_networks_key(self): - """Network-only key function. - Returns an object that identifies this address' network and - netmask. This function is a suitable "key" argument for sorted() - and list.sort(). - """ - return (self._version, self.network_address, self.netmask) - - def subnets(self, prefixlen_diff=1, new_prefix=None): - """The subnets which join to make the current subnet. - In the case that self contains only one IP - (self._prefixlen == 32 for IPv4 or self._prefixlen == 128 - for IPv6), yield an iterator with just ourself. - Args: - prefixlen_diff: An integer, the amount the prefix length - should be increased by. This should not be set if - new_prefix is also set. - new_prefix: The desired new prefix length. This must be a - larger number (smaller prefix) than the existing prefix. - This should not be set if prefixlen_diff is also set. - Returns: - An iterator of IPv(4|6) objects. - Raises: - ValueError: The prefixlen_diff is too small or too large. - OR - prefixlen_diff and new_prefix are both set or new_prefix - is a smaller number than the current prefix (smaller - number means a larger network) - """ - if self._prefixlen == self._max_prefixlen: - yield self - return - - if new_prefix is not None: - if new_prefix < self._prefixlen: - raise ValueError("new prefix must be longer") - if prefixlen_diff != 1: - raise ValueError("cannot set prefixlen_diff and new_prefix") - prefixlen_diff = new_prefix - self._prefixlen - - if prefixlen_diff < 0: - raise ValueError("prefix length diff must be > 0") - new_prefixlen = self._prefixlen + prefixlen_diff - - if new_prefixlen > self._max_prefixlen: - raise ValueError( - "prefix length diff %d is invalid for netblock %s" - % (new_prefixlen, self) - ) - - start = int(self.network_address) - end = int(self.broadcast_address) + 1 - step = (int(self.hostmask) + 1) >> prefixlen_diff - for new_addr in range(start, end, step): - current = self.__class__((new_addr, new_prefixlen)) - yield current - - def supernet(self, prefixlen_diff=1, new_prefix=None): - """The supernet containing the current network. - Args: - prefixlen_diff: An integer, the amount the prefix length of - the network should be decreased by. For example, given a - /24 network and a prefixlen_diff of 3, a supernet with a - /21 netmask is returned. - Returns: - An IPv4 network object. - Raises: - ValueError: If self.prefixlen - prefixlen_diff < 0. I.e., you have - a negative prefix length. - OR - If prefixlen_diff and new_prefix are both set or new_prefix is a - larger number than the current prefix (larger number means a - smaller network) - """ - if self._prefixlen == 0: - return self - - if new_prefix is not None: - if new_prefix > self._prefixlen: - raise ValueError("new prefix must be shorter") - if prefixlen_diff != 1: - raise ValueError("cannot set prefixlen_diff and new_prefix") - prefixlen_diff = self._prefixlen - new_prefix - - new_prefixlen = self.prefixlen - prefixlen_diff - if new_prefixlen < 0: - raise ValueError( - "current prefixlen is %d, cannot have a prefixlen_diff of %d" - % (self.prefixlen, prefixlen_diff) - ) - return self.__class__( - ( - int(self.network_address) & (int(self.netmask) << prefixlen_diff), - new_prefixlen, - ) - ) - - @property - def is_multicast(self): - """Test if the address is reserved for multicast use. - Returns: - A boolean, True if the address is a multicast address. - See RFC 2373 2.7 for details. - """ - return self.network_address.is_multicast and self.broadcast_address.is_multicast - - @staticmethod - def _is_subnet_of(a, b): - try: - # Always false if one is v4 and the other is v6. - if a._version != b._version: - raise TypeError("%s and %s are not of the same version" % (a, b)) - return ( - b.network_address <= a.network_address - and b.broadcast_address >= a.broadcast_address - ) - except AttributeError: - raise TypeError( - "Unable to test subnet containment between %s and %s" % (a, b) - ) - - def subnet_of(self, other): - """Return True if this network is a subnet of other.""" - return self._is_subnet_of(self, other) - - def supernet_of(self, other): - """Return True if this network is a supernet of other.""" - return self._is_subnet_of(other, self) - - @property - def is_reserved(self): - """Test if the address is otherwise IETF reserved. - Returns: - A boolean, True if the address is within one of the - reserved IPv6 Network ranges. - """ - return self.network_address.is_reserved and self.broadcast_address.is_reserved - - @property - def is_link_local(self): - """Test if the address is reserved for link-local. - Returns: - A boolean, True if the address is reserved per RFC 4291. - """ - return ( - self.network_address.is_link_local and self.broadcast_address.is_link_local - ) - - @property - def is_private(self): - """Test if this network belongs to a private range. - Returns: - A boolean, True if the network is reserved per - iana-ipv4-special-registry or iana-ipv6-special-registry. - """ - return any( - self.network_address in priv_network - and self.broadcast_address in priv_network - for priv_network in self._constants._private_networks - ) - - @property - def is_global(self): - """Test if this address is allocated for public networks. - Returns: - A boolean, True if the address is not reserved per - iana-ipv4-special-registry or iana-ipv6-special-registry. - """ - return not self.is_private - - @property - def is_unspecified(self): - """Test if the address is unspecified. - Returns: - A boolean, True if this is the unspecified address as defined in - RFC 2373 2.5.2. - """ - return ( - self.network_address.is_unspecified - and self.broadcast_address.is_unspecified - ) - - @property - def is_loopback(self): - """Test if the address is a loopback address. - Returns: - A boolean, True if the address is a loopback address as defined in - RFC 2373 2.5.3. - """ - return self.network_address.is_loopback and self.broadcast_address.is_loopback - - -class _BaseConstants: - - _private_networks = [] - - -_BaseNetwork._constants = _BaseConstants - - -class _BaseV4: - - """Base IPv4 object. - The following methods are used by IPv4 objects in both single IP - addresses and networks. - """ - - __slots__ = () - _version = 4 - # Equivalent to 255.255.255.255 or 32 bits of 1's. - _ALL_ONES = (2 ** IPV4LENGTH) - 1 - - _max_prefixlen = IPV4LENGTH - # There are only a handful of valid v4 netmasks, so we cache them all - # when constructed (see _make_netmask()). - _netmask_cache = {} - - def _explode_shorthand_ip_string(self): - return str(self) - - @classmethod - def _make_netmask(cls, arg): - """Make a (netmask, prefix_len) tuple from the given argument. - Argument can be: - - an integer (the prefix length) - - a string representing the prefix length (e.g. "24") - - a string representing the prefix netmask (e.g. "255.255.255.0") - """ - if arg not in cls._netmask_cache: - if _is_number(arg): - prefixlen = arg - if not (0 <= prefixlen <= cls._max_prefixlen): - cls._report_invalid_netmask(prefixlen) - else: - try: - # Check for a netmask in prefix length form - prefixlen = cls._prefix_from_prefix_string(arg) - except NetmaskValueError: - # Check for a netmask or hostmask in dotted-quad form. - # This may raise NetmaskValueError. - prefixlen = cls._prefix_from_ip_string(arg) - netmask = IPv4Address(cls._ip_int_from_prefix(prefixlen)) - cls._netmask_cache[arg] = netmask, prefixlen - return cls._netmask_cache[arg] - - @classmethod - def _ip_int_from_string(cls, ip_str): - """Turn the given IP string into an integer for comparison. - Args: - ip_str: A string, the IP ip_str. - Returns: - The IP ip_str as an integer. - Raises: - AddressValueError: if ip_str isn't a valid IPv4 Address. - """ - if not ip_str: - raise AddressValueError("Address cannot be empty") - - octets = ip_str.split(".") - if len(octets) != 4: - raise AddressValueError("Expected 4 octets in %r" % ip_str) - - try: - return _from_bytes(list(map(cls._parse_octet, octets))) - except ValueError as exc: - raise six.raise_from(AddressValueError("%s in %r" % (exc, ip_str)), None) - - @classmethod - def _parse_octet(cls, octet_str): - """Convert a decimal octet into an integer. - Args: - octet_str: A string, the number to parse. - Returns: - The octet as an integer. - Raises: - ValueError: if the octet isn't strictly a decimal from [0..255]. - """ - if not octet_str: - raise ValueError("Empty octet not permitted") - # Reject non-ASCII digits. - if not (_is_ascii(octet_str) and octet_str.isdigit()): - msg = "Only decimal digits permitted in %r" - raise ValueError(msg % octet_str) - # We do the length check second, since the invalid character error - # is likely to be more informative for the user - if len(octet_str) > 3: - msg = "At most 3 characters permitted in %r" - raise ValueError(msg % octet_str) - # Handle leading zeros as strict as glibc's inet_pton() - # See security bug bpo-36384 - if octet_str != "0" and octet_str[0] == "0": - msg = "Leading zeros are not permitted in %r" - raise ValueError(msg % octet_str) - # Convert to integer (we know digits are legal) - octet_int = int(octet_str, 10) - if octet_int > 255: - raise ValueError("Octet %d (> 255) not permitted" % octet_int) - return octet_int - - @classmethod - def _string_from_ip_int(cls, ip_int): - """Turns a 32-bit integer into dotted decimal notation. - Args: - ip_int: An integer, the IP address. - Returns: - The IP address as a string in dotted decimal notation. - """ - return ".".join(list(map(str, _to_bytes(ip_int, 4, "big")))) - - def _reverse_pointer(self): - """Return the reverse DNS pointer name for the IPv4 address. - This implements the method described in RFC1035 3.5. - """ - reverse_octets = str(self).split(".")[::-1] - return ".".join(reverse_octets) + ".in-addr.arpa" - - @property - def max_prefixlen(self): - return self._max_prefixlen - - @property - def version(self): - return self._version - - -class IPv4Address(_BaseV4, _BaseAddress): - - """Represent and manipulate single IPv4 Addresses.""" - - __slots__ = ("_ip", "__weakref__") - - def __init__(self, address): - """ - Args: - address: A string or integer representing the IP - Additionally, an integer can be passed, so - IPv4Address('192.0.2.1') == IPv4Address(3221225985). - or, more generally - IPv4Address(int(IPv4Address('192.0.2.1'))) == - IPv4Address('192.0.2.1') - Raises: - AddressValueError: If ipaddress isn't a valid IPv4 address. - """ - # Efficient constructor from integer. - if _is_number(address): - self._check_int_address(address) - self._ip = address - return - - # Assume input argument to be string or any object representation - # which converts into a formatted IP string. - addr_str = str(address) - if "/" in addr_str: - raise AddressValueError("Unexpected '/' in %s" % address) - self._ip = self._ip_int_from_string(addr_str) - - @property - def is_reserved(self): - """Test if the address is otherwise IETF reserved. - Returns: - A boolean, True if the address is within the - reserved IPv4 Network range. - """ - return self in self._constants._reserved_network - - @property - @lru_cache() - def is_private(self): - """Test if this address is allocated for private networks. - Returns: - A boolean, True if the address is reserved per - iana-ipv4-special-registry. - """ - return any([self in net for net in self._constants._private_networks]) - - @property - @lru_cache() - def is_global(self): - return self not in self._constants._public_network and not self.is_private - - @property - def is_multicast(self): - """Test if the address is reserved for multicast use. - Returns: - A boolean, True if the address is multicast. - See RFC 3171 for details. - """ - return self in self._constants._multicast_network - - @property - def is_unspecified(self): - """Test if the address is unspecified. - Returns: - A boolean, True if this is the unspecified address as defined in - RFC 5735 3. - """ - return self == self._constants._unspecified_address - - @property - def is_loopback(self): - """Test if the address is a loopback address. - Returns: - A boolean, True if the address is a loopback per RFC 3330. - """ - return self in self._constants._loopback_network - - @property - def is_link_local(self): - """Test if the address is reserved for link-local. - Returns: - A boolean, True if the address is link-local per RFC 3927. - """ - return self in self._constants._linklocal_network - - -class IPv4Network(_BaseV4, _BaseNetwork): - - """This class represents and manipulates 32-bit IPv4 network + addresses.. - Attributes: [examples for IPv4Network('192.0.2.0/27')] - .network_address: IPv4Address('192.0.2.0') - .hostmask: IPv4Address('0.0.0.31') - .broadcast_address: IPv4Address('192.0.2.32') - .netmask: IPv4Address('255.255.255.224') - .prefixlen: 27 - """ - - # Class to use when creating address objects - _address_class = IPv4Address - - def __init__(self, address, strict=True): - """Instantiate a new IPv4 network object. - Args: - address: A string or integer representing the IP [& network]. - '192.0.2.0/24' - '192.0.2.0/255.255.255.0' - '192.0.2.0/0.0.0.255' - are all functionally the same in IPv4. Similarly, - '192.0.2.1' - '192.0.2.1/255.255.255.255' - '192.0.2.1/32' - are also functionally equivalent. That is to say, failing to - provide a subnetmask will create an object with a mask of /32. - If the mask (portion after the / in the argument) is given in - dotted quad form, it is treated as a netmask if it starts with a - non-zero field (e.g. /255.0.0.0 == /8) and as a hostmask if it - starts with a zero field (e.g. 0.255.255.255 == /8), with the - single exception of an all-zero mask which is treated as a - netmask == /0. If no mask is given, a default of /32 is used. - Additionally, an integer can be passed, so - IPv4Network('192.0.2.1') == IPv4Network(3221225985) - or, more generally - IPv4Interface(int(IPv4Interface('192.0.2.1'))) == - IPv4Interface('192.0.2.1') - Raises: - AddressValueError: If ipaddress isn't a valid IPv4 address. - NetmaskValueError: If the netmask isn't valid for - an IPv4 address. - ValueError: If strict is True and a network address is not - supplied. - """ - addr, mask = self._split_addr_prefix(address) - - self.network_address = IPv4Address(addr) - self.netmask, self._prefixlen = self._make_netmask(mask) - - @property - @lru_cache() - def is_global(self): - """Test if this address is allocated for public networks. - Returns: - A boolean, True if the address is not reserved per - iana-ipv4-special-registry. - """ - return ( - not ( - self.network_address in IPv4Network("100.64.0.0/10") - and self.broadcast_address in IPv4Network("100.64.0.0/10") - ) - and not self.is_private - ) - - -class _IPv4Constants: - _linklocal_network = IPv4Network("169.254.0.0/16") - - _loopback_network = IPv4Network("127.0.0.0/8") - - _multicast_network = IPv4Network("224.0.0.0/4") - - _public_network = IPv4Network("100.64.0.0/10") - - _private_networks = [ - IPv4Network("0.0.0.0/8"), - IPv4Network("10.0.0.0/8"), - IPv4Network("127.0.0.0/8"), - IPv4Network("169.254.0.0/16"), - IPv4Network("172.16.0.0/12"), - IPv4Network("192.0.0.0/29"), - IPv4Network("192.0.0.170/31"), - IPv4Network("192.0.2.0/24"), - IPv4Network("192.168.0.0/16"), - IPv4Network("198.18.0.0/15"), - IPv4Network("198.51.100.0/24"), - IPv4Network("203.0.113.0/24"), - IPv4Network("240.0.0.0/4"), - IPv4Network("255.255.255.255/32"), - ] - - _reserved_network = IPv4Network("240.0.0.0/4") - - _unspecified_address = IPv4Address("0.0.0.0") - - -IPv4Address._constants = _IPv4Constants -IPv4Network._constants = _IPv4Constants - - -class _BaseV6: - - """Base IPv6 object. - The following methods are used by IPv6 objects in both single IP - addresses and networks. - """ - - __slots__ = () - _version = 6 - _ALL_ONES = (2 ** IPV6LENGTH) - 1 - _HEXTET_COUNT = 8 - _HEX_DIGITS = frozenset("0123456789ABCDEFabcdef") - _max_prefixlen = IPV6LENGTH - - # There are only a bunch of valid v6 netmasks, so we cache them all - # when constructed (see _make_netmask()). - _netmask_cache = {} - - @classmethod - def _make_netmask(cls, arg): - """Make a (netmask, prefix_len) tuple from the given argument. - Argument can be: - - an integer (the prefix length) - - a string representing the prefix length (e.g. "24") - - a string representing the prefix netmask (e.g. "255.255.255.0") - """ - if arg not in cls._netmask_cache: - if _is_number(arg): - prefixlen = arg - if not (0 <= prefixlen <= cls._max_prefixlen): - cls._report_invalid_netmask(prefixlen) - else: - prefixlen = cls._prefix_from_prefix_string(arg) - netmask = IPv6Address(cls._ip_int_from_prefix(prefixlen)) - cls._netmask_cache[arg] = netmask, prefixlen - return cls._netmask_cache[arg] - - @classmethod # noqa C901 - def _ip_int_from_string(cls, ip_str): # noqa C901 - """Turn an IPv6 ip_str into an integer. - Args: - ip_str: A string, the IPv6 ip_str. - Returns: - An int, the IPv6 address - Raises: - AddressValueError: if ip_str isn't a valid IPv6 Address. - """ - if not ip_str: - raise AddressValueError("Address cannot be empty") - - parts = ip_str.split(":") - # An IPv6 address needs at least 2 colons (3 parts). - _min_parts = 3 - if len(parts) < _min_parts: - msg = "At least %d parts expected in %r" % (_min_parts, ip_str) - raise AddressValueError(msg) - - # If the address has an IPv4-style suffix, convert it to hexadecimal. - if "." in parts[-1]: - try: - ipv4_int = IPv4Address(parts.pop())._ip - except AddressValueError as exc: - raise six.raise_from( - AddressValueError("%s in %r" % (exc, ip_str)), None - ) - parts.append("%x" % ((ipv4_int >> 16) & 0xFFFF)) - parts.append("%x" % (ipv4_int & 0xFFFF)) - - # An IPv6 address can't have more than 8 colons (9 parts). - # The extra colon comes from using the "::" notation for a single - # leading or trailing zero part. - _max_parts = cls._HEXTET_COUNT + 1 - if len(parts) > _max_parts: - msg = "At most %d colons permitted in %r" % (_max_parts - 1, ip_str) - raise AddressValueError(msg) - - # Disregarding the endpoints, find '::' with nothing in between. - # This indicates that a run of zeroes has been skipped. - skip_index = None - for i in range(1, len(parts) - 1): - if not parts[i]: - if skip_index is not None: - # Can't have more than one '::' - msg = "At most one '::' permitted in %r" % ip_str - raise AddressValueError(msg) - skip_index = i - - # parts_hi is the number of parts to copy from above/before the '::' - # parts_lo is the number of parts to copy from below/after the '::' - if skip_index is not None: - # If we found a '::', then check if it also covers the endpoints. - parts_hi = skip_index - parts_lo = len(parts) - skip_index - 1 - if not parts[0]: - parts_hi -= 1 - if parts_hi: - msg = "Leading ':' only permitted as part of '::' in %r" - raise AddressValueError(msg % ip_str) # ^: requires ^:: - if not parts[-1]: - parts_lo -= 1 - if parts_lo: - msg = "Trailing ':' only permitted as part of '::' in %r" - raise AddressValueError(msg % ip_str) # :$ requires ::$ - parts_skipped = cls._HEXTET_COUNT - (parts_hi + parts_lo) - if parts_skipped < 1: - msg = "Expected at most %d other parts with '::' in %r" - raise AddressValueError(msg % (cls._HEXTET_COUNT - 1, ip_str)) - else: - # Otherwise, allocate the entire address to parts_hi. The - # endpoints could still be empty, but _parse_hextet() will check - # for that. - if len(parts) != cls._HEXTET_COUNT: - msg = "Exactly %d parts expected without '::' in %r" - raise AddressValueError(msg % (cls._HEXTET_COUNT, ip_str)) - if not parts[0]: - msg = "Leading ':' only permitted as part of '::' in %r" - raise AddressValueError(msg % ip_str) # ^: requires ^:: - if not parts[-1]: - msg = "Trailing ':' only permitted as part of '::' in %r" - raise AddressValueError(msg % ip_str) # :$ requires ::$ - parts_hi = len(parts) - parts_lo = 0 - parts_skipped = 0 - - try: - # Now, parse the hextets into a 128-bit integer. - ip_int = 0 - for i in range(parts_hi): - ip_int <<= 16 - ip_int |= cls._parse_hextet(parts[i]) - ip_int <<= 16 * parts_skipped - for i in range(-parts_lo, 0): - ip_int <<= 16 - ip_int |= cls._parse_hextet(parts[i]) - return ip_int - except ValueError as exc: - raise six.raise_from(AddressValueError("%s in %r" % (exc, ip_str)), None) - - @classmethod - def _parse_hextet(cls, hextet_str): - """Convert an IPv6 hextet string into an integer. - Args: - hextet_str: A string, the number to parse. - Returns: - The hextet as an integer. - Raises: - ValueError: if the input isn't strictly a hex number from - [0..FFFF]. - """ - # Reject non-ASCII digits. - if not cls._HEX_DIGITS.issuperset(hextet_str): - raise ValueError("Only hex digits permitted in %r" % hextet_str) - # We do the length check second, since the invalid character error - # is likely to be more informative for the user - if len(hextet_str) > 4: - msg = "At most 4 characters permitted in %r" - raise ValueError(msg % hextet_str) - # Length check means we can skip checking the integer value - return int(hextet_str, 16) - - @classmethod - def _compress_hextets(cls, hextets): - """Compresses a list of hextets. - Compresses a list of strings, replacing the longest continuous - sequence of "0" in the list with "" and adding empty strings at - the beginning or at the end of the string such that subsequently - calling ":".join(hextets) will produce the compressed version of - the IPv6 address. - Args: - hextets: A list of strings, the hextets to compress. - Returns: - A list of strings. - """ - best_doublecolon_start = -1 - best_doublecolon_len = 0 - doublecolon_start = -1 - doublecolon_len = 0 - for index, hextet in enumerate(hextets): - if hextet == "0": - doublecolon_len += 1 - if doublecolon_start == -1: - # Start of a sequence of zeros. - doublecolon_start = index - if doublecolon_len > best_doublecolon_len: - # This is the longest sequence of zeros so far. - best_doublecolon_len = doublecolon_len - best_doublecolon_start = doublecolon_start - else: - doublecolon_len = 0 - doublecolon_start = -1 - - if best_doublecolon_len > 1: - best_doublecolon_end = best_doublecolon_start + best_doublecolon_len - # For zeros at the end of the address. - if best_doublecolon_end == len(hextets): - hextets += [""] - hextets[best_doublecolon_start:best_doublecolon_end] = [""] - # For zeros at the beginning of the address. - if best_doublecolon_start == 0: - hextets = [""] + hextets - - return hextets - - @classmethod - def _string_from_ip_int(cls, ip_int=None): - """Turns a 128-bit integer into hexadecimal notation. - Args: - ip_int: An integer, the IP address. - Returns: - A string, the hexadecimal representation of the address. - Raises: - ValueError: The address is bigger than 128 bits of all ones. - """ - if ip_int is None: - ip_int = int(cls._ip) - - if ip_int > cls._ALL_ONES: - raise ValueError("IPv6 address is too large") - - hex_str = "%032x" % ip_int - hextets = ["%x" % int(hex_str[x : x + 4], 16) for x in range(0, 32, 4)] - - hextets = cls._compress_hextets(hextets) - return ":".join(hextets) - - def _explode_shorthand_ip_string(self): - """Expand a shortened IPv6 address. - Args: - ip_str: A string, the IPv6 address. - Returns: - A string, the expanded IPv6 address. - """ - if isinstance(self, IPv6Network): - ip_str = str(self.network_address) - else: - ip_str = str(self) - - ip_int = self._ip_int_from_string(ip_str) - hex_str = "%032x" % ip_int - parts = [hex_str[x : x + 4] for x in range(0, 32, 4)] - - return ":".join(parts) - - def _reverse_pointer(self): - """Return the reverse DNS pointer name for the IPv6 address. - This implements the method described in RFC3596 2.5. - """ - reverse_chars = self.exploded[::-1].replace(":", "") - return ".".join(reverse_chars) + ".ip6.arpa" - - @staticmethod - def _split_scope_id(ip_str): - """Helper function to parse IPv6 string address with scope id. - See RFC 4007 for details. - Args: - ip_str: A string, the IPv6 address. - Returns: - (addr, scope_id) tuple. - """ - addr, sep, scope_id = ip_str.partition("%") - if not sep: - scope_id = None - elif not scope_id or "%" in scope_id: - raise AddressValueError('Invalid IPv6 address: "%r"' % ip_str) - return addr, scope_id - - @property - def max_prefixlen(self): - return self._max_prefixlen - - @property - def version(self): - return self._version - - -class IPv6Address(_BaseV6, _BaseAddress): - - """Represent and manipulate single IPv6 Addresses.""" - - __slots__ = ("_ip", "_scope_id", "__weakref__") - - def __init__(self, address): - """Instantiate a new IPv6 address object. - Args: - address: A string or integer representing the IP - Additionally, an integer can be passed, so - IPv6Address('2001:db8::') == - IPv6Address(42540766411282592856903984951653826560) - or, more generally - IPv6Address(int(IPv6Address('2001:db8::'))) == - IPv6Address('2001:db8::') - Raises: - AddressValueError: If address isn't a valid IPv6 address. - """ - - # Efficient constructor from integer. - if _is_number(address): - self._check_int_address(address) - self._ip = address - self._scope_id = None - return - - # Assume input argument to be string or any object representation - # which converts into a formatted IP string. - addr_str = str(address) - if "/" in addr_str: - raise AddressValueError("Unexpected '/' in %s" % address) - addr_str, self._scope_id = self._split_scope_id(addr_str) - self._ip = self._ip_int_from_string(addr_str) - - def __str__(self): - ip_str = super().__str__() - return ip_str + "%" + self._scope_id if self._scope_id else ip_str - - def __hash__(self): - return hash((self._ip, self._scope_id)) - - def __eq__(self, other): - address_equal = super().__eq__(other) - if address_equal is NotImplemented: - return NotImplemented - if not address_equal: - return False - return self._scope_id == getattr(other, "_scope_id", None) - - @property - def scope_id(self): - """Identifier of a particular zone of the address's scope. - See RFC 4007 for details. - Returns: - A string identifying the zone of the address if specified, else None. - """ - return self._scope_id - - @property - def is_multicast(self): - """Test if the address is reserved for multicast use. - Returns: - A boolean, True if the address is a multicast address. - See RFC 2373 2.7 for details. - """ - return self in self._constants._multicast_network - - @property - def is_reserved(self): - """Test if the address is otherwise IETF reserved. - Returns: - A boolean, True if the address is within one of the - reserved IPv6 Network ranges. - """ - return any(self in x for x in self._constants._reserved_networks) - - @property - def is_link_local(self): - """Test if the address is reserved for link-local. - Returns: - A boolean, True if the address is reserved per RFC 4291. - """ - return self in self._constants._linklocal_network - - @property - def is_site_local(self): - """Test if the address is reserved for site-local. - Note that the site-local address space has been deprecated by RFC 3879. - Use is_private to test if this address is in the space of unique local - addresses as defined by RFC 4193. - Returns: - A boolean, True if the address is reserved per RFC 3513 2.5.6. - """ - return self in self._constants._sitelocal_network - - @property - @lru_cache() - def is_private(self): - """Test if this address is allocated for private networks. - Returns: - A boolean, True if the address is reserved per - iana-ipv6-special-registry, or is ipv4_mapped and is - reserved in the iana-ipv4-special-registry. - """ - ipv4_mapped = self.ipv4_mapped - if ipv4_mapped is not None: - return ipv4_mapped.is_private - return any(self in net for net in self._constants._private_networks) - - @property - def is_global(self): - """Test if this address is allocated for public networks. - Returns: - A boolean, true if the address is not reserved per - iana-ipv6-special-registry. - """ - return not self.is_private - - @property - def is_unspecified(self): - """Test if the address is unspecified. - Returns: - A boolean, True if this is the unspecified address as defined in - RFC 2373 2.5.2. - """ - return self._ip == 0 - - @property - def is_loopback(self): - """Test if the address is a loopback address. - Returns: - A boolean, True if the address is a loopback address as defined in - RFC 2373 2.5.3. - """ - return self._ip == 1 - - @property - def ipv4_mapped(self): - """Return the IPv4 mapped address. - Returns: - If the IPv6 address is a v4 mapped address, return the - IPv4 mapped address. Return None otherwise. - """ - if (self._ip >> 32) != 0xFFFF: - return None - return IPv4Address(self._ip & 0xFFFFFFFF) - - @property - def teredo(self): - """Tuple of embedded teredo IPs. - Returns: - Tuple of the (server, client) IPs or None if the address - doesn't appear to be a teredo address (doesn't start with - 2001::/32) - """ - if (self._ip >> 96) != 0x20010000: - return None - return ( - IPv4Address((self._ip >> 64) & 0xFFFFFFFF), - IPv4Address(~self._ip & 0xFFFFFFFF), - ) - - @property - def sixtofour(self): - """Return the IPv4 6to4 embedded address. - Returns: - The IPv4 6to4-embedded address if present or None if the - address doesn't appear to contain a 6to4 embedded address. - """ - if (self._ip >> 112) != 0x2002: - return None - return IPv4Address((self._ip >> 80) & 0xFFFFFFFF) - - -class IPv6Network(_BaseV6, _BaseNetwork): - - """This class represents and manipulates 128-bit IPv6 networks. - Attributes: [examples for IPv6('2001:db8::1000/124')] - .network_address: IPv6Address('2001:db8::1000') - .hostmask: IPv6Address('::f') - .broadcast_address: IPv6Address('2001:db8::100f') - .netmask: IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0') - .prefixlen: 124 - """ - - # Class to use when creating address objects - _address_class = IPv6Address - - def __init__(self, address, strict=True): - """Instantiate a new IPv6 Network object. - Args: - address: A string or integer representing the IPv6 network or the - IP and prefix/netmask. - '2001:db8::/128' - '2001:db8:0000:0000:0000:0000:0000:0000/128' - '2001:db8::' - are all functionally the same in IPv6. That is to say, - failing to provide a subnetmask will create an object with - a mask of /128. - Additionally, an integer can be passed, so - IPv6Network('2001:db8::') == - IPv6Network(42540766411282592856903984951653826560) - or, more generally - IPv6Network(int(IPv6Network('2001:db8::'))) == - IPv6Network('2001:db8::') - strict: A boolean. If true, ensure that we have been passed - A true network address, eg, 2001:db8::1000/124 and not an - IP address on a network, eg, 2001:db8::1/124. - Raises: - AddressValueError: If address isn't a valid IPv6 address. - NetmaskValueError: If the netmask isn't valid for - an IPv6 address. - ValueError: If strict was True and a network address was not - supplied. - """ - addr, mask = self._split_addr_prefix(address) - self.network_address = IPv6Address(addr) - self.netmask, self._prefixlen = self._make_netmask(mask) - - @property - def is_site_local(self): - """Test if the address is reserved for site-local. - Note that the site-local address space has been deprecated by RFC 3879. - Use is_private to test if this address is in the space of unique local - addresses as defined by RFC 4193. - Returns: - A boolean, True if the address is reserved per RFC 3513 2.5.6. - """ - return ( - self.network_address.is_site_local and self.broadcast_address.is_site_local - ) - - -class _IPv6Constants: - - _linklocal_network = IPv6Network("fe80::/10") - - _multicast_network = IPv6Network("ff00::/8") - - _private_networks = [ - IPv6Network("::1/128"), - IPv6Network("::/128"), - IPv6Network("::ffff:0:0/96"), - IPv6Network("100::/64"), - IPv6Network("2001::/23"), - IPv6Network("2001:2::/48"), - IPv6Network("2001:db8::/32"), - IPv6Network("2001:10::/28"), - IPv6Network("fc00::/7"), - IPv6Network("fe80::/10"), - ] - - _reserved_networks = [ - IPv6Network("::/8"), - IPv6Network("100::/8"), - IPv6Network("200::/7"), - IPv6Network("400::/6"), - IPv6Network("800::/5"), - IPv6Network("1000::/4"), - IPv6Network("4000::/3"), - IPv6Network("6000::/3"), - IPv6Network("8000::/3"), - IPv6Network("A000::/3"), - IPv6Network("C000::/3"), - IPv6Network("E000::/4"), - IPv6Network("F000::/5"), - IPv6Network("F800::/6"), - IPv6Network("FE00::/9"), - ] - - _sitelocal_network = IPv6Network("fec0::/10") - - -IPv6Address._constants = _IPv6Constants -IPv6Network._constants = _IPv6Constants diff --git a/kolibri/core/discovery/utils/network/urls.py b/kolibri/core/discovery/utils/network/urls.py index dc1cf071a76..ddce9bcc7dc 100644 --- a/kolibri/core/discovery/utils/network/urls.py +++ b/kolibri/core/discovery/utils/network/urls.py @@ -1,7 +1,5 @@ import re - -from six import raise_from -from six.moves.urllib.parse import urlparse +from urllib.parse import urlparse from . import errors @@ -123,7 +121,7 @@ def parse_address_into_components(address): # noqa C901 try: parsed = urlparse(address) except ValueError as e: - raise_from(errors.InvalidHostname(address), e) + raise errors.InvalidHostname(address) from e p_scheme = parsed.scheme p_hostname = parsed.hostname p_path = parsed.path.rstrip("/") + "/" diff --git a/kolibri/core/exams/test/test_exam_api.py b/kolibri/core/exams/test/test_exam_api.py index 7108fba8e57..6d1854b20da 100644 --- a/kolibri/core/exams/test/test_exam_api.py +++ b/kolibri/core/exams/test/test_exam_api.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import uuid from django.urls import reverse diff --git a/kolibri/core/fields.py b/kolibri/core/fields.py index ce34d7e318c..3abfce1e072 100644 --- a/kolibri/core/fields.py +++ b/kolibri/core/fields.py @@ -6,7 +6,6 @@ from django.db.backends.utils import typecast_timestamp from django.db.models.fields import Field from django.utils import timezone -from django.utils.six import string_types from jsonfield import JSONField as JSONFieldBase @@ -85,7 +84,7 @@ def get_prep_value(self, value): # Casts datetimes into the format expected by the backend if value is None: return value - if isinstance(value, string_types): + if isinstance(value, str): value = parse_timezonestamp(value) return create_timezonestamp(value) @@ -101,7 +100,7 @@ def value_from_object_json_compatible(self, obj): class JSONField(JSONFieldBase): def from_db_value(self, value, expression, connection, context): - if isinstance(value, string_types): + if isinstance(value, str): try: return json.loads(value, **self.load_kwargs) except ValueError: @@ -110,7 +109,7 @@ def from_db_value(self, value, expression, connection, context): return value def to_python(self, value): - if isinstance(value, string_types): + if isinstance(value, str): try: return json.loads(value, **self.load_kwargs) except ValueError: diff --git a/kolibri/core/hooks.py b/kolibri/core/hooks.py index 31b68058682..215d8f8826f 100644 --- a/kolibri/core/hooks.py +++ b/kolibri/core/hooks.py @@ -10,10 +10,6 @@ Anyways, for now to get hooks started, we have some defined here... """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from abc import abstractproperty from django.utils.safestring import mark_safe diff --git a/kolibri/core/kolibri_plugin.py b/kolibri/core/kolibri_plugin.py index 64f6ddbeadf..9904558bc38 100644 --- a/kolibri/core/kolibri_plugin.py +++ b/kolibri/core/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.conf import settings from django.template.loader import render_to_string from django.templatetags.static import static diff --git a/kolibri/core/lessons/test/test_lesson_api.py b/kolibri/core/lessons/test/test_lesson_api.py index 03b81251527..5d701eb4800 100644 --- a/kolibri/core/lessons/test/test_lesson_api.py +++ b/kolibri/core/lessons/test/test_lesson_api.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase diff --git a/kolibri/core/lessons/test/test_lesson_create.py b/kolibri/core/lessons/test/test_lesson_create.py index 1ac1f6b4fbe..4b0a0f55e8b 100644 --- a/kolibri/core/lessons/test/test_lesson_create.py +++ b/kolibri/core/lessons/test/test_lesson_create.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.urls import reverse from rest_framework.test import APITestCase diff --git a/kolibri/core/logger/apps.py b/kolibri/core/logger/apps.py index 4bb4fcd1036..c5aafee1123 100644 --- a/kolibri/core/logger/apps.py +++ b/kolibri/core/logger/apps.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/kolibri/core/logger/management/commands/generateuserdata.py b/kolibri/core/logger/management/commands/generateuserdata.py index e4727a47411..099f3731634 100644 --- a/kolibri/core/logger/management/commands/generateuserdata.py +++ b/kolibri/core/logger/management/commands/generateuserdata.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import csv import io import logging diff --git a/kolibri/core/logger/test/test_api.py b/kolibri/core/logger/test/test_api.py index 2501f8a2ab0..f91932926c2 100644 --- a/kolibri/core/logger/test/test_api.py +++ b/kolibri/core/logger/test/test_api.py @@ -6,7 +6,6 @@ import csv import datetime import os -import sys import tempfile import uuid @@ -72,11 +71,7 @@ def test_csv_download(self): start_date=self.start_date, end_date=self.end_date, ) - if sys.version_info[0] < 3: - csv_file = open(filepath, "rb") - else: - csv_file = open(filepath, "r", newline="") - with csv_file as f: + with open(filepath, "r", newline="") as f: results = list(csv.reader(f)) for row in results[1:]: self.assertEqual(len(results[0]), len(row)) @@ -100,11 +95,7 @@ def test_csv_download_deleted_content(self): start_date=self.start_date, end_date=self.end_date, ) - if sys.version_info[0] < 3: - csv_file = open(filepath, "rb") - else: - csv_file = open(filepath, "r", newline="") - with csv_file as f: + with open(filepath, "r", newline="") as f: results = list(csv.reader(f)) for row in results[1:]: self.assertEqual(len(results[0]), len(row)) @@ -136,11 +127,7 @@ def test_csv_download_unicode_username(self): start_date=self.start_date, end_date=self.end_date, ) - if sys.version_info[0] < 3: - csv_file = open(filepath, "rb") - else: - csv_file = open(filepath, "r", newline="") - with csv_file as f: + with open(filepath, "r", newline="") as f: results = list(csv.reader(f)) for row in results[1:]: self.assertEqual(len(results[0]), len(row)) @@ -246,11 +233,7 @@ def test_csv_download(self): start_date=self.start_date, end_date=self.end_date, ) - if sys.version_info[0] < 3: - csv_file = open(filepath, "rb") - else: - csv_file = open(filepath, "r", newline="") - with csv_file as f: + with open(filepath, "r", newline="") as f: results = list(csv.reader(f)) for row in results[1:]: self.assertEqual(len(results[0]), len(row)) @@ -274,11 +257,7 @@ def test_csv_download_deleted_content(self): start_date=self.start_date, end_date=self.end_date, ) - if sys.version_info[0] < 3: - csv_file = open(filepath, "rb") - else: - csv_file = open(filepath, "r", newline="") - with csv_file as f: + with open(filepath, "r", newline="") as f: results = list(csv.reader(f)) for row in results[1:]: self.assertEqual(len(results[0]), len(row)) @@ -310,11 +289,7 @@ def test_csv_download_unicode_username(self): start_date=self.start_date, end_date=self.end_date, ) - if sys.version_info[0] < 3: - csv_file = open(filepath, "rb") - else: - csv_file = open(filepath, "r", newline="") - with csv_file as f: + with open(filepath, "r", newline="") as f: results = list(csv.reader(f)) for row in results[1:]: self.assertEqual(len(results[0]), len(row)) @@ -335,11 +310,7 @@ def test_csv_download_no_completion_timestamp(self): start_date=self.start_date, end_date=self.end_date, ) - if sys.version_info[0] < 3: - csv_file = open(filepath, "rb") - else: - csv_file = open(filepath, "r", newline="") - with csv_file as f: + with open(filepath, "r", newline="") as f: results = list(csv.reader(f)) for column_label in results[0]: self.assertNotEqual(column_label, labels["completion_timestamp"]) diff --git a/kolibri/core/logger/test/test_attempt_log_consolidation.py b/kolibri/core/logger/test/test_attempt_log_consolidation.py index 7469b465831..197e4235540 100644 --- a/kolibri/core/logger/test/test_attempt_log_consolidation.py +++ b/kolibri/core/logger/test/test_attempt_log_consolidation.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import datetime import uuid from random import choice diff --git a/kolibri/core/logger/test/test_exam_log_migration.py b/kolibri/core/logger/test/test_exam_log_migration.py index 276b41aa4de..4880ec4fbce 100644 --- a/kolibri/core/logger/test/test_exam_log_migration.py +++ b/kolibri/core/logger/test/test_exam_log_migration.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from datetime import timedelta from uuid import uuid4 diff --git a/kolibri/core/logger/test/test_integrated_api.py b/kolibri/core/logger/test/test_integrated_api.py index 08ab5db19a3..ac3131a348a 100644 --- a/kolibri/core/logger/test/test_integrated_api.py +++ b/kolibri/core/logger/test/test_integrated_api.py @@ -13,7 +13,6 @@ from le_utils.constants import modalities from mock import patch from rest_framework.test import APITestCase -from six import string_types from ..models import AttemptLog from ..models import ContentSessionLog @@ -315,7 +314,7 @@ def test_start_assessment_session_logged_in_coach_assigned_succeeds(self): save_queue_mock.mock_calls[0][1][0], quiz_started_notification ) self.assertIsInstance(save_queue_mock.mock_calls[0][1][1], MasteryLog) - self.assertIsInstance(save_queue_mock.mock_calls[0][1][2], string_types) + self.assertIsInstance(save_queue_mock.mock_calls[0][1][2], str) self.assertEqual(response.status_code, 200) self.assertEqual( @@ -2365,7 +2364,7 @@ def test_update_assessment_session_create_attempt_succeeds(self): save_queue_mock.mock_calls[0][1][0], quiz_answered_notification ) self.assertIsInstance(save_queue_mock.mock_calls[0][1][1], AttemptLog) - self.assertIsInstance(save_queue_mock.mock_calls[0][1][2], string_types) + self.assertIsInstance(save_queue_mock.mock_calls[0][1][2], str) def test_update_assessment_session_create_errored_attempt_succeeds(self): with patch("kolibri.core.logger.api.wrap_to_save_queue") as save_queue_mock: @@ -2377,7 +2376,7 @@ def test_update_assessment_session_create_errored_attempt_succeeds(self): save_queue_mock.mock_calls[0][1][0], quiz_answered_notification ) self.assertIsInstance(save_queue_mock.mock_calls[0][1][1], AttemptLog) - self.assertIsInstance(save_queue_mock.mock_calls[0][1][2], string_types) + self.assertIsInstance(save_queue_mock.mock_calls[0][1][2], str) def test_update_assessment_session_create_hinted_attempt_succeeds(self): with patch("kolibri.core.logger.api.wrap_to_save_queue") as save_queue_mock: @@ -2389,7 +2388,7 @@ def test_update_assessment_session_create_hinted_attempt_succeeds(self): save_queue_mock.mock_calls[0][1][0], quiz_answered_notification ) self.assertIsInstance(save_queue_mock.mock_calls[0][1][1], AttemptLog) - self.assertIsInstance(save_queue_mock.mock_calls[0][1][2], string_types) + self.assertIsInstance(save_queue_mock.mock_calls[0][1][2], str) def test_update_session_absolute_progress_triggers_completion(self): with patch("kolibri.core.logger.api.wrap_to_save_queue") as save_queue_mock: @@ -2415,7 +2414,7 @@ def test_update_session_absolute_progress_triggers_completion(self): save_queue_mock.mock_calls[0][1][0], quiz_completed_notification ) self.assertIsInstance(save_queue_mock.mock_calls[0][1][1], MasteryLog) - self.assertIsInstance(save_queue_mock.mock_calls[0][1][2], string_types) + self.assertIsInstance(save_queue_mock.mock_calls[0][1][2], str) def test_update_assessment_session_update_attempt_submitted_quiz_fails(self): timestamp = local_now() diff --git a/kolibri/core/logger/utils/user_data.py b/kolibri/core/logger/utils/user_data.py index 818bcbdf100..1c1f356a902 100644 --- a/kolibri/core/logger/utils/user_data.py +++ b/kolibri/core/logger/utils/user_data.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import datetime import logging import random diff --git a/kolibri/core/notifications/apps.py b/kolibri/core/notifications/apps.py index 036790b37ef..0190ebddb11 100644 --- a/kolibri/core/notifications/apps.py +++ b/kolibri/core/notifications/apps.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/kolibri/core/oidc_provider_hook.py b/kolibri/core/oidc_provider_hook.py index 45de9616850..2b0fdd9647b 100644 --- a/kolibri/core/oidc_provider_hook.py +++ b/kolibri/core/oidc_provider_hook.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.plugins import hooks diff --git a/kolibri/core/public/api_urls.py b/kolibri/core/public/api_urls.py index 5fda56eb11d..2b56003a0ce 100644 --- a/kolibri/core/public/api_urls.py +++ b/kolibri/core/public/api_urls.py @@ -11,10 +11,6 @@ endpoint in place and maintained to the best extent possible so older clients can still use it. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.conf.urls import include from django.conf.urls import url from rest_framework import routers diff --git a/kolibri/core/public/test/test_api.py b/kolibri/core/public/test/test_api.py index c26e2f105c6..2895617dfc1 100644 --- a/kolibri/core/public/test/test_api.py +++ b/kolibri/core/public/test/test_api.py @@ -15,7 +15,6 @@ from rest_framework import status from rest_framework.test import APITestCase from rest_framework.test import APITransactionTestCase -from six import iteritems import kolibri from kolibri.core.auth.models import Facility @@ -208,7 +207,7 @@ def test_public_channel_lookup(self): "matching_tokens": [], "public": True, } - for key, value in iteritems(expected): + for key, value in expected.items(): self.assertEqual(data[key], value) # we don't care what order these elements are in self.assertSetEqual(set(["en", "es"]), set(data["included_languages"])) diff --git a/kolibri/core/tasks/api.py b/kolibri/core/tasks/api.py index 6e5ff71c041..f6feb521ee9 100644 --- a/kolibri/core/tasks/api.py +++ b/kolibri/core/tasks/api.py @@ -9,7 +9,6 @@ from rest_framework import viewsets from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response -from six import string_types from kolibri.core.tasks.exceptions import JobNotFound from kolibri.core.tasks.exceptions import JobNotRestartable @@ -216,7 +215,7 @@ def create(self, request): def _get_job_for_pk(self, request, pk): try: - if not isinstance(pk, string_types): + if not isinstance(pk, str): raise JobNotFound job = job_storage.get_job(job_id=pk) registered_task = TaskRegistry[job.func] diff --git a/kolibri/core/tasks/apps.py b/kolibri/core/tasks/apps.py index b0b117ee811..34bf022ca79 100644 --- a/kolibri/core/tasks/apps.py +++ b/kolibri/core/tasks/apps.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/kolibri/core/tasks/job.py b/kolibri/core/tasks/job.py index 43c7faa1e95..31cc905e4b0 100644 --- a/kolibri/core/tasks/job.py +++ b/kolibri/core/tasks/job.py @@ -5,9 +5,6 @@ import uuid from collections import namedtuple -from six import raise_from -from six import string_types - from kolibri.core.tasks.constants import ( # noqa F401 - imported for backwards compatibility Priority, ) @@ -151,7 +148,7 @@ def to_json(self): except TypeError as e: # A Job's arguments, results, or metadata are prime suspects for # what might cause this error. - raise_from(TypeError("Job objects need to be JSON-serializable"), e) + raise TypeError("Job objects need to be JSON-serializable") from e return string_result @classmethod @@ -202,7 +199,7 @@ def __init__( :param func: func can be a callable object, in which case it is turned into an importable string, or it can be an importable string already. """ - if not callable(func) and not isinstance(func, string_types): + if not callable(func) and not isinstance(func, str): raise TypeError( "Cannot create Job for object of type {}".format(type(func)) ) diff --git a/kolibri/core/tasks/permissions.py b/kolibri/core/tasks/permissions.py index 36e6cc7b700..851b06d8f45 100644 --- a/kolibri/core/tasks/permissions.py +++ b/kolibri/core/tasks/permissions.py @@ -1,12 +1,10 @@ from abc import ABCMeta from abc import abstractmethod -from six import with_metaclass - from kolibri.core.auth.permissions.general import _user_is_admin_for_own_facility -class BasePermission(with_metaclass(ABCMeta)): +class BasePermission(metaclass=ABCMeta): """ Base Permission class from which all other Permission classes should inherit. diff --git a/kolibri/core/tasks/registry.py b/kolibri/core/tasks/registry.py index ae3bc7de377..e220328678f 100644 --- a/kolibri/core/tasks/registry.py +++ b/kolibri/core/tasks/registry.py @@ -5,7 +5,6 @@ from django.apps import apps from rest_framework import serializers from rest_framework.exceptions import PermissionDenied -from six import string_types from kolibri.core.tasks.constants import DEFAULT_QUEUE from kolibri.core.tasks.constants import Priority @@ -116,7 +115,7 @@ def update(self, other): self[key] = value def validate_task(self, task): - if not isinstance(task, string_types): + if not isinstance(task, str): raise serializers.ValidationError("The task type must be a string.") if task not in self: raise serializers.ValidationError( @@ -199,7 +198,7 @@ def __init__( # noqa: C901 raise ValueError("priority must be one of '5' or '10' (integer).") if not isinstance(permission_classes, list): raise TypeError("permission_classes must be of list type.") - if not isinstance(queue, string_types): + if not isinstance(queue, str): raise TypeError("queue must be of string type.") if not isinstance(cancellable, bool): raise TypeError("cancellable must be of bool type.") diff --git a/kolibri/core/tasks/utils.py b/kolibri/core/tasks/utils.py index 8ffad76cb00..4fd488f65c1 100644 --- a/kolibri/core/tasks/utils.py +++ b/kolibri/core/tasks/utils.py @@ -2,7 +2,6 @@ import logging import os import sqlite3 -import sys import time import uuid from threading import Thread @@ -10,7 +9,6 @@ import click from django.utils.functional import SimpleLazyObject from django.utils.module_loading import import_string -from six import string_types from sqlalchemy import create_engine from sqlalchemy import event from sqlalchemy import exc @@ -44,7 +42,7 @@ def callable_to_import_path(func): funcstring = "{module}.{funcname}".format( module=func.__module__, funcname=func.__name__ ) - elif isinstance(func, string_types): + elif isinstance(func, str): funcstring = func else: raise TypeError("Can't handle a function of type {}".format(type(func))) @@ -228,22 +226,17 @@ def __init__(self, total=100): # store provided arguments self.total = total - # Also check that we are not running Python 2: - # https://github.com/learningequality/kolibri/issues/6597 - if sys.version_info[0] == 2: + # Check that we are executing inside a click context + # as we only want to display progress bars from the command line. + try: + click.get_current_context() + # Coerce to an integer for safety, as click uses Python `range` on this + # value, which requires an integer argument + # N.B. because we are only doing this in Python3, safe to just use int, + # as long is Py2 only + self.progressbar = click.progressbar(length=int(total), width=0) + except RuntimeError: self.progressbar = None - else: - # Check that we are executing inside a click context - # as we only want to display progress bars from the command line. - try: - click.get_current_context() - # Coerce to an integer for safety, as click uses Python `range` on this - # value, which requires an integer argument - # N.B. because we are only doing this in Python3, safe to just use int, - # as long is Py2 only - self.progressbar = click.progressbar(length=int(total), width=0) - except RuntimeError: - self.progressbar = None def set_progress(self, current_progress, message): increment = current_progress - self.progress diff --git a/kolibri/core/tasks/worker.py b/kolibri/core/tasks/worker.py index b4093a8fa1b..3b2d9786161 100644 --- a/kolibri/core/tasks/worker.py +++ b/kolibri/core/tasks/worker.py @@ -43,20 +43,13 @@ def execute_job_with_python_worker(job_id): """ import os import socket - import sys import threading - # get_ident was added in Python 3.3 and never backported to 2.7 - if sys.version_info[0] < 3: - thread_ident = threading.current_thread().ident - else: - thread_ident = threading.get_ident() - execute_job( job_id, worker_host=socket.gethostname(), worker_process=str(os.getpid()), - worker_thread=str(thread_ident), + worker_thread=str(threading.get_ident()), ) diff --git a/kolibri/core/templatetags/core_tags.py b/kolibri/core/templatetags/core_tags.py index ee542fbc2f2..45f74d2599b 100644 --- a/kolibri/core/templatetags/core_tags.py +++ b/kolibri/core/templatetags/core_tags.py @@ -2,10 +2,6 @@ Kolibri template tags ===================== """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django import template from django.templatetags.static import static from django.utils.html import format_html diff --git a/kolibri/core/test/test_datetimetzfield.py b/kolibri/core/test/test_datetimetzfield.py index 181fdd22900..03566e880c6 100644 --- a/kolibri/core/test/test_datetimetzfield.py +++ b/kolibri/core/test/test_datetimetzfield.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import pytz from django.test import override_settings from django.test import TestCase diff --git a/kolibri/core/test/test_key_urls.py b/kolibri/core/test/test_key_urls.py index e6f6bd1024f..0b8a558215f 100644 --- a/kolibri/core/test/test_key_urls.py +++ b/kolibri/core/test/test_key_urls.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.conf import settings from django.urls import reverse from django.urls.exceptions import NoReverseMatch diff --git a/kolibri/core/test/test_query.py b/kolibri/core/test/test_query.py index 735f5731f78..2aa51d3e06b 100644 --- a/kolibri/core/test/test_query.py +++ b/kolibri/core/test/test_query.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.db import models from django.test import TestCase diff --git a/kolibri/core/theme_hook.py b/kolibri/core/theme_hook.py index 69489e7432e..642ba12f4b6 100644 --- a/kolibri/core/theme_hook.py +++ b/kolibri/core/theme_hook.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging from abc import abstractproperty diff --git a/kolibri/core/urls.py b/kolibri/core/urls.py index 60d39130827..7c2ce762f37 100644 --- a/kolibri/core/urls.py +++ b/kolibri/core/urls.py @@ -30,10 +30,6 @@ Place a url.py and have your plugin's definition class's ``url_module`` method return the module. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.conf.urls import include from django.conf.urls import url from rest_framework import routers diff --git a/kolibri/core/utils/csv.py b/kolibri/core/utils/csv.py index ea962ccd79d..010232e9672 100644 --- a/kolibri/core/utils/csv.py +++ b/kolibri/core/utils/csv.py @@ -2,19 +2,14 @@ import io import re -import sys from numbers import Number def open_csv_for_writing(filepath): - if sys.version_info[0] < 3: - return io.open(filepath, "wb") return io.open(filepath, "w", newline="", encoding="utf-8-sig") def open_csv_for_reading(filepath): - if sys.version_info[0] < 3: - return io.open(filepath, "rb") return io.open(filepath, "r", newline="", encoding="utf-8-sig") diff --git a/kolibri/core/utils/exception_handler.py b/kolibri/core/utils/exception_handler.py index 8af3d0049ea..9a092f2604a 100644 --- a/kolibri/core/utils/exception_handler.py +++ b/kolibri/core/utils/exception_handler.py @@ -1,4 +1,3 @@ -from django.utils import six from rest_framework import status from rest_framework.exceptions import ErrorDetail from rest_framework.views import exception_handler @@ -30,7 +29,7 @@ def _return_errors(data, path=None): path = path or [] errors = [] if isinstance(data, dict): - for key, value in six.iteritems(data): + for key, value in data.items(): new_path = path + [key] # handle drf error responses if isinstance(value, list): diff --git a/kolibri/core/utils/pagination.py b/kolibri/core/utils/pagination.py index 0d3322064fc..736ec7c2466 100644 --- a/kolibri/core/utils/pagination.py +++ b/kolibri/core/utils/pagination.py @@ -1,6 +1,7 @@ import hashlib from base64 import b64encode from collections import OrderedDict +from urllib.parse import urlencode from django.core.cache import cache from django.core.exceptions import EmptyResultSet @@ -15,7 +16,6 @@ from rest_framework.pagination import NotFound from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response -from six.moves.urllib.parse import urlencode class ValuesPage(Page): diff --git a/kolibri/core/utils/urls.py b/kolibri/core/utils/urls.py index 9325237e6c3..692dbee28ae 100644 --- a/kolibri/core/utils/urls.py +++ b/kolibri/core/utils/urls.py @@ -1,5 +1,6 @@ +from urllib.parse import urljoin + from django.urls import reverse -from six.moves.urllib.parse import urljoin from kolibri.utils.conf import OPTIONS diff --git a/kolibri/core/views.py b/kolibri/core/views.py index 347d824e07b..19e038233f9 100644 --- a/kolibri/core/views.py +++ b/kolibri/core/views.py @@ -1,3 +1,6 @@ +from urllib.parse import urlsplit +from urllib.parse import urlunsplit + from django.contrib.auth import logout from django.http import Http404 from django.http import HttpResponse @@ -7,8 +10,6 @@ from django.urls import reverse from django.urls import translate_url from django.utils.decorators import method_decorator -from django.utils.six.moves.urllib.parse import urlsplit -from django.utils.six.moves.urllib.parse import urlunsplit from django.utils.translation import check_for_language from django.utils.translation import LANGUAGE_SESSION_KEY from django.utils.translation import ugettext_lazy as _ diff --git a/kolibri/core/webpack/hooks.py b/kolibri/core/webpack/hooks.py index 837b8639907..7c23c4d0c65 100644 --- a/kolibri/core/webpack/hooks.py +++ b/kolibri/core/webpack/hooks.py @@ -5,10 +5,6 @@ To manage assets, we use the webpack format. In order to have assets bundled in, you should put them in ``yourapp/assets/src``. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import codecs import io import json @@ -18,6 +14,7 @@ import time from abc import abstractproperty from functools import partial +from urllib.request import url2pathname from django.conf import settings from django.contrib.staticfiles.finders import find as find_staticfiles @@ -25,12 +22,10 @@ from django.core.serializers.json import DjangoJSONEncoder from django.utils.functional import cached_property from django.utils.safestring import mark_safe -from django.utils.six.moves.urllib.request import url2pathname from django.utils.translation import get_language from django.utils.translation import get_language_info from django.utils.translation import to_locale from importlib_resources import files -from six import text_type from kolibri.plugins import hooks @@ -250,7 +245,7 @@ def get_basename(self, url): # a string-alike object to e.g. add ``SCRIPT_NAME`` # WSGI param as a *path prefix* to the output URL. # See https://code.djangoproject.com/ticket/25598. - base_url = text_type(base_url) + base_url = str(base_url) if not url.startswith(base_url): return None diff --git a/kolibri/core/webpack/templatetags/webpack_tags.py b/kolibri/core/webpack/templatetags/webpack_tags.py index e0d11f34ff3..b3bedcfb2a5 100644 --- a/kolibri/core/webpack/templatetags/webpack_tags.py +++ b/kolibri/core/webpack/templatetags/webpack_tags.py @@ -15,10 +15,6 @@ {% base_frontend_async %} """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django import template from .. import hooks diff --git a/kolibri/core/webpack/test/base.py b/kolibri/core/webpack/test/base.py index 4a08c2976bc..eb523865e3a 100644 --- a/kolibri/core/webpack/test/base.py +++ b/kolibri/core/webpack/test/base.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import copy from ..hooks import WebpackBundleHook diff --git a/kolibri/core/webpack/test/test_webpack_tags.py b/kolibri/core/webpack/test/test_webpack_tags.py index c4fd998a87d..8e2419c287a 100644 --- a/kolibri/core/webpack/test/test_webpack_tags.py +++ b/kolibri/core/webpack/test/test_webpack_tags.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.test.testcases import TestCase from .base import Hook diff --git a/kolibri/deployment/default/settings/__init__.py b/kolibri/deployment/default/settings/__init__.py index 98eec85dedc..e69de29bb2d 100644 --- a/kolibri/deployment/default/settings/__init__.py +++ b/kolibri/deployment/default/settings/__init__.py @@ -1,3 +0,0 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals diff --git a/kolibri/deployment/default/settings/base.py b/kolibri/deployment/default/settings/base.py index 0a13e93d0ad..8b3eb645f9c 100644 --- a/kolibri/deployment/default/settings/base.py +++ b/kolibri/deployment/default/settings/base.py @@ -8,17 +8,13 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.11/ref/settings/ """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import os import sys +from urllib.parse import urljoin import pytz from django.conf import locale from morango.constants import settings as morango_settings -from six.moves.urllib.parse import urljoin from tzlocal import get_localzone import kolibri diff --git a/kolibri/deployment/default/settings/debug_panel.py b/kolibri/deployment/default/settings/debug_panel.py index 9e32ea16e8f..1287d059999 100644 --- a/kolibri/deployment/default/settings/debug_panel.py +++ b/kolibri/deployment/default/settings/debug_panel.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from .dev import * # noqa INTERNAL_IPS = ["127.0.0.1"] diff --git a/kolibri/deployment/default/settings/dev.py b/kolibri/deployment/default/settings/dev.py index e008dd80a69..7d7a6e343dd 100644 --- a/kolibri/deployment/default/settings/dev.py +++ b/kolibri/deployment/default/settings/dev.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import os from .base import * # noqa isort:skip @UnusedWildImport diff --git a/kolibri/deployment/default/settings/test.py b/kolibri/deployment/default/settings/test.py index f8e5d3b4abc..1a772118119 100644 --- a/kolibri/deployment/default/settings/test.py +++ b/kolibri/deployment/default/settings/test.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import os import tempfile diff --git a/kolibri/deployment/default/settings/translation.py b/kolibri/deployment/default/settings/translation.py index e088897441d..0b46bc4abe0 100644 --- a/kolibri/deployment/default/settings/translation.py +++ b/kolibri/deployment/default/settings/translation.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import django.conf.locale from .base import * # noqa isort:skip @UnusedWildImport diff --git a/kolibri/deployment/default/urls.py b/kolibri/deployment/default/urls.py index ec949c41f8c..1c12c4d9b2b 100644 --- a/kolibri/deployment/default/urls.py +++ b/kolibri/deployment/default/urls.py @@ -17,10 +17,6 @@ .. moduleauthor:: Learning Equality """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.conf.urls import include from django.conf.urls import url from django.contrib import admin diff --git a/kolibri/plugins/__init__.py b/kolibri/plugins/__init__.py index 732f2b6338c..b8b68ed092d 100644 --- a/kolibri/plugins/__init__.py +++ b/kolibri/plugins/__init__.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import json import logging import os @@ -10,7 +6,6 @@ from importlib import import_module from django.utils.module_loading import module_has_submodule -from six import with_metaclass from kolibri.utils.build_config.default_plugins import DEFAULT_PLUGINS from kolibri.utils.conf import KOLIBRI_HOME @@ -153,7 +148,7 @@ def __call__(cls, *args, **kwargs): return cls._instances[cls] -class KolibriPluginBase(with_metaclass(SingletonMeta)): +class KolibriPluginBase(metaclass=SingletonMeta): """ This is the base class that all Kolibri plugins need to implement. """ diff --git a/kolibri/plugins/app/kolibri_plugin.py b/kolibri/plugins/app/kolibri_plugin.py index ff970491376..5943f63728d 100644 --- a/kolibri/plugins/app/kolibri_plugin.py +++ b/kolibri/plugins/app/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.plugins import KolibriPluginBase diff --git a/kolibri/plugins/coach/kolibri_plugin.py b/kolibri/plugins/coach/kolibri_plugin.py index 1ffa55e3606..9e854c47827 100644 --- a/kolibri/plugins/coach/kolibri_plugin.py +++ b/kolibri/plugins/coach/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging from kolibri.core.auth.constants.user_kinds import COACH diff --git a/kolibri/plugins/coach/test/test_class_summary.py b/kolibri/plugins/coach/test/test_class_summary.py index aa65a72991a..462738ad344 100644 --- a/kolibri/plugins/coach/test/test_class_summary.py +++ b/kolibri/plugins/coach/test/test_class_summary.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import uuid from django.urls import reverse diff --git a/kolibri/plugins/coach/test/test_classroom_notifications.py b/kolibri/plugins/coach/test/test_classroom_notifications.py index ab744e4db74..665e32c02af 100644 --- a/kolibri/plugins/coach/test/test_classroom_notifications.py +++ b/kolibri/plugins/coach/test/test_classroom_notifications.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.urls import reverse from rest_framework.test import APITestCase diff --git a/kolibri/plugins/coach/test/test_difficult_questions.py b/kolibri/plugins/coach/test/test_difficult_questions.py index 93175c8f0fa..6106b83e819 100644 --- a/kolibri/plugins/coach/test/test_difficult_questions.py +++ b/kolibri/plugins/coach/test/test_difficult_questions.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import json from datetime import timedelta diff --git a/kolibri/plugins/coach/test/test_lesson_report.py b/kolibri/plugins/coach/test/test_lesson_report.py index 4aa7cb44f53..8c5982d2fdb 100644 --- a/kolibri/plugins/coach/test/test_lesson_report.py +++ b/kolibri/plugins/coach/test/test_lesson_report.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import datetime import json diff --git a/kolibri/plugins/coach/views.py b/kolibri/plugins/coach/views.py index 935b7075229..dd904872adc 100644 --- a/kolibri/plugins/coach/views.py +++ b/kolibri/plugins/coach/views.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.utils.decorators import method_decorator from django.views.generic.base import TemplateView diff --git a/kolibri/plugins/default_theme/kolibri_plugin.py b/kolibri/plugins/default_theme/kolibri_plugin.py index c4703336d3e..8d8f8318bf2 100644 --- a/kolibri/plugins/default_theme/kolibri_plugin.py +++ b/kolibri/plugins/default_theme/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.templatetags.static import static from kolibri.core import theme_hook diff --git a/kolibri/plugins/demo_server/kolibri_plugin.py b/kolibri/plugins/demo_server/kolibri_plugin.py index 8d0578009d3..f553be30243 100644 --- a/kolibri/plugins/demo_server/kolibri_plugin.py +++ b/kolibri/plugins/demo_server/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.core.webpack import hooks as webpack_hooks from kolibri.plugins import KolibriPluginBase from kolibri.plugins.hooks import register_hook diff --git a/kolibri/plugins/device/assets/src/views/DeprecationWarningBanner.vue b/kolibri/plugins/device/assets/src/views/DeprecationWarningBanner.vue index ed88a3f993d..b2190147b5e 100644 --- a/kolibri/plugins/device/assets/src/views/DeprecationWarningBanner.vue +++ b/kolibri/plugins/device/assets/src/views/DeprecationWarningBanner.vue @@ -15,9 +15,6 @@
-

- {{ coreString('pythonSupportWillBeDropped') }} -

{{ coreString('currentDeviceUsingIE11') }}

@@ -45,14 +42,11 @@ mixins: [commonCoreStrings], computed: { showBanner() { - return this.currentUserOnIE11 || this.userDevicesUsingIE11 || this.py27Deprecated; + return this.currentUserOnIE11 || this.userDevicesUsingIE11; }, userDevicesUsingIE11() { return plugin_data.deprecationWarnings.ie11; }, - py27Deprecated() { - return plugin_data.deprecationWarnings.py27; - }, currentUserOnIE11() { return browser.name === 'IE'; }, diff --git a/kolibri/plugins/device/kolibri_plugin.py b/kolibri/plugins/device/kolibri_plugin.py index e7ca03ea8e5..9945202c4d3 100644 --- a/kolibri/plugins/device/kolibri_plugin.py +++ b/kolibri/plugins/device/kolibri_plugin.py @@ -1,9 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - -import sys - from kolibri.core.auth.constants.user_kinds import SUPERUSER from kolibri.core.hooks import NavigationHook from kolibri.core.hooks import RoleBasedRedirectHook @@ -35,7 +29,6 @@ def plugin_data(self): "isRemoteContent": OPTIONS["Deployment"]["REMOTE_CONTENT"], "canRestart": bool(OPTIONS["Deployment"]["RESTART_HOOKS"]), "deprecationWarnings": { - "py27": sys.version_info.major == 2, "ie11": any_ie11_users(), }, } diff --git a/kolibri/plugins/device/test/test_api.py b/kolibri/plugins/device/test/test_api.py index 9beb5237469..54076044f68 100644 --- a/kolibri/plugins/device/test/test_api.py +++ b/kolibri/plugins/device/test/test_api.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import uuid import mock diff --git a/kolibri/plugins/device/views.py b/kolibri/plugins/device/views.py index 659e9f283d0..7b974239908 100644 --- a/kolibri/plugins/device/views.py +++ b/kolibri/plugins/device/views.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.utils.decorators import method_decorator from django.views.generic.base import TemplateView diff --git a/kolibri/plugins/epub_viewer/kolibri_plugin.py b/kolibri/plugins/epub_viewer/kolibri_plugin.py index 45bb298f973..74a546896b6 100644 --- a/kolibri/plugins/epub_viewer/kolibri_plugin.py +++ b/kolibri/plugins/epub_viewer/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from le_utils.constants import format_presets from kolibri.core.content import hooks as content_hooks diff --git a/kolibri/plugins/facility/kolibri_plugin.py b/kolibri/plugins/facility/kolibri_plugin.py index fa1f7989518..85c0a19c9c4 100644 --- a/kolibri/plugins/facility/kolibri_plugin.py +++ b/kolibri/plugins/facility/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.core.auth.constants.user_kinds import ADMIN from kolibri.core.hooks import NavigationHook from kolibri.core.hooks import RoleBasedRedirectHook diff --git a/kolibri/plugins/facility/views.py b/kolibri/plugins/facility/views.py index 65608f87500..9aa8f091e77 100644 --- a/kolibri/plugins/facility/views.py +++ b/kolibri/plugins/facility/views.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import io import json import os diff --git a/kolibri/plugins/hooks.py b/kolibri/plugins/hooks.py index 96d7458782c..122d365840b 100644 --- a/kolibri/plugins/hooks.py +++ b/kolibri/plugins/hooks.py @@ -153,17 +153,11 @@ def navigation_tags(self): """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging from abc import abstractproperty from functools import partial from inspect import isabstract -import six - from kolibri.plugins import SingletonMeta logger = logging.getLogger(__name__) @@ -188,6 +182,24 @@ def new(cls, *args, **kwds): subclass.__new__ = new +def _add_kolibri_hook_meta(subclass): + """ + Vendored from six add_metaclass. + """ + orig_vars = subclass.__dict__.copy() + slots = orig_vars.get("__slots__") + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop("__dict__", None) + orig_vars.pop("__weakref__", None) + if hasattr(subclass, "__qualname__"): + orig_vars["__qualname__"] = subclass.__qualname__ + return KolibriHookMeta(subclass.__name__, subclass.__bases__, orig_vars) + + def define_hook(subclass=None, only_one_registered=False): """ This method must be used as a decorator to define a new hook inheriting from @@ -200,7 +212,7 @@ def define_hook(subclass=None, only_one_registered=False): if subclass is None: return partial(define_hook, only_one_registered=only_one_registered) - subclass = six.add_metaclass(KolibriHookMeta)(subclass) + subclass = _add_kolibri_hook_meta(subclass) subclass._setup_base_class(only_one_registered=only_one_registered) @@ -324,7 +336,7 @@ def get_hook(cls, unique_id): return cls._registered_hooks.get(unique_id, None) -class KolibriHook(six.with_metaclass(KolibriHookMeta)): +class KolibriHook(metaclass=KolibriHookMeta): @abstractproperty def _not_abstract(self): """ diff --git a/kolibri/plugins/html5_viewer/kolibri_plugin.py b/kolibri/plugins/html5_viewer/kolibri_plugin.py index be9b888afa7..b3af7aea151 100644 --- a/kolibri/plugins/html5_viewer/kolibri_plugin.py +++ b/kolibri/plugins/html5_viewer/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from le_utils.constants import format_presets from kolibri.core.content import hooks as content_hooks diff --git a/kolibri/plugins/learn/kolibri_plugin.py b/kolibri/plugins/learn/kolibri_plugin.py index 6a6e2fc688d..442171befbc 100644 --- a/kolibri/plugins/learn/kolibri_plugin.py +++ b/kolibri/plugins/learn/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.urls import reverse from kolibri.core.auth.constants.user_kinds import ANONYMOUS diff --git a/kolibri/plugins/learn/test/test_learner_classroom.py b/kolibri/plugins/learn/test/test_learner_classroom.py index d47c51cd06a..bc69742ca7a 100644 --- a/kolibri/plugins/learn/test/test_learner_classroom.py +++ b/kolibri/plugins/learn/test/test_learner_classroom.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.urls import reverse from django.utils.timezone import now from le_utils.constants import content_kinds diff --git a/kolibri/plugins/learn/test/test_learner_lesson.py b/kolibri/plugins/learn/test/test_learner_lesson.py index 567841e8963..1e920cff4ff 100644 --- a/kolibri/plugins/learn/test/test_learner_lesson.py +++ b/kolibri/plugins/learn/test/test_learner_lesson.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.urls import reverse from rest_framework.test import APITestCase diff --git a/kolibri/plugins/learn/views.py b/kolibri/plugins/learn/views.py index 58539512af5..83455496aaf 100644 --- a/kolibri/plugins/learn/views.py +++ b/kolibri/plugins/learn/views.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.utils.decorators import method_decorator from django.views.generic.base import TemplateView diff --git a/kolibri/plugins/media_player/kolibri_plugin.py b/kolibri/plugins/media_player/kolibri_plugin.py index caf6175ac6c..25fef8593bb 100644 --- a/kolibri/plugins/media_player/kolibri_plugin.py +++ b/kolibri/plugins/media_player/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from le_utils.constants import format_presets from kolibri.core.content import hooks as content_hooks diff --git a/kolibri/plugins/pdf_viewer/kolibri_plugin.py b/kolibri/plugins/pdf_viewer/kolibri_plugin.py index 31e92b5ca26..b5f8b8c019b 100644 --- a/kolibri/plugins/pdf_viewer/kolibri_plugin.py +++ b/kolibri/plugins/pdf_viewer/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from le_utils.constants import format_presets from kolibri.core.content import hooks as content_hooks diff --git a/kolibri/plugins/perseus_viewer/kolibri_plugin.py b/kolibri/plugins/perseus_viewer/kolibri_plugin.py index aab79229eb8..fd1cb02477f 100644 --- a/kolibri/plugins/perseus_viewer/kolibri_plugin.py +++ b/kolibri/plugins/perseus_viewer/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from le_utils.constants import format_presets from kolibri.core.content import hooks as content_hooks diff --git a/kolibri/plugins/policies/kolibri_plugin.py b/kolibri/plugins/policies/kolibri_plugin.py index 61cb7c4ac1d..d3fd61521dc 100644 --- a/kolibri/plugins/policies/kolibri_plugin.py +++ b/kolibri/plugins/policies/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.core.webpack import hooks as webpack_hooks from kolibri.plugins import KolibriPluginBase from kolibri.plugins.hooks import register_hook diff --git a/kolibri/plugins/policies/views.py b/kolibri/plugins/policies/views.py index f9cc723377a..42643be40f8 100644 --- a/kolibri/plugins/policies/views.py +++ b/kolibri/plugins/policies/views.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.utils.decorators import method_decorator from django.views.generic.base import TemplateView diff --git a/kolibri/plugins/pwa/kolibri_plugin.py b/kolibri/plugins/pwa/kolibri_plugin.py index c35f0f6e361..0709903772b 100644 --- a/kolibri/plugins/pwa/kolibri_plugin.py +++ b/kolibri/plugins/pwa/kolibri_plugin.py @@ -3,10 +3,6 @@ # # Copyright 2023 Endless OS Foundation, LLC # SPDX-License-Identifier: MIT -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.template.loader import render_to_string from kolibri.core.hooks import FrontEndBaseHeadHook diff --git a/kolibri/plugins/pwa/views.py b/kolibri/plugins/pwa/views.py index 5712c928389..d1411338653 100644 --- a/kolibri/plugins/pwa/views.py +++ b/kolibri/plugins/pwa/views.py @@ -3,10 +3,6 @@ # # Copyright 2023 Endless OS Foundation, LLC # SPDX-License-Identifier: MIT -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from urllib.parse import quote from django.utils.decorators import method_decorator diff --git a/kolibri/plugins/registry.py b/kolibri/plugins/registry.py index a5cb0bedbc2..ed3a8e57403 100644 --- a/kolibri/plugins/registry.py +++ b/kolibri/plugins/registry.py @@ -27,10 +27,6 @@ """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging from importlib import import_module diff --git a/kolibri/plugins/setup_wizard/kolibri_plugin.py b/kolibri/plugins/setup_wizard/kolibri_plugin.py index 6d964acbb51..9d96505770c 100644 --- a/kolibri/plugins/setup_wizard/kolibri_plugin.py +++ b/kolibri/plugins/setup_wizard/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.core.device.hooks import SetupHook from kolibri.core.webpack import hooks as webpack_hooks from kolibri.plugins import KolibriPluginBase diff --git a/kolibri/plugins/setup_wizard/test/test_api.py b/kolibri/plugins/setup_wizard/test/test_api.py index 21ff4d6135b..559eac739c8 100644 --- a/kolibri/plugins/setup_wizard/test/test_api.py +++ b/kolibri/plugins/setup_wizard/test/test_api.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.urls import reverse from rest_framework.test import APITestCase diff --git a/kolibri/plugins/setup_wizard/views.py b/kolibri/plugins/setup_wizard/views.py index 9a7a657a04e..9d2c28fcec5 100644 --- a/kolibri/plugins/setup_wizard/views.py +++ b/kolibri/plugins/setup_wizard/views.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.shortcuts import redirect from django.urls import reverse from django.views.generic.base import TemplateView diff --git a/kolibri/plugins/slideshow_viewer/kolibri_plugin.py b/kolibri/plugins/slideshow_viewer/kolibri_plugin.py index b4741a79f95..8eedca83645 100644 --- a/kolibri/plugins/slideshow_viewer/kolibri_plugin.py +++ b/kolibri/plugins/slideshow_viewer/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from le_utils.constants import format_presets from kolibri.core.content import hooks as content_hooks diff --git a/kolibri/plugins/user_auth/hooks.py b/kolibri/plugins/user_auth/hooks.py index b92be66adce..2c5ea7fc758 100644 --- a/kolibri/plugins/user_auth/hooks.py +++ b/kolibri/plugins/user_auth/hooks.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.core.webpack.hooks import WebpackInclusionSyncMixin from kolibri.plugins.hooks import define_hook diff --git a/kolibri/plugins/user_auth/kolibri_plugin.py b/kolibri/plugins/user_auth/kolibri_plugin.py index 2c627f7b518..72803616c5b 100644 --- a/kolibri/plugins/user_auth/kolibri_plugin.py +++ b/kolibri/plugins/user_auth/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.core.auth.constants.user_kinds import ANONYMOUS from kolibri.core.device.utils import get_device_setting from kolibri.core.device.utils import is_landing_page diff --git a/kolibri/plugins/user_auth/templatetags/user_auth_tags.py b/kolibri/plugins/user_auth/templatetags/user_auth_tags.py index 33ea148c297..1262feee1ee 100644 --- a/kolibri/plugins/user_auth/templatetags/user_auth_tags.py +++ b/kolibri/plugins/user_auth/templatetags/user_auth_tags.py @@ -4,10 +4,6 @@ ======================== Tags for including plugin javascript assets into a template. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django import template from .. import hooks diff --git a/kolibri/plugins/user_auth/views.py b/kolibri/plugins/user_auth/views.py index 4328dfc3141..77027442159 100644 --- a/kolibri/plugins/user_auth/views.py +++ b/kolibri/plugins/user_auth/views.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.views.generic.base import TemplateView from kolibri.core.views import RootURLRedirectView diff --git a/kolibri/plugins/user_profile/kolibri_plugin.py b/kolibri/plugins/user_profile/kolibri_plugin.py index 669013ad69f..44eb94327cd 100644 --- a/kolibri/plugins/user_profile/kolibri_plugin.py +++ b/kolibri/plugins/user_profile/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.core.hooks import NavigationHook from kolibri.core.webpack import hooks as webpack_hooks from kolibri.plugins import KolibriPluginBase diff --git a/kolibri/plugins/user_profile/views.py b/kolibri/plugins/user_profile/views.py index 5a0566663e7..a13ecda4e9c 100644 --- a/kolibri/plugins/user_profile/views.py +++ b/kolibri/plugins/user_profile/views.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.utils.decorators import method_decorator from django.views.generic.base import TemplateView diff --git a/kolibri/plugins/utils/__init__.py b/kolibri/plugins/utils/__init__.py index 1892ac22225..ee441800a78 100644 --- a/kolibri/plugins/utils/__init__.py +++ b/kolibri/plugins/utils/__init__.py @@ -11,7 +11,6 @@ from django.core.management import call_command from django.urls import reverse from semver import VersionInfo -from six import string_types if sys.version_info < (3, 10): from importlib_metadata import entry_points @@ -324,7 +323,7 @@ def _update_plugin(self, plugin_name): ) return for app in plugin_instance.INSTALLED_APPS: - if not isinstance(app, AppConfig) and isinstance(app, string_types): + if not isinstance(app, AppConfig) and isinstance(app, str): app = apps.get_containing_app_config(app) app_configs.append(app) old_version = config["PLUGIN_VERSIONS"].get(plugin_name, "") diff --git a/kolibri/plugins/utils/settings.py b/kolibri/plugins/utils/settings.py index 089f04c227e..8d023b8d214 100644 --- a/kolibri/plugins/utils/settings.py +++ b/kolibri/plugins/utils/settings.py @@ -3,7 +3,6 @@ from types import ModuleType from django.apps import AppConfig -from six import string_types from kolibri.plugins.registry import registered_plugins from kolibri.plugins.utils import is_external_plugin @@ -11,7 +10,7 @@ def _validate_settings_module(settings_module): - if isinstance(settings_module, string_types): + if isinstance(settings_module, str): try: return importlib.import_module(settings_module) except ImportError: diff --git a/kolibri/plugins/utils/test/test_hooks.py b/kolibri/plugins/utils/test/test_hooks.py index bf92dedc535..573b3719626 100644 --- a/kolibri/plugins/utils/test/test_hooks.py +++ b/kolibri/plugins/utils/test/test_hooks.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from inspect import isabstract import pytest diff --git a/kolibri/utils/cli.py b/kolibri/utils/cli.py index 4ee533c8e50..149e71dcd45 100644 --- a/kolibri/utils/cli.py +++ b/kolibri/utils/cli.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging import signal import sys diff --git a/kolibri/utils/compat.py b/kolibri/utils/compat.py index 7cb4b0f46dc..7ec084bbf75 100644 --- a/kolibri/utils/compat.py +++ b/kolibri/utils/compat.py @@ -1,38 +1,19 @@ """ Compatibility layer for Python 2+3 """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import sys +from importlib.util import find_spec def module_exists(module_path): """ - Determines if a module exists without loading it (Python 3) - In Python 2, the module will be loaded + Determines if a module exists without loading it """ - if sys.version_info >= (3, 4): - from importlib.util import find_spec - - try: - return find_spec(module_path) is not None - except ImportError: - return False - elif sys.version_info < (3,): - from imp import find_module - - try: - if "." in module_path: - __import__(module_path) - else: - find_module(module_path) - return True - except ImportError: - return False - else: - raise NotImplementedError("No compatibility with Python 3.0 and 3.2") + + try: + return find_spec(module_path) is not None + except ImportError: + return False def monkey_patch_collections(): diff --git a/kolibri/utils/conf.py b/kolibri/utils/conf.py index 9b1ef053624..1b86834c218 100644 --- a/kolibri/utils/conf.py +++ b/kolibri/utils/conf.py @@ -15,10 +15,6 @@ instead of a dict. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging import os diff --git a/kolibri/utils/data.py b/kolibri/utils/data.py index 5e683f45273..48956b31959 100644 --- a/kolibri/utils/data.py +++ b/kolibri/utils/data.py @@ -1,6 +1,5 @@ import re -from six import string_types BYTES_PREFIXES = ("", "K", "M", "G", "T", "P") PREFIX_FACTOR_BYTES = 1000.0 @@ -36,7 +35,7 @@ def bytes_from_humans(size, suffix="B"): if isinstance(size, int): # If it is already an integer, return early. return size - if not isinstance(size, string_types): + if not isinstance(size, str): raise ValueError("size must be an integer or string") # Be lenient by making all input uppercase to maximize chance of a match. size = size.upper() diff --git a/kolibri/utils/env.py b/kolibri/utils/env.py index 142782097c7..1f2d766b181 100644 --- a/kolibri/utils/env.py +++ b/kolibri/utils/env.py @@ -67,18 +67,9 @@ def prepend_cext_path(dist_path): dirname = os.path.join(dist_path, "cext", python_version, system_name) abi3_dirname = os.path.join(dist_path, "cext", "abi3", system_name, machine_name) - # For Linux system with cpython<3.3, there could be abi tags 'm' and 'mu' - if system_name == "Linux" and sys.version_info < (3, 3): - # encode with ucs2 - if sys.maxunicode == 65535: - dirname = os.path.join(dirname, python_version + "m") - # encode with ucs4 - else: - dirname = os.path.join(dirname, python_version + "mu") - dirname = os.path.join(dirname, machine_name) noarch_dir = os.path.join(dist_path, "cext") - if sys.version_info >= (3, 6) and os.path.exists(abi3_dirname): + if os.path.exists(abi3_dirname): sys.path = [str(abi3_dirname)] + sys.path if os.path.exists(dirname): @@ -112,23 +103,6 @@ def set_env(): # Add path for c extensions to sys.path prepend_cext_path(os.path.realpath(os.path.dirname(kolibri_dist.__file__))) - # This was added in - # https://github.com/learningequality/kolibri/pull/580 - # ...we need to (re)move it /benjaoming - # Force python2 to interpret every string as unicode. - if sys.version[0] == "2": - reload(sys) # noqa - sys.setdefaultencoding("utf8") - - # Dynamically add the path of `py2only` to PYTHONPATH in Python 2 so that - # we only import the `future` and `futures` packages from system packages when - # running with Python 3. Please see `build_tools/py2only.py` for details. - sys.path = sys.path + [ - os.path.join( - os.path.realpath(os.path.dirname(kolibri_dist.__file__)), "py2only" - ) - ] - # Set default env for key, value in ENVIRONMENT_VARIABLES.items(): if "default" in value: diff --git a/kolibri/utils/file_transfer.py b/kolibri/utils/file_transfer.py index 12a2c1cd955..dc8398b5f0d 100644 --- a/kolibri/utils/file_transfer.py +++ b/kolibri/utils/file_transfer.py @@ -17,7 +17,6 @@ from requests.exceptions import ConnectionError from requests.exceptions import HTTPError from requests.exceptions import Timeout -from six import with_metaclass from kolibri.utils.filesystem import mkdirp @@ -447,7 +446,7 @@ def seekable(self): return True -class Transfer(with_metaclass(ABCMeta)): +class Transfer(metaclass=ABCMeta): DEFAULT_TIMEOUT = 60 def __init__( diff --git a/kolibri/utils/kolibri_whitenoise.py b/kolibri/utils/kolibri_whitenoise.py index 379710dfd99..18d39bc9da0 100644 --- a/kolibri/utils/kolibri_whitenoise.py +++ b/kolibri/utils/kolibri_whitenoise.py @@ -3,14 +3,14 @@ import stat from collections import OrderedDict from io import BufferedIOBase +from urllib.parse import parse_qs +from urllib.parse import urljoin from wsgiref.headers import Headers from django.contrib.staticfiles import finders from django.core.exceptions import SuspiciousFileOperation from django.core.files.storage import FileSystemStorage from django.utils._os import safe_join -from six.moves.urllib.parse import parse_qs -from six.moves.urllib.parse import urljoin from whitenoise import WhiteNoise from whitenoise.httpstatus_backport import HTTPStatus from whitenoise.responders import MissingFileError diff --git a/kolibri/utils/main.py b/kolibri/utils/main.py index d02a4bd2eb3..2c0cf0e38af 100644 --- a/kolibri/utils/main.py +++ b/kolibri/utils/main.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging.config import os import shutil diff --git a/kolibri/utils/options.py b/kolibri/utils/options.py index 8a3005e196c..34677055514 100644 --- a/kolibri/utils/options.py +++ b/kolibri/utils/options.py @@ -9,15 +9,14 @@ import os import sys from functools import update_wrapper +from urllib.parse import urlparse +from urllib.parse import urlunparse from configobj import ConfigObj from configobj import flatten_errors from configobj import get_extra_values from django.utils.functional import SimpleLazyObject from django.utils.module_loading import import_string -from django.utils.six import string_types -from six.moves.urllib.parse import urlparse -from six.moves.urllib.parse import urlunparse from validate import is_boolean from validate import Validator from validate import VdtTypeError @@ -132,7 +131,7 @@ def _process_list(value, separator=","): if not isinstance(value, list): if not value: value = [] - elif isinstance(value, string_types): + elif isinstance(value, str): value = value.split(separator) else: value = [value] @@ -176,7 +175,7 @@ def language_list(value): def path(value): from kolibri.utils.conf import KOLIBRI_HOME - if not isinstance(value, string_types): + if not isinstance(value, str): raise VdtValueError(repr(value)) # Allow for blank paths if value: @@ -190,7 +189,7 @@ def path_list(value): Check that the supplied value is a semicolon-delimited list of paths. Note: we do not guarantee that these paths all currently exist. """ - if isinstance(value, string_types): + if isinstance(value, str): value = value.split(";") out = [] @@ -248,7 +247,7 @@ def validate_bytes(value): def url_prefix(value): - if not isinstance(value, string_types): + if not isinstance(value, str): raise VdtValueError(value) return value.lstrip("/").rstrip("/") + "/" @@ -301,7 +300,7 @@ def lazy_import_callback(value): is internal to Kolibri, and also because the module may not be available in some contexts. """ - if not isinstance(value, string_types): + if not isinstance(value, str): raise VdtValueError(value) try: # Check that the string is at least parseable as a module name diff --git a/kolibri/utils/pskolibri/__init__.py b/kolibri/utils/pskolibri/__init__.py index 9b64abebe7c..7e552797efa 100644 --- a/kolibri/utils/pskolibri/__init__.py +++ b/kolibri/utils/pskolibri/__init__.py @@ -52,7 +52,6 @@ from kolibri.utils.pskolibri.common import LINUX from kolibri.utils.pskolibri.common import memoize_when_activated from kolibri.utils.pskolibri.common import NoSuchProcess -from kolibri.utils.pskolibri.common import PY3 from kolibri.utils.pskolibri.common import WINDOWS @@ -222,8 +221,6 @@ def _init(self, pid, _ignore_nsp=False): if pid is None: pid = os.getpid() else: - if not PY3 and not isinstance(pid, (int, long)): # noqa F821 - raise TypeError("pid must be an integer (got %r)" % pid) if pid < 0: raise ValueError("pid must be a positive integer (got %s)" % pid) self._pid = pid diff --git a/kolibri/utils/pskolibri/common.py b/kolibri/utils/pskolibri/common.py index a85e121a461..43bc7d5f259 100644 --- a/kolibri/utils/pskolibri/common.py +++ b/kolibri/utils/pskolibri/common.py @@ -10,32 +10,21 @@ from kolibri.utils.android import on_android -PY3 = sys.version_info[0] == 3 POSIX = os.name == "posix" WINDOWS = os.name == "nt" LINUX = sys.platform.startswith("linux") and not on_android() MACOS = sys.platform.startswith("darwin") -if PY3: - def b(s): - return s.encode("latin-1") - - -else: - - def b(s): - return s +def b(s): + return s.encode("latin-1") ENCODING = sys.getfilesystemencoding() -if not PY3: - ENCODING_ERRS = "replace" -else: - try: - ENCODING_ERRS = sys.getfilesystemencodeerrors() # py 3.6 - except AttributeError: - ENCODING_ERRS = "surrogateescape" if POSIX else "replace" +try: + ENCODING_ERRS = sys.getfilesystemencodeerrors() # py 3.6 +except AttributeError: + ENCODING_ERRS = "surrogateescape" if POSIX else "replace" pcputimes = namedtuple( "pcputimes", ["user", "system", "children_user", "children_system"] @@ -149,9 +138,7 @@ def open_binary(fname, **kwargs): def open_text(fname, **kwargs): """On Python 3 opens a file in text mode by using fs encoding and a proper en/decoding errors handler. - On Python 2 this is just an alias for open(name, 'rt'). """ - if PY3: - kwargs.setdefault("encoding", ENCODING) - kwargs.setdefault("errors", ENCODING_ERRS) + kwargs.setdefault("encoding", ENCODING) + kwargs.setdefault("errors", ENCODING_ERRS) return io.open(fname, "rt", **kwargs) diff --git a/kolibri/utils/system.py b/kolibri/utils/system.py index 34d04efac85..143b26de8dc 100644 --- a/kolibri/utils/system.py +++ b/kolibri/utils/system.py @@ -12,15 +12,10 @@ etc.. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging import os import sys -import six from django.db import connections from .conf import KOLIBRI_HOME @@ -58,7 +53,7 @@ def _windows_pid_exists(pid): return False -buffering = int(six.PY3) # No unbuffered text I/O on Python 3 (#20815). +buffering = 1 # No unbuffered text I/O on Python 3 (#20815). def _posix_become_daemon( diff --git a/kolibri/utils/tests/test_data.py b/kolibri/utils/tests/test_data.py index b92e4b90ea2..ecaef68ac7c 100644 --- a/kolibri/utils/tests/test_data.py +++ b/kolibri/utils/tests/test_data.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.test import TestCase from kolibri.utils.data import bytes_for_humans diff --git a/kolibri/utils/tests/test_options.py b/kolibri/utils/tests/test_options.py index 7e08b7441f9..cd9031cf221 100644 --- a/kolibri/utils/tests/test_options.py +++ b/kolibri/utils/tests/test_options.py @@ -1,10 +1,6 @@ """ Tests for `kolibri.utils.options` module. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging import os import sys diff --git a/kolibri/utils/tests/test_sanity_check.py b/kolibri/utils/tests/test_sanity_check.py index d6e09d4e9f0..d2aa72e8a08 100644 --- a/kolibri/utils/tests/test_sanity_check.py +++ b/kolibri/utils/tests/test_sanity_check.py @@ -1,6 +1,5 @@ import sys import tempfile -import unittest from django.db.utils import OperationalError from django.test import TestCase @@ -14,14 +13,6 @@ class SanityCheckTestCase(TestCase): - @unittest.skipIf( - sys.version_info[0] < 3, - """ - This test fails on CI for Python 2.7, but not locally. - Seems to be something to do with the wonky way we're - creating the test container for the test. - """, - ) @patch("kolibri.utils.sanity_checks.logging.error") @override_option( "Paths", "CONTENT_DIR", "Z:\\NOTREAL" if sys.platform == "win32" else "/dir_dne" diff --git a/kolibri/utils/tests/test_server.py b/kolibri/utils/tests/test_server.py index e9b88dc32fe..53e63836a6f 100755 --- a/kolibri/utils/tests/test_server.py +++ b/kolibri/utils/tests/test_server.py @@ -1,10 +1,6 @@ """ Tests for `kolibri.utils.server` module. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import os from unittest import TestCase diff --git a/kolibri/utils/tests/test_version.py b/kolibri/utils/tests/test_version.py index e5815923ee5..6a78b549653 100755 --- a/kolibri/utils/tests/test_version.py +++ b/kolibri/utils/tests/test_version.py @@ -1,10 +1,6 @@ """ Tests for `kolibri` module. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import unittest import mock diff --git a/kolibri/utils/translation.py b/kolibri/utils/translation.py index 724e00b7a33..362aefe596d 100644 --- a/kolibri/utils/translation.py +++ b/kolibri/utils/translation.py @@ -12,7 +12,6 @@ from django.core.exceptions import AppRegistryNotReady from django.core.exceptions import ImproperlyConfigured -from django.utils import six from django.utils import translation as django_translation_module from django.utils.decorators import ContextDecorator from django.utils.safestring import mark_safe @@ -164,10 +163,4 @@ def gettext(message): return do_translate(message, "gettext") -if six.PY3: - ugettext = gettext -else: - - @prefer_django - def ugettext(message): - return do_translate(message, "ugettext") +ugettext = gettext diff --git a/packages/kolibri-tools/lib/i18n/utils.py b/packages/kolibri-tools/lib/i18n/utils.py index 195011ec862..5015fd51311 100644 --- a/packages/kolibri-tools/lib/i18n/utils.py +++ b/packages/kolibri-tools/lib/i18n/utils.py @@ -114,26 +114,14 @@ def json_dump_formatted(data, file_path, file_name): # Format and write the JSON file with io.open(file_path_with_file_name, mode="w+", encoding="utf-8") as file_object: - # Manage unicode for the JSON dumping - if sys.version_info[0] < 3: - output = json.dumps( - data, - sort_keys=True, - indent=2, - separators=(",", ": "), - ensure_ascii=False, - ) - output = unicode(output, "utf-8") # noqa - file_object.write(output) - else: - json.dump( - data, - file_object, - sort_keys=True, - indent=2, - separators=(",", ": "), - ensure_ascii=False, - ) + json.dump( + data, + file_object, + sort_keys=True, + indent=2, + separators=(",", ": "), + ensure_ascii=False, + ) def read_config_file(): diff --git a/requirements/base.txt b/requirements/base.txt index 5e1096c2557..05dc4828b0e 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,18 +4,14 @@ django-filter==1.1.0 # pyup: <2.0.0 django-js-reverse==0.9.1 djangorestframework==3.9.1 django==1.11.29 # pyup: >=1.11,<2 -six==1.14.0 colorlog==3.2.0 # pyup: <4.0.0 configobj==5.0.6 django-mptt==0.9.1 requests==2.27.1 cheroot==8.6.0 magicbus==4.1.2 -futures==3.1.1 # Temporarily pinning this until we can do a Python 2/3 compatible solution of newer versions # pyup: <=3.1.1 -more-itertools==5.0.0 # Last Python 2.7 friendly release # pyup: <6.0 le-utils==0.2.2 jsonfield==2.0.2 -requests-toolbelt==0.9.1 morango==0.7.1 tzlocal==2.1 pytz==2022.1 @@ -26,7 +22,6 @@ django-redis-cache==2.0.0 redis==3.2.1 html5lib==1.0.1 zeroconf-py2compat==0.19.17 -ifcfg==0.21 Click==7.0 whitenoise==4.1.4 idna==2.8 diff --git a/requirements/build.txt b/requirements/build.txt index fbef5cda989..500c3120fdc 100644 --- a/requirements/build.txt +++ b/requirements/build.txt @@ -1,10 +1,11 @@ # Requirements for building wheels -# These requirements have to support Python 2.7 +# These requirements have to support Python 3.6 # This does not depend on runtime stuff so we do not # include base.txt -pex<1.6 +pex==2.1.153 pip>=20.3.4 setuptools>=20.3,<41,!=34.*,!=35.* # https://github.com/pantsbuild/pex/blob/master/pex/version.py#L6 # pyup: ignore beautifulsoup4==4.8.2 requests==2.21.0 pkginfo==1.8.2 +wheel>=0.31.1 diff --git a/setup.py b/setup.py index af2a27b81ff..2e4451232f2 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import os from setuptools import setup @@ -96,8 +92,6 @@ def run(self): "License :: OSI Approved :: MIT License", "Natural Language :: English", "Development Status :: 4 - Beta", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", @@ -108,5 +102,5 @@ def run(self): "Programming Language :: Python :: Implementation :: PyPy", ], cmdclass={"install_scripts": gen_windows_batch_files}, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <3.12", + python_requires=">=3.6, <3.12", ) diff --git a/test/coverage_blame.py b/test/coverage_blame.py index 18fe4afe7f5..e33d757db75 100755 --- a/test/coverage_blame.py +++ b/test/coverage_blame.py @@ -3,10 +3,6 @@ # Derived from http://scottlobdell.me/2015/04/gamifying-test-coverage-project/ # Before running this script, first run tests with coverage using: # tox -e py3.9 -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from collections import Counter from subprocess import PIPE from subprocess import Popen diff --git a/test/test_future_and_futures.py b/test/test_future_and_futures.py deleted file mode 100644 index 6639491d783..00000000000 --- a/test/test_future_and_futures.py +++ /dev/null @@ -1,40 +0,0 @@ -import imp -import os -import sys - -# Import from kolibri first to ensure Kolibri's monkey patches are applied. -from kolibri import dist as kolibri_dist # noreorder - - -dist_dir = os.path.realpath(os.path.dirname(kolibri_dist.__file__)) - - -def test_import_concurrent_py3(): - import concurrent - - if sys.version_info[0] == 3: - # Python 3 is supposed to import its builtin package `concurrent` - # instead of being inside kolibri/dist/py2only or kolibri/dist - concurrent_parent_path = os.path.realpath( - os.path.dirname(os.path.dirname(concurrent.__file__)) - ) - - assert dist_dir != concurrent_parent_path - assert os.path.join(dist_dir, "py2only") != concurrent_parent_path - - -def test_import_future_py2(): - from future.standard_library import TOP_LEVEL_MODULES - - if sys.version_info[0] == 2: - for module_name in TOP_LEVEL_MODULES: - if "test" in module_name: - continue - - module_parent_path = os.path.realpath( - os.path.dirname(imp.find_module(module_name)[1]) - ) - # future's standard libraries such as `html` should not be found - # at the same level as kolibri/dist; otherwise, python3 will try to - # import them from kolibri/dist instead of its builtin packages - assert dist_dir != module_parent_path diff --git a/tox.ini b/tox.ini index 2525a1a4add..011f418a103 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{2.7,3.6,3.7,3.8,3.9,3.10,3.11}, postgres +envlist = py{3.6,3.7,3.8,3.9,3.10,3.11}, postgres [testenv] usedevelop = True @@ -13,7 +13,6 @@ setenv = KOLIBRI_RUN_MODE = tox SKIP_PY_CHECK = 1 basepython = - py2.7: python2.7 py3.6: python3.6 py3.7: python3.7 py3.8: python3.8