dissolve 0.2.0__tar.gz → 0.3.0__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 (57) hide show
  1. dissolve-0.3.0/MANIFEST.in +1 -0
  2. dissolve-0.3.0/PKG-INFO +559 -0
  3. dissolve-0.3.0/README.md +529 -0
  4. {dissolve-0.2.0 → dissolve-0.3.0}/dissolve/__init__.py +1 -1
  5. {dissolve-0.2.0 → dissolve-0.3.0}/dissolve/ast_utils.py +5 -16
  6. dissolve-0.3.0/dissolve/cli.py +87 -0
  7. {dissolve-0.2.0 → dissolve-0.3.0}/dissolve/decorators.py +22 -12
  8. dissolve-0.3.0/dissolve/py.typed +2 -0
  9. dissolve-0.3.0/dissolve/tests/__init__.py +1 -0
  10. dissolve-0.3.0/dissolve/tests/test_replace_me_decorator.py +465 -0
  11. dissolve-0.3.0/dissolve.egg-info/PKG-INFO +559 -0
  12. dissolve-0.3.0/dissolve.egg-info/SOURCES.txt +18 -0
  13. dissolve-0.3.0/dissolve.egg-info/entry_points.txt +2 -0
  14. {dissolve-0.2.0 → dissolve-0.3.0}/dissolve.egg-info/requires.txt +4 -4
  15. {dissolve-0.2.0 → dissolve-0.3.0}/pyproject.toml +32 -7
  16. dissolve-0.3.0/setup.py +6 -0
  17. dissolve-0.2.0/.github/workflows/disperse.yml +0 -15
  18. dissolve-0.2.0/.github/workflows/test.yml +0 -42
  19. dissolve-0.2.0/.gitignore +0 -7
  20. dissolve-0.2.0/CONTRIBUTING.md +0 -70
  21. dissolve-0.2.0/DESIGN.md +0 -247
  22. dissolve-0.2.0/PKG-INFO +0 -467
  23. dissolve-0.2.0/README.rst +0 -437
  24. dissolve-0.2.0/TODO +0 -1
  25. dissolve-0.2.0/disperse.conf +0 -7
  26. dissolve-0.2.0/dissolve/__main__.py +0 -538
  27. dissolve-0.2.0/dissolve/check.py +0 -123
  28. dissolve-0.2.0/dissolve/collector.py +0 -624
  29. dissolve-0.2.0/dissolve/import_utils.py +0 -15
  30. dissolve-0.2.0/dissolve/migrate.py +0 -235
  31. dissolve-0.2.0/dissolve/remove.py +0 -256
  32. dissolve-0.2.0/dissolve/replacer.py +0 -451
  33. dissolve-0.2.0/dissolve/types.py +0 -64
  34. dissolve-0.2.0/dissolve.egg-info/PKG-INFO +0 -467
  35. dissolve-0.2.0/dissolve.egg-info/SOURCES.txt +0 -44
  36. dissolve-0.2.0/dissolve.egg-info/entry_points.txt +0 -2
  37. dissolve-0.2.0/setup.py +0 -4
  38. dissolve-0.2.0/tests/test_ast_utils.py +0 -30
  39. dissolve-0.2.0/tests/test_attribute_deprecation.py +0 -139
  40. dissolve-0.2.0/tests/test_check.py +0 -143
  41. dissolve-0.2.0/tests/test_class_methods.py +0 -315
  42. dissolve-0.2.0/tests/test_class_wrapper_deprecation.py +0 -154
  43. dissolve-0.2.0/tests/test_cli.py +0 -863
  44. dissolve-0.2.0/tests/test_comprehensive_replacements.py +0 -555
  45. dissolve-0.2.0/tests/test_decorator.py +0 -183
  46. dissolve-0.2.0/tests/test_decorator_dependencies.py +0 -119
  47. dissolve-0.2.0/tests/test_docstring_extension.py +0 -149
  48. dissolve-0.2.0/tests/test_edge_cases.py +0 -345
  49. dissolve-0.2.0/tests/test_formatting_edge_cases.py +0 -357
  50. dissolve-0.2.0/tests/test_formatting_preservation.py +0 -570
  51. dissolve-0.2.0/tests/test_migrate.py +0 -775
  52. dissolve-0.2.0/tests/test_remove.py +0 -372
  53. dissolve-0.2.0/tox.ini +0 -8
  54. {dissolve-0.2.0 → dissolve-0.3.0}/COPYING +0 -0
  55. {dissolve-0.2.0 → dissolve-0.3.0}/dissolve.egg-info/dependency_links.txt +0 -0
  56. {dissolve-0.2.0 → dissolve-0.3.0}/dissolve.egg-info/top_level.txt +0 -0
  57. {dissolve-0.2.0 → dissolve-0.3.0}/setup.cfg +0 -0
