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.
Files changed (37) hide show
  1. {drupal_scout-3.0.2/drupal_scout.egg-info → drupal_scout-3.0.3}/PKG-INFO +1 -1
  2. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_worker.py +40 -0
  3. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/worker.py +83 -15
  4. {drupal_scout-3.0.2 → drupal_scout-3.0.3/drupal_scout.egg-info}/PKG-INFO +1 -1
  5. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/pyproject.toml +1 -1
  6. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/LICENSE.txt +0 -0
  7. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/README.md +0 -0
  8. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/bin/drupal-scout +0 -0
  9. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/__init__.py +0 -0
  10. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/application.py +0 -0
  11. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/exceptions.py +0 -0
  12. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/__init__.py +0 -0
  13. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/formatter.py +0 -0
  14. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/formatterfactory.py +0 -0
  15. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/jsonformatter.py +0 -0
  16. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/suggestformatter.py +0 -0
  17. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/formatters/tableformatter.py +0 -0
  18. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/mcp_server.py +0 -0
  19. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/module.py +0 -0
  20. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/output.py +0 -0
  21. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/__init__.py +0 -0
  22. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/conftest.py +0 -0
  23. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_application.py +0 -0
  24. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_exceptions.py +0 -0
  25. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_formatterfactory.py +0 -0
  26. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_jsonformatter.py +0 -0
  27. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_mcp_server.py +0 -0
  28. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_suggestformatter.py +0 -0
  29. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_tableformatter.py +0 -0
  30. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/tests/test_workers_manager.py +0 -0
  31. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout/workers_manager.py +0 -0
  32. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout.egg-info/SOURCES.txt +0 -0
  33. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout.egg-info/dependency_links.txt +0 -0
  34. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout.egg-info/entry_points.txt +0 -0
  35. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout.egg-info/requires.txt +0 -0
  36. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/drupal_scout.egg-info/top_level.txt +0 -0
  37. {drupal_scout-3.0.2 → drupal_scout-3.0.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drupal-scout
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary: Scout out for transitive versions of Drupal modules for the upgrade of the core.
5
5
  Author-email: "Andrew [R-Tech] Tsyhaniuk" <in0mad91@gmail.com>
6
6
  License: MIT License
@@ -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
- if "|" in entry['requirement']:
127
- entry['requirement'] = entry['requirement'].replace("^", "").replace(" ", "")
128
- entry["requirement_parts"] = [p for p in entry['requirement'].split("|") if p]
129
- entry['requirement'] = " || ".join(entry['requirement_parts'])
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
- # Parse all requirement parts to extract major versions
145
- requirement_major_versions = [version.parse(req_part).major for req_part in entry['requirement_parts']]
146
-
147
- # Check if the current major version is in the list of supported major versions
148
- # The requirement uses || to indicate OR, so the module supports any of these versions
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
- suitable_entries = [
155
- entry for entry in suitable_entries
156
- if version.parse(entry['version']) >= version.parse(self.module.version)
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
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drupal-scout
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary: Scout out for transitive versions of Drupal modules for the upgrade of the core.
5
5
  Author-email: "Andrew [R-Tech] Tsyhaniuk" <in0mad91@gmail.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "drupal-scout"
3
- version = "3.0.2"
3
+ version = "3.0.3"
4
4
  description = "Scout out for transitive versions of Drupal modules for the upgrade of the core."
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE.txt" }
File without changes
File without changes
File without changes