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

Allow banning of certain modules and certain module members #1422

Closed
not-my-profile opened this issue Dec 28, 2022 · 15 comments · Fixed by #1436
Closed

Allow banning of certain modules and certain module members #1422

not-my-profile opened this issue Dec 28, 2022 · 15 comments · Fixed by #1436
Labels
rule Implementing or modifying a lint rule

Comments

@not-my-profile
Copy link
Contributor

Sometimes you want to assert that certain modules are never imported. Apparently Pylint and Pyflake have plugins for that (pylint-restricted-imports and flake8-tidy-imports respectively).

I think the disallowed modules / module members could be defined in a new tool.ruff.banned-api section in pyproject.toml. So you could for example have:

[tool.ruff.banned-api]
argparse.msg = "Use typed_argparse instead."
"decimal.Decimal".msg = "Use ints and floats only."
"typing.TypedDict".msg = """Use typing_extensions.TypedDict instead.

typing.TypedDict does not support typing.Generic for Python < 3.11,
so we sometimes have to use typing_extensions.TypedDict instead.
If we sometimes use typing_extensions.TypedDict, we however always
want to use it because:

class Foo(typing.TypedDict): ...
class Bar(typing_extensions.TypedDict): ...
class Baz(Foo, Bar): ...

results in a runtime type error:

> TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
"""

Note that banning module members is a bit challenging because we also have to account for attribute access e.g.

import example
example.evil_function()

# or

import example as other
other.evil_function()

I think it is quite clear that it is impossible for such a lint to prevent all the ways an API can be accessed, for example banned modules could still be accessed via importlib or eval and banning eval is a hopeless endeavor because there are countless ways of accessing it e.g. globals()['__builtins__'].eval or even dataclasses.builtins.eval. Completely preventing attribute access is even more difficult because you'd have to understand data flows (e.g. (lambda x: x.evil_function())(example)).

So I just think the documentation of the lint should clarify that it's meant to prevent accidental usage of the API and can be easily circumvented.

Aside from such false negatives the attribute access check would probably also result in false positives e.g.

import example, other
example = other
example.evil_function()

# or

def foo(example):
    example.evil_function()

So I think the error message for detected attribute access should say something like "it looks like you used a banned API" instead of using assertive language that might confuse users.

Lastly in cases where the fix is just replacing one import for another (e.g. changing typing.TypedDict to typing_extensions.TypedDict) it would be nice if ruff could provide automatic fixing via --fix. This is also the reason why I suggested the .msg in the previous config example because then we could additionally specify e.g:

"typing.TypedDict".fix-to = "typing_extensions.TypedDict"

I have not contributed to ruff yet, but if the people here like the suggested lint, I could look into implementing it :)

@charliermarsh
Copy link
Member

charliermarsh commented Dec 28, 2022

Thanks for writing this up!

