Skip to content

Commit

Permalink
♻️ Remove mypy type inferencer (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kludex authored Jun 16, 2023
1 parent 9f757d6 commit 6a79d75
Show file tree
Hide file tree
Showing 30 changed files with 1,309 additions and 1,268 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

on:
push:
branches:
- main
pull_request: {}

jobs:
lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: set up python
uses: actions/setup-python@v4
with:
python-version: "3.10"

- name: Install hatch
run: pip install hatch

- uses: pre-commit/[email protected]
with:
extra_args: --all-files
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-yaml
args: ['--unsafe']
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace

- repo: local
hooks:
- id: lint
name: Lint
entry: hatch run lint
types: [python]
language: system
pass_filenames: false
61 changes: 44 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
# bump-pydantic
# Bump Pydantic ♻️

[![PyPI - Version](https://img.shields.io/pypi/v/bump-pydantic.svg)](https://pypi.org/project/bump-pydantic)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/bump-pydantic.svg)](https://pypi.org/project/bump-pydantic)
<!-- [![PyPI - Version](https://img.shields.io/pypi/v/bump-pydantic.svg)](https://pypi.org/project/bump-pydantic)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/bump-pydantic.svg)](https://pypi.org/project/bump-pydantic) -->

Utility to bump pydantic from V1 to V2.

-----

**Table of Contents**
### Rules

#### BP001: Replace imports

- ✅ Replace `BaseSettings` from `pydantic` to `pydantic_settings`.
- ✅ Replace `Color` and `PaymentCardNumber` from `pydantic` to `pydantic_extra_types`.

#### BP002: Add default `None` to `Optional[T]`, `Union[T, None]` and `Any` fields

- ✅ Add default `None` to `Optional[T]` fields.

The following code will be transformed:

- [bump-pydantic](#bump-pydantic)
- [Installation](#installation)
- [Usage](#usage)
- [License](#license)
```py
class User(BaseModel):
name: Optional[str]
```

## Installation
Into:

```console
pip install bump-pydantic
```py
class User(BaseModel):
name: Optional[str] = None
```

## Usage
#### BP003: Replace `Config` class by `model_config`

- ✅ Replace `Config` class by `model_config = ConfigDict()`.

The following code will be transformed:

You can run `bump-pydantic` from the command line:
```py
class User(BaseModel):
name: str

```console
bump-pydantic <FILES>
class Config:
extra = 'forbid'
```

## License
Into:

```py
class User(BaseModel):
name: str

model_config = ConfigDict(extra='forbid')
```

`bump-pydantic` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
#### BP004: Replace `BaseModel` methods
96 changes: 1 addition & 95 deletions bump_pydantic/__main__.py
Original file line number Diff line number Diff line change
@@ -1,98 +1,4 @@
import difflib
import os
import sys
import time
from pathlib import Path

import libcst as cst
from libcst.codemod import CodemodContext
from libcst.helpers import calculate_module_and_package
from libcst.metadata import FullRepoManager, PositionProvider, ScopeProvider
from libcst_mypy import MypyTypeInferenceProvider
from typer import Argument, Option, Typer

from bump_pydantic.transformers import gather_transformers

app = Typer(help="Convert Pydantic from V1 to V2.")


@app.command()
def main(
package: Path = Argument(..., exists=True, dir_okay=True, allow_dash=False),
diff: bool = Option(False, help="Show diff instead of applying changes."),
debug: bool = Option(False, help="Show debug logs."),
add_default_none: bool = True,
# NOTE: It looks like there are some issues with the libcst.codemod.RenameCommand.
# To replicate the issue: clone aiopenapi3, and run `python -m bump_pydantic aiopenapi3`.
# For that reason, the default is False.
rename_imports: bool = False,
rename_methods: bool = True,
replace_config_class: bool = True,
replace_config_parameters: bool = True,
) -> None:
# sourcery skip: hoist-similar-statement-from-if, simplify-len-comparison, swap-nested-ifs
files = [str(path.absolute()) for path in package.glob("**/*.py")]

transformers = gather_transformers(
add_default_none=add_default_none,
rename_imports=rename_imports,
rename_methods=rename_methods,
replace_config_class=replace_config_class,
replace_config_parameters=replace_config_parameters,
)

cwd = os.getcwd()
providers = {MypyTypeInferenceProvider, ScopeProvider, PositionProvider}
metadata_manager = FullRepoManager(cwd, files, providers=providers)
print("Inferring types... This may take a while.")
metadata_manager.resolve_cache()
print("Types are inferred.")

start_time = time.time()

# TODO: We can run this in parallel - batch it into files / cores.
# We may need to run the resolve_cache() on each core - not sure.
for transformer in transformers:
for filename in files:
module_and_package = calculate_module_and_package(cwd, filename)
transform = transformer(
CodemodContext(
metadata_manager=metadata_manager,
filename=filename,
full_module_name=module_and_package.name,
full_package_name=module_and_package.package,
)
)
if debug:
print(f"Processing {filename} with {transform.__class__.__name__}")

with open(filename) as fp:
old_code = fp.read()

input_tree = cst.parse_module(old_code)
output_tree = transform.transform_module(input_tree)

input_code = input_tree.code
output_code = output_tree.code

if input_code != output_code:
if diff:
# TODO: Should be colored.
lines = difflib.unified_diff(
input_code.splitlines(keepends=True),
output_code.splitlines(keepends=True),
fromfile=filename,
tofile=filename,
)
sys.stdout.writelines(lines)
else:
with open(filename, "w") as fp:
fp.write(output_tree.code)

modified = [Path(f) for f in files if os.stat(f).st_mtime > start_time]
if len(modified) > 0:
print(f"Refactored {len(modified)} files.")

from bump_pydantic.main import app

if __name__ == "__main__":
app()
18 changes: 18 additions & 0 deletions bump_pydantic/codemods/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import List, Type

from libcst.codemod import ContextAwareTransformer
from libcst.codemod.visitors import AddImportsVisitor

from bump_pydantic.codemods.add_default_none import AddDefaultNoneCommand
from bump_pydantic.codemods.replace_config import ReplaceConfigCodemod
from bump_pydantic.codemods.replace_imports import ReplaceImportsCodemod


def gather_codemods() -> List[Type[ContextAwareTransformer]]:
return [
AddDefaultNoneCommand,
ReplaceConfigCodemod,
ReplaceImportsCodemod,
# AddImportsVisitor needs to be the last.
AddImportsVisitor,
]
Loading

0 comments on commit 6a79d75

Please sign in to comment.