From ad337f67c5d4083c76d29d65e6aef0365692e2c4 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 6 Feb 2023 09:22:46 -0800 Subject: [PATCH] Initial implementation --- .../argument_parser/__init__.py | 0 .../argument_parser/top_level_workspace.py | 108 ++++++++++++++++++ setup.cfg | 4 +- stdeb.cfg | 2 +- test/spell_check.words | 7 ++ 5 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 colcon_top_level_workspace/argument_parser/__init__.py create mode 100644 colcon_top_level_workspace/argument_parser/top_level_workspace.py diff --git a/colcon_top_level_workspace/argument_parser/__init__.py b/colcon_top_level_workspace/argument_parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/colcon_top_level_workspace/argument_parser/top_level_workspace.py b/colcon_top_level_workspace/argument_parser/top_level_workspace.py new file mode 100644 index 0000000..0e71d67 --- /dev/null +++ b/colcon_top_level_workspace/argument_parser/top_level_workspace.py @@ -0,0 +1,108 @@ +# Copyright 2023 Scott K Logan +# Licensed under the Apache License, Version 2.0 + +import os +from pathlib import Path + +from colcon_core.argument_parser import ArgumentParserDecoratorExtensionPoint +from colcon_core.argument_parser import SuppressUsageOutput +from colcon_core.argument_parser.action_collector \ + import ActionCollectorDecorator +from colcon_core.argument_parser.action_collector \ + import SuppressRequiredActions +from colcon_core.argument_parser.action_collector \ + import SuppressTypeConversions +from colcon_core.plugin_system import satisfies_version + + +class TopLevelWorkspaceArgumentParserDecorator( + ArgumentParserDecoratorExtensionPoint +): + """Locate and use a top-level workspace from a subdirectory.""" + + # Lower priority to appear as close to time-of-use as possible + PRIORITY = 75 + + def __init__(self): # noqa: D107 + super().__init__() + satisfies_version( + ArgumentParserDecoratorExtensionPoint.EXTENSION_POINT_VERSION, + '^1.0') + + def decorate_argument_parser(self, *, parser): # noqa: D102 + return TopLevelWorkspaceArgumentDecorator(parser) + + +def _enumerate_parsers(parser): + yield from parser._parsers + for subparser in parser._subparsers: + for p in subparser._parsers: + yield p + yield from _enumerate_parsers(p) + + +class TopLevelWorkspaceArgumentDecorator(ActionCollectorDecorator): + """Change to a top-level workspace if one is found.""" + + def __init__(self, parser): # noqa: D107 + # avoid setting members directly, the base class overrides __setattr__ + # pass them as keyword arguments instead + super().__init__( + parser, + _parsers=[], + _subparsers=[]) + + def add_parser(self, *args, **kwargs): + """Collect association of parsers to their name.""" + parser = super().add_parser(*args, **kwargs) + self._parsers.append(parser) + return parser + + def add_subparsers(self, *args, **kwargs): + """Collect all subparsers.""" + subparser = super().add_subparsers(*args, **kwargs) + self._subparsers.append(subparser) + return subparser + + def parse_args(self, *args, **kwargs): # noqa: D102 + parsers = [self._parser] + parsers.extend(_enumerate_parsers(self)) + with SuppressUsageOutput(parsers): + with SuppressTypeConversions(parsers): + with SuppressRequiredActions(parsers): + known_args, _ = self._parser.parse_known_args( + *args, **kwargs) + + base = 'build' + for arg in ('build_base', 'test_result_base'): + if hasattr(known_args, arg): + base = getattr(known_args, arg) + break + if not os.path.isabs(base): + cwd = Path.cwd() + workspace = find_top_level_workspace(cwd, base) + if workspace and workspace != cwd: + print(f"Using top-level workspace at '{workspace}'") + os.chdir(str(workspace)) + args = self._parser.parse_args(*args, **kwargs) + return args + + +def find_top_level_workspace(candidate, base, this_build_tool='colcon'): + """ + Search for an existing top-level colcon workspace. + + :param candidate: Directory at which the search should begin + :param str base: The base directory + :param str this_build_tool: The name of this build tool + + :returns: Path to an existing workspace root, or None + """ + marker = candidate / base / '.built_by' + if marker.is_file(): + if marker.read_text().rstrip() == this_build_tool: + return candidate + if candidate.parent != candidate: + return find_top_level_workspace( + candidate.parent, base, this_build_tool) + return None diff --git a/setup.cfg b/setup.cfg index fb7fdc7..2b37cb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,7 @@ keywords = colcon [options] python_requires = >=3.6 install_requires = - colcon-core + colcon-core>=0.13.0 packages = find: zip_safe = true @@ -49,6 +49,8 @@ test = junit_suite_name = colcon-top-level-workspace [options.entry_points] +colcon_core.argument_parser = + top_level_workspace = colcon_top_level_workspace.argument_parser.top_level_workspace:TopLevelWorkspaceArgumentParserDecorator [flake8] import-order-style = google diff --git a/stdeb.cfg b/stdeb.cfg index 92099d9..e96dab1 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,5 +1,5 @@ [colcon-top-level-workspace] No-Python2: -Depends3: python3-colcon-core +Depends3: python3-colcon-core (>= 0.13.0) Suite: bionic focal jammy stretch buster bullseye X-Python3-Version: >= 3.6 diff --git a/test/spell_check.words b/test/spell_check.words index 91e2768..386b248 100644 --- a/test/spell_check.words +++ b/test/spell_check.words @@ -1,8 +1,15 @@ apache +chdir colcon iterdir +noqa pathlib +plugin pytest +rstrip +scott scspell setuptools +subparser +subparsers thomas