项目作者: snok

项目描述 :
Flake8 plugin for managing type-checking imports & forward references
高级语言: Python
项目地址: git://github.com/snok/flake8-type-checking.git
创建时间: 2021-02-23T09:08:25Z
项目社区:https://github.com/snok/flake8-type-checking

开源协议:BSD 3-Clause "New" or "Revised" License

下载


Package version
Test status
Supported Python versions
Checked with mypy
codecov

flake8-type-checking

Lets you know which imports to move in or out of
type-checking blocks.

The plugin assumes that the imports you only use for type hinting
are not required at runtime. When imports aren’t strictly required at runtime, it means we can guard them.

Guarding imports provides 3 major benefits:

  • 🔧 It reduces import circularity issues,
  • 🧹 It organizes imports, and
  • 🚀 It completely eliminates the overhead of type hint imports at runtime


Essentially, this code:

  1. import pandas # 15mb library
  2. x: pandas.DataFrame

becomes this:

  1. from typing import TYPE_CHECKING
  2. if TYPE_CHECKING:
  3. import pandas # <-- no longer imported at runtime
  4. x: "pandas.DataFrame"

More examples can be found in the examples section.


If you’re using pydantic,
fastapi, cattrs,
or injector
see the configuration for how to enable support.

Primary features

The plugin will:

  • Tell you when an import should be moved into a type-checking block
  • Tell you when an import should be moved out again

And depending on which error code range you’ve opted into, it will tell you

  • Whether you need to add a from __future__ import annotations import
  • Whether you need to quote an annotation
  • Whether you can unquote a quoted annotation

Error codes

Code Description
TC001 Move application import into a type-checking block
TC002 Move third-party import into a type-checking block
TC003 Move built-in import into a type-checking block
TC004 Move import out of type-checking block. Import is used for more than type hinting.
TC005 Found empty type-checking block
TC006 Annotation in typing.cast() should be a string literal
TC007 Type alias needs to be made into a string literal
TC008 Type alias does not need to be a string literal
TC009 Move declaration out of type-checking block. Variable is used for more than type hinting.
TC010 Operands for cannot be a string literal

Choosing how to handle forward references

You need to choose whether to opt-into using the
TC100- or the TC200-range of error codes.

They represent two different ways of solving the same problem, so please only choose one.

TC100 and TC101 manage forward references by taking advantage of
postponed evaluation of annotations.

Code Description
TC100 Add ‘from __future__ import annotations’ import
TC101 Annotation does not need to be a string literal

TC200 and TC201 manage forward references using string literals.

Code Description
TC200 Annotation needs to be made into a string literal
TC201 Annotation does not need to be a string literal

Enabling error ranges

Add TC and TC1 or TC2 to your flake8 config like this:

  1. [flake8]
  2. max-line-length = 80
  3. max-complexity = 12
  4. ...
  5. ignore = E501
  6. # You can use 'extend-select' (new in flake8 v4):
  7. extend-select = TC, TC2
  8. # OR 'select':
  9. select = C,E,F..., TC, TC2 # or TC1
  10. # OR 'enable-extensions':
  11. enable-extensions = TC, TC2 # or TC1

If you are unsure which TC range to pick, see the rationale for more info.

Installation

  1. pip install flake8-type-checking

Configuration

These options are configurable, and can be set in your flake8 config.

Typing modules

If you re-export typing or typing_extensions members from a compatibility
module, you will need to specify them here in order for inference to work
correctly for special forms like Literal or Annotated.

If you use relative imports for the compatibility module in your code-base
you will need to add separate entries for each kind of relative import you
use.

  • setting name: type-checking-typing-modules
  • type: list
  1. [flake8]
  2. type-checking-typing-modules = mylib.compat, .compat, ..compat # default []

Exempt modules

If you wish to exempt certain modules from
needing to be moved into type-checking blocks, you can specify which
modules to ignore.

  • setting name: type-checking-exempt-modules
  • type: list
  1. [flake8]
  2. type-checking-exempt-modules = typing_extensions # default []

Strict

The plugin, by default, will report TC00[1-3] errors
for imports if there aren’t already other imports from the same module.
When there are other imports from the same module,
the import circularity and performance benefits no longer
apply from guarding an import.

When strict mode is enabled, the plugin will flag all
imports that can be moved.

  • setting name: type-checking-strict
  • type: bool
  1. [flake8]
  2. type-checking-strict = true # default false

Force from __future__ import annotations import

The plugin, by default, will only report a TC100 error, if annotations
contain references to typing only symbols. If you want to enforce a more
consistent style and use a future import in every file that makes use
of annotations, you can enable this setting.

When force-future-annotation is enabled, the plugin will flag all
files that contain annotations but not future import.

  • setting name: type-checking-force-future-annotation
  • type: bool
  1. [flake8]
  2. type-checking-force-future-annotation = true # default false

Pydantic support

If you use Pydantic models in your code, you should enable Pydantic support.
This will treat any class variable annotation as being needed during runtime.

  • name: type-checking-pydantic-enabled
  • type: bool
    1. [flake8]
    2. type-checking-pydantic-enabled = true # default false

    Pydantic support base-class passlist

Disabling checks for all class annotations is a little aggressive.

If you feel comfortable that all base classes named, e.g., NamedTuple are not Pydantic models,
then you can pass the names of the base classes in this setting, to re-enable checking for classes
which inherit from them.

  • name: type-checking-pydantic-enabled-baseclass-passlist
  • type: list
    1. [flake8]
    2. type-checking-pydantic-enabled-baseclass-passlist = NamedTuple, TypedDict # default []

FastAPI support

If you’re using the plugin for a FastAPI project,
you should enable support. This will treat the annotations
of any decorated function as needed at runtime.

Enabling FastAPI support will also enable Pydantic support.

  • name: type-checking-fastapi-enabled
  • type: bool
    1. [flake8]
    2. type-checking-fastapi-enabled = true # default false

One more thing to note for FastAPI users is that dependencies
(functions used in Depends) will produce false positives, unless
you enable dependency support as described below.

FastAPI dependency support

In addition to preventing false positives for decorators, we can
prevent false positives for dependencies. We are making a pretty bad
trade-off however: by enabling this option we treat every annotation
in every function definition across your entire project as a possible
dependency annotation. In other words, we stop linting all function
annotations completely, to avoid the possibility of false positives.
If you prefer to be on the safe side, you should enable this - otherwise
it might be enough to be aware that false positives can happen for functions
used as dependencies.

Enabling dependency support will also enable FastAPI and Pydantic support.

  • name: type-checking-fastapi-dependency-support-enabled
  • type: bool
    1. [flake8]
    2. type-checking-fastapi-dependency-support-enabled = true # default false

SQLAlchemy 2.0+ support

If you’re using SQLAlchemy 2.0+, you can enable support.
This will treat any Mapped[...] types as needed at runtime.
It will also specially treat the enclosed type, since it may or may not
need to be available at runtime depending on whether or not the enclosed
type is a model or not, since models can have circular dependencies.

  • name: type-checking-sqlalchemy-enabled
  • type: bool
    1. type-checking-sqlalchemy-enabled = true # default false

SQLAlchemy 2.0+ support mapped dotted names

Since it’s possible to create subclasses of sqlalchemy.orm.Mapped that
define some custom behavior for the mapped attribute, but otherwise still
behave like any other mapped attribute, i.e. the same runtime restrictions
apply it’s possible to provide additional dotted names that should be treated
like subclasses of Mapped. By default we check for sqlalchemy.orm.Mapped,
sqlalchemy.orm.DynamicMapped and sqlalchemy.orm.WriteOnlyMapped.

If there’s more than one import path for the same Mapped subclass, then you
need to specify each of them as a separate dotted name.

  • name: type-checking-sqlalchemy-mapped-dotted-names
  • type: list
    1. type-checking-sqlalchemy-mapped-dotted-names = a.MyMapped, a.b.MyMapped # default []