@@ -0,0 +1 @@
1
+ include dissolve/py.typed
@@ -0,0 +1,559 @@
1
+ Metadata-Version: 2.4
2
+ Name: dissolve
3
+ Version: 0.3.0
4
+ Summary: Automatically replace use of deprecated APIs
5
+ Author-email: Jelmer Vernooij <jelmer@jelmer.uk>
6
+ License: Apachev2 or later
7
+ Project-URL: Homepage, https://github.com/jelmer/dissolve/
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: Implementation :: CPython
16
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
17
+ Classifier: Operating System :: POSIX
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: COPYING
21
+ Requires-Dist: packaging
22
+ Provides-Extra: testing
23
+ Requires-Dist: pytest; extra == "testing"
24
+ Requires-Dist: pytest-asyncio; extra == "testing"
25
+ Provides-Extra: dev
26
+ Requires-Dist: ruff==0.11.11; extra == "dev"
27
+ Requires-Dist: mypy==1.15.0; extra == "dev"
28
+ Provides-Extra: tool
29
+ Dynamic: license-file
30
+
31
+ # dissolve
32
+
33
+ The dissolve library helps users replace calls to deprecated library APIs by
34
+ automatically substituting the deprecated function call with the body of the
35
+ deprecated function.
36
+
37
+ ## Installation
38
+
39
+ Basic installation (for using the `@replace_me` decorator):
40
+
41
+ ```console
42
+ $ pip install dissolve
43
+ ```
44
+
45
+ This provides the `@replace_me` decorator for marking deprecated functions and generating
46
+ deprecation warnings with replacement suggestions.
47
+
48
+ For the `dissolve` command-line tool, first install the Python package:
49
+
50
+ ```console
51
+ $ pip install dissolve
52
+ ```
53
+
54
+ Then install the high-performance Rust binary:
55
+
56
+ ```console
57
+ $ cargo install dissolve-python
58
+ ```
59
+
60
+ If you don't have Rust installed:
61
+
62
+ ```console
63
+ $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
64
+ $ source ~/.cargo/env
65
+ $ cargo install dissolve-python
66
+ ```
67
+
68
+ The Python package provides a wrapper that automatically detects and uses the Rust binary when available.
69
+
70
+ The CLI provides the following commands:
71
+ - `dissolve migrate`: Automatically replace deprecated function calls
72
+ - `dissolve cleanup`: Remove deprecated functions after migration
73
+ - `dissolve check`: Validate that deprecations can be migrated
74
+ - `dissolve info`: List all deprecated functions and their replacements
75
+
76
+ ## Example
77
+
78
+ E.g. if you had a function "inc" that has been renamed to "increment" in
79
+ version 0.1.0 of your library:
80
+
81
+ ```python
82
+ from dissolve import replace_me
83
+
84
+ def increment(x):
85
+ return x + 1
86
+
87
+ @replace_me(since="0.1.0")
88
+ def inc(x):
89
+ return increment(x)
90
+ ```
91
+
92
+ Running this code will yield a warning:
93
+
94
+ ```console
95
+ ...
96
+ >>> inc(x=3)
97
+ <stdin>:1: DeprecationWarning: <function inc at 0x7feaf5ead5a0> has been deprecated since 0.1.0; use 'increment(x)' instead
98
+ 4
99
+ ```
100
+
101
+ Running the `dissolve migrate` command will automatically replace the
102
+ deprecated function call with the suggested replacement:
103
+
104
+ ```console
105
+ $ dissolve migrate --write myproject/utils.py
106
+ Modified: myproject/utils.py
107
+ ...
108
+ result = increment(x=3)
109
+ ...
110
+ ```
111
+
112
+ **For library users**: The migration step above is typically all you need.
113
+ Your code now uses the new `increment` function instead of the deprecated `inc` function.
114
+
115
+ **For library maintainers**: After users have had time to migrate and you're ready
116
+ to remove the deprecated function from your library, you can use `dissolve cleanup`:
117
+
118
+ ```console
119
+ $ dissolve cleanup --all --write myproject/utils.py
120
+ Modified: myproject/utils.py
121
+ ```
122
+
123
+ This removes the `inc` function entirely from the library, leaving only the `increment` function.
124
+
125
+ ## dissolve migrate
126
+
127
+ The `dissolve migrate` command can automatically update your codebase to
128
+ replace deprecated function calls with their suggested replacements.
129
+
130
+ Usage:
131
+
132
+ ```console
133
+ $ dissolve migrate path/to/code
134
+ ```
135
+
136
+ This will:
137
+
138
+ 1. Search for Python files in the specified path
139
+ 2. Find calls to functions decorated with `@replace_me`
140
+ 3. Replace them with the suggested replacement expression
141
+ 4. Show a diff of the changes
142
+
143
+ Options:
144
+
145
+ * `-w, --write`: Write changes back to files instead of printing to stdout
146
+ * `--check`: Check if files need migration without modifying them (exits with code 1 if changes are needed)
147
+
148
+ Examples:
149
+
150
+ Preview changes:
151
+
152
+ ```console
153
+ $ dissolve migrate myproject/utils.py
154
+ # Migrated: myproject/utils.py
155
+ ...
156
+ result = 5 + 1
157
+ ...
158
+ ```
159
+
160
+ Check if migration is needed:
161
+
162
+ ```console
163
+ $ dissolve migrate --check myproject/
164
+ myproject/utils.py: needs migration
165
+ myproject/core.py: up to date
166
+ $ echo $?
167
+ 1
168
+ ```
169
+
170
+ Apply changes:
171
+
172
+ ```console
173
+ $ dissolve migrate --write myproject/
174
+ Modified: myproject/utils.py
175
+ Unchanged: myproject/core.py
176
+ ```
177
+
178
+ The command respects the replacement expressions defined in the `@replace_me`
179
+ decorator and substitutes actual argument values.
180
+
181
+ ## How dissolve Works
182
+
183
+ Dissolve uses type inference to determine which function calls to migrate. This
184
+ avoids false positives when different classes have methods with the same name.
185
+
186
+ ### Type Tracking
187
+
188
+ When dissolve processes a file, it:
189
+
190
+ 1. Tracks variable assignments to determine types
191
+ 2. Follows imports to resolve fully qualified names
192
+ 3. Scans imported modules for `@replace_me` decorated functions
193
+ 4. Uses this information to match function calls to their definitions
194
+
195
+ For example:
196
+
197
+ ```python
198
+ from mylib import OldClass
199
+ from other_lib import DifferentClass
200
+
201
+ obj1 = OldClass()
202
+ obj1.deprecated_method() # Migrated - dissolve knows obj1 is OldClass
203
+
204
+ obj2 = DifferentClass()
205
+ obj2.deprecated_method() # Not migrated - different class
206
+ ```
207
+
208
+ ### Import Resolution
209
+
210
+ Dissolve resolves imports to handle various import styles:
211
+
212
+ ```python
213
+ # These imports of the same function:
214
+ from mylib.utils import old_function
215
+ from mylib.utils import old_function as legacy_func
216
+ import mylib.utils
217
+
218
+ # Are all recognized in these calls:
219
+ old_function() # Direct import
220
+ legacy_func() # Aliased import
221
+ mylib.utils.old_function() # Module attribute access
222
+ ```
223
+
224
+ ### Context Managers
225
+
226
+ Variable assignments in `with` statements are tracked:
227
+
228
+ ```python
229
+ with open_repo() as r:
230
+ r.stage(files) # dissolve tracks that r is the return type of open_repo()
231
+ ```
232
+
233
+ ### Inheritance
234
+
235
+ Method resolution includes parent classes:
236
+
237
+ ```python
238
+ class Base:
239
+ @replace_me()
240
+ def old_method(self):
241
+ return self.new_method()
242
+
243
+ class Child(Base):
244
+ pass
245
+
246
+ obj = Child()
247
+ obj.old_method() # Migrated even though method is defined in parent class
248
+ ```
249
+
250
+ ## dissolve cleanup
251
+
252
+ The `dissolve cleanup` command is designed for **library maintainers** to remove
253
+ deprecated functions from their codebase after a deprecation period has ended.
254
+ This command removes the entire function definition, not just the `@replace_me`
255
+ decorator.
256
+
257
+ **Audience**: This command is primarily for library authors who want to clean up
258
+ their APIs after users have had time to migrate away from deprecated functions.
259
+
260
+ **Important**: This command removes the entire function definition, which will
261
+ break any code that still calls these functions. Only use this after:
262
+
263
+ 1. Sufficient time has passed for users to migrate (based on your deprecation policy)
264
+ 2. You've verified that usage of these functions has dropped to acceptable levels
265
+ 3. You're prepared to release a new major version (if following semantic versioning)
266
+
267
+ Usage:
268
+
269
+ ```console
270
+ $ dissolve cleanup [options] path/to/code
271
+ ```
272
+
273
+ Options:
274
+
275
+ * `--all`: Remove all functions with `@replace_me` decorators regardless of version
276
+ * `--before VERSION`: Remove only functions with decorators older than the specified version
277
+ * `--current-version VERSION`: Remove functions marked with `remove_in` <= current version
278
+ * `-w, --write`: Write changes back to files (default: print to stdout)
279
+ * `--check`: Check if files have deprecated functions that can be removed without modifying them (exits with code 1 if changes are needed)
280
+
281
+ Examples:
282
+
283
+ Check if deprecated functions can be removed:
284
+
285
+ ```console
286
+ $ dissolve cleanup --check --current-version 2.0.0 mylib/
287
+ mylib/utils.py: needs function cleanup
288
+ mylib/core.py: up to date
289
+ $ echo $?
290
+ 1
291
+ ```
292
+
293
+ Remove functions scheduled for removal in version 2.0.0:
294
+
295
+ ```console
296
+ $ dissolve cleanup --current-version 2.0.0 --write mylib/
297
+ Modified: mylib/utils.py
298
+ Unchanged: mylib/core.py
299
+ ```
300
+
301
+ Remove functions deprecated before version 2.0.0:
302
+
303
+ ```console
304
+ $ dissolve cleanup --before 2.0.0 --write mylib/
305
+ ```
306
+
307
+ This will remove functions like those decorated with `@replace_me(since="1.0.0")`
308
+ but keep functions with `@replace_me(since="2.0.0")` and newer.
309
+
310
+ **Typical workflow for library maintainers:**
311
+
312
+ 1. Add `@replace_me(since="X.Y.Z", remove_in="A.B.C")` to deprecated functions
313
+ 2. Release version X.Y.Z with deprecation warnings
314
+ 3. Wait for the planned removal version A.B.C
315
+ 4. Run `dissolve cleanup --current-version A.B.C --write` to remove deprecated functions
316
+ 5. Release version A.B.C as a new major version
317
+
318
+ ## dissolve check
319
+
320
+ The `dissolve check` command verifies that all `@replace_me` decorated
321
+ functions in your codebase can be successfully processed by the `dissolve
322
+ migrate` command. This is useful for ensuring your deprecation decorators are
323
+ properly formatted.
324
+
325
+ Usage:
326
+
327
+ ```console
328
+ $ dissolve check path/to/code
329
+ ```
330
+
331
+ This will:
332
+
333
+ 1. Search for Python files with `@replace_me` decorated functions
334
+ 2. Verify that each decorated function has a valid replacement expression
335
+ 3. Report any functions that cannot be processed by migrate
336
+
337
+ Examples:
338
+
339
+ Check all files in a directory:
340
+
341
+ ```console
342
+ $ dissolve check myproject/
343
+ myproject/utils.py: 3 @replace_me function(s) can be replaced
344
+ myproject/core.py: 1 @replace_me function(s) can be replaced
345
+ ```
346
+
347
+ When errors are found:
348
+
349
+ ```console
350
+ $ dissolve check myproject/broken.py
351
+ myproject/broken.py: ERRORS found
352
+ Function 'old_func' cannot be processed by migrate
353
+ ```
354
+
355
+ The command exits with code 1 if any errors are found, making it useful in CI
356
+ pipelines to ensure all deprecations are properly formatted.
357
+
358
+ ## Supported objects
359
+
360
+ The `replace_me` decorator can currently be applied to:
361
+
362
+ - Functions
363
+ - Async functions
364
+ - Instance methods
365
+ - Class methods (`@classmethod`)
366
+ - Static methods (`@staticmethod`)
367
+ - Properties (`@property`)
368
+ - Classes
369
+ - Module and class attributes (using `replace_me(value)`)
370
+
371
+ ### Class Deprecation
372
+
373
+ Classes can be deprecated by applying the `@replace_me` decorator to the class definition. The deprecated class should act as a wrapper around the new class, with the `__init__` method creating an instance of the replacement class:
374
+
375
+ ```python
376
+ from dissolve import replace_me
377
+
378
+ class UserManager:
379
+ def __init__(self, database_url, cache_size=100):
380
+ self.db = Database(database_url)
381
+ self.cache = Cache(cache_size)
382
+
383
+ def get_user(self, user_id):
384
+ return self.db.fetch_user(user_id)
385
+
386
+ @replace_me(since="2.0.0")
387
+ class UserService:
388
+ def __init__(self, database_url, cache_size=50):
389
+ self._manager = UserManager(database_url, cache_size * 2)
390
+
391
+ def get_user(self, user_id):
392
+ return self._manager.get_user(user_id)
393
+
394
+ def old_method_name(self, arg):
395
+ return self._manager.new_method_name(arg)
396
+ ```
397
+
398
+ When the deprecated class is instantiated, this will emit a deprecation warning:
399
+
400
+ ```console
401
+ >>> service = UserService("postgres://localhost", cache_size=25)
402
+ <stdin>:1: DeprecationWarning: <class UserService at 0x...> has been deprecated since 2.0.0; use 'UserManager("postgres://localhost", cache_size=25 * 2)' instead
403
+ ```
404
+
405
+ The migration tool will replace all instantiations of the deprecated class with the wrapped class:
406
+
407
+ ```console
408
+ $ dissolve migrate --write myproject.py
409
+ # UserService("config", cache_size=100) becomes:
410
+ # UserManager("config", cache_size=100 * 2)
411
+ ```
412
+
413
+ Class deprecation works with all instantiation patterns including direct calls, list comprehensions, and factory patterns:
414
+
415
+ ```python
416
+ # All of these will be migrated automatically:
417
+ service = UserService(db_url)
418
+ services = [UserService(url) for url in urls]
419
+ factory = lambda: UserService("default")
420
+ ```
421
+
422
+ This approach allows library authors to provide full backward compatibility while guiding users to the new API. The deprecated class acts as a transparent wrapper that forwards method calls to the new implementation, and the migration tool automatically updates all usage sites to use the wrapped class directly.
423
+
424
+ Dissolve will automatically determine the appropriate replacement expression
425
+ based on the body of the decorated object. In some cases, this is not possible,
426
+ such as when the body is a complex expression or when the object is a lambda
427
+ function.
428
+
429
+ ### Attribute Deprecation
430
+
431
+ Module-level constants and class attributes can be deprecated using `replace_me` as a function that wraps the value:
432
+
433
+ ```python
434
+ from dissolve import replace_me
435
+
436
+ # Module-level attribute
437
+ OLD_API_URL = replace_me("https://api.example.com/v2")
438
+
439
+ # Class attribute
440
+ class Config:
441
+ OLD_TIMEOUT = replace_me(30)
442
+ OLD_DEBUG_MODE = replace_me(True)
443
+ ```
444
+
445
+ When these attributes are used in code, the migration tool will replace them with the literal values:
446
+
447
+ ```console
448
+ $ dissolve migrate --write myproject.py
449
+ # Before:
450
+ # url = OLD_API_URL
451
+ # timeout = Config.OLD_TIMEOUT
452
+
453
+ # After:
454
+ # url = "https://api.example.com/v2"
455
+ # timeout = 30
456
+ ```
457
+
458
+ This is particularly useful for deprecating configuration constants that have been replaced by new values or moved to different locations. The `replace_me()` function call serves as a marker for the migration tool without adding any runtime overhead.
459
+
460
+ ### Async Function Deprecation
461
+
462
+ Async functions are fully supported and work just like regular functions:
463
+
464
+ ```python
465
+ from dissolve import replace_me
466
+ import asyncio
467
+
468
+ async def new_fetch_data(url, timeout=30):
469
+ # Modern implementation
470
+ return await fetch_with_timeout(url, timeout)
471
+
472
+ @replace_me(since="3.0.0")
473
+ async def old_fetch_data(url):
474
+ return await new_fetch_data(url, timeout=30)
475
+ ```
476
+
477
+ When called, this will emit:
478
+
479
+ ```console
480
+ >>> await old_fetch_data("https://api.example.com")
481
+ <stdin>:1: DeprecationWarning: <function old_fetch_data at 0x...> has been deprecated since 3.0.0; use 'await new_fetch_data('https://api.example.com', timeout=30)' instead
482
+ ```
483
+
484
+ The replacement expression correctly preserves the `await` keyword for async calls.
485
+
486
+ ### Class Methods and Static Methods
487
+
488
+ Class methods and static methods are fully supported. The `@replace_me` decorator
489
+ can be combined with `@classmethod` and `@staticmethod` decorators:
490
+
491
+ ```python
492
+ from dissolve import replace_me
493
+
494
+ class DataProcessor:
495
+ @classmethod
496
+ @replace_me(since="2.0.0")
497
+ def old_process_data(cls, data):
498
+ return cls.new_process_data(data.strip().upper())
499
+
500
+ @classmethod
501
+ def new_process_data(cls, processed_data):
502
+ return f"Processed: {processed_data}"
503
+
504
+ @staticmethod
505
+ @replace_me(since="2.0.0")
506
+ def old_utility_func(value):
507
+ return new_utility_func(value * 10)
508
+ ```
509
+
510
+ When called, these will emit appropriate deprecation warnings:
511
+
512
+ ```console
513
+ >>> DataProcessor.old_process_data(" hello ")
514
+ <stdin>:1: DeprecationWarning: <function DataProcessor.old_process_data at 0x...> has been deprecated since 2.0.0; use 'DataProcessor.new_process_data(' hello '.strip().upper())' instead
515
+
516
+ >>> DataProcessor.old_utility_func(5)
517
+ <stdin>:1: DeprecationWarning: <function DataProcessor.old_utility_func at 0x...> has been deprecated since 2.0.0; use 'new_utility_func(5 * 10)' instead
518
+ ```
519
+
520
+ The migration tool will correctly replace these calls:
521
+
522
+ ```console
523
+ $ dissolve migrate --write myproject.py
524
+ # DataProcessor.old_process_data("test") becomes:
525
+ # DataProcessor.new_process_data("test".strip().upper())
526
+ ```
527
+
528
+ ## Optional Dependency Usage
529
+
530
+ If you don't want to add a runtime dependency on dissolve, you can define a
531
+ fallback implementation that mimics dissolve's basic deprecation warning
532
+ functionality:
533
+
534
+ ```python
535
+ try:
536
+ from dissolve import replace_me
537
+ except ModuleNotFoundError:
538
+ import warnings
539
+
540
+ def replace_me(since=None, remove_in=None):
541
+ def decorator(func):
542
+ def wrapper(*args, **kwargs):
543
+ msg = f"{func.__name__} has been deprecated"
544
+ if since:
545
+ msg += f" since {since}"
546
+ if remove_in:
547
+ msg += f" and will be removed in {remove_in}"
548
+ msg += ". Consider running 'dissolve migrate' to automatically update your code."
549
+ warnings.warn(msg, DeprecationWarning, stacklevel=2)
550
+ return func(*args, **kwargs)
551
+ return wrapper
552
+ return decorator
553
+ ```
554
+
555
+ This fallback implementation provides the same decorator interface as
556
+ dissolve's `replace_me` decorator. When dissolve is installed, you get full
557
+ deprecation warnings with replacement suggestions and migration support. When
558
+ it's not installed, you still get basic deprecation warnings that include a
559
+ suggestion to use dissolve's migration tool.