Skip to content

Commit

Permalink
Update descriptions for codemods that add dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
drdavella committed Dec 21, 2023
1 parent 47e9179 commit 4c3a356
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 16 deletions.
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
)

0 comments on commit 4c3a356

Please sign in to comment.