selenium-selector-autocorrect 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {selenium_selector_autocorrect-0.1.0/src/selenium_selector_autocorrect.egg-info → selenium_selector_autocorrect-0.1.1}/PKG-INFO +1 -1
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/pyproject.toml +5 -4
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/src/selenium_selector_autocorrect/__init__.py +1 -1
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/src/selenium_selector_autocorrect/ai_providers.py +9 -8
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/src/selenium_selector_autocorrect/auto_correct.py +39 -19
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/src/selenium_selector_autocorrect/correction_tracker.py +44 -19
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/src/selenium_selector_autocorrect/wait_hook.py +50 -19
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1/src/selenium_selector_autocorrect.egg-info}/PKG-INFO +1 -1
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/src/selenium_selector_autocorrect.egg-info/SOURCES.txt +0 -1
- selenium_selector_autocorrect-0.1.0/src/selenium_selector_autocorrect/py.typed +0 -1
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/LICENSE +0 -0
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/README.md +0 -0
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/setup.cfg +0 -0
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/src/selenium_selector_autocorrect.egg-info/dependency_links.txt +0 -0
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/src/selenium_selector_autocorrect.egg-info/requires.txt +0 -0
- {selenium_selector_autocorrect-0.1.0 → selenium_selector_autocorrect-0.1.1}/src/selenium_selector_autocorrect.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: selenium-selector-autocorrect
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Automatic Selenium selector correction using AI when elements fail to be found
|
|
5
5
|
Author-email: Marty Zhou <marty.zhou@example.com>
|
|
6
6
|
Maintainer-email: Marty Zhou <marty.zhou@example.com>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "selenium-selector-autocorrect"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "Automatic Selenium selector correction using AI when elements fail to be found"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -80,11 +80,12 @@ line_length = 120
|
|
|
80
80
|
src_paths = ["src"]
|
|
81
81
|
|
|
82
82
|
[tool.mypy]
|
|
83
|
-
python_version = "3.
|
|
83
|
+
python_version = "3.9"
|
|
84
84
|
warn_return_any = true
|
|
85
85
|
warn_unused_configs = true
|
|
86
|
-
disallow_untyped_defs =
|
|
87
|
-
|
|
86
|
+
disallow_untyped_defs = true
|
|
87
|
+
strict = true
|
|
88
|
+
ignore_missing_imports = false
|
|
88
89
|
|
|
89
90
|
[tool.ruff]
|
|
90
91
|
line-length = 120
|
|
@@ -15,7 +15,7 @@ Environment Variables:
|
|
|
15
15
|
SELENIUM_AUTO_UPDATE_TESTS: Auto-update test files with corrections (default: "0")
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
__version__ = "1.
|
|
18
|
+
__version__ = "0.1.1"
|
|
19
19
|
|
|
20
20
|
from .ai_providers import AIProvider, LocalAIProvider, configure_provider, get_provider
|
|
21
21
|
from .auto_correct import (
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
9
|
|
|
@@ -40,9 +40,9 @@ class LocalAIProvider(AIProvider):
|
|
|
40
40
|
LOCAL_AI_API_URL environment variable (default: http://localhost:8765)
|
|
41
41
|
"""
|
|
42
42
|
|
|
43
|
-
def __init__(self, base_url: str = None):
|
|
44
|
-
self.base_url = base_url or os.environ.get("LOCAL_AI_API_URL", "http://localhost:8765")
|
|
45
|
-
self._available = None
|
|
43
|
+
def __init__(self, base_url: Optional[str] = None) -> None:
|
|
44
|
+
self.base_url: str = base_url or os.environ.get("LOCAL_AI_API_URL", "http://localhost:8765")
|
|
45
|
+
self._available: Optional[bool] = None
|
|
46
46
|
|
|
47
47
|
def is_available(self) -> bool:
|
|
48
48
|
if self._available is not None:
|
|
@@ -83,8 +83,9 @@ class LocalAIProvider(AIProvider):
|
|
|
83
83
|
timeout=30
|
|
84
84
|
)
|
|
85
85
|
response.raise_for_status()
|
|
86
|
-
data = response.json()
|
|
87
|
-
|
|
86
|
+
data: Dict[str, Any] = response.json()
|
|
87
|
+
content: Optional[str] = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
88
|
+
return content
|
|
88
89
|
except requests.exceptions.HTTPError as e:
|
|
89
90
|
if e.response.status_code == 503:
|
|
90
91
|
logger.info(f"Local AI service unavailable (503). Disabling auto-correction.")
|
|
@@ -110,7 +111,7 @@ def get_provider() -> AIProvider:
|
|
|
110
111
|
return _provider_instance
|
|
111
112
|
|
|
112
113
|
|
|
113
|
-
def configure_provider(provider: AIProvider):
|
|
114
|
+
def configure_provider(provider: AIProvider) -> None:
|
|
114
115
|
"""Set a custom AI provider.
|
|
115
116
|
|
|
116
117
|
Args:
|
|
@@ -120,4 +121,4 @@ def configure_provider(provider: AIProvider):
|
|
|
120
121
|
_provider_instance = provider
|
|
121
122
|
|
|
122
123
|
|
|
123
|
-
_provider_instance = None
|
|
124
|
+
_provider_instance: Optional[AIProvider] = None
|
|
@@ -4,9 +4,13 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
-
from typing import Any, Dict, Optional, Tuple
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
|
|
8
8
|
|
|
9
|
-
from .ai_providers import get_provider
|
|
9
|
+
from .ai_providers import AIProvider, get_provider
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
|
13
|
+
from selenium.webdriver.remote.webelement import WebElement
|
|
10
14
|
|
|
11
15
|
logger = logging.getLogger(__name__)
|
|
12
16
|
|
|
@@ -18,22 +22,22 @@ class SelectorAutoCorrect:
|
|
|
18
22
|
enabled: Whether auto-correction is enabled
|
|
19
23
|
"""
|
|
20
24
|
|
|
21
|
-
def __init__(self, enabled: bool = True):
|
|
22
|
-
self.enabled = enabled
|
|
23
|
-
self._provider = None
|
|
25
|
+
def __init__(self, enabled: bool = True) -> None:
|
|
26
|
+
self.enabled: bool = enabled
|
|
27
|
+
self._provider: Optional[AIProvider] = None
|
|
24
28
|
self._correction_cache: Dict[str, str] = {}
|
|
25
29
|
self._suggestion_cache: Dict[str, str] = {}
|
|
26
|
-
self.suggest_better_selectors = os.environ.get("SELENIUM_SUGGEST_BETTER", "0").lower() in ("1", "true", "yes")
|
|
27
|
-
self._confidence_threshold = 50
|
|
28
|
-
self._cache_enabled = True
|
|
30
|
+
self.suggest_better_selectors: bool = os.environ.get("SELENIUM_SUGGEST_BETTER", "0").lower() in ("1", "true", "yes")
|
|
31
|
+
self._confidence_threshold: int = 50
|
|
32
|
+
self._cache_enabled: bool = True
|
|
29
33
|
|
|
30
34
|
@property
|
|
31
|
-
def provider(self):
|
|
35
|
+
def provider(self) -> AIProvider:
|
|
32
36
|
if self._provider is None:
|
|
33
37
|
self._provider = get_provider()
|
|
34
38
|
return self._provider
|
|
35
39
|
|
|
36
|
-
def set_provider(self, provider) -> None:
|
|
40
|
+
def set_provider(self, provider: AIProvider) -> None:
|
|
37
41
|
self._provider = provider
|
|
38
42
|
|
|
39
43
|
def is_service_available(self) -> bool:
|
|
@@ -42,7 +46,7 @@ class SelectorAutoCorrect:
|
|
|
42
46
|
provider = self.provider
|
|
43
47
|
return provider is not None and provider.is_available()
|
|
44
48
|
|
|
45
|
-
def get_visible_elements_summary(self, driver) -> str:
|
|
49
|
+
def get_visible_elements_summary(self, driver: "WebDriver") -> str:
|
|
46
50
|
try:
|
|
47
51
|
script = """
|
|
48
52
|
function getElementSummary() {
|
|
@@ -86,7 +90,13 @@ class SelectorAutoCorrect:
|
|
|
86
90
|
logger.warning(f"Failed to get element summary: {e}")
|
|
87
91
|
return "[]"
|
|
88
92
|
|
|
89
|
-
def suggest_selector(
|
|
93
|
+
def suggest_selector(
|
|
94
|
+
self,
|
|
95
|
+
driver: "WebDriver",
|
|
96
|
+
failed_by: str,
|
|
97
|
+
failed_value: str,
|
|
98
|
+
error_message: str = ""
|
|
99
|
+
) -> Optional[Tuple[str, str]]:
|
|
90
100
|
if not self.enabled or not self.is_service_available():
|
|
91
101
|
return None
|
|
92
102
|
|
|
@@ -139,7 +149,13 @@ Suggest a working selector. Respond with ONLY a JSON object."""
|
|
|
139
149
|
logger.warning(f"[AUTO-CORRECT] Failed to get suggestion: {e}")
|
|
140
150
|
return None
|
|
141
151
|
|
|
142
|
-
def suggest_better_selector(
|
|
152
|
+
def suggest_better_selector(
|
|
153
|
+
self,
|
|
154
|
+
driver: "WebDriver",
|
|
155
|
+
current_by: str,
|
|
156
|
+
current_value: str,
|
|
157
|
+
element: "WebElement"
|
|
158
|
+
) -> Optional[Tuple[str, str]]:
|
|
143
159
|
if not self.suggest_better_selectors or not self.enabled or not self.is_service_available():
|
|
144
160
|
return None
|
|
145
161
|
|
|
@@ -182,12 +198,12 @@ Available Elements:
|
|
|
182
198
|
if self._cache_enabled:
|
|
183
199
|
self._suggestion_cache[cache_key] = "OPTIMAL"
|
|
184
200
|
except Exception as e:
|
|
185
|
-
logger.debug(f"[SUGGEST] Failed to get better selector: {e}")
|
|
201
|
+
logger.debug(f"[AUTO-SUGGEST] Failed to get better selector: {e}")
|
|
186
202
|
return None
|
|
187
203
|
|
|
188
|
-
def _get_element_info(self, element) -> Dict[str, Any]:
|
|
204
|
+
def _get_element_info(self, element: "WebElement") -> Dict[str, Any]:
|
|
189
205
|
try:
|
|
190
|
-
info = {
|
|
206
|
+
info: Dict[str, Any] = {
|
|
191
207
|
"tag": element.tag_name,
|
|
192
208
|
"id": element.get_attribute("id"),
|
|
193
209
|
"name": element.get_attribute("name"),
|
|
@@ -223,7 +239,7 @@ Available Elements:
|
|
|
223
239
|
logger.warning(f"[AUTO-CORRECT] Error parsing suggestion: {e}")
|
|
224
240
|
return None
|
|
225
241
|
|
|
226
|
-
def clear_cache(self):
|
|
242
|
+
def clear_cache(self) -> None:
|
|
227
243
|
self._correction_cache.clear()
|
|
228
244
|
self._suggestion_cache.clear()
|
|
229
245
|
|
|
@@ -240,12 +256,16 @@ def get_auto_correct() -> SelectorAutoCorrect:
|
|
|
240
256
|
return _auto_correct_instance
|
|
241
257
|
|
|
242
258
|
|
|
243
|
-
def set_auto_correct_enabled(enabled: bool):
|
|
259
|
+
def set_auto_correct_enabled(enabled: bool) -> None:
|
|
244
260
|
"""Enable or disable auto-correction globally."""
|
|
245
261
|
get_auto_correct().enabled = enabled
|
|
246
262
|
|
|
247
263
|
|
|
248
|
-
def configure_auto_correct(
|
|
264
|
+
def configure_auto_correct(
|
|
265
|
+
provider: Optional[AIProvider] = None,
|
|
266
|
+
enabled: bool = True,
|
|
267
|
+
suggest_better: bool = False
|
|
268
|
+
) -> None:
|
|
249
269
|
"""Configure auto-correction settings."""
|
|
250
270
|
auto_correct = get_auto_correct()
|
|
251
271
|
auto_correct.enabled = enabled
|
|
@@ -5,20 +5,40 @@ import logging
|
|
|
5
5
|
import os
|
|
6
6
|
import traceback
|
|
7
7
|
from datetime import datetime
|
|
8
|
-
from typing import Any, Dict, List, Optional
|
|
8
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
9
9
|
|
|
10
10
|
import requests
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
class CorrectionRecord(TypedDict, total=False):
|
|
16
|
+
"""Type definition for a correction record."""
|
|
17
|
+
original_by: str
|
|
18
|
+
original_value: str
|
|
19
|
+
corrected_by: str
|
|
20
|
+
corrected_value: str
|
|
21
|
+
success: bool
|
|
22
|
+
test_file: Optional[str]
|
|
23
|
+
test_line: Optional[int]
|
|
24
|
+
timestamp: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ApplyCorrectionsResult(TypedDict):
|
|
28
|
+
"""Type definition for apply_all_corrections result."""
|
|
29
|
+
total: int
|
|
30
|
+
success: int
|
|
31
|
+
failed: int
|
|
32
|
+
details: List[Dict[str, Any]]
|
|
33
|
+
|
|
34
|
+
|
|
15
35
|
class CorrectionTracker:
|
|
16
36
|
"""Tracks selector corrections and manages test file updates."""
|
|
17
37
|
|
|
18
|
-
def __init__(self):
|
|
19
|
-
self._corrections: List[
|
|
20
|
-
self._local_ai_url = os.environ.get("LOCAL_AI_API_URL", "http://localhost:8765")
|
|
21
|
-
self._auto_update_enabled = os.environ.get("SELENIUM_AUTO_UPDATE_TESTS", "0").lower() in ("1", "true", "yes")
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self._corrections: List[CorrectionRecord] = []
|
|
40
|
+
self._local_ai_url: str = os.environ.get("LOCAL_AI_API_URL", "http://localhost:8765")
|
|
41
|
+
self._auto_update_enabled: bool = os.environ.get("SELENIUM_AUTO_UPDATE_TESTS", "0").lower() in ("1", "true", "yes")
|
|
22
42
|
|
|
23
43
|
def record_correction(
|
|
24
44
|
self,
|
|
@@ -29,7 +49,7 @@ class CorrectionTracker:
|
|
|
29
49
|
success: bool = True,
|
|
30
50
|
test_file: Optional[str] = None,
|
|
31
51
|
test_line: Optional[int] = None
|
|
32
|
-
):
|
|
52
|
+
) -> None:
|
|
33
53
|
if test_file is None or test_line is None:
|
|
34
54
|
# Extract from stack trace, prioritizing actual test files
|
|
35
55
|
for frame in traceback.extract_stack():
|
|
@@ -56,7 +76,7 @@ class CorrectionTracker:
|
|
|
56
76
|
if 'test_' in filename or 'test_library' in filename:
|
|
57
77
|
break
|
|
58
78
|
|
|
59
|
-
correction = {
|
|
79
|
+
correction: CorrectionRecord = {
|
|
60
80
|
"original_by": original_by,
|
|
61
81
|
"original_value": original_value,
|
|
62
82
|
"corrected_by": corrected_by,
|
|
@@ -79,16 +99,16 @@ class CorrectionTracker:
|
|
|
79
99
|
logger.info(f"[AUTO-UPDATE] Attempting to update {test_file}...")
|
|
80
100
|
self._auto_update_test_file(correction)
|
|
81
101
|
|
|
82
|
-
def get_corrections(self) -> List[
|
|
102
|
+
def get_corrections(self) -> List[CorrectionRecord]:
|
|
83
103
|
return self._corrections.copy()
|
|
84
104
|
|
|
85
|
-
def get_successful_corrections(self) -> List[
|
|
105
|
+
def get_successful_corrections(self) -> List[CorrectionRecord]:
|
|
86
106
|
return [c for c in self._corrections if c.get("success", False)]
|
|
87
107
|
|
|
88
|
-
def clear_corrections(self):
|
|
108
|
+
def clear_corrections(self) -> None:
|
|
89
109
|
self._corrections.clear()
|
|
90
110
|
|
|
91
|
-
def _auto_update_test_file(self, correction:
|
|
111
|
+
def _auto_update_test_file(self, correction: CorrectionRecord) -> None:
|
|
92
112
|
try:
|
|
93
113
|
test_file = correction.get("test_file")
|
|
94
114
|
if not test_file:
|
|
@@ -148,7 +168,8 @@ class CorrectionTracker:
|
|
|
148
168
|
timeout=30
|
|
149
169
|
)
|
|
150
170
|
edit_response.raise_for_status()
|
|
151
|
-
|
|
171
|
+
result: Dict[str, Any] = edit_response.json()
|
|
172
|
+
return result
|
|
152
173
|
except requests.exceptions.ConnectionError:
|
|
153
174
|
logger.warning(f"[LOCAL AI SERVICE] Not available at {self._local_ai_url}")
|
|
154
175
|
return {"success": False, "errors": ["Local AI service not available"]}
|
|
@@ -156,7 +177,7 @@ class CorrectionTracker:
|
|
|
156
177
|
logger.warning(f"[UPDATE ERROR] {e}")
|
|
157
178
|
return {"success": False, "errors": [str(e)]}
|
|
158
179
|
|
|
159
|
-
def export_corrections_report(self, output_file: str = "selector_corrections.json"):
|
|
180
|
+
def export_corrections_report(self, output_file: str = "selector_corrections.json") -> None:
|
|
160
181
|
with open(output_file, "w") as f:
|
|
161
182
|
json.dump({
|
|
162
183
|
"corrections": self._corrections,
|
|
@@ -168,8 +189,8 @@ class CorrectionTracker:
|
|
|
168
189
|
}, f, indent=2)
|
|
169
190
|
logger.info(f"[CORRECTIONS REPORT] Exported to {output_file}")
|
|
170
191
|
|
|
171
|
-
def apply_all_corrections_to_files(self) ->
|
|
172
|
-
results = {"total": 0, "success": 0, "failed": 0, "details": []}
|
|
192
|
+
def apply_all_corrections_to_files(self) -> ApplyCorrectionsResult:
|
|
193
|
+
results: ApplyCorrectionsResult = {"total": 0, "success": 0, "failed": 0, "details": []}
|
|
173
194
|
for correction in self.get_successful_corrections():
|
|
174
195
|
test_file = correction.get("test_file")
|
|
175
196
|
if not test_file:
|
|
@@ -208,19 +229,23 @@ def get_correction_tracker() -> CorrectionTracker:
|
|
|
208
229
|
|
|
209
230
|
|
|
210
231
|
def record_correction(
|
|
211
|
-
original_by: str,
|
|
212
|
-
|
|
232
|
+
original_by: str,
|
|
233
|
+
original_value: str,
|
|
234
|
+
corrected_by: str,
|
|
235
|
+
corrected_value: str,
|
|
236
|
+
success: bool = True
|
|
237
|
+
) -> None:
|
|
213
238
|
"""Record a selector correction."""
|
|
214
239
|
get_correction_tracker().record_correction(
|
|
215
240
|
original_by, original_value, corrected_by, corrected_value, success
|
|
216
241
|
)
|
|
217
242
|
|
|
218
243
|
|
|
219
|
-
def apply_corrections_to_test_files() ->
|
|
244
|
+
def apply_corrections_to_test_files() -> ApplyCorrectionsResult:
|
|
220
245
|
"""Apply all successful corrections to their source test files."""
|
|
221
246
|
return get_correction_tracker().apply_all_corrections_to_files()
|
|
222
247
|
|
|
223
248
|
|
|
224
|
-
def export_corrections_report(output_file: str = "selector_corrections.json"):
|
|
249
|
+
def export_corrections_report(output_file: str = "selector_corrections.json") -> None:
|
|
225
250
|
"""Export corrections report to JSON file."""
|
|
226
251
|
get_correction_tracker().export_corrections_report(output_file)
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import time
|
|
5
|
-
from typing import Callable, Optional
|
|
5
|
+
from typing import Any, Callable, Optional, Tuple, TypeVar, Union, cast
|
|
6
6
|
|
|
7
7
|
from selenium.common.exceptions import TimeoutException
|
|
8
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
|
8
9
|
from selenium.webdriver.remote.webelement import WebElement
|
|
9
10
|
from selenium.webdriver.support.wait import WebDriverWait
|
|
10
11
|
|
|
@@ -13,19 +14,41 @@ from .correction_tracker import record_correction
|
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
DriverType = Union[WebDriver, WebElement]
|
|
17
19
|
|
|
20
|
+
_original_until: Callable[..., Any] = WebDriverWait.until
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
|
|
23
|
+
def _patched_until(self: WebDriverWait, method: Callable[[WebDriver], T], message: str = "") -> T:
|
|
20
24
|
"""Patched until method with auto-correct support."""
|
|
21
|
-
screen = None
|
|
22
|
-
stacktrace = None
|
|
25
|
+
screen: Optional[str] = None
|
|
26
|
+
stacktrace: Optional[str] = None
|
|
23
27
|
|
|
24
28
|
end_time = time.monotonic() + self._timeout
|
|
25
29
|
while True:
|
|
26
30
|
try:
|
|
27
31
|
value = method(self._driver)
|
|
28
32
|
if value:
|
|
33
|
+
# Check if we should suggest a better selector for the found element
|
|
34
|
+
auto_correct = get_auto_correct()
|
|
35
|
+
if auto_correct.suggest_better_selectors and isinstance(value, WebElement):
|
|
36
|
+
locator = _extract_locator_from_method(method)
|
|
37
|
+
if locator:
|
|
38
|
+
by, value_str = locator
|
|
39
|
+
suggest_driver: WebDriver
|
|
40
|
+
if isinstance(self._driver, WebElement):
|
|
41
|
+
suggest_driver = self._driver.parent # type: ignore[attr-defined]
|
|
42
|
+
else:
|
|
43
|
+
suggest_driver = self._driver
|
|
44
|
+
|
|
45
|
+
better_suggestion = auto_correct.suggest_better_selector(
|
|
46
|
+
suggest_driver, by, value_str, value
|
|
47
|
+
)
|
|
48
|
+
if better_suggestion:
|
|
49
|
+
better_by, better_value = better_suggestion
|
|
50
|
+
logger.info(f"[AUTO-SUGGEST] Found element with {by}='{value_str[:50]}...'")
|
|
51
|
+
logger.info(f"[AUTO-SUGGEST] Suggested better selector: {better_by}='{better_value}'")
|
|
29
52
|
return value
|
|
30
53
|
except self._ignored_exceptions as exc:
|
|
31
54
|
screen = getattr(exc, "screen", None)
|
|
@@ -43,9 +66,11 @@ def _patched_until(self, method: Callable, message: str = ""):
|
|
|
43
66
|
f"[AUTO-CORRECT] Timeout waiting for element {by}='{value_str[:80]}...' - attempting auto-correction"
|
|
44
67
|
)
|
|
45
68
|
|
|
46
|
-
driver
|
|
47
|
-
if isinstance(
|
|
48
|
-
driver =
|
|
69
|
+
driver: WebDriver
|
|
70
|
+
if isinstance(self._driver, WebElement):
|
|
71
|
+
driver = self._driver.parent # type: ignore[attr-defined]
|
|
72
|
+
else:
|
|
73
|
+
driver = self._driver
|
|
49
74
|
|
|
50
75
|
suggestion = auto_correct.suggest_selector(
|
|
51
76
|
driver,
|
|
@@ -71,35 +96,41 @@ def _patched_until(self, method: Callable, message: str = ""):
|
|
|
71
96
|
corrected_value=suggested_value,
|
|
72
97
|
success=True,
|
|
73
98
|
)
|
|
74
|
-
return result
|
|
99
|
+
return cast(T, result)
|
|
75
100
|
except Exception as e:
|
|
76
101
|
logger.warning(f"[AUTO-CORRECT] Suggested selector also failed: {e}")
|
|
77
102
|
|
|
78
103
|
raise TimeoutException(message, screen, stacktrace)
|
|
79
104
|
|
|
80
105
|
|
|
81
|
-
def _extract_locator_from_method(method: Callable) -> Optional[
|
|
106
|
+
def _extract_locator_from_method(method: Callable[..., Any]) -> Optional[Tuple[str, str]]:
|
|
82
107
|
"""Extract locator tuple (by, value) from an expected_conditions method."""
|
|
83
108
|
try:
|
|
84
109
|
if hasattr(method, "locator"):
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
locator: Tuple[str, str] = method.locator
|
|
111
|
+
logger.debug(f"[AUTO-CORRECT] Found locator attribute: {locator}")
|
|
112
|
+
return locator
|
|
87
113
|
|
|
88
114
|
if hasattr(method, "__closure__") and method.__closure__:
|
|
89
115
|
for cell in method.__closure__:
|
|
90
116
|
cell_contents = cell.cell_contents
|
|
91
117
|
logger.debug(f"[AUTO-CORRECT] Checking closure cell: {type(cell_contents)} = {cell_contents}")
|
|
92
118
|
if isinstance(cell_contents, tuple) and len(cell_contents) == 2:
|
|
93
|
-
|
|
119
|
+
first, second = cell_contents
|
|
120
|
+
if isinstance(first, str) and isinstance(second, str):
|
|
94
121
|
logger.debug(f"[AUTO-CORRECT] Extracted locator from closure: {cell_contents}")
|
|
95
|
-
return
|
|
122
|
+
return (first, second)
|
|
96
123
|
logger.warning(f"[AUTO-CORRECT] Could not extract locator from method: {method}")
|
|
97
124
|
except Exception as e:
|
|
98
125
|
logger.exception(f"[AUTO-CORRECT] Error extracting locator: {e}")
|
|
99
126
|
return None
|
|
100
127
|
|
|
101
128
|
|
|
102
|
-
def _create_corrected_method(
|
|
129
|
+
def _create_corrected_method(
|
|
130
|
+
original_method: Callable[..., Any],
|
|
131
|
+
new_by: str,
|
|
132
|
+
new_value: str
|
|
133
|
+
) -> Optional[Callable[[WebDriver], Any]]:
|
|
103
134
|
"""Create a new expected condition method with corrected locator."""
|
|
104
135
|
try:
|
|
105
136
|
from selenium.webdriver.support import expected_conditions as EC
|
|
@@ -128,13 +159,13 @@ def _create_corrected_method(original_method: Callable, new_by: str, new_value:
|
|
|
128
159
|
return None
|
|
129
160
|
|
|
130
161
|
|
|
131
|
-
def install_auto_correct_hook():
|
|
162
|
+
def install_auto_correct_hook() -> None:
|
|
132
163
|
"""Install the auto-correct hook into WebDriverWait."""
|
|
133
|
-
WebDriverWait.until = _patched_until
|
|
164
|
+
WebDriverWait.until = _patched_until # type: ignore[method-assign,assignment]
|
|
134
165
|
logger.info("[AUTO-CORRECT] Hook installed into WebDriverWait")
|
|
135
166
|
|
|
136
167
|
|
|
137
|
-
def uninstall_auto_correct_hook():
|
|
168
|
+
def uninstall_auto_correct_hook() -> None:
|
|
138
169
|
"""Remove the auto-correct hook from WebDriverWait."""
|
|
139
|
-
WebDriverWait.until = _original_until
|
|
170
|
+
WebDriverWait.until = _original_until # type: ignore[method-assign]
|
|
140
171
|
logger.info("[AUTO-CORRECT] Hook removed from WebDriverWait")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: selenium-selector-autocorrect
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Automatic Selenium selector correction using AI when elements fail to be found
|
|
5
5
|
Author-email: Marty Zhou <marty.zhou@example.com>
|
|
6
6
|
Maintainer-email: Marty Zhou <marty.zhou@example.com>
|
|
@@ -5,7 +5,6 @@ src/selenium_selector_autocorrect/__init__.py
|
|
|
5
5
|
src/selenium_selector_autocorrect/ai_providers.py
|
|
6
6
|
src/selenium_selector_autocorrect/auto_correct.py
|
|
7
7
|
src/selenium_selector_autocorrect/correction_tracker.py
|
|
8
|
-
src/selenium_selector_autocorrect/py.typed
|
|
9
8
|
src/selenium_selector_autocorrect/wait_hook.py
|
|
10
9
|
src/selenium_selector_autocorrect.egg-info/PKG-INFO
|
|
11
10
|
src/selenium_selector_autocorrect.egg-info/SOURCES.txt
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Type hints marker for PEP 561
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|