diff --git a/.github/workflows/verify-dunder-init.yml b/.github/workflows/verify-dunder-init.yml index 9d920238ebd4..c398c506730b 100644 --- a/.github/workflows/verify-dunder-init.yml +++ b/.github/workflows/verify-dunder-init.yml @@ -1,4 +1,4 @@ -name: CI +name: Verify Dunder __init__.py Files on: pull_request: diff --git a/common/djangoapps/student/tests/test_linkedin.py b/common/djangoapps/student/tests/test_linkedin.py index 50dd921f25a9..a5de21595bb6 100644 --- a/common/djangoapps/student/tests/test_linkedin.py +++ b/common/djangoapps/student/tests/test_linkedin.py @@ -38,28 +38,20 @@ class LinkedInAddToProfileUrlTests(TestCase): def test_linked_in_url(self, cert_mode, expected_cert_name): config = LinkedInAddToProfileConfigurationFactory() - # We can switch to this once edx-platform reaches Python 3.8 - # expected_url = ( - # 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&' - # 'name={platform}+{cert_name}&certUrl={cert_url}&' - # 'organizationId={company_identifier}' - # ).format( - # platform=quote(settings.PLATFORM_NAME.encode('utf-8')), - # cert_name=expected_cert_name, - # cert_url=quote(self.CERT_URL, safe=''), - # company_identifier=config.company_identifier, - # ) + expected_url = ( + 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&' + 'name={platform}+{cert_name}&certUrl={cert_url}&' + 'organizationId={company_identifier}' + ).format( + platform=quote(settings.PLATFORM_NAME.encode('utf-8')), + cert_name=expected_cert_name, + cert_url=quote(self.CERT_URL, safe=''), + company_identifier=config.company_identifier, + ) actual_url = config.add_to_profile_url(self.COURSE_NAME, cert_mode, self.CERT_URL) - # We can switch to this instead of the assertIn once edx-platform reaches Python 3.8 - # There was a problem with dict ordering in the add_to_profile_url function that will go away then. - # self.assertEqual(actual_url, expected_url) - - assert 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME' in actual_url - assert f'&name={quote(settings.PLATFORM_NAME.encode("utf-8"))}+{expected_cert_name}' in actual_url - assert '&certUrl={cert_url}'.format(cert_url=quote(self.CERT_URL, safe='')) in actual_url - assert f'&organizationId={config.company_identifier}' in actual_url + self.assertEqual(actual_url, expected_url) @ddt.data( ('honor', 'Honor+Code+Credential+for+Test+Course+%E2%98%83'), @@ -72,26 +64,18 @@ def test_linked_in_url(self, cert_mode, expected_cert_name): def test_linked_in_url_with_cert_name_override(self, cert_mode, expected_cert_name): config = LinkedInAddToProfileConfigurationFactory() - # We can switch to this once edx-platform reaches Python 3.8 - # expected_url = ( - # 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&' - # 'name={platform}+{cert_name}&certUrl={cert_url}&' - # 'organizationId={company_identifier}' - # ).format( - # platform=quote(settings.PLATFORM_NAME.encode('utf-8')), - # cert_name=expected_cert_name, - # cert_url=quote(self.CERT_URL, safe=''), - # company_identifier=config.company_identifier, - # ) + expected_url = ( + 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&' + 'name={platform}+{cert_name}&certUrl={cert_url}&' + 'organizationId={company_identifier}' + ).format( + platform=quote(settings.PLATFORM_NAME.encode('utf-8')), + cert_name=expected_cert_name, + cert_url=quote(self.CERT_URL, safe=''), + company_identifier=config.company_identifier, + ) with with_site_configuration_context(configuration=self.SITE_CONFIGURATION): actual_url = config.add_to_profile_url(self.COURSE_NAME, cert_mode, self.CERT_URL) - # We can switch to this instead of the assertIn once edx-platform reaches Python 3.8 - # There was a problem with dict ordering in the add_to_profile_url function that will go away then. - # self.assertEqual(actual_url, expected_url) - - assert 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME' in actual_url - assert f'&name={quote(settings.PLATFORM_NAME.encode("utf-8"))}+{expected_cert_name}' in actual_url - assert '&certUrl={cert_url}'.format(cert_url=quote(self.CERT_URL, safe='')) in actual_url - assert f'&organizationId={config.company_identifier}' in actual_url + self.assertEqual(actual_url, expected_url) diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 9e61095260b6..c640462acf10 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -440,28 +440,19 @@ def test_linked_in_add_to_profile_btn_with_certificate(self): assert response.status_code == 200 self.assertContains(response, 'Add Certificate to LinkedIn') - # We can switch to this and the commented out assertContains once edx-platform reaches Python 3.8 - # expected_url = ( - # 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&' - # 'name={platform}+Honor+Code+Certificate+for+Omega&certUrl={cert_url}&' - # 'organizationId={company_identifier}' - # ).format( - # platform=quote(settings.PLATFORM_NAME.encode('utf-8')), - # cert_url=quote(cert.download_url, safe=''), - # company_identifier=linkedin_config.company_identifier, - # ) - - # self.assertContains(response, escape(expected_url)) - - # These can be removed (in favor of the above) once we are on Python 3.8. Fails in 3.5 because of dict ordering - self.assertContains(response, escape('https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME')) - self.assertContains(response, escape('&name={platform}+Honor+Code+Certificate+for+Omega'.format( - platform=quote(settings.PLATFORM_NAME.encode('utf-8')) - ))) - self.assertContains(response, escape('&certUrl={cert_url}'.format(cert_url=quote(cert.download_url, safe='')))) - self.assertContains(response, escape('&organizationId={company_identifier}'.format( + expected_url = ( + 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&' + 'name={platform}+Honor+Code+Certificate+for+Omega&' + 'certUrl={cert_url}&' + 'organizationId={company_identifier}' + ).format( + platform=quote(settings.PLATFORM_NAME.encode('utf-8')), + cert_url=quote(cert.download_url, safe=''), company_identifier=linkedin_config.company_identifier - ))) + ) + + # Single assertion for the expected LinkedIn URL + self.assertContains(response, escape(expected_url)) @skip_unless_lms def test_dashboard_metadata_caching(self): diff --git a/common/djangoapps/util/cache.py b/common/djangoapps/util/cache.py index ca2ee2050eae..3ec99efc636d 100644 --- a/common/djangoapps/util/cache.py +++ b/common/djangoapps/util/cache.py @@ -53,13 +53,11 @@ def wrapper(request, *args, **kwargs): # specifically the branding index, to do authentication. # If that page is cached the authentication doesn't # happen, so we disable the cache when that feature is enabled. - if ( - not request.user.is_authenticated - ): + if not request.user.is_authenticated: # Use the cache. The same view accessed through different domain names may # return different things, so include the domain name in the key. - domain = str(request.META.get('HTTP_HOST')) + '.' - cache_key = domain + "cache_if_anonymous." + get_language() + '.' + request.path + domain = request.META.get('HTTP_HOST', '') + '.' + cache_key = f"{domain}cache_if_anonymous.{get_language()}.{request.path}" # Include the values of GET parameters in the cache key. for get_parameter in get_parameters: @@ -67,24 +65,20 @@ def wrapper(request, *args, **kwargs): if parameter_value is not None: # urlencode expects data to be of type str, and doesn't deal well with Unicode data # since it doesn't provide a way to specify an encoding. - cache_key = cache_key + '.' + urlencode({ - get_parameter: str(parameter_value).encode('utf-8') - }) + cache_key += '.' + urlencode({get_parameter: str(parameter_value).encode('utf-8')}) response = cache.get(cache_key) - if response: - # A hack to ensure that the response data is a valid text type for both Python 2 and 3. - response_content = list(response._container) # lint-amnesty, pylint: disable=bad-option-value, protected-access, protected-member - response.content = b'' - for item in response_content: - response.write(item) + # Ensure that response content is properly handled for caching + response.content = ( + # pylint: disable=protected-access + b''.join(response._container) if hasattr(response, '_container') else response.content + ) else: response = view_func(request, *args, **kwargs) cache.set(cache_key, response, 60 * 3) return response - else: # Don't use the cache. return view_func(request, *args, **kwargs) diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py index c1b699c44afa..0662d09a41ea 100644 --- a/lms/djangoapps/learner_dashboard/tests/test_programs.py +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -86,15 +86,9 @@ def program_sort_key(cls, program): def assert_dict_contains_subset(self, superset, subset): """ Verify that the dict superset contains the dict subset. - - Works like assertDictContainsSubset, deprecated since Python 3.2. - See: https://docs.python.org/2.7/library/unittest.html#unittest.TestCase.assertDictContainsSubset. """ - superset_keys = set(superset.keys()) - subset_keys = set(subset.keys()) - intersection = {key: superset[key] for key in superset_keys & subset_keys} - - assert subset == intersection + for key, value in subset.items(): + assert key in superset and superset[key] == value, f"{key}: {value} not found in superset or does not match" def test_login_required(self, mock_get_programs): """ diff --git a/openedx/core/djangoapps/content/search/documents.py b/openedx/core/djangoapps/content/search/documents.py index fb385ccc4503..7e547ca4d889 100644 --- a/openedx/core/djangoapps/content/search/documents.py +++ b/openedx/core/djangoapps/content/search/documents.py @@ -106,9 +106,12 @@ def meili_id_from_opaque_key(usage_key: UsageKey) -> str: we could use PublishableEntity's primary key / UUID instead. """ # The slugified key _may_ not be unique so we append a hashed string to make it unique: - key_bin = str(usage_key).encode() - suffix = blake2b(key_bin, digest_size=4).hexdigest() # When we use Python 3.9+, should add usedforsecurity=False - return slugify(str(usage_key)) + "-" + suffix + key_str = str(usage_key) + key_bin = key_str.encode() + + suffix = blake2b(key_bin, digest_size=4, usedforsecurity=False).hexdigest() + + return f"{slugify(key_str)}-{suffix}" def _meili_access_id_from_context_key(context_key: LearningContextKey) -> int: diff --git a/openedx/core/djangoapps/content_libraries/tests/base.py b/openedx/core/djangoapps/content_libraries/tests/base.py index 7f1664e6c7fc..638c053f62c3 100644 --- a/openedx/core/djangoapps/content_libraries/tests/base.py +++ b/openedx/core/djangoapps/content_libraries/tests/base.py @@ -86,11 +86,10 @@ def assertDictContainsEntries(self, big_dict, subset_dict): """ Assert that the first dict contains at least all of the same entries as the second dict. - - Like python 2's assertDictContainsSubset, but with the arguments in the - correct order. """ - assert big_dict.items() >= subset_dict.items() + for key, value in subset_dict.items(): + assert key in big_dict, f"Missing key: {key}" + assert big_dict[key] == value, f"Value for key {key} does not match: expected {value}, got {big_dict[key]}" def assertOrderEqual(self, libraries_list, expected_order): """ diff --git a/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py b/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py index a546cbbcd4bb..4f783fa6890f 100644 --- a/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py +++ b/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py @@ -270,14 +270,14 @@ def _assert_cookie_sent(self, response, is_set): if is_set: assert self.COOKIE_NAME in response.cookies cookie_header = str(response.cookies[self.COOKIE_NAME]) - # lint-amnesty, pylint: disable=bad-option-value, unicode-format-string + expected = 'Set-Cookie: {name}={value}; Domain={domain};'.format( name=self.COOKIE_NAME, value=self.COOKIE_VALUE, domain=self.COOKIE_DOMAIN ) assert expected in cookie_header - # added lower function because in python 3 the value of cookie_header has Secure and secure in python 2 - assert 'Max-Age=31449600; Path=/; secure'.lower() in cookie_header.lower() + # Check for 'Max-Age=31449600; Path=/; secure' in a case-insensitive manner + assert 'max-age=31449600; path=/; secure' in cookie_header.lower() else: assert self.COOKIE_NAME not in response.cookies diff --git a/openedx/core/djangoapps/crawlers/models.py b/openedx/core/djangoapps/crawlers/models.py index b7f9d23d6544..fe0ae3193b0f 100644 --- a/openedx/core/djangoapps/crawlers/models.py +++ b/openedx/core/djangoapps/crawlers/models.py @@ -42,16 +42,14 @@ def is_crawler(cls, request): # If there was no user agent detected or no crawler agents configured, # then just return False. - if (not req_user_agent) or (not crawler_agents): + if not req_user_agent or not crawler_agents: return False - # The crawler_agents list we pull from our model always has unicode objects, but the - # req_user_agent we get from HTTP headers ultimately comes to us via WSGI. That - # value is an ISO-8859-1 encoded byte string in Python 2.7 (and in the HTTP spec), but - # it will be a unicode str when we move to Python 3.x. This code should work under - # either version. + # Decode req_user_agent if it's bytes, so we can work with consistent string types. if isinstance(req_user_agent, bytes): - crawler_agents = [crawler_agent.encode('iso-8859-1') for crawler_agent in crawler_agents] + req_user_agent = req_user_agent.decode('iso-8859-1') + + crawler_agents = [crawler_agent.strip() for crawler_agent in crawler_agents] # We perform prefix matching of the crawler agent here so that we don't # have to worry about version bumps. diff --git a/openedx/core/lib/cache_utils.py b/openedx/core/lib/cache_utils.py index c379ab2d961a..a889da6130ee 100644 --- a/openedx/core/lib/cache_utils.py +++ b/openedx/core/lib/cache_utils.py @@ -207,7 +207,7 @@ def decorator(*args, **kwargs): # pylint: disable=unused-argument,missing-docst def zpickle(data): """Given any data structure, returns a zlib compressed pickled serialization.""" - return zlib.compress(pickle.dumps(data, 4)) # Keep this constant as we upgrade from python 2 to 3. + return zlib.compress(pickle.dumps(data, 4)) def zunpickle(zdata): diff --git a/openedx/core/lib/grade_utils.py b/openedx/core/lib/grade_utils.py index 2e7073e962dc..af3d12c9e2fd 100644 --- a/openedx/core/lib/grade_utils.py +++ b/openedx/core/lib/grade_utils.py @@ -48,11 +48,9 @@ def is_score_higher_or_equal(earned1, possible1, earned2, possible2, treat_undef def round_away_from_zero(number, digits=0): """ Round numbers using the 'away from zero' strategy as opposed to the - 'Banker's rounding strategy.' The strategy refers to how we round when - a number is half way between two numbers. eg. 0.5, 1.5, etc. In python 2 - positive numbers in this category would be rounded up and negative numbers - would be rounded down. ie. away from zero. In python 3 numbers round - towards even. So 0.5 would round to 0 but 1.5 would round to 2. + 'Banker's rounding strategy.' The strategy refers to how we round when + a number is half way between two numbers. eg. 0.5, 1.5, etc. In python 3 + numbers round towards even. So 0.5 would round to 0 but 1.5 would round to 2. See here for more on floating point rounding strategies: https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules diff --git a/pavelib/paver_tests/test_timer.py b/pavelib/paver_tests/test_timer.py index 5ccbf74abcf9..bc9817668347 100644 --- a/pavelib/paver_tests/test_timer.py +++ b/pavelib/paver_tests/test_timer.py @@ -77,17 +77,9 @@ def test_times(self): messages = self.get_log_messages() assert len(messages) == 1 - # I'm not using assertDictContainsSubset because it is - # removed in python 3.2 (because the arguments were backwards) - # and it wasn't ever replaced by anything *headdesk* - assert 'duration' in messages[0] - assert 35.6 == messages[0]['duration'] - - assert 'started_at' in messages[0] - assert start.isoformat(' ') == messages[0]['started_at'] - - assert 'ended_at' in messages[0] - assert end.isoformat(' ') == messages[0]['ended_at'] + assert 'duration' in messages[0] and messages[0]['duration'] == 35.6 + assert 'started_at' in messages[0] and messages[0]['started_at'] == start.isoformat(' ') + assert 'ended_at' in messages[0] and messages[0]['ended_at'] == end.isoformat(' ') @patch.object(timer, 'PAVER_TIMER_LOG', None) def test_no_logs(self): @@ -99,28 +91,18 @@ def test_arguments(self): messages = self.get_log_messages(args=(1, 'foo'), kwargs=dict(bar='baz')) assert len(messages) == 1 - # I'm not using assertDictContainsSubset because it is - # removed in python 3.2 (because the arguments were backwards) - # and it wasn't ever replaced by anything *headdesk* - assert 'args' in messages[0] - assert [repr(1), repr('foo')] == messages[0]['args'] - assert 'kwargs' in messages[0] - assert {'bar': repr('baz')} == messages[0]['kwargs'] + assert 'args' in messages[0] and messages[0]['args'] == [repr(1), repr('foo')] + assert 'kwargs' in messages[0] and messages[0]['kwargs'] == {'bar': repr('baz')} @patch.object(timer, 'PAVER_TIMER_LOG', '/tmp/some-log') def test_task_name(self): messages = self.get_log_messages() assert len(messages) == 1 - # I'm not using assertDictContainsSubset because it is - # removed in python 3.2 (because the arguments were backwards) - # and it wasn't ever replaced by anything *headdesk* - assert 'task' in messages[0] - assert 'pavelib.paver_tests.test_timer.identity' == messages[0]['task'] + assert 'task' in messages[0] and messages[0]['task'] == 'pavelib.paver_tests.test_timer.identity' @patch.object(timer, 'PAVER_TIMER_LOG', '/tmp/some-log') def test_exceptions(self): - @timer.timed def raises(): """ @@ -131,11 +113,7 @@ def raises(): messages = self.get_log_messages(task=raises, raises=Exception) assert len(messages) == 1 - # I'm not using assertDictContainsSubset because it is - # removed in python 3.2 (because the arguments were backwards) - # and it wasn't ever replaced by anything *headdesk* - assert 'exception' in messages[0] - assert 'Exception: The Message!' == messages[0]['exception'] + assert 'exception' in messages[0] and messages[0]['exception'] == 'Exception: The Message!' @patch.object(timer, 'PAVER_TIMER_LOG', '/tmp/some-log-%Y-%m-%d-%H-%M-%S.log') def test_date_formatting(self): diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 1fe478a4b9ae..47614c5a5956 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -19,10 +19,6 @@ # Ticket: https://github.com/openedx/edx-platform/issues/35334 algoliasearch<4.0.0 -# Date: 2024-03-14 -# Temporary to Support the python 3.11 Upgrade -# Issue for unpinning: https://github.com/openedx/edx-platform/issues/35281 -backports.zoneinfo;python_version<"3.9" # Newer versions have zoneinfo available in the standard library # Date: 2020-02-26 # As it is not clarified what exact breaking changes will be introduced as per diff --git a/scripts/gha-shards-readme.md b/scripts/gha-shards-readme.md index 9996a1f3b241..d7a1bd9ef7ab 100644 --- a/scripts/gha-shards-readme.md +++ b/scripts/gha-shards-readme.md @@ -32,4 +32,4 @@ You'd have to update the `unit-test-shards.json` file manually to fix this. ``` pytest --collect-only --ds=cms.envs.test cms/ ``` -For more details on how this check collects and compares the unit tests count please take a look at [verify unit tests count](../.github/workflows/verify-gha-unit-tests-count.yml) +For more details on how this check collects and compares the unit tests count please take a look at [verify unit tests count](../.github/workflows/unit-tests.yml) diff --git a/scripts/structures_pruning/README.rst b/scripts/structures_pruning/README.rst index c16a837a93af..77bc6bfbf0c8 100644 --- a/scripts/structures_pruning/README.rst +++ b/scripts/structures_pruning/README.rst @@ -29,11 +29,11 @@ To download the scripts, you can perform a partial clone of the edx-platform rep Create Python Virtual Environment --------------------------------- -Create a Python virtual environment using Python 3.8: +Create a Python virtual environment using Python 3.11: .. code-block:: bash - python3.8 -m venv ../venv + python3.11 -m venv ../venv source ../venv/bin/activate Install Pip Packages diff --git a/scripts/user_retirement/README.rst b/scripts/user_retirement/README.rst index d5417f7f7e75..380a2489108b 100644 --- a/scripts/user_retirement/README.rst +++ b/scripts/user_retirement/README.rst @@ -28,11 +28,11 @@ To download the scripts, you can perform a partial clone of the edx-platform rep Create Python Virtual Environment --------------------------------- -Create a Python virtual environment using Python 3.8: +Create a Python virtual environment using Python 3.11: .. code-block:: bash - python3.8 -m venv ../venv + python3.11 -m venv ../venv source ../venv/bin/activate Install Pip Packages diff --git a/scripts/user_retirement/utils/helpers.py b/scripts/user_retirement/utils/helpers.py index 1bcbadb4b3c4..804852833436 100644 --- a/scripts/user_retirement/utils/helpers.py +++ b/scripts/user_retirement/utils/helpers.py @@ -43,12 +43,11 @@ def _fail(kind, code, message): """ _log(kind, message) - # Try to get a traceback, if there is one. On Python 3.4 this raises an AttributeError - # if there is no current exception, so we eat that here. - try: - _log(kind, traceback.format_exc()) - except AttributeError: - pass + # Log the traceback if an exception is currently being handled + exc_type, exc_value, exc_traceback = sys.exc_info() + if exc_type is not None: + traceback_str = traceback.format_exception(exc_type, exc_value, exc_traceback) + _log(kind, ''.join(traceback_str)) sys.exit(code) @@ -66,16 +65,12 @@ def _get_error_str_from_exception(exc): """ Return a string from an exception that may or may not have a .content (Slumber) """ - exc_msg = text_type(exc) + exc_msg = str(exc) if hasattr(exc, 'content'): - # Slumber inconveniently discards the decoded .text attribute from the Response object, - # and instead gives us the raw encoded .content attribute, so we need to decode it first. - # Python 2 needs the decode, Py3 does not have it. - try: - exc_msg += '\n' + str(exc.content).decode('utf-8') - except AttributeError: - exc_msg += '\n' + str(exc.content) + # Attempt to decode `exc.content` if it's in bytes, otherwise just convert to str + exc_content = exc.content.decode('utf-8') if isinstance(exc.content, bytes) else str(exc.content) + exc_msg += '\n' + exc_content return exc_msg diff --git a/xmodule/capa/safe_exec/tests/test_safe_exec.py b/xmodule/capa/safe_exec/tests/test_safe_exec.py index fa12e4f69903..36d2cc965710 100644 --- a/xmodule/capa/safe_exec/tests/test_safe_exec.py +++ b/xmodule/capa/safe_exec/tests/test_safe_exec.py @@ -306,13 +306,6 @@ def equal_but_different_dicts(self): """ d1 = {k: 1 for k in "abcdefghijklmnopqrstuvwxyz"} d2 = {k: 1 for k in "bcdefghijklmnopqrstuvwxyza"} - # TODO: remove the next lines once we are in python3.8 - # since python3.8 dict preserve the order of insertion - # and therefore d2 and d1 keys are already in different order. - for i in range(10000): - d2[i] = 1 - for i in range(10000): - del d2[i] # Check that our dicts are equal, but with different key order. assert d1 == d2