I think the most straightforward path would be to model this after flake8-tidy-imports, and use the error code TID251 (we already supported I252 from flake8-tidy-imports, but it's one of the few codes we renamed to avoid conflicts).

We can start by using the same API as flake8-tidy-imports. (Replacement autofix is a bit difficult right now, because we need a way to "inject" an import into a file -- see #835. That is, if you change typing to typing_extensions, you may have to add an import for it.)

@charliermarsh charliermarsh added the rule Implementing or modifying a lint rule label Dec 28, 2022
@charliermarsh
Copy link
Member

It looks like flake8-tidy-imports only flags imports and not arbitrary member accesses, so I'd also be fine starting with that behavior (which makes the check somewhat trivial to implement).

https://github.com/adamchainz/flake8-tidy-imports/blob/main/src/flake8_tidy_imports/__init__.py#L184

I know that doesn't solve your typing.TypedDict problem since it's common to import typing, but we could enforce that as a separate check (banned member access).

@charliermarsh
Copy link
Member

Let me know if you're interested in implementing, and I could write up some more specific instructions.

@not-my-profile
Copy link
Contributor Author

not-my-profile commented Dec 28, 2022

we need a way to "inject" an import into a file

Good point, should we open a separate issue for that?

It looks like flake8-tidy-imports only flags imports and not arbitrary member accesses

According to its README flake8-tidy-imports does support banning members

Note that despite the name, you can ban imported objects too, since the syntax is the same.

but only when they're imported as e.g. import TypedDict from typing and not when accessed via the module name (see adamchainz/flake8-tidy-imports#63)

we could enforce that as a separate check (banned member access)

I am not sure how much sense it makes to have two separate checks for this because from foo import bar is ambiguous ... just from that we cannot know if bar is the foo.bar module or if bar is an object defined in the foo module (which is probably also the reason why flake8-tidy-imports supports this ... it's not intentional, it's a side effect).

With this side-effect in mind I think it makes sense to properly check for attribute access as well, at which point the check is more than I251, ... I am not sure if using an existing identifier implies that the check by ruff is exactly the same?

Yes I'm interested in implementing this :)

@charliermarsh
Copy link
Member

Good point, should we open a separate issue for that?

It's filed here: #835. (I linked the wrong issue above, edited.)

According to its README flake8-tidy-imports does support banning members

Yeah! I saw that too. What I meant was that it doesn't detect module accesses as in the issue you linked. I was mostly suggesting that should be a separate check so-as to maintain compatibility with flake8-tidy-imports, but if that's the intent of the check, and they just consider it a deficiency, then it's fine to do it all as one check code as you've proposed.

@charliermarsh
Copy link
Member

Yes I'm interested in implementing this :)

Great! There are some instructions on adding check codes in general in the contributing guide.

For this code, you'd then want to do two things above those initial steps:

  1. Add the banned-modules option to src/flake8_tidy_imports/settings.rs, probably as a hash map from module name to message.
  2. In checkers/ast.rs, around here, add a call out to a new check in src/flake8_tidy_imports/checks.rs (and similarly for StmtKind::ImportFrom) with the appropriate logic.

@charliermarsh
Copy link
Member

I'd kind of prefer to avoid implementing all of the wildcarding that flake8-tidy-imports supports. It complicates the API and the implementation, and makes it more challenging to do these checks efficiently across the codebase (which will be required if we support flagging member access -- we'll have to perform these checks a lot).

I also can't really find any usages of the wildcarding in code search.

So, IMO, let's skip that for now.

@charliermarsh
Copy link
Member

(You can just ping here as you have questions, I'm happy to help.)

@not-my-profile
Copy link
Contributor Author

Add the banned-modules option

I think naming it banned-api makes more sense since it can also be used to ban module members.

@not-my-profile
Copy link
Contributor Author

Thanks for being so responsive and thoughtful! The CONTRIBUTING.md and cargo +nightly dev generate-all are really quite nice. I did run into some small issues when following CONTRIBUTING.md and just opened #1466 to address these (and also #1465 with a followup fix for something we both missed).

@charliermarsh
Copy link
Member

Thank you for your contribution, and for bearing with my feedback! It's hugely appreciated! I'm really glad to have you as a contributor :)

@sbdchd
Copy link
Contributor

sbdchd commented Jan 4, 2023

I use a the wildcard syntax in a project, so the config ends up being:

[flake8]
banned-modules =
  httpx.* = Use kodiak.http
ban-relative-imports = true

My use case is preventing people from importing httpx at all and instead using the project's wrapper around the library

Maybe there is a way to achieve this without the *?

example uses (in the wrapper module) that flake8-tidy-import lints:

from httpx import (  # noqa: I251
    AsyncClient,
    HTTPError,
    HTTPStatusError,
    Request,
    Response,
)
from httpx._config import DEFAULT_TIMEOUT_CONFIG  # noqa: I251
from httpx._types import TimeoutTypes  # noqa: I251

@charliermarsh
Copy link
Member

charliermarsh commented Jan 4, 2023

Would banned-modules = httpx achieve the same thing? (Sorry, being lazy, but what's the difference between the semantics in those two cases?)

@sbdchd
Copy link
Contributor

sbdchd commented Jan 4, 2023

Oh yeah that's what I should have been using, didn't know about that!

I'll make a separate issue because I think the following config isn't working:

[tool.ruff.flake8-tidy-imports]
# Disallow all relative imports.
ban-relative-imports = "all"
[tool.ruff.flake8-tidy-imports.banned-api]
"httpx".msg = "Use kodiak.http"

@charliermarsh
Copy link
Member

Go for it, I haven’t used that plug-in a ton yet myself but happy to take a look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rule Implementing or modifying a lint rule
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants