Skip to content
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

Expose Ruff's public API as a Python library #659

Open
charliermarsh opened this issue Nov 8, 2022 · 38 comments
Open

Expose Ruff's public API as a Python library #659

charliermarsh opened this issue Nov 8, 2022 · 38 comments
Labels
core Related to core functionality help wanted Contributions especially welcome

Comments

@charliermarsh
Copy link
Member

See: #593

@facundobatista
Copy link

This would mean that we could do something like the following, right?

import ruff
errors = ruff.check_files(list_of_paths)
...

Thanks!

@charliermarsh
Copy link
Member Author

Yup, that's right!

@charliermarsh
Copy link
Member Author

@messense - I wasn't certain on this last time -- if we bundle a Python API with Ruff, will we need to build separate wheels for every Python version?

@messense
Copy link
Contributor

If you can use abi3 features, one wheel per platform, otherwise you need to build separate wheels for every Python version.

@charliermarsh
Copy link
Member Author

Awesome thank you. I think we should be able to do that, so maybe this will be really straightforward.

@provinzkraut
Copy link

Hello there, do you happen to have a rough timeline for when (if?) this is going to happen? I'm looking to integrate ruff into a tool I'm developing, which would require an API of some sort. It would be very helpful to know if this is something I can wait on, or look for another solution / workaround!

@charliermarsh charliermarsh added this to the Release 0.1.0 milestone Dec 8, 2022
@charliermarsh
Copy link
Member Author

@provinzkraut - It's definitely going to happen! I could probably ship it within the next week or so. I'd just been punting on it until I had more people asking for it.

Could I hear a bit more about your use-case, if you don't mind sharing?

@provinzkraut
Copy link

@charliermarsh That's good to hear!

Could I hear a bit more about your use-case, if you don't mind sharing?

Sure. I'm working on a markdown extension to automatically generate pymdown tabs for different Python versions from a source version, i.e. generate 3.7, 3.8, 3.10 tabs from a 3.7 source (repo).

Currently I'm using pyupgrade to generate the versions and autoflake to clean imports that have become superfluous. Especially autoflake is quite slow, making up a majority of the extensions runtime. Since ruff is way faster at this, I'd like to use it (also one less dependency). I fiddled around with using the CLI version, but that's messy and a performance degradation.

@charliermarsh
Copy link
Member Author

@provinzkraut - Ok, cool. Let me see what I can do. I don't know if you're comfortable reading Rust, but would the current Rust public API suit your use-case, were it callable from Python with Python objects etc.?

@charliermarsh
Copy link
Member Author

In short: it takes a file path (to find the pyproject.toml), the raw Python source code, and an autofix setting, and returns a list of checks (which themselves include the raw fixes / patches).

I'm guessing that for your use-case, what you actually want is a function that takes source code (plus settings, to enable a list of checks) and returns fixed source code?

@provinzkraut
Copy link

but would the current Rust public API suit your use-case, were it callable from Python with Python objects etc.?

I looked at this yesterday because I though that maybe it could be as simple as adding a tiny wrapper around the rust lib myself, but it seems to be a bit more involved. The current API doesn't really lend itself that well to my usecase.

I'm guessing that for your use-case, what you actually want is a function that takes source code (plus settings, to enable a list of checks) and returns fixed source code?

That would be ideal, yes. Dealing with a list of checks and extracting what I need from it also wouldn't be that big of an issue, but passing in configuration directly and omitting the config file is crucial, both for the needed configurability (I need to run the fixers with varying configuration for every invocation) and performance (I'm running the fixers many times on small snippets, which means the overhead of looking for and parsing a pyproject.toml every time adds up).

@squiddy
Copy link
Contributor

squiddy commented Dec 27, 2022

I'm working on this now.

squiddy added a commit to squiddy/ruff that referenced this issue Dec 27, 2022
squiddy added a commit to squiddy/ruff that referenced this issue Dec 27, 2022
squiddy added a commit to squiddy/ruff that referenced this issue Dec 27, 2022
squiddy added a commit to squiddy/ruff that referenced this issue Dec 28, 2022
squiddy added a commit to squiddy/ruff that referenced this issue Dec 28, 2022
squiddy added a commit to squiddy/ruff that referenced this issue Dec 28, 2022
squiddy added a commit to squiddy/ruff that referenced this issue Dec 28, 2022
squiddy added a commit to squiddy/ruff that referenced this issue Dec 29, 2022
@charliermarsh charliermarsh added core Related to core functionality and removed enhancement labels Dec 31, 2022
@phillipuniverse
Copy link
Contributor

phillipuniverse commented Jan 17, 2023

I had a need to execute Ruff as an Alembic post write hook. I came up with a very hamfisted approach that I found from the distributed __main__.py

image

alembic.ini:

[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts.  See the documentation for further
# detail and examples

# format using "black" - use the console_scripts runner, against the "black" entrypoint
hooks = ruff, black

ruff.type = ruff

black.type = console_scripts
black.entrypoint = black

and then Alembic's env.py:

import os
import sysconfig
from alembic.script import write_hooks

@write_hooks.register("ruff")
def run_ruff(filename, options):
    ruff = os.path.join(sysconfig.get_path("scripts"), "ruff")
    os.spawnv(os.P_WAIT, ruff, [ruff, filename, "--fix", "--exit-zero"])

@charliermarsh
Copy link
Member Author

👍 Yup that should be safe to do! (The downside being that you have to go through the CLI rather than calling a function directly. Hoping to enable that soon but not working on it right now.)

@charliermarsh charliermarsh removed this from the Release 0.1.0 milestone Feb 3, 2023
@pawamoy
Copy link

pawamoy commented Feb 22, 2024

Adding a data point: in mkdocstrings-python we format function signatures with Black if it is installed. We would like to support Ruff to, but spawning a subprocess for each signature is very costly, so we would greatly appreciate a Python binding that doesn't use subprocesses 🙂 A wrapper that hides the subprocess calls sounds nice, but won't be enough for our use-case.

@MichaReiser
Copy link
Member

@pawamoy that sounds neat. We plan to integrate our LSP into ruff (implemented in Rust). I know, it's not as convenient as a Python API but it would allow you to format files without spawning a process for every signature (although it might still be very costly because it requires multiple LSP calls to format a single code snipped)

@pawamoy
Copy link

pawamoy commented Feb 22, 2024

By calls do you mean network calls? Or could we somehow spawn the LSP server locally (like a daemon)?

@MichaReiser
Copy link
Member

You would spawn the LSP like a daemon and communicate over stdin/stdout.

@pawamoy
Copy link

pawamoy commented Feb 22, 2024

Ah, interesting. Then yeah, that's already much better than subprocesses 🙂 Thanks for the info!

@amyreese
Copy link

Adding a data point: in mkdocstrings-python we format function signatures with Black if it is installed. We would like to support Ruff to, but spawning a subprocess for each signature is very costly, so we would greatly appreciate a Python binding that doesn't use subprocesses 🙂 A wrapper that hides the subprocess calls sounds nice, but won't be enough for our use-case.

I put together an experimental package that uses PyO3 to wrap the Ruff formatter in a Python API that doesn't require any subprocesses. I'd still consider it alpha at best (there's only one callable function), but maybe it could be helpful to others as well?

https://github.com/amyreese/ruff-api

@pawamoy
Copy link

pawamoy commented Feb 22, 2024

Amazing, thanks for sharing! I'll check it out :)

@Zac-HD
Copy link

Zac-HD commented Feb 22, 2024

@charliermarsh just checking in - is there any way to configure the isort settings in --isolated mode, or do I just have to wait? No worries if so, I'm just looking forward to replacing black too...

@AlexWaygood
Copy link
Member

AlexWaygood commented Feb 22, 2024

@charliermarsh just checking in - is there any way to configure the isort settings in --isolated mode, or do I just have to wait? No worries if so, I'm just looking forward to replacing black too...

@Zac-HD, yes, there is! We recently extended the --config flag so that arbitrary configuration options can be overridden via the command line using "inline TOML": https://docs.astral.sh/ruff/configuration/#the-config-cli-flag. So to override the isort extra-standard-library setting in --isolated mode (for example), you'd do something like ruff check path/to/file.py --config "lint.isort.extra-standard-library = ['path']".

@mbelak-dtml
Copy link

Adding another data point:
In edvart, we are currently using isort to sort imports in Python code which is being dynamically.
With a Python API, we could fully switch to ruff. For now, we are using ruff to format the source code, but keeping isort to format the generated code.

@jankatins
Copy link
Contributor

Another data point: it would make it easier to replace programmatic calls to black, like in mdsformat-black: https://github.com/hukkin/mdformat-black/blob/master/mdformat_black/__init__.py

def format_python(unformatted: str, _info_str: str) -> str:
    return black.format_str(unformatted, mode=black.Mode())

@adamchainz
Copy link

I’d want to use an API like black.format_str over in blacken-docs, where Ruff support is tracked in this issue: adamchainz/blacken-docs#352

@MichaReiser
Copy link
Member

MichaReiser commented Jul 17, 2024

Not the most elegant solution and I haven't tried it myself, but it should soon be possible to call the ruff WASM API from Python:

Considering that we have a WASM API now, I'm open to reconsidering a PyO3 API. Let me discuss this internally.

Note: The API would not fall under any semver guarantees. We expect a major breaking change once we introduce multifile analysis. Practically, the API hasn't changed in months.

@MichaReiser
Copy link
Member

I would be open to expose a Ruff Pyo3 API:

  • Shipped as a separate python package (ruff-api?)
  • Has the same or a very similar API as ruff_wasm
  • The API itself isn't under semver. Breaking API changes are possible, even in patch releases (we don't feel comfortable committing to that yet). Otherwise, the same versioning policy as for Ruff applies
  • The package should be released automatically as part of our release workflow.

I'm happy to support if anyone's interested in contributing the API to ruff.

@maxschulz-COL
Copy link

Hey, just to add another data-point: we at Vizro would also love to be able to invoke ruff from within python without subprocess. Something like black.format_str indeed :)

@n8henrie
Copy link

Same here -- would love to replace black.format_str in automatically formatting jupyter cells with https://github.com/n8henrie/jupyter-black/ !

@amyreese
Copy link

@maxschulz-COL @n8henrie @adamchainz I would like to remind folks that https://github.com/amyreese/ruff-api has a working, simple API wrapping both the formatter and import sorter from Ruff, just a pip install ruff-api away. We have been using it to successfully migrate from Black in our monorepo while still maintaining our existing integrations/tooling written in Python. :)

@analog-cbarber
Copy link

That looks great, but it is also documented as "highly experimental", so people maybe reluctant to add that to their tool chains. Why don't you contribute that to the ruff project?

@n8henrie
Copy link

Agreed -- it would be great to have this under ruff's umbrella!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Related to core functionality help wanted Contributions especially welcome
Projects
None yet
Development

No branches or pull requests