Skip to content

Commit c47d702

Browse files
committed
Address Copilot feedback (1)
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
1 parent e4df2af commit c47d702

4 files changed

Lines changed: 209 additions & 5 deletions

File tree

.github/workflows/build-tag.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ jobs:
9393
TWINE_PASSWORD: ${{ secrets.PYPI_UPLOAD_PASS }}
9494
run: |
9595
python -m build --wheel
96-
# --skip-existing makes the step retry-safe: if a previous attempt
97-
# partially succeeded and the workflow is re-run, twine treats an
98-
# already-uploaded file as success instead of failing the step.
96+
# --skip-existing makes re-runs retry-safe. PyPI does not allow
97+
# overwriting an existing release; replacing a tagged version's
98+
# contents requires bumping VERSION and re-tagging.
9999
twine upload --skip-existing dist/*
100100
# The previously-separate distributions (dapr-ext-fastapi, dapr-ext-grpc,
101101
# dapr-ext-langgraph, dapr-ext-strands, dapr-ext-workflow, flask-dapr)

dapr/ext/langgraph/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ cp.delete_thread(config)
7272
- `langgraph >= 0.3.6, < 2.0.0`
7373
- `langchain >= 0.1.17, < 2.0.0`
7474
- `python-ulid >= 3.0.0, < 4.0.0` (for checkpoint ID ordering)
75-
- `msgpack-python >= 0.4.5, < 1.0.0` (for binary serialization)
75+
- `msgpack >= 1.0, < 2.0` (for binary serialization)
7676

7777
## Testing
7878

dapr/ext/strands/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ manager = DaprSessionManager.from_address(
8282
- `strands-agents >= 1.30.0, < 2.0.0`
8383
- `strands-agents-tools >= 0.2.22, < 1.0.0`
8484
- `python-ulid >= 3.0.0, < 4.0.0`
85-
- `msgpack-python >= 0.4.5, < 1.0.0`
85+
- `msgpack >= 1.0, < 2.0`
8686

8787
## Testing
8888

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
Copyright 2026 The Dapr Authors
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
"""
15+
16+
import importlib.metadata
17+
import tempfile
18+
import unittest
19+
import warnings
20+
from pathlib import Path
21+
from typing import Iterable
22+
from unittest import mock
23+
24+
import dapr as dapr_pkg
25+
26+
27+
class _FakeDist:
28+
"""Stand-in for importlib.metadata.Distribution used by the detectors."""
29+
30+
def __init__(self, version: str, files: Iterable[str] | None) -> None:
31+
self.version = version
32+
self._files = None if files is None else [Path(p) for p in files]
33+
34+
@property
35+
def files(self) -> list[Path] | None:
36+
return self._files
37+
38+
39+
def _distribution_factory(installed: dict[str, _FakeDist]):
40+
def _factory(name: str) -> _FakeDist:
41+
try:
42+
return installed[name]
43+
except KeyError as exc:
44+
raise importlib.metadata.PackageNotFoundError(name) from exc
45+
46+
return _factory
47+
48+
49+
class TestDetectLegacyExtensionDists(unittest.TestCase):
50+
def test_returns_empty_when_no_legacy_installed(self):
51+
with mock.patch.object(
52+
dapr_pkg.importlib.metadata, 'distribution', _distribution_factory({})
53+
):
54+
self.assertEqual(dapr_pkg._detect_legacy_extension_dists(), [])
55+
56+
def test_detects_legacy_dist_with_bundled_files(self):
57+
installed = {
58+
'dapr-ext-grpc': _FakeDist(
59+
version='1.18.0',
60+
files=['dapr/ext/grpc/__init__.py', 'dapr/ext/grpc/app.py'],
61+
),
62+
}
63+
with mock.patch.object(
64+
dapr_pkg.importlib.metadata, 'distribution', _distribution_factory(installed)
65+
):
66+
result = dapr_pkg._detect_legacy_extension_dists()
67+
self.assertEqual(result, ['dapr-ext-grpc==1.18.0'])
68+
69+
def test_ignores_dist_that_does_not_own_bundled_path(self):
70+
# Modern flask-dapr-style stub dist that no longer ships dapr/ext/* files.
71+
installed = {
72+
'dapr-ext-grpc': _FakeDist(
73+
version='2.0.0',
74+
files=['some/other/path.py'],
75+
),
76+
}
77+
with mock.patch.object(
78+
dapr_pkg.importlib.metadata, 'distribution', _distribution_factory(installed)
79+
):
80+
self.assertEqual(dapr_pkg._detect_legacy_extension_dists(), [])
81+
82+
def test_flags_install_when_record_is_missing(self):
83+
installed = {
84+
'flask-dapr': _FakeDist(version='1.17.0', files=None),
85+
}
86+
with mock.patch.object(
87+
dapr_pkg.importlib.metadata, 'distribution', _distribution_factory(installed)
88+
):
89+
self.assertEqual(dapr_pkg._detect_legacy_extension_dists(), ['flask-dapr==1.17.0'])
90+
91+
def test_swallows_unexpected_exceptions(self):
92+
def _broken(_name: str):
93+
raise RuntimeError('metadata backend exploded')
94+
95+
with mock.patch.object(dapr_pkg.importlib.metadata, 'distribution', _broken):
96+
self.assertEqual(dapr_pkg._detect_legacy_extension_dists(), [])
97+
98+
99+
class TestDetectMissingBundledFiles(unittest.TestCase):
100+
def _materialize_layout(self, tmp_path: Path, present: set[str]) -> Path:
101+
dapr_root = tmp_path / 'dapr'
102+
dapr_root.mkdir()
103+
(dapr_root / '__init__.py').write_text('')
104+
for prefix in dapr_pkg._LEGACY_DISTS_WITH_BUNDLED_PATHS.values():
105+
if prefix not in present:
106+
continue
107+
if prefix.startswith('dapr/'):
108+
target = dapr_root / prefix[len('dapr/') :] / '__init__.py'
109+
else:
110+
target = tmp_path / prefix / '__init__.py'
111+
target.parent.mkdir(parents=True, exist_ok=True)
112+
target.write_text('')
113+
return dapr_root
114+
115+
def test_returns_empty_when_all_paths_present(self):
116+
all_prefixes = set(dapr_pkg._LEGACY_DISTS_WITH_BUNDLED_PATHS.values())
117+
with tempfile.TemporaryDirectory() as tmp:
118+
dapr_root = self._materialize_layout(Path(tmp), all_prefixes)
119+
with mock.patch.object(dapr_pkg, '__file__', str(dapr_root / '__init__.py')):
120+
self.assertEqual(dapr_pkg._detect_missing_bundled_files(), [])
121+
122+
def test_reports_missing_bundled_paths(self):
123+
present = {'dapr/ext/grpc', 'dapr/ext/workflow'}
124+
with tempfile.TemporaryDirectory() as tmp:
125+
dapr_root = self._materialize_layout(Path(tmp), present)
126+
with mock.patch.object(dapr_pkg, '__file__', str(dapr_root / '__init__.py')):
127+
missing = set(dapr_pkg._detect_missing_bundled_files())
128+
129+
expected = set(dapr_pkg._LEGACY_DISTS_WITH_BUNDLED_PATHS.values()) - present
130+
self.assertEqual(missing, expected)
131+
132+
def test_bails_out_when_dapr_root_not_a_directory(self):
133+
with tempfile.TemporaryDirectory() as tmp:
134+
phantom_init = Path(tmp) / 'not-a-dir' / '__init__.py'
135+
with mock.patch.object(dapr_pkg, '__file__', str(phantom_init)):
136+
self.assertEqual(dapr_pkg._detect_missing_bundled_files(), [])
137+
138+
139+
class TestCheckForLegacyExtensionIssues(unittest.TestCase):
140+
def test_respects_skip_env_var(self):
141+
with mock.patch.dict('os.environ', {'DAPR_SKIP_LEGACY_CHECK': '1'}, clear=False):
142+
with warnings.catch_warnings(record=True) as captured:
143+
warnings.simplefilter('always')
144+
with mock.patch.object(
145+
dapr_pkg, '_detect_legacy_extension_dists', return_value=['dapr-ext-grpc==1.0']
146+
):
147+
dapr_pkg._check_for_legacy_extension_issues()
148+
self.assertEqual(captured, [])
149+
150+
def test_warns_for_legacy_installs_and_skips_filesystem_check(self):
151+
with mock.patch.dict('os.environ', {}, clear=False):
152+
with mock.patch.object(
153+
dapr_pkg, '_detect_legacy_extension_dists', return_value=['dapr-ext-grpc==1.0.0']
154+
):
155+
with mock.patch.object(dapr_pkg, '_detect_missing_bundled_files') as missing_probe:
156+
with warnings.catch_warnings(record=True) as captured:
157+
warnings.simplefilter('always')
158+
dapr_pkg._check_for_legacy_extension_issues()
159+
missing_probe.assert_not_called()
160+
self.assertEqual(len(captured), 1)
161+
warning = captured[0]
162+
self.assertIs(warning.category, FutureWarning)
163+
self.assertIn('dapr-ext-grpc==1.0.0', str(warning.message))
164+
self.assertIn('pip uninstall', str(warning.message))
165+
166+
def test_warns_for_missing_bundled_paths(self):
167+
with mock.patch.dict('os.environ', {}, clear=False):
168+
with mock.patch.object(dapr_pkg, '_detect_legacy_extension_dists', return_value=[]):
169+
with mock.patch.object(
170+
dapr_pkg,
171+
'_detect_missing_bundled_files',
172+
return_value=['dapr/ext/grpc'],
173+
):
174+
with warnings.catch_warnings(record=True) as captured:
175+
warnings.simplefilter('always')
176+
dapr_pkg._check_for_legacy_extension_issues()
177+
self.assertEqual(len(captured), 1)
178+
self.assertIs(captured[0].category, FutureWarning)
179+
self.assertIn('dapr/ext/grpc', str(captured[0].message))
180+
self.assertIn('--force-reinstall', str(captured[0].message))
181+
182+
def test_silent_when_environment_is_clean(self):
183+
with mock.patch.dict('os.environ', {}, clear=False):
184+
with mock.patch.object(dapr_pkg, '_detect_legacy_extension_dists', return_value=[]):
185+
with mock.patch.object(dapr_pkg, '_detect_missing_bundled_files', return_value=[]):
186+
with warnings.catch_warnings(record=True) as captured:
187+
warnings.simplefilter('always')
188+
dapr_pkg._check_for_legacy_extension_issues()
189+
self.assertEqual(captured, [])
190+
191+
192+
class TestEmitLegacyWarning(unittest.TestCase):
193+
def test_falls_back_to_stderr_when_warning_escalated(self):
194+
with warnings.catch_warnings():
195+
warnings.simplefilter('error', FutureWarning)
196+
with mock.patch('sys.stderr') as stderr_mock:
197+
dapr_pkg._emit_legacy_warning('legacy detected')
198+
stderr_mock.write.assert_called()
199+
written = ''.join(call.args[0] for call in stderr_mock.write.call_args_list)
200+
self.assertIn('legacy detected', written)
201+
202+
203+
if __name__ == '__main__':
204+
unittest.main()

0 commit comments

Comments
 (0)