Skip to content

Commit

Permalink
Merge branch 'master' into python-install-dir
Browse files Browse the repository at this point in the history
  • Loading branch information
cottsay committed Jun 10, 2024
2 parents 5359d6f + 520052d commit b3da69a
Show file tree
Hide file tree
Showing 40 changed files with 997 additions and 320 deletions.
52 changes: 52 additions & 0 deletions .github/workflows/bootstrap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Run bootstrap tests

on:
workflow_call:

jobs:
setup:
runs-on: ubuntu-latest
outputs:
strategy: ${{steps.load.outputs.strategy}}

steps:
- uses: actions/checkout@v4
with:
repository: colcon/ci
- id: load
run: |
strategy=$(jq -c -M '.' strategy.json)
echo "strategy=${strategy}" >> $GITHUB_OUTPUT
bootstrap:
needs: [setup]
strategy: ${{ fromJson(needs.setup.outputs.strategy) }}
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{matrix.python}}
- name: Install dependencies
run: |
python -m pip install -U pip setuptools
python -m pip install -U -e .[test]
python -m pip uninstall -y colcon-core
- name: Build and test
run: |
cd ..
python ${{ github.workspace }}/bin/colcon build --paths ${{ github.workspace }}
python ${{ github.workspace }}/bin/colcon test --paths ${{ github.workspace }} --return-code-on-test-failure
- name: Use the installed package (Bash)
if: ${{runner.os != 'windows'}}
shell: bash
run: |
. ../install/local_setup.sh
colcon --help
- name: Use the installed package (CMD)
if: ${{runner.os == 'windows'}}
shell: cmd
run: |
call ..\install\local_setup.bat
colcon --help
59 changes: 4 additions & 55 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,9 @@ on:
pull_request:

jobs:
setup:
runs-on: ubuntu-latest
outputs:
strategy: ${{steps.load.outputs.strategy}}

steps:
- uses: actions/checkout@v3
with:
repository: colcon/ci
- id: load
run: echo "strategy=$(echo $(cat strategy.json))" >> $GITHUB_OUTPUT

pytest:
needs: [setup]
strategy: ${{fromJson(needs.setup.outputs.strategy)}}
runs-on: ${{matrix.os}}

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{matrix.python}}
- uses: colcon/ci@v1
- uses: codecov/codecov-action@v3

uses: colcon/ci/.github/workflows/pytest.yaml@main
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
bootstrap:
needs: [setup]
strategy: ${{fromJson(needs.setup.outputs.strategy)}}
runs-on: ${{matrix.os}}

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{matrix.python}}
- name: Install dependencies
run: |
python -m pip install -U pip setuptools
python -m pip install -U -e .[test]
python -m pip uninstall -y colcon-core
- name: Build and test
run: |
cd ..
python ${{github.workspace}}/bin/colcon build --paths ${{github.workspace}}
python ${{github.workspace}}/bin/colcon test --paths ${{github.workspace}} --return-code-on-test-failure
- name: Use the installed package (Bash)
if: ${{runner.os != 'windows'}}
shell: bash
run: |
. ../install/local_setup.sh
colcon --help
- name: Use the installed package (CMD)
if: ${{runner.os == 'windows'}}
shell: cmd
run: |
call ..\install\local_setup.bat
colcon --help
uses: ./.github/workflows/bootstrap.yaml
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.0'
__version__ = '0.16.1'
6 changes: 4 additions & 2 deletions colcon_core/argument_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,17 @@ def decorate_argument_parser(self, *, parser):
raise NotImplementedError()


