diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..7da1f96
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 100
diff --git a/.github/workflows/codestyle-python-flake8.yml b/.github/workflows/codestyle-python-flake8.yml
new file mode 100644
index 0000000..86f6ae0
--- /dev/null
+++ b/.github/workflows/codestyle-python-flake8.yml
@@ -0,0 +1,18 @@
+name: codestyle-python-flake8
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ flake8:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+ - name: Install Flake8
+ run: pip install flake8
+ - name: Run Flake8
+ run: flake8 python tests/python tests/fixtures
diff --git a/.github/workflows/codestyle-vimscript-vint.yml b/.github/workflows/codestyle-vimscript-vint.yml
new file mode 100644
index 0000000..e7b4b57
--- /dev/null
+++ b/.github/workflows/codestyle-vimscript-vint.yml
@@ -0,0 +1,18 @@
+name: codestyle-vimscript-vint
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ vint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+ - name: Install vint
+ run: pip install vim-vint
+ - name: Run vint
+ run: vint plugin
diff --git a/.github/workflows/unittests-python.yml b/.github/workflows/unittests-python.yml
new file mode 100644
index 0000000..795746f
--- /dev/null
+++ b/.github/workflows/unittests-python.yml
@@ -0,0 +1,18 @@
+name: unittests-python
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ pytest:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+ - name: Install dependencies
+ run: pip install pytest
+ - name: Run pytest
+ run: pytest tests/python
diff --git a/.github/workflows/unittests-vim.yml b/.github/workflows/unittests-vim.yml
new file mode 100644
index 0000000..ed60658
--- /dev/null
+++ b/.github/workflows/unittests-vim.yml
@@ -0,0 +1,47 @@
+name: unittests-vimscript
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ vim-tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install Vim and Neovim
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y vim neovim
+ - name: Run Vimscript tests (Neovim then Vim)
+ run: |
+ set +e
+ rm -f nvim-test.log vim-test.log
+
+ echo
+ echo "Starting Neovim unittest"
+ nvim --headless -u NONE -i NONE -V1nvim-test.log \
+ -c "source tests/vim/run_tests.vim" \
+ -c "qa"
+ nvim_status=$?
+ if [ $nvim_status -ne 0 ]; then
+ echo "Neovim test failed:"
+ cat nvim-test.log
+ fi
+
+ echo
+ echo "Starting Vim unittest"
+ vim -E -u NONE -i NONE -V1vim-test.log \
+ -c "source tests/vim/run_tests.vim" \
+ -c "qa"
+ vim_status=$?
+ if [ $vim_status -ne 0 ]; then
+ echo "Vim test failed:"
+ cat vim-test.log
+ fi
+
+ rm -f nvim-test.log vim-test.log
+
+ if [ $nvim_status -ne 0 ] || [ $vim_status -ne 0 ]; then
+ exit 1
+ fi
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..b298dbf
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,88 @@
+# Contributing
+
+Thank you for your interest in contributing. Please read this document
+carefully before opening an issue or pull request.
+
+
+## Scope and expectations
+
+This project is a Vim plugin implemented in Vim script with a Python backend.
+It is maintained on a best-effort basis by a single maintainer alongside other
+commitments. The primary goals are correctness, stability, and low
+maintenance overhead (but some feature expansion and keeping up with version
+updates of MLflow/Vim/NVim are still of interest!).
+
+The following contributions are most welcome:
+
+* Bug fixes with clear reproduction steps
+* Documentation improvements
+* Small, focused enhancements that align with the existing design
+
+The following are less likely to be accepted:
+
+* Large or architectural changes
+* Features that significantly increase configuration surface area
+* Changes that introduce new heavy dependencies
+
+One goal of this plugin is breadth of support, ie ensuring it works on both
+Vim and NVim, on Linux/MacOS/Windows, and on older versions of the supporting
+tools. So contributions are expected to support the minimum versions of:
+Vim 8.2, NVim v0.11.5, Python 3.10, and MLflow 2.12.0. (Ok that NVim version
+is not old but the rest are - point being to accommodate older versions in
+addition to the newer ones.) Of course MLflow in particular has features in
+v3.x that are not available in x2.x, but the point is to enable it to still
+work with v2.x within the limits of v2.x's capabilities.
+
+Vim-specific contribution requirements:
+
+* `:help` documentation: User-facing changes must update the appropriate
+ `doc/*.txt` help files with proper help tags. README-only documentation is
+ not sufficient for end-user behavior.
+* Backward compatibility: Behavior changes require clear justification and
+ corresponding documentation updates. Preserving existing workflows is
+ strongly preferred.
+
+In addition to unittests and codestyle workflows passing, contributors are
+expected to thoroughly test their updates end-to-end themselves within vim/nvim
+before submitting a PR.
+
+
+## Issues
+
+Before opening an issue:
+
+* Check existing issues to avoid duplicates
+* For bugs, include Vim/NVim version, Python version, OS version, and minimal
+ repro steps
+* For feature requests, clearly describe the use case and why it fits the
+ project’s scope
+
+Issues that are vague, unreproducible, or out of scope may be closed without
+further discussion.
+
+
+## Pull requests
+
+Guidelines:
+
+* Keep PRs small and narrowly scoped
+* One logical change per PR
+* Follow existing code style in both Vim script and Python
+* Add or update tests when applicable
+* Update documentation if behavior changes
+* Write commit messages per [commit.style](https://commit.style).
+
+All PRs must pass CI before they will be reviewed. Review may take time, and
+not all PRs will be merged.
+
+
+## Code of conduct
+
+Contributors are expected to be respectful and professional in their
+communication and behavior at all times.
+
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the
+project’s license.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9434be5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,41 @@
+.PHONY: codestyle-python-flake8 codestyle-vimscript-vint unittests unittests-vim unittests-vim-vim unittests-vim-nvim unittests-python env
+
+dev-env:
+ @python3 -m pip install -r dev-requirements.txt
+
+codestyle: codestyle-vimscript-vint codestyle-python-flake8
+
+codestyle-vimscript-vint:
+ @command -v vint >/dev/null 2>&1 || { echo "vint not available; install it (pip install vim-vint)."; exit 1; }
+ vint plugin
+
+codestyle-python-flake8:
+ @python3 -m flake8 python tests/python tests/fixtures
+
+unittests: unittests-vim unittests-python
+
+unittests-vim: unittests-vim-nvim unittests-vim-vim
+
+unittests-vim-vim:
+ @echo
+ @echo --
+ @echo Starting Vim unittest...
+ @command -v vim >/dev/null 2>&1 || { echo "vim not available on PATH."; exit 1; }
+ @rm -f vim-test.log
+ @vim -E -u NONE -i NONE -V1vim-test.log -c "source tests/vim/run_tests.vim" -c "qa" || { cat vim-test.log; exit 1; }
+ @rm -f vim-test.log
+
+unittests-vim-nvim:
+ @echo
+ @echo --
+ @echo Starting NVim unittest...
+ @command -v nvim >/dev/null 2>&1 || { echo "nvim not available on PATH."; exit 1; }
+ @rm -f nvim-test.log
+ @nvim --headless -u NONE -i NONE -V1nvim-test.log -c "source tests/vim/run_tests.vim" -c "qa" || { cat nvim-test.log; exit 1; }
+ @rm -f nvim-test.log
+
+unittests-python:
+ @echo --
+ @echo Starting Python unittest...
+ @command -v pytest >/dev/null 2>&1 || { echo "pytest not available; install it in your virtualenv."; exit 1; }
+ @pytest tests/python
diff --git a/README.md b/README.md
index e758520..bc0d5de 100644
--- a/README.md
+++ b/README.md
@@ -1,112 +1,95 @@
# vim-mlflow
-
-
+[](https://github.com/aganse/vim-mlflow/actions/workflows/unittests-python.yml)
+[](https://github.com/aganse/vim-mlflow/actions/workflows/unittests-vim.yml)
+[](https://github.com/aganse/vim-mlflow/actions/workflows/codestyle-python-flake8.yml)
+[](https://github.com/aganse/vim-mlflow/actions/workflows/codestyle-vimscript-vint.yml)
+
+
-`vim‑mlflow` is a lightweight Vim plugin that lets you browse and interact with
-MLflow experiments, runs, metrics, parameters, tags, and artifacts directly
+
+`vim‑mlflow` is a lightweight Vim/NVim plugin that lets you browse and interact
+with MLflow experiments, runs, metrics, parameters, tags, and artifacts directly
in your Vim editor. It opens a dedicated sidebar and a detail pane so you can
explore data without leaving the terminal, even allowing you to plot metric
histories and browse non-graphical artifacts. The plugin is written in
Vimscript with embedded Python and talks to MLflow through its Python API.
-It works with both MLflow3.x and MLflow2.x ML tracking servers (but not GenAI
-traces/etc currently).
-
->
-> :bulb: Note this repo is part of a group that you might find useful together
-> (but all are separate tools that can be used independently):
->
-> * [aganse/docker_mlflow_db](https://github.com/aganse/docker_mlflow_db):
-> ready-to-run MLflow server with PostgreSQL, AWS S3, Nginx
->
-> * [aganse/py_tf2_gpu_dock_mlflow](https://github.com/aganse/py_tf2_gpu_dock_mlflow):
-> ready-to-run Python/Tensorflow2/MLflow-Projects setup to train models on GPU
->
-> * [aganse/vim_mlflow](https://github.com/aganse/vim-mlflow):
-> a Vim plugin to browse the MLflow parameters and metrics instead of GUI
->
-
+It works with both MLflow3.x and MLflow2.x ML tracking servers (but not the
+GenAI traces/etc in MLflow3.x currently; feedback/demand can guide such future
+steps).
[](doc/demo_1.0.0_light.gif)
-## Summary
-* Open a sidebar (`__MLflow__`) that lists all experiments on the connected
- MLflow server.
-* Expand experiments to see individual runs.
-* Drill into a run to view metrics, parameters, tags, and artifacts.
-* Open a run comparison pane (`__MLflowRuns__`) to compare metrics
- across multiple selected runs.
-* View ASCII plots of metric histories, and text artifacts inline.
-* Completely configurable via Vim variables (e.g. in your ~/.vimrc file).
-
-> [!IMPORTANT]
-> `vim‑mlflow` requires a Python3‑enabled Vim and the `mlflow` Python package
-> installed in the same environment that Vim is launched from. You'll also want
-> to make sure that the version of Python3 with which you created your Python
-> environment matches the version of Python3 in your vim installation (e.g. you
-> can check this in vim with `:py3 import sys; print(sys.version)`). If you
-> just use the latest version of everything you probably won't have any issue.
+## TL;DR
+* Must run Vim/NVim in a python environment with `mlflow` installed.
+* Vim must be a compiled-with-python version (check `vim --verison` for `+python3`);
+ or for NVim just install the `pynvim` package in that python environment as well.
+* Put `Plugin 'aganse/vim-mlflow'` or your package manager equivalent in your
+ resource file to load plugin.
+* At minimum set `let g:mlflow_tracking_uri = "http://:"`
+ in your resource file to your MLflow tracking server. Other options listed below.
+* Press `\m` (leader-key and `m`) to start the plugin, and press `?` in there to
+ check the help listing for other keys. Navigate with the standard vim movement
+ keys, and "open" various items via `o` or ``.
## Installation
-`vim‑mlflow` works in Vim versions compiled with python3 support.
#### 1. Check that your Vim supports python3:
-- `vim --version | grep +python3`
-- (If no +python3 line is found, install a Vim build that was compiled with Python3.)
+- In Vim: `vim --version | grep +python3` (if no +python3 line is found, you
+ must install a Vim build compiled with Python3.)
+- In NVim you're good to go as long as you install pynvim in your python env
+ down in #3.
+- Tested successfully on Vim 8.2+ and NVim v0.11.5, with Python3.10+.
-#### 2. Highly recommended to create/use a virtual environment:
+#### 2. Highly recommended to create/use a python virtual environment:
- `python3 -m venv .venv`
- `source .venv/bin/activate # syntax for linux/mac`
-- (Technically this step is optional if you really insist, but it's recommended.)
-#### 3. Install the `mlflow` Python package:
-- `pip install mlflow`
-- The plugin imports and uses this mlflow package to connect to your MLflow
- tracking server. For the relatively basic functions in this plugin, the
- version of the MLflow package doesn't need to exactly match that of your
- tracking server.
+#### 3. Install the `mlflow` Python package (and also `pynvim` for Nvim):
+- `pip install mlflow` (in both Vim and NVim)
+- In NVim you also need this package in your env to support the python:
+ `pip install pynvim`
-#### 4. Add the plugin to your plugin manager:
-- *Vundle* add `Plugin 'aganse/vim-mlflow'` to .vimrc, run `:PluginInstall`.
-- *Plug* add `Plug 'aganse/vim-mlflow'` to .vimrc, run `:PlugInstall`.
-- *Or just path‑based* `cp -r . ~/.vim/plugin/vim-mlflow`
+#### 4. Load Vim-mlflow in your Vim/NVim resource file:
+- Add the plugin to your plugin manager, e.g. using Vundle add
+ `Plugin 'aganse/vim-mlflow'` to your resource file and run `:PluginInstall`.
+- Or could do manually, e.g. in NVim's `~/.config/nvim/init.vim` could load
+ via: `set runtimepath+=/Users/aganse/Documents/src/python/vim-mlflow`
-#### 5. Set your MLflow tracking URI (and any other config settings you like) in your ~/.vimrc:
-- `let g:mlflow_tracking_uri = "http://localhost:5000"`
+#### 5. Set your config settings in your Vim/NVim resource file:
+- Set your MLflow tracking URI. Fyi the default is `http://localhost:5000`,
+ which may be relevant for a simple local test setup, but often you'll have
+ some other host and port to set:
+ `let g:mlflow_tracking_uri = "http://:"`
+- The Configuration section has quite a list of settings (colors, characters,
+ sizing, etc) that can be customized.
+
+- For NVim you may need to set `setlocal nowrap` in your resource file - see
+ last Troubleshooting tip below regarding line-wrap default in NVim affecting
+ content layout.
## Usage
-Ensure you're in your python environment with MLflow before starting Vim.
-Then start the plugin using `m` or `:call RunMLflow()` (e.g. on my
-system the `` key is `\` so I do `\m`).
-You can also update that mapping in your ~/.vimrc file to set a new leader/key
-to start vim-mlflow in your Vim session, for example
-`nnoremap m :call RunMLflow()`.
-
-Starting the plugin opens the `__MLflow__` sidebar. Navigate the cursor around
-with the standard vim movement keys. A few of the more important plugin-specific
-key bindings inside the sidebar are:
-
-| Key | Action |
-|-----|--------|
-| `o`, `` | Open experiment/run/plot/artifact/section under cursor |
-| `r` | Requery the MLflow display |
-| `` | Mark runs in the runs list |
-| `R` | Open the `__MLflowRuns__` window to show and compare more details for the marked runs |
-
-Pressing `o` or `` while your cursor is on an experiment will open that
-experiment, updating the Runs list. Pressing it on a run will open that run and
-update the Metrics, Parameters, Tags, and Artifacts shown from the run. Pressing
-it on a metric with a history will open an ASCII plot of that metric's time
-series. Press `?` in the sidebar for a full help listing of the keys map.
-
+* Ensure you're in your python environment with MLflow before starting Vim.
+* Press `\m` to start vim-mlflow (default setting, ie leader-key and m. or can
+ use `:call RunMLflow()`). You can update that leader/key mapping via
+ `nnoremap m :call RunMLflow()`.
+* Vim-mlflow opens a sidebar (`__MLflow__`) that lists all experiments on the
+ connected MLflow server.
+* Navigate the cursor around with the standard vim movement keys, and "open"
+ various items via `o` or `` key.
+* Select experiments to show their respective lists of runs; drill into runs to
+ view metrics, parameters, tags, and artifacts. View ASCII plots of metric
+ histories, and text artifacts inline.
+* Open a run comparison pane (`__MLflowRuns__`) to compare metrics across
+ multiple selected runs in multiple experiments.
+* Press `?` in the sidebar for a full help listing of the keys map.
## Configuration
-
-Only `g:mlflow_tracking_uri` is required to be set by user (e.g. in .vimrc).
+Only `g:mlflow_tracking_uri` is required to be set by user (e.g. in resource file).
But a typical small set of vim-mlflow config variables that one might set is:
```vim
" Vim-mlflow settings
@@ -117,88 +100,96 @@ let g:vim_mlflow_expts_length = 10 " experiments to show at a time
let g:vim_mlflow_runs_length = 15 " runs to show at a time
```
-Full list of vim-mlflow config variables that may be of interest to set in .vimrc:
-| variable | description |
-| -------------------------------- | --------------------------------------- |
-| `g:mlflow_tracking_uri` _(required)_ | The MLFLOW_TRACKING_URI of the MLflow tracking server to connect to (default is `"http://localhost:5000"`)|
-| `g:vim_mlflow_timeout` | Timeout in float seconds if cannot access MLflow tracking server (default is 0.5)|
-| `g:vim_mlflow_buffername` | Buffername of the MLflow side pane (default is `__MLflow__`)|
-| `g:vim_mlflow_runs_buffername` | Buffername of the MLflowRuns side pane (default is `__MLflow__`)|
-| `g:vim_mlflow_vside` | Which side to open the MLflow pane on: 'left' or 'right' (default is `right`)|
-| `g:vim_mlflow_hside` | Whether to open the MLflowRuns pane 'below' or 'above' (default is `below`)|
-| `g:vim_mlflow_width` | Width of the vim-mlflow window in chars (default is 70)|
-| `g:vim_mlflow_height` | Width of the vim-mlflow window in chars (default is 10)|
-| `g:vim_mlflow_expts_length` | Number of expts to show in list (default is 8)|
-| `g:vim_mlflow_runs_length` | Number of runs to show in list (default is 8)|
-| `g:vim_mlflow_viewtype` | Show 1:activeonly, 2:deletedonly, or 3:all expts and runs (default is 1)|
-| `g:vim_mlflow_show_scrollicons` | Show the little up/down scroll arrows on expt/run lists, 1 or 0 (default is 1, ie yes show them)|
-| `g:vim_mlflow_icon_useunicode` | Allow unicode vs just ascii chars in UI, 1 or 0 (default is 0, ascii)|
-| `g:vim_mlflow_icon_vdivider` | Default is `'─'` if `vim_mlflow_icon_useunicode` else `'-'`|
-| `g:vim_mlflow_icon_scrollstop` | Default is `'▰'` if `vim_mlflow_icon_useunicode` else `''`|
-| `g:vim_mlflow_icon_scrollup` | Default is `'▲'` if `vim_mlflow_icon_useunicode` else `'^'`|
-| `g:vim_mlflow_icon_scrolldown` | Default is `'▼'` if `vim_mlflow_icon_useunicode` else `'v'`|
-| `g:vim_mlflow_icon_markrun` | Default is `'▶'` if `vim_mlflow_icon_useunicode` else `'>'`|
-| `g:vim_mlflow_icon_hdivider` | Default is `'│'` if `vim_mlflow_icon_useunicode` else `'|'`|
-| `g:vim_mlflow_icon_plotpts` | Default is `'●'` if `vim_mlflow_icon_useunicode` else `'*'`|
-| `g:vim_mlflow_icon_between_plotpts` | Default is `'•'` if `vim_mlflow_icon_useunicode` else `'.'`|
-| `g:vim_mlflow_color_titles` | Element highlight color label (default is `'Statement'`)|
-| `g:vim_mlflow_color_divlines` | Element highlight color label (default is `'vimParenSep'`)|
-| `g:vim_mlflow_color_scrollicons `| Element highlight color label (default is `'vimParenSep'`)|
-| `g:vim_mlflow_color_selectedexpt`| Element highlight color label (default is `'String'`)|
-| `g:vim_mlflow_color_selectedrun` | Element highlight color label (default is `'Number'`)|
-| `g:vim_mlflow_color_help` | Element highlight color label (default is `'Comment'`)|
-| `g:vim_mlflow_color_markrun` | Element highlight color label (default is `'vimParenSep'`)|
-| `g:vim_mlflow_color_hiddencol` | Element highlight color label (default is `'Comment'`)|
-| `g:vim_mlflow_color_plot_title` | Highlight group for plot titles (default `'Statement'`)|
-| `g:vim_mlflow_color_plot_axes` | Highlight group for plot axes text (default `'vimParenSep'`)|
-| `g:vim_mlflow_color_plotpts` | Highlight group for plot point glyphs (default `'Constant'`)|
-| `g:vim_mlflow_color_between_plotpts` | Highlight group for line segments between points (default `'Comment'`)|
-| `g:vim_mlflow_plot_height` | ASCII plot height in rows when graphing metric history (default `25`)|
-| `g:vim_mlflow_plot_width` | ASCII plot width in columns (default `70`)|
-| `g:vim_mlflow_plot_xaxis` | `'step'` or `'timestamp'` for metric plot x-axis (default `'step'`)|
-| `g:vim_mlflow_plot_reuse_buffer` | If `1`, reuse a single `__MLflowMetricPlot__` buffer; if `0`, create sequential plot buffers (default `1`)|
-| `g:vim_mlflow_artifacts_max_depth` | Maximum artifact directory depth shown when expanding folders (default `3`)|
+By default Vim-mlflow uses standard color groups like "Comment" and "Statement"
+to color its components so that whatever your colorscheme is it should "just
+work" in vim-mlflow. (E.g. the animated GIF above used
+[PaperColor](https://github.com/vim-scripts/PaperColor.vim) colorscheme;
+see also its [dark-mode equivalent animated GIF](doc/demo_1.0.0_dark.gif)).
+But all details can be changed, per listing below.
+With no configuration parameters set, ascii characters with no color are used.
+See the [full listing of vim-mlflow config variables](doc/configuration_params.md)
+that may be of interest to set in your resource file.
## Troubleshooting
-
-- If the plugin fails to load, double-check that `mlflow` is importable from
- the Python environment embedded in Vim (`:py3 import mlflow` should succeed).
-- The sidebar can be slow on high-latency connections to MLflow, because as
- currently implemented, each refresh spins up a short-lived Python process and
- re-queries MLflow anew. Running Vim close to the tracking server seems to
- work fine (e.g. on same machine, and fine even if the database MLflow is
- using is in AWS RDS while MLflow is in a tiny EC2 instance). For slower
- connections, increasing `g:vim_mlflow_timeout` might help avoid access
- timeouts. If there's demand, a future plugin version could keep a persistent
- python process that keeps state in memory and requeries the database much
- less often, but this hasn't seemed to be of much concern so far.
+- The sidebar may be slow on high-latency MLflow connections because each
+ refresh starts a short-lived Python process and re-queries MLflow.
+ Performance seems fine when Vim runs close to the tracking server; running
+ all components in AWS within same region, it has worked well for our team.
+ On slower links, increasing `g:vim_mlflow_timeout` may help. A future version
+ could use a persistent Python process to reduce queries if necessary, but so
+ far this has not been a common enough concern.
- Unicode icons require a font that includes box-drawing characters. Set
`g:vim_mlflow_icon_useunicode = 0` if glyphs look broken as the simple quick
fix, and also note there are config vars to change individual icon characters.
-- Can I view non‑text artifacts? – Text files (`*.txt`, `*.json`, `*.yaml`,
- `MLmodel`) open directly in the plugin. Binary artifacts' filenames are
- shown but cannot be opened in terminal.
-
+- Text artifacts (`*.txt`, `*.json`, `*.yaml`, `MLmodel`) open directly in the
+ plugin. Binary artifacts are listed but cannot be opened in the plugin.
+- If the plugin fails to load in classic Vim, verify that Vim supports Python
+ (vim --version) and that mlflow is importable in Vim’s Python environment
+ (:py3 import mlflow). In Neovim, also ensure pynvim is installed.
+- In NVim if the layout seems screwy, check step 5 above in Installation
+ regarding `nowrap`.
+- Neovim enables line wrapping by default, which can break table layouts in
+ this plugin. Adding `setlocal nowrap` fixes this globally. The issue is most
+ noticeable in the MLflowRuns window (opened with R), which displays many
+ columns.
+
+
+## Contributing
+Contributions are welcome; just note this project is maintained on a best-effort
+basis by a single maintainer (Andy Ganse) alongside other commitments, and so
+focused on long-term maintainability more than rapid feature growth. Bug fixes,
+documentation improvements, and small, well-scoped enhancements are the most
+likely to be accepted. Feature requests may be declined if they significantly
+increase complexity, maintenance burden, or diverge from the project’s stated
+scope. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening issues or
+pull requests, and note that response and review times may not be fast.
+
+#### Dev tools to be aware of when contributing
+This repo has unittests and codestyle checks, implemented in both CI workflows
+and also available locally via the following Makefile calls. To run these you
+need a few more packages in your Python environment than when just using the
+plugin as above. So for dev purposes after entering your Python environment
+(e.g. `source .venv/bin/activate`) run `make dev-env` which will install the
+list of packages in dev-requirements.txt. Then you can run the following to
+confirm they pass before submitting a PR for review (and to ease passing the
+CI workflows):
+- `make unittests` runs Vimscript unittests in both Vim and Neovim, and also
+ the Python unittests.
+- `make codestyle` lints the Vimscript in `plugin/` with `vint` and the
+ Python in `python/` with `flake8`.
## Legacy/older versions
-
- Legacy/older versions of this plugin can be accessed by git checking out an
- earlier version locally, and then referencing it in your .vimrc like
- (e.g. for Vundle) `Plugin 'file:///my/path/to/python/vim-mlflow'`.
-
- | vim-mlflow git tag | tested with mlflow version |
- | ---------------------| -------------------------- |
- | v0.8 | 1.26.1 |
- | v0.9 | 1.30.0, 2.7.1 |
- | v1.0.0 (this version)| 2.12.0, 2.19.0, 3.6.0 |
-
+Legacy/older versions of this plugin can be accessed by git checking out an
+earlier version locally, and then referencing it in your .vimrc (for classic
+Vim like `Plugin 'file:///my/path/to/python/vim-mlflow'`.
+Or similarly you can set that path in your runtimepath in NVim (without the
+`file://`).
+That said, it's recommended to use >= v1.0.0 - that's the first "official"
+release (we'll just keep adding to this table as more releases come out).
+
+| vim-mlflow git tag | tested with mlflow version | tested with vim version |
+| ---------------------| -------------------------- | ----------------------- |
+| v0.8 | 1.26.1 | vim 8.2 |
+| v0.9 | 1.30.0, 2.7.1 | vim 8.2 |
+| v1.0.0 (this version)| 2.12.0, 2.19.0, 3.6.0 | vim 9.1, nvim v0.11.5 |
+
+
+## Related repos by aganse
+Vim-mlflow is part of a group of tools that you might find useful together (but
+all are separate tools that can be used independently). In particular the
+following two tools allow to populate test contents into a temporary MLflow
+tracking server for dev/test purposes - they're the contents seen in screencast
+above:
+* [aganse/docker_mlflow_db](https://github.com/aganse/docker_mlflow_db):
+ ready-to-run MLflow server with PostgreSQL, AWS S3, Nginx
+* [aganse/py_torch_gpu_dock_mlflow](https://github.com/aganse/py_torch_gpu_dock_mlflow):
+ ready-to-run Python/PyTorch/MLflow-Projects setup to train models on GPU
## Making the animated screen-shot gif
-
* Install [rust](https://rust-lang.org/tools/install) (`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`)
* Install [asciinema](https://github.com/asciinema/asciinema) (`cargo install --locked --git https://github.com/asciinema/asciinema`)
* `~/.cargo/bin/asciinema rec demo.cast # start recording terminal screen to file`
@@ -207,21 +198,13 @@ Full list of vim-mlflow config variables that may be of interest to set in .vimr
* `~/.cargo/bin/agg --speed 2 demo.cast demo.gif # convert the asciinema cast to animated gif`
-
## Acknowledgements
-
With many thanks to:
-* The Writing Vim plugin in Python article by Timur Rubeko, 2017 Aug 11, at
+* "Writing a Vim plugin in Python" article by Timur Rubeko, 2017 Aug 11, at
http://candidtim.github.io/vim/2017/08/11/write-vim-plugin-in-python.html
-* Analyzing Your MLflow Data with DataFrames by Max Allen, 2019 Oct 3, at
- https://slacker.ro/2019/10/03/analyzing-your-mlflow-data-with-dataframes
+* "Analyzing Your MLflow Data with DataFrames" by Max Allen, 2019 Oct 3, at
+ https://www.databricks.com/blog/2019/10/03/analyzing-your-mlflow-data-with-dataframes.html
* The Python Interface to Vim, 2019 Dec 07, at
https://vimhelp.org/if_pyth.txt.html#python-vim
-* Alternative to execfile in Python 3, Stack Overflow, 2011 Jun 15 at
- https://stackoverflow.com/questions/6357361/alternative-to-execfile-in-python-3/6357418#6357418
* MLFlow Python API at
https://www.mlflow.org/docs/latest/python_api/mlflow.html
-* MLFlow REST API at
- https://www.mlflow.org/docs/latest/rest-api.html
-* MLFlow Projects page at
- https://www.mlflow.org/docs/latest/projects.html
diff --git a/TODO b/TODO
deleted file mode 100644
index 7ccb028..0000000
--- a/TODO
+++ /dev/null
@@ -1,3 +0,0 @@
-Make graceful handling for when no VIRTUAL_ENV exists (currently barfs on vim startup).
-Reduce frequency of accessing the MLflow API (database) and store results more in memory to speed up UI.
-Resolve remaining intermittant bugs in MLflowRuns pane column hiding.
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..7dea76e
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+1.0.1
diff --git a/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
index 0000000..e5adeec
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1,6 @@
+mlflow
+pynvim
+pytest
+flake8
+vim-vint
+setuptools
diff --git a/doc/configuration_params.md b/doc/configuration_params.md
new file mode 100644
index 0000000..9f99d9c
--- /dev/null
+++ b/doc/configuration_params.md
@@ -0,0 +1,43 @@
+Full list of vim-mlflow config variables that may be of interest to set in resource file:
+
+| variable | description |
+| -------------------------------- | --------------------------------------- |
+| `g:mlflow_tracking_uri` _(required)_ | The MLFLOW_TRACKING_URI of the MLflow tracking server to connect to (default is `"http://localhost:5000"`)|
+| `g:vim_mlflow_timeout` | Timeout in float seconds if cannot access MLflow tracking server (default is 0.5)|
+| `g:vim_mlflow_buffername` | Buffername of the MLflow side pane (default is `__MLflow__`)|
+| `g:vim_mlflow_runs_buffername` | Buffername of the MLflowRuns side pane (default is `__MLflow__`)|
+| `g:vim_mlflow_vside` | Which side to open the MLflow pane on: 'left' or 'right' (default is `right`)|
+| `g:vim_mlflow_hside` | Whether to open the MLflowRuns pane 'below' or 'above' (default is `below`)|
+| `g:vim_mlflow_width` | Width of the vim-mlflow window in chars (default is 70)|
+| `g:vim_mlflow_height` | Width of the vim-mlflow window in chars (default is 10)|
+| `g:vim_mlflow_expts_length` | Number of expts to show in list (default is 8)|
+| `g:vim_mlflow_runs_length` | Number of runs to show in list (default is 8)|
+| `g:vim_mlflow_viewtype` | Show 1:activeonly, 2:deletedonly, or 3:all expts and runs (default is 1)|
+| `g:vim_mlflow_show_scrollicons` | Show the little up/down scroll arrows on expt/run lists, 1 or 0 (default is 1, ie yes show them)|
+| `g:vim_mlflow_icon_useunicode` | Allow unicode vs just ascii chars in UI, 1 or 0 (default is 0, ascii)|
+| `g:vim_mlflow_icon_vdivider` | Default is `'─'` if `vim_mlflow_icon_useunicode` else `'-'`|
+| `g:vim_mlflow_icon_scrollstop` | Default is `'▰'` if `vim_mlflow_icon_useunicode` else `''`|
+| `g:vim_mlflow_icon_scrollup` | Default is `'▲'` if `vim_mlflow_icon_useunicode` else `'^'`|
+| `g:vim_mlflow_icon_scrolldown` | Default is `'▼'` if `vim_mlflow_icon_useunicode` else `'v'`|
+| `g:vim_mlflow_icon_markrun` | Default is `'▶'` if `vim_mlflow_icon_useunicode` else `'>'`|
+| `g:vim_mlflow_icon_hdivider` | Default is `'│'` if `vim_mlflow_icon_useunicode` else `'|'`|
+| `g:vim_mlflow_icon_plotpts` | Default is `'●'` if `vim_mlflow_icon_useunicode` else `'*'`|
+| `g:vim_mlflow_icon_between_plotpts` | Default is `'•'` if `vim_mlflow_icon_useunicode` else `'.'`|
+| `g:vim_mlflow_color_titles` | Element highlight color label (default is `'Statement'`)|
+| `g:vim_mlflow_color_divlines` | Element highlight color label (default is `'vimParenSep'`)|
+| `g:vim_mlflow_color_scrollicons `| Element highlight color label (default is `'vimParenSep'`)|
+| `g:vim_mlflow_color_selectedexpt`| Element highlight color label (default is `'String'`)|
+| `g:vim_mlflow_color_selectedrun` | Element highlight color label (default is `'Number'`)|
+| `g:vim_mlflow_color_help` | Element highlight color label (default is `'Comment'`)|
+| `g:vim_mlflow_color_markrun` | Element highlight color label (default is `'vimParenSep'`)|
+| `g:vim_mlflow_color_hiddencol` | Element highlight color label (default is `'Comment'`)|
+| `g:vim_mlflow_color_plot_title` | Highlight group for plot titles (default `'Statement'`)|
+| `g:vim_mlflow_color_plot_axes` | Highlight group for plot axes text (default `'vimParenSep'`)|
+| `g:vim_mlflow_color_plotpts` | Highlight group for plot point glyphs (default `'Constant'`)|
+| `g:vim_mlflow_color_between_plotpts` | Highlight group for line segments between points (default `'Comment'`)|
+| `g:vim_mlflow_plot_height` | ASCII plot height in rows when graphing metric history (default `25`)|
+| `g:vim_mlflow_plot_width` | ASCII plot width in columns (default `70`)|
+| `g:vim_mlflow_plot_xaxis` | `'step'` or `'timestamp'` for metric plot x-axis (default `'step'`)|
+| `g:vim_mlflow_plot_reuse_buffer` | If `1`, reuse a single `__MLflowMetricPlot__` buffer; if `0`, create sequential plot buffers (default `1`)|
+| `g:vim_mlflow_artifacts_max_depth` | Maximum artifact directory depth shown when expanding folders (default `3`)|
+
diff --git a/doc/demo_0.9.0.gif b/doc/demo_0.9.0.gif
deleted file mode 100644
index 8d5a733..0000000
Binary files a/doc/demo_0.9.0.gif and /dev/null differ
diff --git a/doc/example_screen_shot_0.9.0.png b/doc/example_screen_shot_0.9.0.png
deleted file mode 100644
index 6c5465d..0000000
Binary files a/doc/example_screen_shot_0.9.0.png and /dev/null differ
diff --git a/doc/tags b/doc/tags
new file mode 100644
index 0000000..12a1981
--- /dev/null
+++ b/doc/tags
@@ -0,0 +1,11 @@
+vim-mlflow vim-mlflow.txt /*vim-mlflow*
+vim-mlflow-config vim-mlflow.txt /*vim-mlflow-config*
+vim-mlflow-contents vim-mlflow.txt /*vim-mlflow-contents*
+vim-mlflow-install vim-mlflow.txt /*vim-mlflow-install*
+vim-mlflow-links vim-mlflow.txt /*vim-mlflow-links*
+vim-mlflow-mappings vim-mlflow.txt /*vim-mlflow-mappings*
+vim-mlflow-overview vim-mlflow.txt /*vim-mlflow-overview*
+vim-mlflow-start vim-mlflow.txt /*vim-mlflow-start*
+vim-mlflow-troubleshooting vim-mlflow.txt /*vim-mlflow-troubleshooting*
+vim-mlflow-windows vim-mlflow.txt /*vim-mlflow-windows*
+vim-mlflow.txt vim-mlflow.txt /*vim-mlflow.txt*
diff --git a/doc/vim-mlflow.txt b/doc/vim-mlflow.txt
index 7acc6a7..98f74fc 100644
--- a/doc/vim-mlflow.txt
+++ b/doc/vim-mlflow.txt
@@ -1,17 +1,185 @@
-help_vim_mlflow.txt For Vim version 8.2 Last change: 2020 Sep 15
+*vim-mlflow.txt* For Vim version 8.2+ and Neovim v0.11.5+ Last change: 2025 Dec 16
-A Vim plugin to view in Vim the tables and info one sees in an MLFlow website
+ *vim-mlflow*
+vim-mlflow is a Vim/Neovim plugin for browsing MLflow experiments, runs,
+metrics, parameters, tags, and artifacts without leaving your editor. It
+provides a sidebar for navigation and an optional comparison window for marked
+runs.
+==============================================================================
+CONTENTS *vim-mlflow-contents*
-With many thanks to the Writing Vim plugin in Python article by Timur Rubeko at
-http://candidtim.github.io/vim/2017/08/11/write-vim-plugin-in-python.html
+1. Overview |vim-mlflow-overview|
+2. Requirements & install |vim-mlflow-install|
+3. Getting started |vim-mlflow-start|
+4. Windows & navigation |vim-mlflow-windows|
+5. Key mappings |vim-mlflow-mappings|
+6. Configuration |vim-mlflow-config|
+7. Troubleshooting & tips |vim-mlflow-troubleshooting|
+8. Further reading |vim-mlflow-links|
+==============================================================================
+1. Overview *vim-mlflow-overview*
-Referring to this plugin in your .vimrc file:
+vim-mlflow surfaces MLflow tracking data directly inside Vim. The plugin:
+ - Lists experiments and runs in a dedicated sidebar buffer (`__MLflow__`).
+ - Shows run details (parameters, metrics, tags, artifacts) inline.
+ - Plots metric histories as ASCII graphs inside Vim splits.
+ - Opens a secondary window (`__MLflowRuns__`) to compare multiple runs.
-* For normal users not developing/tweaking the plugin:
- `Plugin 'aganse/vim-mlflow'`
+The project focuses on correctness and low maintenance overhead. Features that
+require extensive configuration or background services are intentionally kept
+limited.
-* For those who cloned the repo locally and want to tweak/dev further, use
- the file:// protocol with absolute path, like this for example:
- `Plugin 'file:///Users/aganse/Documents/src/python/vim-mlflow'`
+==============================================================================
+2. Requirements & install *vim-mlflow-install*
+
+vim-mlflow needs a Python-enabled Vim or Neovim and the MLflow Python package.
+
+Minimum supported versions
+ - Vim 8.2 compiled with `+python3`
+ - Neovim v0.11.5 with the `pynvim` package installed
+
+Python environment
+ 1. Create or activate a virtual environment (recommended):
+ `python3 -m venv .venv && source .venv/bin/activate`
+ 2. Install MLflow (and pynvim for Neovim users):
+ `pip install mlflow`
+ `pip install pynvim`
+
+Plugin install
+ - Plugin managers: `Plugin 'aganse/vim-mlflow'`
+ - Manual runtimepath example (Neovim): `set runtimepath+=/path/to/vim-mlflow`
+
+After installation, restart Vim so the plugin is sourced.
+
+==============================================================================
+3. Getting started *vim-mlflow-start*
+
+Basic setup in your `vimrc`/`init.vim`:
+>
+ let g:mlflow_tracking_uri = 'http://localhost:5000'
+ let g:vim_mlflow_width = 70
+<
+Launch the UI with either the default mapping or a command:
+ - Default mapping: `m` (typically `\m`)
+ - Command form: `:call RunMLflow()`
+
+Make sure your Vim session runs inside a Python environment where `mlflow` (and
+`pynvim`, if needed) is installed; otherwise the plugin cannot connect.
+
+==============================================================================
+4. Windows & navigation *vim-mlflow-windows*
+
+Sidebar (`__MLflow__`)
+ - Lists experiments, runs, and run details.
+ - Sections for parameters, metrics, tags, and artifacts can be toggled.
+ - Uses scroll hints and highlighting to show the active experiment/run.
+
+Runs window (`__MLflowRuns__`)
+ - Optional comparison buffer opened with `R` inside the sidebar.
+ - Displays a tabular view of marked runs, including tags/params/metrics based
+ on the toggles you choose.
+
+Metric plots
+ - Press `o` or `` on metrics marked with `[final value; o to plot]` to
+ open an ASCII graph in a vertical split. Plots reuse a dedicated buffer by
+ default but can be configured to open new windows for each metric.
+
+Artifacts
+ - Expand directories and open supported artifacts (text, JSON, YAML, MLmodel)
+ directly inside Vim. Binary artifacts are listed but stay unopened.
+
+==============================================================================
+5. Key mappings *vim-mlflow-mappings*
+
+Default mappings in the sidebar buffer (`__MLflow__`):
+
+ `?` Toggle the inline help banner.
+ `` Open the experiment/run/section under the cursor.
+ `o` Same as ``; also plots metrics where available.
+ `` Mark or unmark the highlighted run (visible in the runs window).
+ `r` Refresh the sidebar and reset expanded artifacts.
+ `R` Open or refresh the marked runs window (`__MLflowRuns__`).
+ `` Toggle the Parameters section visibility.
+ `` Toggle the Metrics section visibility.
+ `` Toggle the Tags section visibility.
+ `` Toggle the Artifacts section visibility.
+ `@` Rotate the order of detail sections (params/metrics/tags/artifacts).
+ `A` Cycle Active → Deleted → All experiments/runs (MLflow view type).
+ `n` / `p` Scroll the experiment or run list down / up.
+ `N` / `P` Jump to the bottom / top of the current list.
+
+Default mappings in the runs window (`__MLflowRuns__`):
+
+ `?` Toggle the window-specific help banner.
+ `R` Refresh the listing of marked runs.
+ `.` Collapse or expand the column under the cursor.
+ `x` Remove the run under the cursor from the marked list.
+ `` Undo all column layout tweaks (collapse/hide/move).
+ `` Include or omit run parameters in the table.
+ `` Include or omit run metrics in the table.
+ `` Include or omit run tags in the table.
+
+All mappings are buffer-local; redefining them globally is unnecessary. You can
+remap the entry point by adjusting your own `` mapping or calling
+`RunMLflow()` explicitly.
+
+==============================================================================
+6. Configuration *vim-mlflow-config*
+
+Only `g:mlflow_tracking_uri` is required. Common options:
+
+ `g:vim_mlflow_width` Sidebar width in columns (default 70).
+ `g:vim_mlflow_height` Runs window height in rows (default 10).
+ `g:vim_mlflow_expts_length` Number of experiments displayed at once (default 8).
+ `g:vim_mlflow_runs_length` Number of runs displayed at once (default 8).
+ `g:vim_mlflow_icon_useunicode` Use Unicode box-drawing/icons (default 0 for ASCII).
+ `g:vim_mlflow_plot_width` / `g:vim_mlflow_plot_height`
+ Dimensions of metric plots (default 70x25).
+ `g:vim_mlflow_plot_reuse_buffer` Reuse a single plot buffer (default 1).
+ `g:vim_mlflow_section_order` List order for run details (default
+ `['params', 'metrics', 'tags', 'artifacts']`).
+ `g:vim_mlflow_timeout` HTTP timeout (seconds) when probing the tracking URI
+ before querying MLflow (default 0.5).
+
+See `doc/configuration_params.md` in the repository for a complete table of
+settings, including highlight groups and custom icon characters.
+
+==============================================================================
+7. Troubleshooting & tips *vim-mlflow-troubleshooting*
+
+Slow refreshes~
+ Each refresh queries MLflow synchronously. Performance is best when Vim runs
+ close to the tracking server. Increase `g:vim_mlflow_timeout` if connections
+ are flaky. Long round-trips may still block Vim temporarily.
+
+Unicode glyph issues~
+ Set `let g:vim_mlflow_icon_useunicode = 0` to fall back to ASCII characters if
+ your terminal font lacks box-drawing support. Individual icons can also be
+ overridden in your config.
+
+Plugin does not load~
+ - Confirm Vim reports `+python3` (`vim --version | grep +python3`).
+ - In Neovim ensure `pynvim` is installed in the same environment.
+ - From Vim run `:py3 import mlflow` to verify MLflow is importable.
+
+Neovim layout wrapping~
+ Neovim enables line wrapping by default. Add `setlocal nowrap` in your config
+ if columns in vim-mlflow buffers appear misaligned.
+
+Artifacts~
+ Text artifacts (`*.txt`, `*.json`, `*.yaml`, `MLmodel`) open directly. Binary
+ artifacts are listed but not opened.
+
+==============================================================================
+8. Further reading *vim-mlflow-links*
+
+Primary documentation (repository):
+ - Project README for animated demos and deeper background.
+ - `doc/configuration_params.md` for the full configuration reference.
+ - CONTRIBUTING.md for contribution guidelines and support expectations.
+
+==============================================================================
+
+vim:tw=78:ts=8:ft=help:norl:
diff --git a/plugin/vim-mlflow.vim b/plugin/vim-mlflow.vim
index dd6649b..84f4c16 100644
--- a/plugin/vim-mlflow.vim
+++ b/plugin/vim-mlflow.vim
@@ -1,7 +1,18 @@
-let g:vim_mlflow_version = get(g:, 'vim_mlflow_version', '1.0.0')
+scriptencoding utf-8
let s:plugin_root_dir = fnamemodify(resolve(expand(':p')), ':h')
-if !has('python3')
+let s:version_path = fnamemodify(s:plugin_root_dir . '/../VERSION', ':p')
+if filereadable(s:version_path)
+ let s:raw_version = readfile(s:version_path)
+ let s:file_version = empty(s:raw_version) ? 'dev' : trim(s:raw_version[0])
+else
+ let s:file_version = 'dev'
+endif
+let g:vim_mlflow_version = get(g:, 'vim_mlflow_version', s:file_version)
+
+let s:num_expts = 0
+let s:num_runs = 0
+if !get(g:, 'vim_mlflow_skip_python_check', 0) && !has('python3')
echo 'Error: vim must be compiled with +python3 to run the vim-mlflow plugin.'
finish
endif
@@ -65,10 +76,10 @@ function! SetDefaults()
let g:vim_mlflow_artifact_expanded = get(g:, 'vim_mlflow_artifact_expanded', {})
let g:vim_mlflow_artifacts_max_depth = get(g:, 'vim_mlflow_artifacts_max_depth', 3)
let g:vim_mlflow_section_order = get(g:, 'vim_mlflow_section_order', ['params', 'metrics', 'tags', 'artifacts'])
- if type(g:vim_mlflow_section_order) != type([])
+ if type(g:vim_mlflow_section_order) !=# type([])
let g:vim_mlflow_section_order = ['params', 'metrics', 'tags', 'artifacts']
else
- let g:vim_mlflow_section_order = filter(copy(g:vim_mlflow_section_order), {_, v -> index(['params', 'metrics', 'tags', 'artifacts'], v) != -1})
+ let g:vim_mlflow_section_order = filter(copy(g:vim_mlflow_section_order), {_, v -> index(['params', 'metrics', 'tags', 'artifacts'], v) !=# -1})
if empty(g:vim_mlflow_section_order)
let g:vim_mlflow_section_order = ['params', 'metrics', 'tags', 'artifacts']
endif
@@ -135,17 +146,17 @@ function! s:ColorizePlotBuffer()
call matchadd(g:vim_mlflow_color_plot_title, '\%1l.*')
call matchadd(g:vim_mlflow_color_selectedexpt, '\%1l\zs#[0-9]\+\ze', 15)
call matchadd(g:vim_mlflow_color_selectedrun, '\%1l\zs#[0-9a-zA-Z]\{5}\ze', 15)
- if g:vim_mlflow_icon_hdivider != ''
+ if g:vim_mlflow_icon_hdivider !=# ''
call matchadd(g:vim_mlflow_color_plot_axes, '\V' . g:vim_mlflow_icon_hdivider)
endif
- if g:vim_mlflow_icon_vdivider != ''
+ if g:vim_mlflow_icon_vdivider !=# ''
call matchadd(g:vim_mlflow_color_plot_axes, '\V' . g:vim_mlflow_icon_vdivider)
endif
call matchadd(g:vim_mlflow_color_plot_axes, '\V+')
- if g:vim_mlflow_icon_plotpts != ''
+ if g:vim_mlflow_icon_plotpts !=# ''
call matchadd(g:vim_mlflow_color_plotpts, '\V' . g:vim_mlflow_icon_plotpts)
endif
- if g:vim_mlflow_icon_between_plotpts != ''
+ if g:vim_mlflow_icon_between_plotpts !=# ''
call matchadd(g:vim_mlflow_color_between_plotpts, '\V' . g:vim_mlflow_icon_between_plotpts)
endif
endfunction
@@ -193,11 +204,11 @@ function! RunMLflow()
" Set the defaults for all the user-specifiable options
call SetDefaults()
- if bufwinnr(g:vim_mlflow_buffername) == -1
+ if bufwinnr(g:vim_mlflow_buffername) ==# -1
" Open a new split on specified side
- if g:vim_mlflow_vside == 'left'
+ if g:vim_mlflow_vside ==# 'left'
execute 'lefta vsplit ' . g:vim_mlflow_buffername
- elseif g:vim_mlflow_vside == 'right'
+ elseif g:vim_mlflow_vside ==# 'right'
execute 'rightb vsplit ' . g:vim_mlflow_buffername
else
echo 'unrecognized value for g:vim_mlflow_vside = ' . g:vim_mlflow_vside
@@ -239,11 +250,11 @@ endfunction
function! OpenRunsWindow()
- if bufwinnr(g:vim_mlflow_runs_buffername) == -1
+ if bufwinnr(g:vim_mlflow_runs_buffername) ==# -1
" Open a new split on specified side
- if g:vim_mlflow_hside == 'below'
+ if g:vim_mlflow_hside ==# 'below'
execute 'botright split ' . g:vim_mlflow_runs_buffername
- elseif g:vim_mlflow_hside == 'above'
+ elseif g:vim_mlflow_hside ==# 'above'
execute 'topleft split ' . g:vim_mlflow_runs_buffername
else
echo 'unrecognized value for g:vim_mlflow_hside = ' . g:vim_mlflow_hside
@@ -285,14 +296,14 @@ function! RemoveMarkedRunViaCurpos()
" Maybe some issue with vim 'magic'; if () could work then substitute is unnecesary.
let l:run = substitute(matchstr(l:currentLine, '\m\#[0-9a-fA-F]\{5\} '), '[\# ]', '', 'g')
- if l:run != ''
+ if l:run !=# ''
let s:markruns_list = filter(s:markruns_list, 'v:val[:5] !~ "'.l:run[:5].'"')
endif
call RefreshRunsBuffer()
" Also refresh the main pane so marks disappear there too.
let l:current_win = win_getid()
let l:mlflow_winnr = bufwinnr(g:vim_mlflow_buffername)
- if l:mlflow_winnr != -1
+ if l:mlflow_winnr !=# -1
execute l:mlflow_winnr . 'wincmd w'
call RefreshMLflowBuffer(0)
call win_gotoid(l:current_win)
@@ -309,13 +320,13 @@ function! AssignExptRunFromCurpos(curpos)
let l:expt = substitute(matchstr(l:currentLine, '\m\#[0-9]\{1,4\}\:'), '[\#\:]', '', 'g')
let l:run = substitute(matchstr(l:currentLine, '\m\#[0-9a-fA-F]\{5\}\:'), '[\#\:]', '', 'g')
- if l:expt != ''
+ if l:expt !=# ''
let s:current_exptid = l:expt
let s:current_runid = ''
let s:runs_first_idx = 0
let g:vim_mlflow_artifact_expanded = {}
endif
- if l:run != ''
+ if l:run !=# ''
let s:current_runid = l:run
let g:vim_mlflow_artifact_expanded = {}
endif
@@ -330,7 +341,7 @@ function! RefreshMLflowBuffer(doassign, ...)
let l:reset_artifacts = 0
" Allow callers to pass cursor position and/or reset flag via a:000.
if len(a:000) >= 1
- if type(a:000[0]) == type([])
+ if type(a:000[0]) ==# type([])
let l:curpos = a:000[0]
if len(a:000) >= 2
let l:reset_artifacts = a:000[1]
@@ -400,12 +411,12 @@ endfunction
function! ColorizeRunsBuffer()
call matchadd(g:vim_mlflow_color_titles, 'expt_id.*$')
- if g:vim_mlflow_icon_vdivider != ''
+ if g:vim_mlflow_icon_vdivider !=# ''
call matchadd(g:vim_mlflow_color_divlines, repeat(g:vim_mlflow_icon_vdivider, 4).'*')
endif
call matchadd(g:vim_mlflow_color_help, '^".*')
call matchadd(g:vim_mlflow_color_hiddencol, ' : ')
- if g:vim_mlflow_icon_vdivider != ''
+ if g:vim_mlflow_icon_vdivider !=# ''
call matchadd(g:vim_mlflow_color_hiddencol, ' '.g:vim_mlflow_icon_vdivider.' ')
endif
endfunction
@@ -418,17 +429,17 @@ function! ColorizeMLflowBuffer()
call matchadd(g:vim_mlflow_color_titles, 'Metrics in run .*:')
call matchadd(g:vim_mlflow_color_titles, 'Artifacts in run .*:')
call matchadd(g:vim_mlflow_color_titles, 'Tags in run .*:')
- if g:vim_mlflow_icon_vdivider != ''
+ if g:vim_mlflow_icon_vdivider !=# ''
call matchadd(g:vim_mlflow_color_divlines, repeat(g:vim_mlflow_icon_vdivider, 4).'*')
endif
- if g:vim_mlflow_icon_scrollstop != ''
+ if g:vim_mlflow_icon_scrollstop !=# ''
call matchadd(g:vim_mlflow_color_scrollicons, '^'.g:vim_mlflow_icon_scrollstop)
endif
call matchadd(g:vim_mlflow_color_scrollicons, '^'.g:vim_mlflow_icon_scrollup)
call matchadd(g:vim_mlflow_color_scrollicons, '^'.g:vim_mlflow_icon_scrolldown)
call matchadd(g:vim_mlflow_color_help, '^".*')
call matchadd(g:vim_mlflow_color_markrun, '^'.g:vim_mlflow_icon_markrun.'.*$')
- if s:expt_hi != ''
+ if s:expt_hi !=# ''
call matchdelete(s:expt_hi)
call matchdelete(s:run_hi)
endif
@@ -456,7 +467,7 @@ function! MarkRun()
\ l:curpos[1]<=l:top_to_expts+s:actual_expts_length+l:expts_to_runs+s:actual_runs_length
let l:currentLine = getline(l:curpos[1])
let l:runid5 = substitute(matchstr(l:currentLine, '\m\#[0-9a-fA-F]\{5\}\:'), '[\#\:]', '', 'g')
- if l:runid5 != ''
+ if l:runid5 !=# ''
if index(s:markruns_list, l:runid5) >= 0 " If item is in the list.
call remove(s:markruns_list, index(s:markruns_list, l:runid5))
else
@@ -564,7 +575,7 @@ function! HideColumn()
let s:numreducedcols = len(l:hiddencols_le_colnum)
let l:colnum = len(split(l:line, ' ')) - 1 + s:numreducedcols
- if index(s:hiddencols_list, l:colnum) == -1
+ if index(s:hiddencols_list, l:colnum) ==# -1
call add(s:hiddencols_list, str2nr(l:colnum))
endif
call RefreshRunsBuffer()
@@ -675,7 +686,7 @@ function! ListHelpMsg()
normal! 1G
let s:help_msg_is_showing = 1
else
- execute "normal! 1G". len(s:helptext) . "dd"
+ execute 'normal! 1G' . len(s:helptext) . 'dd'
call append(line('^'), '" Press ? for help')
call append(line('^'), l:title)
let s:help_msg_is_showing = 0
@@ -709,7 +720,7 @@ function! RunsListHelpMsg()
normal! 1G
let s:runhelp_msg_is_showing = 1
else
- execute "normal! 1G". len(s:helptext) . "dd"
+ execute 'normal! 1G' . len(s:helptext) . 'dd'
call append(line('^'), '" Press ? for help')
call append(line('^'), 'Vim-MLflow Marked Runs')
let s:runhelp_msg_is_showing = 0
@@ -832,13 +843,13 @@ function! HandleMetricPlotUnderCursor()
let l:line = line('.')
let l:curline = getline('.')
let l:metric_lines = get(g:, 'vim_mlflow_metric_lines', [])
- if type(l:metric_lines) != type([])
+ if type(l:metric_lines) !=# type([])
let l:metric_lines = []
endif
if empty(l:metric_lines)
return 0
endif
- if index(l:metric_lines, l:line) == -1
+ if index(l:metric_lines, l:line) ==# -1
return 0
endif
if l:curline !~# '^\s\{2,}\S\+:'
@@ -849,30 +860,30 @@ function! HandleMetricPlotUnderCursor()
return 0
endif
let l:metric = l:match[1]
- if ! exists("s:current_runid") || s:current_runid == ""
- echo "vim-mlflow: no run selected."
+ if ! exists('s:current_runid') || s:current_runid ==# ''
+ echo 'vim-mlflow: no run selected.'
return 1
endif
let l:all_histories = get(g:, 'vim_mlflow_metric_histories', {})
- if type(l:all_histories) != type({})
+ if type(l:all_histories) !=# type({})
let l:all_histories = {}
endif
if ! has_key(l:all_histories, s:current_runid)
- echo "vim-mlflow: metric history unavailable; try refreshing."
+ echo 'vim-mlflow: metric history unavailable; try refreshing.'
return 1
endif
let l:histories = l:all_histories[s:current_runid]
if ! has_key(l:histories, l:metric)
- echo "vim-mlflow: metric history not found."
+ echo 'vim-mlflow: metric history not found.'
return 1
endif
let l:history = l:histories[l:metric]
if len(l:history) <= 1
- echo "vim-mlflow: metric has no series to plot."
+ echo 'vim-mlflow: metric has no series to plot.'
return 1
endif
if ! exists('*json_encode')
- echo "vim-mlflow: json support is required for plotting."
+ echo 'vim-mlflow: json support is required for plotting.'
return 1
endif
let l:history_json = json_encode(l:history)
@@ -939,12 +950,12 @@ vim.vars['vim_mlflow_plot_lines'] = lines
vim.vars['vim_mlflow_plot_title'] = title
EOF
if ! exists('g:vim_mlflow_plot_lines')
- echo "vim-mlflow: failed to generate plot."
+ echo 'vim-mlflow: failed to generate plot.'
return 1
endif
let l:plot_lines = get(g:, 'vim_mlflow_plot_lines', [])
if empty(l:plot_lines)
- echo "vim-mlflow: no plot data available."
+ echo 'vim-mlflow: no plot data available.'
return 1
endif
let l:plot_title = get(g:, 'vim_mlflow_plot_title', '')
@@ -963,7 +974,7 @@ function! MLflowActionUnderCursor()
let l:line = line('.')
let l:key = string(l:line)
for l:entry in get(g:, 'vim_mlflow_section_headers', [])
- if get(l:entry, 'line', -1) == l:line
+ if get(l:entry, 'line', -1) ==# l:line
let l:section = get(l:entry, 'section', '')
if !empty(l:section)
call ToggleSection(l:section)
@@ -979,7 +990,7 @@ function! MLflowActionUnderCursor()
if l:info.openable
call OpenArtifactFile(l:info.path)
else
- echo "vim-mlflow: artifact not opened (unsupported type)."
+ echo 'vim-mlflow: artifact not opened (unsupported type).'
endif
endif
return 1
@@ -1006,7 +1017,7 @@ endfunction
function! RotateMLflowSections()
- let l:order = filter(copy(g:vim_mlflow_section_order), {_, v -> index(['params', 'metrics', 'tags', 'artifacts'], v) != -1})
+ let l:order = filter(copy(g:vim_mlflow_section_order), {_, v -> index(['params', 'metrics', 'tags', 'artifacts'], v) !=# -1})
if empty(l:order)
let l:order = ['params', 'metrics', 'tags', 'artifacts']
endif
@@ -1033,7 +1044,7 @@ endfunction
function! ToggleSection(section)
- if index(['params', 'metrics', 'tags', 'artifacts'], a:section) == -1
+ if index(['params', 'metrics', 'tags', 'artifacts'], a:section) ==# -1
return
endif
call s:ToggleSectionInternal(a:section)
@@ -1064,7 +1075,7 @@ endfunction
function! OpenArtifactFile(path)
- if a:path == ''
+ if a:path ==# ''
return
endif
python3 << EOF
@@ -1123,7 +1134,7 @@ except Exception as exc:
vim.vars['vim_mlflow_artifact_error'] = str(exc)
EOF
if !exists('g:vim_mlflow_artifact_local')
- echo "vim-mlflow: failed to download artifact."
+ echo 'vim-mlflow: failed to download artifact.'
return
endif
let l:local = g:vim_mlflow_artifact_local
@@ -1142,7 +1153,7 @@ EOF
if filereadable(l:local)
call s:ShowArtifactBuffer(a:path, l:local)
else
- echo "vim-mlflow: artifact not readable."
+ echo 'vim-mlflow: artifact not readable.'
endif
endfunction
@@ -1151,9 +1162,9 @@ function! s:ShowArtifactBuffer(path, localpath)
let l:current_win = win_getid()
let l:bufname = 'artifact://' . a:path
let l:winnr = bufwinnr(l:bufname)
- if l:winnr == -1
+ if l:winnr ==# -1
let l:scratch = s:FindScratchWindow()
- if l:scratch != -1
+ if l:scratch !=# -1
call win_gotoid(l:scratch)
execute 'enew'
else
@@ -1187,13 +1198,13 @@ endfunction
function! s:SetBufferFiletype(path)
let l:lower = tolower(a:path)
- if l:lower =~ '\v\.json$'
+ if l:lower =~# '\v\.json$'
setfiletype json
- elseif l:lower =~ '\v\.(yaml|yml)$'
+ elseif l:lower =~# '\v\.(yaml|yml)$'
setfiletype yaml
- elseif l:lower =~ '\v\.txt$'
+ elseif l:lower =~# '\v\.txt$'
setfiletype text
- elseif l:lower =~ '\vmlmodel$'
+ elseif l:lower =~# '\vmlmodel$'
setfiletype yaml
else
setfiletype text
@@ -1208,7 +1219,7 @@ function! s:FindScratchWindow()
continue
endif
let l:name = bufname(l:buf)
- if (empty(l:name) || l:name =~? '^artifact://') && getbufvar(l:buf, '&buftype') == ''
+ if (empty(l:name) || l:name =~? '^artifact://') && getbufvar(l:buf, '&buftype') ==# ''
return win_getid(l:w)
endif
endfor
diff --git a/python/vim_mlflow.py b/python/vim_mlflow.py
index 51fd902..77bba08 100644
--- a/python/vim_mlflow.py
+++ b/python/vim_mlflow.py
@@ -1,20 +1,15 @@
-from datetime import datetime
-from urllib.request import urlopen
-import math
-import os
-import json
-import tempfile
import contextlib
import io
+import json
+import math
+import os
+from datetime import datetime
+from urllib.request import urlopen
import mlflow
+import vim
from mlflow.entities import ViewType
from mlflow.tracking import MlflowClient
-# import pandas as pd
-import vim
-import warnings
-
-#warnings.simplefilter(action='ignore', category=FutureWarning)
from vim_mlflow_utils import format_run_duration
@@ -42,7 +37,10 @@ def getMLflowExpts(mlflow_tracking_uri):
output_lines = []
num_expts_viewtype = len(expts)
vim.command("let s:num_expts='" + str(num_expts_viewtype) + "'")
- output_lines.append(f"{vim.eval('s:num_expts')} {VIEWTYPE_LABELS.get(view_idx, VIEWTYPE_LABELS[1])} Experiments:")
+ output_lines.append(
+ f"{vim.eval('s:num_expts')} {VIEWTYPE_LABELS.get(view_idx, VIEWTYPE_LABELS[1])} "
+ "Experiments:"
+ )
if vim.eval("g:vim_mlflow_show_scrollicons"):
if int(vim.eval("s:expts_first_idx")) == 0:
scrollicon = vim.eval("g:vim_mlflow_icon_scrollstop")
@@ -50,19 +48,26 @@ def getMLflowExpts(mlflow_tracking_uri):
scrollicon = vim.eval("g:vim_mlflow_icon_scrollup")
else:
scrollicon = ""
- output_lines.append(scrollicon + vim.eval("g:vim_mlflow_icon_vdivider")*30)
+ output_lines.append(scrollicon + vim.eval("g:vim_mlflow_icon_vdivider") * 30)
expts = sorted(expts, key=lambda e: int(e.experiment_id), reverse=True)
beginexpt_idx = int(vim.eval("s:expts_first_idx"))
- endexpt_idx = int(vim.eval("s:expts_first_idx"))+int(vim.eval("g:vim_mlflow_expts_length"))
- for expt in expts[beginexpt_idx: endexpt_idx]:
+ endexpt_idx = int(vim.eval("s:expts_first_idx")) + int(
+ vim.eval("g:vim_mlflow_expts_length")
+ )
+ for expt in expts[beginexpt_idx:endexpt_idx]:
if view_type == ViewType.ALL:
- stage_letter = lifecycles.get(expt.lifecycle_stage, expt.lifecycle_stage[:1].upper())
- output_lines.append(f"#{expt.experiment_id}: {stage_letter} {expt.name}")
+ stage_letter = lifecycles.get(
+ expt.lifecycle_stage, expt.lifecycle_stage[:1].upper()
+ )
+ output_lines.append(
+ f"#{expt.experiment_id}: {stage_letter} {expt.name}"
+ )
else:
output_lines.append(f"#{expt.experiment_id}: {expt.name}")
if vim.eval("g:vim_mlflow_show_scrollicons"):
- if int(vim.eval("s:expts_first_idx")) == \
- int(vim.eval("s:num_expts-min([g:vim_mlflow_expts_length, s:num_expts])")):
+ if int(vim.eval("s:expts_first_idx")) == int(
+ vim.eval("s:num_expts-min([g:vim_mlflow_expts_length, s:num_expts])")
+ ):
scrollicon = vim.eval("g:vim_mlflow_icon_scrollstop")
else:
scrollicon = vim.eval("g:vim_mlflow_icon_scrolldown")
@@ -72,7 +77,9 @@ def getMLflowExpts(mlflow_tracking_uri):
return output_lines, [expt.experiment_id for expt in expts]
except ModuleNotFoundError:
- print('Sorry, `mlflow` is not installed. See :h vim-mlflow for more details on setup.')
+ print(
+ "Sorry, `mlflow` is not installed. See :h vim-mlflow for more details on setup."
+ )
def getRunsListForExpt(mlflow_tracking_uri, current_exptid):
@@ -86,7 +93,10 @@ def getRunsListForExpt(mlflow_tracking_uri, current_exptid):
output_lines = []
num_runs_viewtype = len(runs)
vim.command("let s:num_runs='" + str(num_runs_viewtype) + "'")
- output_lines.append(f"{vim.eval('s:num_runs')} {VIEWTYPE_LABELS.get(view_idx, VIEWTYPE_LABELS[1])} Runs in expt #{current_exptid}:")
+ output_lines.append(
+ f"{vim.eval('s:num_runs')} {VIEWTYPE_LABELS.get(view_idx, VIEWTYPE_LABELS[1])} "
+ f"Runs in expt #{current_exptid}:"
+ )
if vim.eval("g:vim_mlflow_show_scrollicons"):
if int(vim.eval("s:runs_first_idx")) == 0:
scrollicon = vim.eval("g:vim_mlflow_icon_scrollstop")
@@ -94,14 +104,18 @@ def getRunsListForExpt(mlflow_tracking_uri, current_exptid):
scrollicon = vim.eval("g:vim_mlflow_icon_scrollup")
else:
scrollicon = ""
- output_lines.append(scrollicon + vim.eval("g:vim_mlflow_icon_vdivider")*30)
+ output_lines.append(scrollicon + vim.eval("g:vim_mlflow_icon_vdivider") * 30)
runs = sorted(runs, key=lambda r: r.info.start_time, reverse=True)
beginrun_idx = int(vim.eval("s:runs_first_idx"))
- endrun_idx = int(vim.eval("s:runs_first_idx"))+int(vim.eval("g:vim_mlflow_runs_length"))
+ endrun_idx = int(vim.eval("s:runs_first_idx")) + int(
+ vim.eval("g:vim_mlflow_runs_length")
+ )
visible_rows = []
- for run in runs[beginrun_idx: endrun_idx]:
+ for run in runs[beginrun_idx:endrun_idx]:
if run.info.start_time:
- st = datetime.utcfromtimestamp(run.info.start_time/1e3).strftime("%Y-%m-%d %H:%M:%S")
+ st = datetime.utcfromtimestamp(run.info.start_time / 1e3).strftime(
+ "%Y-%m-%d %H:%M:%S"
+ )
else:
st = "N/A"
mark = " "
@@ -113,7 +127,9 @@ def getRunsListForExpt(mlflow_tracking_uri, current_exptid):
user = runtags.get("mlflow.user") or run.info.user_id or "-"
stage_letter = ""
if view_type == ViewType.ALL:
- stage_letter = lifecycles.get(run.info.lifecycle_stage, run.info.lifecycle_stage[:1].upper())
+ stage_letter = lifecycles.get(
+ run.info.lifecycle_stage, run.info.lifecycle_stage[:1].upper()
+ )
if run.info.start_time and run.info.end_time:
duration_seconds = (run.info.end_time - run.info.start_time) / 1e3
else:
@@ -144,11 +160,13 @@ def getRunsListForExpt(mlflow_tracking_uri, current_exptid):
duration_col = row["duration"].rjust(duration_width)
user_col = row["user"].ljust(user_width)
output_lines.append(
- f"{row['mark']}#{row['run_id']}: {stage_prefix}{row['start']} {status_col} {duration_col} {user_col} {row['name']}"
+ f"{row['mark']}#{row['run_id']}: {stage_prefix}{row['start']} {status_col} "
+ f"{duration_col} {user_col} {row['name']}"
)
if vim.eval("g:vim_mlflow_show_scrollicons"):
- if int(vim.eval("s:runs_first_idx")) == \
- int(vim.eval("s:num_runs-min([g:vim_mlflow_runs_length, s:num_runs])")):
+ if int(vim.eval("s:runs_first_idx")) == int(
+ vim.eval("s:num_runs-min([g:vim_mlflow_runs_length, s:num_runs])")
+ ):
scrollicon = vim.eval("g:vim_mlflow_icon_scrollstop")
else:
scrollicon = vim.eval("g:vim_mlflow_icon_scrolldown")
@@ -158,7 +176,9 @@ def getRunsListForExpt(mlflow_tracking_uri, current_exptid):
return output_lines, [run.info.run_id for run in runs]
except ModuleNotFoundError:
- print('Sorry, `mlflow` is not installed. See :h vim-mlflow for more details on setup.')
+ print(
+ "Sorry, `mlflow` is not installed. See :h vim-mlflow for more details on setup."
+ )
def getMetricsListForRun(mlflow_tracking_uri, current_runid, show=True, header_icon=""):
@@ -194,8 +214,8 @@ def getMetricsListForRun(mlflow_tracking_uri, current_runid, show=True, header_i
output_lines.append("")
# Cache histories in a global dict so Vimscript can access them.
- vim.vars['vim_mlflow_metric_histories'] = {current_runid: metric_histories}
- vim.vars['vim_mlflow_current_runinfo'] = {
+ vim.vars["vim_mlflow_metric_histories"] = {current_runid: metric_histories}
+ vim.vars["vim_mlflow_current_runinfo"] = {
"run_id": run.info.run_id,
"run_name": run.info.run_name or "",
"experiment_id": run.info.experiment_id,
@@ -203,7 +223,9 @@ def getMetricsListForRun(mlflow_tracking_uri, current_runid, show=True, header_i
return output_lines, metric_offsets
except ModuleNotFoundError:
- print('Sorry, `mlflow` is not installed. See :h vim-mlflow for more details on setup.')
+ print(
+ "Sorry, `mlflow` is not installed. See :h vim-mlflow for more details on setup."
+ )
def getParamsListForRun(mlflow_tracking_uri, current_runid, show=True, header_icon=""):
@@ -223,7 +245,9 @@ def getParamsListForRun(mlflow_tracking_uri, current_runid, show=True, header_ic
return output_lines
except ModuleNotFoundError:
- print('Sorry, `mlflow` is not installed. See :h vim-mlflow for more details on setup.')
+ print(
+ "Sorry, `mlflow` is not installed. See :h vim-mlflow for more details on setup."
+ )
def getTagsListForRun(mlflow_tracking_uri, current_runid, show=True, header_icon=""):
@@ -243,7 +267,9 @@ def getTagsListForRun(mlflow_tracking_uri, current_runid, show=True, header_icon
return output_lines
except ModuleNotFoundError:
- print('Sorry, `mlflow` is not installed. See :h vim-mlflow for more details on setup.')
+ print(
+ "Sorry, `mlflow` is not installed. See :h vim-mlflow for more details on setup."
+ )
def _clean_metric_history(history):
@@ -281,7 +307,7 @@ def _downsample_points(points, target_len):
end = start + 1
slice_points = points[start:end]
if not slice_points:
- slice_points = points[start:start + 1]
+ slice_points = points[start: start + 1]
avg_x = sum(p[0] for p in slice_points) / len(slice_points)
avg_y = sum(p[1] for p in slice_points) / len(slice_points)
downsampled.append((avg_x, avg_y))
@@ -306,7 +332,9 @@ def _collect_artifacts(client, run_id, path="", depth=0, max_depth=50):
"children": [],
}
if item.is_dir and depth < max_depth:
- node["children"] = _collect_artifacts(client, run_id, item.path, depth + 1, max_depth)
+ node["children"] = _collect_artifacts(
+ client, run_id, item.path, depth + 1, max_depth
+ )
nodes.append(node)
return nodes
@@ -380,14 +408,20 @@ def download_artifact_file(tracking_uri, run_id, artifact_path, target_dir):
mlflow.set_tracking_uri(tracking_uri)
client = MlflowClient()
os.makedirs(target_dir, exist_ok=True)
- with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
- local_path = client.download_artifacts(run_id, artifact_path, dst_path=target_dir)
+ with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(
+ io.StringIO()
+ ):
+ local_path = client.download_artifacts(
+ run_id, artifact_path, dst_path=target_dir
+ )
if os.path.isdir(local_path):
raise IsADirectoryError(f"{artifact_path} is a directory")
return local_path
-def render_metric_plot(run_id, metric_name, history, width, height, xaxis_mode, experiment_id, run_name):
+def render_metric_plot(
+ run_id, metric_name, history, width, height, xaxis_mode, experiment_id, run_name
+):
experiment_id = str(experiment_id) if experiment_id else "-"
run_name = str(run_name) if run_name else ""
cleaned = _clean_metric_history(history)
@@ -406,7 +440,9 @@ def render_metric_plot(run_id, metric_name, history, width, height, xaxis_mode,
if xaxis_mode not in {"step", "timestamp"}:
xaxis_mode = "step"
- if xaxis_mode == "timestamp" and all(pt.get("timestamp") is not None for pt in cleaned):
+ if xaxis_mode == "timestamp" and all(
+ pt.get("timestamp") is not None for pt in cleaned
+ ):
baseline = cleaned[0]["timestamp"]
xs = [(pt["timestamp"] - baseline) / 1000.0 for pt in cleaned]
x_label = "seconds (relative)"
@@ -495,30 +531,38 @@ def getMainPageMLflow(mlflow_tracking_uri):
out = []
version = vim.eval("get(g:, 'vim_mlflow_version', 'dev')")
out.append(f"Vim-MLflow v{version}")
- out.append("\" Press ? for help")
+ out.append('" Press ? for help')
out.append("")
out.append("")
- vim.vars['vim_mlflow_artifact_lineinfo'] = {}
- vim.vars['vim_mlflow_metric_lines'] = []
- vim.vars['vim_mlflow_section_headers'] = []
- if verifyTrackingUrl(mlflow_tracking_uri, timeout=float(vim.eval("g:vim_mlflow_timeout"))):
+ vim.vars["vim_mlflow_artifact_lineinfo"] = {}
+ vim.vars["vim_mlflow_metric_lines"] = []
+ vim.vars["vim_mlflow_section_headers"] = []
+ if verifyTrackingUrl(
+ mlflow_tracking_uri, timeout=float(vim.eval("g:vim_mlflow_timeout"))
+ ):
text, exptids = getMLflowExpts(mlflow_tracking_uri)
out.extend(text)
out.append("")
if vim.eval("s:current_exptid") == "":
vim.command("let s:current_exptid='" + exptids[0] + "'")
- text, runids = getRunsListForExpt(mlflow_tracking_uri, vim.eval("s:current_exptid"))
+ text, runids = getRunsListForExpt(
+ mlflow_tracking_uri, vim.eval("s:current_exptid")
+ )
out.extend(text)
out.append("")
if runids:
if vim.eval("s:current_runid") == "":
vim.command("let s:current_runid='" + runids[0] + "'")
- elif len(vim.eval("s:current_runid"))==5:
- fullrunid = [runid for runid in runids if runid[:5]==vim.eval("s:current_runid")][0]
+ elif len(vim.eval("s:current_runid")) == 5:
+ fullrunid = [
+ runid
+ for runid in runids
+ if runid[:5] == vim.eval("s:current_runid")
+ ][0]
vim.command("let s:current_runid='" + fullrunid + "'")
section_order = vim.eval("g:vim_mlflow_section_order")
if not section_order:
- section_order = ['params', 'metrics', 'tags', 'artifacts']
+ section_order = ["params", "metrics", "tags", "artifacts"]
states = {
"params": vim.eval("s:params_are_showing") == "1",
"metrics": vim.eval("s:metrics_are_showing") == "1",
@@ -532,14 +576,21 @@ def getMainPageMLflow(mlflow_tracking_uri):
section_header_entries = []
metric_line_numbers = []
for section in section_order:
- if section not in ('params', 'metrics', 'tags', 'artifacts'):
+ if section not in ("params", "metrics", "tags", "artifacts"):
continue
show = states.get(section, False)
header_icon = open_icon if show else closed_icon
header_line = len(out) + 1
- if section == 'params':
- out.extend(getParamsListForRun(mlflow_tracking_uri, current_run, show=show, header_icon=header_icon))
- elif section == 'metrics':
+ if section == "params":
+ out.extend(
+ getParamsListForRun(
+ mlflow_tracking_uri,
+ current_run,
+ show=show,
+ header_icon=header_icon,
+ )
+ )
+ elif section == "metrics":
metrics_output, offsets = getMetricsListForRun(
mlflow_tracking_uri,
current_run,
@@ -548,19 +599,32 @@ def getMainPageMLflow(mlflow_tracking_uri):
)
out.extend(metrics_output)
if show:
- metric_line_numbers.extend([header_line + offset for offset in offsets])
- elif section == 'tags':
- out.extend(getTagsListForRun(mlflow_tracking_uri, current_run, show=show, header_icon=header_icon))
- elif section == 'artifacts':
+ metric_line_numbers.extend(
+ [header_line + offset for offset in offsets]
+ )
+ elif section == "tags":
+ out.extend(
+ getTagsListForRun(
+ mlflow_tracking_uri,
+ current_run,
+ show=show,
+ header_icon=header_icon,
+ )
+ )
+ elif section == "artifacts":
divider_char = vim.eval("g:vim_mlflow_icon_vdivider") or "-"
max_depth = int(vim.eval("g:vim_mlflow_artifacts_max_depth"))
mark_icon = closed_icon
open_dir_icon = open_icon
if show:
- expanded_json = vim.eval("json_encode(get(g:, 'vim_mlflow_artifact_expanded', {}))")
+ expanded_json = vim.eval(
+ "json_encode(get(g:, 'vim_mlflow_artifact_expanded', {}))"
+ )
expanded = json.loads(expanded_json)
client = MlflowClient(tracking_uri=mlflow_tracking_uri)
- tree = _collect_artifacts(client, current_run, max_depth=max_depth)
+ tree = _collect_artifacts(
+ client, current_run, max_depth=max_depth
+ )
artifact_lines, artifact_info = _render_artifact_section(
short_run,
tree,
@@ -579,26 +643,28 @@ def getMainPageMLflow(mlflow_tracking_uri):
target_line = start_line + offset
entry["line"] = target_line
lineinfo_map[str(target_line)] = entry
- vim.vars['vim_mlflow_artifact_lineinfo'] = lineinfo_map
+ vim.vars["vim_mlflow_artifact_lineinfo"] = lineinfo_map
out.extend(artifact_lines)
else:
- vim.vars['vim_mlflow_artifact_lineinfo'] = {}
+ vim.vars["vim_mlflow_artifact_lineinfo"] = {}
header = f"{header_icon} Artifacts in run #{short_run}:"
out.extend([header, divider_char * 30, ""])
section_header_entries.append({"line": header_line, "section": section})
- vim.vars['vim_mlflow_section_headers'] = section_header_entries
- vim.vars['vim_mlflow_metric_lines'] = metric_line_numbers
+ vim.vars["vim_mlflow_section_headers"] = section_header_entries
+ vim.vars["vim_mlflow_metric_lines"] = metric_line_numbers
else:
- vim.vars['vim_mlflow_artifact_lineinfo'] = {}
- vim.vars['vim_mlflow_section_headers'] = []
- vim.vars['vim_mlflow_metric_lines'] = []
+ vim.vars["vim_mlflow_artifact_lineinfo"] = {}
+ vim.vars["vim_mlflow_section_headers"] = []
+ vim.vars["vim_mlflow_metric_lines"] = []
else:
out.append("Could not connect to mlflow_tracking_uri")
out.append(mlflow_tracking_uri)
- out.append(f"within the g:vim_mlflow_timeout={float(vim.eval('g:vim_mlflow_timeout')):.2f}")
+ out.append(
+ f"within the g:vim_mlflow_timeout={float(vim.eval('g:vim_mlflow_timeout')):.2f}"
+ )
out.append("Are you sure that's the right URI?")
- vim.vars['vim_mlflow_section_headers'] = []
- vim.vars['vim_mlflow_metric_lines'] = []
+ vim.vars["vim_mlflow_section_headers"] = []
+ vim.vars["vim_mlflow_metric_lines"] = []
return out
@@ -615,9 +681,10 @@ def verifyTrackingUrl(url, timeout=1.0):
raise RuntimeError("Incorrect and possibly insecure protocol in url")
try:
- if urlopen(url, timeout=timeout).getcode()==200:
+ if urlopen(url, timeout=timeout).getcode() == 200:
out = True
- except:
+ except Exception as exc: # fallback if you tr
+ print("Unexpected failure: %s", exc)
out = False
return out
diff --git a/python/vim_mlflow_runs.py b/python/vim_mlflow_runs.py
index ceb9e85..4b41c38 100644
--- a/python/vim_mlflow_runs.py
+++ b/python/vim_mlflow_runs.py
@@ -1,15 +1,11 @@
-from datetime import datetime
import re
-from urllib.request import urlopen, Request
+from urllib.request import urlopen
-import mlflow
from mlflow.entities import ViewType
from mlflow.tracking import MlflowClient
import pandas as pd
import vim
-import warnings
-#warnings.simplefilter(action='ignore', category=FutureWarning)
from vim_mlflow_utils import format_run_duration
@@ -33,7 +29,6 @@ def getRunsPageMLflow(mlflow_tracking_uri):
out.append("No marked runs.")
return out
-
if verifyTrackingUrl(mlflow_tracking_uri, timeout=float(vim.eval("g:vim_mlflow_timeout"))):
# Find full runids for the short-runids in s:markruns_list
@@ -42,7 +37,10 @@ def getRunsPageMLflow(mlflow_tracking_uri):
view_type = VIEWTYPE_MAP.get(view_idx, ViewType.ACTIVE_ONLY)
runinfos = []
if vim.eval("s:current_exptid") != "":
- runinfos = client.search_runs([str(vim.eval("s:current_exptid"))], run_view_type=view_type)
+ runinfos = client.search_runs(
+ [str(vim.eval("s:current_exptid"))],
+ run_view_type=view_type
+ )
fullmarkrunids = []
for run in runinfos:
if run.info.run_id[:5] in vim.eval("s:markruns_list"):
@@ -60,11 +58,11 @@ def getRunsPageMLflow(mlflow_tracking_uri):
for runid in fullmarkrunids:
mldict = client.get_run(runid).to_dictionary()
rundict = mldict["info"]
- if vim.eval("s:runs_tags_are_showing")=='1':
+ if vim.eval("s:runs_tags_are_showing") == '1':
rundict.update(mldict["data"]["tags"])
- if vim.eval("s:runs_params_are_showing")=='1':
+ if vim.eval("s:runs_params_are_showing") == '1':
rundict.update(mldict["data"]["params"])
- if vim.eval("s:runs_metrics_are_showing")=='1':
+ if vim.eval("s:runs_metrics_are_showing") == '1':
rundict.update(mldict["data"]["metrics"])
runsforpd.append(rundict)
runsdf = pd.DataFrame(runsforpd)
@@ -86,8 +84,11 @@ def getRunsPageMLflow(mlflow_tracking_uri):
"mlflow.project.env": "env",
}
)
- runsdf = runsdf.drop(columns=["run_uuid", "user_id", "mlflow.gitRepoURL"], errors="ignore") # duplicated cols
- runsdf = runsdf.drop(columns=["mlflow.log-model.history", "artifact_uri"], errors="ignore") # huge columns
+ # duplicated cols:
+ runsdf = runsdf.drop(columns=["run_uuid", "user_id", "mlflow.gitRepoURL"], errors="ignore")
+ # huge columns:
+ runsdf = runsdf.drop(columns=["mlflow.log-model.history", "artifact_uri"], errors="ignore")
+
if "start_time" in runsdf.columns:
runsdf.insert(0, "start_time", runsdf.pop("start_time"))
if "end_time" in runsdf.columns:
@@ -96,7 +97,7 @@ def getRunsPageMLflow(mlflow_tracking_uri):
runsdf.insert(0, "expt_id", runsdf.pop("expt_id"))
# Shorten specified columns
- runsdf["run_id"] = runsdf["run_id"].apply(lambda x: x[:5]) # run_id is always in run results!
+ runsdf["run_id"] = runsdf["run_id"].apply(lambda x: x[:5]) # run_id always in run results!
if "source.name" in runsdf.columns:
runsdf["source.name"] = runsdf["source.name"].apply(lambda x: x.split("/")[-1])
if "git.commit" in runsdf.columns:
@@ -137,14 +138,18 @@ def getRunsPageMLflow(mlflow_tracking_uri):
runsdf.columns = colnames
# Hide (remove) specified columns
- cols2keep = [int(col) for col in range(runsdf.shape[1]) if str(col) not in vim.eval("s:hiddencols_list")]
+ cols2keep = [
+ int(col)
+ for col in range(runsdf.shape[1])
+ if str(col) not in vim.eval("s:hiddencols_list")
+ ]
runsdf = runsdf.iloc[:, cols2keep]
# Output dataframe
lines = runsdf.to_string(index=False, justify="center").split('\n')
for i, line in enumerate(lines):
out.append(line)
- if i==0:
+ if i == 0:
out.append(make_headerline(lines, vim.eval("g:vim_mlflow_icon_vdivider")))
# Retaining these lines while still debugging occasional column-hiding bug:
@@ -166,7 +171,8 @@ def make_headerline(lines, divchar):
a = re.sub('[^ ]', divchar, lines[0])
for i in range(len(lines)-1):
b = re.sub('[^ ]', divchar, lines[i+1])
- a = ''.join(map(lambda x: divchar if x[0]==divchar or x[1]==divchar else ' ', zip(a, b)))
+ a = ''.join(map(lambda x: divchar if x[0] == divchar or x[1] == divchar else ' ',
+ zip(a, b)))
return a
@@ -183,9 +189,10 @@ def verifyTrackingUrl(url, timeout=1.0):
raise RuntimeError("Incorrect and possibly insecure protocol in url")
try:
- if urlopen(url, timeout=timeout).getcode()==200:
+ if urlopen(url, timeout=timeout).getcode() == 200:
out = True
- except:
+ except Exception as exc: # fallback if you tr
+ print("Unexpected failure: %s", exc)
out = False
return out
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..11d225c
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,22 @@
+# Test Suite
+
+This directory contains unit tests for both the Python helper modules and the
+Vimscript plugin logic.
+
+## Layout
+
+- `tests/python/`: Pytest-based unit tests that exercise `python/vim_mlflow*.py`.
+- `tests/vim/`: Headless Vimscript assertions run through `vim` or `nvim`.
+- `tests/fixtures/`: Lightweight helpers that build fake MLflow objects for tests.
+
+## Running Locally
+
+```bash
+# Python tests
+pytest tests/python
+
+# Vimscript tests (requires vim or nvim)
+nvim --headless -u NONE -i NONE -c "source tests/vim/run_tests.vim" -c "qa"
+# or
+vim -Es -u NONE -i NONE -c "source tests/vim/run_tests.vim" -c "qa"
+```
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/mlflow.py b/tests/fixtures/mlflow.py
new file mode 100644
index 0000000..1f25fcb
--- /dev/null
+++ b/tests/fixtures/mlflow.py
@@ -0,0 +1,65 @@
+"""Lightweight helpers to build fake MLflow objects for tests."""
+
+from types import SimpleNamespace
+from typing import Dict, Iterable, Optional
+
+
+def make_experiment(exp_id: int, name: str, lifecycle: str = "active") -> SimpleNamespace:
+ """Return a minimal stand-in for mlflow.entities.Experiment."""
+ return SimpleNamespace(
+ experiment_id=str(exp_id),
+ name=name,
+ lifecycle_stage=lifecycle,
+ )
+
+
+def make_run(
+ run_id: str,
+ start_time_ms: Optional[int],
+ end_time_ms: Optional[int],
+ *,
+ status: str = "FINISHED",
+ lifecycle_stage: str = "active",
+ user_id: str = "user",
+ run_name: str = "",
+ tags: Optional[Dict[str, str]] = None,
+ metrics: Optional[Dict[str, float]] = None,
+) -> SimpleNamespace:
+ """Return a minimal stand-in for mlflow.entities.Run."""
+ merged_tags = {"mlflow.user": user_id}
+ if run_name:
+ merged_tags["mlflow.runName"] = run_name
+ if tags:
+ merged_tags.update(tags)
+
+ run_metrics = metrics or {}
+
+ info = SimpleNamespace(
+ run_id=run_id,
+ start_time=start_time_ms,
+ end_time=end_time_ms,
+ status=status,
+ lifecycle_stage=lifecycle_stage,
+ user_id=user_id,
+ run_name=run_name,
+ )
+ data = SimpleNamespace(
+ tags=merged_tags,
+ metrics=run_metrics,
+ )
+ return SimpleNamespace(info=info, data=data)
+
+
+def make_metric_history(values: Iterable[float], *, start_step: int = 0) -> list:
+ """Build a sequence that mimics mlflow.entities.Metric."""
+ history = []
+ timestamp = 1_700_000_000_000
+ for idx, value in enumerate(values, start=start_step):
+ history.append(
+ SimpleNamespace(
+ step=idx,
+ timestamp=timestamp + idx * 1000,
+ value=value,
+ )
+ )
+ return history
diff --git a/tests/python/__init__.py b/tests/python/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/python/conftest.py b/tests/python/conftest.py
new file mode 100644
index 0000000..0ea068e
--- /dev/null
+++ b/tests/python/conftest.py
@@ -0,0 +1,123 @@
+import ast
+import importlib
+import re
+import sys
+import types
+from pathlib import Path
+from typing import Dict, List
+
+import pytest
+
+
+ROOT = Path(__file__).resolve().parents[2]
+PYTHON_SRC = ROOT / "python"
+if str(PYTHON_SRC) not in sys.path:
+ sys.path.insert(0, str(PYTHON_SRC))
+
+
+class _FakeVim:
+ """A very small subset of the `vim` module needed for unit tests."""
+
+ def __init__(self) -> None:
+ self.vars: Dict[str, object] = {}
+ self.g: Dict[str, object] = {}
+ self.s: Dict[str, object] = {}
+
+ def eval(self, expression: str):
+ if expression.startswith("g:"):
+ return self.g.get(expression[2:], 0)
+ if expression.startswith("s:"):
+ return self.s.get(expression[2:], 0)
+
+ def _replace(match: re.Match) -> str:
+ scope = match.group(1)
+ name = match.group(2)
+ mapping = "_g" if scope == "g" else "_s"
+ return f"{mapping}['{name}']"
+
+ translated = re.sub(r"([gs]):([A-Za-z0-9_]+)", _replace, expression)
+ return eval(translated, {"min": min}, {"_g": self.g, "_s": self.s}) # noqa: S307
+
+ def command(self, command: str) -> None:
+ if not command.startswith("let "):
+ raise NotImplementedError("Only :let commands are supported in tests.")
+ rest = command[4:].strip()
+ name, raw_value = rest.split("=", 1)
+ name = name.strip()
+ raw_value = raw_value.strip()
+ value = self._parse_value(raw_value)
+ if name.startswith("g:"):
+ self.g[name[2:]] = value
+ elif name.startswith("s:"):
+ self.s[name[2:]] = value
+ else:
+ raise NotImplementedError("Only g: and s: scopes are supported.")
+
+ @staticmethod
+ def _parse_value(raw: str):
+ try:
+ value = ast.literal_eval(raw)
+ except (ValueError, SyntaxError):
+ return raw
+ if isinstance(value, str) and value.isdigit():
+ return int(value)
+ return value
+
+
+@pytest.fixture
+def vim_mlflow_env(monkeypatch):
+ """Prepare stubbed mlflow and vim modules, then import vim_mlflow."""
+
+ fake_vim = _FakeVim()
+ vim_module = types.ModuleType("vim")
+ vim_module.vars = fake_vim.vars
+ vim_module.eval = fake_vim.eval
+ vim_module.command = fake_vim.command
+ monkeypatch.setitem(sys.modules, "vim", vim_module)
+
+ fake_state = {
+ "experiments": [],
+ "runs": [],
+ "runs_by_id": {},
+ "metric_history": {},
+ }
+
+ class FakeMlflowClient:
+ def __init__(self, tracking_uri: str):
+ self.tracking_uri = tracking_uri
+
+ def search_experiments(self, view_type):
+ return list(fake_state["experiments"])
+
+ def search_runs(self, experiment_ids: List[str], run_view_type):
+ return list(fake_state["runs"])
+
+ def get_run(self, run_id: str):
+ return fake_state["runs_by_id"][run_id]
+
+ def get_metric_history(self, run_id: str, key: str):
+ return list(fake_state["metric_history"].get((run_id, key), []))
+
+ class FakeViewType:
+ ACTIVE_ONLY = object()
+ DELETED_ONLY = object()
+ ALL = object()
+
+ mlflow_module = types.ModuleType("mlflow")
+ entities_module = types.ModuleType("mlflow.entities")
+ tracking_module = types.ModuleType("mlflow.tracking")
+
+ entities_module.ViewType = FakeViewType
+ tracking_module.MlflowClient = FakeMlflowClient
+
+ monkeypatch.setitem(sys.modules, "mlflow", mlflow_module)
+ monkeypatch.setitem(sys.modules, "mlflow.entities", entities_module)
+ monkeypatch.setitem(sys.modules, "mlflow.tracking", tracking_module)
+
+ if "vim_mlflow" in sys.modules:
+ del sys.modules["vim_mlflow"]
+ import vim_mlflow # noqa: F401
+
+ module = importlib.import_module("vim_mlflow")
+
+ return module, fake_state, fake_vim
diff --git a/tests/python/test_vim_mlflow.py b/tests/python/test_vim_mlflow.py
new file mode 100644
index 0000000..bdbf686
--- /dev/null
+++ b/tests/python/test_vim_mlflow.py
@@ -0,0 +1,103 @@
+from tests.fixtures.mlflow import make_experiment, make_metric_history, make_run
+from vim_mlflow_utils import format_run_duration
+
+
+def test_format_run_duration_handles_edge_cases():
+ assert format_run_duration(None) == "-"
+ assert format_run_duration(float("inf")) == "infh"
+ assert format_run_duration(float("nan")) == "-"
+ assert format_run_duration(-1) == "-"
+ assert format_run_duration(42) == "42s"
+ assert format_run_duration(600) == "10m"
+ assert format_run_duration(3 * 3600) == "3.0h"
+
+
+def test_get_mlflow_experiments_formats_output(vim_mlflow_env):
+ module, state, fake_vim = vim_mlflow_env
+
+ state["experiments"] = [
+ make_experiment(1, "Alpha"),
+ make_experiment(2, "Beta"),
+ ]
+
+ fake_vim.g.update(
+ {
+ "vim_mlflow_viewtype": 1,
+ "vim_mlflow_expts_length": 8,
+ "vim_mlflow_show_scrollicons": 1,
+ "vim_mlflow_icon_vdivider": "|",
+ "vim_mlflow_icon_scrollstop": "X",
+ "vim_mlflow_icon_scrollup": "^",
+ "vim_mlflow_icon_scrolldown": "v",
+ }
+ )
+ fake_vim.s.update(
+ {
+ "expts_first_idx": 0,
+ }
+ )
+
+ lines, experiment_ids = module.getMLflowExpts("http://example.com")
+
+ assert lines[0] == "2 Active Experiments:"
+ assert lines[1] == "X" + "|" * 30
+ assert lines[2] == "#2: Beta"
+ assert lines[3] == "#1: Alpha"
+ assert lines[-1] == "X"
+ assert experiment_ids == ["2", "1"]
+
+
+def test_get_runs_list_formats_columns(vim_mlflow_env):
+ module, state, fake_vim = vim_mlflow_env
+
+ start_time = 1_700_000_000_000
+ run_1 = make_run(
+ "run-aaa111",
+ start_time_ms=start_time + 5_000,
+ end_time_ms=start_time + 65_000,
+ user_id="alice",
+ run_name="Warmup",
+ )
+ run_2 = make_run(
+ "run-bbb222",
+ start_time_ms=start_time,
+ end_time_ms=None,
+ status="RUNNING",
+ user_id="bob",
+ run_name="Long job",
+ )
+
+ state["runs"] = [run_1, run_2]
+ state["runs_by_id"] = {run_1.info.run_id: run_1, run_2.info.run_id: run_2}
+ state["metric_history"][(run_1.info.run_id, "loss")] = make_metric_history([0.4, 0.2])
+
+ fake_vim.g.update(
+ {
+ "vim_mlflow_viewtype": 1,
+ "vim_mlflow_runs_length": 8,
+ "vim_mlflow_show_scrollicons": 1,
+ "vim_mlflow_icon_vdivider": "|",
+ "vim_mlflow_icon_scrollstop": "X",
+ "vim_mlflow_icon_scrollup": "^",
+ "vim_mlflow_icon_scrolldown": "v",
+ "vim_mlflow_icon_markrun": ">",
+ }
+ )
+ fake_vim.s.update(
+ {
+ "runs_first_idx": 0,
+ "markruns_list": [run_1.info.run_id[:5]],
+ }
+ )
+
+ lines, run_ids = module.getRunsListForExpt("http://example.com", "99")
+
+ assert lines[0] == "2 Active Runs in expt #99:"
+ assert lines[1] == "X" + "|" * 30
+ assert lines[2].startswith(f">{'#'}{run_1.info.run_id[:5]}")
+ assert "alice" in lines[2]
+ assert "Warmup" in lines[2]
+ assert lines[3].startswith(f" #{run_2.info.run_id[:5]}")
+ assert "RUNNING" in lines[3]
+ assert lines[-1] == "X"
+ assert run_ids == [run_1.info.run_id, run_2.info.run_id]
diff --git a/tests/vim/run_tests.vim b/tests/vim/run_tests.vim
new file mode 100644
index 0000000..5f38231
--- /dev/null
+++ b/tests/vim/run_tests.vim
@@ -0,0 +1,24 @@
+let g:vim_mlflow_skip_python_check = 1
+let v:errors = []
+
+source plugin/vim-mlflow.vim
+
+call SetDefaults()
+call assert_equal('http://localhost:5000', g:mlflow_tracking_uri)
+call assert_equal(8, g:vim_mlflow_expts_length)
+call assert_equal('-', g:vim_mlflow_icon_vdivider)
+
+let g:vim_mlflow_icon_useunicode = 1
+unlet g:vim_mlflow_icon_vdivider
+call SetDefaults()
+call assert_equal(nr2char(9472), g:vim_mlflow_icon_vdivider)
+
+let g:vim_mlflow_section_order = 'invalid'
+call SetDefaults()
+call assert_equal(['params', 'metrics', 'tags', 'artifacts'], g:vim_mlflow_section_order)
+
+if len(v:errors) > 0
+ echoerr join(v:errors, "\n")
+ " echom string(v:errors)
+ cquit 1
+endif