diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e199302e..5810de03 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,9 @@ Release 0.12.0 (unreleased) * Skip patches outside manifest dir (#942) * Make patch path in metadata platform independent (#937) * Fix extra newlines in patch for new files (#945) +* Replace colored-logs and Halo with Rich (#960) +* Respect `NO_COLOR `_ (#960) +* Group logging under a project name header (#960) Release 0.11.0 (released 2026-01-03) ==================================== diff --git a/dfetch/__main__.py b/dfetch/__main__.py index 0cc82c8b..312610d4 100644 --- a/dfetch/__main__.py +++ b/dfetch/__main__.py @@ -6,6 +6,9 @@ import argparse import sys from collections.abc import Sequence +from typing import Optional + +from rich.console import Console import dfetch.commands.check import dfetch.commands.diff @@ -18,8 +21,7 @@ import dfetch.commands.validate import dfetch.log import dfetch.util.cmdline - -logger = dfetch.log.setup_root(__name__) +from dfetch.log import DLogger class DfetchFatalException(Exception): @@ -34,6 +36,9 @@ def create_parser() -> argparse.ArgumentParser: parser.add_argument( "--verbose", "-v", action="store_true", help="Increase verbosity" ) + parser.add_argument( + "--no-color", action="store_true", help="Disable colored output" + ) parser.set_defaults(func=_help) subparsers = parser.add_subparsers(help="commands") @@ -50,16 +55,21 @@ def create_parser() -> argparse.ArgumentParser: return parser -def _help(args: argparse.Namespace) -> None: - """Show the help.""" - raise RuntimeError("Select a function") +def _help(_: argparse.Namespace) -> None: + """Show help if no subcommand was selected.""" + parser = create_parser() + parser.print_help() -def run(argv: Sequence[str]) -> None: +def run(argv: Sequence[str], console: Optional[Console] = None) -> None: """Start dfetch.""" - logger.print_title() args = create_parser().parse_args(argv) + console = console or dfetch.log.make_console(no_color=args.no_color) + logger: DLogger = dfetch.log.setup_root(__name__, console=console) + + logger.print_title() + if args.verbose: dfetch.log.increase_verbosity() diff --git a/dfetch/commands/common.py b/dfetch/commands/common.py index 5ccd2b58..9f3878cf 100644 --- a/dfetch/commands/common.py +++ b/dfetch/commands/common.py @@ -44,15 +44,15 @@ def _make_recommendation( recommendations (List[ProjectEntry]): List of recommendations childmanifest_path (str): Path to the source of recommendations """ - logger.warning( - "\n".join( + logger.print_warning_line( + project.name, + " ".join( [ - "", f'"{project.name}" depends on the following project(s) ' "which are not part of your manifest:", f"(found in {childmanifest_path})", ] - ) + ), ) recommendation_json = yaml.dump( diff --git a/dfetch/commands/environment.py b/dfetch/commands/environment.py index 99109baf..7ce1e63a 100644 --- a/dfetch/commands/environment.py +++ b/dfetch/commands/environment.py @@ -23,6 +23,8 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, _: argparse.Namespace) -> None: """Perform listing the environment.""" - logger.print_info_line("platform", f"{platform.system()} {platform.release()}") + logger.print_report_line( + "platform", f"{platform.system()} {platform.release()}" + ) for project_type in SUPPORTED_PROJECT_TYPES: project_type.list_tool_info() diff --git a/dfetch/commands/validate.py b/dfetch/commands/validate.py index 7b3a992a..4845f23a 100644 --- a/dfetch/commands/validate.py +++ b/dfetch/commands/validate.py @@ -35,4 +35,4 @@ def __call__(self, args: argparse.Namespace) -> None: manifest_path = find_manifest() parse(manifest_path) manifest_path = os.path.relpath(manifest_path, os.getcwd()) - logger.print_info_line(manifest_path, "valid") + logger.print_report_line(manifest_path, "valid") diff --git a/dfetch/log.py b/dfetch/log.py index 5b67838e..274d42f9 100644 --- a/dfetch/log.py +++ b/dfetch/log.py @@ -1,63 +1,185 @@ """Logging related items.""" import logging -from typing import cast +import os +import sys +from contextlib import nullcontext +from typing import Any, Optional, Union, cast -import coloredlogs -from colorama import Fore +from rich.console import Console +from rich.highlighter import NullHighlighter +from rich.logging import RichHandler +from rich.status import Status from dfetch import __version__ +def make_console(no_color: bool = False) -> Console: + """Create a Rich Console with proper color handling.""" + return Console( + no_color=no_color + or os.getenv("NO_COLOR") is not None + or not sys.stdout.isatty() + ) + + +def configure_root_logger(console: Optional[Console] = None) -> None: + """Configure the root logger with RichHandler using the provided Console.""" + console = console or make_console() + + handler = RichHandler( + console=console, + show_time=False, + show_path=False, + show_level=False, + markup=True, + rich_tracebacks=True, + highlighter=NullHighlighter(), + ) + + logging.basicConfig( + level=logging.INFO, + format="%(message)s", + handlers=[handler], + force=True, + ) + + class DLogger(logging.Logger): """Logging class extended with specific log items for dfetch.""" + _printed_projects: set[str] = set() + + def print_report_line(self, name: str, info: str) -> None: + """Print a line for a report.""" + self.info( + f" [bold][bright_green]{name:20s}:[/bright_green][blue] {info}[/blue][/bold]" + ) + def print_info_line(self, name: str, info: str) -> None: - """Print a line of info.""" - self.info(f" {Fore.GREEN}{name:20s}:{Fore.BLUE} {info}") + """Print a line of info, only printing the project name once.""" + if name not in DLogger._printed_projects: + self.info(f" [bold][bright_green]{name}:[/bright_green][/bold]") + DLogger._printed_projects.add(name) + + self.info(f" [bold blue]> {info}[/bold blue]") def print_warning_line(self, name: str, info: str) -> None: - """Print a line of info.""" - self.info(f" {Fore.GREEN}{name:20s}:{Fore.YELLOW} {info}") + """Print a warning line: green name, yellow value.""" + if name not in DLogger._printed_projects: + self.info(f" [bold][bright_green]{name}:[/bright_green][/bold]") + DLogger._printed_projects.add(name) + + self.info(f" [bold bright_yellow]> {info}[/bold bright_yellow]") def print_title(self) -> None: """Print the DFetch tool title and version.""" - self.info(f"{Fore.BLUE}Dfetch ({__version__})") + self.info(f"[bold blue]Dfetch ({__version__})[/bold blue]") def print_info_field(self, field_name: str, field: str) -> None: """Print a field with corresponding value.""" - self.print_info_line(field_name, field if field else "") - - -def setup_root(name: str) -> DLogger: - """Create the root logger.""" - logger = get_logger(name) - - msg_format = "%(message)s" - - level_style = { - "critical": {"color": "magenta", "bright": True, "bold": True}, - "debug": {"color": "green", "bright": True, "bold": True}, - "error": {"color": "red", "bright": True, "bold": True}, - "info": {"color": 4, "bright": True, "bold": True}, - "notice": {"color": "magenta", "bright": True, "bold": True}, - "spam": {"color": "green", "faint": True}, - "success": {"color": "green", "bright": True, "bold": True}, - "verbose": {"color": "blue", "bright": True, "bold": True}, - "warning": {"color": "yellow", "bright": True, "bold": True}, - } - - coloredlogs.install(fmt=msg_format, level_styles=level_style, level="INFO") - - return logger + self.print_report_line(field_name, field if field else "") + + def warning(self, msg: object, *args: Any, **kwargs: Any) -> None: + """Log warning.""" + super().warning( + f" [bold bright_yellow]{msg}[/bold bright_yellow]", *args, **kwargs + ) + + def error(self, msg: object, *args: Any, **kwargs: Any) -> None: + """Log error.""" + super().error(f"[red]{msg}[/red]", *args, **kwargs) + + def status( + self, message: str, spinner: str = "dots", enabled: bool = True + ) -> Union[Status, nullcontext[None]]: + """Show status message with spinner if enabled.""" + rich_console = None + logger: Optional[logging.Logger] = self + while logger: + for handler in getattr(logger, "handlers", []): + if isinstance(handler, RichHandler): + rich_console = handler.console + break + if rich_console or not getattr(logger, "parent", None): + break + logger = logger.parent + + if not rich_console or not enabled: + return nullcontext(None) + + return Status( + f"[bold bright_blue]> {message}[/bold bright_blue]", + spinner=spinner, + console=rich_console, + ) + + @classmethod + def reset_projects(cls) -> None: + """Clear the record of printed project names.""" + cls._printed_projects.clear() + + +class IndentFilter(logging.Filter): # pylint: disable=too-few-public-methods + """Adds indentation to all log messages that pass through this filter.""" + + def __init__(self, prefix: str = " "): + """Initialize the IndentFilter with a prefix.""" + super().__init__() + self.prefix = prefix + + def filter(self, record: logging.LogRecord) -> bool: + """Add indentation to the log record message.""" + color = "blue" if record.levelno < logging.WARNING else "yellow" + + record.msg = f"{self.prefix}[{color}]{record.msg}[/{color}]" + return True + + +def setup_root(name: str, console: Optional[Console] = None) -> DLogger: + """Create and return the root logger.""" + logging.setLoggerClass(DLogger) + configure_root_logger(console) + logger = logging.getLogger(name) + return cast(DLogger, logger) def increase_verbosity() -> None: - """Increase the verbosity of the logger.""" - coloredlogs.increase_verbosity() - - -def get_logger(name: str) -> DLogger: - """Get logger for a module.""" + """Increase verbosity of the root logger.""" + levels = [ + logging.CRITICAL, + logging.ERROR, + logging.WARNING, + logging.INFO, + logging.DEBUG, + ] + logger_ = logging.getLogger() + current_level = logger_.getEffectiveLevel() + try: + idx = levels.index(current_level) + if idx < len(levels) - 1: + new_level = levels[idx + 1] + else: + new_level = levels[-1] + except ValueError: + new_level = logging.DEBUG + logger_.setLevel(new_level) + + +def get_logger(name: str, console: Optional[Console] = None) -> DLogger: + """Get logger for a module, optionally configuring console colors.""" logging.setLoggerClass(DLogger) - return cast(DLogger, logging.getLogger(name)) + logger = logging.getLogger(name) + logger.propagate = True + if console: + configure_root_logger(console) + return cast(DLogger, logger) + + +def configure_external_logger(name: str, level: int = logging.INFO) -> None: + """Configure an external logger from a third party package.""" + logger = logging.getLogger(name) + logger.setLevel(level) + logger.propagate = True + logger.handlers.clear() + logger.addFilter(IndentFilter()) diff --git a/dfetch/project/subproject.py b/dfetch/project/subproject.py index 837b84bb..c8bc7cc3 100644 --- a/dfetch/project/subproject.py +++ b/dfetch/project/subproject.py @@ -7,8 +7,6 @@ from collections.abc import Sequence from typing import Optional -from halo import Halo - from dfetch.log import get_logger from dfetch.manifest.project import ProjectEntry from dfetch.manifest.version import Version @@ -119,11 +117,8 @@ def update( logger.debug(f"Clearing destination {self.local_path}") safe_rm(self.local_path) - with Halo( - text=f"Fetching {self.__project.name} {to_fetch}", - spinner="dots", - text_color="green", - enabled=self._show_animations, + with logger.status( + f"Fetching {self.__project.name} {to_fetch}", enabled=self._show_animations ): actually_fetched = self._fetch_impl(to_fetch) self._log_project(f"Fetched {actually_fetched}") @@ -159,8 +154,8 @@ def _apply_patches(self) -> list[str]: normalized_patch_path = str(relative_patch_path.as_posix()) - apply_patch(normalized_patch_path, root=self.local_path) - self._log_project(f'Applied patch "{normalized_patch_path}"') + self._log_project(f'Applying patch "{normalized_patch_path}"') + apply_patch(logger, normalized_patch_path, root=self.local_path) applied_patches.append(normalized_patch_path) return applied_patches @@ -169,11 +164,8 @@ def check_for_update( ) -> None: """Check if there is an update available.""" on_disk_version = self.on_disk_version() - with Halo( - text=f"Checking {self.__project.name}", - spinner="dots", - text_color="green", - enabled=self._show_animations, + with logger.status( + f"Checking {self.__project.name}", enabled=self._show_animations ): latest_version = self._check_for_newer_version() @@ -229,7 +221,7 @@ def _log_project(self, msg: str) -> None: @staticmethod def _log_tool(name: str, msg: str) -> None: - logger.print_info_line(name, msg.strip()) + logger.print_report_line(name, msg.strip()) @property def local_path(self) -> str: @@ -299,9 +291,10 @@ def on_disk_version(self) -> Optional[Version]: try: return Metadata.from_file(self.__metadata.path).version except TypeError: - logger.warning( + logger.print_warning_line( + self.__project.name, f"{pathlib.Path(self.__metadata.path).relative_to(os.getcwd()).as_posix()}" - " is an invalid metadata file, not checking on disk version!" + " is an invalid metadata file, not checking on disk version!", ) return None @@ -317,9 +310,10 @@ def _on_disk_hash(self) -> Optional[str]: try: return Metadata.from_file(self.__metadata.path).hash except TypeError: - logger.warning( + logger.print_warning_line( + self.__project.name, f"{pathlib.Path(self.__metadata.path).relative_to(os.getcwd()).as_posix()}" - " is an invalid metadata file, not checking local hash!" + " is an invalid metadata file, not checking local hash!", ) return None diff --git a/dfetch/vcs/patch.py b/dfetch/vcs/patch.py index a70e85d9..02b13f49 100644 --- a/dfetch/vcs/patch.py +++ b/dfetch/vcs/patch.py @@ -8,9 +8,9 @@ import patch_ng -from dfetch.log import get_logger +from dfetch.log import DLogger, configure_external_logger -logger = get_logger(__name__) +configure_external_logger("patch_ng") def _git_mode(path: Path) -> str: @@ -59,7 +59,11 @@ def dump_patch(patch_set: patch_ng.PatchSet) -> str: return "\n".join(patch_lines) + "\n" if patch_lines else "" -def apply_patch(patch_path: str, root: str = ".") -> None: +def apply_patch( + logger: DLogger, + patch_path: str, + root: str = ".", +) -> None: """Apply the specified patch relative to the root.""" patch_set = patch_ng.fromfile(patch_path) diff --git a/doc/_ext/sphinxcontrib_asciinema/.dfetch_data.yaml b/doc/_ext/sphinxcontrib_asciinema/.dfetch_data.yaml index 5c941f3c..49a36e79 100644 --- a/doc/_ext/sphinxcontrib_asciinema/.dfetch_data.yaml +++ b/doc/_ext/sphinxcontrib_asciinema/.dfetch_data.yaml @@ -2,8 +2,8 @@ # For more info see https://dfetch.rtfd.io/en/latest/getting_started.html dfetch: branch: master - hash: dcd1473e1a3ca613b804e3e51e7ee342 - last_fetch: 07/01/2026, 21:38:48 + hash: 5b0a3a18e1e83d363f9eb0ac4b3fca17 + last_fetch: 26/01/2026, 23:40:59 patch: - doc/_ext/patches/001-autoformat-sphinxcontrib.asciinema.patch - doc/_ext/patches/002-fix-options-sphinxcontrib.asciinema.patch diff --git a/doc/legal.rst b/doc/legal.rst index 5e36a740..f7ed3a1d 100644 --- a/doc/legal.rst +++ b/doc/legal.rst @@ -77,34 +77,33 @@ We use `PyYAML`_ for parsing manifests (which are YAML). This uses the MIT licen .. _`PyYAML`: https://pyyaml.org/ -python-coloredlogs -~~~~~~~~~~~~~~~~~~ -`Colored logs`_ is used for the colored text output. +Rich +~~~~ +`Rich`_ is used for the colored text output. :: - Copyright (c) 2020 Peter Odding + Copyright (c) 2020 Will McGugan - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. -.. _`Colored logs`: https://coloredlogs.readthedocs.io/en/latest/ +.. _`Rich`: https://rich.readthedocs.io/en/latest/ pykwalify ~~~~~~~~~ @@ -137,41 +136,7 @@ pykwalify .. _`pykwalify`: https://github.com/Grokzen/pykwalify -Colorama -~~~~~~~~ -`colorama`_ is also used for the colored text output. - -:: - - Copyright (c) 2010 Jonathan Hartley - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the name of the copyright holders, nor those of its contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -.. _`colorama`: https://github.com/tartley/colorama Typing-extensions @@ -431,37 +396,6 @@ cyclonedx-python-lib .. _`cyclonedx-python-lib`: https://github.com/CycloneDX/cyclonedx-python-lib/ -Halo -~~~~ -`Halo`_ is used to show a nice spinner during long-running operations. - -:: - - MIT License - - Copyright (c) 2017 Manraj Singh - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - -.. _`Halo`: https://github.com/manrajgrover/halo/ - Sarif-om ~~~~~~~~ `Sarif-om`_ is used for generating reports in Sarif format for Github. diff --git a/features/check-git-repo.feature b/features/check-git-repo.feature index 0d03930c..25a24d97 100644 --- a/features/check-git-repo.feature +++ b/features/check-git-repo.feature @@ -27,8 +27,10 @@ Feature: Checking dependencies from a git repository Then the output shows """ Dfetch (0.11.0) - ext/test-repo-rev-only: wanted (e1fda19a57b873eb8e6ae37780594cbb77b70f1a), available (e1fda19a57b873eb8e6ae37780594cbb77b70f1a) - ext/test-rev-and-branch: wanted (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a) + ext/test-repo-rev-only: + > wanted (e1fda19a57b873eb8e6ae37780594cbb77b70f1a), available (e1fda19a57b873eb8e6ae37780594cbb77b70f1a) + ext/test-rev-and-branch: + > wanted (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a) """ Scenario: A newer tag is available than in manifest @@ -51,7 +53,8 @@ Feature: Checking dependencies from a git repository Then the output shows """ Dfetch (0.11.0) - ext/test-repo-tag-v1: wanted (v1), available (v2.0) + ext/test-repo-tag-v1: + > wanted (v1), available (v2.0) """ Scenario: Check is done after an update @@ -80,8 +83,10 @@ Feature: Checking dependencies from a git repository Then the output shows """ Dfetch (0.11.0) - ext/test-repo-rev-only: up-to-date (e1fda19a57b873eb8e6ae37780594cbb77b70f1a) - ext/test-rev-and-branch: wanted & current (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a) + ext/test-repo-rev-only: + > up-to-date (e1fda19a57b873eb8e6ae37780594cbb77b70f1a) + ext/test-rev-and-branch: + > wanted & current (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a) """ Scenario: Tag is updated in manifest @@ -112,7 +117,8 @@ Feature: Checking dependencies from a git repository Then the output shows """ Dfetch (0.11.0) - ext/test-repo-tag : wanted (v2.0), current (v1), available (v2.0) + ext/test-repo-tag: + > wanted (v2.0), current (v1), available (v2.0) """ Scenario: A local change is reported @@ -133,8 +139,9 @@ Feature: Checking dependencies from a git repository Then the output shows """ Dfetch (0.11.0) - SomeProject : Local changes were detected, please generate a patch using 'dfetch diff SomeProject' and add it to your manifest using 'patch:'. Alternatively overwrite the local changes with 'dfetch update --force SomeProject' - SomeProject : up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0) + SomeProject: + > Local changes were detected, please generate a patch using 'dfetch diff SomeProject' and add it to your manifest using 'patch:'. Alternatively overwrite the local changes with 'dfetch update --force SomeProject' + > up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0) """ Scenario: Change to ignored files are not reported @@ -153,7 +160,8 @@ Feature: Checking dependencies from a git repository Then the output shows """ Dfetch (0.11.0) - SomeProject : up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0) + SomeProject: + > up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0) """ Scenario: A non-existent remote is reported @@ -201,9 +209,12 @@ Feature: Checking dependencies from a git repository Then the output shows """ Dfetch (0.11.0) - SomeProjectMissingTag: wanted (i-dont-exist), but not available at the upstream. - SomeProjectNonExistentBranch: wanted (i-dont-exist), but not available at the upstream. - SomeProjectNonExistentRevision: wanted (0123112321234123512361236123712381239123), but not available at the upstream. + SomeProjectMissingTag: + > wanted (i-dont-exist), but not available at the upstream. + SomeProjectNonExistentBranch: + > wanted (i-dont-exist), but not available at the upstream. + SomeProjectNonExistentRevision: + > wanted (0123112321234123512361236123712381239123), but not available at the upstream. """ Scenario: Credentials required for remote diff --git a/features/check-specific-projects.feature b/features/check-specific-projects.feature index 79ae9a2d..6d69b63e 100644 --- a/features/check-specific-projects.feature +++ b/features/check-specific-projects.feature @@ -28,5 +28,6 @@ Feature: Checking specific projects Then the output shows """ Dfetch (0.11.0) - ext/test-rev-and-branch: wanted (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a) + ext/test-rev-and-branch: + > wanted (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a) """ diff --git a/features/check-svn-repo.feature b/features/check-svn-repo.feature index 8553309a..d864cf40 100644 --- a/features/check-svn-repo.feature +++ b/features/check-svn-repo.feature @@ -30,8 +30,10 @@ Feature: Checking dependencies from a svn repository Then the output shows """ Dfetch (0.11.0) - cunit-svn-rev-only : wanted (170), available (trunk - 170) - cunit-svn-rev-and-branch: wanted (mingw64 - 156), available (mingw64 - 170) + cunit-svn-rev-only: + > wanted (170), available (trunk - 170) + cunit-svn-rev-and-branch: + > wanted (mingw64 - 156), available (mingw64 - 170) """ Scenario: A newer tag is available than in manifest @@ -55,7 +57,8 @@ Feature: Checking dependencies from a svn repository Then the output shows """ Dfetch (0.11.0) - cutter-svn-tag : wanted (1.1.7), available (1.1.8) + cutter-svn-tag: + > wanted (1.1.7), available (1.1.8) """ Scenario: Check is done after an update @@ -92,9 +95,12 @@ Feature: Checking dependencies from a svn repository Then the output shows """ Dfetch (0.11.0) - cunit-svn-rev-only : wanted (169), current (trunk - 169), available (trunk - 170) - cunit-svn-rev-and-branch: wanted & current (mingw64 - 156), available (mingw64 - 170) - ext/test-non-standard-svn: wanted (latest), current (1), available (1) + cunit-svn-rev-only: + > wanted (169), current (trunk - 169), available (trunk - 170) + cunit-svn-rev-and-branch: + > wanted & current (mingw64 - 156), available (mingw64 - 170) + ext/test-non-standard-svn: + > wanted (latest), current (1), available (1) """ Scenario: A non-standard SVN repository can be checked @@ -113,7 +119,8 @@ Feature: Checking dependencies from a svn repository Then the output shows """ Dfetch (0.11.0) - SomeProject : wanted (latest), current (1), available (1) + SomeProject: + > wanted (latest), current (1), available (1) """ Scenario: A non-existent remote is reported @@ -156,7 +163,8 @@ Feature: Checking dependencies from a svn repository Then the output shows """ Dfetch (0.11.0) - cutter-svn-tag : wanted (non-existent-tag), but not available at the upstream. + cutter-svn-tag: + > wanted (non-existent-tag), but not available at the upstream. """ Scenario: Change to ignored files are not reported @@ -176,5 +184,6 @@ Feature: Checking dependencies from a svn repository Then the output shows """ Dfetch (0.11.0) - SomeProject : up-to-date (v1) + SomeProject: + > up-to-date (v1) """ diff --git a/features/checked-project-has-dependencies.feature b/features/checked-project-has-dependencies.feature index 8b8f59de..6c6dbcdd 100644 --- a/features/checked-project-has-dependencies.feature +++ b/features/checked-project-has-dependencies.feature @@ -30,7 +30,8 @@ Feature: Check for dependencies in projects Then the output shows """ Dfetch (0.11.0) - SomeProject : wanted (v1), available (v1) + SomeProject: + > wanted (v1), available (v1) """ Scenario: A recommendation is done due to a missing dependency @@ -67,7 +68,8 @@ Feature: Check for dependencies in projects """ Dfetch (0.11.0) Multiple manifests found, using dfetch.yaml - SomeProject : up-to-date (v1) + SomeProject: + > up-to-date (v1) "SomeProject" depends on the following project(s) which are not part of your manifest: (found in third-party/SomeProject/dfetch.yaml) @@ -110,6 +112,8 @@ Feature: Check for dependencies in projects """ Dfetch (0.11.0) Multiple manifests found, using dfetch.yaml - SomeProject : up-to-date (v1) - SomeOtherProject : up-to-date (v1) + SomeProject: + > up-to-date (v1) + SomeOtherProject: + > up-to-date (v1) """ diff --git a/features/diff-in-git.feature b/features/diff-in-git.feature index 6941f660..3c4a85ac 100644 --- a/features/diff-in-git.feature +++ b/features/diff-in-git.feature @@ -66,7 +66,8 @@ Feature: Diff in git Then the output shows """ Dfetch (0.11.0) - SomeProject : No diffs found since 59efb91396fd369eb113b43382783294dc8ed6d2 + SomeProject: + > No diffs found since 59efb91396fd369eb113b43382783294dc8ed6d2 """ Scenario: Diff is generated on uncommitted changes @@ -92,5 +93,6 @@ Feature: Diff in git Then the output shows """ Dfetch (0.11.0) - SomeProject : No diffs found since 59efb91396fd369eb113b43382783294dc8ed6d2 + SomeProject: + > No diffs found since 59efb91396fd369eb113b43382783294dc8ed6d2 """ diff --git a/features/diff-in-svn.feature b/features/diff-in-svn.feature index eceba4e0..ca686f4c 100644 --- a/features/diff-in-svn.feature +++ b/features/diff-in-svn.feature @@ -63,7 +63,8 @@ Feature: Diff in svn Then the output shows """ Dfetch (0.11.0) - SomeProject : No diffs found since 1 + SomeProject: + > No diffs found since 1 """ Scenario: A patch file is generated on uncommitted changes @@ -89,5 +90,6 @@ Feature: Diff in svn Then the output shows """ Dfetch (0.11.0) - SomeProject : No diffs found since 1 + SomeProject: + > No diffs found since 1 """ diff --git a/features/environment.py b/features/environment.py index 30291015..10d083e9 100644 --- a/features/environment.py +++ b/features/environment.py @@ -4,6 +4,7 @@ import tempfile from behave import fixture, use_fixture +from rich.console import Console from dfetch.util.util import safe_rmtree @@ -28,6 +29,12 @@ def before_scenario(context, _): """Hook called before scenario is executed.""" use_fixture(tmpdir, context) + context.console = Console( + record=True, + force_terminal=True, + width=1024, + ) + def before_all(context): """Hook called before first test is run.""" diff --git a/features/fetch-checks-destination.feature b/features/fetch-checks-destination.feature index e177ad20..0443eb38 100644 --- a/features/fetch-checks-destination.feature +++ b/features/fetch-checks-destination.feature @@ -21,7 +21,8 @@ Feature: Fetch checks destinations Then the output shows """ Dfetch (0.11.0) - ext/test-repo-tag : Skipping, path "." is not allowed as destination. + ext/test-repo-tag: + > Skipping, path "." is not allowed as destination. Destination must be in a valid subfolder. "." is not valid! """ @@ -42,6 +43,7 @@ Feature: Fetch checks destinations Then the output shows """ Dfetch (0.11.0) - ext/test-repo-tag : Skipping, path "../../some-higher-folder" is outside manifest directory tree. + ext/test-repo-tag: + > Skipping, path "../../some-higher-folder" is outside manifest directory tree. Destination must be in the manifests folder or a subfolder. "../../some-higher-folder" is outside this tree! """ diff --git a/features/fetch-file-pattern-git.feature b/features/fetch-file-pattern-git.feature index 08f689a9..8c56c72c 100644 --- a/features/fetch-file-pattern-git.feature +++ b/features/fetch-file-pattern-git.feature @@ -23,7 +23,8 @@ Feature: Fetch file pattern from git repo Then the output shows """ Dfetch (0.11.0) - SomeProjectWithAnInterestingFile: Fetched v1 + SomeProjectWithAnInterestingFile: + > Fetched v1 """ Then 'MyProject' looks like: """ diff --git a/features/fetch-file-pattern-svn.feature b/features/fetch-file-pattern-svn.feature index 08453972..0db7c793 100644 --- a/features/fetch-file-pattern-svn.feature +++ b/features/fetch-file-pattern-svn.feature @@ -22,7 +22,8 @@ Feature: Fetch file pattern from svn repo Then the output shows """ Dfetch (0.11.0) - SomeProjectWithAnInterestingFile: Fetched trunk - 1 + SomeProjectWithAnInterestingFile: + > Fetched trunk - 1 """ Then 'MyProject' looks like: """ diff --git a/features/fetch-git-repo.feature b/features/fetch-git-repo.feature index b6b54612..adfb6e0c 100644 --- a/features/fetch-git-repo.feature +++ b/features/fetch-git-repo.feature @@ -64,7 +64,8 @@ Feature: Fetching dependencies from a git repository Then the output shows """ Dfetch (0.11.0) - ext/test-repo-tag : Fetched v2.0 + ext/test-repo-tag: + > Fetched v2.0 """ Scenario: Version check ignored when force flag is given @@ -84,5 +85,6 @@ Feature: Fetching dependencies from a git repository Then the output shows """ Dfetch (0.11.0) - ext/test-repo-tag : Fetched v1 + ext/test-repo-tag: + > Fetched v1 """ diff --git a/features/fetch-single-file-git.feature b/features/fetch-single-file-git.feature index c6434cce..13cc994a 100644 --- a/features/fetch-single-file-git.feature +++ b/features/fetch-single-file-git.feature @@ -22,7 +22,8 @@ Feature: Fetch single file from git repo Then the output shows """ Dfetch (0.11.0) - SomeProjectWithAnInterestingFile: Fetched v1 + SomeProjectWithAnInterestingFile: + > Fetched v1 """ Then 'MyProject' looks like: """ @@ -53,7 +54,8 @@ Feature: Fetch single file from git repo Then the output shows """ Dfetch (0.11.0) - SomeProjectWithAnInterestingFile: Fetched v1 + SomeProjectWithAnInterestingFile: + > Fetched v1 """ Then 'MyProject' looks like: """ diff --git a/features/fetch-single-file-svn.feature b/features/fetch-single-file-svn.feature index 1b530ffc..2d471a4a 100644 --- a/features/fetch-single-file-svn.feature +++ b/features/fetch-single-file-svn.feature @@ -22,7 +22,8 @@ Feature: Fetch single file from svn repo Then the output shows """ Dfetch (0.11.0) - SomeProjectWithAnInterestingFile: Fetched trunk - 1 + SomeProjectWithAnInterestingFile: + > Fetched trunk - 1 """ And 'MyProject' looks like: """ diff --git a/features/fetch-with-ignore-git.feature b/features/fetch-with-ignore-git.feature index d29404ea..0515be32 100644 --- a/features/fetch-with-ignore-git.feature +++ b/features/fetch-with-ignore-git.feature @@ -29,7 +29,8 @@ Feature: Fetch with ignore in git Then the output shows """ Dfetch (0.11.0) - SomeInterestingProject: Fetched v1 + SomeInterestingProject: + > Fetched v1 """ Then 'MyProject' looks like: """ @@ -58,7 +59,8 @@ Feature: Fetch with ignore in git Then the output shows """ Dfetch (0.11.0) - SomeInterestingProject: Fetched v1 + SomeInterestingProject: + > Fetched v1 """ Then 'MyProject' looks like: """ @@ -90,7 +92,8 @@ Feature: Fetch with ignore in git Then the output shows """ Dfetch (0.11.0) - SomeInterestingProject: Fetched v1 + SomeInterestingProject: + > Fetched v1 """ Then 'MyProject' looks like: """ diff --git a/features/fetch-with-ignore-svn.feature b/features/fetch-with-ignore-svn.feature index 424ef128..e1104d29 100644 --- a/features/fetch-with-ignore-svn.feature +++ b/features/fetch-with-ignore-svn.feature @@ -28,7 +28,8 @@ Feature: Fetch with ignore in svn Then the output shows """ Dfetch (0.11.0) - SomeInterestingProject: Fetched trunk - 1 + SomeInterestingProject: + > Fetched trunk - 1 """ Then 'MyProject' looks like: """ @@ -56,7 +57,8 @@ Feature: Fetch with ignore in svn Then the output shows """ Dfetch (0.11.0) - SomeInterestingProject: Fetched trunk - 1 + SomeInterestingProject: + > Fetched trunk - 1 """ Then 'MyProject' looks like: """ @@ -87,7 +89,8 @@ Feature: Fetch with ignore in svn Then the output shows """ Dfetch (0.11.0) - SomeInterestingProject: Fetched trunk - 1 + SomeInterestingProject: + > Fetched trunk - 1 """ Then 'MyProject' looks like: """ diff --git a/features/guard-against-overwriting-git.feature b/features/guard-against-overwriting-git.feature index 979b61d2..7f147343 100644 --- a/features/guard-against-overwriting-git.feature +++ b/features/guard-against-overwriting-git.feature @@ -24,7 +24,8 @@ Feature: Guard against overwriting in git Then the output shows """ Dfetch (0.11.0) - SomeProject : skipped - local changes after last update (use --force to overwrite) + SomeProject: + > skipped - local changes after last update (use --force to overwrite) """ Scenario: Force flag overrides local changes check @@ -33,7 +34,8 @@ Feature: Guard against overwriting in git Then the output shows """ Dfetch (0.11.0) - SomeProject : Fetched v2 + SomeProject: + > Fetched v2 """ Scenario: Ignored files are overwritten @@ -43,5 +45,6 @@ Feature: Guard against overwriting in git Then the output shows """ Dfetch (0.11.0) - SomeProject : Fetched v2 + SomeProject: + > Fetched v2 """ diff --git a/features/guard-against-overwriting-svn.feature b/features/guard-against-overwriting-svn.feature index f0b1393e..74f4992a 100644 --- a/features/guard-against-overwriting-svn.feature +++ b/features/guard-against-overwriting-svn.feature @@ -24,7 +24,8 @@ Feature: Guard against overwriting in svn Then the output shows """ Dfetch (0.11.0) - SomeProject : skipped - local changes after last update (use --force to overwrite) + SomeProject: + > skipped - local changes after last update (use --force to overwrite) """ Scenario: Force flag overrides local changes check @@ -33,7 +34,8 @@ Feature: Guard against overwriting in svn Then the output shows """ Dfetch (0.11.0) - SomeProject : Fetched v2 + SomeProject: + > Fetched v2 """ Scenario: Ignored files are overwritten @@ -43,5 +45,6 @@ Feature: Guard against overwriting in svn Then the output shows """ Dfetch (0.11.0) - SomeProject : Fetched v2 + SomeProject: + > Fetched v2 """ diff --git a/features/handle-invalid-metadata.feature b/features/handle-invalid-metadata.feature index fae2e679..180bf0db 100644 --- a/features/handle-invalid-metadata.feature +++ b/features/handle-invalid-metadata.feature @@ -23,7 +23,8 @@ Feature: Handle invalid metadata files Then the output shows """ Dfetch (0.11.0) - ext/test-repo-tag/.dfetch_data.yaml is an invalid metadata file, not checking on disk version! - ext/test-repo-tag/.dfetch_data.yaml is an invalid metadata file, not checking local hash! - ext/test-repo-tag : Fetched v1 + ext/test-repo-tag: + > ext/test-repo-tag/.dfetch_data.yaml is an invalid metadata file, not checking on disk version! + > ext/test-repo-tag/.dfetch_data.yaml is an invalid metadata file, not checking local hash! + > Fetched v1 """ diff --git a/features/journey-basic-patching.feature b/features/journey-basic-patching.feature index 10e9e9e8..febfd4e4 100644 --- a/features/journey-basic-patching.feature +++ b/features/journey-basic-patching.feature @@ -56,6 +56,8 @@ Feature: Basic patch journey Then the output shows """ Dfetch (0.11.0) - test-repo : Fetched v1 - test-repo : Applied patch "test-repo.patch" + test-repo: + > Fetched v1 + test-repo: + > Applied patch "test-repo.patch" """ diff --git a/features/journey-basic-usage.feature b/features/journey-basic-usage.feature index 713cee27..fe3154f7 100644 --- a/features/journey-basic-usage.feature +++ b/features/journey-basic-usage.feature @@ -29,7 +29,8 @@ Feature: Basic usage journey Then the output shows """ Dfetch (0.11.0) - ext/test-repo-tag : wanted & current (v1), available (v2.0) + ext/test-repo-tag: + > wanted & current (v1), available (v2.0) """ When the manifest 'dfetch.yaml' is changed to """ diff --git a/features/keep-license-in-project.feature b/features/keep-license-in-project.feature index 7e109067..6359131c 100644 --- a/features/keep-license-in-project.feature +++ b/features/keep-license-in-project.feature @@ -25,11 +25,6 @@ Feature: Keep license in project | SomeOtherFolder/SomeOtherFile.txt | | SomeSubFolder/LICENSE | When I run "dfetch update" - Then the output shows - """ - Dfetch (0.11.0) - SomeProjectWithLicense: Fetched v1 - """ Then 'MyProject' looks like: """ MyProject/ @@ -58,11 +53,6 @@ Feature: Keep license in project | SomeFolder/SomeFile.txt | | SomeOtherFolder/SomeOtherFile.txt | When I run "dfetch update" - Then the output shows - """ - Dfetch (0.11.0) - SomeProjectWithLicense: Fetched trunk - 1 - """ Then 'MyProject' looks like: """ MyProject/ @@ -90,11 +80,6 @@ Feature: Keep license in project | SomeFolder/SomeFile.txt | | SomeOtherFolder/SomeOtherFile.txt | When I run "dfetch update" - Then the output shows - """ - Dfetch (0.11.0) - SomeProjectWithLicense: Fetched trunk - 1 - """ Then 'MyProject' looks like: """ MyProject/ diff --git a/features/patch-after-fetch-git.feature b/features/patch-after-fetch-git.feature index 81a1fbdb..e57ffc6c 100644 --- a/features/patch-after-fetch-git.feature +++ b/features/patch-after-fetch-git.feature @@ -68,8 +68,10 @@ Feature: Patch after fetching from git repo Then the output shows """ Dfetch (0.11.0) - ext/test-repo-tag : Fetched v2.0 - source/target file does not exist: + ext/test-repo-tag: + > Fetched v2.0 + > Applying patch "diff.patch" + source/target file does not exist: --- b'README1.md' +++ b'README1.md' Applying patch "diff.patch" failed @@ -124,11 +126,12 @@ Feature: Patch after fetching from git repo And the output shows """ Dfetch (0.11.0) - ext/test-repo-tag : Fetched v2.0 - successfully patched 1/1: b'README.md' - ext/test-repo-tag : Applied patch "001-diff.patch" - successfully patched 1/1: b'README.md' - ext/test-repo-tag : Applied patch "002-diff.patch" + ext/test-repo-tag: + > Fetched v2.0 + > Applying patch "001-diff.patch" + successfully patched 1/1: b'README.md' + > Applying patch "002-diff.patch" + successfully patched 1/1: b'README.md' """ Scenario: Fallback to other file encodings if patch file is not UTF-8 encoded @@ -167,11 +170,12 @@ Feature: Patch after fetching from git repo And the output shows """ Dfetch (0.11.0) - ext/test-repo-tag : Fetched v2.0 - error: no patch data found! - After retrying found that patch-file "diff.patch" is not UTF-8 encoded, consider saving it with UTF-8 encoding. - successfully patched 1/1: b'README.md' - ext/test-repo-tag : Applied patch "diff.patch" + ext/test-repo-tag: + > Fetched v2.0 + > Applying patch "diff.patch" + error: no patch data found! + After retrying found that patch-file "diff.patch" is not UTF-8 encoded, consider saving it with UTF-8 encoding. + successfully patched 1/1: b'README.md' """ Scenario: Patch files are outside manifest dir @@ -194,6 +198,7 @@ Feature: Patch after fetching from git repo Then the output shows """ Dfetch (0.11.0) - ext/test-repo-tag : Fetched v2.0 - ext/test-repo-tag : Skipping patch "../diff.patch" which is outside /some/path. + ext/test-repo-tag: + > Fetched v2.0 + > Skipping patch "../diff.patch" which is outside /some/path. """ diff --git a/features/steps/generic_steps.py b/features/steps/generic_steps.py index 24737fee..4c3c2d0e 100644 --- a/features/steps/generic_steps.py +++ b/features/steps/generic_steps.py @@ -8,6 +8,7 @@ import os import pathlib import re +from contextlib import contextmanager from itertools import zip_longest from typing import Iterable, List, Optional, Pattern, Tuple, Union @@ -15,9 +16,10 @@ from behave.runner import Context from dfetch.__main__ import DfetchFatalException, run +from dfetch.log import DLogger from dfetch.util.util import in_directory -ansi_escape = re.compile(r"\x1b(?:[@A-Z\\-_]|\[[0-9:;<=>?]*[ -/]*[@-~])") +ansi_escape = re.compile(r"\[/?[a-z\_ ]+\]") dfetch_title = re.compile(r"Dfetch \(\d+.\d+.\d+\)") timestamp = re.compile(r"\d+\/\d+\/\d+, \d+:\d+:\d+") git_hash = re.compile(r"(\s?)[a-f0-9]{40}(\s?)") @@ -28,23 +30,40 @@ abs_path = re.compile(r"/tmp/[\w_]+") +@contextmanager +def temporary_env(key: str, value: str): + """Temporarily set an environment variable inside a context.""" + old_value = os.environ.get(key) + os.environ[key] = value + try: + yield + finally: + if old_value is None: + del os.environ[key] + else: + os.environ[key] = old_value + + def remote_server_path(context): """Get the path to the remote dir.""" return "/".join(context.remotes_dir_path.split(os.sep)) def call_command(context: Context, args: list[str], path: Optional[str] = ".") -> None: - length_at_start = len(context.captured.output) - with in_directory(path or "."): - try: - run(args) - context.cmd_returncode = 0 - except DfetchFatalException: - context.cmd_returncode = 1 - # Remove the color code + title - context.cmd_output = dfetch_title.sub( - "", ansi_escape.sub("", context.captured.output[length_at_start:].strip("\n")) - ) + before = context.console.export_text() + + DLogger.reset_projects() + + with temporary_env("CI", "true"): + with in_directory(path or "."): + try: + run(args, context.console) + context.cmd_returncode = 0 + except DfetchFatalException: + context.cmd_returncode = 1 + + after = context.console.export_text() + context.cmd_output = after[len(before) :].strip("\n") def check_file(path, content): @@ -81,7 +100,7 @@ def check_content( ): expected = multisub( patterns=[ - (git_hash, r"\1[commit hash]\2"), + (git_hash, r"\1[commit-hash]\2"), (iso_timestamp, "[timestamp]"), (urn_uuid, "[urn-uuid]"), (bom_ref, "[bom-ref]"), @@ -91,7 +110,7 @@ def check_content( actual = multisub( patterns=[ - (git_hash, r"\1[commit hash]\2"), + (git_hash, r"\1[commit-hash]\2"), (iso_timestamp, "[timestamp]"), (urn_uuid, "[urn-uuid]"), (bom_ref, "[bom-ref]"), @@ -155,6 +174,12 @@ def list_dir(path): return result +def normalize_lines(text: str) -> list[str]: + """Normalize text for diffing.""" + lines = text.splitlines() + return [line.rstrip() for line in lines if line.strip() != ""] + + def check_output(context, line_count=None): """Check command output against expected text. @@ -164,9 +189,9 @@ def check_output(context, line_count=None): """ expected_text = multisub( patterns=[ - (git_hash, r"\1[commit hash]\2"), + (git_hash, r"\1[commit-hash]\2"), (timestamp, "[timestamp]"), - (dfetch_title, ""), + (ansi_escape, ""), (svn_error, "svn: EXXXXXX: "), ], text=context.text, @@ -174,7 +199,7 @@ def check_output(context, line_count=None): actual_text = multisub( patterns=[ - (git_hash, r"\1[commit hash]\2"), + (git_hash, r"\1[commit-hash]\2"), (timestamp, "[timestamp]"), (ansi_escape, ""), ( @@ -187,8 +212,10 @@ def check_output(context, line_count=None): text=context.cmd_output, ) - actual_lines = actual_text.splitlines()[:line_count] - diff = difflib.ndiff(actual_lines, expected_text.splitlines()) + actual_lines = normalize_lines(actual_text)[:line_count] + expected_lines = normalize_lines(expected_text) + + diff = difflib.ndiff(actual_lines, expected_lines) diffs = [x for x in diff if x[0] in ("+", "-")] if diffs: diff --git a/features/updated-project-has-dependencies.feature b/features/updated-project-has-dependencies.feature index 77e7c456..67b7a28c 100644 --- a/features/updated-project-has-dependencies.feature +++ b/features/updated-project-has-dependencies.feature @@ -4,6 +4,7 @@ Feature: Updated project has dependencies manifest. *Dfetch* should recommend the user to add these dependencies to the manifest. + @wip Scenario: Git projects are specified in the manifest Given the manifest 'dfetch.yaml' in MyProject """ @@ -41,19 +42,18 @@ Feature: Updated project has dependencies Then the output shows """ Dfetch (0.11.0) - SomeProjectWithChild: Fetched v1 + SomeProjectWithChild: + > Fetched v1 + > "SomeProjectWithChild" depends on the following project(s) which are not part of your manifest: (found in third-party/SomeProjectWithChild/dfetch.yaml) + - name: SomeOtherProject + url: some-remote-server/SomeOtherProject.git + tag: v1 + - name: ext/test-repo-tag-v1 + url: https://github.com/dfetch-org/test-repo + tag: v1 - "SomeProjectWithChild" depends on the following project(s) which are not part of your manifest: - (found in third-party/SomeProjectWithChild/dfetch.yaml) - - - name: SomeOtherProject - url: some-remote-server/SomeOtherProject.git - tag: v1 - - name: ext/test-repo-tag-v1 - url: https://github.com/dfetch-org/test-repo - tag: v1 - - SomeProjectWithoutChild: Fetched v1 + SomeProjectWithoutChild: + > Fetched v1 """ And 'MyProject' looks like: """ @@ -88,7 +88,8 @@ Feature: Updated project has dependencies Then the output shows """ Dfetch (0.11.0) - SomeProject : Fetched v1 + SomeProject: + > Fetched v1 SomeProject/dfetch.yaml: Schema validation failed: "very-invalid-manifest\n" diff --git a/pyproject.toml b/pyproject.toml index 4f7fc379..be3cfab7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,10 +40,8 @@ classifiers = [ ] dependencies = [ "PyYAML==6.0.3", - "coloredlogs==15.0.1", "strictyaml==1.7.3", - "halo==0.0.31", - "colorama==0.4.6", + "rich==14.2.0", "typing-extensions==4.15.0", "tldextract==5.3.0", "sarif-om==1.0.4", diff --git a/tests/test_report.py b/tests/test_report.py index 86ea4d69..466f77d6 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -34,12 +34,12 @@ def test_report(name, projects): fake_superproject.root_directory = Path("/tmp") with patch("dfetch.commands.report.SuperProject", return_value=fake_superproject): - with patch("dfetch.log.DLogger.print_info_line") as mocked_print_info_line: + with patch("dfetch.log.DLogger.print_report_line") as mocked_print_report_line: report(DEFAULT_ARGS) if projects: for project in projects: - mocked_print_info_line.assert_any_call("project", project["name"]) + mocked_print_report_line.assert_any_call("project", project["name"]) else: - mocked_print_info_line.assert_not_called() + mocked_print_report_line.assert_not_called()