Skip to content

feature request: Support calling formatters with any callable binary path such as a container command #669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
1 task done
nickjj opened this issue Mar 12, 2025 · 8 comments
Labels
enhancement New feature or request

Comments

@nickjj
Copy link

nickjj commented Mar 12, 2025

Did you check existing requests?

  • I have searched the existing issues

Describe the feature

I run all of my applications in Docker. That means tools like Black, Flake8, isort, Rubocop and friends are all inside of a container. They are not callable directly on my host machine where Neovim is installed.

I'd love to be able to do something like this (I'm using LazyVim):

return {
  {
    "stevearc/conform.nvim",
    opts = {
      formatters_by_ft = {
        python = { "black" },
      },
      formatters = {
        black = {
          command = "docker compose exec web black",
        },
      },
    },
  },
}

Which would run Black in a container and pickup the custom pyproject.toml file in the root of the project with config settings for Black. If the container isn't running then it would silently fail.

When I use the above configuration :ConformInfo says black unavailable: command not found. Here is the full details:

Log file: /home/nick/.local/state/nvim/conform.log
           - unexpected token `completeopt` (19:16 to 19:27), expected `}` after last field
           - unexpected token `,` (19:92 to 19:93), trailing commas are not allowed
           - unexpected token `}` (19:16 to 19:91), expected `end` to close function body block
           - unexpected token `opts` (23:7 to 23:11), expected `}` after last field
           - unexpected token `opts` (23:7 to 23:11), unexpected statement after last statement
           - unexpected token `end` (58:5 to 58:8), unexpected token, this needs to be a statement

Formatters for this buffer:
black unavailable: Command not found

As a sanity check, I did install Black locally just to confirm my config is active and it does work, although it didn't pick up the pyproject.toml file in the project because it formatted my code with what I think are default Black settings but that's a separate concern.

On the command line I would run docker compose exec web black . to run the formatter in an already running container, such as a web application. It works great and I have used this pattern for years for all types of formatters but I'd love to be able integrate these formatters in Neovim.

What is the significance of this feature?

Cannot use this plugin without it.

@nickjj nickjj added the enhancement New feature or request label Mar 12, 2025
@stevearc
Copy link
Owner

You are passing in the string "docker compose exec web black" as the command, which means that we will look for an executable named docker compose exec web black, which obviously doesn't exist. You can use command = "docker" and pass the rest of the arguments in as args.

I've made the ConformInfo more explicit to help make this issue more obvious

@nickjj
Copy link
Author

nickjj commented Mar 19, 2025

Thanks, but that also fails.

Here's what I put in:

return {
  {
    "stevearc/conform.nvim",
    opts = {
      formatters_by_ft = {
        python = { "black" },
      },
      formatters = {
        black = {
          command = "docker",
          args = { "compose", "exec", "web", "black" },
        },
      },
    },
  },
}

Here's the error:

Log file: /home/nick/.local/state/nvim/conform.log
           - unexpected token `{` (11:17 to 11:18), expected `}` after last field
           - unexpected token `{` (11:17 to 11:18), expected `}` after last field
           - unexpected token `{` (11:17 to 11:18), expected `}` after last field
           - unexpected token `{` (11:17 to 11:18), expected `}` after last field
           - unexpected token `{` (11:17 to 11:18), expected `}` after last field
           - unexpected token `{` (11:17 to 11:18), unexpected token, this needs to be a statement
          
          2025-03-19 18:11:48[ERROR] Formatter 'black' error: service "web" is not running
          
          2025-03-19 18:12:20[ERROR] Formatter 'black' error: Usage: black [OPTIONS] SRC ...
          
          One of 'SRC' or 'code' is required.
          
          2025-03-19 18:12:23[ERROR] Formatter 'black' error: Usage: black [OPTIONS] SRC ...
          
          One of 'SRC' or 'code' is required.
          
          2025-03-19 18:12:29[ERROR] Formatter 'black' error: Usage: black [OPTIONS] SRC ...
          
          One of 'SRC' or 'code' is required.
          
          2025-03-19 18:13:51[ERROR] Formatter 'black' error: Usage: black [OPTIONS] SRC ...
          
          One of 'SRC' or 'code' is required.

