drupal-scout 3.0.2__tar.gz → 3.0.3__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.
- {drupal_scout-3.0.2/drupal_scout.egg-info → drupal_scout-3.0.3}/PKG-INFO +1 -1
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_worker.py +40 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/worker.py +83 -15
- {drupal_scout-3.0.2 → drupal_scout-3.0.3/drupal_scout.egg-info}/PKG-INFO +1 -1
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/pyproject.toml +1 -1
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/LICENSE.txt +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/README.md +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/bin/drupal-scout +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/__init__.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/application.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/exceptions.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/__init__.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/formatter.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/formatterfactory.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/jsonformatter.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/suggestformatter.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/tableformatter.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/mcp_server.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/module.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/output.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/__init__.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/conftest.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_application.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_exceptions.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_formatterfactory.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_jsonformatter.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_mcp_server.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_suggestformatter.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_tableformatter.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_workers_manager.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/workers_manager.py +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout.egg-info/SOURCES.txt +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout.egg-info/dependency_links.txt +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout.egg-info/entry_points.txt +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout.egg-info/requires.txt +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout.egg-info/top_level.txt +0 -0
- {drupal_scout-3.0.2 → drupal_scout-3.0.3}/setup.cfg +0 -0
|
@@ -161,6 +161,46 @@ class TestWorker(TestCase):
|
|
|
161
161
|
# Should find no entries for Drupal 11
|
|
162
162
|
self.assertEqual(len(suitable), 0)
|
|
163
163
|
|
|
164
|
+
def test_find_suitable_entries_complex_specifiers(self):
|
|
165
|
+
"""
|
|
166
|
+
Test finding suitable entries with complex requirements like ^10.2 || <10.5, 10.2<10.5, >=9.5 <11.
|
|
167
|
+
"""
|
|
168
|
+
module = Module(name='drupal/test_module')
|
|
169
|
+
worker = Worker(module=module, current_core='10.6.3')
|
|
170
|
+
|
|
171
|
+
transitive_entries = [
|
|
172
|
+
{'version': '1.0.0', 'requirement': '10.2<10.5', 'requirement_parts': ['10.2<10.5']},
|
|
173
|
+
{'version': '2.0.0', 'requirement': '^10.2 || <10.5', 'requirement_parts': ['^10.2', '<10.5']},
|
|
174
|
+
{'version': '3.0.0', 'requirement': '>=9.5 <11', 'requirement_parts': ['>=9.5 <11']},
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
suitable = worker.find_suitable_entries(transitive_entries)
|
|
178
|
+
|
|
179
|
+
# 10.6.3 satisfies ^10.2 (>=10.2) and >=9.5 <11, but not 10.2<10.5 (<10.5)
|
|
180
|
+
self.assertEqual(len(suitable), 2)
|
|
181
|
+
self.assertEqual(suitable[0]['version'], '2.0.0')
|
|
182
|
+
self.assertEqual(suitable[1]['version'], '3.0.0')
|
|
183
|
+
|
|
184
|
+
def test_find_suitable_entries_unparseable_requirement_fallback(self):
|
|
185
|
+
"""
|
|
186
|
+
Test that unparseable or malformed requirement strings log warnings and do not crash the worker.
|
|
187
|
+
"""
|
|
188
|
+
module = Module(name='drupal/test_module')
|
|
189
|
+
worker = Worker(module=module, current_core='10.6.3')
|
|
190
|
+
|
|
191
|
+
transitive_entries = [
|
|
192
|
+
{'version': '1.0.0', 'requirement': 'invalid_specifier_!@#', 'requirement_parts': ['invalid_specifier_!@#']},
|
|
193
|
+
{'version': '2.0.0', 'requirement': '10.x.x', 'requirement_parts': ['10.x.x']},
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
suitable = worker.find_suitable_entries(transitive_entries)
|
|
197
|
+
# Should gracefully evaluate without raising an exception
|
|
198
|
+
self.assertTrue(isinstance(suitable, list))
|
|
199
|
+
# '10.x.x' should match via major-version fallback (major 10 == current core major 10)
|
|
200
|
+
# 'invalid_specifier_!@#' has no digits to extract, so it should not match
|
|
201
|
+
self.assertEqual(len(suitable), 1)
|
|
202
|
+
self.assertEqual(suitable[0]['version'], '2.0.0')
|
|
203
|
+
|
|
164
204
|
|
|
165
205
|
@pytest.mark.asyncio
|
|
166
206
|
async def test_get_retries_on_connection_error():
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
import re
|
|
3
4
|
|
|
4
5
|
import aiohttp
|
|
5
6
|
import jq
|
|
6
7
|
from packaging import version
|
|
8
|
+
from packaging.specifiers import SpecifierSet, InvalidSpecifier
|
|
9
|
+
from packaging.version import InvalidVersion
|
|
7
10
|
from .exceptions import ModuleNotFoundException
|
|
8
11
|
from .module import Module
|
|
9
12
|
|
|
@@ -123,13 +126,74 @@ class Worker:
|
|
|
123
126
|
'.packages."' + self.module.name + '" | .[] | select(.require != null) | {"version", '
|
|
124
127
|
'"requirement":.require."drupal/core"}').input(response_contents).all()
|
|
125
128
|
for entry in entries:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
req_str = entry.get('requirement', '')
|
|
130
|
+
if req_str and "|" in req_str:
|
|
131
|
+
req_clean = req_str.replace("^", "").replace(" ", "")
|
|
132
|
+
parts = [p.strip() for p in re.split(r'\|+', req_clean) if p.strip()]
|
|
133
|
+
entry["requirement_parts"] = parts
|
|
134
|
+
entry['requirement'] = " || ".join(parts)
|
|
130
135
|
transitive_entries.append(entry)
|
|
131
136
|
return transitive_entries
|
|
132
137
|
|
|
138
|
+
def _is_clause_satisfied(self, clause: str) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
Check if a single requirement clause is satisfied by self.current_core.
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
core_ver = version.parse(self.current_core)
|
|
144
|
+
except InvalidVersion:
|
|
145
|
+
try:
|
|
146
|
+
core_ver = version.parse(f"{self.current_core}.0.0")
|
|
147
|
+
except InvalidVersion:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
raw_clause = clause.strip()
|
|
151
|
+
if not raw_clause:
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
# Fast path for simple numbers or caret major versions e.g. '8', '^8', '9', '10'
|
|
155
|
+
if re.match(r'^[\^~]?\d+$', raw_clause):
|
|
156
|
+
try:
|
|
157
|
+
major = int(re.sub(r'[^\d]', '', raw_clause))
|
|
158
|
+
return core_ver.major == major
|
|
159
|
+
except ValueError:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
# Normalize specifiers for SpecifierSet
|
|
163
|
+
spec_str = raw_clause
|
|
164
|
+
def _expand_caret(m: re.Match) -> str:
|
|
165
|
+
ver_str = m.group(1)
|
|
166
|
+
major = int(ver_str.split('.')[0])
|
|
167
|
+
return f">={ver_str}, <{major + 1}.0.0"
|
|
168
|
+
spec_str = re.sub(r'\^(\d+(?:\.\d+)*)', _expand_caret, spec_str)
|
|
169
|
+
spec_str = re.sub(r'(\d+(?:\.\d+)*)\s*([<>=!~])', r'\1, \2', spec_str)
|
|
170
|
+
parts = [p.strip() for p in spec_str.split(',') if p.strip()]
|
|
171
|
+
norm_parts = []
|
|
172
|
+
for p in parts:
|
|
173
|
+
if re.match(r'^\d+\.x$', p):
|
|
174
|
+
norm_parts.append(f"=={p.split('.')[0]}.*")
|
|
175
|
+
elif re.match(r'^\d+$', p):
|
|
176
|
+
norm_parts.append(f"=={p}.*")
|
|
177
|
+
elif re.match(r'^\d+(\.\d+)+$', p):
|
|
178
|
+
norm_parts.append(f">={p}")
|
|
179
|
+
else:
|
|
180
|
+
norm_parts.append(p)
|
|
181
|
+
norm_spec_str = ", ".join(norm_parts)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
spec_set = SpecifierSet(norm_spec_str)
|
|
185
|
+
return core_ver in spec_set
|
|
186
|
+
except (InvalidSpecifier, InvalidVersion) as exc:
|
|
187
|
+
logger.warning("Failed to evaluate requirement clause %r as SpecifierSet: %s", clause, exc)
|
|
188
|
+
matches = re.findall(r'\b\d+\b', raw_clause)
|
|
189
|
+
if matches:
|
|
190
|
+
try:
|
|
191
|
+
majors = [int(m) for m in matches]
|
|
192
|
+
return core_ver.major in majors
|
|
193
|
+
except ValueError:
|
|
194
|
+
pass
|
|
195
|
+
return False
|
|
196
|
+
|
|
133
197
|
def find_suitable_entries(self, transitive_entries: list) -> list:
|
|
134
198
|
"""
|
|
135
199
|
Get the suitable transitive versions of the module.
|
|
@@ -139,20 +203,24 @@ class Worker:
|
|
|
139
203
|
:rtype: list
|
|
140
204
|
"""
|
|
141
205
|
suitable_entries = []
|
|
142
|
-
current_major_version = version.parse(self.current_core).major
|
|
143
206
|
for entry in transitive_entries:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if current_major_version in requirement_major_versions:
|
|
207
|
+
req_parts = entry.get('requirement_parts', [])
|
|
208
|
+
if not req_parts and 'requirement' in entry:
|
|
209
|
+
req_parts = [p.strip() for p in re.split(r'\|+', entry['requirement']) if p.strip()]
|
|
210
|
+
|
|
211
|
+
if any(self._is_clause_satisfied(part) for part in req_parts):
|
|
150
212
|
suitable_entries.append(entry)
|
|
151
213
|
|
|
152
214
|
# apply post-filtering if the lock version is used and the module version is specified
|
|
153
215
|
if self.use_lock_version and self.module.version:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
216
|
+
filtered = []
|
|
217
|
+
for entry in suitable_entries:
|
|
218
|
+
try:
|
|
219
|
+
if version.parse(entry['version']) >= version.parse(self.module.version):
|
|
220
|
+
filtered.append(entry)
|
|
221
|
+
except Exception:
|
|
222
|
+
filtered.append(entry)
|
|
223
|
+
suitable_entries = filtered
|
|
224
|
+
|
|
158
225
|
return suitable_entries
|
|
226
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|