Cattrs support

If you’re using the plugin in a project which uses cattrs,
you can enable support. This will treat the annotations
of any decorated attrs class as needed at runtime, since
cattrs.unstructure calls will fail when loading
classes where types are not available at runtime.

Note: the cattrs support setting does not yet detect and
ignore class var annotations on dataclasses or other non-attrs class types.
This can be added in the future if needed.

  • name: type-checking-cattrs-enabled
  • type: bool
    1. [flake8]
    2. type-checking-cattrs-enabled = true # default false

Injector support

If you’re using the injector library, you can enable support.
This will treat any Inject[Dependency] types as needed at runtime.

  • name: type-checking-injector-enabled
  • type: bool
    1. type-checking-injector-enabled = true # default false

Rationale

Why did we create this plugin?

Good type hinting typically requires a lot of project imports, which can increase
the risk of import cycles
in a project. The recommended way of preventing this problem is to use typing.TYPE_CHECKING blocks
to guard these types of imports. In particular, TC001 helps protect against this issue.

Once imports are guarded, they will no longer be evaluated/imported during runtime. The
consequence of this is that these imports can no longer be treated as if they
were imported outside the block. Instead we need to use forward references.

For Python version >= 3.7, there are actually two ways of solving this issue.
You can either make your annotations string literals, or you can use a __futures__ import to enable postponed evaluation of annotations.
See this excellent stackoverflow answer
for a great explanation of the differences.

Examples


Performance example

Imports for type hinting can have a performance impact.

python import pandas def dataframe_length(df: pandas.DataFrame) -> int: return len(df)

In this example, we import a 15mb library, for a single type hint.

We don’t need to perform this operation at runtime, at all.
If we know that the import will not otherwise be needed by surrounding code,
we can simply guard it, like this:

python from typing import TYPE_CHECKING if TYPE_CHECKING: import pandas # <-- no longer imported at runtime def dataframe_length(df: "pandas.DataFrame") -> int: return len(df)

Now the import is no longer made at runtime. If you’re unsure about how this works, see the mypy docs for a basic introduction.

Import circularity example

Bad code

models/a.py
python from models.b import B class A(Model): def foo(self, b: B): ...

models/b.py
python from models.a import A class B(Model): def bar(self, a: A): ...

Will result in these errors

shell >> a.py: TC002 Move third-party import 'models.b.B' into a type-checking block >> b.py: TC002 Move third-party import 'models.a.A' into a type-checking block

and consequently trigger these errors if imports are purely moved into type-checking block, without proper forward reference handling

shell >> a.py: TC100 Add 'from __future__ import annotations' import >> b.py: TC100 Add 'from __future__ import annotations' import

or

shell >> a.py: TC200 Annotation 'B' needs to be made into a string literal >> b.py: TC200 Annotation 'A' needs to be made into a string literal

Good code

models/a.py
python # TC1 from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from models.b import B class A(Model): def foo(self, b: B): ...
or
python # TC2 from typing import TYPE_CHECKING if TYPE_CHECKING: from models.b import B class A(Model): def foo(self, b: 'B'): ...

models/b.py
python # TC1 from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from models.a import A class B(Model): def bar(self, a: A): ...

or

python # TC2 from typing import TYPE_CHECKING if TYPE_CHECKING: from models.a import A class B(Model): def bar(self, a: 'A'): ...

Examples from the wild

Here are a few examples of public projects that use flake8-type-checking:

- Example from the Poetry codebase
- Example from the asgi-correlation-id codebase

Running the plugin as a pre-commit hook

You can run this flake8 plugin as a pre-commit hook:

  1. - repo: https://github.com/pycqa/flake8
  2. rev: 4.0.1
  3. hooks:
  4. - id: flake8
  5. additional_dependencies:
  6. - flake8-type-checking

Contributing

Please feel free to open an issue or a PR 👏