def get_argument_parser_extensions():
def get_argument_parser_extensions(*, group_name=None):
"""
Get the available argument parser extensions.
The extensions are ordered by their priority and entry point name.
:rtype: OrderedDict
"""
extensions = instantiate_extensions(__name__)
if group_name is None:
group_name = __name__
extensions = instantiate_extensions(group_name)
for name, extension in extensions.items():
extension.ARGUMENT_PARSER_DECORATOR_NAME = name
return order_extensions_by_priority(extensions)
Expand Down
85 changes: 62 additions & 23 deletions colcon_core/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from colcon_core.argument_parser import decorate_argument_parser # noqa: E402 E501 I100 I202
from colcon_core.argument_parser import SuppressUsageOutput # noqa: E402
from colcon_core.extension_point import load_extension_points # noqa: E402
from colcon_core.feature_flags import check_implemented_flags # noqa: E402
from colcon_core.location import create_log_path # noqa: E402
from colcon_core.location import get_log_path # noqa: E402
from colcon_core.location import set_default_config_path # noqa: E402
Expand Down Expand Up @@ -86,12 +87,14 @@ def register_command_exit_handler(handler):
:param handler: The callable
"""
global _command_exit_handlers
if handler not in _command_exit_handlers:
_command_exit_handlers.append(handler)


def main(*, command_name='colcon', argv=None):
def main(
*, command_name='colcon', argv=None, verb_group_name=None,
environment_variable_group_name=None,
):
"""
Execute the main logic of the command.
Expand All @@ -113,9 +116,11 @@ def main(*, command_name='colcon', argv=None):
:param list argv: The list of arguments
:returns: The return code
"""
global _command_exit_handlers
try:
return _main(command_name=command_name, argv=argv)
return _main(
command_name=command_name, argv=argv,
verb_group_name=verb_group_name,
environment_variable_group_name=environment_variable_group_name)
except KeyboardInterrupt:
return signal.SIGINT
finally:
Expand All @@ -125,8 +130,9 @@ def main(*, command_name='colcon', argv=None):
handler()


def _main(*, command_name, argv):
global colcon_logger
def _main(
*, command_name, argv, verb_group_name, environment_variable_group_name,
):
# default log level, for searchability: COLCON_LOG_LEVEL
colcon_logger.setLevel(logging.WARNING)
set_logger_level_from_env(
Expand All @@ -135,14 +141,17 @@ def _main(*, command_name, argv):
'Command line arguments: {argv}'
.format(argv=argv if argv is not None else sys.argv))

# warn about any specified feature flags that are already implemented
check_implemented_flags()

# set default locations for config files, for searchability: COLCON_HOME
set_default_config_path(
path=(Path('~') / f'.{command_name}').expanduser(),
env_var=f'{command_name}_HOME'.upper())

parser = create_parser('colcon_core.environment_variable')
parser = create_parser(environment_variable_group_name)

verb_extensions = get_verb_extensions()
verb_extensions = get_verb_extensions(group_name=verb_group_name)

# add subparsers for all verb extensions but without arguments for now
subparser = create_subparser(
Expand Down Expand Up @@ -206,7 +215,7 @@ def _main(*, command_name, argv):
return verb_main(context, colcon_logger)


def create_parser(environment_variables_group_name):
def create_parser(environment_variables_group_name=None):
"""
Create the argument parser.
Expand All @@ -226,20 +235,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 All @@ -256,9 +275,28 @@ def get_prog_name():
if basename == '__main__.py':
# use the module name in case the script was invoked with python -m ...
prog = os.path.basename(os.path.dirname(prog))
elif shutil.which(basename) == prog:
# use basename only if it is on the PATH
prog = basename
else:
default_prog = shutil.which(basename) or ''
default_ext = os.path.splitext(default_prog)[1]
real_prog = prog
if (
sys.platform == 'win32' and
os.path.splitext(real_prog)[1] != default_ext
):
# On Windows, setuptools entry points drop the file extension from
# argv[0], but shutil.which does not. If the two don't end in the
# same extension, try appending the shutil extension for a better
# chance at matching.
real_prog += default_ext
try:
# The os.path.samefile requires that both files exist on disk, but
# has the advantage of working around symlinks, UNC-style paths,
# DOS 8.3 path atoms, and path normalization.
if os.path.samefile(default_prog, real_prog):
# use basename only if it is on the PATH
prog = basename
except (FileNotFoundError, NotADirectoryError):
pass
return prog


Expand All @@ -276,7 +314,7 @@ def _split_lines(self, text, width):
return lines


def get_environment_variables_epilog(group_name):
def get_environment_variables_epilog(group_name=None):
"""
Get a message enumerating the registered environment variables.
Expand All @@ -285,8 +323,12 @@ def get_environment_variables_epilog(group_name):
:returns: The message for the argument parser epilog
:rtype: str
"""
if group_name is None:
group_name = 'colcon_core.environment_variable'
# 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 @@ -375,9 +417,6 @@ def create_subparser(parser, cmd_name, verb_extensions, *, attribute):
selected verb
:returns: The special action object
"""
global colcon_logger
assert verb_extensions, 'No verb extensions'

# list of available verbs with their descriptions
verbs = []
for name, extension in verb_extensions.items():
Expand All @@ -387,9 +426,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
Empty file.
Empty file.
26 changes: 26 additions & 0 deletions colcon_core/distutils/commands/symlink_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2023 Open Source Robotics Foundation, Inc.
# Licensed under the Apache License, Version 2.0

from distutils.command.install_data import install_data
import os


class symlink_data(install_data): # noqa: N801
"""Like install_data, but symlink files instead of copying."""

def copy_file(self, src, dst, **kwargs): # noqa: D102
if kwargs.get('link'):
return super().copy_file(src, dst, **kwargs)

if self.force:
# os.symlink fails if the destination exists as a regular file
if os.path.isdir(dst):
target = os.path.join(dst, os.path.basename(src))
else:
target = dst
if os.path.exists(dst) and not os.path.islink(dst):
os.remove(target)

kwargs['link'] = 'sym'
src = os.path.abspath(src)
return super().copy_file(src, dst, **kwargs)
6 changes: 4 additions & 2 deletions colcon_core/environment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,17 @@ def create_environment_hooks(self, prefix_path, pkg_name):
raise NotImplementedError()


def get_environment_extensions():
def get_environment_extensions(*, group_name=None):
"""
Get the available environment extensions.
The extensions are ordered by their priority and entry point name.
:rtype: OrderedDict
"""
extensions = instantiate_extensions(__name__)
if group_name is None:
group_name = __name__
extensions = instantiate_extensions(group_name)
for name in list(extensions.keys()):
extension = extensions[name]
extension.ENVIRONMENT_NAME = name
Expand Down
Loading

0 comments on commit b3da69a

Please sign in to comment.