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

✅ Add integration tests #17

Merged
merged 2 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions bump_pydantic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,11 @@ def main(
diff: bool = Option(False, help="Show diff instead of applying changes."),
version: bool = Option(None, "--version", callback=version_callback, is_eager=True),
):
cwd = os.getcwd()
files_str = [path.absolute() for path in package.glob("**/*.py")]
files = [str(file.relative_to(cwd)) for file in files_str]
files_str = list(package.glob("**/*.py"))
files = [str(file.relative_to(".")) for file in files_str]

providers = {ScopeProvider, PositionProvider, FullyQualifiedNameProvider}
metadata_manager = FullRepoManager(cwd, files, providers=providers) # type: ignore[arg-type]
metadata_manager = FullRepoManager(".", files, providers=providers) # type: ignore[arg-type]
metadata_manager.resolve_cache()

scratch: Dict[str, Any] = {}
Expand Down
11 changes: 0 additions & 11 deletions project/add_none.py

This file was deleted.

17 changes: 0 additions & 17 deletions project/config_to_model.py

This file was deleted.

4 changes: 0 additions & 4 deletions project/rename_method.py

This file was deleted.

9 changes: 0 additions & 9 deletions project/replace_generic.py

This file was deleted.

5 changes: 0 additions & 5 deletions project/settings.py

This file was deleted.

File renamed without changes.
266 changes: 266 additions & 0 deletions tests/integration/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
from __future__ import annotations

import difflib
from pathlib import Path

import pytest
from typer.testing import CliRunner

from bump_pydantic.main import app


class Folder:
def __init__(self, name: str, *files: Folder | File) -> None:
self.name = name
self._files = files

@property
def files(self) -> list[Folder | File]:
return sorted(self._files, key=lambda f: f.name)

def create_structure(self, root: Path) -> None:
path = root / self.name
path.mkdir()

for file in self.files:
if isinstance(file, Folder):
file.create_structure(path)
else:
(path / file.name).write_text(file.content)

@classmethod
def from_structure(cls, root: Path) -> Folder:
name = root.name
files: list[File | Folder] = []

for path in root.iterdir():
if path.is_dir():
files.append(cls.from_structure(path))
else:
files.append(File(path.name, path.read_text().splitlines()))

return Folder(name, *files)

def __eq__(self, __value: object) -> bool:
if isinstance(__value, File):
return False

if not isinstance(__value, Folder):
return NotImplemented

if self.name != __value.name:
return False

if len(self.files) != len(__value.files):
return False

for self_file, other_file in zip(self.files, __value.files):
if self_file != other_file:
return False

return True


class File:
def __init__(self, name: str, content: list[str] | None = None) -> None:
self.name = name
self.content = "\n".join(content or [])

def __eq__(self, __value: object) -> bool:
if not isinstance(__value, File):
return NotImplemented

if self.name != __value.name:
return False

return self.content == __value.content


@pytest.fixture()
def before() -> Folder:
return Folder(
"project",
File("__init__.py"),
File(
"settings.py",
content=[
"from pydantic import BaseSettings",
"",
"",
"class Settings(BaseSettings):",
" a: int",
],
),
File(
"add_none.py",
content=[
"from typing import Any, Dict, Optional, Union",
"",
"from pydantic import BaseModel",
"",
"",
"class A(BaseModel):",
" a: int | None",
" b: Optional[int]",
" c: Union[int, None]",
" d: Any",
" e: Dict[str, str]",
],
),
File(
"config_to_model.py",
content=[
"from pydantic import BaseModel",
"",
"",
"class A(BaseModel):",
" class Config:",
" orm_mode = True",
" validate_all = True",
"",
"",
"class BaseConfig:",
" orm_mode = True",
" validate_all = True",
"",
"",
"class B(BaseModel):",
" class Config(BaseConfig):",
" ...",
],
),
# File(
# "rename_method.py",
# content=[
# "from project.add_none import A",
# "",
# 'a = A(a=1, b=2, c=3, d=4, e={"ha": "ha"})',
# "a.dict()",
# ],
# ),
File(
"replace_generic.py",
content=[
"from typing import Generic, TypeVar",
"",
"from pydantic.generics import GenericModel",
"",
"T = TypeVar('T')",
"",
"",
"class User(GenericModel, Generic[T]):",
" name: str",
],
),
)


@pytest.fixture()
def expected() -> Folder:
return Folder(
"project",
File("__init__.py"),
File(
"settings.py",
content=[
"from pydantic_settings import BaseSettings",
"",
"",
"class Settings(BaseSettings):",
" a: int",
],
),
File(
"add_none.py",
content=[
"from typing import Any, Dict, Optional, Union",
"",
"from pydantic import BaseModel",
"",
"",
"class A(BaseModel):",
" a: int | None = None",
" b: Optional[int] = None",
" c: Union[int, None] = None",
" d: Any = None",
" e: Dict[str, str]",
],
),
File(
"config_to_model.py",
content=[
"from pydantic import ConfigDict, BaseModel",
"",
"",
"class A(BaseModel):",
" model_config = ConfigDict(orm_mode=True, validate_all=True)",
"",
"",
"class BaseConfig:",
" orm_mode = True",
" validate_all = True",
"",
"",
"class B(BaseModel):",
" # TODO[pydantic]: The `Config` class inherits from another class, please create the `model_config` manually.", # noqa: E501
" # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information.",
" class Config(BaseConfig):",
" ...",
],
),
# File(
# "rename_method.py",
# content=[
# "from project.add_none import A",
# "",
# 'a = A(a=1, b=2, c=3, d=4, e={"ha": "ha"})',
# "a.dict()",
# ],
# ),
File(
"replace_generic.py",
content=[
"from typing import Generic, TypeVar",
"from pydantic import BaseModel",
"",
"T = TypeVar('T')",
"",
"",
"class User(BaseModel, Generic[T]):",
" name: str",
],
),
)


def find_issue(current: Folder, expected: Folder) -> str:
for current_file, expected_file in zip(current.files, expected.files):
if current_file != expected_file:
if current_file.name != expected_file.name:
return f"Files have different names: {current_file.name} != {expected_file.name}"
if isinstance(current_file, Folder) or isinstance(expected_file, Folder):
return f"One of the files is a folder: {current_file.name} != {expected_file.name}"
return "\n".join(
difflib.unified_diff(
current_file.content.splitlines(),
expected_file.content.splitlines(),
fromfile=current_file.name,
tofile=expected_file.name,
)
)
return "Unknown"


def test_command_line(tmp_path: Path, before: Folder, expected: Folder) -> None:
runner = CliRunner()

with runner.isolated_filesystem(temp_dir=tmp_path) as td:
before.create_structure(root=Path(td))

result = runner.invoke(app, [before.name])
assert result.exit_code == 0, result.output
assert result.output == "Refactored 4 files.\n"

after = Folder.from_structure(Path(td) / before.name)

assert after == expected, find_issue(after, expected)
Empty file added tests/unit/__init__.py
Empty file.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading