From e6cf29673fa572e14c33289f8e3ac75beae779fa Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Sat, 2 Sep 2023 18:26:14 -0500 Subject: [PATCH 01/16] Add markers to linter tests, move linter imports (#576) Test markers can be used to easily (de-)select tests, and colcon exposes mechanisms to do so. Linters are a category of tests that are commonly called out. Additionally, if we move the imports for some of our single-purpose tests into the test function, we can avoid installing the linter dependencies entirely. This is a common case in platform packaging, where linter errors are not actionable and the dependencies are not typically installed. --- setup.cfg | 3 +++ test/spell_check.words | 1 + test/test_copyright_license.py | 3 +++ test/test_flake8.py | 15 ++++++++------- test/test_spell_check.py | 11 +++++++---- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/setup.cfg b/setup.cfg index 855b91d2..407add80 100644 --- a/setup.cfg +++ b/setup.cfg @@ -77,6 +77,9 @@ filterwarnings = # Suppress resource warnings pending investigation always::ResourceWarning junit_suite_name = colcon-core +markers = + flake8 + linter python_classes = !TestFailure [options.entry_points] diff --git a/test/spell_check.words b/test/spell_check.words index 6539aa60..a1cb3d99 100644 --- a/test/spell_check.words +++ b/test/spell_check.words @@ -53,6 +53,7 @@ junit levelname libexec lineno +linter linux lstrip mkdtemp diff --git a/test/test_copyright_license.py b/test/test_copyright_license.py index 7b4ffb38..b428f4d3 100644 --- a/test/test_copyright_license.py +++ b/test/test_copyright_license.py @@ -4,7 +4,10 @@ from pathlib import Path import sys +import pytest + +@pytest.mark.linter def test_copyright_license(): missing = check_files([ Path(__file__).parents[1], diff --git a/test/test_flake8.py b/test/test_flake8.py index 4fdd553a..c7214ed6 100644 --- a/test/test_flake8.py +++ b/test/test_flake8.py @@ -5,19 +5,20 @@ from pathlib import Path import sys -from flake8 import LOG -from flake8.api.legacy import get_style_guide -from pydocstyle.utils import log +import pytest -# avoid debug / info / warning messages from flake8 internals -LOG.setLevel(logging.ERROR) +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + from flake8.api.legacy import get_style_guide + # avoid debug / info / warning messages from flake8 internals + logging.getLogger('flake8').setLevel(logging.ERROR) -def test_flake8(): # for some reason the pydocstyle logger changes to an effective level of 1 # set higher level to prevent the output to be flooded with debug messages - log.setLevel(logging.WARNING) + logging.getLogger('pydocstyle').setLevel(logging.WARNING) style_guide = get_style_guide( extend_ignore=['D100', 'D104'], diff --git a/test/test_spell_check.py b/test/test_spell_check.py index d6718d14..10e5725d 100644 --- a/test/test_spell_check.py +++ b/test/test_spell_check.py @@ -4,10 +4,6 @@ from pathlib import Path import pytest -from scspell import Report -from scspell import SCSPELL_BUILTIN_DICT -from scspell import spell_check - spell_check_words_path = Path(__file__).parent / 'spell_check.words' @@ -18,7 +14,12 @@ def known_words(): return spell_check_words_path.read_text().splitlines() +@pytest.mark.linter def test_spell_check(known_words): + from scspell import Report + from scspell import SCSPELL_BUILTIN_DICT + from scspell import spell_check + source_filenames = [ Path(__file__).parents[1] / 'bin' / 'colcon', Path(__file__).parents[1] / 'setup.py'] + \ @@ -46,11 +47,13 @@ def test_spell_check(known_words): ', '.join(sorted(unused_known_words)) +@pytest.mark.linter def test_spell_check_word_list_order(known_words): assert known_words == sorted(known_words), \ 'The word list should be ordered alphabetically' +@pytest.mark.linter def test_spell_check_word_list_duplicates(known_words): assert len(known_words) == len(set(known_words)), \ 'The word list should not contain duplicates' From 3cab8051c7a7099ff6efae30e91195ff82469edf Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Fri, 8 Sep 2023 10:27:52 -0500 Subject: [PATCH 02/16] Allow additional Popen arguments through task.run (#577) This is a follow-up to b6c977a55b17d22c35ba94603c89f1c5e1ebe20c, which made the same change to subprocess.run, which this function calls. --- colcon_core/task/__init__.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/colcon_core/task/__init__.py b/colcon_core/task/__init__.py index e5c2b709..d2bed7f8 100644 --- a/colcon_core/task/__init__.py +++ b/colcon_core/task/__init__.py @@ -148,8 +148,7 @@ async def check_call( async def run( - context, cmd, *, cwd=None, env=None, shell=False, use_pty=None, - capture_output=None + context, cmd, *, use_pty=None, capture_output=None, **other_popen_kwargs ): """ Run the command described by cmd. @@ -159,10 +158,11 @@ async def run( All output to `stdout` and `stderr` is posted as `StdoutLine` and `StderrLine` events to the event queue. + See the documentation of `subprocess.Popen() + ` for + other parameters. + :param cmd: The command and its arguments - :param cwd: the working directory for the subprocess - :param env: a dictionary with environment variables - :param shell: whether to use the shell as the program to execute :param use_pty: whether to use a pseudo terminal :param capture_output: whether to store stdout and stderr :returns: the result of the completed process @@ -174,12 +174,16 @@ def stdout_callback(line): def stderr_callback(line): context.put_event_into_queue(StderrLine(line)) + cwd = other_popen_kwargs.get('cwd', None) + env = other_popen_kwargs.get('env', None) + shell = other_popen_kwargs.get('shell', False) + context.put_event_into_queue( Command(cmd, cwd=cwd, env=env, shell=shell)) completed = await colcon_core_subprocess_run( cmd, stdout_callback, stderr_callback, - cwd=cwd, env=env, shell=shell, use_pty=use_pty, - capture_output=capture_output) + use_pty=use_pty, capture_output=capture_output, + **other_popen_kwargs) context.put_event_into_queue( CommandEnded( cmd, cwd=cwd, env=env, shell=shell, From 42c1e2b8ba7a6dd57087e55ce9f6f854cb74e87c Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Tue, 12 Sep 2023 09:49:41 -0500 Subject: [PATCH 03/16] Address flake8 E721 violations (#580) These violations don't show up in our CI right now because we've pinned flake8<6, but when that pin is dropped, we'll have enforcement for this rule. --- colcon_core/package_augmentation/__init__.py | 2 +- colcon_core/package_descriptor.py | 2 +- colcon_core/verb/__init__.py | 2 +- test/test_argument_default.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/colcon_core/package_augmentation/__init__.py b/colcon_core/package_augmentation/__init__.py index 71acf7c1..3b579292 100644 --- a/colcon_core/package_augmentation/__init__.py +++ b/colcon_core/package_augmentation/__init__.py @@ -213,7 +213,7 @@ def update_metadata(desc, key, value): old_value |= value return - if type(old_value) != type(value): + if type(old_value) is not type(value): logger.warning( f"update package '{desc.name}' metadata '{key}' from value " f"'{old_value}' to '{value}'") diff --git a/colcon_core/package_descriptor.py b/colcon_core/package_descriptor.py index 19ad113f..0e61006d 100644 --- a/colcon_core/package_descriptor.py +++ b/colcon_core/package_descriptor.py @@ -149,7 +149,7 @@ def __hash__(self): # noqa: D105 return hash((self.type, self.name)) def __eq__(self, other): # noqa: D105 - if type(self) != type(other): + if type(self) is not type(other): return NotImplemented if (self.type, self.name) != (other.type, other.name): return False diff --git a/colcon_core/verb/__init__.py b/colcon_core/verb/__init__.py index 9b6e986e..640dc218 100644 --- a/colcon_core/verb/__init__.py +++ b/colcon_core/verb/__init__.py @@ -174,7 +174,7 @@ def update_object( return severity = 5 \ - if old_value is None or type(old_value) == type(value) \ + if old_value is None or type(old_value) is type(value) \ else logging.WARNING logger.log( severity, f"overwrite package '{package_name}' {argument_type} " diff --git a/test/test_argument_default.py b/test/test_argument_default.py index 8775c6d7..f7b2adeb 100644 --- a/test/test_argument_default.py +++ b/test/test_argument_default.py @@ -19,7 +19,7 @@ def test_argument_default(): unwrap_default_value(value) default_value = wrap_default_value(value) assert is_default_value(default_value) - assert type(default_value) != type(value) + assert type(default_value) is not type(value) with pytest.raises(ValueError): wrap_default_value(default_value) unwrapped_value = unwrap_default_value(default_value) @@ -27,5 +27,5 @@ def test_argument_default(): value = 42 unchanged_value = wrap_default_value(value) - assert type(unchanged_value) == type(value) + assert type(unchanged_value) is type(value) assert unchanged_value == value From 3241f1ca90261d84887740f7b745202a0e2defa3 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Wed, 13 Sep 2023 10:36:19 -0500 Subject: [PATCH 04/16] Drop ResourceWarning suppression (#581) This suppression is no longer necessary after the fix introduced as part of 74a6705c476d7fd2908186e783b7a11182f6db4f. --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 407add80..0281aeeb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -74,8 +74,6 @@ filterwarnings = ignore:The loop argument is deprecated::asyncio ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated::pydocstyle ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated::pyreadline - # Suppress resource warnings pending investigation - always::ResourceWarning junit_suite_name = colcon-core markers = flake8 From 5c8c498f84f3c3f4f72570b6a716b86c6e0868b7 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Wed, 13 Sep 2023 14:28:26 -0500 Subject: [PATCH 05/16] Don't pass COV_CORE_SOURCE to python build subprocess (#579) It appears that when this varible is set, pytest-cov will import modules during the interpreter initialization before the sitecustomize hook runs. Among the modules imported is sysconfig, which caches its global state before we're able to modify it, thus rendering the entire hook unusable. The documentation for the 'site' module has more details here, but I wasn't able to find a more aggressive way to prioritize our hook over others. For now, we should suppress COV_CORE_SOURCE to unblock new tests in colcon itself. --- colcon_core/task/python/build.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/colcon_core/task/python/build.py b/colcon_core/task/python/build.py index c177b809..b1b12b07 100644 --- a/colcon_core/task/python/build.py +++ b/colcon_core/task/python/build.py @@ -76,6 +76,10 @@ async def build(self, *, additional_hooks=None): # noqa: D102 env = dict(env) env['PYTHONPATH'] = str(prefix_override) + os.pathsep + \ python_lib + os.pathsep + env.get('PYTHONPATH', '') + # coverage capture interferes with sitecustomize + # See also: https://docs.python.org/3/library/site.html#module-site + # See also: colcon/colcon-core#579 + env.pop('COV_CORE_SOURCE', None) # determine if setuptools specific commands are available available_commands = await self._get_available_commands( From 63f95b4553354a2d0f861b8385a7f6b4af2c66cb Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Wed, 13 Sep 2023 14:31:21 -0500 Subject: [PATCH 06/16] Remove setup.py link after successful _undo_develop (#578) This function reverts a previous setuptools 'develop' operation, but doesn't actually clean up the artifacts under the build tree that it used to determine that 'develop' was previously used. The result is that after performing a build with --symlink-install, all subsequent builds of that package without --symlink-install will run the commands to "undo" the develop operation even if there is nothing to undo. This should provide a small performance improvement under those specific circumstances. --- colcon_core/task/python/build.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/colcon_core/task/python/build.py b/colcon_core/task/python/build.py index b1b12b07..65d0e907 100644 --- a/colcon_core/task/python/build.py +++ b/colcon_core/task/python/build.py @@ -202,6 +202,8 @@ async def _undo_develop(self, pkg, args, env): ] completed = await run( self.context, cmd, cwd=args.build_base, env=env) + if not completed.returncode: + os.remove(setup_py_build_space) return completed.returncode def _undo_install(self, pkg, args, setup_py_data, python_lib): From da1000f1fee9db5ac394dcf6f99d4468b1983ac6 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Thu, 14 Sep 2023 22:12:58 -0500 Subject: [PATCH 07/16] Add missing debian dependency on python3-packaging (#583) Fixes: 7b70e61175ab7 --- stdeb.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdeb.cfg b/stdeb.cfg index d9304b40..d6fda2f2 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,6 +1,6 @@ [colcon-core] No-Python2: -Depends3: python3-distlib, python3-empy, python3-pytest, python3-setuptools, python3 (>= 3.8) | python3-importlib-metadata +Depends3: python3-distlib, python3-empy, python3-packaging, python3-pytest, python3-setuptools, python3 (>= 3.8) | python3-importlib-metadata Recommends3: python3-pytest-cov Suggests3: python3-pytest-repeat, python3-pytest-rerunfailures Suite: bionic focal jammy stretch buster bullseye From c1fe1e9c170b78194e77a63016e858a8e925c4e1 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 18 Sep 2023 20:56:17 -0500 Subject: [PATCH 08/16] Add more tests for task.python.build (#582) A high-level view of this change: * Take test_build_python and move the majority of the content to _test_build_python * Make it tolerant of multiple invocations * Write a new test_build_python that just calls the new function * Write a _symlink variant * Write a _symlink_first variant which invokes the function twice * Write a _symlink_second variant which inverts the order --- test/test_build_python.py | 131 +++++++++++++++++++++++++------------- 1 file changed, 86 insertions(+), 45 deletions(-) diff --git a/test/test_build_python.py b/test/test_build_python.py index e86c5971..27060ef1 100644 --- a/test/test_build_python.py +++ b/test/test_build_python.py @@ -45,60 +45,101 @@ def monkey_patch_put_event_into_queue(monkeypatch): ) -def test_build_package(): +def _test_build_package(tmp_path_str, *, symlink_install): event_loop = new_event_loop() asyncio.set_event_loop(event_loop) try: - with TemporaryDirectory(prefix='test_colcon_') as tmp_path_str: - tmp_path = Path(tmp_path_str) - python_build_task = PythonBuildTask() - package = PackageDescriptor(tmp_path / 'src') - package.name = 'test_package' - package.type = 'python' + tmp_path = Path(tmp_path_str) + python_build_task = PythonBuildTask() + package = PackageDescriptor(tmp_path / 'src') + package.name = 'test_package' + package.type = 'python' + package.metadata['get_python_setup_options'] = lambda _: { + 'packages': ['my_module'], + } - context = TaskContext( - pkg=package, - args=SimpleNamespace( - path=str(tmp_path / 'src'), - build_base=str(tmp_path / 'build'), - install_base=str(tmp_path / 'install'), - symlink_install=False, - ), - dependencies={} - ) - python_build_task.set_context(context=context) + context = TaskContext( + pkg=package, + args=SimpleNamespace( + path=str(tmp_path / 'src'), + build_base=str(tmp_path / 'build'), + install_base=str(tmp_path / 'install'), + symlink_install=symlink_install, + ), + dependencies={} + ) + python_build_task.set_context(context=context) + + pkg = python_build_task.context.pkg + + pkg.path.mkdir(exist_ok=True) + (pkg.path / 'setup.py').write_text( + 'from setuptools import setup\n' + 'setup(\n' + ' name="test_package",\n' + ' packages=["my_module"],\n' + ')\n' + ) + (pkg.path / 'my_module').mkdir(exist_ok=True) + (pkg.path / 'my_module' / '__init__.py').touch() + + src_base = Path(python_build_task.context.args.path) + + source_files_before = set(src_base.rglob('*')) + rc = event_loop.run_until_complete(python_build_task.build()) + assert not rc + source_files_after = set(src_base.rglob('*')) + assert source_files_before == source_files_after + + build_base = Path(python_build_task.context.args.build_base) + assert build_base.rglob('my_module/__init__.py') + + return Path(python_build_task.context.args.install_base) + finally: + event_loop.close() - pkg = python_build_task.context.pkg - pkg.path.mkdir() - (pkg.path / 'setup.py').write_text( - 'from setuptools import setup\n' - 'setup(\n' - ' name="test_package",\n' - ' packages=["my_module"],\n' - ')\n' - ) - (pkg.path / 'my_module').mkdir() - (pkg.path / 'my_module' / '__init__.py').touch() +def test_build_package(): + with TemporaryDirectory(prefix='test_colcon_') as tmp_path_str: + install_base = _test_build_package(tmp_path_str, symlink_install=False) - src_base = Path(python_build_task.context.args.path) + assert 1 == len(list(install_base.rglob('my_module/__init__.py'))) - source_files_before = set(src_base.rglob('*')) - rc = event_loop.run_until_complete(python_build_task.build()) - assert not rc - source_files_after = set(src_base.rglob('*')) - assert source_files_before == source_files_after + pkg_info, = install_base.rglob('PKG-INFO') + assert 'Name: test-package' in pkg_info.read_text().splitlines() - build_base = Path(python_build_task.context.args.build_base) - assert 1 == len(list(build_base.rglob('my_module/__init__.py'))) - install_base = Path(python_build_task.context.args.install_base) - assert 1 == len(list(install_base.rglob('my_module/__init__.py'))) +def test_build_package_symlink(): + with TemporaryDirectory(prefix='test_colcon_') as tmp_path_str: + install_base = _test_build_package(tmp_path_str, symlink_install=True) - pkg_info, = install_base.rglob('PKG-INFO') - assert 'Name: test-package' in pkg_info.read_text().splitlines() - finally: - event_loop.close() + assert 1 == len(list(install_base.rglob('test-package.egg-link'))) + + +def test_build_package_symlink_first(): + with TemporaryDirectory(prefix='test_colcon_') as tmp_path_str: + install_base = _test_build_package(tmp_path_str, symlink_install=True) + + assert 1 == len(list(install_base.rglob('test-package.egg-link'))) + assert 0 == len(list(install_base.rglob('PKG-INFO'))) + + install_base = _test_build_package(tmp_path_str, symlink_install=False) + + assert 0 == len(list(install_base.rglob('test-package.egg-link'))) + assert 1 == len(list(install_base.rglob('PKG-INFO'))) + + +def test_build_package_symlink_second(): + with TemporaryDirectory(prefix='test_colcon_') as tmp_path_str: + install_base = _test_build_package(tmp_path_str, symlink_install=False) + + assert 0 == len(list(install_base.rglob('test-package.egg-link'))) + assert 1 == len(list(install_base.rglob('PKG-INFO'))) + + install_base = _test_build_package(tmp_path_str, symlink_install=True) + + assert 1 == len(list(install_base.rglob('test-package.egg-link'))) + assert 0 == len(list(install_base.rglob('PKG-INFO'))) def test_build_package_libexec_pattern(): @@ -126,7 +167,7 @@ def test_build_package_libexec_pattern(): pkg = python_build_task.context.pkg - pkg.path.mkdir() + pkg.path.mkdir(exist_ok=True) (pkg.path / 'setup.py').write_text( 'from setuptools import setup\n' 'setup()\n' @@ -144,7 +185,7 @@ def test_build_package_libexec_pattern(): '[install]\n' 'install-scripts=$base/lib/test_package\n' ) - (pkg.path / 'my_module').mkdir() + (pkg.path / 'my_module').mkdir(exist_ok=True) (pkg.path / 'my_module' / '__init__.py').write_text( 'def main():\n' ' print("Hello, World!")\n' From b544529b499a23d08dacd05b0caae22b769a1ee3 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Fri, 22 Sep 2023 10:08:25 -0500 Subject: [PATCH 09/16] 0.13.0 --- colcon_core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colcon_core/__init__.py b/colcon_core/__init__.py index 1941e36f..944950db 100644 --- a/colcon_core/__init__.py +++ b/colcon_core/__init__.py @@ -1,4 +1,4 @@ # Copyright 2016-2020 Dirk Thomas # Licensed under the Apache License, Version 2.0 -__version__ = '0.12.1' +__version__ = '0.13.0' From a9de9091ae1d08fda52eb20d0027259fa3dd2ebe Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 25 Sep 2023 15:23:57 -0500 Subject: [PATCH 10/16] Fix compatibility with Python < 3.10 importlib.metadata (#585) It appears that the Distribution.name property was introduced in Python 3.10. Fixes: 5f9f5fff95b8c --- colcon_core/extension_point.py | 7 ++++--- test/test_extension_point.py | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/colcon_core/extension_point.py b/colcon_core/extension_point.py index ce8eb383..ff348ba5 100644 --- a/colcon_core/extension_point.py +++ b/colcon_core/extension_point.py @@ -53,9 +53,10 @@ def get_all_extension_points(): entry_points = defaultdict(dict) seen = set() for dist in distributions(): - if dist.name in seen: + dist_name = dist.metadata['Name'] + if dist_name in seen: continue - seen.add(dist.name) + seen.add(dist_name) for entry_point in dist.entry_points: # skip groups which are not registered as extension points if entry_point.group not in colcon_extension_points: @@ -69,7 +70,7 @@ def get_all_extension_points(): f"from '{dist._path}' " f"overwriting '{previous}'") entry_points[entry_point.group][entry_point.name] = \ - (entry_point.value, dist.name, dist.version) + (entry_point.value, dist_name, dist.version) return entry_points diff --git a/test/test_extension_point.py b/test/test_extension_point.py index 1cfce2a1..7111b796 100644 --- a/test/test_extension_point.py +++ b/test/test_extension_point.py @@ -28,13 +28,17 @@ class Dist(): version = '0.0.0' def __init__(self, entry_points): - self.name = f'dist-{id(self)}' + self.metadata = {'Name': f'dist-{id(self)}'} self._entry_points = entry_points @property def entry_points(self): return list(self._entry_points) + @property + def name(self): + return self.metadata['Name'] + def iter_entry_points(*, group=None): if group == EXTENSION_POINT_GROUP_NAME: From 2ce42f1379bd26d9fa39aad11b2eba420cdd08cb Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 25 Sep 2023 15:24:21 -0500 Subject: [PATCH 11/16] 0.13.1 --- colcon_core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colcon_core/__init__.py b/colcon_core/__init__.py index 944950db..9e74a90a 100644 --- a/colcon_core/__init__.py +++ b/colcon_core/__init__.py @@ -1,4 +1,4 @@ # Copyright 2016-2020 Dirk Thomas # Licensed under the Apache License, Version 2.0 -__version__ = '0.13.0' +__version__ = '0.13.1' From 731092b7c9595cb4f7070c328b7584846dbf9659 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 2 Oct 2023 13:42:28 -0500 Subject: [PATCH 12/16] Revert from using importlib.metadata back to pkg_resources (#587) It appears that we accidentally shipped this incompatible change on Ubuntu Bionic, which is currently under ESM. While we do intend to drop mainline support for Bionic, we should leave it in a good state. This reverts commit a9de9091ae1d08fda52eb20d0027259fa3dd2ebe. This reverts commit 5f9f5fff95b8c6021933d7d5419879a9230b9462. This reverts commit 7b70e61175ab735ea5448ab6e88b4882975566b5. --- colcon_core/extension_point.py | 78 ++++++++++--------- colcon_core/package_identification/python.py | 14 +--- colcon_core/plugin_system.py | 8 +- colcon_core/task/python/test/pytest.py | 6 +- debian/patches/setup.cfg.patch | 6 +- setup.cfg | 4 +- stdeb.cfg | 2 +- test/spell_check.words | 2 +- test/test_extension_point.py | 80 ++++++++++---------- 9 files changed, 93 insertions(+), 107 deletions(-) diff --git a/colcon_core/extension_point.py b/colcon_core/extension_point.py index ff348ba5..54191e7a 100644 --- a/colcon_core/extension_point.py +++ b/colcon_core/extension_point.py @@ -6,18 +6,11 @@ import os import traceback -try: - from importlib.metadata import distributions - from importlib.metadata import EntryPoint - from importlib.metadata import entry_points -except ImportError: - # TODO: Drop this with Python 3.7 support - from importlib_metadata import distributions - from importlib_metadata import EntryPoint - from importlib_metadata import entry_points - from colcon_core.environment_variable import EnvironmentVariable from colcon_core.logging import colcon_logger +from pkg_resources import EntryPoint +from pkg_resources import iter_entry_points +from pkg_resources import WorkingSet """Environment variable to block extensions""" EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE = EnvironmentVariable( @@ -51,26 +44,27 @@ def get_all_extension_points(): colcon_extension_points.setdefault(EXTENSION_POINT_GROUP_NAME, None) entry_points = defaultdict(dict) - seen = set() - for dist in distributions(): - dist_name = dist.metadata['Name'] - if dist_name in seen: - continue - seen.add(dist_name) - for entry_point in dist.entry_points: + working_set = WorkingSet() + for dist in sorted(working_set): + entry_map = dist.get_entry_map() + for group_name in entry_map.keys(): # skip groups which are not registered as extension points - if entry_point.group not in colcon_extension_points: + if group_name not in colcon_extension_points: continue - if entry_point.name in entry_points[entry_point.group]: - previous = entry_points[entry_point.group][entry_point.name] - logger.error( - f"Entry point '{entry_point.group}.{entry_point.name}' is " - f"declared multiple times, '{entry_point.value}' " - f"from '{dist._path}' " - f"overwriting '{previous}'") - entry_points[entry_point.group][entry_point.name] = \ - (entry_point.value, dist_name, dist.version) + group = entry_map[group_name] + for entry_point_name, entry_point in group.items(): + if entry_point_name in entry_points[group_name]: + previous = entry_points[group_name][entry_point_name] + logger.error( + f"Entry point '{group_name}.{entry_point_name}' is " + f"declared multiple times, '{entry_point}' " + f"overwriting '{previous}'") + value = entry_point.module_name + if entry_point.attrs: + value += f":{'.'.join(entry_point.attrs)}" + entry_points[group_name][entry_point_name] = ( + value, dist.project_name, getattr(dist, 'version', None)) return entry_points @@ -82,21 +76,19 @@ def get_extension_points(group): :returns: mapping of extension point names to extension point values :rtype: dict """ - extension_points = {} - try: - # Python 3.10 and newer - query = entry_points(group=group) - except TypeError: - query = entry_points().get(group, ()) - for entry_point in query: - if entry_point.name in extension_points: - previous_entry_point = extension_points[entry_point.name] + entry_points = {} + for entry_point in iter_entry_points(group=group): + if entry_point.name in entry_points: + previous_entry_point = entry_points[entry_point.name] logger.error( f"Entry point '{group}.{entry_point.name}' is declared " - f"multiple times, '{entry_point.value}' overwriting " + f"multiple times, '{entry_point}' overwriting " f"'{previous_entry_point}'") - extension_points[entry_point.name] = entry_point.value - return extension_points + value = entry_point.module_name + if entry_point.attrs: + value += f":{'.'.join(entry_point.attrs)}" + entry_points[entry_point.name] = value + return entry_points def load_extension_points(group, *, excludes=None): @@ -154,4 +146,10 @@ def load_extension_point(name, value, group): raise RuntimeError( 'The entry point name is listed in the environment variable ' f"'{EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE.name}'") - return EntryPoint(name, value, group).load() + if ':' in value: + module_name, attr = value.split(':', 1) + attrs = attr.split('.') + else: + module_name = value + attrs = () + return EntryPoint(name, module_name, attrs).resolve() diff --git a/colcon_core/package_identification/python.py b/colcon_core/package_identification/python.py index 0b11c8aa..9d5e467b 100644 --- a/colcon_core/package_identification/python.py +++ b/colcon_core/package_identification/python.py @@ -90,17 +90,11 @@ def get_configuration(setup_cfg): except ImportError: from setuptools.config import read_configuration except ImportError as e: - try: - from importlib.metadata import distribution - except ImportError: - from importlib_metadata import distribution - from packaging.version import Version - try: - setuptools_version = distribution('setuptools').version - except ModuleNotFoundError: - setuptools_version = '0' + from pkg_resources import get_distribution + from pkg_resources import parse_version + setuptools_version = get_distribution('setuptools').version minimum_version = '30.3.0' - if Version(setuptools_version) < Version(minimum_version): + if parse_version(setuptools_version) < parse_version(minimum_version): e.msg += ', ' \ "'setuptools' needs to be at least version " \ f'{minimum_version}, if a newer version is not available ' \ diff --git a/colcon_core/plugin_system.py b/colcon_core/plugin_system.py index 7d3934a3..4794e2a9 100644 --- a/colcon_core/plugin_system.py +++ b/colcon_core/plugin_system.py @@ -6,7 +6,7 @@ from colcon_core.extension_point import load_extension_points from colcon_core.logging import colcon_logger -from packaging.version import Version +from pkg_resources import parse_version logger = colcon_logger.getChild(__name__) @@ -166,8 +166,8 @@ def satisfies_version(version, caret_range): :raises RuntimeError: if the version doesn't match the caret range """ assert caret_range.startswith('^'), 'Only supports caret ranges' - extension_point_version = Version(version) - extension_version = Version(caret_range[1:]) + extension_point_version = parse_version(version) + extension_version = parse_version(caret_range[1:]) next_extension_version = _get_upper_bound_caret_version( extension_version) @@ -192,4 +192,4 @@ def _get_upper_bound_caret_version(version): minor = 0 else: minor += 1 - return Version('%d.%d.0' % (major, minor)) + return parse_version('%d.%d.0' % (major, minor)) diff --git a/colcon_core/task/python/test/pytest.py b/colcon_core/task/python/test/pytest.py index 335e2140..6b51d267 100644 --- a/colcon_core/task/python/test/pytest.py +++ b/colcon_core/task/python/test/pytest.py @@ -13,7 +13,7 @@ from colcon_core.task.python.test import has_test_dependency from colcon_core.task.python.test import PythonTestingStepExtensionPoint from colcon_core.verb.test import logger -from packaging.version import Version +from pkg_resources import parse_version class PytestPythonTestingStep(PythonTestingStepExtensionPoint): @@ -64,7 +64,7 @@ async def step(self, context, env, setup_py_data): # noqa: D102 # use -o option only when available # https://github.com/pytest-dev/pytest/blob/3.3.0/CHANGELOG.rst from pytest import __version__ as pytest_version - if Version(pytest_version) >= Version('3.3.0'): + if parse_version(pytest_version) >= parse_version('3.3.0'): args += [ '-o', 'cache_dir=' + str(PurePosixPath( *(Path(context.args.build_base).parts)) / '.pytest_cache'), @@ -95,7 +95,7 @@ async def step(self, context, env, setup_py_data): # noqa: D102 ] # use --cov-branch option only when available # https://github.com/pytest-dev/pytest-cov/blob/v2.5.0/CHANGELOG.rst - if Version(pytest_cov_version) >= Version('2.5.0'): + if parse_version(pytest_cov_version) >= parse_version('2.5.0'): args += [ '--cov-branch', ] diff --git a/debian/patches/setup.cfg.patch b/debian/patches/setup.cfg.patch index add781c2..14d9486e 100644 --- a/debian/patches/setup.cfg.patch +++ b/debian/patches/setup.cfg.patch @@ -5,9 +5,9 @@ Author: Dirk Thomas --- setup.cfg 2018-05-27 11:22:33.000000000 -0700 +++ setup.cfg.patched 2018-05-27 11:22:33.000000000 -0700 -@@ -33,9 +33,12 @@ - importlib-metadata; python_version < "3.8" - packaging +@@ -31,9 +31,12 @@ + distlib + EmPy pytest - pytest-cov - pytest-repeat diff --git a/setup.cfg b/setup.cfg index 0281aeeb..7da68786 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,8 +30,6 @@ install_requires = coloredlogs; sys_platform == 'win32' distlib EmPy - importlib-metadata; python_version < "3.8" - packaging # the pytest dependency and its extensions are provided for convenience # even though they are only conditional pytest @@ -69,7 +67,7 @@ filterwarnings = error # Suppress deprecation warnings in other packages ignore:lib2to3 package is deprecated::scspell - ignore:pkg_resources is deprecated as an API::colcon_core.entry_point + ignore:pkg_resources is deprecated as an API ignore:SelectableGroups dict interface is deprecated::flake8 ignore:The loop argument is deprecated::asyncio ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated::pydocstyle diff --git a/stdeb.cfg b/stdeb.cfg index d6fda2f2..e274f195 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,6 +1,6 @@ [colcon-core] No-Python2: -Depends3: python3-distlib, python3-empy, python3-packaging, python3-pytest, python3-setuptools, python3 (>= 3.8) | python3-importlib-metadata +Depends3: python3-distlib, python3-empy, python3-packaging, python3-pytest, python3-setuptools Recommends3: python3-pytest-cov Suggests3: python3-pytest-repeat, python3-pytest-rerunfailures Suite: bionic focal jammy stretch buster bullseye diff --git a/test/spell_check.words b/test/spell_check.words index a1cb3d99..c3414f9e 100644 --- a/test/spell_check.words +++ b/test/spell_check.words @@ -2,6 +2,7 @@ addopts apache argparse asyncio +attrs autouse basepath bazqux @@ -46,7 +47,6 @@ hardcodes hookimpl hookwrapper https -importlib isatty iterdir junit diff --git a/test/test_extension_point.py b/test/test_extension_point.py index 7111b796..73b6a2d4 100644 --- a/test/test_extension_point.py +++ b/test/test_extension_point.py @@ -17,100 +17,96 @@ from .environment_context import EnvironmentContext -Group1 = EntryPoint('group1', 'g1', EXTENSION_POINT_GROUP_NAME) -Group2 = EntryPoint('group2', 'g2', EXTENSION_POINT_GROUP_NAME) -ExtA = EntryPoint('extA', 'eA', Group1.name) -ExtB = EntryPoint('extB', 'eB', Group1.name) +Group1 = EntryPoint('group1', 'g1') +Group2 = EntryPoint('group2', 'g2') class Dist(): - version = '0.0.0' + project_name = 'dist' - def __init__(self, entry_points): - self.metadata = {'Name': f'dist-{id(self)}'} - self._entry_points = entry_points + def __init__(self, group_name, group): + self._group_name = group_name + self._group = group - @property - def entry_points(self): - return list(self._entry_points) + def __lt__(self, other): + return self._group_name < other._group_name - @property - def name(self): - return self.metadata['Name'] + def get_entry_map(self): + return self._group -def iter_entry_points(*, group=None): +def iter_entry_points(*, group): if group == EXTENSION_POINT_GROUP_NAME: return [Group1, Group2] - elif group == Group1.name: - return [ExtA, ExtB] - assert not group - return { - EXTENSION_POINT_GROUP_NAME: [Group1, Group2], - Group1.name: [ExtA, ExtB], - } + assert group == Group1.name + ep1 = EntryPoint('extA', 'eA') + ep2 = EntryPoint('extB', 'eB') + return [ep1, ep2] -def distributions(): +def working_set(): return [ - Dist(iter_entry_points(group='group1')), - Dist([EntryPoint('extC', 'eC', Group2.name)]), - Dist([EntryPoint('extD', 'eD', 'groupX')]), + Dist('group1', { + 'group1': {ep.name: ep for ep in iter_entry_points(group='group1')} + }), + Dist('group2', {'group2': {'extC': EntryPoint('extC', 'eC')}}), + Dist('groupX', {'groupX': {'extD': EntryPoint('extD', 'eD')}}), ] def test_all_extension_points(): with patch( - 'colcon_core.extension_point.entry_points', + 'colcon_core.extension_point.iter_entry_points', side_effect=iter_entry_points ): with patch( - 'colcon_core.extension_point.distributions', - side_effect=distributions + 'colcon_core.extension_point.WorkingSet', + side_effect=working_set ): # successfully load a known entry point extension_points = get_all_extension_points() assert set(extension_points.keys()) == {'group1', 'group2'} assert set(extension_points['group1'].keys()) == {'extA', 'extB'} - assert extension_points['group1']['extA'][0] == 'eA' + assert extension_points['group1']['extA'] == ( + 'eA', Dist.project_name, None) def test_extension_point_blocklist(): # successful loading of extension point without a blocklist with patch( - 'colcon_core.extension_point.entry_points', + 'colcon_core.extension_point.iter_entry_points', side_effect=iter_entry_points ): with patch( - 'colcon_core.extension_point.distributions', - side_effect=distributions + 'colcon_core.extension_point.WorkingSet', + side_effect=working_set ): extension_points = get_extension_points('group1') assert 'extA' in extension_points.keys() extension_point = extension_points['extA'] assert extension_point == 'eA' - with patch.object(EntryPoint, 'load', return_value=None) as load: + with patch.object(EntryPoint, 'resolve', return_value=None) as resolve: load_extension_point('extA', 'eA', 'group1') - assert load.call_count == 1 + assert resolve.call_count == 1 # successful loading of entry point not in blocklist - load.reset_mock() + resolve.reset_mock() with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST=os.pathsep.join([ 'group1.extB', 'group2.extC']) ): load_extension_point('extA', 'eA', 'group1') - assert load.call_count == 1 + assert resolve.call_count == 1 # entry point in a blocked group can't be loaded - load.reset_mock() + resolve.reset_mock() with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST='group1'): with pytest.raises(RuntimeError) as e: load_extension_point('extA', 'eA', 'group1') assert 'The entry point group name is listed in the environment ' \ 'variable' in str(e.value) - assert load.call_count == 0 + assert resolve.call_count == 0 # entry point listed in the blocklist can't be loaded with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST=os.pathsep.join([ @@ -120,10 +116,10 @@ def test_extension_point_blocklist(): load_extension_point('extA', 'eA', 'group1') assert 'The entry point name is listed in the environment ' \ 'variable' in str(e.value) - assert load.call_count == 0 + assert resolve.call_count == 0 -def entry_point_load(self, *args, **kwargs): +def entry_point_resolve(self, *args, **kwargs): if self.name == 'exception': raise Exception('entry point raising exception') if self.name == 'runtime_error': @@ -133,7 +129,7 @@ def entry_point_load(self, *args, **kwargs): return DEFAULT -@patch.object(EntryPoint, 'load', entry_point_load) +@patch.object(EntryPoint, 'resolve', entry_point_resolve) @patch( 'colcon_core.extension_point.get_extension_points', return_value={'exception': 'a', 'runtime_error': 'b', 'success': 'c'} From 1960796bc19a846af68af1bbe8e6e41eb517d1c1 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 2 Oct 2023 13:42:47 -0500 Subject: [PATCH 13/16] 0.14.0 --- colcon_core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colcon_core/__init__.py b/colcon_core/__init__.py index 9e74a90a..770302ce 100644 --- a/colcon_core/__init__.py +++ b/colcon_core/__init__.py @@ -1,4 +1,4 @@ # Copyright 2016-2020 Dirk Thomas # Licensed under the Apache License, Version 2.0 -__version__ = '0.13.1' +__version__ = '0.14.0' From f2c3296e5f56f9330eeb1e3998b8c6af62d417f2 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 2 Oct 2023 14:25:39 -0500 Subject: [PATCH 14/16] Update target platforms for debs (#588) * Bionic is in ESM and does not package importlib-metadata * Buster does not package importlib-metadata * Stretch has Python 3.5 and hasn't been support for a long time now * Bookworm is the current "stable" Debian --- publish-python.yaml | 4 +--- stdeb.cfg | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/publish-python.yaml b/publish-python.yaml index 4b255406..3a0a1f1a 100644 --- a/publish-python.yaml +++ b/publish-python.yaml @@ -8,9 +8,7 @@ artifacts: config: repository: dirk-thomas/colcon distributions: - - ubuntu:bionic - ubuntu:focal - ubuntu:jammy - - debian:stretch - - debian:buster - debian:bullseye + - debian:bookworm diff --git a/stdeb.cfg b/stdeb.cfg index e274f195..54f6e44a 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -3,5 +3,5 @@ No-Python2: Depends3: python3-distlib, python3-empy, python3-packaging, python3-pytest, python3-setuptools Recommends3: python3-pytest-cov Suggests3: python3-pytest-repeat, python3-pytest-rerunfailures -Suite: bionic focal jammy stretch buster bullseye +Suite: focal jammy bullseye bookworm X-Python3-Version: >= 3.6 From 20a5e9f1acac2b058424623534ab1812dfb395d6 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Tue, 3 Oct 2023 16:22:44 -0500 Subject: [PATCH 15/16] Restore use of importlib.metadata instead of pkg_resources (#589) This reverts commit 731092b7c9595cb4f7070c328b7584846dbf9659. This restores commit a9de9091ae1d08fda52eb20d0027259fa3dd2ebe. This restores commit 5f9f5fff95b8c6021933d7d5419879a9230b9462. This restores commit 7b70e61175ab735ea5448ab6e88b4882975566b5. --- colcon_core/extension_point.py | 78 +++++++++---------- colcon_core/package_identification/python.py | 14 +++- colcon_core/plugin_system.py | 8 +- colcon_core/task/python/test/pytest.py | 6 +- debian/patches/setup.cfg.patch | 6 +- setup.cfg | 4 +- stdeb.cfg | 2 +- test/spell_check.words | 2 +- test/test_extension_point.py | 80 ++++++++++---------- 9 files changed, 107 insertions(+), 93 deletions(-) diff --git a/colcon_core/extension_point.py b/colcon_core/extension_point.py index 54191e7a..ff348ba5 100644 --- a/colcon_core/extension_point.py +++ b/colcon_core/extension_point.py @@ -6,11 +6,18 @@ import os import traceback +try: + from importlib.metadata import distributions + from importlib.metadata import EntryPoint + from importlib.metadata import entry_points +except ImportError: + # TODO: Drop this with Python 3.7 support + from importlib_metadata import distributions + from importlib_metadata import EntryPoint + from importlib_metadata import entry_points + from colcon_core.environment_variable import EnvironmentVariable from colcon_core.logging import colcon_logger -from pkg_resources import EntryPoint -from pkg_resources import iter_entry_points -from pkg_resources import WorkingSet """Environment variable to block extensions""" EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE = EnvironmentVariable( @@ -44,27 +51,26 @@ def get_all_extension_points(): colcon_extension_points.setdefault(EXTENSION_POINT_GROUP_NAME, None) entry_points = defaultdict(dict) - working_set = WorkingSet() - for dist in sorted(working_set): - entry_map = dist.get_entry_map() - for group_name in entry_map.keys(): + seen = set() + for dist in distributions(): + dist_name = dist.metadata['Name'] + if dist_name in seen: + continue + seen.add(dist_name) + for entry_point in dist.entry_points: # skip groups which are not registered as extension points - if group_name not in colcon_extension_points: + if entry_point.group not in colcon_extension_points: continue - group = entry_map[group_name] - for entry_point_name, entry_point in group.items(): - if entry_point_name in entry_points[group_name]: - previous = entry_points[group_name][entry_point_name] - logger.error( - f"Entry point '{group_name}.{entry_point_name}' is " - f"declared multiple times, '{entry_point}' " - f"overwriting '{previous}'") - value = entry_point.module_name - if entry_point.attrs: - value += f":{'.'.join(entry_point.attrs)}" - entry_points[group_name][entry_point_name] = ( - value, dist.project_name, getattr(dist, 'version', None)) + if entry_point.name in entry_points[entry_point.group]: + previous = entry_points[entry_point.group][entry_point.name] + logger.error( + f"Entry point '{entry_point.group}.{entry_point.name}' is " + f"declared multiple times, '{entry_point.value}' " + f"from '{dist._path}' " + f"overwriting '{previous}'") + entry_points[entry_point.group][entry_point.name] = \ + (entry_point.value, dist_name, dist.version) return entry_points @@ -76,19 +82,21 @@ def get_extension_points(group): :returns: mapping of extension point names to extension point values :rtype: dict """ - entry_points = {} - for entry_point in iter_entry_points(group=group): - if entry_point.name in entry_points: - previous_entry_point = entry_points[entry_point.name] + extension_points = {} + try: + # Python 3.10 and newer + query = entry_points(group=group) + except TypeError: + query = entry_points().get(group, ()) + for entry_point in query: + if entry_point.name in extension_points: + previous_entry_point = extension_points[entry_point.name] logger.error( f"Entry point '{group}.{entry_point.name}' is declared " - f"multiple times, '{entry_point}' overwriting " + f"multiple times, '{entry_point.value}' overwriting " f"'{previous_entry_point}'") - value = entry_point.module_name - if entry_point.attrs: - value += f":{'.'.join(entry_point.attrs)}" - entry_points[entry_point.name] = value - return entry_points + extension_points[entry_point.name] = entry_point.value + return extension_points def load_extension_points(group, *, excludes=None): @@ -146,10 +154,4 @@ def load_extension_point(name, value, group): raise RuntimeError( 'The entry point name is listed in the environment variable ' f"'{EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE.name}'") - if ':' in value: - module_name, attr = value.split(':', 1) - attrs = attr.split('.') - else: - module_name = value - attrs = () - return EntryPoint(name, module_name, attrs).resolve() + return EntryPoint(name, value, group).load() diff --git a/colcon_core/package_identification/python.py b/colcon_core/package_identification/python.py index 9d5e467b..0b11c8aa 100644 --- a/colcon_core/package_identification/python.py +++ b/colcon_core/package_identification/python.py @@ -90,11 +90,17 @@ def get_configuration(setup_cfg): except ImportError: from setuptools.config import read_configuration except ImportError as e: - from pkg_resources import get_distribution - from pkg_resources import parse_version - setuptools_version = get_distribution('setuptools').version + try: + from importlib.metadata import distribution + except ImportError: + from importlib_metadata import distribution + from packaging.version import Version + try: + setuptools_version = distribution('setuptools').version + except ModuleNotFoundError: + setuptools_version = '0' minimum_version = '30.3.0' - if parse_version(setuptools_version) < parse_version(minimum_version): + if Version(setuptools_version) < Version(minimum_version): e.msg += ', ' \ "'setuptools' needs to be at least version " \ f'{minimum_version}, if a newer version is not available ' \ diff --git a/colcon_core/plugin_system.py b/colcon_core/plugin_system.py index 4794e2a9..7d3934a3 100644 --- a/colcon_core/plugin_system.py +++ b/colcon_core/plugin_system.py @@ -6,7 +6,7 @@ from colcon_core.extension_point import load_extension_points from colcon_core.logging import colcon_logger -from pkg_resources import parse_version +from packaging.version import Version logger = colcon_logger.getChild(__name__) @@ -166,8 +166,8 @@ def satisfies_version(version, caret_range): :raises RuntimeError: if the version doesn't match the caret range """ assert caret_range.startswith('^'), 'Only supports caret ranges' - extension_point_version = parse_version(version) - extension_version = parse_version(caret_range[1:]) + extension_point_version = Version(version) + extension_version = Version(caret_range[1:]) next_extension_version = _get_upper_bound_caret_version( extension_version) @@ -192,4 +192,4 @@ def _get_upper_bound_caret_version(version): minor = 0 else: minor += 1 - return parse_version('%d.%d.0' % (major, minor)) + return Version('%d.%d.0' % (major, minor)) diff --git a/colcon_core/task/python/test/pytest.py b/colcon_core/task/python/test/pytest.py index 6b51d267..335e2140 100644 --- a/colcon_core/task/python/test/pytest.py +++ b/colcon_core/task/python/test/pytest.py @@ -13,7 +13,7 @@ from colcon_core.task.python.test import has_test_dependency from colcon_core.task.python.test import PythonTestingStepExtensionPoint from colcon_core.verb.test import logger -from pkg_resources import parse_version +from packaging.version import Version class PytestPythonTestingStep(PythonTestingStepExtensionPoint): @@ -64,7 +64,7 @@ async def step(self, context, env, setup_py_data): # noqa: D102 # use -o option only when available # https://github.com/pytest-dev/pytest/blob/3.3.0/CHANGELOG.rst from pytest import __version__ as pytest_version - if parse_version(pytest_version) >= parse_version('3.3.0'): + if Version(pytest_version) >= Version('3.3.0'): args += [ '-o', 'cache_dir=' + str(PurePosixPath( *(Path(context.args.build_base).parts)) / '.pytest_cache'), @@ -95,7 +95,7 @@ async def step(self, context, env, setup_py_data): # noqa: D102 ] # use --cov-branch option only when available # https://github.com/pytest-dev/pytest-cov/blob/v2.5.0/CHANGELOG.rst - if parse_version(pytest_cov_version) >= parse_version('2.5.0'): + if Version(pytest_cov_version) >= Version('2.5.0'): args += [ '--cov-branch', ] diff --git a/debian/patches/setup.cfg.patch b/debian/patches/setup.cfg.patch index 14d9486e..add781c2 100644 --- a/debian/patches/setup.cfg.patch +++ b/debian/patches/setup.cfg.patch @@ -5,9 +5,9 @@ Author: Dirk Thomas --- setup.cfg 2018-05-27 11:22:33.000000000 -0700 +++ setup.cfg.patched 2018-05-27 11:22:33.000000000 -0700 -@@ -31,9 +31,12 @@ - distlib - EmPy +@@ -33,9 +33,12 @@ + importlib-metadata; python_version < "3.8" + packaging pytest - pytest-cov - pytest-repeat diff --git a/setup.cfg b/setup.cfg index 7da68786..0281aeeb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,8 @@ install_requires = coloredlogs; sys_platform == 'win32' distlib EmPy + importlib-metadata; python_version < "3.8" + packaging # the pytest dependency and its extensions are provided for convenience # even though they are only conditional pytest @@ -67,7 +69,7 @@ filterwarnings = error # Suppress deprecation warnings in other packages ignore:lib2to3 package is deprecated::scspell - ignore:pkg_resources is deprecated as an API + ignore:pkg_resources is deprecated as an API::colcon_core.entry_point ignore:SelectableGroups dict interface is deprecated::flake8 ignore:The loop argument is deprecated::asyncio ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated::pydocstyle diff --git a/stdeb.cfg b/stdeb.cfg index 54f6e44a..9c4793cd 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,6 +1,6 @@ [colcon-core] No-Python2: -Depends3: python3-distlib, python3-empy, python3-packaging, python3-pytest, python3-setuptools +Depends3: python3-distlib, python3-empy, python3-packaging, python3-pytest, python3-setuptools, python3 (>= 3.8) | python3-importlib-metadata Recommends3: python3-pytest-cov Suggests3: python3-pytest-repeat, python3-pytest-rerunfailures Suite: focal jammy bullseye bookworm diff --git a/test/spell_check.words b/test/spell_check.words index c3414f9e..a1cb3d99 100644 --- a/test/spell_check.words +++ b/test/spell_check.words @@ -2,7 +2,6 @@ addopts apache argparse asyncio -attrs autouse basepath bazqux @@ -47,6 +46,7 @@ hardcodes hookimpl hookwrapper https +importlib isatty iterdir junit diff --git a/test/test_extension_point.py b/test/test_extension_point.py index 73b6a2d4..7111b796 100644 --- a/test/test_extension_point.py +++ b/test/test_extension_point.py @@ -17,96 +17,100 @@ from .environment_context import EnvironmentContext -Group1 = EntryPoint('group1', 'g1') -Group2 = EntryPoint('group2', 'g2') +Group1 = EntryPoint('group1', 'g1', EXTENSION_POINT_GROUP_NAME) +Group2 = EntryPoint('group2', 'g2', EXTENSION_POINT_GROUP_NAME) +ExtA = EntryPoint('extA', 'eA', Group1.name) +ExtB = EntryPoint('extB', 'eB', Group1.name) class Dist(): - project_name = 'dist' + version = '0.0.0' - def __init__(self, group_name, group): - self._group_name = group_name - self._group = group + def __init__(self, entry_points): + self.metadata = {'Name': f'dist-{id(self)}'} + self._entry_points = entry_points - def __lt__(self, other): - return self._group_name < other._group_name + @property + def entry_points(self): + return list(self._entry_points) - def get_entry_map(self): - return self._group + @property + def name(self): + return self.metadata['Name'] -def iter_entry_points(*, group): +def iter_entry_points(*, group=None): if group == EXTENSION_POINT_GROUP_NAME: return [Group1, Group2] - assert group == Group1.name - ep1 = EntryPoint('extA', 'eA') - ep2 = EntryPoint('extB', 'eB') - return [ep1, ep2] + elif group == Group1.name: + return [ExtA, ExtB] + assert not group + return { + EXTENSION_POINT_GROUP_NAME: [Group1, Group2], + Group1.name: [ExtA, ExtB], + } -def working_set(): +def distributions(): return [ - Dist('group1', { - 'group1': {ep.name: ep for ep in iter_entry_points(group='group1')} - }), - Dist('group2', {'group2': {'extC': EntryPoint('extC', 'eC')}}), - Dist('groupX', {'groupX': {'extD': EntryPoint('extD', 'eD')}}), + Dist(iter_entry_points(group='group1')), + Dist([EntryPoint('extC', 'eC', Group2.name)]), + Dist([EntryPoint('extD', 'eD', 'groupX')]), ] def test_all_extension_points(): with patch( - 'colcon_core.extension_point.iter_entry_points', + 'colcon_core.extension_point.entry_points', side_effect=iter_entry_points ): with patch( - 'colcon_core.extension_point.WorkingSet', - side_effect=working_set + 'colcon_core.extension_point.distributions', + side_effect=distributions ): # successfully load a known entry point extension_points = get_all_extension_points() assert set(extension_points.keys()) == {'group1', 'group2'} assert set(extension_points['group1'].keys()) == {'extA', 'extB'} - assert extension_points['group1']['extA'] == ( - 'eA', Dist.project_name, None) + assert extension_points['group1']['extA'][0] == 'eA' def test_extension_point_blocklist(): # successful loading of extension point without a blocklist with patch( - 'colcon_core.extension_point.iter_entry_points', + 'colcon_core.extension_point.entry_points', side_effect=iter_entry_points ): with patch( - 'colcon_core.extension_point.WorkingSet', - side_effect=working_set + 'colcon_core.extension_point.distributions', + side_effect=distributions ): extension_points = get_extension_points('group1') assert 'extA' in extension_points.keys() extension_point = extension_points['extA'] assert extension_point == 'eA' - with patch.object(EntryPoint, 'resolve', return_value=None) as resolve: + with patch.object(EntryPoint, 'load', return_value=None) as load: load_extension_point('extA', 'eA', 'group1') - assert resolve.call_count == 1 + assert load.call_count == 1 # successful loading of entry point not in blocklist - resolve.reset_mock() + load.reset_mock() with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST=os.pathsep.join([ 'group1.extB', 'group2.extC']) ): load_extension_point('extA', 'eA', 'group1') - assert resolve.call_count == 1 + assert load.call_count == 1 # entry point in a blocked group can't be loaded - resolve.reset_mock() + load.reset_mock() with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST='group1'): with pytest.raises(RuntimeError) as e: load_extension_point('extA', 'eA', 'group1') assert 'The entry point group name is listed in the environment ' \ 'variable' in str(e.value) - assert resolve.call_count == 0 + assert load.call_count == 0 # entry point listed in the blocklist can't be loaded with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST=os.pathsep.join([ @@ -116,10 +120,10 @@ def test_extension_point_blocklist(): load_extension_point('extA', 'eA', 'group1') assert 'The entry point name is listed in the environment ' \ 'variable' in str(e.value) - assert resolve.call_count == 0 + assert load.call_count == 0 -def entry_point_resolve(self, *args, **kwargs): +def entry_point_load(self, *args, **kwargs): if self.name == 'exception': raise Exception('entry point raising exception') if self.name == 'runtime_error': @@ -129,7 +133,7 @@ def entry_point_resolve(self, *args, **kwargs): return DEFAULT -@patch.object(EntryPoint, 'resolve', entry_point_resolve) +@patch.object(EntryPoint, 'load', entry_point_load) @patch( 'colcon_core.extension_point.get_extension_points', return_value={'exception': 'a', 'runtime_error': 'b', 'success': 'c'} From a6aef1515b946f5af47b16d55b4f9ffaaaa27126 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Tue, 3 Oct 2023 16:26:52 -0500 Subject: [PATCH 16/16] 0.15.0 --- colcon_core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colcon_core/__init__.py b/colcon_core/__init__.py index 770302ce..21c54fc4 100644 --- a/colcon_core/__init__.py +++ b/colcon_core/__init__.py @@ -1,4 +1,4 @@ # Copyright 2016-2020 Dirk Thomas # Licensed under the Apache License, Version 2.0 -__version__ = '0.14.0' +__version__ = '0.15.0'