Skip to content

Commit c5ce089

Browse files
Implement provider registry (#633)
* Implement provider registry * Update src/codemodder/codemods/base_codemod.py Co-authored-by: Dani Alcala <[email protected]> --------- Co-authored-by: Dani Alcala <[email protected]>
1 parent 6667654 commit c5ce089

File tree

8 files changed

+102
-6
lines changed

8 files changed

+102
-6
lines changed

src/codemodder/codemodder.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pathlib import Path
77
from typing import DefaultDict, Sequence
88

9-
from codemodder import __version__, registry
9+
from codemodder import __version__, providers, registry
1010
from codemodder.cli import parse_args
1111
from codemodder.code_directory import match_files
1212
from codemodder.codemods.api import BaseCodemod
@@ -142,6 +142,7 @@ def run(original_args) -> int:
142142
start = datetime.datetime.now()
143143

144144
codemod_registry = registry.load_registered_codemods()
145+
provider_registry = providers.load_providers()
145146

146147
# A little awkward, but we need the codemod registry in order to validate potential arguments
147148
argv = parse_args(original_args, codemod_registry)
@@ -174,6 +175,7 @@ def run(original_args) -> int:
174175
argv.dry_run,
175176
argv.verbose,
176177
codemod_registry,
178+
provider_registry,
177179
repo_manager,
178180
argv.path_include,
179181
argv.path_exclude,

src/codemodder/codemods/base_codemod.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class BaseCodemod(metaclass=ABCMeta):
7878
detector: BaseDetector | None
7979
transformer: BaseTransformerPipeline
8080
default_extensions: list[str] | None
81+
provider: str | None
8182

8283
def __init__(
8384
self,
@@ -86,12 +87,14 @@ def __init__(
8687
detector: BaseDetector | None = None,
8788
transformer: BaseTransformerPipeline,
8889
default_extensions: list[str] | None = None,
90+
provider: str | None = None,
8991
):
9092
# Metadata should only be accessed via properties
9193
self._metadata = metadata
9294
self.detector = detector
9395
self.transformer = transformer
9496
self.default_extensions = default_extensions or [".py"]
97+
self.provider = provider
9598

9699
@property
97100
@abstractmethod
@@ -159,6 +162,15 @@ def _apply(
159162
files_to_analyze: list[Path],
160163
rules: list[str],
161164
) -> None:
165+
if self.provider and (
166+
not (provider := context.providers.get_provider(self.provider))
167+
or not provider.is_available
168+
):
169+
logger.warning(
170+
"provider %s is not available, skipping codemod", self.provider
171+
)
172+
return
173+
162174
results = (
163175
# It seems like semgrep doesn't like our fully-specified id format
164176
self.detector.apply(self.name, context, files_to_analyze)

src/codemodder/codemods/test/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from codemodder.context import CodemodExecutionContext
99
from codemodder.diff import create_diff
10+
from codemodder.providers import load_providers
1011
from codemodder.registry import CodemodCollection, CodemodRegistry
1112
from codemodder.semgrep import run as semgrep_run
1213

@@ -61,6 +62,7 @@ def run_and_assert(
6162
dry_run=False,
6263
verbose=False,
6364
registry=mock.MagicMock(),
65+
providers=load_providers(),
6466
repo_manager=mock.MagicMock(),
6567
path_include=[f.name for f in files_to_check],
6668
path_exclude=path_exclude,
@@ -184,6 +186,7 @@ def run_and_assert(
184186
verbose=False,
185187
tool_result_files_map={self.tool: [str(tmp_results_file_path)]},
186188
registry=mock.MagicMock(),
189+
providers=load_providers(),
187190
repo_manager=mock.MagicMock(),
188191
path_include=[f.name for f in files_to_check],
189192
path_exclude=path_exclude,

src/codemodder/context.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from codemodder.logging import log_list, logger
2020
from codemodder.project_analysis.file_parsers.package_store import PackageStore
2121
from codemodder.project_analysis.python_repo_manager import PythonRepoManager
22+
from codemodder.providers import ProviderRegistry
2223
from codemodder.registry import CodemodRegistry
2324
from codemodder.utils.timer import Timer
2425

@@ -37,6 +38,7 @@ class CodemodExecutionContext:
3738
dry_run: bool = False
3839
verbose: bool = False
3940
registry: CodemodRegistry
41+
providers: ProviderRegistry
4042
repo_manager: PythonRepoManager
4143
timer: Timer
4244
path_include: list[str]
@@ -51,6 +53,7 @@ def __init__(
5153
dry_run: bool,
5254
verbose: bool,
5355
registry: CodemodRegistry,
56+
providers: ProviderRegistry,
5457
repo_manager: PythonRepoManager,
5558
path_include: list[str],
5659
path_exclude: list[str],
@@ -66,6 +69,7 @@ def __init__(
6669
self._unfixed_findings_by_codemod = {}
6770
self.dependencies = {}
6871
self.registry = registry
72+
self.providers = providers
6973
self.repo_manager = repo_manager
7074
self.timer = Timer()
7175
self.path_include = path_include

src/codemodder/providers.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from abc import ABCMeta, abstractmethod
2+
from collections import UserDict
3+
from importlib.metadata import entry_points
4+
5+
from codemodder.logging import logger
6+
7+
8+
class BaseProvider(metaclass=ABCMeta):
9+
name: str
10+
_resource: str | None
11+
12+
def __init__(self, name):
13+
self.name = name
14+
self._resource = self.load()
15+
16+
@abstractmethod
17+
def load(self) -> str | None:
18+
pass
19+
20+
@property
21+
def is_available(self) -> bool:
22+
return self.resource is not None
23+
24+
@property
25+
def resource(self) -> str:
26+
if self._resource is None:
27+
raise ValueError(f"Resource for provider {self.name} is not available")
28+
return self._resource
29+
30+
31+
class ProviderRegistry(UserDict):
32+
def add_provider(self, name: str, provider: BaseProvider):
33+
self[name] = provider
34+
35+
def get_provider(self, name: str) -> BaseProvider | None:
36+
return self.get(name)
37+
38+
39+
def load_providers() -> ProviderRegistry:
40+
registry = ProviderRegistry()
41+
logger.debug("loading registered providers")
42+
for entry_point in entry_points().select(group="codemod_providers"):
43+
logger.debug(
44+
'- loading provider "%s" from "%s"',
45+
entry_point.name,
46+
entry_point.module,
47+
)
48+
try:
49+
provider = entry_point.load()
50+
except Exception:
51+
logger.exception(
52+
'Failed to load provider "%s" from "%s": %s',
53+
entry_point.name,
54+
entry_point.module,
55+
)
56+
continue
57+
58+
registry.add_provider(entry_point.name, provider(entry_point.name))
59+
60+
return registry

tests/codemods/test_xml_transformer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ def test_transformer_failure(mocker, caplog):
1414
side_effect=Exception,
1515
)
1616
file_context = FileContext(
17-
"home",
17+
Path("home"),
1818
Path("test.xml"),
1919
)
2020
execution_context = CodemodExecutionContext(
2121
directory=mocker.MagicMock(),
2222
dry_run=True,
2323
verbose=False,
2424
registry=mocker.MagicMock(),
25+
providers=None,
2526
repo_manager=mocker.MagicMock(),
2627
path_include=[],
2728
path_exclude=[],
@@ -53,6 +54,7 @@ def test_transformer(mocker):
5354
dry_run=True,
5455
verbose=False,
5556
registry=mocker.MagicMock(),
57+
providers=None,
5658
repo_manager=mocker.MagicMock(),
5759
path_include=[],
5860
path_exclude=[],

tests/test_context.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ def test_successful_dependency_description(self, mocker):
2828
repo_manager = PythonRepoManager(mocker.Mock())
2929
codemod = registry.match_codemods(codemod_include=["url-sandbox"])[0]
3030

31-
context = Context(mocker.Mock(), True, False, registry, repo_manager, [], [])
31+
context = Context(
32+
mocker.Mock(), True, False, registry, None, repo_manager, [], []
33+
)
3234
context.add_dependencies(codemod.id, {Security})
3335

3436
pkg_store_name = "pyproject.toml"
@@ -57,7 +59,9 @@ def test_failed_dependency_description(self, mocker):
5759
repo_manager = PythonRepoManager(mocker.Mock())
5860
codemod = registry.match_codemods(codemod_include=["url-sandbox"])[0]
5961

60-
context = Context(mocker.Mock(), True, False, registry, repo_manager, [], [])
62+
context = Context(
63+
mocker.Mock(), True, False, registry, None, repo_manager, [], []
64+
)
6165
context.add_dependencies(codemod.id, {Security})
6266

6367
mocker.patch(
@@ -89,6 +93,7 @@ def test_setup_llm_client_no_env_vars(self, mocker):
8993
True,
9094
False,
9195
load_registered_codemods(),
96+
None,
9297
PythonRepoManager(mocker.Mock()),
9398
[],
9499
[],
@@ -102,6 +107,7 @@ def test_setup_openai_llm_client(self, mocker):
102107
True,
103108
False,
104109
load_registered_codemods(),
110+
None,
105111
PythonRepoManager(mocker.Mock()),
106112
[],
107113
[],
@@ -121,6 +127,7 @@ def test_setup_azure_llm_client(self, mocker):
121127
True,
122128
False,
123129
load_registered_codemods(),
130+
None,
124131
PythonRepoManager(mocker.Mock()),
125132
[],
126133
[],
@@ -140,6 +147,7 @@ def test_setup_azure_llm_client_missing_one(self, mocker, env_var):
140147
True,
141148
False,
142149
load_registered_codemods(),
150+
None,
143151
PythonRepoManager(mocker.Mock()),
144152
[],
145153
[],
@@ -160,6 +168,7 @@ def test_get_api_version_from_env(self, mocker):
160168
True,
161169
False,
162170
load_registered_codemods(),
171+
None,
163172
PythonRepoManager(mocker.Mock()),
164173
[],
165174
[],

tests/test_libcst_transformer.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from pathlib import Path
2+
13
import mock
24
from libcst._exceptions import ParserSyntaxError
35

@@ -55,12 +57,13 @@
5557

5658

5759
def _apply_and_assert(mocker, transformer):
58-
file_context = FileContext("home", FILE_PATH)
60+
file_context = FileContext(Path("home"), FILE_PATH)
5961
execution_context = CodemodExecutionContext(
6062
directory=mocker.MagicMock(),
6163
dry_run=True,
6264
verbose=False,
6365
registry=mocker.MagicMock(),
66+
providers=None,
6467
repo_manager=mocker.MagicMock(),
6568
path_include=[],
6669
path_exclude=[],
@@ -77,7 +80,7 @@ def _apply_and_assert(mocker, transformer):
7780

7881
def _apply_and_assert_with_tool(mocker, transformer, reason, results):
7982
file_context = FileContext(
80-
"home",
83+
Path("home"),
8184
FILE_PATH,
8285
results=results,
8386
)
@@ -86,6 +89,7 @@ def _apply_and_assert_with_tool(mocker, transformer, reason, results):
8689
dry_run=True,
8790
verbose=False,
8891
registry=mocker.MagicMock(),
92+
providers=None,
8993
repo_manager=mocker.MagicMock(),
9094
path_include=[],
9195
path_exclude=[],

0 commit comments

Comments
 (0)