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

Update descriptions for codemods that add dependencies #187

Merged
merged 1 commit into from
Dec 21, 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
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repos:
exclude: |
(?x)^(
src/core_codemods/docs/.*|
src/codemodder/dependency.py |
integration_tests/.*|
tests/test_codemodder.py
)$
Expand Down
31 changes: 15 additions & 16 deletions src/codemodder/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
from typing import List, Iterator

from codemodder.change import ChangeSet
from codemodder.dependency import Dependency
from codemodder.dependency import (
Dependency,
build_dependency_notification,
build_failed_dependency_notification,
)
from codemodder.executor import CodemodExecutorWrapper
from codemodder.file_context import FileContext
from codemodder.logging import logger, log_list
Expand All @@ -15,22 +19,10 @@
from codemodder.utils.timer import Timer


DEPENDENCY_NOTIFICATION = """```
💡 This codemod adds a dependency to your project. \
Currently we add the dependency to a file named `requirements.txt` if it \
exists in your project.

There are a number of other places where Python project dependencies can be \
expressed, including `setup.py`, `pyproject.toml`, and `setup.cfg`. We are \
working on adding support for these files, but for now you may need to update \
these files manually before accepting this change.
```
"""


class CodemodExecutionContext: # pylint: disable=too-many-instance-attributes
_results_by_codemod: dict[str, list[ChangeSet]] = {}
_failures_by_codemod: dict[str, list[Path]] = {}
_dependency_update_by_codemod: dict[str, PackageStore | None] = {}
dependencies: dict[str, set[Dependency]] = {}
directory: Path
dry_run: bool = False
Expand Down Expand Up @@ -107,6 +99,7 @@ def process_dependencies(
"unable to write dependencies for %s: no dependency file found",
codemod_id,
)
self._dependency_update_by_codemod[codemod_id] = None
return record

# pylint: disable-next=cyclic-import
Expand All @@ -116,6 +109,7 @@ def process_dependencies(
dm = DependencyManager(package_store, self.directory)
if (changeset := dm.write(list(dependencies), self.dry_run)) is not None:
self.add_results(codemod_id, [changeset])
self._dependency_update_by_codemod[codemod_id] = package_store
for dep in dependencies:
record[dep] = package_store
break
Expand All @@ -124,8 +118,13 @@ def process_dependencies(

def add_description(self, codemod: CodemodExecutorWrapper):
description = codemod.description
if codemod.adds_dependency:
description = f"{description}\n\n{DEPENDENCY_NOTIFICATION}"
if dependencies := list(self.dependencies.get(codemod.id, [])):
if pkg_store := self._dependency_update_by_codemod.get(codemod.id):
description += build_dependency_notification(
pkg_store.type.value, dependencies[0]
)
else:
description += build_failed_dependency_notification(dependencies[0])

return description

Expand Down
68 changes: 68 additions & 0 deletions src/codemodder/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,71 @@ def __hash__(self):
oss_link="https://github.com/pixee/python-security",
package_link="https://pypi.org/project/security/",
)


DEPENDENCY_NOTIFICATION = """
## Dependency Updates

This codemod relies on an external dependency. We have automatically added this dependency to your project's `{filename}` file.

{description}

There are a number of places where Python project dependencies can be expressed, including `setup.py`, `pyproject.toml`, `setup.cfg`, and `requirements.txt` files. If this change is incorrect, or if you are using another packaging system such as `poetry`, it may be necessary for you to manually add the dependency to the proper location in your project.
"""

FAILED_DEPENDENCY_NOTIFICATION = """
## Dependency Updates

This codemod relies on an external dependency. However, we were unable to automatically add the dependency to your project.

{description}

There are a number of places where Python project dependencies can be expressed, including `setup.py`, `pyproject.toml`, `setup.cfg`, and `requirements.txt` files. You may need to manually add this dependency to the proper location in your project.

### Manual Installation

For `setup.py`:
```diff
install_requires=[
+ "{requirement}",
],
```

For `pyproject.toml` (using `setuptools`):
```diff
[project]
dependencies = [
+ "{requirement}",
]
```

For `setup.cfg`:
```diff
[options]
install_requires =
+ {requirement}
```

For `requirements.txt`:
```diff
+{requirement}
```

For more information on adding dependencies to `setuptools` projects, see [the setuptools documentation](https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#declaring-required-dependency).

If you are using another build system, please refer to the documentation for that system to determine how to add dependencies.
"""


def build_dependency_notification(filename: str, dependency: Dependency) -> str:
return DEPENDENCY_NOTIFICATION.format(
filename=filename,
description=dependency.description,
)


def build_failed_dependency_notification(dependency: Dependency) -> str:
return FAILED_DEPENDENCY_NOTIFICATION.format(
description=dependency.description,
requirement=dependency.requirement,
)
65 changes: 65 additions & 0 deletions tests/test_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from codemodder.context import CodemodExecutionContext as Context
from codemodder.dependency import Security
from codemodder.registry import load_registered_codemods
from codemodder.project_analysis.python_repo_manager import PythonRepoManager


class TestContext:
def test_successful_dependency_description(self, mocker):
registry = load_registered_codemods()
repo_manager = PythonRepoManager(mocker.Mock())
codemod = registry.match_codemods(codemod_include=["url-sandbox"])[0]

context = Context(mocker.Mock(), True, False, registry, repo_manager)
context.add_dependencies(codemod.id, {Security})

pkg_store_name = "pyproject.toml"

pkg_store = mocker.Mock()
pkg_store.type.value = pkg_store_name
mocker.patch(
"codemodder.project_analysis.file_parsers.base_parser.BaseParser.parse",
return_value=[pkg_store],
)

context.process_dependencies(codemod.id)
description = context.add_description(codemod)

assert description.startswith(codemod.description)
assert "## Dependency Updates\n" in description
assert (
f"We have automatically added this dependency to your project's `{pkg_store_name}` file."
in description
)
assert Security.description in description
assert "### Manual Installation\n" not in description

def test_failed_dependency_description(self, mocker):
registry = load_registered_codemods()
repo_manager = PythonRepoManager(mocker.Mock())
codemod = registry.match_codemods(codemod_include=["url-sandbox"])[0]

context = Context(mocker.Mock(), True, False, registry, repo_manager)
context.add_dependencies(codemod.id, {Security})

mocker.patch(
"codemodder.project_analysis.python_repo_manager.PythonRepoManager._parse_all_stores",
return_value=[],
)

context.process_dependencies(codemod.id)
description = context.add_description(codemod)

assert description.startswith(codemod.description)
assert "## Dependency Updates\n" in description
assert Security.description in description
assert "### Manual Installation\n" in description
assert (
f"""For `setup.py`:
```diff
install_requires=[
+ "{Security.requirement}",
],
```"""
in description
)
Loading