Skip to content

Commit

Permalink
Merge branch 'master' into cottsay/get_prog_name
Browse files Browse the repository at this point in the history
  • Loading branch information
cottsay committed Apr 25, 2024
2 parents c174d01 + 15ed7d6 commit 2d5b1ba
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 82 deletions.
2 changes: 1 addition & 1 deletion colcon_core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2016-2020 Dirk Thomas
# Licensed under the Apache License, Version 2.0

__version__ = '0.15.2'
__version__ = '0.16.1'
27 changes: 19 additions & 8 deletions colcon_core/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,20 +226,30 @@ class CustomArgumentParser(argparse.ArgumentParser):

def _parse_optional(self, arg_string):
result = super()._parse_optional(arg_string)
if result == (None, arg_string, None):
# Up until https://github.com/python/cpython/pull/114180 ,
# _parse_optional() returned a 3-tuple when it couldn't classify
# the option. As of that PR (which is in Python 3.13, and
# backported to Python 3.12), it returns a 4-tuple. Check for
# either here.
if result in (
(None, arg_string, None),
(None, arg_string, None, None),
):
# in the case there the arg is classified as an unknown 'O'
# override that and classify it as an 'A'
return None
return result

epilog = get_environment_variables_epilog(environment_variables_group_name)
if epilog:
epilog += '\n\n'
epilog += READTHEDOCS_MESSAGE

# top level parser
parser = CustomArgumentParser(
prog=get_prog_name(),
formatter_class=CustomFormatter,
epilog=(
get_environment_variables_epilog(
environment_variables_group_name
) + '\n\n' + READTHEDOCS_MESSAGE))
epilog=epilog)

# enable introspecting and intercepting all command line arguments
parser = decorate_argument_parser(parser)
Expand Down Expand Up @@ -299,6 +309,8 @@ def get_environment_variables_epilog(group_name):
"""
# list environment variables with descriptions
entry_points = load_extension_points(group_name)
if not entry_points:
return ''
env_vars = {
env_var.name: env_var.description for env_var in entry_points.values()}
epilog_lines = []
Expand Down Expand Up @@ -388,7 +400,6 @@ def create_subparser(parser, cmd_name, verb_extensions, *, attribute):
:returns: The special action object
"""
global colcon_logger
assert verb_extensions, 'No verb extensions'

# list of available verbs with their descriptions
verbs = []
Expand All @@ -399,9 +410,9 @@ def create_subparser(parser, cmd_name, verb_extensions, *, attribute):
# add subparser with description of verb extensions
subparser = parser.add_subparsers(
title=f'{cmd_name} verbs',
description='\n'.join(verbs),
description='\n'.join(verbs) or None,
dest=attribute,
help=f'call `{cmd_name} VERB -h` for specific help',
help=f'call `{cmd_name} VERB -h` for specific help' if verbs else None,
)
return subparser

Expand Down
98 changes: 70 additions & 28 deletions colcon_core/extension_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
# Licensed under the Apache License, Version 2.0

from collections import defaultdict
from itertools import chain
import os
import sys
import traceback
import warnings

try:
from importlib.metadata import distributions
Expand All @@ -26,7 +29,6 @@

logger = colcon_logger.getChild(__name__)


"""
The group name for entry points identifying colcon extension points.
Expand All @@ -36,6 +38,8 @@
"""
EXTENSION_POINT_GROUP_NAME = 'colcon_core.extension_point'

_ENTRY_POINTS_CACHE = []


def _get_unique_distributions():
seen = set()
Expand All @@ -46,6 +50,50 @@ def _get_unique_distributions():
yield dist


def _get_entry_points():
for dist in _get_unique_distributions():
for entry_point in dist.entry_points:
# Modern EntryPoint instances should already have this set
if not hasattr(entry_point, 'dist'):
entry_point.dist = dist
yield entry_point


def _get_cached_entry_points():
if not _ENTRY_POINTS_CACHE:
if sys.version_info >= (3, 10):
# We prefer using importlib.metadata.entry_points because it
# has an internal optimization which allows us to load the entry
# points without reading the individual PKG-INFO files, while
# still visiting each unique distribution only once.
all_entry_points = entry_points()
if isinstance(all_entry_points, dict):
# Prior to Python 3.12, entry_points returned a (deprecated)
# dict. Unfortunately, the "future-proof" recommended
# pattern is to add filter parameters, but we actually
# want to cache everything so that doesn't work here.
with warnings.catch_warnings():
warnings.filterwarnings(
'ignore',
'SelectableGroups dict interface is deprecated',
DeprecationWarning,
module=__name__)
all_entry_points = chain.from_iterable(
all_entry_points.values())
_ENTRY_POINTS_CACHE.extend(all_entry_points)
else:
# If we don't have Python 3.10, we must read each PKG-INFO to
# get the name of the distribution so that we can skip the
# "shadowed" distributions properly.
_ENTRY_POINTS_CACHE.extend(_get_entry_points())
return _ENTRY_POINTS_CACHE


def clear_entry_point_cache():
"""Purge the entry point cache."""
_ENTRY_POINTS_CACHE.clear()


def get_all_extension_points():
"""
Get all extension points related to `colcon` and any of its extensions.
Expand All @@ -59,23 +107,24 @@ def get_all_extension_points():
colcon_extension_points = get_extension_points(EXTENSION_POINT_GROUP_NAME)
colcon_extension_points.setdefault(EXTENSION_POINT_GROUP_NAME, None)

entry_points = defaultdict(dict)
for dist in _get_unique_distributions():
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:
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.metadata['Name'], dist.version)
return entry_points
extension_points = defaultdict(dict)
for entry_point in _get_cached_entry_points():
if entry_point.group not in colcon_extension_points:
continue

dist_metadata = entry_point.dist.metadata
ep_tuple = (
entry_point.value,
dist_metadata['Name'], dist_metadata['Version'],
)
if entry_point.name in extension_points[entry_point.group]:
previous = extension_points[entry_point.group][entry_point.name]
logger.error(
f"Entry point '{entry_point.group}.{entry_point.name}' is "
f"declared multiple times, '{ep_tuple}' "
f"overwriting '{previous}'")
extension_points[entry_point.group][entry_point.name] = ep_tuple
return extension_points


def get_extension_points(group):
Expand All @@ -87,16 +136,9 @@ def get_extension_points(group):
:rtype: dict
"""
extension_points = {}
try:
# Python 3.10 and newer
query = entry_points(group=group)
except TypeError:
query = (
entry_point
for dist in _get_unique_distributions()
for entry_point in dist.entry_points
if entry_point.group == group)
for entry_point in query:
for entry_point in _get_cached_entry_points():
if entry_point.group != group:
continue
if entry_point.name in extension_points:
previous_entry_point = extension_points[entry_point.name]
logger.error(
Expand Down
19 changes: 12 additions & 7 deletions colcon_core/task/python/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from pathlib import Path
import shutil
import sys
from sys import executable

from colcon_core.environment import create_environment_hooks
from colcon_core.environment import create_environment_scripts
Expand All @@ -26,6 +25,12 @@

logger = colcon_logger.getChild(__name__)

_PYTHON_CMD = [
sys.executable,
'-W',
'ignore:setup.py install is deprecated',
]


def _get_install_scripts(path):
setup_cfg_path = os.path.join(path, 'setup.cfg')
Expand Down Expand Up @@ -92,7 +97,7 @@ async def build(self, *, additional_hooks=None): # noqa: D102

# invoke `setup.py install` step with lots of arguments
# to avoid placing any files in the source space
cmd = [executable, 'setup.py']
cmd = _PYTHON_CMD + ['setup.py']
if 'egg_info' in available_commands:
# `setup.py egg_info` requires the --egg-base to exist
os.makedirs(args.build_base, exist_ok=True)
Expand Down Expand Up @@ -139,8 +144,8 @@ async def build(self, *, additional_hooks=None): # noqa: D102
try:
# --editable causes this to skip creating/editing the
# easy-install.pth file
cmd = [
executable, 'setup.py',
cmd = _PYTHON_CMD + [
'setup.py',
'develop',
'--editable',
'--build-directory',
Expand Down Expand Up @@ -181,7 +186,7 @@ async def build(self, *, additional_hooks=None): # noqa: D102

async def _get_available_commands(self, path, env):
output = await check_output(
[executable, 'setup.py', '--help-commands'], cwd=path, env=env)
_PYTHON_CMD + ['setup.py', '--help-commands'], cwd=path, env=env)
commands = set()
for line in output.splitlines():
if not line.startswith(b' '):
Expand All @@ -208,8 +213,8 @@ async def _undo_develop(self, pkg, args, env):
args.build_base, '%s.egg-info' % pkg.name.replace('-', '_'))
setup_py_build_space = os.path.join(args.build_base, 'setup.py')
if os.path.exists(egg_info) and os.path.islink(setup_py_build_space):
cmd = [
executable, 'setup.py',
cmd = _PYTHON_CMD + [
'setup.py',
'develop',
'--uninstall', '--editable',
'--build-directory', os.path.join(args.build_base, 'build')
Expand Down
6 changes: 5 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ zip_safe = false

[options.extras_require]
test =
flake8>=3.6.0,<7
flake8>=3.6.0
flake8-blind-except
flake8-builtins
flake8-class-newline
Expand Down Expand Up @@ -156,6 +156,10 @@ colcon_core.task.python.template = *.em

[flake8]
import-order-style = google
per-file-ignores =
colcon_core/distutils/__init__.py:A005
colcon_core/logging.py:A005
colcon_core/subprocess.py:A005

[coverage:run]
source = colcon_core
1 change: 1 addition & 0 deletions stdeb.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ No-Python2:
Depends3: python3-distlib, python3-empy (<4), python3-packaging, python3-pytest, python3-setuptools, python3 (>= 3.8) | python3-importlib-metadata
Recommends3: python3-pytest-cov
Suggests3: python3-pytest-repeat, python3-pytest-rerunfailures
Replaces3: colcon
Suite: focal jammy noble bookworm trixie
X-Python3-Version: >= 3.6
3 changes: 3 additions & 0 deletions test/spell_check.words
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ apache
argparse
asyncio
autouse
backported
basepath
bazqux
blocklist
Expand All @@ -17,6 +18,7 @@ configparser
contextlib
coroutine
coroutines
cpython
datetime
debian
debinfo
Expand Down Expand Up @@ -50,6 +52,7 @@ importlib
importorskip
isatty
iterdir
itertools
junit
levelname
libexec
Expand Down
11 changes: 11 additions & 0 deletions test/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ def test_main():
assert rc == signal.SIGINT


def test_main_no_verbs_or_env():
with ExtensionPointContext():
with patch(
'colcon_core.command.load_extension_points',
return_value={},
):
with pytest.raises(SystemExit) as e:
main(argv=['--help'])
assert e.value.code == 0


def test_create_parser():
with ExtensionPointContext():
parser = create_parser('colcon_core.environment_variable')
Expand Down
Loading

0 comments on commit 2d5b1ba

Please sign in to comment.