diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..01cdb68 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,47 @@ +name: Linting and style checking + +on: + push: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.actor }} + cancel-in-progress: true + +jobs: + stylua: + name: Formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: JohnnyMorganz/stylua-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: v2.1.0 + # CLI arguments + args: --color always --respect-ignores --check . + + gendoc: + name: Document generation + runs-on: ubuntu-latest + steps: + - name: Install Neovim + uses: rhysd/action-setup-vim@v1 + with: + neovim: true + version: v0.11.3 + - uses: actions/checkout@v4 + - name: Generate documentation + run: make --silent documentation + - name: Check for changes + run: if [[ -n $(git status -s) ]]; then exit 1; fi + + case-sensitivity: + name: File case sensitivity + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check Case Sensitivity + uses: credfeto/action-case-checker@v1.2.1 diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..6090f42 --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,6 @@ +column_width = 120 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferDouble" +call_parentheses = "Always" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8f9af0b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing + +## Adding a Python Dependency Backend + +Within `detect.lua` is a table called `check_paths` and `check_paths_ordered_keys` + +You can have either a file or pattern here to detect the python dependency program (i.e `pip` or `uv`) +which has a callback function to do the install. + +`check_paths_ordered_keys` is the order of the table of precedence when detecting + +## Snippets + +`snip/init.lua` has a table of LuaSnip snippets. + +You can add to this list and determine if the snippet can activate based on conditions like `is_in_test_file` which only loads snippets while in files with pattern `test_*` + +## Tests + +Tests are ran by [mini.nvim](https://github.com/echasnovski/mini.nvim) tests [module](https://github.com/echasnovski/mini.nvim/blob/main/TESTING.md). + +You can run tests just by running `make test` + +## Documentation + +Docs are generated by [mini.nvim](https://github.com/echasnovski/mini.nvim) docs [module](https://github.com/echasnovski/mini.doc). + +Can be updated by running `make documentation` + +## Lint + +Checked by `stylua` + +Run `make lint` to check status and `make lint-fix` to have stylua make changes. + +## Examples + +In the `examples` directory, you can find mock python projects to test dependency backends. +These can be used for local and automation testing. diff --git a/Makefile b/Makefile index 388cd50..1886ff2 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,15 @@ test: deps test_requirements test_file: deps test_requirements nvim --headless --noplugin -u ./scripts/minimal_init.lua -c "lua MiniTest.run_file('$(FILE)')" +documentation: deps/mini.nvim + nvim --headless --noplugin -u ./scripts/minimal_init.lua -c "lua require('mini.doc').generate()" -c "qa!" + +lint: + stylua --color always --respect-ignores --check . + +lint-fix: + stylua --color always --respect-ignores . + # Install all test dependencies deps: deps/mini.nvim \ deps/nvim-dap \ diff --git a/README.md b/README.md index 1e3a346..3cf1e8f 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,6 @@ Python Tools for Neovim -> [!WARNING] -> This plugin is currently in beta status and can be subject to breaking changes -> Please file issues when found and feel free to contribute - https://github.com/user-attachments/assets/025f8475-e946-4875-bc91-53508ea5d3fa ## Installation diff --git a/doc/python.txt b/doc/python.txt new file mode 100644 index 0000000..15c4545 --- /dev/null +++ b/doc/python.txt @@ -0,0 +1,690 @@ +============================================================================== +------------------------------------------------------------------------------ + *Python* + `Python` +*python.nvim* Python tool box for neovim + +MIT License Copyright (c) 2025 Joshua Cold + +============================================================================== + +|python.nvim| is a collection of tools for python development in neovim. +Helping speed up dependency management, debugging and other tasks that +apart of developing in the python language. + +Class ~ +{python} + +------------------------------------------------------------------------------ + *Python.setup()* + `Python.setup`({opts}) +Parameters ~ +{opts} `(optional)` `(python.Config)` + + +============================================================================== +------------------------------------------------------------------------------ +Class ~ +{PythonSubcommand} +Fields ~ +{impl} `(fun(args:string[], opts: table))` The command implementation +{complete} `(optional)` `(fun(subcmd_arg_lead: string): string[])` (optional) Command completions callback, taking the lead of the subcommand's arguments + +------------------------------------------------------------------------------ + *subcommand_tbl* + `subcommand_tbl` +Type ~ +`(table)` + +------------------------------------------------------------------------------ + *python_cmd()* + `python_cmd`({opts}) +Parameters ~ +{opts} `(table)` :h lua-guide-commands-create + + +============================================================================== +------------------------------------------------------------------------------ + *PythonConfig* + `PythonConfig` +Type ~ +`(python.Config)` + +------------------------------------------------------------------------------ + *PythonConfig.defaults* + `PythonConfig.defaults` +Python.nvim config +Default values: +>lua + PythonConfig.defaults = { + -- Should return a list of tables with a `name` and a `path` entry each. + -- Gets the argument `venvs_path` set below. + -- By default just lists the entries in `venvs_path`. + ---@return VEnv[] + get_venvs = function(venvs_path) + return require("python.venv").get_venvs(venvs_path) + end, + -- Path for venvs picker + venvs_path = vim.fn.expand("~/.virtualenvs"), + -- Something to do after setting an environment + post_set_venv = nil, + -- base path for creating new venvs + auto_create_venv_path = function(parent_dir) + return vim.fs.joinpath(parent_dir, ".venv") + end, + -- Patterns for autocmd LspAttach that trigger the auto venv logic + -- Add onto this list if you depend on venvs for other file types + -- like .yaml, .yml for ansible + auto_venv_lsp_attach_patterns = { "*.py" }, + + -- Filetypes to activate commands for python.nvim + command_setup_filetypes = { "python" }, + + -- Load python.nvim python snippets + python_lua_snippets = false, + + -- List of text actions to take on InsertLeave, TextChanged + -- Put in empty table or nil to disable + enabled_text_actions = { + "f-strings", -- When inserting {}, put in an f-string + }, + -- Adjust when enabled_text_actions is triggered + enabled_text_actions_autocmd_events = { "InsertLeave" }, + + treesitter = { + functions = { + -- Wrap treesitter identifier under cursor using substitute_options + wrapper = { + -- Substitute options for PythonTSWrapWithFunc + substitute_options = { + "print(%s)", + "log.debug(%s)", + "log.info(%s)", + "log.warning(%s)", + "log.error(%s)", + "np.array(%s)", + }, + + -- Look for tree-sitter types to wrap + find_types = { + "tuple", + "string", + "true", + "false", + "list", + "call", + "parenthesized_expression", + "expression_statement", + "integer", + }, + }, + }, + }, + -- Load python keymaps. Everything starting with p... + keymaps = { + -- following nvim_set_keymap() mode, lhs, rhs, opts + mappings = { + ["pv"] = { "n", "Python venv pick", { desc = "python.nvim: pick venv" } }, + ["pi"] = { "n", "Python venv install", { desc = "python.nvim: python venv install" } }, + ["pd"] = { "n", "Python dap", { desc = "python.nvim: python run debug program" } }, + + -- Test Actions + ["ptt"] = { "n", "Python test", { desc = "python.nvim: python run test suite" } }, + ["ptm"] = { "n", "Python test_method", { desc = "python.nvim: python run test method" } }, + ["ptf"] = { "n", "Python test_file", { desc = "python.nvim: python run test file" } }, + ["ptdd"] = { "n", "Python test_debug", { desc = "python.nvim: run test suite in debug mode." } }, + ["ptdm"] = { + "n", + "Python test_method_debug", + { desc = "python.nvim: run test method in debug mode." }, + }, + ["ptdf"] = { + "n", + "Python test_file_debug", + { desc = "python.nvim: run test file in debug mode." }, + }, + + -- VEnv Actions + ["ped"] = { + "n", + "Python venv delete_select", + { desc = "python.nvim: select and delete a known venv." }, + }, + ["peD"] = { "n", "Python venv delete", { desc = "python.nvim: delete current venv set." } }, + + -- Language Actions + ["ppe"] = { + "n", + "Python treesitter toggle_enumerate", + { desc = "python.nvim: turn list into enumerate" }, + }, + ["ppw"] = { + "n", + "Python treesitter wrap_cursor", + { desc = "python.nvim: wrap treesitter identifier with pattern" }, + }, + ["pw"] = { + "v", + ":Python treesitter wrap_cursor", + { desc = "python.nvim: wrap treesitter identifier with pattern" }, + }, + }, + }, + -- Settings regarding ui handling + ui = { + -- Amount of time to pause closing of ui after a finished task + ui_close_timeout = 5000, + + -- Default ui style for interfaces created by python.nvim + ---@alias python_ui_default_style "'popup'|'split'|nil" + default_ui_style = "popup", + + -- Customize the position and behavior of the ui style + popup = { + win_opts = { + -- border = "rounded", + -- relative = "win", + -- focusable = true, + -- title = "python.nvim", + -- anchor = "SE", + -- zindex = 999, + -- width = 40, + -- height = 20, + -- row = vim.o.lines - 3, + -- col = vim.o.columns -2, + }, + }, + split = { + win_opts = { + -- split = 'below', + -- win = 0, + -- width = 40, + -- height = 10, + -- focusable = true, + }, + }, + }, + + -- Tell neotest-python which test runner to use + test = { + test_runner = "pytest", + }, + } +< +Class ~ +{python.Config} + +------------------------------------------------------------------------------ + *PythonConfig.setup()* + `PythonConfig.setup`({opts}) +Parameters ~ +{opts} `(optional)` `(python.Config)` + + +============================================================================== +------------------------------------------------------------------------------ +DAP integrations + +------------------------------------------------------------------------------ + *get_venv()* + `get_venv`() +Get venv for DAP functions +Return ~ +`(VEnv | nil)` venv + +------------------------------------------------------------------------------ + *PythonDap.prepare_debugpy()* + `PythonDap.prepare_debugpy`({callback}) +Read venv with debug py and launch callback +Parameters ~ +{callback} `(optional)` `(function)` + +------------------------------------------------------------------------------ + *create_dap_config()* + `create_dap_config`({cwd}, {venv}, {python_state}) +Interactively create dap configuration +Parameters ~ +{cwd} `(string)` current working directory for python_state key +{venv} `(VEnv)` venv to attach to pythonPath +{python_state} `(PythonState)` python_state object + + +============================================================================== +------------------------------------------------------------------------------ + *hatch_installed_versions()* + `hatch_installed_versions`() +Get list of python versions hatch has installed +Return ~ +`(table)` list of available python versions from hatch + +------------------------------------------------------------------------------ + *hatch_available_versions()* + `hatch_available_versions`() +Get list of python versions hatch can install +Return ~ +`(table)` list of available python versions from hatch + +------------------------------------------------------------------------------ + *hatch_install_version()* + `hatch_install_version`({version}) +Install a python version using hatch +Parameters ~ +{version} `(string)` Python version to install via hatch + +------------------------------------------------------------------------------ + *hatch_delete_version()* + `hatch_delete_version`({version}) +Delete a python version using hatch +Parameters ~ +{version} `(string)` Python version to delete via hatch + + +============================================================================== +------------------------------------------------------------------------------ + *PythonLSPCommands.pyright_change_type_checking()* + `PythonLSPCommands.pyright_change_type_checking`({mode}) +Change the type checking mode of either basedpyright or pyright if found. +Parameters ~ +{mode} `(string)` type checking mode + + +============================================================================== +------------------------------------------------------------------------------ + *PythonStateVEnv* + `PythonStateVEnv` +Class ~ +{PythonStateVEnv} +Fields ~ +{python_interpreter} `(string | nil)` +{venv_path} `(string | nil)` +{install_method} `(string)` +{install_file} `(string)` +{source} `(string)` + +------------------------------------------------------------------------------ + *PythonStateDap* + `PythonStateDap` +Class ~ +{PythonStateDap} +Fields ~ +{file} `(string)` +{args} `(string[])` + +------------------------------------------------------------------------------ + *PythonState* + `PythonState` +Class ~ +{PythonState} +Fields ~ +{venvs} `(table)` +{dap} `(table)` + +------------------------------------------------------------------------------ + *PythonState.State()* + `PythonState.State`() +Return ~ +`(PythonState)` + +------------------------------------------------------------------------------ + *PythonState.save()* + `PythonState.save`({new_state}) +Parameters ~ +{new_state} `(PythonState)` + + +============================================================================== +------------------------------------------------------------------------------ + *getNodeAtCursor()* + `getNodeAtCursor`() +node at cursor and validate that the user has at least nvim 0.9 +Return ~ +`(nil|TSNode)` nil if no node or nvim version too old + +------------------------------------------------------------------------------ + *findNodeOfParentsWithType()* + `findNodeOfParentsWithType`({node}, {node_types}) +Parameters ~ +{node} `(TSNode)` node to start search +{node_types} `(table)` list of node types to look for +Return ~ +`(nil | TSNode)` + +------------------------------------------------------------------------------ + *getNodeText()* + `getNodeText`({node}) +Parameters ~ +{node} `(TSNode)` +Return ~ +`(string)` + +------------------------------------------------------------------------------ + *replaceNodeText()* + `replaceNodeText`({node}, {replacementText}) +Parameters ~ +{node} `(TSNode)` +{replacementText} `(string)` + +------------------------------------------------------------------------------ + *visual_wrap_subsitute_options()* + `visual_wrap_subsitute_options`({subtitute_option}, {line_mode}) +Parameters ~ +{subtitute_option} `(nil|string)` if string then use as substitute +{line_mode} `(bool)` if visual mode is line mode + otherwise select from config + +------------------------------------------------------------------------------ + *PythonTreeSitterCommands.ts_wrap_at_cursor()* + `PythonTreeSitterCommands.ts_wrap_at_cursor`({subtitute_option}) +Parameters ~ +{subtitute_option} `(nil|string)` if string then use as substitute + otherwise select from config + + +============================================================================== +------------------------------------------------------------------------------ + *UI* + `UI` +Class ~ +{UI} +Fields ~ +{win_opts} `(table)` +{win} `(number | nil)` +{buf} `(number | nil)` + +------------------------------------------------------------------------------ + *PythonUI.deactivate_system_call_ui()* + `PythonUI.deactivate_system_call_ui`({timeout}) +Parameters ~ +{timeout} `(optional)` `(integer)` time in milliseconds to close ui. Defaults to config option + +------------------------------------------------------------------------------ + *PythonUI.activate_system_call_ui()* + `PythonUI.activate_system_call_ui`() +Open a ui window to show the output of the command being called. + +------------------------------------------------------------------------------ + *PythonUI.show_system_call_progress()* + `PythonUI.show_system_call_progress`({err}, {data}, {flush}, {callback}) +Open a ui w"indow to show the output of the command being called. +Parameters ~ +{err} `(string)` stderr data +{data} `(string)` stdout data +{flush} `(boolean)` clear ui text and replace with full output +{callback} `(function)` callback function with no arguments + + +============================================================================== +------------------------------------------------------------------------------ + *uv_available_versions()* + `uv_available_versions`() +Get list of python versions uv can install +Return ~ +`(table)` list of available python versions from uv + +------------------------------------------------------------------------------ + *uv_installed_versions()* + `uv_installed_versions`() +Get list of python versions uv has already installed +Return ~ +`(table)` list of available python versions from uv + +------------------------------------------------------------------------------ + *uv_install_version()* + `uv_install_version`({version}) +Install a python version using uv +Parameters ~ +{version} `(string)` Python version to install via uv + +------------------------------------------------------------------------------ + *uv_delete_version()* + `uv_delete_version`({version}) +Delete a python version using uv +Parameters ~ +{version} `(string)` Python version to install via uv + +------------------------------------------------------------------------------ + *uv()* + `uv`({opts}) +Execute uv directly with arguments passed by user. +Parameters ~ +{opts} `(vim.api.keyset.create_user_command.command_args)` + + +============================================================================== +------------------------------------------------------------------------------ +Class ~ +{VEnv} +Fields ~ +{source} `(string)` +{path} `(string)` +{name} `(string)` + +------------------------------------------------------------------------------ + *PythonVENV.set_venv_path()* + `PythonVENV.set_venv_path`({venv}) +active VEnv, updating venv and PATH variables. +Parameters ~ +{venv} `(VEnv | nil)` + +------------------------------------------------------------------------------ + *PythonVENV.current_venv()* + `PythonVENV.current_venv`() +the currently set VEnv object from plugin memory +Return ~ +`(VEnv | nil)` + +------------------------------------------------------------------------------ + *get_venvs_for()* + `get_venvs_for`({base_path}, {source}, {opts}) +Return ~ +`(VEnv[])` + +------------------------------------------------------------------------------ + *PythonVENV.get_venvs()* + `PythonVENV.get_venvs`({venvs_path}) +a list of venvs from multiple supported sources. +Return ~ +`(table)` List of venvs found from multiple lists + +------------------------------------------------------------------------------ + *has_high_priority_in_path()* + `has_high_priority_in_path`({first}, {second}) +who appears first in PATH. Returns `true` if `first` appears first and `false` otherwise +Parameters ~ +{first} `(string|nil)` +{second} `(string|nil)` +Return ~ +`(boolean)` + +------------------------------------------------------------------------------ + *PythonVENV.load_existing_venv()* + `PythonVENV.load_existing_venv`() +in a venv that is already set in env vars. + +------------------------------------------------------------------------------ + *PythonVENV.pick_venv()* + `PythonVENV.pick_venv`() +pick a venv to set as active. + + +============================================================================== +------------------------------------------------------------------------------ + *PythonVENVCreate.delete_venv_from_state()* + `PythonVENVCreate.delete_venv_from_state`({venv_key}, {delete_dir}) +venv from state by key +Parameters ~ +{venv_key} `(string)` +{delete_dir} `(boolean)` attempt deletion of venv from directory + +------------------------------------------------------------------------------ + *PythonVENVCreate.python_set_venv()* + `PythonVENVCreate.python_set_venv`({venv_path}, {venv_name}, {venv_source}) +Set venv. Only set venv if its different than current. +local venv_dir = settings.auto_create_venv_dir +Parameters ~ +{venv_path} `(string | nil)` full path to venv directory +{venv_name} `(string)` name of the venv to set +{venv_source} `(optional)` `(string)` name of the source of the venv. useful in determining conda or venv + +------------------------------------------------------------------------------ + *PythonVENVCreate.uv_sync()* + `PythonVENVCreate.uv_sync`({uv_lock_path}, {venv_dir}, {callback}, {script}) +Run uv sync at lock file directory. Set env path when done. +Parameters ~ +{uv_lock_path} `(string)` full path to pdm lock file +{venv_dir} `(string)` full path to pdm lock file +{callback} `(function)` +{script} `(boolean)` are we installing from a script definition + +------------------------------------------------------------------------------ + *PythonVENVCreate.pdm_sync()* + `PythonVENVCreate.pdm_sync`({pdm_lock_path}, {venv_dir}, {callback}) +Run pdm sync at lock file directory. Set env path when done. +Parameters ~ +{pdm_lock_path} `(string)` full path to pdm lock file +{venv_dir} `(string)` full path to pdm lock file +{callback} `(function)` + +------------------------------------------------------------------------------ + *PythonVENVCreate.poetry_sync()* + `PythonVENVCreate.poetry_sync`({poetry_lock_path}, {venv_dir}, {callback}) +Run pdm sync at lock file directory. Set env path when done. +Parameters ~ +{poetry_lock_path} `(string)` full path to pdm lock file +{venv_dir} `(string)` full path to pdm lock file +{callback} `(function)` + +------------------------------------------------------------------------------ + *PythonVENVCreate.pip_install_with_venv()* +`PythonVENVCreate.pip_install_with_venv`({requirements_path}, {venv_dir}, {callback}) +Create venv with python venv module and pip install at location +Parameters ~ +{requirements_path} `(string)` full path to requirements.txt, dev-requirements.txt or pyproject.toml +{venv_dir} `(string)` +{callback} `(function)` + +------------------------------------------------------------------------------ + *PythonVENVCreate.create_venv_with_python()* + `PythonVENVCreate.create_venv_with_python`({venv_path}, {python_interpreter}) +the python venv with selected interpreter" +Parameters ~ +{venv_path} `(string)` path of venv to create +{python_interpreter} `(string)` path to python executable to use + +------------------------------------------------------------------------------ + *pick_python_interpreter()* + `pick_python_interpreter`() +Have user select a python interpreter from found options. +Expected to be running in a coroutine for vim.ui +Return ~ +`(string | nil)` Path to python interpreter + +------------------------------------------------------------------------------ + *pick_venv_path()* + `pick_venv_path`({detect}) +Have user input the path to the venv they want to create, with a supplied default +Expected to be running in a coroutine for vim.ui +Parameters ~ +{detect} `(DetectVEnv)` +Return ~ +`(string | nil)` Path to venv that user wants to create + +------------------------------------------------------------------------------ + *venv_install_file()* + `venv_install_file`({detect}) +Do Update or Installation of the venv, either update an existing venv or have the user select to +create a new venv and install dependencies, selecting a python interpreter. +Parameters ~ +{detect} `(DetectVEnv)` + +------------------------------------------------------------------------------ + *PythonVENVCreate.create_and_install_venv()* + `PythonVENVCreate.create_and_install_venv`() +Automatically create venv directory and use multiple method to auto install dependencies +Use module level variable Auto_set_python_venv_parent_dir to keep track of the last venv dir, so + We don't do the creation process again when you are in the same project. + +------------------------------------------------------------------------------ + *PythonVENVCreate.delete_venv()* + `PythonVENVCreate.delete_venv`({select}) +a venv from state and filesystem +Parameters ~ +{select} `(boolean)` + +------------------------------------------------------------------------------ + *PythonVENVCreate.user_set_venv_in_state_confirmation()* + `PythonVENVCreate.user_set_venv_in_state_confirmation`({venv}) +Interactively set a venv in state. +This is used when users manually select a venv and want it cached for next run. +Parameters ~ +{venv} `(VEnv)` venv object to pull from + + +============================================================================== +------------------------------------------------------------------------------ + *DetectVEnv* + `DetectVEnv` +Class ~ +{DetectVEnv} +Fields ~ +{dir} `(string)` Current working directory found containing venv +{venv} `(PythonStateVEnv)` information on the detected venv + +------------------------------------------------------------------------------ + *DetectVEnv:found_in_cwd()* + `DetectVEnv:found_in_cwd`() +Check if cwd is current in state + +------------------------------------------------------------------------------ + *PythonVENVDetect.search_up()* + `PythonVENVDetect.search_up`({dir_or_file}) +Search for file or directory until we either the top of the git repo or root +Parameters ~ +{dir_or_file} `(string)` name of directory or file +Return ~ +`(string | nil)` found either nil or full path of found file/directory + +------------------------------------------------------------------------------ + *PythonVENVDetect.search_for_detected_type()* + `PythonVENVDetect.search_for_detected_type`() +Go through the list of possible dependency sources and return the first match. +check_paths_ordered_keys is an opinionated list of dependency sources that match +what the community probably wants detected first. +Return ~ +`(string | nil)` found +Return ~ +`(string | nil)` search + +------------------------------------------------------------------------------ + *PythonVENVDetect.detect_venv_dependency_file()* + `PythonVENVDetect.detect_venv_dependency_file`({notify}, {cwd_allowed}) +Return ~ +`(DetectVEnv | nil)` +Parameters ~ +{notify} `(boolean)` Send notification when venv is not found +{cwd_allowed} `(optional)` `(boolean)` Allow use of cwd when detecting + + +============================================================================== +------------------------------------------------------------------------------ + *PythonVENVInterpreters.hatch_interpreters()* + `PythonVENVInterpreters.hatch_interpreters`() + +Return ~ +`(table)` found_hatch_pythons list of python interpreters found by hatch + +------------------------------------------------------------------------------ + *PythonVENVInterpreters.uv_interpreters()* + `PythonVENVInterpreters.uv_interpreters`() + +Return ~ +`(table)` found_uv_pythons list of python interpreters found by uv + +------------------------------------------------------------------------------ + *PythonVENVInterpreters.python_interpreters()* + `PythonVENVInterpreters.python_interpreters`() +Return ~ +`(table)` list of potential python interpreters to use + + + vim:tw=78:ts=8:noet:ft=help:norl: \ No newline at end of file diff --git a/doc/tags b/doc/tags new file mode 100644 index 0000000..1c8c77a --- /dev/null +++ b/doc/tags @@ -0,0 +1,64 @@ +DetectVEnv python.txt /*DetectVEnv* +DetectVEnv:found_in_cwd() python.txt /*DetectVEnv:found_in_cwd()* +Python python.txt /*Python* +Python.setup() python.txt /*Python.setup()* +PythonConfig python.txt /*PythonConfig* +PythonConfig.defaults python.txt /*PythonConfig.defaults* +PythonConfig.setup() python.txt /*PythonConfig.setup()* +PythonDap.prepare_debugpy() python.txt /*PythonDap.prepare_debugpy()* +PythonLSPCommands.pyright_change_type_checking() python.txt /*PythonLSPCommands.pyright_change_type_checking()* +PythonState python.txt /*PythonState* +PythonState.State() python.txt /*PythonState.State()* +PythonState.save() python.txt /*PythonState.save()* +PythonStateDap python.txt /*PythonStateDap* +PythonStateVEnv python.txt /*PythonStateVEnv* +PythonTreeSitterCommands.ts_wrap_at_cursor() python.txt /*PythonTreeSitterCommands.ts_wrap_at_cursor()* +PythonUI.activate_system_call_ui() python.txt /*PythonUI.activate_system_call_ui()* +PythonUI.deactivate_system_call_ui() python.txt /*PythonUI.deactivate_system_call_ui()* +PythonUI.show_system_call_progress() python.txt /*PythonUI.show_system_call_progress()* +PythonVENV.current_venv() python.txt /*PythonVENV.current_venv()* +PythonVENV.get_venvs() python.txt /*PythonVENV.get_venvs()* +PythonVENV.load_existing_venv() python.txt /*PythonVENV.load_existing_venv()* +PythonVENV.pick_venv() python.txt /*PythonVENV.pick_venv()* +PythonVENV.set_venv_path() python.txt /*PythonVENV.set_venv_path()* +PythonVENVCreate.create_and_install_venv() python.txt /*PythonVENVCreate.create_and_install_venv()* +PythonVENVCreate.create_venv_with_python() python.txt /*PythonVENVCreate.create_venv_with_python()* +PythonVENVCreate.delete_venv() python.txt /*PythonVENVCreate.delete_venv()* +PythonVENVCreate.delete_venv_from_state() python.txt /*PythonVENVCreate.delete_venv_from_state()* +PythonVENVCreate.pdm_sync() python.txt /*PythonVENVCreate.pdm_sync()* +PythonVENVCreate.pip_install_with_venv() python.txt /*PythonVENVCreate.pip_install_with_venv()* +PythonVENVCreate.poetry_sync() python.txt /*PythonVENVCreate.poetry_sync()* +PythonVENVCreate.python_set_venv() python.txt /*PythonVENVCreate.python_set_venv()* +PythonVENVCreate.user_set_venv_in_state_confirmation() python.txt /*PythonVENVCreate.user_set_venv_in_state_confirmation()* +PythonVENVCreate.uv_sync() python.txt /*PythonVENVCreate.uv_sync()* +PythonVENVDetect.detect_venv_dependency_file() python.txt /*PythonVENVDetect.detect_venv_dependency_file()* +PythonVENVDetect.search_for_detected_type() python.txt /*PythonVENVDetect.search_for_detected_type()* +PythonVENVDetect.search_up() python.txt /*PythonVENVDetect.search_up()* +PythonVENVInterpreters.hatch_interpreters() python.txt /*PythonVENVInterpreters.hatch_interpreters()* +PythonVENVInterpreters.python_interpreters() python.txt /*PythonVENVInterpreters.python_interpreters()* +PythonVENVInterpreters.uv_interpreters() python.txt /*PythonVENVInterpreters.uv_interpreters()* +UI python.txt /*UI* +create_dap_config() python.txt /*create_dap_config()* +findNodeOfParentsWithType() python.txt /*findNodeOfParentsWithType()* +getNodeAtCursor() python.txt /*getNodeAtCursor()* +getNodeText() python.txt /*getNodeText()* +get_venv() python.txt /*get_venv()* +get_venvs_for() python.txt /*get_venvs_for()* +has_high_priority_in_path() python.txt /*has_high_priority_in_path()* +hatch_available_versions() python.txt /*hatch_available_versions()* +hatch_delete_version() python.txt /*hatch_delete_version()* +hatch_install_version() python.txt /*hatch_install_version()* +hatch_installed_versions() python.txt /*hatch_installed_versions()* +pick_python_interpreter() python.txt /*pick_python_interpreter()* +pick_venv_path() python.txt /*pick_venv_path()* +python.nvim python.txt /*python.nvim* +python_cmd() python.txt /*python_cmd()* +replaceNodeText() python.txt /*replaceNodeText()* +subcommand_tbl python.txt /*subcommand_tbl* +uv() python.txt /*uv()* +uv_available_versions() python.txt /*uv_available_versions()* +uv_delete_version() python.txt /*uv_delete_version()* +uv_install_version() python.txt /*uv_install_version()* +uv_installed_versions() python.txt /*uv_installed_versions()* +venv_install_file() python.txt /*venv_install_file()* +visual_wrap_subsitute_options() python.txt /*visual_wrap_subsitute_options()* diff --git a/lazy.lua b/lazy.lua index 17bbac3..37b9d40 100644 --- a/lazy.lua +++ b/lazy.lua @@ -1,5 +1,5 @@ return { - 'joshzcold/python.nvim', + "joshzcold/python.nvim", dependencies = { { "mfussenegger/nvim-dap" }, { "mfussenegger/nvim-dap-python" }, @@ -7,5 +7,5 @@ return { { "L3MON4D3/LuaSnip" }, { "nvim-neotest/neotest" }, { "nvim-neotest/neotest-python" }, - } + }, } diff --git a/lua/lualine/components/python.lua b/lua/lualine/components/python.lua index af917f0..5b40b5c 100644 --- a/lua/lualine/components/python.lua +++ b/lua/lualine/components/python.lua @@ -1,21 +1,21 @@ -local M = require('lualine.component'):extend() +local M = require("lualine.component"):extend() local default_opts = { - icon = '', - color = { fg = '#FFD43B' }, + icon = "", + color = { fg = "#FFD43B" }, } function M:init(options) - options = vim.tbl_deep_extend('keep', options or {}, default_opts) + options = vim.tbl_deep_extend("keep", options or {}, default_opts) M.super.init(self, options) end function M:update_status() - local venv = require('python.venv').current_venv() + local venv = require("python.venv").current_venv() if venv then return venv.name else - return 'no venv' + return "no venv" end end diff --git a/lua/python/commands.lua b/lua/python/commands.lua index ce58e0c..66700b8 100644 --- a/lua/python/commands.lua +++ b/lua/python/commands.lua @@ -1,5 +1,5 @@ -- python.nvim commands -local M = {} +local PythonCommands = {} ---@class PythonSubcommand ---@field impl fun(args:string[], opts: table) The command implementation @@ -44,17 +44,18 @@ local subcommand_tbl = { "test_file", "test_debug", "test_method_debug", - "test_file_debug" + "test_file_debug", } - return vim.iter(install_args) - :filter(function(install_arg) - -- If the user has typed `:Rocks install ne`, - -- this will match 'neorg' - return install_arg:find(subcmd_arg_lead) ~= nil - end) - :totable() - end + return vim + .iter(install_args) + :filter(function(install_arg) + -- If the user has typed `:Rocks install ne`, + -- this will match 'neorg' + return install_arg:find(subcmd_arg_lead) ~= nil + end) + :totable() + end, }, dap = { impl = function(args, _) @@ -69,14 +70,15 @@ local subcommand_tbl = { complete = function(subcmd_arg_lead) local install_args = {} - return vim.iter(install_args) - :filter(function(install_arg) - -- If the user has typed `:Rocks install ne`, - -- this will match 'neorg' - return install_arg:find(subcmd_arg_lead) ~= nil - end) - :totable() - end + return vim + .iter(install_args) + :filter(function(install_arg) + -- If the user has typed `:Rocks install ne`, + -- this will match 'neorg' + return install_arg:find(subcmd_arg_lead) ~= nil + end) + :totable() + end, }, hatch = { impl = function(args, _) @@ -109,14 +111,15 @@ local subcommand_tbl = { "list", } - return vim.iter(install_args) - :filter(function(install_arg) - -- If the user has typed `:Rocks install ne`, - -- this will match 'neorg' - return install_arg:find(subcmd_arg_lead) ~= nil - end) - :totable() - end + return vim + .iter(install_args) + :filter(function(install_arg) + -- If the user has typed `:Rocks install ne`, + -- this will match 'neorg' + return install_arg:find(subcmd_arg_lead) ~= nil + end) + :totable() + end, }, lsp = { impl = function(args, _) @@ -136,14 +139,15 @@ local subcommand_tbl = { if subcmd_arg_lead:find("pyright_change_type_checking_mode") ~= nil then return { "off", "basic", "standard", "strict", "all" } end - return vim.iter(install_args) - :filter(function(install_arg) - -- If the user has typed `:Rocks install ne`, - -- this will match 'neorg' - return install_arg:find(subcmd_arg_lead) ~= nil - end) - :totable() - end + return vim + .iter(install_args) + :filter(function(install_arg) + -- If the user has typed `:Rocks install ne`, + -- this will match 'neorg' + return install_arg:find(subcmd_arg_lead) ~= nil + end) + :totable() + end, }, uv = { impl = function(args, _) @@ -169,14 +173,15 @@ local subcommand_tbl = { "install_python", "delete_python", } - return vim.iter(install_args) - :filter(function(install_arg) - -- If the user has typed `:Rocks install ne`, - -- this will match 'neorg' - return install_arg:find(subcmd_arg_lead) ~= nil - end) - :totable() - end + return vim + .iter(install_args) + :filter(function(install_arg) + -- If the user has typed `:Rocks install ne`, + -- this will match 'neorg' + return install_arg:find(subcmd_arg_lead) ~= nil + end) + :totable() + end, }, treesitter = { impl = function(args, _) @@ -205,17 +210,18 @@ local subcommand_tbl = { "test", } if subcmd_arg_lead:find("wrap_cursor") ~= nil then - local config = require('python.config') + local config = require("python.config") return config.treesitter.functions.wrapper.substitute_options end - return vim.iter(install_args) - :filter(function(install_arg) - -- If the user has typed `:Rocks install ne`, - -- this will match 'neorg' - return install_arg:find(subcmd_arg_lead) ~= nil - end) - :totable() - end + return vim + .iter(install_args) + :filter(function(install_arg) + -- If the user has typed `:Rocks install ne`, + -- this will match 'neorg' + return install_arg:find(subcmd_arg_lead) ~= nil + end) + :totable() + end, }, venv = { impl = function(args, _) @@ -252,14 +258,15 @@ local subcommand_tbl = { "delete", "delete_select", } - return vim.iter(install_args) - :filter(function(install_arg) - -- If the user has typed `:Rocks install ne`, - -- this will match 'neorg' - return install_arg:find(subcmd_arg_lead) ~= nil - end) - :totable() - end + return vim + .iter(install_args) + :filter(function(install_arg) + -- If the user has typed `:Rocks install ne`, + -- this will match 'neorg' + return install_arg:find(subcmd_arg_lead) ~= nil + end) + :totable() + end, }, } @@ -278,7 +285,7 @@ local function python_cmd(opts) subcommand.impl(args, opts) end -function M.load_commands() +function PythonCommands.load_commands() -- NOTE: the options will vary, based on your use case. vim.api.nvim_create_user_command("Python", python_cmd, { nargs = "+", @@ -286,11 +293,7 @@ function M.load_commands() complete = function(arg_lead, cmdline, _) -- Get the subcommand. local subcmd_key, subcmd_arg_lead = cmdline:match("^['<,'>]*Python[!]*%s(%S+)%s(.*)$") - if subcmd_key - and subcmd_arg_lead - and subcommand_tbl[subcmd_key] - and subcommand_tbl[subcmd_key].complete - then + if subcmd_key and subcmd_arg_lead and subcommand_tbl[subcmd_key] and subcommand_tbl[subcmd_key].complete then -- The subcommand has completions. Return them. return subcommand_tbl[subcmd_key].complete(subcmd_arg_lead) end @@ -298,19 +301,16 @@ function M.load_commands() if cmdline:match("^['<,'>]*Python[!]*%s+%w*$") then -- Filter subcommands that match local subcommand_keys = vim.tbl_keys(subcommand_tbl) - return vim.iter(subcommand_keys) - :filter(function(key) - return key:find(arg_lead) ~= nil - end) - :totable() + return vim + .iter(subcommand_keys) + :filter(function(key) + return key:find(arg_lead) ~= nil + end) + :totable() end end, bang = true, -- If you want to support ! modifiers }) end -return setmetatable(M, { - __index = function(_, k) - return require("python.commands")[k] - end, -}) +return PythonCommands diff --git a/lua/python/config.lua b/lua/python/config.lua index 313a51b..24a2020 100644 --- a/lua/python/config.lua +++ b/lua/python/config.lua @@ -1,23 +1,26 @@ ---@diagnostic disable: missing-fields, inject-field ---@type python.Config -local M = {} +local PythonConfig = {} +--- Python.nvim config +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) ---@class python.Config -local defaults = { +PythonConfig.defaults = { -- Should return a list of tables with a `name` and a `path` entry each. -- Gets the argument `venvs_path` set below. -- By default just lists the entries in `venvs_path`. ---@return VEnv[] get_venvs = function(venvs_path) - return require('python.venv').get_venvs(venvs_path) + return require("python.venv").get_venvs(venvs_path) end, -- Path for venvs picker - venvs_path = vim.fn.expand('~/.virtualenvs'), + venvs_path = vim.fn.expand("~/.virtualenvs"), -- Something to do after setting an environment post_set_venv = nil, -- base path for creating new venvs auto_create_venv_path = function(parent_dir) - return vim.fs.joinpath(parent_dir, '.venv') + return vim.fs.joinpath(parent_dir, ".venv") end, -- Patterns for autocmd LspAttach that trigger the auto venv logic -- Add onto this list if you depend on venvs for other file types @@ -33,7 +36,7 @@ local defaults = { -- List of text actions to take on InsertLeave, TextChanged -- Put in empty table or nil to disable enabled_text_actions = { - "f-strings" -- When inserting {}, put in an f-string + "f-strings", -- When inserting {}, put in an f-string }, -- Adjust when enabled_text_actions is triggered enabled_text_actions_autocmd_events = { "InsertLeave" }, @@ -54,37 +57,68 @@ local defaults = { -- Look for tree-sitter types to wrap find_types = { - "tuple", "string", "true", "false", "list", "call", "parenthesized_expression", "expression_statement", - "integer" - } - } - } + "tuple", + "string", + "true", + "false", + "list", + "call", + "parenthesized_expression", + "expression_statement", + "integer", + }, + }, + }, }, -- Load python keymaps. Everything starting with p... keymaps = { -- following nvim_set_keymap() mode, lhs, rhs, opts mappings = { - ['pv'] = { "n", "Python venv pick", { desc = "python.nvim: pick venv" }, }, - ['pi'] = { "n", "Python venv install", { desc = "python.nvim: python venv install" } }, - ['pd'] = { "n", "Python dap", { desc = "python.nvim: python run debug program" } }, + ["pv"] = { "n", "Python venv pick", { desc = "python.nvim: pick venv" } }, + ["pi"] = { "n", "Python venv install", { desc = "python.nvim: python venv install" } }, + ["pd"] = { "n", "Python dap", { desc = "python.nvim: python run debug program" } }, -- Test Actions - ['ptt'] = { "n", "Python test", { desc = "python.nvim: python run test suite" } }, - ['ptm'] = { "n", "Python test_method", { desc = "python.nvim: python run test method" } }, - ['ptf'] = { "n", "Python test_file", { desc = "python.nvim: python run test file" } }, - ['ptdd'] = { "n", "Python test_debug", { desc = "python.nvim: run test suite in debug mode." } }, - ['ptdm'] = { "n", "Python test_method_debug", { desc = "python.nvim: run test method in debug mode." } }, - ['ptdf'] = { "n", "Python test_file_debug", { desc = "python.nvim: run test file in debug mode." } }, + ["ptt"] = { "n", "Python test", { desc = "python.nvim: python run test suite" } }, + ["ptm"] = { "n", "Python test_method", { desc = "python.nvim: python run test method" } }, + ["ptf"] = { "n", "Python test_file", { desc = "python.nvim: python run test file" } }, + ["ptdd"] = { "n", "Python test_debug", { desc = "python.nvim: run test suite in debug mode." } }, + ["ptdm"] = { + "n", + "Python test_method_debug", + { desc = "python.nvim: run test method in debug mode." }, + }, + ["ptdf"] = { + "n", + "Python test_file_debug", + { desc = "python.nvim: run test file in debug mode." }, + }, -- VEnv Actions - ['ped'] = { "n", "Python venv delete_select", { desc = "python.nvim: select and delete a known venv." } }, - ['peD'] = { "n", "Python venv delete", { desc = "python.nvim: delete current venv set." } }, + ["ped"] = { + "n", + "Python venv delete_select", + { desc = "python.nvim: select and delete a known venv." }, + }, + ["peD"] = { "n", "Python venv delete", { desc = "python.nvim: delete current venv set." } }, -- Language Actions - ['ppe'] = { "n", "Python treesitter toggle_enumerate", { desc = "python.nvim: turn list into enumerate" } }, - ['ppw'] = { "n", "Python treesitter wrap_cursor", { desc = "python.nvim: wrap treesitter identifier with pattern" } }, - ['pw'] = { "v", ":Python treesitter wrap_cursor", { desc = "python.nvim: wrap treesitter identifier with pattern" } }, - } + ["ppe"] = { + "n", + "Python treesitter toggle_enumerate", + { desc = "python.nvim: turn list into enumerate" }, + }, + ["ppw"] = { + "n", + "Python treesitter wrap_cursor", + { desc = "python.nvim: wrap treesitter identifier with pattern" }, + }, + ["pw"] = { + "v", + ":Python treesitter wrap_cursor", + { desc = "python.nvim: wrap treesitter identifier with pattern" }, + }, + }, }, -- Settings regarding ui handling ui = { @@ -108,7 +142,7 @@ local defaults = { -- height = 20, -- row = vim.o.lines - 3, -- col = vim.o.columns -2, - } + }, }, split = { win_opts = { @@ -117,15 +151,16 @@ local defaults = { -- width = 40, -- height = 10, -- focusable = true, - } - } + }, + }, }, -- Tell neotest-python which test runner to use test = { - test_runner = "pytest" - } + test_runner = "pytest", + }, } +--minidoc_afterlines_end -- Only for existing keys in `target`. local function tbl_deep_extend_existing(target, source, prev_key) @@ -153,16 +188,16 @@ local function tbl_deep_extend_existing(target, source, prev_key) end ---@param opts? python.Config -function M.setup(opts) +function PythonConfig.setup(opts) opts = opts or {} - M.config = tbl_deep_extend_existing(defaults, opts, "config") + PythonConfig.config = tbl_deep_extend_existing(PythonConfig.defaults, opts, "config") end -return setmetatable(M, { +return setmetatable(PythonConfig, { __index = function(_, key) - if M.config == nil then - M.setup() + if PythonConfig.config == nil then + PythonConfig.setup() end - return M.config[key] + return PythonConfig.config[key] end, }) diff --git a/lua/python/dap/init.lua b/lua/python/dap/init.lua index 4b60494..b304853 100644 --- a/lua/python/dap/init.lua +++ b/lua/python/dap/init.lua @@ -1,13 +1,13 @@ --- DAP integrations -local M = {} +local PythonDap = {} local state = require("python.state") --- Get venv for DAP functions ---@return VEnv | nil venv local function get_venv() - local venv = require('python.venv').current_venv() + local venv = require("python.venv").current_venv() if not venv then vim.notify_once("python.nvim: Need a venv to do DAP actions", vim.log.levels.WARN) return @@ -15,26 +15,24 @@ local function get_venv() return venv end - --- Read venv with debug py and launch callback ---@param callback? function -function M.prepare_debugpy(callback) +function PythonDap.prepare_debugpy(callback) local venv = get_venv() if not venv then return end vim.schedule(function() - vim.system( - { vim.fs.joinpath(venv.path, "bin", "pip"), "install", "debugpy" }, - {}, function(obj) - if obj.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj.stderr), vim.log.levels.ERROR) - return - end - vim.notify_once(string.format('python.nvim: Installed debugpy into %s', venv.name), vim.log.levels.INFO) - vim.schedule(function() callback(venv) end) + vim.system({ vim.fs.joinpath(venv.path, "bin", "pip"), "install", "debugpy" }, {}, function(obj) + if obj.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj.stderr), vim.log.levels.ERROR) + return end - ) + vim.notify_once(string.format("python.nvim: Installed debugpy into %s", venv.name), vim.log.levels.INFO) + vim.schedule(function() + callback(venv) + end) + end) end) end @@ -45,22 +43,22 @@ end local function create_dap_config(cwd, venv, python_state) local dap = require("dap") vim.ui.select({ "file", "file:args", "program:args" }, { - prompt = "python.nvim: Select new dap style configuration: " + prompt = "python.nvim: Select new dap style configuration: ", }, function(choice) if not choice then return end local config = { - type = 'python', - request = 'launch', + type = "python", + request = "launch", name = cwd, - program = '${file}', - pythonPath = vim.fs.joinpath(venv.path, "bin", "python") + program = "${file}", + pythonPath = vim.fs.joinpath(venv.path, "bin", "python"), } if choice == "file:args" then vim.ui.input({ - prompt = "Program Arguments: " + prompt = "Program Arguments: ", }, function(input) local args = vim.split(input, " ") local _args = {} @@ -73,14 +71,14 @@ local function create_dap_config(cwd, venv, python_state) ::continue:: end - config['args'] = args + config["args"] = args end) elseif choice == "program:args" then vim.ui.input({ - prompt = "Program with arguments: " + prompt = "Program with arguments: ", }, function(program) local args = vim.split(program, " ") - config['program'] = args[1] + config["program"] = args[1] local _args = {} for idx, arg in pairs(args) do @@ -96,13 +94,13 @@ local function create_dap_config(cwd, venv, python_state) ::continue:: end - config['args'] = _args + config["args"] = _args end) end python_state.dap[cwd] = config state.save(python_state) vim.schedule(function() - M.prepare_debugpy(function() + PythonDap.prepare_debugpy(function() vim.notify("python.nvim dap config: " .. vim.inspect(config)) dap.run(config) end) @@ -110,32 +108,28 @@ local function create_dap_config(cwd, venv, python_state) end) end -function M.python_dap_run() - M.prepare_debugpy(function(venv) - vim.schedule(function() - local dap_python = require("dap-python") - dap_python.setup(vim.fs.joinpath(venv.path, "bin", "python3"), {}) - local python_state = state.State() - local cwd = vim.fn.getcwd() - if python_state.dap[cwd] == nil then - create_dap_config(cwd, venv, python_state) - else - vim.ui.select({ "Yes", "Create New" }, { - prompt = "Use this config?: " .. vim.inspect(python_state.dap[cwd]) - }, function(choice) - if choice == "Create New" then - create_dap_config(cwd, venv, python_state) - elseif choice == "Yes" then - dap.run(python_state.dap[cwd]) - end - end) - end - end) +function PythonDap.python_dap_run() + PythonDap.prepare_debugpy(function(venv) + vim.schedule(function() + local dap_python = require("dap-python") + dap_python.setup(vim.fs.joinpath(venv.path, "bin", "python3"), {}) + local python_state = state.State() + local cwd = vim.fn.getcwd() + if python_state.dap[cwd] == nil then + create_dap_config(cwd, venv, python_state) + else + vim.ui.select({ "Yes", "Create New" }, { + prompt = "Use this config?: " .. vim.inspect(python_state.dap[cwd]), + }, function(choice) + if choice == "Create New" then + create_dap_config(cwd, venv, python_state) + elseif choice == "Yes" then + dap.run(python_state.dap[cwd]) + end + end) + end end) + end) end -return setmetatable(M, { - __index = function(_, k) - return require("python.dap")[k] - end, -}) +return PythonDap diff --git a/lua/python/hatch/commands.lua b/lua/python/hatch/commands.lua index 959b0ce..e8fd61b 100644 --- a/lua/python/hatch/commands.lua +++ b/lua/python/hatch/commands.lua @@ -1,11 +1,12 @@ local ui = require("python.ui") -local M = {} +local PythonHatchCommands = {} - -function M.check_hatch() +function PythonHatchCommands.check_hatch() if vim.fn.executable("hatch") == 0 then - vim.notify_once(("python.nvim: Program 'hatch' is required: %s"):format("https://hatch.pypa.io/latest/"), - vim.log.levels.ERROR) + vim.notify_once( + ("python.nvim: Program 'hatch' is required: %s"):format("https://hatch.pypa.io/latest/"), + vim.log.levels.ERROR + ) return false end return true @@ -15,24 +16,25 @@ end ---@return table list of available python versions from hatch local function hatch_installed_versions() local output = {} - vim.system({ "hatch", "python", "show", "--ascii" }, {}, function(obj) - local found_installed = {} - for line in vim.gsplit(obj.stdout, "\n", { plain = true }) do - local available_line = string.match(line, "Available") - if available_line then - break + vim + .system({ "hatch", "python", "show", "--ascii" }, {}, function(obj) + local found_installed = {} + for line in vim.gsplit(obj.stdout, "\n", { plain = true }) do + local available_line = string.match(line, "Available") + if available_line then + break + end + local matched = string.match(line, "(%d%.%d+%s*|%s*%d.%d+)") + table.insert(found_installed, 1, matched) end - local matched = string.match(line, "(%d%.%d+%s*|%s*%d.%d+)") - table.insert(found_installed, 1, matched) - end - - for _, f in pairs(found_installed) do - local parts = vim.split(f, "|", { trimempty = true }) - parts[1] = parts[1]:gsub("%s+", "") - table.insert(output, 1, parts[1]) - end - end):wait() + for _, f in pairs(found_installed) do + local parts = vim.split(f, "|", { trimempty = true }) + parts[1] = parts[1]:gsub("%s+", "") + table.insert(output, 1, parts[1]) + end + end) + :wait() return output end @@ -40,93 +42,81 @@ end ---@return table list of available python versions from hatch local function hatch_available_versions() local output = {} - vim.system({ "hatch", "python", "show", "--ascii" }, {}, function(obj) - local found_available = {} - local available_line_shown = false - for line in vim.gsplit(obj.stdout, "\n", { plain = true }) do - local available_line = string.match(line, "Available") - if available_line then - available_line_shown = true - end + vim + .system({ "hatch", "python", "show", "--ascii" }, {}, function(obj) + local found_available = {} + local available_line_shown = false + for line in vim.gsplit(obj.stdout, "\n", { plain = true }) do + local available_line = string.match(line, "Available") + if available_line then + available_line_shown = true + end - if available_line_shown then - local matched = string.match(line, "(%s+%d%.%d+%s+|%s+%d.%d+)") - table.insert(found_available, 1, matched) + if available_line_shown then + local matched = string.match(line, "(%s+%d%.%d+%s+|%s+%d.%d+)") + table.insert(found_available, 1, matched) + end end - end - - for _, f in pairs(found_available) do - local parts = vim.split(f, "|", { trimempty = true }) - parts[1] = parts[1]:gsub("%s+", "") - table.insert(output, 1, parts[1]) - end - end):wait() + for _, f in pairs(found_available) do + local parts = vim.split(f, "|", { trimempty = true }) + parts[1] = parts[1]:gsub("%s+", "") + table.insert(output, 1, parts[1]) + end + end) + :wait() return output end - --- Install a python version using hatch ---@param version string Python version to install via hatch local function hatch_install_version(version) - vim.schedule( - function() - vim.system( - { "hatch", "python", "install", version }, - { - stdout = ui.show_system_call_progress - }, - function(obj2) - vim.schedule(function() - if obj2.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) - ui.deactivate_system_call_ui(10000) - else - ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() - ui.deactivate_system_call_ui() - end) - end + vim.schedule(function() + vim.system({ "hatch", "python", "install", version }, { + stdout = ui.show_system_call_progress, + }, function(obj2) + vim.schedule(function() + if obj2.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) + ui.deactivate_system_call_ui(10000) + else + ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() + ui.deactivate_system_call_ui() end) end - ) - vim.schedule(function() - ui.activate_system_call_ui() end) - end - ) + end) + vim.schedule(function() + ui.activate_system_call_ui() + end) + end) end --- Delete a python version using hatch ---@param version string Python version to delete via hatch local function hatch_delete_version(version) - vim.schedule( - function() - vim.system( - { "hatch", "python", "remove", version }, - { - stdout = ui.show_system_call_progress - }, - function(obj2) - vim.schedule(function() - if obj2.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) - ui.deactivate_system_call_ui(10000) - else - ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() - ui.deactivate_system_call_ui() - end) - end + vim.schedule(function() + vim.system({ "hatch", "python", "remove", version }, { + stdout = ui.show_system_call_progress, + }, function(obj2) + vim.schedule(function() + if obj2.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) + ui.deactivate_system_call_ui(10000) + else + ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() + ui.deactivate_system_call_ui() end) end - ) - vim.schedule(function() - ui.activate_system_call_ui() end) - end - ) + end) + vim.schedule(function() + ui.activate_system_call_ui() + end) + end) end -function M.hatch_install_python() +function PythonHatchCommands.hatch_install_python() local versions = hatch_available_versions() vim.ui.select(versions, { prompt = "Select a python version to install via hatch: " }, function(selection) if not selection then @@ -136,12 +126,12 @@ function M.hatch_install_python() end) end -function M.hatch_list_python() +function PythonHatchCommands.hatch_list_python() local versions = hatch_installed_versions() vim.print(versions) end -function M.hatch_delete_python() +function PythonHatchCommands.hatch_delete_python() local versions = hatch_installed_versions() vim.ui.select(versions, { prompt = "Select a python version to delete in hatch: " }, function(selection) if not selection then @@ -151,8 +141,4 @@ function M.hatch_delete_python() end) end -return setmetatable(M, { - __index = function(_, k) - return require("python.hatch.commands")[k] - end, -}) +return PythonHatchCommands diff --git a/lua/python/hatch/init.lua b/lua/python/hatch/init.lua index b5fa8e3..688b06b 100644 --- a/lua/python/hatch/init.lua +++ b/lua/python/hatch/init.lua @@ -1,7 +1,3 @@ -local M = {} +local PythonHatch = {} -return setmetatable(M, { - __index = function(_, k) - return require("python.hatch")[k] - end, -}) +return PythonHatch diff --git a/lua/python/init.lua b/lua/python/init.lua index 3b5f84b..e6c38bb 100644 --- a/lua/python/init.lua +++ b/lua/python/init.lua @@ -1,8 +1,18 @@ +--- *python.nvim* Python tool box for neovim +--- +--- MIT License Copyright (c) 2025 Joshua Cold +--- +--- ============================================================================== +--- +--- |python.nvim| is a collection of tools for python development in neovim. +--- Helping speed up dependency management, debugging and other tasks that +--- apart of developing in the python language. +--- ---@class python -local M = {} +local Python = {} ---@param opts? python.Config -function M.setup(opts) +function Python.setup(opts) local config = require("python.config") config.setup(opts) @@ -58,7 +68,7 @@ function M.setup(opts) if config.enabled_text_actions then local ts = require("python.treesitter.commands") local enabled_text_actions_map = { - ["f-strings"] = ts.pythonFStr + ["f-strings"] = ts.pythonFStr, } vim.api.nvim_create_autocmd(config.enabled_text_actions_autocmd_events, { @@ -81,8 +91,4 @@ function M.setup(opts) end end -return setmetatable(M, { - __index = function(_, k) - return require("python")[k] - end, -}) +return Python diff --git a/lua/python/keymap.lua b/lua/python/keymap.lua index e6cb2d0..6fada78 100644 --- a/lua/python/keymap.lua +++ b/lua/python/keymap.lua @@ -1,6 +1,6 @@ local config = require("python.config") -local venv = require("python.venv") +local M = {} local function map(mode, lhs, rhs, opts) local options = { noremap = true, silent = true } @@ -10,8 +10,6 @@ local function map(mode, lhs, rhs, opts) vim.api.nvim_set_keymap(mode, lhs, rhs, options) end -M = {} - function M.load_keymaps() if next(config.keymaps.mappings) == nil then return diff --git a/lua/python/lsp/commands.lua b/lua/python/lsp/commands.lua index 1e7a4cb..bbbaceb 100644 --- a/lua/python/lsp/commands.lua +++ b/lua/python/lsp/commands.lua @@ -1,10 +1,10 @@ -- LSP user commands -local M = {} +local PythonLSPCommands = {} --- Change the type checking mode of either basedpyright or pyright if found. ---@param mode string type checking mode -function M.pyright_change_type_checking(mode) +function PythonLSPCommands.pyright_change_type_checking(mode) local check_clients = { "pyright", "basedpyright" } for _, client_name in pairs(check_clients) do @@ -28,8 +28,4 @@ function M.pyright_change_type_checking(mode) end end -return setmetatable(M, { - __index = function(_, k) - return require("python.lsp.commands")[k] - end, -}) +return PythonLSPCommands diff --git a/lua/python/lsp/init.lua b/lua/python/lsp/init.lua index 3ced2b7..ca199d9 100644 --- a/lua/python/lsp/init.lua +++ b/lua/python/lsp/init.lua @@ -1,74 +1,74 @@ -local M = {} +local PythonLSP = {} local function call_did_change_configuration(client, config) client.settings = config if client.notify("workspace/didChangeConfiguration", { settings = client.settings }) then - vim.notify_once(string.format("python.nvim: Updated configuration of lsp server: '%s'", client.name), - vim.log.levels.INFO) + vim.notify_once( + string.format("python.nvim: Updated configuration of lsp server: '%s'", client.name), + vim.log.levels.INFO + ) else - vim.notify_once(string.format("python.nvim: Updating configuration of lsp server: '%s' has failed", client.name), - vim.log.levels.ERROR) + vim.notify_once( + string.format("python.nvim: Updating configuration of lsp server: '%s' has failed", client.name), + vim.log.levels.ERROR + ) end end -M.python_lsp_servers = { +PythonLSP.python_lsp_servers = { basedpyright = { callback = function(venv_path, client) local new_settings = vim.tbl_deep_extend("force", client.settings, { python = { - pythonPath = venv_path - } + pythonPath = venv_path, + }, }) call_did_change_configuration(client, new_settings) - end + end, }, pyright = { callback = function(venv_path, client) local new_settings = vim.tbl_deep_extend("force", client.settings, { python = { - pythonPath = venv_path - } + pythonPath = venv_path, + }, }) call_did_change_configuration(client, new_settings) - end + end, }, pylsp = { -- python-lsp-server does not have a specific setting for python path callback = function(_, client) vim.cmd(":LspRestart pylsp") - vim.notify_once(string.format("python.nvim: restart lsp client: '%s'", client.name), - vim.log.levels.INFO) - end + vim.notify_once(string.format("python.nvim: restart lsp client: '%s'", client.name), vim.log.levels.INFO) + end, }, jedi_language_server = { -- jedi_language_server doesn't support didChangeConfiguration -- https://github.com/pappasam/jedi-language-server/issues/58 callback = function(_, client) - vim.notify_once(string.format("python.nvim: restart lsp client: '%s'", client.name), - vim.log.levels.INFO) + vim.notify_once(string.format("python.nvim: restart lsp client: '%s'", client.name), vim.log.levels.INFO) vim.cmd(":LspRestart jedi_language_server") - end + end, }, -- TODO find better method when sith-lsp becomes more widely available -- https://github.com/LaBatata101/sith-language-server SithLSP = { callback = function(_, client) - vim.notify_once(string.format("python.nvim: restart lsp client: '%s'", client.name), - vim.log.levels.INFO) + vim.notify_once(string.format("python.nvim: restart lsp client: '%s'", client.name), vim.log.levels.INFO) vim.cmd(":LspRestart SithLSP") - end + end, }, -- For my homies in devops ansiblels = { callback = function(_, client) - vim.notify_once(string.format("python.nvim: restart lsp client: '%s'", client.name), - vim.log.levels.INFO) + vim.notify_once(string.format("python.nvim: restart lsp client: '%s'", client.name), vim.log.levels.INFO) vim.cmd(":LspRestart ansiblels") - end - } + end, + }, } -function M.notify_workspace_did_change() +function PythonLSP.notify_workspace_did_change() local clients = vim.lsp.get_clients() local venv = require("python.venv").current_venv() if not clients then @@ -82,16 +82,12 @@ function M.notify_workspace_did_change() ---@class vim.lsp.Client for _, client in pairs(clients) do - if M.python_lsp_servers[client.name] == nil then + if PythonLSP.python_lsp_servers[client.name] == nil then goto continue end - M.python_lsp_servers[client.name].callback(venv_python, client) + PythonLSP.python_lsp_servers[client.name].callback(venv_python, client) ::continue:: end end -return setmetatable(M, { - __index = function(_, k) - return require("python.lsp")[k] - end, -}) +return PythonLSP diff --git a/lua/python/snip/init.lua b/lua/python/snip/init.lua index 4dee3d8..d6cf6f7 100644 --- a/lua/python/snip/init.lua +++ b/lua/python/snip/init.lua @@ -1,4 +1,4 @@ -local M = {} +local PythonSnippets = {} local ls = require("luasnip") local fmt = require("luasnip.extras.fmt").fmt @@ -6,11 +6,11 @@ local fmta = require("luasnip.extras.fmt").fmta local rep = require("luasnip.extras").rep local ai = require("luasnip.nodes.absolute_indexer") local partial = require("luasnip.extras").partial -local nodes = require('python.treesitter.nodes') -local config = require('python.config') +local nodes = require("python.treesitter.nodes") +local config = require("python.config") local function is_in_test_file() - local filename = vim.fn.expand('%:p') + local filename = vim.fn.expand("%:p") -- no required convention for python tests. -- Just assume if test is in the full path we might be in a test file. return string.find(filename, "test") @@ -41,33 +41,43 @@ local not_in_fn = { } local always = { - show_condition = function() return true end, - condition = function() return true end, + show_condition = function() + return true + end, + condition = function() + return true + end, } local snippets = { -- Main ls.s( { trig = "main", name = "Main", dscr = "python.nvim: Create a main function" }, - fmt([[ + fmt( + [[ def main(): {} if __name__ == "__main__": main() - ]], ls.i(0)), + ]], + ls.i(0) + ), not_in_fn ), ls.s( { trig = "ifmain", name = "Main", dscr = "python.nvim: Create a main function" }, - fmt([[ + fmt( + [[ def main(): {} if __name__ == "__main__": main() - ]], ls.i(0)), + ]], + ls.i(0) + ), not_in_fn ), @@ -75,10 +85,10 @@ local snippets = { { trig = "argparse_typed", name = "Argparse Typed Arguments", - dscr = - "python.nvim: Argparse mapping to dataclass for typed completion of arguments" + dscr = "python.nvim: Argparse mapping to dataclass for typed completion of arguments", }, - fmt([[ + fmt( + [[ import argparse from dataclasses import dataclass @@ -100,8 +110,9 @@ local snippets = { my_arg_1 = ls.i(1, { "my_arg_1" }), my_arg_2 = rep(1), my_arg_3 = rep(1), - finally = ls.i(0) - }), + finally = ls.i(0), + } + ), not_in_fn ), @@ -109,9 +120,10 @@ local snippets = { { trig = "colors", name = "Color variables", - dscr = "python.nvim: Python const variables of terminal colors escape sequences." + dscr = "python.nvim: Python const variables of terminal colors escape sequences.", }, - fmt([[ + fmt( + [[ PURPLE = "\033[95m" BLUE = "\033[94m" CYAN = "\033[96m" @@ -124,9 +136,11 @@ local snippets = { UNDERLINE = "\033[4m" ITALICS = "\033[3m" {} - ]], { - ls.i(0) - }), + ]], + { + ls.i(0), + } + ), always ), @@ -134,139 +148,164 @@ local snippets = { { trig = "match_case", name = "Match-Case statement", - dscr = "python.nvim: Python Match Case statement with default case." + dscr = "python.nvim: Python Match Case statement with default case.", }, - fmt([[ + fmt( + [[ match {var}: case "{one}": ...{finally} case _: ... - ]], { - var = ls.i(1, { "var" }), - one = ls.i(2, { "one" }), - finally = ls.i(0) - }), + ]], + { + var = ls.i(1, { "var" }), + one = ls.i(2, { "one" }), + finally = ls.i(0), + } + ), always ), ls.s( { trig = "switch_case", name = "Match-Case statement", - dscr = "python.nvim: Python Match Case statement with default case." + dscr = "python.nvim: Python Match Case statement with default case.", }, - fmt([[ + fmt( + [[ match {var}: case "{one}": ...{finally} case _: ... - ]], { - var = ls.i(1, { "var" }), - one = ls.i(2, { "one" }), - finally = ls.i(0) - }), + ]], + { + var = ls.i(1, { "var" }), + one = ls.i(2, { "one" }), + finally = ls.i(0), + } + ), always ), ls.s( { trig = "if_ternary_condition", name = "Python version of a ternay", - dscr = "python.nvim: Single line conditional, emulating ternarary" + dscr = "python.nvim: Single line conditional, emulating ternarary", }, - fmt([[ + fmt( + [[ {var} = "{foo}" if {True} else "{bar}"{finally} - ]], { - var = ls.i(1, { "var" }), - foo = ls.i(2, { "foo" }), - True = ls.i(3, { "True" }), - bar = ls.i(4, { "bar" }), - finally = ls.i(0) - }), + ]], + { + var = ls.i(1, { "var" }), + foo = ls.i(2, { "foo" }), + True = ls.i(3, { "True" }), + bar = ls.i(4, { "bar" }), + finally = ls.i(0), + } + ), always ), ls.s( { trig = "list_comprehension", name = "Python create list loop expression", - dscr = "python.nvim: Create new list from expression inside of list" + dscr = "python.nvim: Create new list from expression inside of list", }, - fmt([[ + fmt( + [[ {var} = [a for a in x if a == True] - ]], { - var = ls.i(1, { "var" }), - }), + ]], + { + var = ls.i(1, { "var" }), + } + ), always ), ls.s( { trig = "merge_dicts", name = "Python create dict from 2 dicts using spread operation", - dscr = "python.nvim: Create dictionary from spread operator" + dscr = "python.nvim: Create dictionary from spread operator", }, - fmt([[ + fmt( + [[ {var} = {{**{dict_1}, **{dict_2} }} - ]], { - var = ls.i(1, { "var" }), - dict_1 = ls.i(1, { "dict_1" }), - dict_2 = ls.i(1, { "dict_2" }), - }), + ]], + { + var = ls.i(1, { "var" }), + dict_1 = ls.i(1, { "dict_1" }), + dict_2 = ls.i(1, { "dict_2" }), + } + ), always ), ls.s( { trig = "with_open", name = "python create open file io", - dscr = "python.nvim: create io object with open" + dscr = "python.nvim: create io object with open", }, - fmt([[ + fmt( + [[ with open("/path", "{r}", encoding="utf-8") as f: ...{finally} - ]], { - r = ls.i(1, { "r" }), - finally = ls.i(0), - }), + ]], + { + r = ls.i(1, { "r" }), + finally = ls.i(0), + } + ), always ), ls.s( { trig = "for_with_index", name = "Python For in loop with index", - dscr = "python.nvim: for in loop with index in python" + dscr = "python.nvim: for in loop with index in python", }, - fmt([[ + fmt( + [[ for i, {val} in enumerate({var}): ...{finally} - ]], { - val = ls.i(1, { "val" }), - var = ls.i(2, { "var" }), - finally = ls.i(0), - }), + ]], + { + val = ls.i(1, { "val" }), + var = ls.i(2, { "var" }), + finally = ls.i(0), + } + ), always ), ls.s( { trig = "enumerate", name = "Python For in loop with index", - dscr = "python.nvim: for in loop with index in python" + dscr = "python.nvim: for in loop with index in python", }, - fmt([[ + fmt( + [[ for i, {val} in enumerate({var}): ...{finally} - ]], { - val = ls.i(1, { "val" }), - var = ls.i(2, { "var" }), - finally = ls.i(0), - }), + ]], + { + val = ls.i(1, { "val" }), + var = ls.i(2, { "var" }), + finally = ls.i(0), + } + ), always ), ls.s( { trig = "colored_logs", name = "Python colorized logger formatter", - dscr = "python.nvim: logging module formatter for colored logs" + dscr = "python.nvim: logging module formatter for colored logs", }, - fmt([[ + fmt( + [[ import logging class ColoredFormatter(logging.Formatter): @@ -300,7 +339,9 @@ local snippets = { ch.setLevel(logging.INFO) ch.setFormatter(ColoredFormatter()) log.addHandler(ch) - ]], {}, {} + ]], + {}, + {} ), {}, not_in_fn @@ -312,12 +353,13 @@ local snippets = { name = "requests GET", dscr = "python.nvim: requests Library get call", }, - fmt([[ + fmt( + [[ resp = requests.get(url="{}", headers={{}}, params={{}}) resp.raise_for_status() ]], { - ls.i(1, "http://localhost") + ls.i(1, "http://localhost"), }, {} ), @@ -329,14 +371,15 @@ local snippets = { name = "requests POST", dscr = "python.nvim: requests Library post call", }, - fmt([[ + fmt( + [[ resp = requests.post(url="{}", headers={{}}, params={{}}, body={{ }}) resp.raise_for_status() ]], { - ls.i(1, "http://localhost") + ls.i(1, "http://localhost"), }, {} ), @@ -348,14 +391,15 @@ local snippets = { name = "requests PUT", dscr = "python.nvim: requests Library put call", }, - fmt([[ + fmt( + [[ resp = requests.put(url="{}", headers={{}}, params={{}}, body={{ }}) resp.raise_for_status() ]], { - ls.i(1, "http://localhost") + ls.i(1, "http://localhost"), }, {} ), @@ -367,12 +411,13 @@ local snippets = { name = "requests DELETE", dscr = "python.nvim: requests Library delete call", }, - fmt([[ + fmt( + [[ resp = requests.delete(url="{}", headers={{}}, params={{}}) resp.raise_for_status() ]], { - ls.i(1, "http://localhost") + ls.i(1, "http://localhost"), }, {} ), @@ -384,7 +429,8 @@ local snippets = { name = "argparse string arg", dscr = "python.nvim: argparse string argument", }, - fmt([[ + fmt( + [[ parser.add_argument('--{}', metavar='{}', type=str, help='{}', required=True) ]], { @@ -402,7 +448,8 @@ local snippets = { name = "argparse string bool", dscr = "python.nvim: argparse bool argument flag", }, - fmt([[ + fmt( + [[ parser.add_argument('--{}', metavar='{}', action="store_true", help='{}') ]], { @@ -420,7 +467,8 @@ local snippets = { name = "argparse string list", dscr = "python.nvim: argparse list arguments", }, - fmt([[ + fmt( + [[ parser.add_argument('--{}', metavar='{}', action="append", help='{}') ]], { @@ -438,7 +486,8 @@ local snippets = { name = "UV script shebang with dependencies", dscr = "python.nvim: UV script shebang with dependency definition.", }, - fmt([[ + fmt( + [[ #!/usr/bin/env -S uv run --script # # /// script @@ -457,7 +506,7 @@ local snippets = { ), } -function M.load_snippets() +function PythonSnippets.load_snippets() if not config.python_lua_snippets then return end @@ -467,8 +516,4 @@ function M.load_snippets() vim.g._python_nvim_snippets_loaded = true end -return setmetatable(M, { - __index = function(_, k) - return require("python.snip")[k] - end, -}) +return PythonSnippets diff --git a/lua/python/state/init.lua b/lua/python/state/init.lua index a88fbfa..daa51ab 100644 --- a/lua/python/state/init.lua +++ b/lua/python/state/init.lua @@ -1,8 +1,8 @@ -local M = {} +local PythonStateM = {} ---@class PythonStateVEnv ----@field python_interpreter string | nil ----@field venv_path string | nil +---@field python_interpreter string +---@field venv_path string ---@field install_method string ---@field install_file string ---@field source string @@ -32,13 +32,14 @@ Should look like this } } -]] -- +]] +-- ---@class PythonState ---@field venvs table ---@field dap table PythonState = PythonState or { venvs = {}, - dap = {} + dap = {}, } local data_path = vim.fn.stdpath("data") @@ -46,7 +47,7 @@ local state_dir = vim.fs.joinpath(data_path, "python.nvim") local state_path = vim.fs.joinpath(state_dir, "state.json") ---@return PythonState -function M.State() +function PythonStateM.State() if vim.fn.isdirectory(state_dir) == 0 then if vim.fn.mkdir(state_dir, "p") ~= 1 then error(string.format("python.nvim: mkdirp error creating directory: %s", state_dir)) @@ -96,13 +97,9 @@ local function merge_tables(...) end ---@param new_state PythonState -function M.save(new_state) +function PythonStateM.save(new_state) local result_state = merge_tables(PythonState, new_state) - vim.fn.writefile({vim.json.encode(result_state)}, state_path, "s") + vim.fn.writefile({ vim.json.encode(result_state) }, state_path, "s") end -return setmetatable(M, { - __index = function(_, k) - return require("python.state")[k] - end, -}) +return PythonStateM diff --git a/lua/python/test/init.lua b/lua/python/test/init.lua index cb9cf1f..928e25c 100644 --- a/lua/python/test/init.lua +++ b/lua/python/test/init.lua @@ -1,4 +1,4 @@ -local M = {} +local PythonTest = {} local dap = require("python.dap") local config = require("python.config") @@ -8,7 +8,7 @@ local neotest = require("neotest") -- Setup neotest and merge configuration to supply current venv local function neotest_setup() - local venv = require('python.venv').current_venv() + local venv = require("python.venv").current_venv() local neotest_python_config = { runner = config.test.test_runner, @@ -22,27 +22,27 @@ local function neotest_setup() local new_config = { adapters = { - require("neotest-python")(neotest_python_config) - } + require("neotest-python")(neotest_python_config), + }, } -- Merge existing configuration - local merged = vim.tbl_deep_extend('force', neotest_config, new_config) + local merged = vim.tbl_deep_extend("force", neotest_config, new_config) neotest.setup(merged) end -function M.neotest_test_method() +function PythonTest.neotest_test_method() neotest_setup() neotest.run.run() end -function M.neotest_test() +function PythonTest.neotest_test() neotest_setup() neotest.run.run({ suite = true }) neotest.summary.open() end -function M.neotest_debug_test() +function PythonTest.neotest_debug_test() dap.prepare_debugpy(function(venv) vim.schedule(function() dap_python.setup(vim.fs.joinpath(venv.path, "bin", "python3"), {}) @@ -52,13 +52,13 @@ function M.neotest_debug_test() end) end -function M.neotest_test_file() +function PythonTest.neotest_test_file() neotest_setup() neotest.run.run(vim.fn.expand("%")) neotest.summary.open() end -function M.neotest_debug_test_file() +function PythonTest.neotest_debug_test_file() dap.prepare_debugpy(function(venv) vim.schedule(function() dap_python.setup(vim.fs.joinpath(venv.path, "bin", "python3"), {}) @@ -68,7 +68,7 @@ function M.neotest_debug_test_file() end) end -function M.neotest_debug_test_method() +function PythonTest.neotest_debug_test_method() dap.prepare_debugpy(function(venv) vim.schedule(function() dap_python.setup(vim.fs.joinpath(venv.path, "bin", "python3"), {}) @@ -78,11 +78,6 @@ function M.neotest_debug_test_method() end) end -function M.load_commands() -end +function PythonTest.load_commands() end -return setmetatable(M, { - __index = function(_, k) - return require("python.test")[k] - end, -}) +return PythonTest diff --git a/lua/python/treesitter/commands.lua b/lua/python/treesitter/commands.lua index 8359ed9..7afcc32 100644 --- a/lua/python/treesitter/commands.lua +++ b/lua/python/treesitter/commands.lua @@ -1,246 +1,254 @@ -- Utility functions and commands utilizing treesitter -local M = {} -local config = require('python.config') +local PythonTreeSitterCommands = {} +local config = require("python.config") ---get node at cursor and validate that the user has at least nvim 0.9 ---@return nil|TSNode nil if no node or nvim version too old local function getNodeAtCursor() - if vim.treesitter.get_node == nil then - vim.notify("python.nvim requires at least nvim 0.9.", vim.log.levels.WARN) - return - end - return vim.treesitter.get_node() + if vim.treesitter.get_node == nil then + vim.notify("python.nvim requires at least nvim 0.9.", vim.log.levels.WARN) + return + end + return vim.treesitter.get_node() end ---@param node TSNode node to start search ---@param node_types table list of node types to look for ---@return nil | TSNode local function findNodeOfParentsWithType(node, node_types) - local nodeType = node:type() - if vim.list_contains(node_types, nodeType) then - return node - end - local parent = node:parent() - if parent then - return findNodeOfParentsWithType(parent, node_types) - end - return nil + local nodeType = node:type() + if vim.list_contains(node_types, nodeType) then + return node + end + local parent = node:parent() + if parent then + return findNodeOfParentsWithType(parent, node_types) + end + return nil end ---@param node TSNode ---@return string -local function getNodeText(node) return vim.treesitter.get_node_text(node, 0) end +local function getNodeText(node) + return vim.treesitter.get_node_text(node, 0) +end ---@param node TSNode ---@param replacementText string local function replaceNodeText(node, replacementText) - local startRow, startCol, endRow, endCol = node:range() - local lines = vim.split(replacementText, "\n") - vim.api.nvim_buf_set_text(0, startRow, startCol, endRow, endCol, lines) + local startRow, startCol, endRow, endCol = node:range() + local lines = vim.split(replacementText, "\n") + vim.api.nvim_buf_set_text(0, startRow, startCol, endRow, endCol, lines) end -function M.ts_toggle_enumerate() - local node = getNodeAtCursor() - if not node then return end - - local listNode - if node:type() == "for_statement" then - listNode = node - elseif node:type() == "indentifier" or node:type() == "pattern_list" then - listNode = node:parent() - else - vim.notify_once("python.nvim: Treesitter, not on a python list", vim.log.levels.WARN) - return - end - - if not listNode then return end - local left = listNode:field("left")[1] - local right = listNode:field("right")[1] - if not left or not right then - return - end - local left_text = getNodeText(left) - local right_text = getNodeText(right) - if not left_text or not right_text then - return - end - - local left_items = vim.split(left_text, ",", { trimempty = true }) - local is_enumerate = string.match(right_text, "^enumerate.+") - - if #left_items == 1 and not is_enumerate then - left_text = ("idx, %s"):format(left_text) - right_text = ("enumerate(%s)"):format(right_text) - - replaceNodeText(right, right_text) - replaceNodeText(left, left_text) - elseif #left_items > 1 and is_enumerate then - local right_text_match = string.match(right_text, [[^enumerate%W(.+)%W]]) - right_text = right_text_match - replaceNodeText(right, right_text) - - left_text = left_items[2] - left_text = left_text:gsub("%s+", "") - replaceNodeText(left, left_text) - end +function PythonTreeSitterCommands.ts_toggle_enumerate() + local node = getNodeAtCursor() + if not node then + return + end + + local listNode + if node:type() == "for_statement" then + listNode = node + elseif node:type() == "indentifier" or node:type() == "pattern_list" then + listNode = node:parent() + else + vim.notify_once("python.nvim: Treesitter, not on a python list", vim.log.levels.WARN) + return + end + + if not listNode then + return + end + local left = listNode:field("left")[1] + local right = listNode:field("right")[1] + if not left or not right then + return + end + local left_text = getNodeText(left) + local right_text = getNodeText(right) + if not left_text or not right_text then + return + end + + local left_items = vim.split(left_text, ",", { trimempty = true }) + local is_enumerate = string.match(right_text, "^enumerate.+") + + if #left_items == 1 and not is_enumerate then + left_text = ("idx, %s"):format(left_text) + right_text = ("enumerate(%s)"):format(right_text) + + replaceNodeText(right, right_text) + replaceNodeText(left, left_text) + elseif #left_items > 1 and is_enumerate then + local right_text_match = string.match(right_text, [[^enumerate%W(.+)%W]]) + right_text = right_text_match + replaceNodeText(right, right_text) + + left_text = left_items[2] + left_text = left_text:gsub("%s+", "") + replaceNodeText(left, left_text) + end end local function get_cursor() - local cursor = vim.api.nvim_win_get_cursor(0) - return { row = cursor[1], col = cursor[2] } + local cursor = vim.api.nvim_win_get_cursor(0) + return { row = cursor[1], col = cursor[2] } end - local function get_visual_selection() - local _, start_row, start_col, _ = unpack(vim.fn.getpos("'<")) - local _, end_row, end_col, _ = unpack(vim.fn.getpos("'>")) - local cursor = vim.api.nvim_win_get_cursor(0) - - - start_row = start_row -1 - start_col = start_col -1 - end_row = end_row -1 - end_col = end_col - - - local result = { - start = { - row = start_row, - col = start_col - }, - ending = { - row = end_row, - col = end_col - } - } - return result + local _, start_row, start_col, _ = unpack(vim.fn.getpos("'<")) + local _, end_row, end_col, _ = unpack(vim.fn.getpos("'>")) + local cursor = vim.api.nvim_win_get_cursor(0) + + start_row = start_row - 1 + start_col = start_col - 1 + end_row = end_row - 1 + end_col = end_col + + local result = { + start = { + row = start_row, + col = start_col, + }, + ending = { + row = end_row, + col = end_col, + }, + } + return result end ---@param subtitute_option nil|string if string then use as substitute ---@param line_mode bool if visual mode is line mode --- otherwise select from config local function visual_wrap_subsitute_options(subtitute_option, line_mode) - - -- TODO dont duplicate logic with select and not select - - local positions = get_visual_selection() - local start_pos = positions.start - local end_pos = positions.ending - local selected_buf_text = vim.api.nvim_buf_get_text(0, start_pos.row, start_pos.col, end_pos.row, end_pos.col, {}) - local node_text = vim.fn.join(selected_buf_text, "\n") - local new_text - - if subtitute_option and subtitute_option ~= "" then - new_text = subtitute_option:format(node_text) - local lines = vim.split(new_text, "\n") - if end_pos.col == 2147483647 then - end_pos.col = -1 - end - local status, _ = pcall(vim.api.nvim_buf_set_text, 0, start_pos.row, start_pos.col, end_pos.row, end_pos.col, lines) - if not status then - vim.api.nvim_buf_set_text(0, start_pos.row, start_pos.col, end_pos.row, end_pos.col -1, lines) - end - return - end - vim.ui.select(config.treesitter.functions.wrapper.substitute_options, { - prompt = ("Wrapping: %s <- with:"):format(node_text), - }, function(selection) - if not selection then - return - end - new_text = selection:format(node_text) - local lines = vim.split(new_text, "\n") - - if end_pos.col == 2147483647 then - end_pos.col = -1 - end - local status, _ = pcall(vim.api.nvim_buf_set_text, 0, start_pos.row, start_pos.col, end_pos.row, end_pos.col, lines) - if not status then - vim.api.nvim_buf_set_text(0, start_pos.row, start_pos.col, end_pos.row, end_pos.col -1, lines) - end - return - end) + -- TODO dont duplicate logic with select and not select + + local positions = get_visual_selection() + local start_pos = positions.start + local end_pos = positions.ending + local selected_buf_text = vim.api.nvim_buf_get_text(0, start_pos.row, start_pos.col, end_pos.row, end_pos.col, {}) + local node_text = vim.fn.join(selected_buf_text, "\n") + local new_text + + if subtitute_option and subtitute_option ~= "" then + new_text = subtitute_option:format(node_text) + local lines = vim.split(new_text, "\n") + if end_pos.col == 2147483647 then + end_pos.col = -1 + end + local status, _ = pcall(vim.api.nvim_buf_set_text, 0, start_pos.row, start_pos.col, end_pos.row, end_pos.col, lines) + if not status then + vim.api.nvim_buf_set_text(0, start_pos.row, start_pos.col, end_pos.row, end_pos.col - 1, lines) + end + return + end + vim.ui.select(config.treesitter.functions.wrapper.substitute_options, { + prompt = ("Wrapping: %s <- with:"):format(node_text), + }, function(selection) + if not selection then + return + end + new_text = selection:format(node_text) + local lines = vim.split(new_text, "\n") + + if end_pos.col == 2147483647 then + end_pos.col = -1 + end + local status, _ = pcall(vim.api.nvim_buf_set_text, 0, start_pos.row, start_pos.col, end_pos.row, end_pos.col, lines) + if not status then + vim.api.nvim_buf_set_text(0, start_pos.row, start_pos.col, end_pos.row, end_pos.col - 1, lines) + end + return + end) end ---@param subtitute_option nil|string if string then use as substitute --- otherwise select from config -function M.ts_wrap_at_cursor(subtitute_option) - local m = vim.fn.visualmode() -- detect current mode - - if m == 'v' or m == '\22' then - visual_wrap_subsitute_options(subtitute_option) - return - elseif m == 'V' then - visual_wrap_subsitute_options(subtitute_option, true) - return - end - - local node = getNodeAtCursor() - if not node then return end - - local node_types = config.treesitter.functions.wrapper.find_types - local find_node = findNodeOfParentsWithType(node, node_types) - if not find_node then - vim.notify(("python.nvim: Could not find ts node of type: %s"):format(vim.inspect(node_types))) - return - end - - local node_text = getNodeText(find_node) - local new_text - - if subtitute_option and subtitute_option ~= "" then - new_text = subtitute_option:format(node_text) - replaceNodeText(find_node, new_text) - return - end - vim.ui.select(config.treesitter.functions.wrapper.substitute_options, { - prompt = ("Wrapping: %s <- with:"):format(node_text), - }, function(selection) - if not selection then - return - end - new_text = selection:format(node_text) - replaceNodeText(find_node, new_text) - return - end) +function PythonTreeSitterCommands.ts_wrap_at_cursor(subtitute_option) + local m = vim.fn.visualmode() -- detect current mode + + if m == "v" or m == "\22" then + visual_wrap_subsitute_options(subtitute_option) + return + elseif m == "V" then + visual_wrap_subsitute_options(subtitute_option, true) + return + end + + local node = getNodeAtCursor() + if not node then + return + end + + local node_types = config.treesitter.functions.wrapper.find_types + local find_node = findNodeOfParentsWithType(node, node_types) + if not find_node then + vim.notify(("python.nvim: Could not find ts node of type: %s"):format(vim.inspect(node_types))) + return + end + + local node_text = getNodeText(find_node) + local new_text + + if subtitute_option and subtitute_option ~= "" then + new_text = subtitute_option:format(node_text) + replaceNodeText(find_node, new_text) + return + end + vim.ui.select(config.treesitter.functions.wrapper.substitute_options, { + prompt = ("Wrapping: %s <- with:"):format(node_text), + }, function(selection) + if not selection then + return + end + new_text = selection:format(node_text) + replaceNodeText(find_node, new_text) + return + end) end -function M.pythonFStr() - local maxCharacters = 200 -- safeguard to prevent converting invalid code - local node = getNodeAtCursor() - if not node then return end - - local strNode - if node:type() == "string" then - strNode = node - elseif node:type():find("^string_") then - strNode = node:parent() - elseif node:type() == "escape_sequence" then - strNode = node:parent():parent() - else - return - end - if not strNode then return end - local text = getNodeText(strNode) - - -- GUARD - if text == "" then return end -- don't convert empty strings, user might want to enter sth - if #text > maxCharacters then return end -- safeguard on converting invalid code - - local isFString = text:find("^r?f") -- rf -> raw-formatted-string - local hasBraces = text:find("{.-[^%d,%s].-}") -- nonRegex-braces, see #12 and #15 - - if not isFString and hasBraces then - text = "f" .. text - replaceNodeText(strNode, text) - elseif isFString and not hasBraces then - text = text:sub(2) - replaceNodeText(strNode, text) - end +function PythonTreeSitterCommands.pythonFStr() + local maxCharacters = 200 -- safeguard to prevent converting invalid code + local node = getNodeAtCursor() + if not node then + return + end + + local strNode + if node:type() == "string" then + strNode = node + elseif node:type():find("^string_") then + strNode = node:parent() + elseif node:type() == "escape_sequence" then + strNode = node:parent():parent() + else + return + end + if not strNode then + return + end + local text = getNodeText(strNode) + + -- GUARD + if text == "" then + return + end -- don't convert empty strings, user might want to enter sth + if #text > maxCharacters then + return + end -- safeguard on converting invalid code + + local isFString = text:find("^r?f") -- rf -> raw-formatted-string + local hasBraces = text:find("{.-[^%d,%s].-}") -- nonRegex-braces, see #12 and #15 + + if not isFString and hasBraces then + text = "f" .. text + replaceNodeText(strNode, text) + elseif isFString and not hasBraces then + text = text:sub(2) + replaceNodeText(strNode, text) + end end -return setmetatable(M, { - __index = function(_, k) - return require("python.treesitter.commands")[k] - end, -}) +return PythonTreeSitterCommands diff --git a/lua/python/treesitter/init.lua b/lua/python/treesitter/init.lua index 5679247..b406475 100644 --- a/lua/python/treesitter/init.lua +++ b/lua/python/treesitter/init.lua @@ -1,18 +1,18 @@ -local tsutil = require('nvim-treesitter.ts_utils') -local nodes = require('python.treesitter.nodes') +local tsutil = require("nvim-treesitter.ts_utils") +local nodes = require("python.treesitter.nodes") -local M = { +local PythonTreeSitter = { queries = { - query_func = '(function_definition)', - } + query_func = "(function_definition)", + }, } -function M.test_ts_queries() +function PythonTreeSitter.test_ts_queries() local current_node = tsutil.get_node_at_cursor() if not current_node then return end - for name, query in pairs(M.queries) do + for name, query in pairs(PythonTreeSitter.queries) do ---@class vim.treesitter.Query result = vim.treesitter.query.parse("python", query) print(vim.inspect(result.captures)) @@ -20,8 +20,4 @@ function M.test_ts_queries() end end -return setmetatable(M, { - __index = function(_, k) - return require("python.treesitter")[k] - end, -}) +return PythonTreeSitter diff --git a/lua/python/treesitter/nodes.lua b/lua/python/treesitter/nodes.lua index 8c7291d..c1315a4 100644 --- a/lua/python/treesitter/nodes.lua +++ b/lua/python/treesitter/nodes.lua @@ -1,10 +1,10 @@ -local M = {} +local PythonTreeSitterNodes = {} -- part of the code from polarmutex/contextprint.nvim -local ts_utils = require('nvim-treesitter.ts_utils') -local ts_query = require('nvim-treesitter.query') -local parsers = require('nvim-treesitter.parsers') -local locals = require('nvim-treesitter.locals') +local ts_utils = require("nvim-treesitter.ts_utils") +local ts_query = require("nvim-treesitter.query") +local parsers = require("nvim-treesitter.parsers") +local locals = require("nvim-treesitter.locals") -- local vim_query = require("vim.treesitter.query") local api = vim.api local fn = vim.fn @@ -14,8 +14,7 @@ if parse == nil then parse = vim.treesitter.query.parse_query end - -M.count_parents = function(node) +PythonTreeSitterNodes.count_parents = function(node) local count = 0 local n = node.declaring_node while n ~= nil do @@ -27,9 +26,9 @@ end -- @param nodes Array -- perf note. I could memoize some values here... -M.sort_nodes = function(nodes) +PythonTreeSitterNodes.sort_nodes = function(nodes) table.sort(nodes, function(a, b) - return M.count_parents(a) < M.count_parents(b) + return PythonTreeSitterNodes.count_parents(a) < PythonTreeSitterNodes.count_parents(b) end) return nodes end @@ -42,8 +41,8 @@ end -- name: string -- type: string -- }] -M.get_nodes = function(query, lang, defaults, bufnr) - if lang ~= 'python' then +PythonTreeSitterNodes.get_nodes = function(query, lang, defaults, bufnr) + if lang ~= "python" then return nil end bufnr = bufnr or 0 @@ -51,7 +50,7 @@ M.get_nodes = function(query, lang, defaults, bufnr) return parse(lang, query) end) if not success then - vim.notify_once('treesitter parse failed, make sure treesitter installed and setup correctly', vim.log.levels.WARN) + vim.notify_once("treesitter parse failed, make sure treesitter installed and setup correctly", vim.log.levels.WARN) return nil end @@ -62,22 +61,22 @@ M.get_nodes = function(query, lang, defaults, bufnr) for match in ts_query.iter_prepared_matches(parsed_query, root, bufnr, start_row, end_row) do local sRow, sCol, eRow, eCol local declaration_node - local type = 'nil' - local name = 'nil' + local type = "nil" + local name = "nil" locals.recurse_local_nodes(match, function(_, node, path) - local idx = string.find(path, '.', 1, true) + local idx = string.find(path, ".", 1, true) local op = string.sub(path, idx + 1, #path) -- local a1, b1, c1, d1 = vim.treesitter.get_node_range(node) type = string.sub(path, 1, idx - 1) if name == nil then - name = defaults[type] or 'empty' + name = defaults[type] or "empty" end - if op == 'name' then + if op == "name" then name = get_node_text(node, bufnr) - elseif op == 'declaration' then + elseif op == "declaration" then declaration_node = node sRow, sCol, eRow, eCol = node:range() sRow = sRow + 1 @@ -103,15 +102,15 @@ end local nodes = {} local nodestime = {} -M.get_all_nodes = function(query, lang, defaults, bufnr, pos_row, pos_col, ntype) +PythonTreeSitterNodes.get_all_nodes = function(query, lang, defaults, bufnr, pos_row, pos_col, ntype) -- ulog(query, lang, defaults, pos_row, pos_col) bufnr = bufnr or api.nvim_get_current_buf() local key = tostring(bufnr) .. query - local filetime = fn.getftime(fn.expand('%')) + local filetime = fn.getftime(fn.expand("%")) if nodes[key] ~= nil and nodestime[key] ~= nil and filetime == nodestime[key] then return nodes[key] end - if lang ~= 'go' then + if lang ~= "go" then return nil end -- ulog(bufnr, nodestime[key], filetime) @@ -134,9 +133,9 @@ M.get_all_nodes = function(query, lang, defaults, bufnr, pos_row, pos_col, ntype local sRow, sCol, eRow, eCol local declaration_node local type_node - local type = '' - local name = '' - local op = '' + local type = "" + local name = "" + local op = "" -- local method_receiver = "" -- ulog(match) @@ -145,15 +144,15 @@ M.get_all_nodes = function(query, lang, defaults, bufnr, pos_row, pos_col, ntype -- The query may return multiple nodes, e.g. -- (type_declaration (type_spec name:(type_identifier)@type_decl.name type:(type_identifier)@type_decl.type))@type_decl.declaration -- returns { { @type_decl.name, @type_decl.type, @type_decl.declaration} ... } - local idx = string.find(path, '.[^.]*$') -- find last `.` + local idx = string.find(path, ".[^.]*$") -- find last `.` op = string.sub(path, idx + 1, #path) local a1, b1, c1, d1 = vim.treesitter.get_node_range(node) - local dbg_txt = get_node_text(node, bufnr) or '' + local dbg_txt = get_node_text(node, bufnr) or "" if #dbg_txt > 100 then - dbg_txt = string.sub(dbg_txt, 1, 100) .. '...' + dbg_txt = string.sub(dbg_txt, 1, 100) .. "..." end - type = string.sub(path, 1, idx - 1) -- e.g. struct.name, type is struct - if type:find('type') and op == 'type' then -- type_declaration.type + type = string.sub(path, 1, idx - 1) -- e.g. struct.name, type is struct + if type:find("type") and op == "type" then -- type_declaration.type node_type = get_node_text(node, bufnr) -- ulog('type: ' .. type) end @@ -195,8 +194,7 @@ M.get_all_nodes = function(query, lang, defaults, bufnr, pos_row, pos_col, ntype end if type_node ~= nil and ntype then -- ulog('type_only') - sRow, sCol, eRow, eCol = - ts_utils.get_vim_range({ vim.treesitter.get_node_range(type_node) }, bufnr) + sRow, sCol, eRow, eCol = ts_utils.get_vim_range({ vim.treesitter.get_node_range(type_node) }, bufnr) table.insert(results, { type_node = type_node, dim = { s = { r = sRow, c = sCol }, e = { r = eRow, c = eCol } }, @@ -212,14 +210,14 @@ M.get_all_nodes = function(query, lang, defaults, bufnr, pos_row, pos_col, ntype return results end -M.nodes_in_buf = function(query, default, bufnr, row, col) +PythonTreeSitterNodes.nodes_in_buf = function(query, default, bufnr, row, col) bufnr = bufnr or vim.api.nvim_get_current_buf() - local ft = vim.api.nvim_buf_get_option(bufnr, 'ft') + local ft = vim.api.nvim_buf_get_option(bufnr, "ft") if row == nil or col == nil then row, col = unpack(vim.api.nvim_win_get_cursor(0)) row, col = row, col + 1 end - local ns = M.get_all_nodes(query, ft, default, bufnr, row, col, true) + local ns = PythonTreeSitterNodes.get_all_nodes(query, ft, default, bufnr, row, col, true) if ns == nil then -- vim.notify_once('Unable to find any nodes.', vim.log.levels.DEBUG) -- ulog('Unable to find any nodes. place your cursor on a go symbol and try again') @@ -229,31 +227,28 @@ M.nodes_in_buf = function(query, default, bufnr, row, col) return ns end -M.nodes_at_cursor = function(query, default, bufnr, ntype) +PythonTreeSitterNodes.nodes_at_cursor = function(query, default, bufnr, ntype) local row, col = unpack(vim.api.nvim_win_get_cursor(0)) row, col = row, col + 1 bufnr = bufnr or vim.api.nvim_get_current_buf() - local ft = vim.api.nvim_buf_get_option(bufnr, 'ft') - if ft ~= 'go' then + local ft = vim.api.nvim_buf_get_option(bufnr, "ft") + if ft ~= "go" then return end - local ns = M.get_all_nodes(query, ft, default, bufnr, row, col, ntype) + local ns = PythonTreeSitterNodes.get_all_nodes(query, ft, default, bufnr, row, col, ntype) if ns == nil then - vim.notify_once( - 'Unable to find any nodes. place your cursor on a go symbol and try again', - vim.log.levels.DEBUG - ) + vim.notify_once("Unable to find any nodes. place your cursor on a go symbol and try again", vim.log.levels.DEBUG) -- ulog('Unable to find any nodes. place your cursor on a go symbol and try again') return nil end -- ulog(#ns) - local nodes_at_cursor = M.sort_nodes(M.intersect_nodes(ns, row, col)) + local nodes_at_cursor = PythonTreeSitterNodes.sort_nodes(PythonTreeSitterNodes.intersect_nodes(ns, row, col)) if not nodes_at_cursor then -- cmp-command-line will causing cursor to move to end of line -- lets try move back a bit and try to find nodes again row, col = unpack(vim.api.nvim_win_get_cursor(0)) row, col = row, col - 5 - nodes_at_cursor = M.sort_nodes(M.intersect_nodes(ns, row, col)) + nodes_at_cursor = PythonTreeSitterNodes.sort_nodes(PythonTreeSitterNodes.intersect_nodes(ns, row, col)) end -- ulog(row, col, vim.inspect(nodes_at_cursor):sub(1, 100)) if nodes_at_cursor == nil or #nodes_at_cursor == 0 then @@ -270,7 +265,7 @@ M.nodes_at_cursor = function(query, default, bufnr, ntype) return nodes_at_cursor end -function M.inside_function() +function PythonTreeSitterNodes.inside_function() local current_node = ts_utils.get_node_at_cursor() if not current_node then return false @@ -278,7 +273,7 @@ function M.inside_function() local expr = current_node while expr do - if expr:type() == 'function_definition' then + if expr:type() == "function_definition" then return true end expr = expr:parent() @@ -286,8 +281,4 @@ function M.inside_function() return false end -return setmetatable(M, { - __index = function(_, k) - return require("python.treesitter.nodes")[k] - end, -}) +return PythonTreeSitterNodes diff --git a/lua/python/ui/init.lua b/lua/python/ui/init.lua index d4ecadf..1d7bc02 100644 --- a/lua/python/ui/init.lua +++ b/lua/python/ui/init.lua @@ -1,7 +1,7 @@ -local config = require('python.config') +local config = require("python.config") local split_default_style = { - split = 'below', + split = "below", win = 0, width = 40, height = 10, @@ -19,7 +19,7 @@ local popup_default_style = { height = 20, row = vim.o.lines - 3, col = vim.o.columns - 2, - style = "minimal" + style = "minimal", } ---@class UI @@ -54,20 +54,20 @@ local empty_system_ui = { line_count = 0, } -local M = { - system_ui = vim.deepcopy(empty_system_ui) +local PythonUI = { + system_ui = vim.deepcopy(empty_system_ui), } local function _deactivate_system_call_ui() - if M.system_ui.ui then - M.system_ui.ui:unmount() + if PythonUI.system_ui.ui then + PythonUI.system_ui.ui:unmount() end - M.system_ui = vim.deepcopy(empty_system_ui) + PythonUI.system_ui = vim.deepcopy(empty_system_ui) end -- Turn off ui ---@param timeout? integer time in milliseconds to close ui. Defaults to config option -function M.deactivate_system_call_ui(timeout) +function PythonUI.deactivate_system_call_ui(timeout) if timeout == nil then timeout = config.ui.ui_close_timeout end @@ -82,22 +82,22 @@ function M.deactivate_system_call_ui(timeout) end --- Open a ui window to show the output of the command being called. -function M.activate_system_call_ui() - M.deactivate_system_call_ui(0) +function PythonUI.activate_system_call_ui() + PythonUI.deactivate_system_call_ui(0) local ui = nil if config.ui.default_ui_style == "popup" then - local win_opts = vim.tbl_deep_extend('keep', config.ui.popup.win_opts or {}, popup_default_style) + local win_opts = vim.tbl_deep_extend("keep", config.ui.popup.win_opts or {}, popup_default_style) ui = UI:new({ win_opts = win_opts }) end if config.ui.default_ui_style == "split" then - local win_opts = vim.tbl_deep_extend('keep', config.ui.split.win_opts or {}, split_default_style) + local win_opts = vim.tbl_deep_extend("keep", config.ui.split.win_opts or {}, split_default_style) ui = UI:new({ win_opts = win_opts }) end if ui then -- mount/open the component ui:mount() end - M.system_ui.ui = ui + PythonUI.system_ui.ui = ui end --- Open a ui w"indow to show the output of the command being called. @@ -105,8 +105,8 @@ end ---@param data string stdout data ---@param flush boolean clear ui text and replace with full output ---@param callback function callback function with no arguments -function M.show_system_call_progress(err, data, flush, callback) - if not M.system_ui.ui then +function PythonUI.show_system_call_progress(err, data, flush, callback) + if not PythonUI.system_ui.ui then return end @@ -119,32 +119,28 @@ function M.show_system_call_progress(err, data, flush, callback) end vim.schedule(function() - out = out:gsub('\r', '') - local _, line_count = out:gsub('\n', '\n') + out = out:gsub("\r", "") + local _, line_count = out:gsub("\n", "\n") if flush then - M.system_ui.line_count = 0 - pcall(vim.api.nvim_buf_set_text, M.system_ui.ui.buf, 0, 0, 0, 0, {}) + PythonUI.system_ui.line_count = 0 + pcall(vim.api.nvim_buf_set_text, PythonUI.system_ui.ui.buf, 0, 0, 0, 0, {}) end - local row = M.system_ui.line_count + local row = PythonUI.system_ui.line_count local increase = row + line_count - if not M.system_ui.ui.buf then + if not PythonUI.system_ui.ui.buf then return end -- Don't throw errors if we can't set the text on the next line for something reason - pcall(vim.api.nvim_buf_set_text, M.system_ui.ui.buf, row, 0, row, 0, vim.fn.split(out .. "\n", "\n")) - pcall(vim.api.nvim_win_set_cursor, M.system_ui.ui.win, { row, 0 }) + pcall(vim.api.nvim_buf_set_text, PythonUI.system_ui.ui.buf, row, 0, row, 0, vim.fn.split(out .. "\n", "\n")) + pcall(vim.api.nvim_win_set_cursor, PythonUI.system_ui.ui.win, { row, 0 }) - M.system_ui.line_count = increase + PythonUI.system_ui.line_count = increase if callback then callback() end end) end -return setmetatable(M, { - __index = function(_, k) - return require("python.ui")[k] - end, -}) +return PythonUI diff --git a/lua/python/uv/commands.lua b/lua/python/uv/commands.lua index 37c6896..f1991e1 100644 --- a/lua/python/uv/commands.lua +++ b/lua/python/uv/commands.lua @@ -1,8 +1,7 @@ local ui = require("python.ui") -local M = {} +local PythonUVCommands = {} - -function M.check_uv() +function PythonUVCommands.check_uv() if vim.fn.executable("uv") == 0 then return false end @@ -13,14 +12,16 @@ end ---@return table list of available python versions from uv local function uv_available_versions() local output = {} - vim.system({ "uv", "python", "list", "--only-downloads", "--output-format", "json" }, {}, function(obj) - local found_available = {} - local available_line_shown = false - local python_json = vim.json.decode(obj.stdout) - for _, pobj in pairs(python_json) do - table.insert(output, 1, pobj['key']) - end - end):wait() + vim + .system({ "uv", "python", "list", "--only-downloads", "--output-format", "json" }, {}, function(obj) + local found_available = {} + local available_line_shown = false + local python_json = vim.json.decode(obj.stdout) + for _, pobj in pairs(python_json) do + table.insert(output, 1, pobj["key"]) + end + end) + :wait() return output end @@ -28,77 +29,67 @@ end ---@return table list of available python versions from uv local function uv_installed_versions() local output = {} - vim.system({ "uv", "python", "list", "--only-installed", "--output-format", "json" }, {}, function(obj) - local found_available = {} - local available_line_shown = false - local python_json = vim.json.decode(obj.stdout) - for _, pobj in pairs(python_json) do - if string.find(pobj['path'], "uv") then - table.insert(output, 1, pobj['key']) + vim + .system({ "uv", "python", "list", "--only-installed", "--output-format", "json" }, {}, function(obj) + local found_available = {} + local available_line_shown = false + local python_json = vim.json.decode(obj.stdout) + for _, pobj in pairs(python_json) do + if string.find(pobj["path"], "uv") then + table.insert(output, 1, pobj["key"]) + end end - end - end):wait() + end) + :wait() return output end --- Install a python version using uv ---@param version string Python version to install via uv local function uv_install_version(version) - vim.schedule( - function() - vim.system( - { "uv", "python", "install", version }, - { - stdout = ui.show_system_call_progress - }, - function(obj2) - vim.schedule(function() - if obj2.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) - ui.deactivate_system_call_ui(10000) - else - ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() - ui.deactivate_system_call_ui() - end) - end + vim.schedule(function() + vim.system({ "uv", "python", "install", version }, { + stdout = ui.show_system_call_progress, + }, function(obj2) + vim.schedule(function() + if obj2.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) + ui.deactivate_system_call_ui(10000) + else + ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() + ui.deactivate_system_call_ui() end) end - ) - vim.schedule(function() - ui.activate_system_call_ui() end) - end - ) + end) + vim.schedule(function() + ui.activate_system_call_ui() + end) + end) end --- Delete a python version using uv ---@param version string Python version to install via uv local function uv_delete_version(version) - vim.schedule( - function() - vim.system( - { "uv", "python", "uninstall", version }, - { - stdout = ui.show_system_call_progress - }, - function(obj2) - vim.schedule(function() - if obj2.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) - ui.deactivate_system_call_ui(10000) - else - ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() - ui.deactivate_system_call_ui() - end) - end + vim.schedule(function() + vim.system({ "uv", "python", "uninstall", version }, { + stdout = ui.show_system_call_progress, + }, function(obj2) + vim.schedule(function() + if obj2.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) + ui.deactivate_system_call_ui(10000) + else + ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() + ui.deactivate_system_call_ui() end) end - ) - vim.schedule(function() - ui.activate_system_call_ui() end) - end - ) + end) + vim.schedule(function() + ui.activate_system_call_ui() + end) + end) end --- Execute uv directly with arguments passed by user. @@ -114,36 +105,30 @@ local function uv(opts) table.insert(args, fa) ::continue:: end - local cmd = { 'uv', unpack(args) } + local cmd = { "uv", unpack(args) } vim.schedule(function() - vim.system( - cmd, - { - cwd = vim.fn.getcwd(), - stdout = ui.show_system_call_progress, - stderr = ui.show_system_call_progress, - }, - function(obj) - vim.schedule( - function() - if obj.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj.stderr), vim.log.levels.ERROR) - return - end - ui.show_system_call_progress(obj.stderr, obj.stdout, true, function() - ui.deactivate_system_call_ui() - end) - end - ) - end - ) + vim.system(cmd, { + cwd = vim.fn.getcwd(), + stdout = ui.show_system_call_progress, + stderr = ui.show_system_call_progress, + }, function(obj) + vim.schedule(function() + if obj.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj.stderr), vim.log.levels.ERROR) + return + end + ui.show_system_call_progress(obj.stderr, obj.stdout, true, function() + ui.deactivate_system_call_ui() + end) + end) + end) ui.activate_system_call_ui() end) end local function uv_completion(arglead, cmdlin, cursorpos) local args = vim.split(cmdlin, " ") - local cmd = { 'uv' } + local cmd = { "uv" } for _, arg in pairs(args) do if arg == "UV" or arg == "" then @@ -175,8 +160,8 @@ local function uv_completion(arglead, cmdlin, cursorpos) end local patterns = { - "(%-%-%w+[%w-]*)", -- Matching '--argmument' in the output - "(%-%w),", -- Matching '-f' flag in the output + "(%-%-%w+[%w-]*)", -- Matching '--argmument' in the output + "(%-%w),", -- Matching '-f' flag in the output "\n%s%s([a-z-]+)%s", -- Matching ' subcommand' flag in the output } @@ -189,7 +174,7 @@ local function uv_completion(arglead, cmdlin, cursorpos) return result end -function M.uv_delete_python() +function PythonUVCommands.uv_delete_python() local versions = uv_installed_versions() vim.ui.select(versions, { prompt = "Select a python version to delete via uv: " }, function(selection) if not selection then @@ -199,7 +184,7 @@ function M.uv_delete_python() end) end -function M.uv_install_python() +function PythonUVCommands.uv_install_python() local versions = uv_available_versions() vim.ui.select(versions, { prompt = "Select a python version to install via uv: " }, function(selection) if not selection then @@ -209,8 +194,8 @@ function M.uv_install_python() end) end -function M.load_commands() - if not M.check_uv() then +function PythonUVCommands.load_commands() + if not PythonUVCommands.check_uv() then return end -- Special user command to pass through uv commands @@ -219,12 +204,8 @@ function M.load_commands() end, { desc = "python.nvim: pass-through uv commands.", complete = uv_completion, - nargs = "*" + nargs = "*", }) end -return setmetatable(M, { - __index = function(_, k) - return require("python.uv")[k] - end, -}) +return PythonUVCommands diff --git a/lua/python/uv/init.lua b/lua/python/uv/init.lua index 74f97e6..2c39729 100644 --- a/lua/python/uv/init.lua +++ b/lua/python/uv/init.lua @@ -1,7 +1,3 @@ -local M = {} +local PythonUV = {} -return setmetatable(M, { - __index = function(_, k) - return require("python.uv")[k] - end, -}) +return PythonUV diff --git a/lua/python/venv/create.lua b/lua/python/venv/create.lua index 95b6975..66e1304 100644 --- a/lua/python/venv/create.lua +++ b/lua/python/venv/create.lua @@ -1,27 +1,29 @@ -local config = require('python.config') +local config = require("python.config") local state = require("python.state") local interpreters = require("python.venv.interpreters") local ui = require("python.ui") -local M = {} +local PythonVENVCreate = {} ---Remove venv from state by key ---@param venv_key string ---@param delete_dir boolean attempt deletion of venv from directory -function M.delete_venv_from_state(venv_key, delete_dir) +function PythonVENVCreate.delete_venv_from_state(venv_key, delete_dir) if not venv_key then - vim.notify_once(string.format("python.nvim: Could not delete venv from state. venv_key was nil"), - vim.log.levels.ERROR) + vim.notify_once( + string.format("python.nvim: Could not delete venv from state. venv_key was nil"), + vim.log.levels.ERROR + ) return end local lsp = require("python.lsp") - local python_venv = require('python.venv') - + local python_venv = require("python.venv") local python_state = state.State() if not python_state.venvs[venv_key] then vim.notify_once( string.format("python.nvim: Could not delete venv from state. %s was not found in state.venv", venv_key), - vim.log.levels.ERROR) + vim.log.levels.ERROR + ) return end @@ -34,7 +36,7 @@ function M.delete_venv_from_state(venv_key, delete_dir) end if vim.fn.isdirectory(old_venv_path) then vim.ui.select({ "Yes", "No" }, { - prompt = ("Delete this venv directory?: %s"):format(old_venv_path) + prompt = ("Delete this venv directory?: %s"):format(old_venv_path), }, function(choice) if choice and choice == "Yes" then vim.fn.delete(old_venv_path, "rf") @@ -60,10 +62,10 @@ end ---@param venv_path string | nil full path to venv directory ---@param venv_name string name of the venv to set ---@param venv_source? string name of the source of the venv. useful in determining conda or venv -function M.python_set_venv(venv_path, venv_name, venv_source) +function PythonVENVCreate.python_set_venv(venv_path, venv_name, venv_source) local lsp = require("python.lsp") if venv_path then - local python_venv = require('python.venv') + local python_venv = require("python.venv") local current_venv_name = nil local current_venv = python_venv.current_venv() if current_venv then @@ -85,48 +87,46 @@ end ---@param venv_dir string full path to pdm lock file ---@param callback function ---@param script boolean are we installing from a script definition -function M.uv_sync(uv_lock_path, venv_dir, callback, script) +function PythonVENVCreate.uv_sync(uv_lock_path, venv_dir, callback, script) vim.schedule(function() if vim.fn.executable("uv") == 0 then vim.notify_once( ("python.nvim: 'uv' application not found please install: %s"):format("https://github.com/astral-sh/uv"), - vim.log.levels.ERROR) + vim.log.levels.ERROR + ) return end - vim.notify_once('python.nvim: starting uv sync at: ' .. uv_lock_path, vim.log.levels.INFO) + vim.notify_once("python.nvim: starting uv sync at: " .. uv_lock_path, vim.log.levels.INFO) local dir_name = vim.fs.dirname(uv_lock_path) local cmd = { - 'uv', 'sync', "--active", "--frozen" + "uv", + "sync", + "--active", + "--frozen", } -- Install from /// script definition in python script if script then vim.notify_once("python.nvim: Installing dependencies from uv script block") - cmd = { 'uv', 'sync', '--active', '--script', vim.api.nvim_buf_get_name(0) } + cmd = { "uv", "sync", "--active", "--script", vim.api.nvim_buf_get_name(0) } end - vim.system( - cmd, - { - cwd = dir_name, - stdout = ui.show_system_call_progress, - env = { - VIRTUAL_ENV = venv_dir - } + vim.system(cmd, { + cwd = dir_name, + stdout = ui.show_system_call_progress, + env = { + VIRTUAL_ENV = venv_dir, }, - function(obj) - vim.schedule( - function() - if obj.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj.stderr), vim.log.levels.ERROR) - return - end - ui.show_system_call_progress(obj.stderr, obj.stdout, true, function() - ui.deactivate_system_call_ui() - end) - callback() - end - ) - end - ) + }, function(obj) + vim.schedule(function() + if obj.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj.stderr), vim.log.levels.ERROR) + return + end + ui.show_system_call_progress(obj.stderr, obj.stdout, true, function() + ui.deactivate_system_call_ui() + end) + callback() + end) + end) ui.activate_system_call_ui() end) end @@ -135,166 +135,141 @@ end ---@param pdm_lock_path string full path to pdm lock file ---@param venv_dir string full path to pdm lock file ---@param callback function -function M.pdm_sync(pdm_lock_path, venv_dir, callback) +function PythonVENVCreate.pdm_sync(pdm_lock_path, venv_dir, callback) if vim.fn.executable("pdm") == 0 then vim.notify_once( ("python.nvim: 'pdm' application not found please install: %s"):format("https://pdm-project.org/en/latest/"), - vim.log.levels.ERROR) + vim.log.levels.ERROR + ) return end - vim.notify_once('python.nvim: starting pdm sync at: ' .. pdm_lock_path, vim.log.levels.INFO) + vim.notify_once("python.nvim: starting pdm sync at: " .. pdm_lock_path, vim.log.levels.INFO) local dir_name = vim.fs.dirname(pdm_lock_path) - vim.system( - { 'pdm', 'use', '-f', venv_dir }, - { - cwd = dir_name - }, - function(obj1) - if obj1.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj1.stderr), vim.log.levels.ERROR) - ui.deactivate_system_call_ui(10000) - return - end - vim.system( - { 'pdm', 'sync' }, - { - cwd = dir_name, - stdout = ui.show_system_call_progress - }, - function(obj2) - vim.schedule( - function() - if obj2.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) - return - end - ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() - ui.deactivate_system_call_ui() - end) - callback() - end - ) - end - ) + vim.system({ "pdm", "use", "-f", venv_dir }, { + cwd = dir_name, + }, function(obj1) + if obj1.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj1.stderr), vim.log.levels.ERROR) + ui.deactivate_system_call_ui(10000) + return + end + vim.system({ "pdm", "sync" }, { + cwd = dir_name, + stdout = ui.show_system_call_progress, + }, function(obj2) vim.schedule(function() - ui.activate_system_call_ui() + if obj2.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) + return + end + ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() + ui.deactivate_system_call_ui() + end) + callback() end) - end - ) + end) + vim.schedule(function() + ui.activate_system_call_ui() + end) + end) end --- Run pdm sync at lock file directory. Set env path when done. ---@param poetry_lock_path string full path to pdm lock file ---@param venv_dir string full path to pdm lock file ---@param callback function -function M.poetry_sync(poetry_lock_path, venv_dir, callback) +function PythonVENVCreate.poetry_sync(poetry_lock_path, venv_dir, callback) if vim.fn.executable("poetry") == 0 then vim.notify_once( ("python.nvim: 'poetry' application not found please install: %s"):format("https://python-poetry.org/"), - vim.log.levels.ERROR) + vim.log.levels.ERROR + ) return end - vim.notify_once('python.nvim: starting poetry sync at: ' .. poetry_lock_path, vim.log.levels.INFO) + vim.notify_once("python.nvim: starting poetry sync at: " .. poetry_lock_path, vim.log.levels.INFO) local dir_name = vim.fs.dirname(poetry_lock_path) - vim.system( - { 'poetry', 'env', 'use', vim.fs.joinpath(venv_dir, "bin", "python") }, - { - cwd = dir_name - }, - function(obj1) - if obj1.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj1.stderr), vim.log.levels.ERROR) - ui.deactivate_system_call_ui(10000) - return - end - vim.system( - { 'poetry', 'sync', '--no-root' }, - { - cwd = dir_name, - stdout = ui.show_system_call_progress - }, - function(obj2) - vim.schedule( - function() - if obj2.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) - return - end - ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() - ui.deactivate_system_call_ui() - end) - callback() - end - ) - end - ) + vim.system({ "poetry", "env", "use", vim.fs.joinpath(venv_dir, "bin", "python") }, { + cwd = dir_name, + }, function(obj1) + if obj1.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj1.stderr), vim.log.levels.ERROR) + ui.deactivate_system_call_ui(10000) + return + end + vim.system({ "poetry", "sync", "--no-root" }, { + cwd = dir_name, + stdout = ui.show_system_call_progress, + }, function(obj2) vim.schedule(function() - ui.activate_system_call_ui() + if obj2.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) + return + end + ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() + ui.deactivate_system_call_ui() + end) + callback() end) - end - ) + end) + vim.schedule(function() + ui.activate_system_call_ui() + end) + end) end --- Create venv with python venv module and pip install at location ---@param requirements_path string full path to requirements.txt, dev-requirements.txt or pyproject.toml ---@param venv_dir string ---@param callback function -function M.pip_install_with_venv(requirements_path, venv_dir, callback) +function PythonVENVCreate.pip_install_with_venv(requirements_path, venv_dir, callback) local dir_name = vim.fs.dirname(requirements_path) vim.notify_once( - 'python.nvim: starting pip install at: ' .. requirements_path .. ' in venv: ' .. venv_dir, + "python.nvim: starting pip install at: " .. requirements_path .. " in venv: " .. venv_dir, vim.log.levels.INFO ) - vim.schedule( - function() - local pip_path = venv_dir .. '/' .. 'bin/pip' - local pip_cmd = { pip_path, 'install', '-r', requirements_path } + vim.schedule(function() + local pip_path = venv_dir .. "/" .. "bin/pip" + local pip_cmd = { pip_path, "install", "-r", requirements_path } - if string.find(requirements_path, 'pyproject.toml$') then - pip_cmd = { pip_path, 'install', '.' } - end - vim.system( - pip_cmd, - { - cwd = dir_name, - stdout = ui.show_system_call_progress - }, - function(obj2) - vim.schedule(function() - if obj2.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) - ui.deactivate_system_call_ui(10000) - else - ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() - ui.deactivate_system_call_ui() - end) - callback() - end + if string.find(requirements_path, "pyproject.toml$") then + pip_cmd = { pip_path, "install", "." } + end + vim.system(pip_cmd, { + cwd = dir_name, + stdout = ui.show_system_call_progress, + }, function(obj2) + vim.schedule(function() + if obj2.code ~= 0 then + vim.notify_once("python.nvim: " .. vim.inspect(obj2.stderr), vim.log.levels.ERROR) + ui.deactivate_system_call_ui(10000) + else + ui.show_system_call_progress(obj2.stderr, obj2.stdout, true, function() + ui.deactivate_system_call_ui() end) + callback() end - ) - vim.schedule(function() - ui.activate_system_call_ui() end) - end - ) + end) + vim.schedule(function() + ui.activate_system_call_ui() + end) + end) end ---Create the python venv with selected interpreter" ---@param venv_path string path of venv to create ---@param python_interpreter string path to python executable to use -function M.create_venv_with_python(venv_path, python_interpreter) - vim.notify_once('python.nvim: creating venv at: ' .. venv_path, vim.log.levels.INFO) - local cmd = { python_interpreter, '-m', 'venv', venv_path } - vim.system( - cmd, - {}, - function(obj) +function PythonVENVCreate.create_venv_with_python(venv_path, python_interpreter) + vim.notify_once("python.nvim: creating venv at: " .. venv_path, vim.log.levels.INFO) + local cmd = { python_interpreter, "-m", "venv", venv_path } + vim + .system(cmd, {}, function(obj) if obj.code ~= 0 then - vim.notify_once('python.nvim: ' .. vim.inspect(obj.stderr .. obj.stdout), vim.log.levels.ERROR) + vim.notify_once("python.nvim: " .. vim.inspect(obj.stderr .. obj.stdout), vim.log.levels.ERROR) return end - end):wait() + end) + :wait() end --- Have user select a python interpreter from found options. @@ -303,8 +278,9 @@ end local function pick_python_interpreter() local co = coroutine.running() vim.schedule(function() - vim.ui.select(interpreters.python_interpreters(), { prompt = 'Select a python interpreter: ' }, - function(str) coroutine.resume(co, str) end) + vim.ui.select(interpreters.python_interpreters(), { prompt = "Select a python interpreter: " }, function(str) + coroutine.resume(co, str) + end) end) local python_interpreter_user_input = coroutine.yield() if not python_interpreter_user_input then @@ -313,7 +289,6 @@ local function pick_python_interpreter() return python_interpreter_user_input end - --- Have user input the path to the venv they want to create, with a supplied default --- Expected to be running in a coroutine for vim.ui ---@param detect DetectVEnv @@ -326,12 +301,11 @@ local function pick_venv_path(detect) local coro = coroutine.running() vim.schedule(function() vim.ui.input({ - prompt = description .. "\nInput new venv path: ", - default = default_input - }, - function(str) - coroutine.resume(coro, str) - end) + prompt = description .. "\nInput new venv path: ", + default = default_input, + }, function(str) + coroutine.resume(coro, str) + end) end) local venv_path_user_input = coroutine.yield() @@ -341,8 +315,10 @@ local function pick_venv_path(detect) local wanted_dir = vim.fs.dirname(venv_path_user_input) if vim.fn.isdirectory(wanted_dir) == 0 then - vim.notify_once(string.format("Error: directory of new venv doesn't exist: '%s'", venv_path_user_input), - vim.log.levels.ERROR) + vim.notify_once( + string.format("Error: directory of new venv doesn't exist: '%s'", venv_path_user_input), + vim.log.levels.ERROR + ) return end return venv_path_user_input @@ -367,7 +343,8 @@ local function venv_install_file(detect) if not install_function then vim.notify( ("python.nvim: Unable to find an install function for detected method: '%s'"):format(detect.venv.install_method), - vim.log.levels.ERROR) + vim.log.levels.ERROR + ) return end install_function(detect.venv.install_file, detect.venv.venv_path, function() end) @@ -389,7 +366,7 @@ local function venv_install_file(detect) return end - M.create_venv_with_python(venv_path_user_input, python_interpreter_user_input) + PythonVENVCreate.create_venv_with_python(venv_path_user_input, python_interpreter_user_input) install_function(detect.venv.install_file, venv_path_user_input, function() local val = { @@ -397,12 +374,12 @@ local function venv_install_file(detect) venv_path = venv_path_user_input, install_method = detect.venv.install_method, install_file = detect.venv.install_file, - source = "venv" + source = "venv", } local python_state = state.State() local venv_name = vim.fs.basename(vim.fs.dirname(detect.venv.install_file)) - M.python_set_venv(val.venv_path, venv_name) + PythonVENVCreate.python_set_venv(val.venv_path, venv_name) python_state.venvs[detect.dir] = val state.save(python_state) end) @@ -415,9 +392,7 @@ local function set_venv_state() local detect = detectM.detect_venv_dependency_file(false, false) if detect == nil then - vim.notify( - "python.nvim: Could not find a python dependency file for this cwd", - vim.log.levels.WARN) + vim.notify("python.nvim: Could not find a python dependency file for this cwd", vim.log.levels.WARN) return end @@ -427,7 +402,7 @@ end --- Automatically create venv directory and use multiple method to auto install dependencies --- Use module level variable Auto_set_python_venv_parent_dir to keep track of the last venv dir, so --- We don't do the creation process again when you are in the same project. -function M.create_and_install_venv() +function PythonVENVCreate.create_and_install_venv() set_venv_state() end @@ -440,18 +415,18 @@ local function delete_venv_from_selection() end vim.ui.select(keys, { - prompt = "Delete venv project from state: " + prompt = "Delete venv project from state: ", }, function(choice) if choice == nil then return end - M.delete_venv_from_state(choice, true) + PythonVENVCreate.delete_venv_from_state(choice, true) end) end ---Delete a venv from state and filesystem ---@param select boolean -function M.delete_venv(select) +function PythonVENVCreate.delete_venv(select) local detectM = require("python.venv.detect") if select then delete_venv_from_selection() @@ -462,17 +437,17 @@ function M.delete_venv(select) vim.notify("python.nvim: Current venv in not found in state. Cant continue", vim.log.levels.WARN) return end - M.delete_venv_from_state(detect.dir, true) + PythonVENVCreate.delete_venv_from_state(detect.dir, true) end --- Interactively set a venv in state. --- This is used when users manually select a venv and want it cached for next run. ---@param venv VEnv venv object to pull from -function M.user_set_venv_in_state_confirmation(venv) +function PythonVENVCreate.user_set_venv_in_state_confirmation(venv) local cwd = vim.fn.getcwd() local python_state = state.State() vim.ui.select({ "Yes", "No" }, { - prompt = string.format("Save env path for this cwd? '%s' -> '%s': ", cwd, venv.path) + prompt = string.format("Save env path for this cwd? '%s' -> '%s': ", cwd, venv.path), }, function(choice) if choice == "Yes" then python_state.venvs[cwd] = { @@ -480,28 +455,28 @@ function M.user_set_venv_in_state_confirmation(venv) venv_path = venv.path, install_method = "unknown", install_file = "unknown", - source = venv.source + source = venv.source, } state.save(python_state) - vim.notify_once(string.format( - "python.nvim: Saved venv '%s' for cwd '%s'. Use :PythonVEnvDeleteSelect to remove it.", - venv.path, cwd)) + vim.notify_once( + string.format( + "python.nvim: Saved venv '%s' for cwd '%s'. Use :PythonVEnvDeleteSelect to remove it.", + venv.path, + cwd + ) + ) end end) end -function M.detect_contents_for_dependencies(_, _) +function PythonVENVCreate.detect_contents_for_dependencies(_, _) local detectM = require("python.venv.detect") for pattern, install_function in pairs(detectM.check_file_patterns) do - local match = vim.fn.getline(vim.fn.search(pattern, 'n')) + local match = vim.fn.getline(vim.fn.search(pattern, "n")) if match ~= nil then install_function() end end end -return setmetatable(M, { - __index = function(_, k) - return require("python.venv.create")[k] - end, -}) +return PythonVENVCreate diff --git a/lua/python/venv/detect.lua b/lua/python/venv/detect.lua index fc9e8f2..58ae918 100644 --- a/lua/python/venv/detect.lua +++ b/lua/python/venv/detect.lua @@ -1,8 +1,8 @@ local state = require("python.state") local create = require("python.venv.create") -local M = {} -local IS_WINDOWS = vim.uv.os_uname().sysname == 'Windows_NT' +local PythonVENVDetect = {} +local IS_WINDOWS = vim.uv.os_uname().sysname == "Windows_NT" ---@class DetectVEnv ---@field dir string Current working directory found containing venv @@ -44,11 +44,7 @@ function DetectVEnv:found_in_cwd() if not venv_name then return false end - create.python_set_venv( - python_state.venvs[cwd].venv_path, - venv_name, - python_state.venvs[cwd].source - ) + create.python_set_venv(python_state.venvs[cwd].venv_path, venv_name, python_state.venvs[cwd].source) self.dir = cwd self.venv = python_state.venvs[cwd] return true @@ -56,78 +52,84 @@ function DetectVEnv:found_in_cwd() return false end -M.check_paths = { - ['requirements.txt'] = { +PythonVENVDetect.check_paths = { + ["requirements.txt"] = { func = function(install_file, venv_dir, callback) create.pip_install_with_venv(install_file, venv_dir, callback) end, type = "file", - desc = "Installing dependencies via requirements.txt and pip" + desc = "Installing dependencies via requirements.txt and pip", }, - ['dev-requirements.txt'] = { + ["dev-requirements.txt"] = { func = function(install_file, venv_dir, callback) create.pip_install_with_venv(install_file, venv_dir, callback) end, type = "file", - desc = "Installing dependencies via dev-requirements.txt and pip" + desc = "Installing dependencies via dev-requirements.txt and pip", }, - ['pyproject.toml'] = { + ["pyproject.toml"] = { func = function(install_file, venv_dir, callback) create.pip_install_with_venv(install_file, venv_dir, callback) end, type = "file", - desc = "Installing dependencies via pyproject.toml and pip" + desc = "Installing dependencies via pyproject.toml and pip", }, - ['poetry.lock'] = { + ["poetry.lock"] = { func = function(install_file, venv_dir, callback) create.poetry_sync(install_file, venv_dir, callback) end, type = "file", - desc = "Installing dependencies via poetry.lock and poetry" + desc = "Installing dependencies via poetry.lock and poetry", }, - ['pdm.lock'] = { + ["pdm.lock"] = { func = function(install_file, venv_dir, callback) create.pdm_sync(install_file, venv_dir, callback) end, type = "file", - desc = "Installing dependencies via pdm.lock and pdm" + desc = "Installing dependencies via pdm.lock and pdm", }, - ['uv.lock'] = { + ["uv.lock"] = { func = function(install_file, venv_dir, callback) create.uv_sync(install_file, venv_dir, callback, false) end, type = "file", - desc = "Installing dependencies via uv.lock and uv" + desc = "Installing dependencies via uv.lock and uv", }, - ['/// script'] = { + ["/// script"] = { func = function(install_file, venv_dir, callback) create.uv_sync(install_file, venv_dir, callback, true) end, type = "pattern", - desc = "Installing dependencies via uv /// script block and uv --script" - } + desc = "Installing dependencies via uv /// script block and uv --script", + }, } -M.check_paths_ordered_keys = { - "uv.lock", "/// script", "pdm.lock", "poetry.lock", "pyproject.toml", "dev-requirements.txt", "requirements.txt" +PythonVENVDetect.check_paths_ordered_keys = { + "uv.lock", + "/// script", + "pdm.lock", + "poetry.lock", + "pyproject.toml", + "dev-requirements.txt", + "requirements.txt", } --- Search for file or directory until we either the top of the git repo or root ---@param dir_or_file string name of directory or file ---@return string | nil found either nil or full path of found file/directory -function M.search_up(dir_or_file) +function PythonVENVDetect.search_up(dir_or_file) local found = nil local dir_to_check = nil -- get parent directory of current file in buffer via vim expand - local dir_template = '%:p:h' - while not found and (dir_to_check ~= '/' or dir_to_check ~= "C:\\" or dir_to_check ~= "D:\\") do + local dir_template = "%:p:h" + while not found and (dir_to_check ~= "/" or dir_to_check ~= "C:\\" or dir_to_check ~= "D:\\") do dir_to_check = vim.fn.expand(dir_template) local check_path = vim.fs.joinpath(dir_to_check, dir_or_file) - local check_git = vim.fs.joinpath(dir_to_check, '.git') + local check_git = vim.fs.joinpath(dir_to_check, ".git") if vim.fn.isdirectory(check_path) == 1 or vim.fn.filereadable(check_path) == 1 then found = vim.fs.joinpath(dir_to_check, dir_or_file) else - dir_template = dir_template .. ':h' + dir_template = dir_template .. ":h" end -- If we hit a .git directory then stop searching and return found even if nil if vim.fn.isdirectory(check_git) == 1 then @@ -142,12 +144,12 @@ end --- what the community probably wants detected first. ---@return string | nil found ---@return string | nil search -function M.search_for_detected_type() - for _, search in pairs(M.check_paths_ordered_keys) do - local check_type = M.check_paths[search].type +function PythonVENVDetect.search_for_detected_type() + for _, search in pairs(PythonVENVDetect.check_paths_ordered_keys) do + local check_type = PythonVENVDetect.check_paths[search].type if check_type == "file" then - local found = M.search_up(search) + local found = PythonVENVDetect.search_up(search) if found ~= nil then return found, search end @@ -171,8 +173,8 @@ end ---@return DetectVEnv | nil ---@param notify boolean Send notification when venv is not found ---@param cwd_allowed? boolean Allow use of cwd when detecting -function M.detect_venv_dependency_file(notify, cwd_allowed) - local found, found_search_path = M.search_for_detected_type() +function PythonVENVDetect.detect_venv_dependency_file(notify, cwd_allowed) + local found, found_search_path = PythonVENVDetect.search_for_detected_type() local found_parent_dir = vim.fs.dirname(found) @@ -203,7 +205,8 @@ function M.detect_venv_dependency_file(notify, cwd_allowed) if notify then vim.notify_once( string.format("python.nvim: venv not found for '%s' run :PythonVEnvInstall to create one ", found_parent_dir), - vim.log.levels.WARN) + vim.log.levels.WARN + ) end return detect end @@ -212,8 +215,4 @@ function M.detect_venv_dependency_file(notify, cwd_allowed) return nil end -return setmetatable(M, { - __index = function(_, k) - return require("python.venv.detect")[k] - end, -}) +return PythonVENVDetect diff --git a/lua/python/venv/init.lua b/lua/python/venv/init.lua index fabb237..289c207 100644 --- a/lua/python/venv/init.lua +++ b/lua/python/venv/init.lua @@ -3,41 +3,41 @@ ---@field path string ---@field name string -local M = {} +local PythonVENV = {} local current_venv = nil - -local IS_WINDOWS = vim.uv.os_uname().sysname == 'Windows_NT' -local ORIGINAL_PATH = vim.fn.getenv('PATH') +local IS_WINDOWS = vim.uv.os_uname().sysname == "Windows_NT" +local ORIGINAL_PATH = vim.fn.getenv("PATH") local update_PATH = function(path) local sep local dir if IS_WINDOWS then - sep = ';' - dir = 'Scripts' + sep = ";" + dir = "Scripts" else - sep = ':' - dir = 'bin' + sep = ":" + dir = "bin" end - vim.fn.setenv('PATH', vim.fs.joinpath(path, dir .. sep .. ORIGINAL_PATH)) + vim.fn.setenv("PATH", vim.fs.joinpath(path, dir .. sep .. ORIGINAL_PATH)) end +---Set active VEnv, updating venv and PATH variables. ---@param venv VEnv | nil -function M.set_venv_path(venv) +function PythonVENV.set_venv_path(venv) if venv == nil then current_venv = venv return end local config = require("python.config") - if venv.source == 'conda' or venv.source == 'micromamba' then - vim.fn.setenv('CONDA_PREFIX', venv.path) - vim.fn.setenv('CONDA_DEFAULT_ENV', venv.name) - vim.fn.setenv('CONDA_PROMPT_MODIFIER', '(' .. venv.name .. ')') + if venv.source == "conda" or venv.source == "micromamba" then + vim.fn.setenv("CONDA_PREFIX", venv.path) + vim.fn.setenv("CONDA_DEFAULT_ENV", venv.name) + vim.fn.setenv("CONDA_PROMPT_MODIFIER", "(" .. venv.name .. ")") venv.name = ("(conda) %s"):format(venv.name) else - vim.fn.setenv('VIRTUAL_ENV', venv.path) + vim.fn.setenv("VIRTUAL_ENV", venv.path) end current_venv = venv -- TODO: remove old path @@ -47,15 +47,18 @@ function M.set_venv_path(venv) end end +---Get the currently set VEnv object from plugin memory ---@return VEnv | nil -function M.current_venv() +function PythonVENV.current_venv() return current_venv end ---@return VEnv[] local get_venvs_for = function(base_path, source, opts) local options = { only_dirs = true } - if opts then options = vim.tbl_extend('force', options, opts) end + if opts then + options = vim.tbl_extend("force", options, opts) + end local venvs = {} if base_path == nil then @@ -75,70 +78,70 @@ end local get_pixi_base_path = function() local current_dir = vim.fn.getcwd() - local pixi_root = vim.fs.joinpath(current_dir, '.pixi') + local pixi_root = vim.fs.joinpath(current_dir, ".pixi") if vim.fn.filereadable(pixi_root) == 0 then return nil else - return vim.fs.joinpath(pixi_root, 'envs') + return vim.fs.joinpath(pixi_root, "envs") end end local get_conda_base_path = function() - local conda_exe = vim.fn.getenv('CONDA_EXE') + local conda_exe = vim.fn.getenv("CONDA_EXE") if conda_exe == vim.NIL then return nil else - return vim.fs.joinpath(vim.fs.dirname(vim.fs.dirname(conda_exe)), 'envs') + return vim.fs.joinpath(vim.fs.dirname(vim.fs.dirname(conda_exe)), "envs") end end local get_conda_base_env = function() local venvs = {} - local path = os.getenv('CONDA_EXE') + local path = os.getenv("CONDA_EXE") if path then table.insert(venvs, { - name = 'base', - path = vim.fn.fnamemodify(path, ':p:h:h'), - source = 'conda', + name = "base", + path = vim.fn.fnamemodify(path, ":p:h:h"), + source = "conda", }) end return venvs end local get_micromamba_base_path = function() - local micromamba_root_prefix = vim.fn.getenv('MAMBA_ROOT_PREFIX') + local micromamba_root_prefix = vim.fn.getenv("MAMBA_ROOT_PREFIX") if micromamba_root_prefix == vim.NIL then return nil else - return vim.fs.joinpath(micromamba_root_prefix, 'envs') + return vim.fs.joinpath(micromamba_root_prefix, "envs") end end local get_pyenv_base_path = function() - local pyenv_root = vim.fn.getenv('PYENV_ROOT') + local pyenv_root = vim.fn.getenv("PYENV_ROOT") if pyenv_root == vim.NIL then return nil else - return vim.fs.joinpath(pyenv_root, 'versions') + return vim.fs.joinpath(pyenv_root, "versions") end end -M.get_venvs = function(venvs_path) +---Get a list of venvs from multiple supported sources. +---@return table List of venvs found from multiple lists +PythonVENV.get_venvs = function(venvs_path) local venvs = {} - vim.list_extend(venvs, get_venvs_for(venvs_path, 'venv')) - vim.list_extend(venvs, get_venvs_for(get_pixi_base_path(), 'pixi')) - vim.list_extend(venvs, get_venvs_for(get_conda_base_path(), 'conda')) + vim.list_extend(venvs, get_venvs_for(venvs_path, "venv")) + vim.list_extend(venvs, get_venvs_for(get_pixi_base_path(), "pixi")) + vim.list_extend(venvs, get_venvs_for(get_conda_base_path(), "conda")) vim.list_extend(venvs, get_conda_base_env()) - vim.list_extend(venvs, get_venvs_for(get_micromamba_base_path(), 'micromamba')) - vim.list_extend(venvs, get_venvs_for(get_pyenv_base_path(), 'pyenv')) - vim.list_extend(venvs, get_venvs_for(get_pyenv_base_path(), 'pyenv', { only_dirs = false })) + vim.list_extend(venvs, get_venvs_for(get_micromamba_base_path(), "micromamba")) + vim.list_extend(venvs, get_venvs_for(get_pyenv_base_path(), "pyenv")) + vim.list_extend(venvs, get_venvs_for(get_pyenv_base_path(), "pyenv", { only_dirs = false })) return venvs end ---- ---Checks who appears first in PATH. Returns `true` if `first` appears first and `false` otherwise ---- ---@param first string|nil ---@param second string|nil ---@return boolean @@ -154,29 +157,30 @@ local has_high_priority_in_path = function(first, second) local find_first = string.find(ORIGINAL_PATH, first) local find_second = string.find(ORIGINAL_PATH, second) if find_first and find_second then - return find_first < find_second + return find_first < find_second end return false end -M.load_existing_venv = function() +---Load in a venv that is already set in env vars. +PythonVENV.load_existing_venv = function() local venv - local venv_env = vim.fn.getenv('VIRTUAL_ENV') + local venv_env = vim.fn.getenv("VIRTUAL_ENV") if venv_env ~= vim.NIL then venv = { name = vim.fs.basename(venv_env), path = venv_env, - source = 'venv', + source = "venv", } end - local conda_env = vim.fn.getenv('CONDA_DEFAULT_ENV') + local conda_env = vim.fn.getenv("CONDA_DEFAULT_ENV") if conda_env ~= vim.NIL and has_high_priority_in_path(conda_env, venv_env) then venv = { name = ("(conda) %s"):format(conda_env), - path = vim.fn.getenv('CONDA_PREFIX'), - source = 'conda', + path = vim.fn.getenv("CONDA_PREFIX"), + source = "conda", } end @@ -185,29 +189,25 @@ M.load_existing_venv = function() end end -M.pick_venv = function() +---Interactively pick a venv to set as active. +PythonVENV.pick_venv = function() local config = require("python.config") local create = require("python.venv.create") vim.schedule(function() local items = config.get_venvs(config.venvs_path) vim.ui.select(items, { - prompt = 'Select python venv: ', + prompt = "Select python venv: ", format_item = function(item) - return string.format('%s (%s) [%s]', item.name, item.path, item.source) + return string.format("%s (%s) [%s]", item.name, item.path, item.source) end, }, function(choice) if not choice then return end - M.set_venv_path(choice) + PythonVENV.set_venv_path(choice) create.user_set_venv_in_state_confirmation(choice) end) end) end - -return setmetatable(M, { - __index = function(_, k) - return require("python.venv")[k] - end, -}) +return PythonVENV diff --git a/lua/python/venv/interpreters.lua b/lua/python/venv/interpreters.lua index a5b1724..bd89b18 100644 --- a/lua/python/venv/interpreters.lua +++ b/lua/python/venv/interpreters.lua @@ -1,15 +1,15 @@ -local M = {} -local IS_WINDOWS = vim.uv.os_uname().sysname == 'Windows_NT' -local IS_MACOS = vim.uv.os_uname().sysname == 'Darwin' +local PythonVENVInterpreters = {} +local IS_WINDOWS = vim.uv.os_uname().sysname == "Windows_NT" +local IS_MACOS = vim.uv.os_uname().sysname == "Darwin" --- ---@return table found_hatch_pythons list of python interpreters found by hatch -function M.hatch_interpreters() +function PythonVENVInterpreters.hatch_interpreters() if vim.fn.executable("hatch") == 1 then local hatch_python_paths = vim.fn.expand("~/.local/share/hatch/pythons") if vim.fn.isdirectory(hatch_python_paths) then - local found_hatch_pythons = vim.fn.globpath(hatch_python_paths, vim.fs.joinpath("**", "bin", "python3.*"), false, - true) + local found_hatch_pythons = + vim.fn.globpath(hatch_python_paths, vim.fs.joinpath("**", "bin", "python3.*"), false, true) if found_hatch_pythons then return found_hatch_pythons end @@ -20,12 +20,11 @@ end --- ---@return table found_uv_pythons list of python interpreters found by uv -function M.uv_interpreters() +function PythonVENVInterpreters.uv_interpreters() if vim.fn.executable("uv") == 1 then local uv_python_paths = vim.fn.expand("~/.local/share/uv/python") if vim.fn.isdirectory(uv_python_paths) then - local found_uv_pythons = vim.fn.globpath(uv_python_paths, vim.fs.joinpath("**", "bin", "python3.*"), false, - true) + local found_uv_pythons = vim.fn.globpath(uv_python_paths, vim.fs.joinpath("**", "bin", "python3.*"), false, true) if found_uv_pythons then return found_uv_pythons end @@ -35,27 +34,27 @@ function M.uv_interpreters() end ---@return table list of potential python interpreters to use -function M.python_interpreters() +function PythonVENVInterpreters.python_interpreters() -- TODO detect python interpreters from windows if IS_WINDOWS then return { "python3" } end -- TODO for macos we probably need to look in other places other than homebrew - local pythons = vim.fn.globpath("/usr/bin/", 'python3.*', false, true) + local pythons = vim.fn.globpath("/usr/bin/", "python3.*", false, true) if IS_MACOS then - local homebrew_path = vim.fn.globpath("/opt/homebrew/bin/", 'python3.*', false, true) + local homebrew_path = vim.fn.globpath("/opt/homebrew/bin/", "python3.*", false, true) for _, p in pairs(homebrew_path) do table.insert(pythons, 1, p) end end - local found_uv = M.uv_interpreters() + local found_uv = PythonVENVInterpreters.uv_interpreters() if found_uv then for _, p in pairs(found_uv) do table.insert(pythons, 1, p) end end - local found_hatch = M.hatch_interpreters() + local found_hatch = PythonVENVInterpreters.hatch_interpreters() if found_hatch then for _, p in pairs(found_hatch) do table.insert(pythons, 1, p) @@ -71,15 +70,13 @@ function M.python_interpreters() end end if not interpreters then - vim.notify_once("python.nvim: Warning could not detect python interpreters. Defaulting to python3", - vim.log.levels.WARN) + vim.notify_once( + "python.nvim: Warning could not detect python interpreters. Defaulting to python3", + vim.log.levels.WARN + ) interpreters = { "python3" } end return interpreters end -return setmetatable(M, { - __index = function(_, k) - return require("python.venv.interpreters")[k] - end, -}) +return PythonVENVInterpreters diff --git a/scripts/minimal_init.lua b/scripts/minimal_init.lua index 6fa3652..7045eeb 100644 --- a/scripts/minimal_init.lua +++ b/scripts/minimal_init.lua @@ -17,11 +17,12 @@ if #vim.api.nvim_list_uis() == 0 then "deps/LuaSnip", } local runtime_path = vim.fn.join(runtime_dependencies, ",") - vim.cmd('set rtp+=' .. runtime_path) + vim.cmd("set rtp+=" .. runtime_path) -- Set up 'mini.test' - require('luasnip.extras.fmt') - require('luasnip.nodes.absolute_indexer') - require('nvim-treesitter.locals') - require('mini.test').setup() + require("luasnip.extras.fmt") + require("luasnip.nodes.absolute_indexer") + require("nvim-treesitter.locals") + require("mini.test").setup() + require("mini.doc").setup() end diff --git a/tests/_test_template.lua b/tests/_test_template.lua index 2e479cd..03d508b 100644 --- a/tests/_test_template.lua +++ b/tests/_test_template.lua @@ -12,7 +12,7 @@ local T = new_set({ -- This will be executed before every (even nested) case pre_case = function() -- Restart child process with custom 'init.lua' script - child.restart({ '-u', 'scripts/minimal_init.lua' }) + child.restart({ "-u", "scripts/minimal_init.lua" }) -- Load tested plugin child.lua([[require('python').setup()]]) end, diff --git a/tests/test_config.lua b/tests/test_config.lua index 860c974..1503781 100644 --- a/tests/test_config.lua +++ b/tests/test_config.lua @@ -12,7 +12,7 @@ local T = new_set({ -- This will be executed before every (even nested) case pre_case = function() -- Restart child process with custom 'init.lua' script - child.restart({ '-u', 'scripts/minimal_init.lua' }) + child.restart({ "-u", "scripts/minimal_init.lua" }) -- Load tested plugin child.lua([[config = require('python.config')]]) end, @@ -21,21 +21,21 @@ local T = new_set({ }, }) -T['setup'] = MiniTest.new_set() +T["setup"] = MiniTest.new_set() -T['setup']['init'] = function() - child.lua('config.setup({})') - eq(child.lua('return config.python_lua_snippets'), false) +T["setup"]["init"] = function() + child.lua("config.setup({})") + eq(child.lua("return config.python_lua_snippets"), false) end -T['setup']['override'] = function() - child.lua('config.setup({ python_lua_snippets = true })') - eq(child.lua('return config.python_lua_snippets'), true) +T["setup"]["override"] = function() + child.lua("config.setup({ python_lua_snippets = true })") + eq(child.lua("return config.python_lua_snippets"), true) end -T['setup']['not_found'] = function() - expect.error(function() - child.lua('config.setup({ ui = { popup = {foobar = true}} })') +T["setup"]["not_found"] = function() + expect.error(function() + child.lua("config.setup({ ui = { popup = {foobar = true}} })") end, ".*user inputted config key: foobar is not found.*") end diff --git a/tests/test_detect.lua b/tests/test_detect.lua index a96ab53..e6bddcb 100644 --- a/tests/test_detect.lua +++ b/tests/test_detect.lua @@ -12,7 +12,7 @@ local T = new_set({ -- This will be executed before every (even nested) case pre_case = function() -- Restart child process with custom 'init.lua' script - child.restart({ '-u', 'scripts/minimal_init.lua' }) + child.restart({ "-u", "scripts/minimal_init.lua" }) -- Load tested plugin end, -- This will be executed one after all tests from this set are finished @@ -21,55 +21,55 @@ local T = new_set({ }) --- expect to detect wanted methods per project. -T['detect'] = MiniTest.new_set({ +T["detect"] = MiniTest.new_set({ parametrize = { { { project_path = "examples/python_projects/pip_requirements", install_file = "requirements.txt", - dependency_method = "requirements.txt" - } + dependency_method = "requirements.txt", + }, }, { { project_path = "examples/python_projects/pip_pyproject", install_file = "pyproject.toml", - dependency_method = "pyproject.toml" - } + dependency_method = "pyproject.toml", + }, }, { { project_path = "examples/python_projects/uv", install_file = "uv.lock", - dependency_method = "uv.lock" - } + dependency_method = "uv.lock", + }, }, { { project_path = "examples/python_projects/poetry", install_file = "poetry.lock", - dependency_method = "poetry.lock" - } + dependency_method = "poetry.lock", + }, }, { { project_path = "examples/python_projects/uv_script", install_file = "uv-script.py", - dependency_method = "/// script" - } + dependency_method = "/// script", + }, }, { { project_path = "examples/python_projects/no_dep", install_file = "test.py", dependency_method = "", - expect_nil = true - } + expect_nil = true, + }, }, - } + }, }) -T['detect']['methods'] = function(args) +T["detect"]["methods"] = function(args) require("python.venv.detect") local project_path = args.project_path child.cmd("cd " .. project_path) @@ -82,9 +82,9 @@ T['detect']['methods'] = function(args) -- Weird vim.fs.joinpath in test is meant to have vim output the same pathing -- format that happens in windows from joinpath in detect_venv_dependency_file - if vim.uv.os_uname().sysname == 'Windows_NT' then - abspath = vim.fs.joinpath(vim.fs.dirname(child.lua("return vim.fn.expand('%:p:h')")), - vim.fs.basename(args.project_path)) + if vim.uv.os_uname().sysname == "Windows_NT" then + abspath = + vim.fs.joinpath(vim.fs.dirname(child.lua("return vim.fn.expand('%:p:h')")), vim.fs.basename(args.project_path)) end ---@type DetectVEnv | vim.NIL @@ -93,8 +93,8 @@ T['detect']['methods'] = function(args) venv = { install_file = vim.fs.joinpath(abspath, args.install_file), install_method = args.dependency_method, - source = "venv" - } + source = "venv", + }, }) if args.expect_nil then ex = vim.NIL diff --git a/tests/test_interpreters.lua b/tests/test_interpreters.lua index a62b105..bc8ed04 100644 --- a/tests/test_interpreters.lua +++ b/tests/test_interpreters.lua @@ -1,4 +1,3 @@ - -- Define helper aliases local new_set = MiniTest.new_set local expect, eq = MiniTest.expect, MiniTest.expect.equality @@ -13,7 +12,7 @@ local T = new_set({ -- This will be executed before every (even nested) case pre_case = function() -- Restart child process with custom 'init.lua' script - child.restart({ '-u', 'scripts/minimal_init.lua' }) + child.restart({ "-u", "scripts/minimal_init.lua" }) -- Load tested plugin child.lua([[require('python').setup()]]) end, @@ -22,9 +21,9 @@ local T = new_set({ }, }) -T['interpreters'] = MiniTest.new_set() +T["interpreters"] = MiniTest.new_set() -T['interpreters']['python'] = function() +T["interpreters"]["python"] = function() child.lua("inter = require('python.venv.interpreters')") local pythons = child.lua("return inter.python_interpreters()") assert(#pythons > 0) diff --git a/tests/test_keymap.lua b/tests/test_keymap.lua index 0372df3..dd6577f 100644 --- a/tests/test_keymap.lua +++ b/tests/test_keymap.lua @@ -12,7 +12,7 @@ local T = new_set({ -- This will be executed before every (even nested) case pre_case = function() -- Restart child process with custom 'init.lua' script - child.restart({ '-u', 'scripts/minimal_init.lua' }) + child.restart({ "-u", "scripts/minimal_init.lua" }) -- Load tested plugin child.lua([[require('python').setup()]]) end, @@ -22,7 +22,7 @@ local T = new_set({ }) -- Keymaps can be loaded -T['keymaps'] = function() +T["keymaps"] = function() child.cmd("cd examples/python_projects/uv") child.cmd("e main.py") local keymap = child.cmd_capture("map pi") diff --git a/tests/test_luasnip.lua b/tests/test_luasnip.lua index b18f534..bccc4a6 100644 --- a/tests/test_luasnip.lua +++ b/tests/test_luasnip.lua @@ -12,7 +12,7 @@ local T = new_set({ -- This will be executed before every (even nested) case pre_case = function() -- Restart child process with custom 'init.lua' script - child.restart({ '-u', 'scripts/minimal_init.lua' }) + child.restart({ "-u", "scripts/minimal_init.lua" }) -- Load tested plugin child.lua([[require('python').setup({ python_lua_snippets = true })]]) end, @@ -21,7 +21,7 @@ local T = new_set({ }, }) -T['snips will load'] = function() +T["snips will load"] = function() child.lua([[M = require('python.snip')]]) child.lua([[M.load_snippets()]]) eq(child.lua([[return vim.g._python_nvim_snippets_loaded]]), true) diff --git a/tests/test_uv.lua b/tests/test_uv.lua index d012124..e332834 100644 --- a/tests/test_uv.lua +++ b/tests/test_uv.lua @@ -12,7 +12,7 @@ local T = new_set({ -- This will be executed before every (even nested) case pre_case = function() -- Restart child process with custom 'init.lua' script - child.restart({ '-u', 'scripts/minimal_init.lua' }) + child.restart({ "-u", "scripts/minimal_init.lua" }) -- Load tested plugin child.lua([[require('python').setup()]]) end, @@ -20,14 +20,13 @@ local T = new_set({ post_once = child.stop, }, }) -T['uv'] = MiniTest.new_set({ +T["uv"] = MiniTest.new_set({ hooks = { - pre_case = function() - end - } + pre_case = function() end, + }, }) -T['uv']['command'] = function() +T["uv"]["command"] = function() child.cmd("cd examples/python_projects/uv") child.cmd("e main.py") child.cmd([[!rm -rf .venv]]) diff --git a/tests/test_venv_create.lua b/tests/test_venv_create.lua index ca91eb2..16944ef 100644 --- a/tests/test_venv_create.lua +++ b/tests/test_venv_create.lua @@ -12,7 +12,7 @@ local T = new_set({ -- This will be executed before every (even nested) case pre_case = function() -- Restart child process with custom 'init.lua' script - child.restart({ '-u', 'scripts/minimal_init.lua' }) + child.restart({ "-u", "scripts/minimal_init.lua" }) -- Load tested plugin child.lua([[require('python').setup({})]]) end, @@ -21,11 +21,10 @@ local T = new_set({ }, }) - -T['create'] = MiniTest.new_set({ n_retry = 3 }) +T["create"] = MiniTest.new_set({ n_retry = 3 }) -- NOTE: requires python3 and python3-venv installed on system -T['create']['venv'] = function() +T["create"]["venv"] = function() child.lua("create = require('python.venv.create')") child.cmd("cd examples/python_projects/uv") child.cmd("!rm -rf .venv") @@ -36,7 +35,7 @@ T['create']['venv'] = function() end -- NOTE: requires uv installed on system -T['create']['uv_sync'] = function() +T["create"]["uv_sync"] = function() child.lua("create = require('python.venv.create')") child.cmd("cd examples/python_projects/uv") child.cmd("!rm -rf .venv") @@ -44,8 +43,8 @@ T['create']['uv_sync'] = function() child.lua("create.create_venv_with_python('.venv', 'python3')") child.lua("create.uv_sync('uv.lock', '.venv', function()end, false)") - local dep_path = vim.fn.system( - [[examples/python_projects/uv/.venv/bin/python -c 'import sys; print(sys.path[-1], end="")']]) + local dep_path = + vim.fn.system([[examples/python_projects/uv/.venv/bin/python -c 'import sys; print(sys.path[-1], end="")']]) assert(string.find(dep_path, "python_projects", 1, true))