From bb166863c1afd35cd1cfe4a1f6bebc05e165478a Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Mon, 20 Nov 2023 14:21:45 -0300 Subject: [PATCH 1/4] create DM subdir --- src/codemodder/context.py | 2 +- src/codemodder/dependency_management/__init__.py | 1 + .../{ => dependency_management}/dependency_manager.py | 0 tests/conftest.py | 4 +++- tests/test_dependency_manager.py | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 src/codemodder/dependency_management/__init__.py rename src/codemodder/{ => dependency_management}/dependency_manager.py (100%) diff --git a/src/codemodder/context.py b/src/codemodder/context.py index 719f7e79..4d58036c 100644 --- a/src/codemodder/context.py +++ b/src/codemodder/context.py @@ -6,7 +6,7 @@ from codemodder.change import ChangeSet from codemodder.dependency import Dependency -from codemodder.dependency_manager import DependencyManager +from codemodder.dependency_management import DependencyManager from codemodder.executor import CodemodExecutorWrapper from codemodder.file_context import FileContext from codemodder.logging import logger, log_list diff --git a/src/codemodder/dependency_management/__init__.py b/src/codemodder/dependency_management/__init__.py new file mode 100644 index 00000000..ad101a44 --- /dev/null +++ b/src/codemodder/dependency_management/__init__.py @@ -0,0 +1 @@ +from .dependency_manager import DependencyManager, Requirement diff --git a/src/codemodder/dependency_manager.py b/src/codemodder/dependency_management/dependency_manager.py similarity index 100% rename from src/codemodder/dependency_manager.py rename to src/codemodder/dependency_management/dependency_manager.py diff --git a/tests/conftest.py b/tests/conftest.py index c366bea1..38b3712d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,7 +44,9 @@ def disable_write_dependencies(): """ Unit tests should not write any dependency files """ - dm_write = mock.patch("codemodder.dependency_manager.DependencyManager.write") + dm_write = mock.patch( + "codemodder.dependency_management.dependency_manager.DependencyManager.write" + ) dm_write.start() yield diff --git a/tests/test_dependency_manager.py b/tests/test_dependency_manager.py index f4a98cbf..35474099 100644 --- a/tests/test_dependency_manager.py +++ b/tests/test_dependency_manager.py @@ -3,7 +3,7 @@ import pytest from codemodder.dependency import DefusedXML, Security -from codemodder.dependency_manager import DependencyManager, Requirement +from codemodder.dependency_management import DependencyManager, Requirement @pytest.fixture(autouse=True, scope="module") From 71d93fb67f5870ee4902d46e03e44f6a30675ebb Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Tue, 21 Nov 2023 09:12:50 -0300 Subject: [PATCH 2/4] codemod plain cst --- src/codemodder/codemods/utils.py | 5 +++ .../dependency_management/setup_py_codemod.py | 37 +++++++++++++++++ tests/dependency_management/__init__.py | 0 .../test_dependency_manager.py | 0 .../test_setup_py_codemod.py | 41 +++++++++++++++++++ tests/samples/pkg_w_setuppy/setup.py | 25 +++++++++++ .../pkg_w_setuppy/src/sample/__init__.py | 0 .../samples/pkg_w_setuppy/src/sample/hello.py | 1 + 8 files changed, 109 insertions(+) create mode 100644 src/codemodder/dependency_management/setup_py_codemod.py create mode 100644 tests/dependency_management/__init__.py rename tests/{ => dependency_management}/test_dependency_manager.py (100%) create mode 100644 tests/dependency_management/test_setup_py_codemod.py create mode 100644 tests/samples/pkg_w_setuppy/setup.py create mode 100644 tests/samples/pkg_w_setuppy/src/sample/__init__.py create mode 100644 tests/samples/pkg_w_setuppy/src/sample/hello.py diff --git a/src/codemodder/codemods/utils.py b/src/codemodder/codemods/utils.py index 7086fb76..eaad73aa 100644 --- a/src/codemodder/codemods/utils.py +++ b/src/codemodder/codemods/utils.py @@ -113,6 +113,11 @@ def is_django_settings_file(file_path: Path): return False +def is_setup_py_file(file_path: str): + name = Path(file_path).name + return name == "setup.py" + + def get_call_name(call: cst.Call) -> str: """ Extracts the full name from a function call diff --git a/src/codemodder/dependency_management/setup_py_codemod.py b/src/codemodder/dependency_management/setup_py_codemod.py new file mode 100644 index 00000000..7320b7cb --- /dev/null +++ b/src/codemodder/dependency_management/setup_py_codemod.py @@ -0,0 +1,37 @@ +import libcst as cst +from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand +from codemodder.codemods.utils import is_setup_py_file +from codemodder.codemods.utils_mixin import NameResolutionMixin + + +class SetupPyAddDependencies(VisitorBasedCodemodCommand, NameResolutionMixin): + def __init__(self, context: CodemodContext, dependencies): + """ + :param dependencies: + """ + super().__init__(context) + self.filename = self.context.filename + self.dependencies = dependencies + + def visit_Module(self, _: cst.Module) -> bool: + """ + Only visit module with this codemod if it's a setup.py file. + """ + return is_setup_py_file(self.filename) + + def leave_Call(self, original_node: cst.Call, updated_node: cst.Call): + true_name = self.find_base_name(original_node.func) + if true_name != "setuptools.setup": + return original_node + + # todo: add self.dependencies to install_requires arg + breakpoint() + return updated_node + + +# filename = "tests/samples/pkg_w_setuppy/setup.py" +# with open(filename, "r", encoding="utf-8") as f: +# source_tree = cst.parse_module(f.read()) +# +# codemod = SetupPyAddDependencies(CodemodContext(filename=filename), ["dep1"]) +# codemod.transform_module(source_tree) diff --git a/tests/dependency_management/__init__.py b/tests/dependency_management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_dependency_manager.py b/tests/dependency_management/test_dependency_manager.py similarity index 100% rename from tests/test_dependency_manager.py rename to tests/dependency_management/test_dependency_manager.py diff --git a/tests/dependency_management/test_setup_py_codemod.py b/tests/dependency_management/test_setup_py_codemod.py new file mode 100644 index 00000000..d623b495 --- /dev/null +++ b/tests/dependency_management/test_setup_py_codemod.py @@ -0,0 +1,41 @@ +from codemodder.dependency_management.setup_py_codemod import SetupPyAddDependencies +from libcst.codemod import CodemodTest, CodemodContext +from packaging.requirements import Requirement + +TEST_DEPENDENCIES = [Requirement("defusedxml==0.7.1"), Requirement("security~=1.2.0")] + + +class TestSetupPyCodemod(CodemodTest): + TRANSFORM = SetupPyAddDependencies + CONTEXT = CodemodContext(filename="pkg/setup.py") + + def test_setup_call(self): + before = """ + from setuptools import setup + setup( + name="test pkg", + description="testing", + long_description="...", + author="Pixee", + packages=find_packages("src"), + package_dir={"": "src"}, + python_requires=">3.6", + install_requires=[ + "protobuf>=3.12,<3.18; python_version < '3'", + "protobuf>=3.12,<4; python_version >= '3'", + "psutil>=5.7,<6", + "requests>=2.4.2,<3", + ], + entry_points={}, + ) + """ + + after = "" + + self.assertCodemod( + before, after, TEST_DEPENDENCIES, context_override=self.CONTEXT + ) + + # def test_different_setup_call(self): + # test does not call install_requires + # test with no dependencies inside install_requires diff --git a/tests/samples/pkg_w_setuppy/setup.py b/tests/samples/pkg_w_setuppy/setup.py new file mode 100644 index 00000000..a204cb70 --- /dev/null +++ b/tests/samples/pkg_w_setuppy/setup.py @@ -0,0 +1,25 @@ +from os import path +from setuptools import find_packages, setup + +root_dir = path.abspath(path.dirname(__file__)) + +print(root_dir) + +setup( + name="test pkg", + description="testing", + long_description="...", + # The project's main homepage. + # Author details + author="Pixee", + packages=find_packages("src"), + package_dir={"": "src"}, + python_requires=">3.6", + install_requires=[ + "protobuf>=3.12,<3.18; python_version < '3'", + "protobuf>=3.12,<4; python_version >= '3'", + "psutil>=5.7,<6", + "requests>=2.4.2,<3", + ], + entry_points={}, +) diff --git a/tests/samples/pkg_w_setuppy/src/sample/__init__.py b/tests/samples/pkg_w_setuppy/src/sample/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/samples/pkg_w_setuppy/src/sample/hello.py b/tests/samples/pkg_w_setuppy/src/sample/hello.py new file mode 100644 index 00000000..8cde7829 --- /dev/null +++ b/tests/samples/pkg_w_setuppy/src/sample/hello.py @@ -0,0 +1 @@ +print("hello world") From 78c4f9dd29d09fc73ff068c66f9c5c6c7310fd87 Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Tue, 21 Nov 2023 13:52:51 -0300 Subject: [PATCH 3/4] use base codemod --- src/codemodder/codemods/utils.py | 5 +- .../dependency_management/setup_py_codemod.py | 65 ++++-- tests/codemods/base_codemod_test.py | 24 +-- .../test_setup_py_codemod.py | 201 ++++++++++++++++-- tests/samples/pkg_w_setuppy/setup.py | 25 --- .../pkg_w_setuppy/src/sample/__init__.py | 0 .../samples/pkg_w_setuppy/src/sample/hello.py | 1 - 7 files changed, 251 insertions(+), 70 deletions(-) delete mode 100644 tests/samples/pkg_w_setuppy/setup.py delete mode 100644 tests/samples/pkg_w_setuppy/src/sample/__init__.py delete mode 100644 tests/samples/pkg_w_setuppy/src/sample/hello.py diff --git a/src/codemodder/codemods/utils.py b/src/codemodder/codemods/utils.py index eaad73aa..b2c01e3a 100644 --- a/src/codemodder/codemods/utils.py +++ b/src/codemodder/codemods/utils.py @@ -113,9 +113,8 @@ def is_django_settings_file(file_path: Path): return False -def is_setup_py_file(file_path: str): - name = Path(file_path).name - return name == "setup.py" +def is_setup_py_file(file_path: Path): + return file_path.name == "setup.py" def get_call_name(call: cst.Call) -> str: diff --git a/src/codemodder/dependency_management/setup_py_codemod.py b/src/codemodder/dependency_management/setup_py_codemod.py index 7320b7cb..5c51fcd2 100644 --- a/src/codemodder/dependency_management/setup_py_codemod.py +++ b/src/codemodder/dependency_management/setup_py_codemod.py @@ -1,16 +1,26 @@ import libcst as cst -from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand +from libcst.codemod import CodemodContext +from libcst import matchers +from codemodder.codemods.api import BaseCodemod +from codemodder.codemods.base_codemod import ReviewGuidance from codemodder.codemods.utils import is_setup_py_file from codemodder.codemods.utils_mixin import NameResolutionMixin +from codemodder.file_context import FileContext -class SetupPyAddDependencies(VisitorBasedCodemodCommand, NameResolutionMixin): - def __init__(self, context: CodemodContext, dependencies): - """ - :param dependencies: - """ - super().__init__(context) - self.filename = self.context.filename +class SetupPyAddDependencies(BaseCodemod, NameResolutionMixin): + NAME = "setup-py-add-dependencies" + REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW + SUMMARY = "Add Dependencies to `setup.py` `install_requires`" + DESCRIPTION = SUMMARY + REFERENCES: list = [] + + def __init__( + self, codemod_context: CodemodContext, file_context: FileContext, dependencies + ): + BaseCodemod.__init__(self, codemod_context, file_context) + NameResolutionMixin.__init__(self) + self.filename = self.file_context.file_path self.dependencies = dependencies def visit_Module(self, _: cst.Module) -> bool: @@ -24,14 +34,35 @@ def leave_Call(self, original_node: cst.Call, updated_node: cst.Call): if true_name != "setuptools.setup": return original_node - # todo: add self.dependencies to install_requires arg - breakpoint() - return updated_node + new_args = self.replace_arg(original_node) + return self.update_arg_target(updated_node, new_args) + def replace_arg(self, original_node: cst.Call): + new_args = [] + for arg in original_node.args: + if matchers.matches( + arg.keyword, matchers.Name("install_requires") + ) and matchers.matches(arg.value, matchers.List()): + new = self.add_dependencies_to_arg(arg) + else: + new = arg + new_args.append(new) + return new_args -# filename = "tests/samples/pkg_w_setuppy/setup.py" -# with open(filename, "r", encoding="utf-8") as f: -# source_tree = cst.parse_module(f.read()) -# -# codemod = SetupPyAddDependencies(CodemodContext(filename=filename), ["dep1"]) -# codemod.transform_module(source_tree) + def add_dependencies_to_arg(self, arg: cst.Arg) -> cst.Arg: + new_dependencies = [ + cst.Element(value=cst.SimpleString(value=f'"{str(dep)}"')) + for dep in self.dependencies + ] + # TODO: detect if elements are separated by newline in source code. + return cst.Arg( + keyword=arg.keyword, + value=arg.value.with_changes( + elements=arg.value.elements + tuple(new_dependencies) + ), + equal=arg.equal, + comma=arg.comma, + star=arg.star, + whitespace_after_star=arg.whitespace_after_star, + whitespace_after_arg=arg.whitespace_after_arg, + ) diff --git a/tests/codemods/base_codemod_test.py b/tests/codemods/base_codemod_test.py index 1a80b099..221aaa47 100644 --- a/tests/codemods/base_codemod_test.py +++ b/tests/codemods/base_codemod_test.py @@ -21,6 +21,14 @@ class BaseCodemodTest: def setup_method(self): self.file_context = None + def initialize_codemod(self, input_tree): + wrapper = cst.MetadataWrapper(input_tree) + codemod_instance = self.codemod( + CodemodContext(wrapper=wrapper), + self.file_context, + ) + return codemod_instance + def run_and_assert(self, tmpdir, input_code, expected): tmp_file_path = Path(tmpdir / "code.py") self.run_and_assert_filepath(tmpdir, tmp_file_path, input_code, expected) @@ -41,12 +49,8 @@ def run_and_assert_filepath(self, root, file_path, input_code, expected): [], [], ) - wrapper = cst.MetadataWrapper(input_tree) - command_instance = self.codemod( - CodemodContext(wrapper=wrapper), - self.file_context, - ) - output_tree = command_instance.transform_module(input_tree) + codemod_instance = self.initialize_codemod(input_tree) + output_tree = codemod_instance.transform_module(input_tree) assert output_tree.code == dedent(expected) @@ -92,12 +96,8 @@ def run_and_assert_filepath(self, root, file_path, input_code, expected): [], results, ) - wrapper = cst.MetadataWrapper(input_tree) - command_instance = self.codemod( - CodemodContext(wrapper=wrapper), - self.file_context, - ) - output_tree = command_instance.transform_module(input_tree) + codemod_instance = self.initialize_codemod(input_tree) + output_tree = codemod_instance.transform_module(input_tree) assert output_tree.code == dedent(expected) diff --git a/tests/dependency_management/test_setup_py_codemod.py b/tests/dependency_management/test_setup_py_codemod.py index d623b495..b9133303 100644 --- a/tests/dependency_management/test_setup_py_codemod.py +++ b/tests/dependency_management/test_setup_py_codemod.py @@ -1,15 +1,28 @@ +import pytest +import libcst as cst from codemodder.dependency_management.setup_py_codemod import SetupPyAddDependencies -from libcst.codemod import CodemodTest, CodemodContext +from libcst.codemod import CodemodContext +from tests.codemods.base_codemod_test import BaseCodemodTest from packaging.requirements import Requirement +from pathlib import Path TEST_DEPENDENCIES = [Requirement("defusedxml==0.7.1"), Requirement("security~=1.2.0")] -class TestSetupPyCodemod(CodemodTest): - TRANSFORM = SetupPyAddDependencies - CONTEXT = CodemodContext(filename="pkg/setup.py") +class TestSetupPyCodemod(BaseCodemodTest): + codemod = SetupPyAddDependencies - def test_setup_call(self): + def initialize_codemod(self, input_tree): + """This codemod is initialized with different args than other codemods.""" + wrapper = cst.MetadataWrapper(input_tree) + codemod_instance = self.codemod( + CodemodContext(wrapper=wrapper), + self.file_context, + dependencies=TEST_DEPENDENCIES, + ) + return codemod_instance + + def test_setup_call(self, tmpdir): before = """ from setuptools import setup setup( @@ -24,18 +37,182 @@ def test_setup_call(self): "protobuf>=3.12,<3.18; python_version < '3'", "protobuf>=3.12,<4; python_version >= '3'", "psutil>=5.7,<6", - "requests>=2.4.2,<3", + "requests>=2.4.2,<3" ], entry_points={}, ) """ - after = "" + after = """ + from setuptools import setup + setup( + name="test pkg", + description="testing", + long_description="...", + author="Pixee", + packages=find_packages("src"), + package_dir={"": "src"}, + python_requires=">3.6", + install_requires=[ + "protobuf>=3.12,<3.18; python_version < '3'", + "protobuf>=3.12,<4; python_version >= '3'", + "psutil>=5.7,<6", + "requests>=2.4.2,<3", "defusedxml==0.7.1", "security~=1.2.0" + ], + entry_points={}, + ) + """ + tmp_file_path = Path(tmpdir / "setup.py") + self.run_and_assert_filepath(tmpdir, tmp_file_path, before, after) - self.assertCodemod( - before, after, TEST_DEPENDENCIES, context_override=self.CONTEXT + def test_other_setup_func(self, tmpdir): + before = """ + from something import setup + setup( + name="test pkg", + install_requires=[ + "protobuf>=3.12,<3.18; python_version < '3'", + "protobuf>=3.12,<4; python_version >= '3'", + "psutil>=5.7,<6", + "requests>=2.4.2,<3" + ], + entry_points={}, ) + """ + tmp_file_path = Path(tmpdir / "setup.py") + self.run_and_assert_filepath(tmpdir, tmp_file_path, before, before) - # def test_different_setup_call(self): - # test does not call install_requires - # test with no dependencies inside install_requires + def test_not_setup_file(self, tmpdir): + before = """ + from setuptools import setup + setup( + name="test pkg", + description="testing", + long_description="...", + author="Pixee", + packages=find_packages("src"), + package_dir={"": "src"}, + python_requires=">3.6", + install_requires=[ + "protobuf>=3.12,<3.18; python_version < '3'", + "protobuf>=3.12,<4; python_version >= '3'", + "psutil>=5.7,<6", + "requests>=2.4.2,<3" + ], + entry_points={}, + ) + """ + tmp_file_path = Path(tmpdir / "not-setup.py") + self.run_and_assert_filepath(tmpdir, tmp_file_path, before, before) + + def test_setup_call_no_install_requires(self, tmpdir): + before = """ + from setuptools import setup + setup( + name="test pkg", + description="testing", + long_description="...", + author="Pixee", + packages=find_packages("src"), + package_dir={"": "src"}, + python_requires=">3.6", + ) + """ + tmp_file_path = Path(tmpdir / "setup.py") + self.run_and_assert_filepath(tmpdir, tmp_file_path, before, before) + + def test_setup_no_existing_requirements(self, tmpdir): + before = """ + from setuptools import setup + setup( + name="test pkg", + description="testing", + long_description="...", + author="Pixee", + packages=find_packages("src"), + package_dir={"": "src"}, + python_requires=">3.6", + install_requires=[], + entry_points={}, + ) + """ + + after = """ + from setuptools import setup + setup( + name="test pkg", + description="testing", + long_description="...", + author="Pixee", + packages=find_packages("src"), + package_dir={"": "src"}, + python_requires=">3.6", + install_requires=["defusedxml==0.7.1", "security~=1.2.0"], + entry_points={}, + ) + """ + tmp_file_path = Path(tmpdir / "setup.py") + self.run_and_assert_filepath(tmpdir, tmp_file_path, before, after) + + def test_setup_call_bad_install_requires(self, tmpdir): + before = """ + from setuptools import setup + setup( + name="test pkg", + description="testing", + long_description="...", + author="Pixee", + packages=find_packages("src"), + package_dir={"": "src"}, + python_requires=">3.6", + install_requires="some-package", + ) + """ + tmp_file_path = Path(tmpdir / "setup.py") + self.run_and_assert_filepath(tmpdir, tmp_file_path, before, before) + + @pytest.mark.skip("Need to add support.") + def test_setup_call_requirements_separate(self, tmpdir): + before = """ + from setuptools import setup + requirements = [ + "protobuf>=3.12,<3.18; python_version < '3'", + "protobuf>=3.12,<4; python_version >= '3'", + "psutil>=5.7,<6", + "requests>=2.4.2,<3" + ] + setup( + name="test pkg", + description="testing", + long_description="...", + author="Pixee", + packages=find_packages("src"), + package_dir={"": "src"}, + python_requires=">3.6", + install_requires=requirements, + entry_points={}, + ) + """ + + after = """ + from setuptools import setup + requirements = [ + "protobuf>=3.12,<3.18; python_version < '3'", + "protobuf>=3.12,<4; python_version >= '3'", + "psutil>=5.7,<6", + "requests>=2.4.2,<3", "defusedxml==0.7.1", "security~=1.2.0" + ] + setup( + name="test pkg", + description="testing", + long_description="...", + author="Pixee", + packages=find_packages("src"), + package_dir={"": "src"}, + python_requires=">3.6", + install_requires=requirements, + entry_points={}, + ) + """ + tmp_file_path = Path(tmpdir / "setup.py") + self.run_and_assert_filepath(tmpdir, tmp_file_path, before, after) diff --git a/tests/samples/pkg_w_setuppy/setup.py b/tests/samples/pkg_w_setuppy/setup.py deleted file mode 100644 index a204cb70..00000000 --- a/tests/samples/pkg_w_setuppy/setup.py +++ /dev/null @@ -1,25 +0,0 @@ -from os import path -from setuptools import find_packages, setup - -root_dir = path.abspath(path.dirname(__file__)) - -print(root_dir) - -setup( - name="test pkg", - description="testing", - long_description="...", - # The project's main homepage. - # Author details - author="Pixee", - packages=find_packages("src"), - package_dir={"": "src"}, - python_requires=">3.6", - install_requires=[ - "protobuf>=3.12,<3.18; python_version < '3'", - "protobuf>=3.12,<4; python_version >= '3'", - "psutil>=5.7,<6", - "requests>=2.4.2,<3", - ], - entry_points={}, -) diff --git a/tests/samples/pkg_w_setuppy/src/sample/__init__.py b/tests/samples/pkg_w_setuppy/src/sample/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/samples/pkg_w_setuppy/src/sample/hello.py b/tests/samples/pkg_w_setuppy/src/sample/hello.py deleted file mode 100644 index 8cde7829..00000000 --- a/tests/samples/pkg_w_setuppy/src/sample/hello.py +++ /dev/null @@ -1 +0,0 @@ -print("hello world") From f9127a79f06dcf4663dd3be1593f971693af976f Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Wed, 22 Nov 2023 07:38:17 -0300 Subject: [PATCH 4/4] add type annotation --- src/codemodder/dependency_management/setup_py_codemod.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/codemodder/dependency_management/setup_py_codemod.py b/src/codemodder/dependency_management/setup_py_codemod.py index 5c51fcd2..0f3a239a 100644 --- a/src/codemodder/dependency_management/setup_py_codemod.py +++ b/src/codemodder/dependency_management/setup_py_codemod.py @@ -6,6 +6,7 @@ from codemodder.codemods.utils import is_setup_py_file from codemodder.codemods.utils_mixin import NameResolutionMixin from codemodder.file_context import FileContext +from packaging.requirements import Requirement class SetupPyAddDependencies(BaseCodemod, NameResolutionMixin): @@ -16,7 +17,10 @@ class SetupPyAddDependencies(BaseCodemod, NameResolutionMixin): REFERENCES: list = [] def __init__( - self, codemod_context: CodemodContext, file_context: FileContext, dependencies + self, + codemod_context: CodemodContext, + file_context: FileContext, + dependencies: list[Requirement], ): BaseCodemod.__init__(self, codemod_context, file_context) NameResolutionMixin.__init__(self)