From 7021e8abc275cc32020d9efcf0518c0b020f8f17 Mon Sep 17 00:00:00 2001 From: Sergio Valbuena Date: Tue, 29 Jul 2025 13:51:56 +0200 Subject: [PATCH 1/4] Log python version --- fancylog/fancylog.py | 19 ++++++++++++++++++ tests/tests/test_general.py | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/fancylog/fancylog.py b/fancylog/fancylog.py index 3686ae7..4301392 100644 --- a/fancylog/fancylog.py +++ b/fancylog/fancylog.py @@ -28,6 +28,7 @@ def start_logging( write_header=True, write_git=True, write_cli_args=True, + write_python_version=True, write_variables=True, log_to_file=True, log_to_console=True, @@ -62,6 +63,8 @@ def start_logging( Write information about the git repository. Default: True write_cli_args Log the command-line arguments. Default: True + write_python_version + Log the Python version. Default: True write_variables Write the attributes of selected objects. Default: True log_to_file @@ -105,6 +108,7 @@ def start_logging( write_header=write_header, write_git=write_git, write_cli_args=write_cli_args, + write_python_version=write_python_version, write_variables=write_variables, log_header=log_header, ) @@ -132,6 +136,7 @@ def __init__( write_header=True, write_git=True, write_cli_args=True, + write_python_version=True, write_variables=True, log_header=None, ): @@ -148,6 +153,8 @@ def __init__( self.write_git_info(self.program.__name__) if write_cli_args: self.write_command_line_arguments() + if write_python_version: + self.write_python_version() if write_variables and variable_objects: self.write_variables(variable_objects) @@ -206,6 +213,18 @@ def write_command_line_arguments(self, header="COMMAND LINE ARGUMENTS"): self.file.write(f"Command: {sys.argv[0]} \n") self.file.write(f"Input arguments: {sys.argv[1:]}") + def write_python_version(self, header="PYTHON VERSION"): + """Write the Python version used to run the script. + + Parameters + ---------- + header + Title of the section that will be written to the log file. + + """ + self.write_separated_section_header(header) + self.file.write(f"Python version: {sys.version.split()[0]}") + def write_variables(self, variable_objects): """Write a section for variables with their values. diff --git a/tests/tests/test_general.py b/tests/tests/test_general.py index f850afc..08a89b1 100644 --- a/tests/tests/test_general.py +++ b/tests/tests/test_general.py @@ -1,4 +1,5 @@ import logging +import platform import pytest from rich.logging import RichHandler @@ -206,3 +207,41 @@ def test_named_logger_doesnt_propagate(tmp_path, capsys): assert "PQ&*" not in captured.out, ( "logger writing to stdout through root handler" ) + + +def test_correct_python_version_header(tmp_path): + """Python version section should exist only if + logger is created with the parameter write_python_version as True. + """ + + ver_header = f"{lateral_separator} PYTHON VERSION {lateral_separator}\n" + + fancylog.start_logging(tmp_path, fancylog, write_python_version=False) + + log_file = next(tmp_path.glob("*.log")) + + # Test header missing when write_python_version set to False + with open(log_file) as file: + assert ver_header not in file.read() + + fancylog.start_logging(tmp_path, fancylog, write_python_version=True) + + log_file = next(tmp_path.glob("*.log")) + + # Test header present when write_python_version set to True + with open(log_file) as file: + assert ver_header in file.read() + + +def test_correct_python_version_logged(tmp_path): + """Python version logged should be equal to + the output of platform.python_version(). + """ + + fancylog.start_logging(tmp_path, fancylog, write_python_version=True) + + log_file = next(tmp_path.glob("*.log")) + + # Test logged python version is equal to platform.python_version() + with open(log_file) as file: + assert f"Python version: {platform.python_version()}" in file.read() From 47d2c7c65d0c62c18519c5a29135788bd6113e2f Mon Sep 17 00:00:00 2001 From: Sergio Valbuena Date: Thu, 31 Jul 2025 18:17:11 +0200 Subject: [PATCH 2/4] Log environment info --- fancylog/fancylog.py | 94 +++++++++++++++++++++++++++++++++-- tests/tests/test_general.py | 98 +++++++++++++++++++++++++++++++++---- 2 files changed, 178 insertions(+), 14 deletions(-) diff --git a/fancylog/fancylog.py b/fancylog/fancylog.py index 4301392..06f8fac 100644 --- a/fancylog/fancylog.py +++ b/fancylog/fancylog.py @@ -1,8 +1,10 @@ """Wrapper around the standard logging module, with additional information.""" import contextlib +import json import logging import os +import subprocess import sys from datetime import datetime from importlib.util import find_spec @@ -28,7 +30,8 @@ def start_logging( write_header=True, write_git=True, write_cli_args=True, - write_python_version=True, + write_python=True, + write_env_packages=True, write_variables=True, log_to_file=True, log_to_console=True, @@ -63,8 +66,10 @@ def start_logging( Write information about the git repository. Default: True write_cli_args Log the command-line arguments. Default: True - write_python_version + write_python Log the Python version. Default: True + write_env_packages + Log the packages in the conda/pip environment. Default: True write_variables Write the attributes of selected objects. Default: True log_to_file @@ -108,7 +113,8 @@ def start_logging( write_header=write_header, write_git=write_git, write_cli_args=write_cli_args, - write_python_version=write_python_version, + write_python=write_python, + write_env_packages=write_env_packages, write_variables=write_variables, log_header=log_header, ) @@ -136,7 +142,8 @@ def __init__( write_header=True, write_git=True, write_cli_args=True, - write_python_version=True, + write_python=True, + write_env_packages=True, write_variables=True, log_header=None, ): @@ -153,8 +160,10 @@ def __init__( self.write_git_info(self.program.__name__) if write_cli_args: self.write_command_line_arguments() - if write_python_version: + if write_python: self.write_python_version() + if write_env_packages: + self.write_environment_packages() if write_variables and variable_objects: self.write_variables(variable_objects) @@ -225,6 +234,81 @@ def write_python_version(self, header="PYTHON VERSION"): self.write_separated_section_header(header) self.file.write(f"Python version: {sys.version.split()[0]}") + def write_environment_packages(self, header="ENVIRONMENT"): + """Write the local/global environment packages used to run the script. + + Attempt to collect conda packages and, if this fails, + collect pip packages. + + Parameters + ---------- + header + Title of the section that will be written to the log file + + """ + self.write_separated_section_header(header) + + # Attempt to log conda env name and packages + try: + conda_env = os.environ["CONDA_PREFIX"].split(os.sep)[-1] + conda_exe = os.environ["CONDA_EXE"] + conda_list = subprocess.run( + [conda_exe, "list", "--json"], capture_output=True, text=True + ) + + conda_pkgs = json.loads(conda_list.stdout) + + self.file.write(f"Conda environment: {conda_env}\n\n") + self.file.write("Environment packages (conda):\n") + self.file.write(f"{'Name':20} {'Version':15}\n") + for pkg in conda_pkgs: + self.file.write(f"{pkg['name']:20} {pkg['version']:15}\n") + + # If no conda env, fall back to logging pip + except KeyError: + # Log local-available packages first + python_executable = sys.executable + local_pip_list = subprocess.run( + [ + python_executable, + "-m", + "pip", + "list", + "--local", + "--format=json", + ], + capture_output=True, + text=True, + ) + + local_env_pkgs = json.loads(local_pip_list.stdout) + local_env_pkgs_names = [] + + self.file.write( + "No conda environment found, reporting pip packages\n\n" + ) + self.file.write("Local environment packages (pip):\n") + self.file.write(f"{'Name':20} {'Version':15}\n") + for pkg in local_env_pkgs: + local_env_pkgs_names.append(pkg["name"]) + self.file.write(f"{pkg['name']:20} {pkg['version']:15}\n") + self.file.write("\n") + + # Log global-available packages (if any) + global_pip_list = subprocess.run( + [python_executable, "-m", "pip", "list", "--format=json"], + capture_output=True, + text=True, + ) + + global_env_pkgs = json.loads(global_pip_list.stdout) + + self.file.write("Global environment packages (pip):\n") + self.file.write(f"{'Name':20} {'Version':15}\n") + for pkg in global_env_pkgs: + if pkg["name"] not in local_env_pkgs_names: + self.file.write(f"{pkg['name']:20} {pkg['version']:15}\n") + def write_variables(self, variable_objects): """Write a section for variables with their values. diff --git a/tests/tests/test_general.py b/tests/tests/test_general.py index 08a89b1..d3c0f2b 100644 --- a/tests/tests/test_general.py +++ b/tests/tests/test_general.py @@ -1,5 +1,10 @@ +import json import logging +import os import platform +import subprocess +import sys +from importlib.metadata import distributions import pytest from rich.logging import RichHandler @@ -209,26 +214,32 @@ def test_named_logger_doesnt_propagate(tmp_path, capsys): ) -def test_correct_python_version_header(tmp_path): - """Python version section should exist only if - logger is created with the parameter write_python_version as True. +def test_python_version_header_absent(tmp_path): + """Python version section does not exist if logger + is created with the parameter write_python_version as False. """ - ver_header = f"{lateral_separator} PYTHON VERSION {lateral_separator}\n" - fancylog.start_logging(tmp_path, fancylog, write_python_version=False) + fancylog.start_logging(tmp_path, fancylog, write_python=False) log_file = next(tmp_path.glob("*.log")) - # Test header missing when write_python_version set to False + # Test header missing when write_python set to False with open(log_file) as file: assert ver_header not in file.read() - fancylog.start_logging(tmp_path, fancylog, write_python_version=True) + +def test_python_version_header_present(tmp_path): + """Python version section exists if logger + is created with the parameter write_python_version as True. + """ + ver_header = f"{lateral_separator} PYTHON VERSION {lateral_separator}\n" + + fancylog.start_logging(tmp_path, fancylog, write_python=True) log_file = next(tmp_path.glob("*.log")) - # Test header present when write_python_version set to True + # Test header present when write_python set to True with open(log_file) as file: assert ver_header in file.read() @@ -238,10 +249,79 @@ def test_correct_python_version_logged(tmp_path): the output of platform.python_version(). """ - fancylog.start_logging(tmp_path, fancylog, write_python_version=True) + fancylog.start_logging(tmp_path, fancylog, write_python=True) log_file = next(tmp_path.glob("*.log")) # Test logged python version is equal to platform.python_version() with open(log_file) as file: assert f"Python version: {platform.python_version()}" in file.read() + + +def test_environment_header_absent(tmp_path): + """Environment section does not exist if logger + is created with the parameter write_env_packages as False. + """ + env_header = f"{lateral_separator} ENVIRONMENT {lateral_separator}\n" + + fancylog.start_logging(tmp_path, fancylog, write_env_packages=False) + + log_file = next(tmp_path.glob("*.log")) + + # Test header missing when write_env_packages set to False + with open(log_file) as file: + assert env_header not in file.read() + + +def test_environment_header_present(tmp_path): + """Environment section exists if logger + is created with the parameter write_env_packages as True. + """ + env_header = f"{lateral_separator} ENVIRONMENT {lateral_separator}\n" + + fancylog.start_logging(tmp_path, fancylog, write_env_packages=True) + + log_file = next(tmp_path.glob("*.log")) + + # Test header present when write_env_packages set to True + with open(log_file) as file: + assert env_header in file.read() + + +def test_correct_pkg_version_logged(tmp_path): + """Package versions logged should be equal to + the output of `conda list` or `pip list`. + """ + fancylog.start_logging(tmp_path, fancylog, write_env_packages=True) + + log_file = next(tmp_path.glob("*.log")) + + try: + # If there is a conda environment, assert that the correct + # version is logged for all pkgs + conda_exe = os.environ["CONDA_EXE"] + conda_list = subprocess.run( + [conda_exe, "list", "--json"], capture_output=True, text=True + ) + + conda_pkgs = json.loads(conda_list.stdout) + for pkg in conda_pkgs: + assert f"{pkg['name']:20} {pkg['version']:15}\n" + + except KeyError: + # If there is no conda environment, assert that the correct + # version is logged for all packages logged with pip list + with open(log_file) as file: + file_content = file.read() + + # Test local environment versions + local_site_packages = next( + p for p in sys.path if "site-packages" in p + ) + + for dist in distributions(): + if str(dist.locate_file("")).startswith(local_site_packages): + assert ( + f"{dist.metadata['Name']:20} {dist.version}" + in file_content + ) From efb12fe71677b20b94fefb99f19f478976d3771b Mon Sep 17 00:00:00 2001 From: Sergio Valbuena Date: Tue, 5 Aug 2025 17:24:31 +0200 Subject: [PATCH 3/4] Parametrize header tests, add tests mocking environment, remove duplication when logging pkgs Add tests mocking environment, remove duplication writing packages --- fancylog/fancylog.py | 76 +++++++++++-------- tests/tests/test_general.py | 145 +++++++++++++++++++++++++----------- 2 files changed, 144 insertions(+), 77 deletions(-) diff --git a/fancylog/fancylog.py b/fancylog/fancylog.py index 06f8fac..7061c06 100644 --- a/fancylog/fancylog.py +++ b/fancylog/fancylog.py @@ -69,7 +69,7 @@ def start_logging( write_python Log the Python version. Default: True write_env_packages - Log the packages in the conda/pip environment. Default: True + Log the packages in the environment. Default: True write_variables Write the attributes of selected objects. Default: True log_to_file @@ -256,58 +256,70 @@ def write_environment_packages(self, header="ENVIRONMENT"): [conda_exe, "list", "--json"], capture_output=True, text=True ) - conda_pkgs = json.loads(conda_list.stdout) + env_pkgs = json.loads(conda_list.stdout) self.file.write(f"Conda environment: {conda_env}\n\n") self.file.write("Environment packages (conda):\n") - self.file.write(f"{'Name':20} {'Version':15}\n") - for pkg in conda_pkgs: - self.file.write(f"{pkg['name']:20} {pkg['version']:15}\n") + self.write_packages(env_pkgs) # If no conda env, fall back to logging pip except KeyError: - # Log local-available packages first python_executable = sys.executable - local_pip_list = subprocess.run( + pip_list = subprocess.run( [ python_executable, "-m", "pip", "list", - "--local", + "--verbose", "--format=json", ], capture_output=True, text=True, ) - local_env_pkgs = json.loads(local_pip_list.stdout) - local_env_pkgs_names = [] + all_pkgs = json.loads(pip_list.stdout) - self.file.write( - "No conda environment found, reporting pip packages\n\n" - ) - self.file.write("Local environment packages (pip):\n") - self.file.write(f"{'Name':20} {'Version':15}\n") - for pkg in local_env_pkgs: - local_env_pkgs_names.append(pkg["name"]) - self.file.write(f"{pkg['name']:20} {pkg['version']:15}\n") - self.file.write("\n") - - # Log global-available packages (if any) - global_pip_list = subprocess.run( - [python_executable, "-m", "pip", "list", "--format=json"], - capture_output=True, - text=True, - ) + try: + # If there is a local env, log local packages first + env_pkgs = [ + pkg + for pkg in all_pkgs + if os.getenv("VIRTUAL_ENV") in pkg["location"] + ] + + self.file.write( + "No conda environment found, reporting pip packages\n\n" + ) + self.file.write("Local environment packages (pip):\n") + self.write_packages(env_pkgs) + self.file.write("\n") + + # Log global-available packages (if any) + global_pkgs = [pkg for pkg in all_pkgs if pkg not in env_pkgs] - global_env_pkgs = json.loads(global_pip_list.stdout) + self.file.write("Global environment packages (pip):\n") + self.write_packages(global_pkgs) - self.file.write("Global environment packages (pip):\n") - self.file.write(f"{'Name':20} {'Version':15}\n") - for pkg in global_env_pkgs: - if pkg["name"] not in local_env_pkgs_names: - self.file.write(f"{pkg['name']:20} {pkg['version']:15}\n") + except TypeError: + self.file.write( + "No environment found, reporting global pip packages\n\n" + ) + self.write_packages(all_pkgs) + + def write_packages(self, env_pkgs): + """Write the packages in the local environment. + + Parameters + ---------- + env_pkgs + A dictionary of environment packages, the name and version + of which will be written. + + """ + self.file.write(f"{'Name':20} {'Version':15}\n") + for pkg in env_pkgs: + self.file.write(f"{pkg['name']:20} {pkg['version']:15}\n") def write_variables(self, variable_objects): """Write a section for variables with their values. diff --git a/tests/tests/test_general.py b/tests/tests/test_general.py index d3c0f2b..27698f2 100644 --- a/tests/tests/test_general.py +++ b/tests/tests/test_general.py @@ -5,6 +5,7 @@ import subprocess import sys from importlib.metadata import distributions +from unittest.mock import MagicMock, patch import pytest from rich.logging import RichHandler @@ -214,34 +215,16 @@ def test_named_logger_doesnt_propagate(tmp_path, capsys): ) -def test_python_version_header_absent(tmp_path): - """Python version section does not exist if logger - is created with the parameter write_python_version as False. - """ +@pytest.mark.parametrize("boolean, operator", [(True, True), (False, False)]) +def test_python_version_header(boolean, operator, tmp_path): ver_header = f"{lateral_separator} PYTHON VERSION {lateral_separator}\n" - fancylog.start_logging(tmp_path, fancylog, write_python=False) + fancylog.start_logging(tmp_path, fancylog, write_python=boolean) log_file = next(tmp_path.glob("*.log")) - # Test header missing when write_python set to False with open(log_file) as file: - assert ver_header not in file.read() - - -def test_python_version_header_present(tmp_path): - """Python version section exists if logger - is created with the parameter write_python_version as True. - """ - ver_header = f"{lateral_separator} PYTHON VERSION {lateral_separator}\n" - - fancylog.start_logging(tmp_path, fancylog, write_python=True) - - log_file = next(tmp_path.glob("*.log")) - - # Test header present when write_python set to True - with open(log_file) as file: - assert ver_header in file.read() + assert (ver_header in file.read()) == operator def test_correct_python_version_logged(tmp_path): @@ -258,34 +241,16 @@ def test_correct_python_version_logged(tmp_path): assert f"Python version: {platform.python_version()}" in file.read() -def test_environment_header_absent(tmp_path): - """Environment section does not exist if logger - is created with the parameter write_env_packages as False. - """ - env_header = f"{lateral_separator} ENVIRONMENT {lateral_separator}\n" +@pytest.mark.parametrize("boolean, operator", [(True, True), (False, False)]) +def test_environment_header(boolean, operator, tmp_path): + ver_header = f"{lateral_separator} ENVIRONMENT {lateral_separator}\n" - fancylog.start_logging(tmp_path, fancylog, write_env_packages=False) + fancylog.start_logging(tmp_path, fancylog, write_env_packages=boolean) log_file = next(tmp_path.glob("*.log")) - # Test header missing when write_env_packages set to False with open(log_file) as file: - assert env_header not in file.read() - - -def test_environment_header_present(tmp_path): - """Environment section exists if logger - is created with the parameter write_env_packages as True. - """ - env_header = f"{lateral_separator} ENVIRONMENT {lateral_separator}\n" - - fancylog.start_logging(tmp_path, fancylog, write_env_packages=True) - - log_file = next(tmp_path.glob("*.log")) - - # Test header present when write_env_packages set to True - with open(log_file) as file: - assert env_header in file.read() + assert (ver_header in file.read()) == operator def test_correct_pkg_version_logged(tmp_path): @@ -325,3 +290,93 @@ def test_correct_pkg_version_logged(tmp_path): f"{dist.metadata['Name']:20} {dist.version}" in file_content ) + + +def test_mock_pip_pkgs(tmp_path): + """Mock pip list subprocess + and test that packages are logged correctly. + """ + + # Simulated `conda list --json` output + fake_pip_output = json.dumps( + [ + {"name": "fancylog", "version": "1.1.1", "location": "fake_env"}, + {"name": "pytest", "version": "1.1.1", "location": "global_env"}, + ] + ) + + # Patch the environment and subprocess + with ( + patch.dict(os.environ, {}, clear=False), + patch("os.getenv") as mock_getenv, + patch("subprocess.run") as mock_run, + ): + # Eliminate conda environment packages triggers logging pip list + (os.environ.pop("CONDA_PREFIX", None),) + os.environ.pop("CONDA_EXE", None) + + mock_getenv.return_value = "fake_env" + + # Mocked subprocess result + mock_run.return_value = MagicMock(stdout=fake_pip_output, returncode=0) + + fancylog.start_logging(tmp_path, fancylog, write_env_packages=True) + + log_file = next(tmp_path.glob("*.log")) + + # Log contains conda subheaders and mocked pkgs versions + with open(log_file) as file: + file_content = file.read() + + assert ( + "No conda environment found, reporting pip packages" + ) in file_content + + assert f"{'fancylog':20} {'1.1.1'}" + assert f"{'pytest':20} {'1.1.1'}" + + +def test_mock_conda_pkgs(tmp_path): + """Mock conda environment variables + and test that packages are logged correctly. + """ + + fake_conda_env_name = "test_env" + fake_conda_prefix = os.path.join( + "path", "conda", "envs", fake_conda_env_name + ) + fake_conda_exe = os.path.join("fake", "conda") + + # Simulated `conda list --json` output + fake_conda_output = json.dumps( + [ + {"name": "fancylog", "version": "1.1.1"}, + {"name": "pytest", "version": "1.1.1"}, + ] + ) + + # Patch the environment and subprocess + with ( + patch.dict( + os.environ, + {"CONDA_PREFIX": fake_conda_prefix, "CONDA_EXE": fake_conda_exe}, + ), + patch("subprocess.run") as mock_run, + ): + # Mocked subprocess result + mock_run.return_value = MagicMock( + stdout=fake_conda_output, returncode=0 + ) + + fancylog.start_logging(tmp_path, fancylog, write_env_packages=True) + + log_file = next(tmp_path.glob("*.log")) + + # Log contains conda subheaders and mocked pkgs versions + with open(log_file) as file: + file_content = file.read() + + assert "Conda environment:" in file_content + assert "Environment packages (conda):" in file_content + assert f"{'fancylog':20} {'1.1.1'}" + assert f"{'pytest':20} {'1.1.1'}" From fbce37dea2295b7de1ebb893bb464482c2101395 Mon Sep 17 00:00:00 2001 From: Sergio Valbuena Date: Thu, 7 Aug 2025 14:47:18 +0200 Subject: [PATCH 4/4] Add test to mock lack of any local environment --- tests/tests/test_general.py | 49 +++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/tests/tests/test_general.py b/tests/tests/test_general.py index 27698f2..3efcff6 100644 --- a/tests/tests/test_general.py +++ b/tests/tests/test_general.py @@ -297,7 +297,7 @@ def test_mock_pip_pkgs(tmp_path): and test that packages are logged correctly. """ - # Simulated `conda list --json` output + # Simulated `pip list --json` output fake_pip_output = json.dumps( [ {"name": "fancylog", "version": "1.1.1", "location": "fake_env"}, @@ -312,7 +312,7 @@ def test_mock_pip_pkgs(tmp_path): patch("subprocess.run") as mock_run, ): # Eliminate conda environment packages triggers logging pip list - (os.environ.pop("CONDA_PREFIX", None),) + os.environ.pop("CONDA_PREFIX", None) os.environ.pop("CONDA_EXE", None) mock_getenv.return_value = "fake_env" @@ -380,3 +380,48 @@ def test_mock_conda_pkgs(tmp_path): assert "Environment packages (conda):" in file_content assert f"{'fancylog':20} {'1.1.1'}" assert f"{'pytest':20} {'1.1.1'}" + + +def test_mock_no_environment(tmp_path): + """Mock lack of any environment, + and test that packages are logged correctly. + """ + + # Simulated `pip list --json` output + fake_pip_output = json.dumps( + [ + {"name": "fancylog", "version": "1.1.1", "location": "fake_env"}, + {"name": "pytest", "version": "1.1.1", "location": "global_env"}, + ] + ) + + # Patch the environment and subprocess + with ( + patch.dict(os.environ, {}, clear=False), + patch("os.getenv") as mock_getenv, + patch("subprocess.run") as mock_run, + ): + # Eliminate conda environment packages triggers logging pip list + os.environ.pop("CONDA_PREFIX", None) + os.environ.pop("CONDA_EXE", None) + + # Mock lack of any local environment + mock_getenv.return_value = None + + # Mocked subprocess result + mock_run.return_value = MagicMock(stdout=fake_pip_output, returncode=0) + + fancylog.start_logging(tmp_path, fancylog, write_env_packages=True) + + log_file = next(tmp_path.glob("*.log")) + + # Log contains conda subheaders and mocked pkgs versions + with open(log_file) as file: + file_content = file.read() + + assert ( + "No environment found, reporting global pip packages" + ) in file_content + + assert f"{'fancylog':20} {'1.1.1'}" + assert f"{'pytest':20} {'1.1.1'}"