From e0749e63ae8c06019297e8bda5443ac4c463359b Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Sun, 15 Nov 2020 11:13:44 -0800 Subject: [PATCH 1/7] adding option for `--log-file` on the `load` command --- CHANGES | 1 + pyproject.toml | 2 +- tests/test_cli.py | 27 +++++++++++ tmuxp/__about__.py | 2 +- tmuxp/cli.py | 112 +++++++++++++++++++++++++++++++++------------ tmuxp/log.py | 12 ++++- 6 files changed, 125 insertions(+), 31 deletions(-) diff --git a/CHANGES b/CHANGES index fcc69453b04..a428aa4b3a5 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,7 @@ Here you can find the recent changes to tmuxp current ------- +- Adding option to dump `load` output to log file - *Insert changes/features/fixes for next release here* tmuxp 1.6.2 (2020-11-08) diff --git a/pyproject.toml b/pyproject.toml index 56bd8985bda..8a8efa679b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ skip-string-normalization = true [tool.poetry] name = "tmuxp" -version = "1.6.2" +version = "1.6.3" description = "tmux session manager" license = "MIT" authors = ["Tony Narlock "] diff --git a/tests/test_cli.py b/tests/test_cli.py index 303370a0165..1e14e51f918 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -408,6 +408,33 @@ def test_load_zsh_autotitle_warning(cli_args, tmpdir, monkeypatch): assert 'Please set' not in result.output +@pytest.mark.parametrize( + "cli_args", + [ + (['load', '.', '--log-file', 'log.txt']), + ], +) +def test_load_log_file(cli_args, tmpdir, monkeypatch): + # create dummy tmuxp yaml that breaks to prevent actually loading tmux + tmpdir.join('.tmuxp.yaml').write( + """ +session_name: hello + """ + ) + tmpdir.join('.oh-my-zsh').ensure(dir=True) + monkeypatch.setenv('HOME', str(tmpdir)) + + with tmpdir.as_cwd(): + print('tmpdir: {0}'.format(tmpdir)) + runner = CliRunner() + + # If autoconfirm (-y) no need to prompt y + input_args = 'y\ny\n' if '-y' not in cli_args else '' + + runner.invoke(cli.cli, cli_args, input=input_args) + assert 'Loading' in tmpdir.join('log.txt').open().read() + + @pytest.mark.parametrize("cli_cmd", ['shell', ('shell', '--pdb')]) @pytest.mark.parametrize( "cli_args,inputs,env,expected_output", diff --git a/tmuxp/__about__.py b/tmuxp/__about__.py index eaa33c135f6..f94e45557bd 100644 --- a/tmuxp/__about__.py +++ b/tmuxp/__about__.py @@ -1,6 +1,6 @@ __title__ = 'tmuxp' __package_name__ = 'tmuxp' -__version__ = '1.6.2' +__version__ = '1.6.3' __description__ = 'tmux session manager' __email__ = 'tony@git-pull.com' __author__ = 'Tony Narlock' diff --git a/tmuxp/cli.py b/tmuxp/cli.py index 72e79ce624e..c4d1645e487 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -42,6 +42,14 @@ def get_cwd(): return os.getcwd() +def tmuxp_echo(message=None, log_level='INFO', **click_kwargs): + """ + Combines logging.log and click.echo + """ + logger.log(log.LOG_LEVELS[log_level], click.unstyle(message)) + click.echo(message, **click_kwargs) + + def get_config_dir(): """ Return tmuxp configuration directory. @@ -246,8 +254,8 @@ def scan_config_argument(ctx, param, value, config_dir=None): config_dir = config_dir() if not config: - click.echo("Enter at least one CONFIG") - click.echo(ctx.get_help(), color=ctx.color) + tmuxp_echo("Enter at least one CONFIG") + tmuxp_echo(ctx.get_help(), color=ctx.color) ctx.exit() if isinstance(value, string_types): @@ -357,11 +365,14 @@ def scan_config(config, config_dir=None): ] if len(candidates) > 1: - click.secho( - 'Multiple .tmuxp.{yml,yaml,json} configs in %s' % dirname(config), - fg="red", + tmuxp_echo( + click.style( + 'Multiple .tmuxp.{yml,yaml,json} configs in %s' + % dirname(config), + fg="red", + ) ) - click.echo( + tmuxp_echo( click.wrap_text( 'This is undefined behavior, use only one. ' 'Use file names e.g. myproject.json, coolproject.yaml. ' @@ -381,7 +392,46 @@ def scan_config(config, config_dir=None): return config +<<<<<<< Updated upstream def _reattach(session): +======= +def load_plugins(sconf): + """ + Load and return plugins in config + """ + plugins = [] + if 'plugins' in sconf: + for plugin in sconf['plugins']: + try: + module_name = plugin.split('.') + module_name = '.'.join(module_name[:-1]) + plugin_name = plugin.split('.')[-1] + plugin = getattr(importlib.import_module(module_name), plugin_name) + plugins.append(plugin()) + except exc.TmuxpPluginException as error: + if not click.confirm( + '%sSkip loading %s?' + % (click.style(str(error), fg='yellow'), plugin_name), + default=True, + ): + tmuxp_echo( + click.style('[Not Skipping] ', fg='yellow') + + 'Plugin versions constraint not met. Exiting...' + ) + sys.exit(1) + except Exception as error: + tmuxp_echo( + click.style('[Plugin Error] ', fg='red') + + "Couldn\'t load {0}\n".format(plugin) + + click.style('{0}'.format(error), fg='yellow') + ) + sys.exit(1) + + return plugins + + +def _reattach(builder): +>>>>>>> Stashed changes """ Reattach session (depending on env being inside tmux already or not) @@ -505,6 +555,11 @@ def load_workspace( # get the canonical path, eliminating any symlinks config_file = os.path.realpath(config_file) + tmuxp_echo( + click.style('[Loading] ', fg='green') + + click.style(config_file, fg='blue', bold=True) + ) + # kaptan allows us to open a yaml or json file as a dict sconfig = kaptan.Kaptan() sconfig = sconfig.import_config(config_file).get() @@ -525,7 +580,7 @@ def load_workspace( try: # load WorkspaceBuilder object for tmuxp config / tmux server builder = WorkspaceBuilder(sconf=sconfig, server=t) except exc.EmptyConfigException: - click.echo('%s is empty or parsed no config data' % config_file, err=True) + tmuxp_echo('%s is empty or parsed no config data' % config_file, err=True) return session_name = sconfig['session_name'] @@ -545,11 +600,6 @@ def load_workspace( return try: - click.echo( - click.style('[Loading] ', fg='green') - + click.style(config_file, fg='blue', bold=True) - ) - builder.build() # load tmux session via workspace builder if 'TMUX' in os.environ: # tmuxp ran from inside tmux @@ -586,8 +636,8 @@ def load_workspace( except exc.TmuxpException as e: import traceback - click.echo(traceback.format_exc(), err=True) - click.echo(e, err=True) + tmuxp_echo(traceback.format_exc(), err=True) + tmuxp_echo(e, err=True) choice = click.prompt( 'Error loading workspace. (k)ill, (a)ttach, (d)etach?', @@ -597,7 +647,7 @@ def load_workspace( if choice == 'k': builder.session.kill_session() - click.echo('Session killed.') + tmuxp_echo('Session killed.') elif choice == 'a': if 'TMUX' in os.environ: builder.session.switch_client() @@ -625,12 +675,12 @@ def cli(log_level): try: has_minimum_version() except TmuxCommandNotFound: - click.echo('tmux not found. tmuxp requires you install tmux first.') + tmuxp_echo('tmux not found. tmuxp requires you install tmux first.') sys.exit() except exc.TmuxpException as e: - click.echo(e, err=True) + tmuxp_echo(e, err=True) sys.exit() - setup_logger(level=log_level.upper()) + setup_logger(logger=logger, level=log_level.upper()) def setup_logger(logger=None, level='INFO'): @@ -649,12 +699,12 @@ def setup_logger(logger=None, level='INFO'): logger = logging.getLogger() if not logger.handlers: # setup logger handlers - channel = logging.StreamHandler() - channel.setFormatter(log.DebugLogFormatter()) - + # channel = logging.StreamHandler() + # channel.setFormatter(log.DebugLogFormatter()) # channel.setFormatter(log.LogFormatter()) + logger.setLevel(level) - logger.addHandler(channel) + # logger.addHandler(channel) def startup(config_dir): @@ -875,6 +925,7 @@ def command_freeze(session_name, socket_name, socket_path, force): flag_value=88, help='Like -2, but indicates that the terminal supports 88 colours.', ) +@click.option('--log-file', help='File to log errors/output to') def command_load( ctx, config, @@ -884,6 +935,7 @@ def command_load( answer_yes, detached, colors, + log_file, ): """Load a tmux workspace from each CONFIG. @@ -908,6 +960,10 @@ def command_load( detached mode. """ util.oh_my_zsh_auto_title() + if log_file: + logfile_handler = logging.FileHandler(log_file) + logfile_handler.setFormatter(log.LogFormatter()) + logger.addHandler(logfile_handler) tmux_options = { 'socket_name': socket_name, @@ -919,8 +975,8 @@ def command_load( } if not config: - click.echo("Enter at least one CONFIG") - click.echo(ctx.get_help(), color=ctx.color) + tmuxp_echo("Enter at least one CONFIG") + tmuxp_echo(ctx.get_help(), color=ctx.color) ctx.exit() if isinstance(config, string_types): @@ -962,7 +1018,7 @@ def import_config(configfile, importfunc): else: sys.exit('Unknown config format.') - click.echo( + tmuxp_echo( newconfig + '---------------------------------------------------------------' '\n' 'Configuration import does its best to convert files.\n' @@ -984,9 +1040,9 @@ def import_config(configfile, importfunc): buf.write(newconfig) buf.close() - click.echo('Saved to %s.' % dest) + tmuxp_echo('Saved to %s.' % dest) else: - click.echo( + tmuxp_echo( 'tmuxp has examples in JSON and YAML format at ' '\n' 'View tmuxp docs at ' @@ -1125,4 +1181,4 @@ def format_tmux_resp(std_resp): % format_tmux_resp(tmux_cmd('show-window-options', '-g')), ] - click.echo('\n'.join(output)) + tmuxp_echo('\n'.join(output)) diff --git a/tmuxp/log.py b/tmuxp/log.py index 561877835b5..7cafe264d54 100644 --- a/tmuxp/log.py +++ b/tmuxp/log.py @@ -21,6 +21,15 @@ 'CRITICAL': Fore.RED, } +LOG_LEVELS = { + 'CRITICAL': 50, + 'ERROR': 40, + 'WARNING': 30, + 'INFO': 20, + 'DEBUG': 10, + 'NOTSET': 0, +} + def default_log_template(self, record): """ @@ -89,8 +98,9 @@ def format(self, record): prefix = self.template(record) % record.__dict__ + parts = prefix.split(record.message) formatted = prefix + " " + record.message - return formatted.replace("\n", "\n ") + return formatted.replace("\n", "\n" + parts[0] + " ") def debug_log_template(self, record): From d6c138307d7c0f61fa606ca62bef7b09304d2d77 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Sun, 15 Nov 2020 11:17:09 -0800 Subject: [PATCH 2/7] fixing stash merge issue --- tmuxp/cli.py | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/tmuxp/cli.py b/tmuxp/cli.py index c4d1645e487..94b5bbaaa39 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -392,46 +392,7 @@ def scan_config(config, config_dir=None): return config -<<<<<<< Updated upstream def _reattach(session): -======= -def load_plugins(sconf): - """ - Load and return plugins in config - """ - plugins = [] - if 'plugins' in sconf: - for plugin in sconf['plugins']: - try: - module_name = plugin.split('.') - module_name = '.'.join(module_name[:-1]) - plugin_name = plugin.split('.')[-1] - plugin = getattr(importlib.import_module(module_name), plugin_name) - plugins.append(plugin()) - except exc.TmuxpPluginException as error: - if not click.confirm( - '%sSkip loading %s?' - % (click.style(str(error), fg='yellow'), plugin_name), - default=True, - ): - tmuxp_echo( - click.style('[Not Skipping] ', fg='yellow') - + 'Plugin versions constraint not met. Exiting...' - ) - sys.exit(1) - except Exception as error: - tmuxp_echo( - click.style('[Plugin Error] ', fg='red') - + "Couldn\'t load {0}\n".format(plugin) - + click.style('{0}'.format(error), fg='yellow') - ) - sys.exit(1) - - return plugins - - -def _reattach(builder): ->>>>>>> Stashed changes """ Reattach session (depending on env being inside tmux already or not) From 97af76cfa1382921e1bf0ecc536b8cb46bd113f2 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Sun, 15 Nov 2020 14:41:25 -0800 Subject: [PATCH 3/7] Updated docs. Fixed a non-standard-conforming global option --- README.rst | 10 +++++++++- docs/cli.rst | 13 ++++++++++++- tmuxp/cli.py | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 91a5179e042..56074ba1025 100644 --- a/README.rst +++ b/README.rst @@ -177,9 +177,16 @@ You can auto confirm the prompt. In this case no preview will be shown. $ tmuxp convert --yes filename -Debug Info +Debugging Helpers ---------- +The `load` command provides a way to log output to a log file for debugging +purposes. + +.. code-block:: sh + + $ tmuxp load --log-file . + Collect system info to submit with a Github issue: .. code-block:: sh @@ -192,6 +199,7 @@ Collect system info to submit with a Github issue: # ... so on + Docs / Reading material ----------------------- diff --git a/docs/cli.rst b/docs/cli.rst index 5d9b3fc4bee..87a67a089b6 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -184,12 +184,23 @@ are created, the last session is named from the terminal. $ tmxup load -s ... +The output of the ``load`` command can be logged to a file for +debugging purposes. the log level can be controlled with the global +``--log-level`` option (defaults to INFO). + +.. code-block:: bash + + $ tmuxp load --log-file + $ tmuxp --log-level load --log-file + + .. _cli_debug_info: Debug Info ---------- -Use to collect all relevant information for submitting an issue to the project. +Use to collect all relevant information for submitting an issue to +the project. .. code-block:: bash diff --git a/tmuxp/cli.py b/tmuxp/cli.py index 94b5bbaaa39..a9afd67287d 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -623,7 +623,7 @@ def load_workspace( @click.group(context_settings={'obj': {}}) @click.version_option(__version__, '-V', '--version', message='%(prog)s %(version)s') @click.option( - '--log_level', + '--log-level', default='INFO', help='Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)', ) From 8542708d661f74af9b372247b7c45deb7ecdb3e5 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Sat, 21 Nov 2020 06:23:58 -0800 Subject: [PATCH 4/7] adding in style selection to the logging formatter --- tmuxp/log.py | 60 +++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/tmuxp/log.py b/tmuxp/log.py index 7cafe264d54..bb9b3d0a45f 100644 --- a/tmuxp/log.py +++ b/tmuxp/log.py @@ -31,7 +31,16 @@ } -def default_log_template(self, record): +def set_style( + message, stylized, style_before=None, style_after=None, prefix='', suffix='' +): + if stylized: + return prefix + style_before + message + style_after + suffix + + return prefix + message + suffix + + +def default_log_template(self, record, stylized=False): """ Return the prefix for the log message. Template for Formatter. @@ -48,37 +57,34 @@ def default_log_template(self, record): """ reset = Style.RESET_ALL - levelname = ( - LEVEL_COLORS.get(record.levelname) - + Style.BRIGHT - + '(%(levelname)s)' - + Style.RESET_ALL - + ' ' + levelname = set_style( + '(%(levelname)s)', + stylized, + style_before=(LEVEL_COLORS.get(record.levelname) + Style.BRIGHT), + style_after=Style.RESET_ALL, + suffix=' ', ) - asctime = ( - '[' - + Fore.BLACK - + Style.DIM - + Style.BRIGHT - + '%(asctime)s' - + Fore.RESET - + Style.RESET_ALL - + ']' + asctime = set_style( + '%(asctime)s', + stylized, + style_before=(Fore.BLACK + Style.DIM + Style.BRIGHT), + style_after=(Fore.RESET + Style.RESET_ALL), + prefix='[', + suffix=']', ) - name = ( - ' ' - + Fore.WHITE - + Style.DIM - + Style.BRIGHT - + '%(name)s' - + Fore.RESET - + Style.RESET_ALL - + ' ' + name = set_style( + '%(name)s', + stylized, + style_before=(Fore.WHITE + Style.DIM + Style.BRIGHT), + style_after=(Fore.RESET + Style.RESET_ALL), + prefix=' ', + suffix=' ', ) - tpl = reset + levelname + asctime + name + reset + if stylized: + return reset + levelname + asctime + name + reset - return tpl + return levelname + asctime + name class LogFormatter(logging.Formatter): From 008507b779066b3194e14cb7d451b88066c4c197 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Sat, 21 Nov 2020 06:29:04 -0800 Subject: [PATCH 5/7] adding a style switch to the log --- tmuxp/cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tmuxp/cli.py b/tmuxp/cli.py index a9afd67287d..53271f3f4db 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -42,11 +42,15 @@ def get_cwd(): return os.getcwd() -def tmuxp_echo(message=None, log_level='INFO', **click_kwargs): +def tmuxp_echo(message=None, log_level='INFO', style_log=False, **click_kwargs): """ Combines logging.log and click.echo """ - logger.log(log.LOG_LEVELS[log_level], click.unstyle(message)) + if style_log: + logger.log(log.LOG_LEVELS[log_level], message) + else: + logger.log(log.LOG_LEVELS[log_level], click.unstyle(message)) + click.echo(message, **click_kwargs) From 0ca92907212db6da6ad857ac28aad979a2794925 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Sat, 21 Nov 2020 06:43:41 -0800 Subject: [PATCH 6/7] updating the set-env to use the new format --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b63e5ab096b..97486bc4c8b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,13 +29,13 @@ jobs: run: | curl -O -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py python get-poetry.py -y --version 1.0.10 - echo "::set-env name=PATH::$HOME/.poetry/bin:$PATH" + echo "PATH={$HOME}/.poetry/bin:{$PATH}" >> $GITHUB_ENV rm get-poetry.py - name: Get poetry cache paths from config run: | - echo ::set-env name=poetry_cache_dir::$(poetry config --list | sed -n 's/.*cache-dir = //p' | sed -e 's/^"//' -e 's/"$//') - echo ::set-env name=poetry_virtualenvs_path::$(poetry config --list | sed -n 's/.*virtualenvs.path = .* # //p' | sed -e 's/^"//' -e 's/"$//') + echo "poetry_cache_dir=$(poetry config --list | sed -n 's/.*cache-dir = //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV + echo "poetry_virtualenvs_path::$(poetry config --list | sed -n 's/.*virtualenvs.path = .* # //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV - name: Configure poetry shell: bash From 287fcc1df4f69edf7d6d83c2e99f7fc8c33564d9 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Sat, 21 Nov 2020 06:49:07 -0800 Subject: [PATCH 7/7] fixing test action by using actual bash instead of a templating thing --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 97486bc4c8b..984af9c993a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,13 +29,13 @@ jobs: run: | curl -O -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py python get-poetry.py -y --version 1.0.10 - echo "PATH={$HOME}/.poetry/bin:{$PATH}" >> $GITHUB_ENV + echo "PATH=${HOME}/.poetry/bin:${PATH}" >> $GITHUB_ENV rm get-poetry.py - name: Get poetry cache paths from config run: | echo "poetry_cache_dir=$(poetry config --list | sed -n 's/.*cache-dir = //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV - echo "poetry_virtualenvs_path::$(poetry config --list | sed -n 's/.*virtualenvs.path = .* # //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV + echo "poetry_virtualenvs_path=$(poetry config --list | sed -n 's/.*virtualenvs.path = .* # //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV - name: Configure poetry shell: bash