mini-arcade-core 0.10.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

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 (78) hide show
  1. mini_arcade_core/__init__.py +51 -63
  2. mini_arcade_core/backend/__init__.py +2 -6
  3. mini_arcade_core/backend/backend.py +148 -8
  4. mini_arcade_core/backend/events.py +1 -1
  5. mini_arcade_core/{keymaps/sdl.py → backend/sdl_map.py} +1 -1
  6. mini_arcade_core/engine/__init__.py +0 -0
  7. mini_arcade_core/engine/commands.py +169 -0
  8. mini_arcade_core/engine/game.py +369 -0
  9. mini_arcade_core/engine/render/__init__.py +0 -0
  10. mini_arcade_core/engine/render/packet.py +56 -0
  11. mini_arcade_core/engine/render/pipeline.py +63 -0
  12. mini_arcade_core/engine/render/viewport.py +203 -0
  13. mini_arcade_core/managers/__init__.py +0 -22
  14. mini_arcade_core/managers/cheats.py +71 -240
  15. mini_arcade_core/managers/inputs.py +5 -3
  16. mini_arcade_core/runtime/__init__.py +0 -0
  17. mini_arcade_core/runtime/audio/__init__.py +0 -0
  18. mini_arcade_core/runtime/audio/audio_adapter.py +20 -0
  19. mini_arcade_core/runtime/audio/audio_port.py +36 -0
  20. mini_arcade_core/runtime/capture/__init__.py +0 -0
  21. mini_arcade_core/runtime/capture/capture_adapter.py +143 -0
  22. mini_arcade_core/runtime/capture/capture_port.py +51 -0
  23. mini_arcade_core/runtime/context.py +53 -0
  24. mini_arcade_core/runtime/file/__init__.py +0 -0
  25. mini_arcade_core/runtime/file/file_adapter.py +20 -0
  26. mini_arcade_core/runtime/file/file_port.py +31 -0
  27. mini_arcade_core/runtime/input/__init__.py +0 -0
  28. mini_arcade_core/runtime/input/input_adapter.py +49 -0
  29. mini_arcade_core/runtime/input/input_port.py +31 -0
  30. mini_arcade_core/runtime/input_frame.py +71 -0
  31. mini_arcade_core/runtime/scene/__init__.py +0 -0
  32. mini_arcade_core/runtime/scene/scene_adapter.py +97 -0
  33. mini_arcade_core/runtime/scene/scene_port.py +149 -0
  34. mini_arcade_core/runtime/services.py +35 -0
  35. mini_arcade_core/runtime/window/__init__.py +0 -0
  36. mini_arcade_core/runtime/window/window_adapter.py +90 -0
  37. mini_arcade_core/runtime/window/window_port.py +109 -0
  38. mini_arcade_core/scenes/__init__.py +0 -22
  39. mini_arcade_core/scenes/autoreg.py +1 -1
  40. mini_arcade_core/scenes/registry.py +21 -19
  41. mini_arcade_core/scenes/sim_scene.py +41 -0
  42. mini_arcade_core/scenes/systems/__init__.py +0 -0
  43. mini_arcade_core/scenes/systems/base_system.py +40 -0
  44. mini_arcade_core/scenes/systems/system_pipeline.py +57 -0
  45. mini_arcade_core/sim/__init__.py +0 -0
  46. mini_arcade_core/sim/protocols.py +41 -0
  47. mini_arcade_core/sim/runner.py +222 -0
  48. mini_arcade_core/spaces/__init__.py +0 -12
  49. mini_arcade_core/spaces/d2/__init__.py +0 -30
  50. mini_arcade_core/spaces/d2/boundaries2d.py +10 -1
  51. mini_arcade_core/spaces/d2/collision2d.py +25 -28
  52. mini_arcade_core/spaces/d2/geometry2d.py +18 -0
  53. mini_arcade_core/spaces/d2/kinematics2d.py +2 -8
  54. mini_arcade_core/spaces/d2/physics2d.py +9 -0
  55. mini_arcade_core/ui/__init__.py +0 -26
  56. mini_arcade_core/ui/menu.py +271 -85
  57. mini_arcade_core/utils/__init__.py +10 -0
  58. mini_arcade_core/utils/deprecated_decorator.py +45 -0
  59. mini_arcade_core/utils/logging.py +168 -0
  60. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/METADATA +1 -1
  61. mini_arcade_core-1.0.1.dist-info/RECORD +66 -0
  62. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/WHEEL +1 -1
  63. mini_arcade_core/commands.py +0 -84
  64. mini_arcade_core/entity.py +0 -72
  65. mini_arcade_core/game.py +0 -287
  66. mini_arcade_core/keymaps/__init__.py +0 -15
  67. mini_arcade_core/managers/base.py +0 -132
  68. mini_arcade_core/managers/entities.py +0 -38
  69. mini_arcade_core/managers/overlays.py +0 -53
  70. mini_arcade_core/managers/system.py +0 -26
  71. mini_arcade_core/scenes/model.py +0 -34
  72. mini_arcade_core/scenes/runtime.py +0 -29
  73. mini_arcade_core/scenes/scene.py +0 -109
  74. mini_arcade_core/scenes/system.py +0 -69
  75. mini_arcade_core/ui/overlays.py +0 -41
  76. mini_arcade_core-0.10.0.dist-info/RECORD +0 -40
  77. /mini_arcade_core/{keymaps → backend}/keys.py +0 -0
  78. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -7,12 +7,17 @@ from __future__ import annotations