It says it's not running but it is running. Here's what happens when I run the command from my terminal:

$ docker compose exec web black .
reformatted /app/hello/app.py

All done!
1 file reformatted, 20 files left unchanged.

As for the syntax I used, I took it from this example in the docs:

require("conform").formatters.shfmt = {
  inherit = false,
  command = "shfmt",
  args = { "-i", "2", "-filename", "$FILENAME" },
}

@nickjj
Copy link
Author

nickjj commented Mar 19, 2025

Also if I use this:

return {
  {
    "stevearc/conform.nvim",
    opts = {
      formatters_by_ft = {
        python = { "black" },
      },
      formatters = {
        black = {
          command = "docker",
          args = { "compose", "exec", "web", "black", "$FILENAME" },
        },
      },
    },
  },
}

Which throws:

Error: Invalid value for 'SRC ...': Path '/home/nick/src/github/docker-flask-example/hello/app.py' does not exist.

Which makes sense because the absolute path doesn't exist in Docker. Is there a way to pass in the relative path, such as hello/app.py in the above example, assuming I opened Neovim from ~/src/github/docker-flask-example?

@stevearc
Copy link
Owner

Yes, you will note that the built-in black formatter passes some args

args = {
"--stdin-filename",
"$FILENAME",
"--quiet",
"-",
},

You will need to replicate those args to get the formatter to work properly.

You can use $RELATIVE_FILEPATH instead of $FILENAME to use the relative path.

@nickjj
Copy link
Author

nickjj commented Mar 19, 2025

Thanks, that is working. I didn't find $RELATIVE_FILEPATH in the docs btw, but now that I see it exists, #349 has a callout to it.

The last piece of the puzzle (if you don't mind), would be having Black inherit the root directory's pyproject.toml file. When running Black from the command line I didn't need to pass any explicit flags to black for it to work when I called black with black . from the root of the project but now I guess Black is getting called from arbitrary paths based on the file's location.

Black's documentation says it should walk back directories until it finds one. In this case the file is available in the container but it never finds it when called this way.

@nickjj
Copy link
Author

nickjj commented Mar 19, 2025

If you're open for it, I'd be happy to contribute a PR to document $RELATIVE_FILEPATH.

Maybe in the readme file to replace:

      -- A list of strings, or a function that returns a list of strings
      -- Return a single string instead of a list to run the command in a shell
      args = { "--stdin-from-filename", "$FILENAME" },

With:

      -- A list of strings, or a function that returns a list of strings
      -- Return a single string instead of a list to run the command in a shell
      -- $FILENAME contains the absolute path, $RELATIVE_FILENAME contains the relative path
      args = { "--stdin-from-filename", "$FILENAME" },

The way I scanned the docs was by CTRL+f'ing it for "relative" and coming up empty.

@stevearc
Copy link
Owner

The last piece of the puzzle (if you don't mind), would be having Black inherit the root directory's pyproject.toml file.

That would be the cwd

cwd = util.root_file({
-- https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file
"pyproject.toml",
}),

This is probably calculating an absolute path, so you'll need to figure out what path makes sense to pass to the docker command.

I hadn't added docs about the magic strings thus far because I didn't really have a good place to put them, and most people don't need to interact with them. I think I can just stick them in a section in the README.

@nickjj
Copy link
Author

nickjj commented Mar 20, 2025

I believe pyproject.toml is working with the default cwd. I have mine set for 79 lines and I never knew this, but it doesn't seem to apply to docstring comments. Even calling it with the CLI failed to format docstring comments to 79 lines but it did format code lines to 79 which is different than the default of 88.

Yesterday when I reported that, I had only tested it with a docstring comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants