From ecd67c1f46f8363024cb274141dc72fb9760330b Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 1 Dec 2023 18:11:03 +0100 Subject: [PATCH 1/7] Apply manageprojects updates --- .github/workflows/tests.yml | 2 +- .gitignore | 4 ++-- README.md | 8 +++++--- cli.py | 2 +- dev-cli.py | 2 +- pyproject.toml | 6 +++++- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3fc65be..8e33dff 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.10", "3.9"] + python-version: ["3.12", "3.11", "3.10", "3.9"] env: PYTHONUNBUFFERED: 1 PYTHONWARNINGS: always diff --git a/.gitignore b/.gitignore index 7ce4683..94a4547 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,8 @@ *.egg-info __pycache__ /dist/ -/coverage.json -/coverage.xml +/coverage.* +*.orig !.github !.editorconfig diff --git a/README.md b/README.md index 10a8540..48684aa 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ To make a new release, do this: [comment]: <> (✂✂✂ auto generated history start ✂✂✂) +* [**dev**](https://github.com/jedie/cli-base-utilities/compare/v0.4.5...main) + * 2023-12-01 - Apply manageprojects updates * [v0.4.5](https://github.com/jedie/cli-base-utilities/compare/v0.4.4...v0.4.5) * 2023-11-30 - Configure unittests via "load_tests Protocol" hook * 2023-11-30 - Update requirements and add "flake8-bugbear" @@ -103,12 +105,12 @@ To make a new release, do this: * 2023-11-01 - Bugfix "AssertionError: Expected only one line" in Git.first_commit_info() * [v0.4.3](https://github.com/jedie/cli-base-utilities/compare/v0.4.2...v0.4.3) * 2023-11-01 - Git history renderer: Collapse older entries -* [v0.4.2](https://github.com/jedie/cli-base-utilities/compare/v0.4.1...v0.4.2) - * 2023-11-01 - Remove duplicate git commits and keep only test last one, e.g.: "update requirements" - * 2023-11-01 - Bugfix git history: Add commits before the first tag
Expand older history entries ... +* [v0.4.2](https://github.com/jedie/cli-base-utilities/compare/v0.4.1...v0.4.2) + * 2023-11-01 - Remove duplicate git commits and keep only test last one, e.g.: "update requirements" + * 2023-11-01 - Bugfix git history: Add commits before the first tag * [v0.4.1](https://github.com/jedie/cli-base-utilities/compare/v0.4.0...v0.4.1) * 2023-10-08 - Remove commit URLs from history and handle release a new version * 2023-10-08 - NEW: Generate a project history base on git commits/tags. diff --git a/cli.py b/cli.py index 140aadd..8150af2 100755 --- a/cli.py +++ b/cli.py @@ -106,7 +106,7 @@ def main(argv): # Call our entry point CLI: try: - verbose_check_call(PROJECT_SHELL_SCRIPT, *sys.argv[1:]) + verbose_check_call(PROJECT_SHELL_SCRIPT, *argv[1:]) except subprocess.CalledProcessError as err: sys.exit(err.returncode) diff --git a/dev-cli.py b/dev-cli.py index 39dfdb2..fef5ec9 100755 --- a/dev-cli.py +++ b/dev-cli.py @@ -106,7 +106,7 @@ def main(argv): # Call our entry point CLI: try: - verbose_check_call(PROJECT_SHELL_SCRIPT, *sys.argv[1:]) + verbose_check_call(PROJECT_SHELL_SCRIPT, *argv[1:]) except subprocess.CalledProcessError as err: sys.exit(err.returncode) diff --git a/pyproject.toml b/pyproject.toml index c0fb525..0cf1075 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,7 +117,7 @@ exclude_lines = [ legacy_tox_ini = """ [tox] isolated_build = True -envlist = py{311,310,39} +envlist = py{312,311,310,39} skip_missing_interpreters = True [testenv] @@ -128,6 +128,9 @@ commands_pre = pip-sync requirements.dev.txt commands = {envpython} -m coverage run --context='{envname}' + {envpython} -m coverage combine --append + {envpython} -m coverage xml + {envpython} -m coverage report """ @@ -147,6 +150,7 @@ cookiecutter_template = "https://github.com/jedie/cookiecutter_templates/" cookiecutter_directory = "piptools-python" applied_migrations = [ "034be26", # 2023-08-05T21:37:12+02:00 + "9a6c2db", # 2023-11-30T20:56:46+01:00 ] [manageprojects.cookiecutter_context.cookiecutter] From 7f1c7fa3f524990036fcf0cca8a12c98b302eed3 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 1 Dec 2023 18:36:59 +0100 Subject: [PATCH 2/7] add tests for EraseCoverageData() --- README.md | 1 + cli_base/cli_tools/tests/test_dev_tools.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 cli_base/cli_tools/tests/test_dev_tools.py diff --git a/README.md b/README.md index 48684aa..577da97 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ To make a new release, do this: [comment]: <> (✂✂✂ auto generated history start ✂✂✂) * [**dev**](https://github.com/jedie/cli-base-utilities/compare/v0.4.5...main) + * 2023-12-01 - add tests for EraseCoverageData() * 2023-12-01 - Apply manageprojects updates * [v0.4.5](https://github.com/jedie/cli-base-utilities/compare/v0.4.4...v0.4.5) * 2023-11-30 - Configure unittests via "load_tests Protocol" hook diff --git a/cli_base/cli_tools/tests/test_dev_tools.py b/cli_base/cli_tools/tests/test_dev_tools.py new file mode 100644 index 0000000..1f31514 --- /dev/null +++ b/cli_base/cli_tools/tests/test_dev_tools.py @@ -0,0 +1,21 @@ +from unittest import TestCase +from unittest.mock import patch + +from cli_base.cli_tools.dev_tools import EraseCoverageData + + +class DevToolsTestCase(TestCase): + def test_erase_coverage_data(self): + erase_coverage_data = EraseCoverageData() + erase_coverage_data.erased = False + + with patch('cli_base.cli_tools.dev_tools.verbose_check_call') as func_mock: + erase_coverage_data() + func_mock.assert_called_once_with('coverage', 'erase', verbose=True, exit_on_error=True, cwd=None) + self.assertIs(erase_coverage_data.erased, True) + + # Skip on second call: + with patch('cli_base.cli_tools.dev_tools.verbose_check_call') as func_mock: + erase_coverage_data() + func_mock.assert_not_called() + self.assertIs(erase_coverage_data.erased, True) From 3c3f6e9458616c07300fc6fb8fa6a933a934a568 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 1 Dec 2023 18:42:23 +0100 Subject: [PATCH 3/7] Add "run_coverage()" to "dev_tools" and polish tox, unittest, too. Bugfix also double coverage report generation in tox "commands_pre" --- README.md | 3 +- cli_base/cli/dev.py | 42 ++++--- cli_base/cli_tools/dev_tools.py | 122 +++++++++++++++++---- cli_base/cli_tools/tests/test_dev_tools.py | 96 +++++++++++++++- pyproject.toml | 3 - 5 files changed, 216 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 577da97..38ca962 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Usage: ./dev-cli.py [OPTIONS] COMMAND [ARGS]... ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮ │ check-code-style Check code style by calling darker + flake8 │ -│ coverage Run and show coverage. │ +│ coverage Run tests and show coverage report. │ │ fix-code-style Fix code style of all cli_base source code files via darker │ │ install Run pip-sync and install 'cli_base' via pip as editable. │ │ mypy Run Mypy (configured in pyproject.toml) │ @@ -96,6 +96,7 @@ To make a new release, do this: [comment]: <> (✂✂✂ auto generated history start ✂✂✂) * [**dev**](https://github.com/jedie/cli-base-utilities/compare/v0.4.5...main) + * 2023-12-01 - Add "run_coverage()" to "dev_tools" and polish tox, unittest, too. * 2023-12-01 - add tests for EraseCoverageData() * 2023-12-01 - Apply manageprojects updates * [v0.4.5](https://github.com/jedie/cli-base-utilities/compare/v0.4.4...v0.4.5) diff --git a/cli_base/cli/dev.py b/cli_base/cli/dev.py index 7fb0aaa..8eb2d85 100644 --- a/cli_base/cli/dev.py +++ b/cli_base/cli/dev.py @@ -14,7 +14,7 @@ import cli_base from cli_base import constants -from cli_base.cli_tools.dev_tools import coverage_combine_report, erase_coverage_data, run_tox, run_unittest_cli +from cli_base.cli_tools.dev_tools import run_coverage, run_tox, run_unittest_cli from cli_base.cli_tools.subprocess_utils import verbose_check_call from cli_base.cli_tools.test_utils.snapshot import UpdateTestSnapshotFiles from cli_base.cli_tools.verbosity import OPTION_KWARGS_VERBOSE @@ -71,22 +71,6 @@ def mypy(verbosity: int): cli.add_command(mypy) -@click.command() -@click.option('-v', '--verbosity', **OPTION_KWARGS_VERBOSE) -def coverage(verbosity: int): - """ - Run and show coverage. - """ - try: - verbose_check_call('coverage', 'run', verbose=verbosity > 0, exit_on_error=True) - coverage_combine_report(verbose=verbosity > 0) - finally: - erase_coverage_data(verbose=verbosity > 0) - - -cli.add_command(coverage) - - @click.command() def install(): """ @@ -236,6 +220,17 @@ def test(): cli.add_command(test) +@click.command() # Dummy command +def coverage(): + """ + Run tests and show coverage report. + """ + run_coverage() + + +cli.add_command(coverage) + + @click.command() # Dummy "tox" command def tox(): """ @@ -261,12 +256,15 @@ def main(): print_version(cli_base) if len(sys.argv) >= 2: - # Check if we just pass a command call + # Check if we can just pass a command call to origin CLI: command = sys.argv[1] - if command == 'test': - run_unittest_cli() - elif command == 'tox': - run_tox() + command_map = { + 'test': run_unittest_cli, + 'tox': run_tox, + 'coverage': run_coverage, + } + if real_func := command_map.get(command): + real_func(argv=sys.argv, exit_after_run=True) # Execute Click CLI: cli() diff --git a/cli_base/cli_tools/dev_tools.py b/cli_base/cli_tools/dev_tools.py index f73cdab..a849a6f 100644 --- a/cli_base/cli_tools/dev_tools.py +++ b/cli_base/cli_tools/dev_tools.py @@ -1,10 +1,12 @@ +import contextlib import os import sys +from subprocess import CalledProcessError from cli_base.cli_tools.subprocess_utils import verbose_check_call -def is_verbose(*, argv): +def is_verbose(*, argv: list) -> bool: if '-v' in argv or '--verbose' in argv: return True return False @@ -17,29 +19,44 @@ class EraseCoverageData: erased = False - def __call__(self, *, cwd=None, verbose=True): + def __call__(self, *, argv=None, cwd=None, verbose=None, exit_on_error=True): + if argv is None: + argv = sys.argv + + if verbose is None: + verbose = is_verbose(argv=argv) + if not self.erased: - verbose_check_call('coverage', 'erase', verbose=verbose, exit_on_error=True, cwd=cwd) + verbose_check_call('coverage', 'erase', verbose=verbose, exit_on_error=exit_on_error, cwd=cwd) self.erased = True # Call only once at runtime! erase_coverage_data = EraseCoverageData() -def coverage_combine_report(*, cwd=None, verbose=True): - verbose_check_call('coverage', 'combine', '--append', verbose=verbose, exit_on_error=True, cwd=cwd) - verbose_check_call('coverage', 'report', verbose=verbose, exit_on_error=True, cwd=cwd) - verbose_check_call('coverage', 'xml', verbose=verbose, exit_on_error=True, cwd=cwd) - verbose_check_call('coverage', 'json', verbose=verbose, exit_on_error=True, cwd=cwd) - erase_coverage_data(verbose=True) +def coverage_combine_report(*, argv=None, cwd=None, verbose=None, exit_on_error=True): + if argv is None: + argv = sys.argv + + if verbose is None: + verbose = is_verbose(argv=argv) + + verbose_check_call('coverage', 'combine', '--append', verbose=verbose, exit_on_error=exit_on_error, cwd=cwd) + verbose_check_call('coverage', 'report', verbose=verbose, exit_on_error=exit_on_error, cwd=cwd) + verbose_check_call('coverage', 'xml', verbose=verbose, exit_on_error=exit_on_error, cwd=cwd) + verbose_check_call('coverage', 'json', verbose=verbose, exit_on_error=exit_on_error, cwd=cwd) + erase_coverage_data(verbose=verbose, exit_on_error=exit_on_error, cwd=cwd) -def run_unittest_cli(extra_env=None, verbose=None, exit_after_run=True): +def run_unittest_cli(*, argv=None, extra_env=None, verbose=None, exit_after_run=True): """ Call the origin unittest CLI and pass all args to it. """ + if argv is None: + argv = sys.argv + if verbose is None: - verbose = is_verbose(argv=sys.argv) + verbose = is_verbose(argv=argv) if extra_env is None: extra_env = dict() @@ -51,13 +68,15 @@ def run_unittest_cli(extra_env=None, verbose=None, exit_after_run=True): ) ) - args = sys.argv[2:] + args = argv[2:] if not args: if verbose: args = ('--verbose', '--locals', '--buffer') else: args = ('--locals', '--buffer') + return_code = 0 + try: verbose_check_call( sys.executable, @@ -66,25 +85,86 @@ def run_unittest_cli(extra_env=None, verbose=None, exit_after_run=True): *args, timeout=15 * 60, extra_env=extra_env, + exit_on_error=False, ) + except SystemExit as err: + return_code = err.code + except CalledProcessError as err: + return_code = err.returncode finally: inside_tox_run = 'TOX_ENV_NAME' in os.environ # Called by tox run? - if not inside_tox_run: - erase_coverage_data(verbose=verbose) + if not inside_tox_run: # Don't erase coverage data if unittests runs via tox + with contextlib.suppress(SystemExit): + erase_coverage_data(verbose=verbose) if exit_after_run: - sys.exit(0) + if verbose: + print(f'Exit unittest with code {return_code!r}') + sys.exit(return_code) -def run_tox(): +def run_tox(*, argv=None, verbose=None, exit_after_run=True): """ Call tox and pass all command arguments to it """ - verbose = is_verbose(argv=sys.argv) + if argv is None: + argv = sys.argv + + if verbose is None: + verbose = is_verbose(argv=argv) + + return_code = 0 + + try: + verbose_check_call(sys.executable, '-m', 'tox', *argv[2:]) + with contextlib.suppress(SystemExit): + # Ignore exit code from "coverage", because tox exit code is more important here. + coverage_combine_report(verbose=verbose) + except SystemExit as err: + return_code = err.code + except CalledProcessError as err: + return_code = err.returncode + finally: + with contextlib.suppress(SystemExit): + erase_coverage_data(verbose=verbose) + + if exit_after_run: + if verbose: + print(f'Exit tox with code {return_code!r}') + sys.exit(return_code) + + +def run_coverage(*, argv=None, verbose=None, exit_on_error=True, exit_after_run=True): + """ + Call coverage and pass all command arguments to it + """ + if argv is None: + argv = sys.argv + + if verbose is None: + verbose = is_verbose(argv=argv) + + if len(argv) < 3: + # Autostart coverage run if no args passed + argv += ('run',) + + run_call = argv[2] == 'run' # Will there by coverage data created? + + return_code = 0 + try: - verbose_check_call(sys.executable, '-m', 'tox', *sys.argv[2:]) + verbose_check_call('coverage', *argv[2:], verbose=verbose, exit_on_error=exit_on_error) + if run_call: + coverage_combine_report(verbose=verbose) + except SystemExit as err: + return_code = err.code + except CalledProcessError as err: + return_code = err.returncode finally: - coverage_combine_report(verbose=verbose) - erase_coverage_data(verbose=verbose) + if run_call: + erase_coverage_data(verbose=verbose) - sys.exit(0) + if exit_after_run: + if verbose: + print(f'Exit coverage with code {return_code!r}') + sys.exit(return_code) diff --git a/cli_base/cli_tools/tests/test_dev_tools.py b/cli_base/cli_tools/tests/test_dev_tools.py index 1f31514..b56e88d 100644 --- a/cli_base/cli_tools/tests/test_dev_tools.py +++ b/cli_base/cli_tools/tests/test_dev_tools.py @@ -1,7 +1,13 @@ from unittest import TestCase from unittest.mock import patch -from cli_base.cli_tools.dev_tools import EraseCoverageData +from manageprojects.test_utils.subprocess import SimpleRunReturnCallback, SubprocessCallMock + +from cli_base.cli.dev import PACKAGE_ROOT +from cli_base.cli_tools.dev_tools import EraseCoverageData, run_coverage, run_tox, run_unittest_cli +from cli_base.cli_tools.test_utils.assertion import assert_in +from cli_base.cli_tools.test_utils.rich_test_utils import NoColorRichClickCli +from cli_base.constants import PY_BIN_PATH class DevToolsTestCase(TestCase): @@ -10,8 +16,10 @@ def test_erase_coverage_data(self): erase_coverage_data.erased = False with patch('cli_base.cli_tools.dev_tools.verbose_check_call') as func_mock: - erase_coverage_data() - func_mock.assert_called_once_with('coverage', 'erase', verbose=True, exit_on_error=True, cwd=None) + erase_coverage_data( + argv=('./dev-cli.py', 'coverage'), # no "--verbose" + ) + func_mock.assert_called_once_with('coverage', 'erase', verbose=False, exit_on_error=True, cwd=None) self.assertIs(erase_coverage_data.erased, True) # Skip on second call: @@ -19,3 +27,85 @@ def test_erase_coverage_data(self): erase_coverage_data() func_mock.assert_not_called() self.assertIs(erase_coverage_data.erased, True) + + def test_run_unittest(self): + with SubprocessCallMock(return_callback=SimpleRunReturnCallback(stdout='mocked output')) as call_mock: + run_unittest_cli(argv=('./dev-cli.py', 'unittest'), exit_after_run=False) + self.assertEqual( + call_mock.get_popenargs(rstrip_paths=(PY_BIN_PATH,)), + [['.../python', '-m', 'unittest', '--locals', '--buffer']], + ) + + def test_run_unittest_via_cli(self): + with NoColorRichClickCli() as cm: + stdout = cm.invoke(cli_bin=PACKAGE_ROOT / 'dev-cli.py', args=('test', '--help')) + assert_in( + stdout, + parts=( + 'unittest --help', + 'usage: python -m unittest [-h]', + ), + ) + + def test_run_tox(self): + with SubprocessCallMock(return_callback=SimpleRunReturnCallback(stdout='mocked output')) as call_mock: + run_tox(argv=('./dev-cli.py', 'tox'), exit_after_run=False) + self.assertEqual( + call_mock.get_popenargs(rstrip_paths=(PY_BIN_PATH,)), + [ + ['.../python', '-m', 'tox'], + ['.../coverage', 'combine', '--append'], + ['.../coverage', 'report'], + ['.../coverage', 'xml'], + ['.../coverage', 'json'], + ], + ) + + def test_run_tox_via_cli(self): + with NoColorRichClickCli() as cm: + stdout = cm.invoke(cli_bin=PACKAGE_ROOT / 'dev-cli.py', args=('tox', '--help')) + assert_in( + stdout, + parts=( + 'tox --help', + 'usage: tox [-h]', + ), + ) + + def test_run_coverage(self): + with SubprocessCallMock(return_callback=SimpleRunReturnCallback(stdout='mocked output')) as call_mock: + run_coverage(argv=('./dev-cli.py', 'coverage'), exit_after_run=False) + self.assertEqual( + call_mock.get_popenargs(rstrip_paths=(PY_BIN_PATH,)), + [ + ['.../coverage', 'run'], + ['.../coverage', 'combine', '--append'], + ['.../coverage', 'report'], + ['.../coverage', 'xml'], + ['.../coverage', 'json'], + ['.../coverage', 'erase'], + ], + ) + + # help will not combine report and erase data: + with SubprocessCallMock(return_callback=SimpleRunReturnCallback(stdout='mocked output')) as call_mock: + try: + run_coverage(argv=('./dev-cli.py', 'coverage', '--help')) + except SystemExit as err: + self.assertEqual(err.code, 0) + self.assertEqual( + call_mock.get_popenargs(rstrip_paths=(PY_BIN_PATH,)), + [['.../coverage', '--help']], + ) + + def test_run_coverage_via_cli(self): + with NoColorRichClickCli() as cm: + stdout = cm.invoke(cli_bin=PACKAGE_ROOT / 'dev-cli.py', args=('coverage', '--help')) + assert_in( + stdout, + parts=( + '.venv/bin/cli_base_dev coverage --help', + 'Coverage.py', + 'usage: coverage [options] [args]', + ), + ) diff --git a/pyproject.toml b/pyproject.toml index 0cf1075..b32712c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,9 +128,6 @@ commands_pre = pip-sync requirements.dev.txt commands = {envpython} -m coverage run --context='{envname}' - {envpython} -m coverage combine --append - {envpython} -m coverage xml - {envpython} -m coverage report """ From f9bd963a7fa759d198c0a2e6d853cf1cfe1b8cb4 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 1 Dec 2023 20:16:50 +0100 Subject: [PATCH 4/7] Bugfix expand_user() if SUDO_USER is the same as current user --- README.md | 1 + cli_base/cli_tools/path_utils.py | 4 ++-- cli_base/cli_tools/tests/test_path_utils.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 38ca962..aa5947f 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ To make a new release, do this: [comment]: <> (✂✂✂ auto generated history start ✂✂✂) * [**dev**](https://github.com/jedie/cli-base-utilities/compare/v0.4.5...main) + * 2023-12-01 - Bugfix expand_user() if SUDO_USER is the same as current user * 2023-12-01 - Add "run_coverage()" to "dev_tools" and polish tox, unittest, too. * 2023-12-01 - add tests for EraseCoverageData() * 2023-12-01 - Apply manageprojects updates diff --git a/cli_base/cli_tools/path_utils.py b/cli_base/cli_tools/path_utils.py index 68d4616..59212fe 100644 --- a/cli_base/cli_tools/path_utils.py +++ b/cli_base/cli_tools/path_utils.py @@ -50,8 +50,8 @@ def expand_user(path: Path) -> Path: sudo_user_home = pwd.getpwnam(sudo_user).pw_dir with OverrideEnviron(HOME=sudo_user_home): return Path(path).expanduser() - else: - return Path(path).expanduser() + + return Path(path).expanduser() def backup(file_path: Path, max_try=100) -> Path: diff --git a/cli_base/cli_tools/tests/test_path_utils.py b/cli_base/cli_tools/tests/test_path_utils.py index 65b4d0b..a6f3131 100644 --- a/cli_base/cli_tools/tests/test_path_utils.py +++ b/cli_base/cli_tools/tests/test_path_utils.py @@ -2,6 +2,8 @@ from pathlib import Path from unittest import TestCase +from manageprojects.test_utils.logs import AssertLogs + from cli_base.cli_tools.path_utils import backup, expand_user from cli_base.cli_tools.test_utils.environment_fixtures import AsSudoCallOverrideEnviron @@ -21,6 +23,14 @@ def test_expand_user(self): self.assertEqual(Path('~/example/').expanduser(), Path('/root/example')) self.assertEqual(expand_user(Path('~/example/')), real_example) + # What happen if SUDO_USER is the same as getpass.getuser() ? + with AsSudoCallOverrideEnviron(SUDO_USER='root', LOGNAME='root'), AssertLogs( + self, loggers=('cli_base',) + ) as logs: + self.assertEqual(Path('~').expanduser(), Path('/root')) + self.assertEqual(expand_user(Path('~')), Path('/root')) + logs.assert_in('Do not run this as root user!', "SUDO_USER:'root' <-> root") + def test_backup(self): with tempfile.TemporaryDirectory(prefix='test_') as temp_dir: temp_path = Path(temp_dir) From b021d56799346019aafb91c20a36b3b29793f49c Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 1 Dec 2023 20:22:43 +0100 Subject: [PATCH 5/7] NEW: test utils: AssertLogs() context manager --- README.md | 1 + cli_base/cli_tools/test_utils/logs.py | 42 +++++++++++++++++++ cli_base/cli_tools/tests/test_git.py | 2 +- cli_base/cli_tools/tests/test_path_utils.py | 3 +- cli_base/cli_tools/tests/test_version_info.py | 2 +- 5 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 cli_base/cli_tools/test_utils/logs.py diff --git a/README.md b/README.md index aa5947f..e4c53f5 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ To make a new release, do this: [comment]: <> (✂✂✂ auto generated history start ✂✂✂) * [**dev**](https://github.com/jedie/cli-base-utilities/compare/v0.4.5...main) + * 2023-12-01 - NEW: test utils: AssertLogs() context manager * 2023-12-01 - Bugfix expand_user() if SUDO_USER is the same as current user * 2023-12-01 - Add "run_coverage()" to "dev_tools" and polish tox, unittest, too. * 2023-12-01 - add tests for EraseCoverageData() diff --git a/cli_base/cli_tools/test_utils/logs.py b/cli_base/cli_tools/test_utils/logs.py new file mode 100644 index 0000000..7b69c72 --- /dev/null +++ b/cli_base/cli_tools/test_utils/logs.py @@ -0,0 +1,42 @@ +import logging +from pathlib import Path +from unittest import TestCase + + +class AssertLogs: + """ + Capture and assert log output from different loggers. + """ + + def __init__( + self, + test_case: TestCase, + loggers: tuple[str, ...] = ('manageprojects', 'cookiecutter'), + level=logging.DEBUG, + ): + assertLogs = test_case.assertLogs + + self.logs = [] + for logger in loggers: + self.logs.append(assertLogs(logger, level=level)) + + self.context_managers = None + + def __enter__(self): + self.context_managers = [log.__enter__() for log in self.logs] + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for log in self.logs: + log.__exit__(exc_type, exc_val, exc_tb) + + def assert_in(self, *test_parts): + outputs = [] + for cm in self.context_managers: + outputs.extend(cm.output) + + output = '\n'.join(outputs) + for part in test_parts: + if isinstance(part, Path): + part = str(part) + assert part in output, f'Log part {part!r} not found in:\n{output}' diff --git a/cli_base/cli_tools/tests/test_git.py b/cli_base/cli_tools/tests/test_git.py index f996f18..42add6e 100644 --- a/cli_base/cli_tools/tests/test_git.py +++ b/cli_base/cli_tools/tests/test_git.py @@ -7,7 +7,6 @@ from bx_py_utils.test_utils.datetime import parse_dt from bx_py_utils.test_utils.snapshot import assert_text_snapshot -from manageprojects.test_utils.logs import AssertLogs from manageprojects.test_utils.subprocess import SimpleRunReturnCallback, SubprocessCallMock from manageprojects.utilities.temp_path import TemporaryDirectory from packaging.version import Version @@ -15,6 +14,7 @@ from cli_base.cli.dev import PACKAGE_ROOT from cli_base.cli_tools.git import Git, GitHistoryEntry, GithubInfo, GitlabInfo, GitLogLine, GitTagInfo, GitTagInfos from cli_base.cli_tools.test_utils.git_utils import init_git +from cli_base.cli_tools.test_utils.logs import AssertLogs class GitTestCase(TestCase): diff --git a/cli_base/cli_tools/tests/test_path_utils.py b/cli_base/cli_tools/tests/test_path_utils.py index a6f3131..90d29df 100644 --- a/cli_base/cli_tools/tests/test_path_utils.py +++ b/cli_base/cli_tools/tests/test_path_utils.py @@ -2,10 +2,9 @@ from pathlib import Path from unittest import TestCase -from manageprojects.test_utils.logs import AssertLogs - from cli_base.cli_tools.path_utils import backup, expand_user from cli_base.cli_tools.test_utils.environment_fixtures import AsSudoCallOverrideEnviron +from cli_base.cli_tools.test_utils.logs import AssertLogs class PathUtilsTestCase(TestCase): diff --git a/cli_base/cli_tools/tests/test_version_info.py b/cli_base/cli_tools/tests/test_version_info.py index 763dc4f..8822fce 100644 --- a/cli_base/cli_tools/tests/test_version_info.py +++ b/cli_base/cli_tools/tests/test_version_info.py @@ -2,13 +2,13 @@ from unittest import TestCase from bx_py_utils.test_utils.redirect import RedirectOut -from manageprojects.test_utils.logs import AssertLogs from manageprojects.utilities.temp_path import TemporaryDirectory import cli_base from cli_base import __version__ from cli_base.cli.dev import PACKAGE_ROOT from cli_base.cli_tools.git import Git +from cli_base.cli_tools.test_utils.logs import AssertLogs from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRich from cli_base.cli_tools.version_info import print_version From 1dbb3f200d7212fcd52281caf3b3e0fbc72c8bb4 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 1 Dec 2023 20:27:03 +0100 Subject: [PATCH 6/7] fix flake8 see: https://github.com/jedie/cli-base-utilities/actions/runs/7063994793/job/19231122720#step:9:686 --- README.md | 1 + cli_base/cli_tools/path_utils.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e4c53f5..716c90b 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ To make a new release, do this: [comment]: <> (✂✂✂ auto generated history start ✂✂✂) * [**dev**](https://github.com/jedie/cli-base-utilities/compare/v0.4.5...main) + * 2023-12-01 - fix flake8 * 2023-12-01 - NEW: test utils: AssertLogs() context manager * 2023-12-01 - Bugfix expand_user() if SUDO_USER is the same as current user * 2023-12-01 - Add "run_coverage()" to "dev_tools" and polish tox, unittest, too. diff --git a/cli_base/cli_tools/path_utils.py b/cli_base/cli_tools/path_utils.py index 59212fe..fd203bb 100644 --- a/cli_base/cli_tools/path_utils.py +++ b/cli_base/cli_tools/path_utils.py @@ -61,7 +61,8 @@ def backup(file_path: Path, max_try=100) -> Path: """ assert_is_file(file_path) for number in range(1, max_try + 1): - bak_file_candidate = file_path.with_name(f'{file_path.name}.bak{number if number>1 else ""}') + number_suffix = number if number > 1 else '' + bak_file_candidate = file_path.with_name(f'{file_path.name}.bak{number_suffix}') if not bak_file_candidate.is_file(): logger.info('Backup %s to %s', file_path, bak_file_candidate) shutil.copyfile(file_path, bak_file_candidate) From fe432af4bb2f69ef1ce8407eb1ed8f7a542272d4 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 1 Dec 2023 20:38:10 +0100 Subject: [PATCH 7/7] Release as v0.5.0 --- README.md | 2 +- cli_base/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 716c90b..b27c9c4 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ To make a new release, do this: [comment]: <> (✂✂✂ auto generated history start ✂✂✂) -* [**dev**](https://github.com/jedie/cli-base-utilities/compare/v0.4.5...main) +* [v0.5.0](https://github.com/jedie/cli-base-utilities/compare/v0.4.5...v0.5.0) * 2023-12-01 - fix flake8 * 2023-12-01 - NEW: test utils: AssertLogs() context manager * 2023-12-01 - Bugfix expand_user() if SUDO_USER is the same as current user diff --git a/cli_base/__init__.py b/cli_base/__init__.py index 16f23c0..42f4b2a 100644 --- a/cli_base/__init__.py +++ b/cli_base/__init__.py @@ -3,5 +3,5 @@ Helpers to bild a CLI program """ -__version__ = '0.4.5' +__version__ = '0.5.0' __author__ = 'Jens Diemer '