7
7
  from dataclasses import dataclass
8
8
  from typing import Callable, Optional, Sequence
9
9
 
10
- from mini_arcade_core.backend import Backend, Color, Event, EventType
11
- from mini_arcade_core.commands import BaseCommand, QuitGameCommand
12
- from mini_arcade_core.game import Game
13
- from mini_arcade_core.keymaps import Key
14
- from mini_arcade_core.scenes import BaseSceneSystem, Scene, SceneModel
15
- from mini_arcade_core.spaces.d2 import Size2D
10
+ from mini_arcade_core.backend import Backend
11
+ from mini_arcade_core.backend.events import Event, EventType
12
+ from mini_arcade_core.backend.keys import Key
13
+ from mini_arcade_core.backend.types import Color
14
+ from mini_arcade_core.engine.commands import Command, CommandQueue, QuitCommand
15
+ from mini_arcade_core.engine.render.packet import RenderPacket
16
+ from mini_arcade_core.runtime.context import RuntimeContext
17
+ from mini_arcade_core.runtime.input_frame import InputFrame
18
+ from mini_arcade_core.scenes.systems.system_pipeline import SystemPipeline
19
+ from mini_arcade_core.sim.protocols import SimScene
20
+ from mini_arcade_core.spaces.d2.geometry2d import Size2D
16
21
 
17
22
 
18
23
  @dataclass(frozen=True)
@@ -26,20 +31,20 @@ class MenuItem:
26
31
 
27
32
  id: str
28
33
  label: str
29
- on_select: BaseCommand
30
- label_fn: Optional[Callable[[Game], str]] = None
34
+ command_factory: Callable[[], Command]
35
+ label_fn: Optional[Callable[[object], str]] = None
31
36
 
32
- def resolved_label(self, game: Game) -> str:
37
+ def resolved_label(self, ctx: object) -> str:
33
38
  """
34
39
  Get the resolved label for this menu item.
35
40
 
36
- :param game: The current game instance.
37
- :type game: Game
41
+ :param ctx: The current ctx instance.
42
+ :type ctx: object
38
43
 
39
44
  :return: The resolved label string.
40
45
  :rtype: str
41
46
  """
42
- return self.label_fn(game) if self.label_fn else self.label
47
+ return self.label_fn(ctx) if self.label_fn else self.label
43
48
 
44
49
 
45
50
  # Justification: Data container for styling options needs
@@ -123,6 +128,7 @@ class MenuStyle:
123
128
  class Menu:
124
129
  """A simple text-based menu system."""
125
130
 
131
+ # TODO: Solve too-many-arguments warning later
126
132
  # Justification: Multiple attributes for menu state
127
133
  # pylint: disable=too-many-arguments
128
134
  def __init__(
@@ -146,6 +152,9 @@ class Menu:
146
152
 
147
153
  :param style: Optional MenuStyle for customizing appearance.
148
154
  :type style: MenuStyle | None
155
+
156
+ :param on_select: Optional callback when an item is selected.
157
+ :type on_select: Optional[Callable[[MenuItem], None]]
149
158
  """
150
159
  self.items = list(items)
151
160
  self.viewport = viewport
@@ -159,6 +168,21 @@ class Menu:
159
168
 
160
169
  # pylint: enable=too-many-arguments
161
170
 
171
+ def set_items(self, items: Sequence[MenuItem]):
172
+ """Set the menu items.
173
+ :param items: Sequence of new MenuItem instances.
174
+ :type items: Sequence[MenuItem]
175
+ """
176
+ self.items = list(items)
177
+
178
+ def set_selected_index(self, index: int):
179
+ """Set the selected index of the menu.
180
+ :param index: New selected index.
181
+ :type index: int
182
+ """
183
+ if 0 <= index < len(self.items):
184
+ self.selected_index = index
185
+
162
186
  def set_labels(self, labels: Sequence[str]):
163
187
  """Set the labels of the menu items.
164
188
  :param labels: Sequence of new labels for the menu items.
@@ -170,7 +194,7 @@ class Menu:
170
194
  self.items[index] = MenuItem(
171
195
  id=item.id,
172
196
  label=label,
173
- on_select=item.on_select,
197
+ command_factory=item.command_factory,
174
198
  label_fn=item.label_fn,
175
199
  )
176
200
 
@@ -230,6 +254,7 @@ class Menu:
230
254
 
231
255
  return False
232
256
 
257
+ # TODO: Delegate drawing to a renderer class later
233
258
  def draw(self, surface: Backend):
234
259
  """
235
260
  Draw the menu onto the given backend surface.
@@ -242,7 +267,7 @@ class Menu:
242
267
  "Menu requires viewport=Size2D for centering/layout"
243
268
  )
244
269
 
245
- vw, vh = self.viewport.width, self.viewport.height
270
+ vw, vh = self.viewport
246
271
 
247
272
  # 0) Solid background (for main menus)
248
273
  if self.style.background_color is not None:
@@ -322,6 +347,7 @@ class Menu:
322
347
  font_size=self.style.item_font_size,
323
348
  )
324
349
 
350
+ # TODO: Solve too-many-locals warning later
325
351
  # Justification: Local variables for layout calculations
326
352
  # pylint: disable=too-many-locals
327
353
  def _draw_buttons(self, surface: Backend, x_center: int, cursor_y: int):
@@ -434,13 +460,14 @@ class Menu:
434
460
  + self.style.title_margin_bottom
435
461
  )
436
462
 
437
- # Sticky width (never shrink)
463
+ # Sticky width (never shrink)
438
464
  if self.stable_width:
439
465
  self._max_content_w_seen = max(self._max_content_w_seen, max_w)
440
466
  max_w = self._max_content_w_seen
441
467
 
442
468
  return max_w, content_h, title_h
443
469
 
470
+ # TODO: Solve too-many-arguments warning later
444
471
  # Justification: Many arguments for text drawing utility
445
472
  # pylint: disable=too-many-arguments
446
473
  @staticmethod
@@ -465,130 +492,289 @@ class Menu:
465
492
 
466
493
 
467
494
  @dataclass
468
- class MenuModel(SceneModel):
469
- """Data model for menu scenes."""
495
+ class MenuModel:
496
+ """
497
+ Data model for menu scenes.
498
+
499
+ :ivar selected (int): Currently selected menu item index.
500
+ :ivar move_cooldown (float): Cooldown time between menu moves.
501
+ :ivar _cooldown_timer (float): Internal timer for move cooldown.
502
+ """
470
503
 
471
- up_key: Key = Key.UP
472
- down_key: Key = Key.DOWN
473
- select_key: Key = Key.ENTER
504
+ selected: int = 0
505
+ move_cooldown: float = 0.12
506
+ _cooldown_timer: float = 0.0
474
507
 
508
+ def step_timer(self, dt: float):
509
+ """
510
+ Step the internal cooldown timer.
511
+
512
+ :param dt: Delta time since last update.
513
+ :type dt: float
514
+ """
515
+ if self._cooldown_timer > 0:
516
+ self._cooldown_timer = max(0.0, self._cooldown_timer - dt)
517
+
518
+ def can_move(self) -> bool:
519
+ """
520
+ Check if the menu can move selection (cooldown elapsed).
475
521
 
476
- class MenuSystem(BaseSceneSystem):
522
+ :return: True if movement is allowed, False otherwise.
523
+ :rtype: bool
524
+ """
525
+ return self._cooldown_timer <= 0.0
526
+
527
+ def consume_move(self):
528
+ """Consume a move action and reset the cooldown timer."""
529
+ self._cooldown_timer = self.move_cooldown
530
+
531
+
532
+ # TODO: Solve too-many-instance-attributes warning later
533
+ # Justification: Context for menu tick needs multiple attributes.
534
+ # pylint: disable=too-many-instance-attributes
535
+ @dataclass
536
+ class MenuTickContext:
537
+ """
538
+ Context for a single tick of the menu scene.
539
+
540
+ :ivar input_frame (InputFrame): The current input frame.
541
+ :ivar dt (float): Delta time since last tick.
542
+ :ivar menu (Menu): The Menu instance.
543
+ :ivar model (MenuModel): The MenuModel instance.
544
+ :ivar commands (CommandQueue): The command queue for pushing commands.
545
+ :ivar intent (MenuIntent | None): The current menu intent.
546
+ :ivar quit_cmd_factory (callable | None): Factory for quit command.
547
+ :ivar packet (RenderPacket | None): The resulting render packet.
477
548
  """
478
- Scene system to manage menu interaction and rendering.
479
549
 
480
- :ivar menu (Menu): The Menu instance being managed.
550
+ input_frame: InputFrame
551
+ dt: float
552
+
553
+ menu: "Menu"
554
+ model: "MenuModel"
555
+ commands: CommandQueue
556
+
557
+ # computed data that systems can write into
558
+ intent: "MenuIntent | None" = None
559
+
560
+ # scene hook: a callable to produce quit cmd (per scene override)
561
+ quit_cmd_factory: callable | None = None
562
+
563
+ # final output
564
+ packet: RenderPacket | None = None
565
+
566
+
567
+ # pylint: enable=too-many-instance-attributes
568
+
569
+
570
+ @dataclass(frozen=True)
571
+ class MenuIntent:
481
572
  """
573
+ Represents the user's intent in the menu for the current tick.
482
574
 
483
- menu: Menu
484
- scene: BaseMenuScene
575
+ :ivar move_up (bool): Whether the user intends to move up.
576
+ :ivar move_down (bool): Whether the user intends to move down.
577
+ :ivar select (bool): Whether the user intends to select the current item.
578
+ :ivar quit (bool): Whether the user intends to quit the menu.
579
+ """
485
580
 
486
- def __init__(self, scene):
487
- super().__init__(scene)
488
- self.menu: Menu | None = None
581
+ move_up: bool = False
582
+ move_down: bool = False
583
+ select: bool = False
584
+ quit: bool = False
489
585
 
490
- def on_enter(self):
491
- self.menu = Menu(
492
- self.scene.menu_items(),
493
- viewport=self.scene.size,
494
- title=self.scene.menu_title,
495
- style=self.scene.menu_style(),
496
- on_select=lambda item: item.on_select.execute(self.scene.game),
586
+
587
+ @dataclass
588
+ class MenuInputSystem:
589
+ """Converts InputFrame -> MenuIntent."""
590
+
591
+ name: str = "menu_input"
592
+ order: int = 10
593
+
594
+ def step(self, ctx: MenuTickContext):
595
+ """Step the input system to extract menu intent."""
596
+ pressed = ctx.input_frame.keys_pressed
597
+ ctx.intent = MenuIntent(
598
+ move_up=Key.UP in pressed,
599
+ move_down=Key.DOWN in pressed,
600
+ select=(Key.ENTER in pressed) or (Key.SPACE in pressed),
601
+ quit=Key.ESCAPE in pressed,
497
602
  )
498
603
 
499
- def handle_event(self, event: Event) -> bool:
500
- if self.menu is None:
501
- return False
502
604
 
503
- # Let menu update selection; on select -> emit event / run command
504
- selected_action = self.menu.handle_event(
505
- event,
506
- up_key=self.scene.model.up_key,
507
- down_key=self.scene.model.down_key,
508
- select_key=self.scene.model.select_key,
509
- )
510
- return selected_action
605
+ @dataclass
606
+ class MenuNavigationSystem:
607
+ """Menu navigation system."""
511
608
 
512
- def draw(self, surface: Backend):
513
- self.menu.set_labels(
514
- [it.resolved_label(self.scene.game) for it in self.menu.items]
515
- )
516
- self.menu.draw(surface)
609
+ name: str = "menu_nav"
610
+ order: int = 20
611
+
612
+ def step(self, ctx: MenuTickContext):
613
+ """Update menu selection based on intent."""
614
+ intent = ctx.intent
615
+ if intent is None:
616
+ return
617
+
618
+ ctx.model.step_timer(ctx.dt)
619
+
620
+ if not ctx.model.can_move():
621
+ return
622
+
623
+ if intent.move_up:
624
+ ctx.menu.move_up()
625
+ ctx.model.selected = ctx.menu.selected_index
626
+ ctx.model.consume_move()
627
+ return
628
+
629
+ if intent.move_down:
630
+ ctx.menu.move_down()
631
+ ctx.model.selected = ctx.menu.selected_index
632
+ ctx.model.consume_move()
633
+
634
+
635
+ @dataclass
636
+ class MenuActionSystem:
637
+ """Menu action execution system."""
638
+
639
+ name: str = "menu_actions"
640
+ order: int = 30
641
+
642
+ def step(self, ctx: MenuTickContext):
643
+ """Execute actions based on menu intent."""
644
+ intent = ctx.intent
645
+ if intent is None:
646
+ return
647
+
648
+ if intent.select and ctx.menu.items:
649
+ item = ctx.menu.items[ctx.menu.selected_index]
650
+ ctx.commands.push(item.command_factory())
517
651
 
652
+ if intent.quit and ctx.quit_cmd_factory is not None:
653
+ cmd = ctx.quit_cmd_factory()
654
+ if cmd is not None:
655
+ ctx.commands.push(cmd)
518
656
 
519
- class BaseMenuScene(Scene):
657
+
658
+ @dataclass
659
+ class MenuRenderSystem:
660
+ """Menu rendering system."""
661
+
662
+ name: str = "menu_render"
663
+ order: int = 100
664
+
665
+ def step(self, ctx: MenuTickContext):
666
+ """Set the render packet to draw the menu."""
667
+ ctx.packet = RenderPacket.from_ops([ctx.menu.draw])
668
+
669
+
670
+ class BaseMenuScene(SimScene):
520
671
  """
521
672
  Base scene class for menu-based scenes.
522
673
 
523
674
  :ivar model (MenuModel): The data model for the menu scene.
524
675
  """
525
676
 
526
- model: MenuModel
677
+ menu: Menu
678
+ systems: SystemPipeline[MenuTickContext]
527
679
 
528
- def __init__(self, game: Game):
529
- super().__init__(game)
680
+ def __init__(self, ctx: RuntimeContext):
681
+ super().__init__(ctx)
530
682
  self.model = MenuModel()
531
683
 
532
- # hooks
533
684
  @property
534
685
  def menu_title(self) -> str | None:
535
686
  """
536
- Optional title text for the menu.
687
+ Get the title of the menu.
537
688
 
538
- :return: Title string or None for no title.
689
+ :return: The menu title string, or None for no title.
539
690
  :rtype: str | None
540
691
  """
541
692
  return None
542
693
 
543
694
  def menu_style(self) -> MenuStyle:
544
695
  """
545
- MenuStyle instance for customizing menu appearance.
696
+ Get the style configuration for the menu.
546
697
 
547
- :return: MenuStyle instance.
698
+ :return: The MenuStyle instance for styling the menu.
548
699
  :rtype: MenuStyle
549
700
  """
550
701
  return MenuStyle()
551
702
 
552
703
  def menu_items(self) -> list[MenuItem]:
553
704
  """
554
- Return the list of MenuItem instances for this menu.
705
+ Get the list of menu items for the menu.
555
706
 
556
- :return: List of MenuItem instances.
707
+ :return: List of MenuItem instances for the menu.
557
708
  :rtype: list[MenuItem]
558
-
559
- :raises NotImplementedError: If not overridden in subclass.
560
709
  """
561
710
  raise NotImplementedError
562
711
 
563
- def quit_command(self) -> BaseCommand[Game] | None:
712
+ def quit_command(self):
564
713
  """
565
- Quit command to bind to ESCAPE and window close events.
714
+ Get the command to execute when quitting the menu.
566
715
 
567
- :return: BaseCommand instance to execute on quit, or None for no action.
568
- :rtype: BaseCommand[Game] | None
716
+ :return: The command to execute on quit.
717
+ :rtype: Command
569
718
  """
570
- return QuitGameCommand() # core default (optional)
719
+ # default behavior: quit game
720
+ return QuitCommand()
571
721
 
572
722
  def on_enter(self):
573
- # install menu system (core)
574
- self.services.systems.add(MenuSystem(self))
723
+ self.menu = Menu(
724
+ self._build_display_items(),
725
+ viewport=self.context.services.window.get_virtual_size(),
726
+ title=self.menu_title,
727
+ style=self.menu_style(),
728
+ )
729
+ self.menu.selected_index = self.model.selected
730
+ self.systems = SystemPipeline()
731
+ self.systems.extend(
732
+ [
733
+ MenuInputSystem(),
734
+ MenuNavigationSystem(),
735
+ MenuActionSystem(),
736
+ MenuRenderSystem(),
737
+ ]
738
+ )
575
739
 
576
- # bind quit if provided
577
- cmd = self.quit_command()
578
- if cmd:
579
- self.services.input.on_key_down(Key.ESCAPE, cmd, "quit")
580
- self.services.input.on_quit(cmd, "quit")
740
+ def tick(self, input_frame: InputFrame, dt: float) -> RenderPacket:
741
+ items = self.menu_items()
742
+ if not items:
743
+ return RenderPacket()
581
744
 
582
- self._systems_on_enter()
745
+ self.model.step_timer(dt)
583
746
 
584
- def handle_event(self, event: Event):
585
- if self._systems_handle_event(event):
586
- return
587
- self.services.input.handle_event(event, self)
747
+ self.menu.set_items(self._build_display_items())
748
+ self.menu.set_selected_index(self.model.selected)
588
749
 
589
- def draw(self, surface: Backend):
590
- self._systems_draw(surface)
750
+ ctx = MenuTickContext(
751
+ input_frame=input_frame,
752
+ dt=dt,
753
+ menu=self.menu,
754
+ model=self.model,
755
+ commands=self.context.command_queue,
756
+ quit_cmd_factory=self.quit_command,
757
+ )
758
+
759
+ self.systems.step(ctx)
591
760
 
592
- def on_exit(self): ...
761
+ # always return packet from pipeline
762
+ return ctx.packet or RenderPacket()
593
763
 
594
- def update(self, dt: float): ...
764
+ def _build_display_items(self) -> list[MenuItem]:
765
+ """
766
+ Resolve dynamic labels (label_fn(ctx)) into the label field the Menu draws.
767
+ Keeps command_factory intact.
768
+ """
769
+ src = self.menu_items()
770
+ out: list[MenuItem] = []
771
+ for it in src:
772
+ out.append(
773
+ MenuItem(
774
+ id=it.id,
775
+ label=it.resolved_label(self.context),
776
+ command_factory=it.command_factory,
777
+ label_fn=it.label_fn,
778
+ )
779
+ )
780
+ return out
@@ -0,0 +1,10 @@
1
+ """
2
+ Mini Arcade Core Utilities Package
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from .deprecated_decorator import deprecated
8
+ from .logging import logger
9
+
10
+ __all__ = ["logger", "deprecated"]
@@ -0,0 +1,45 @@
1
+ """
2
+ Deprecated utilities for the mini-arcade-core package.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import functools
8
+
9
+ from mini_arcade_core.utils.logging import logger
10
+
11
+
12
+ def deprecated(
13
+ reason: str | None = None,
14
+ version: str | None = None,
15
+ alternative: str | None = None,
16
+ ):
17
+ """
18
+ Mark a function as deprecated.
19
+
20
+ :param reason: Optional reason for deprecation
21
+ :type reason: str | None
22
+
23
+ :param version: Optional version when it will be removed
24
+ :type version: str | None
25
+
26
+ :param alternative: Optional alternative function to use
27
+ :type alternative: str | None
28
+ """
29
+
30
+ def decorator(func):
31
+ @functools.wraps(func)
32
+ def wrapper(*args, **kwargs):
33
+ message = f"The function {func.__name__} is deprecated"
34
+ if version:
35
+ message += f" and will be removed in version {version}"
36
+ if reason:
37
+ message += f". {reason}"
38
+ if alternative:
39
+ message += f" Use {alternative} instead."
40
+ logger.warning(message)
41
+ return func(*args, **kwargs)
42
+
43
+ return wrapper
44
+
45
+ return decorator