@@ -508,6 +508,338 @@ def decorator(func: CFT) -> CFT:
508
508
509
509
return decorator
510
510
511
+ # cogs
512
+
513
+ def add_cog (self , cog : Cog , * , override : bool = False ) -> None :
514
+ """Adds a "cog" to the bot.
515
+
516
+ A cog is a class that has its own event listeners and commands.
517
+
518
+ .. versionchanged:: 2.0
519
+
520
+ :exc:`.ClientException` is raised when a cog with the same name
521
+ is already loaded.
522
+
523
+ Parameters
524
+ -----------
525
+ cog: :class:`.Cog`
526
+ The cog to register to the bot.
527
+ override: :class:`bool`
528
+ If a previously loaded cog with the same name should be ejected
529
+ instead of raising an error.
530
+
531
+ .. versionadded:: 2.0
532
+
533
+ Raises
534
+ -------
535
+ TypeError
536
+ The cog does not inherit from :class:`.Cog`.
537
+ CommandError
538
+ An error happened during loading.
539
+ .ClientException
540
+ A cog with the same name is already loaded.
541
+ """
542
+
543
+ if not isinstance (cog , Cog ):
544
+ raise TypeError ('cogs must derive from Cog' )
545
+
546
+ cog_name = cog .__cog_name__
547
+ existing = self .__cogs .get (cog_name )
548
+
549
+ if existing is not None :
550
+ if not override :
551
+ raise discord .ClientException (f'Cog named { cog_name !r} already loaded' )
552
+ self .remove_cog (cog_name )
553
+
554
+ cog = cog ._inject (self )
555
+ self .__cogs [cog_name ] = cog
556
+
557
+ def get_cog (self , name : str ) -> Optional [Cog ]:
558
+ """Gets the cog instance requested.
559
+
560
+ If the cog is not found, ``None`` is returned instead.
561
+
562
+ Parameters
563
+ -----------
564
+ name: :class:`str`
565
+ The name of the cog you are requesting.
566
+ This is equivalent to the name passed via keyword
567
+ argument in class creation or the class name if unspecified.
568
+
569
+ Returns
570
+ --------
571
+ Optional[:class:`Cog`]
572
+ The cog that was requested. If not found, returns ``None``.
573
+ """
574
+ return self .__cogs .get (name )
575
+
576
+ def remove_cog (self , name : str ) -> Optional [Cog ]:
577
+ """Removes a cog from the bot and returns it.
578
+
579
+ All registered commands and event listeners that the
580
+ cog has registered will be removed as well.
581
+
582
+ If no cog is found then this method has no effect.
583
+
584
+ Parameters
585
+ -----------
586
+ name: :class:`str`
587
+ The name of the cog to remove.
588
+
589
+ Returns
590
+ -------
591
+ Optional[:class:`.Cog`]
592
+ The cog that was removed. ``None`` if not found.
593
+ """
594
+
595
+ cog = self .__cogs .pop (name , None )
596
+ if cog is None :
597
+ return
598
+
599
+ help_command = self ._help_command
600
+ if help_command and help_command .cog is cog :
601
+ help_command .cog = None
602
+ cog ._eject (self )
603
+
604
+ return cog
605
+
606
+ @property
607
+ def cogs (self ) -> Mapping [str , Cog ]:
608
+ """Mapping[:class:`str`, :class:`Cog`]: A read-only mapping of cog name to cog."""
609
+ return types .MappingProxyType (self .__cogs )
610
+
611
+ # extensions
612
+
613
+ def _remove_module_references (self , name : str ) -> None :
614
+ # find all references to the module
615
+ # remove the cogs registered from the module
616
+ for cogname , cog in self .__cogs .copy ().items ():
617
+ if _is_submodule (name , cog .__module__ ):
618
+ self .remove_cog (cogname )
619
+
620
+ # remove all the commands from the module
621
+ for cmd in self .all_commands .copy ().values ():
622
+ if cmd .module is not None and _is_submodule (name , cmd .module ):
623
+ if isinstance (cmd , GroupMixin ):
624
+ cmd .recursively_remove_all_commands ()
625
+ self .remove_command (cmd .name )
626
+
627
+ # remove all the listeners from the module
628
+ for event_list in self .extra_events .copy ().values ():
629
+ remove = []
630
+ for index , event in enumerate (event_list ):
631
+ if event .__module__ is not None and _is_submodule (name , event .__module__ ):
632
+ remove .append (index )
633
+
634
+ for index in reversed (remove ):
635
+ del event_list [index ]
636
+
637
+ def _call_module_finalizers (self , lib : types .ModuleType , key : str ) -> None :
638
+ try :
639
+ func = getattr (lib , 'teardown' )
640
+ except AttributeError :
641
+ pass
642
+ else :
643
+ try :
644
+ func (self )
645
+ except Exception :
646
+ pass
647
+ finally :
648
+ self .__extensions .pop (key , None )
649
+ sys .modules .pop (key , None )
650
+ name = lib .__name__
651
+ for module in list (sys .modules .keys ()):
652
+ if _is_submodule (name , module ):
653
+ del sys .modules [module ]
654
+
655
+ def _load_from_module_spec (self , spec : importlib .machinery .ModuleSpec , key : str ) -> None :
656
+ # precondition: key not in self.__extensions
657
+ lib = importlib .util .module_from_spec (spec )
658
+ sys .modules [key ] = lib
659
+ try :
660
+ spec .loader .exec_module (lib ) # type: ignore
661
+ except Exception as e :
662
+ del sys .modules [key ]
663
+ raise discord .ExtensionFailed (key , e ) from e
664
+
665
+ try :
666
+ setup = getattr (lib , 'setup' )
667
+ except AttributeError :
668
+ del sys .modules [key ]
669
+ raise discord .NoEntryPointError (key )
670
+
671
+ try :
672
+ setup (self )
673
+ except Exception as e :
674
+ del sys .modules [key ]
675
+ self ._remove_module_references (lib .__name__ )
676
+ self ._call_module_finalizers (lib , key )
677
+ raise discord .ExtensionFailed (key , e ) from e
678
+ else :
679
+ self .__extensions [key ] = lib
680
+
681
+ def _resolve_name (self , name : str , package : Optional [str ]) -> str :
682
+ try :
683
+ return importlib .util .resolve_name (name , package )
684
+ except ImportError :
685
+ raise discord .ExtensionNotFound (name )
686
+
687
+ def load_extension (self , name : str , * , package : Optional [str ] = None ) -> None :
688
+ """Loads an extension.
689
+
690
+ An extension is a python module that contains commands, cogs, or
691
+ listeners.
692
+
693
+ An extension must have a global function, ``setup`` defined as
694
+ the entry point on what to do when the extension is loaded. This entry
695
+ point must have a single argument, the ``bot``.
696
+
697
+ Parameters
698
+ ------------
699
+ name: :class:`str`
700
+ The extension name to load. It must be dot separated like
701
+ regular Python imports if accessing a sub-module. e.g.
702
+ ``foo.test`` if you want to import ``foo/test.py``.
703
+ package: Optional[:class:`str`]
704
+ The package name to resolve relative imports with.
705
+ This is required when loading an extension using a relative path, e.g ``.foo.test``.
706
+ Defaults to ``None``.
707
+
708
+ .. versionadded:: 1.7
709
+
710
+ Raises
711
+ --------
712
+ ExtensionNotFound
713
+ The extension could not be imported.
714
+ This is also raised if the name of the extension could not
715
+ be resolved using the provided ``package`` parameter.
716
+ ExtensionAlreadyLoaded
717
+ The extension is already loaded.
718
+ NoEntryPointError
719
+ The extension does not have a setup function.
720
+ ExtensionFailed
721
+ The extension or its setup function had an execution error.
722
+ """
723
+
724
+ name = self ._resolve_name (name , package )
725
+ if name in self .__extensions :
726
+ raise discord .ExtensionAlreadyLoaded (name )
727
+
728
+ spec = importlib .util .find_spec (name )
729
+ if spec is None :
730
+ raise discord .ExtensionNotFound (name )
731
+
732
+ self ._load_from_module_spec (spec , name )
733
+
734
+ def unload_extension (self , name : str , * , package : Optional [str ] = None ) -> None :
735
+ """Unloads an extension.
736
+
737
+ When the extension is unloaded, all commands, listeners, and cogs are
738
+ removed from the bot and the module is un-imported.
739
+
740
+ The extension can provide an optional global function, ``teardown``,
741
+ to do miscellaneous clean-up if necessary. This function takes a single
742
+ parameter, the ``bot``, similar to ``setup`` from
743
+ :meth:`~.Bot.load_extension`.
744
+
745
+ Parameters
746
+ ------------
747
+ name: :class:`str`
748
+ The extension name to unload. It must be dot separated like
749
+ regular Python imports if accessing a sub-module. e.g.
750
+ ``foo.test`` if you want to import ``foo/test.py``.
751
+ package: Optional[:class:`str`]
752
+ The package name to resolve relative imports with.
753
+ This is required when unloading an extension using a relative path, e.g ``.foo.test``.
754
+ Defaults to ``None``.
755
+
756
+ .. versionadded:: 1.7
757
+
758
+ Raises
759
+ -------
760
+ ExtensionNotFound
761
+ The name of the extension could not
762
+ be resolved using the provided ``package`` parameter.
763
+ ExtensionNotLoaded
764
+ The extension was not loaded.
765
+ """
766
+
767
+ name = self ._resolve_name (name , package )
768
+ lib = self .__extensions .get (name )
769
+ if lib is None :
770
+ raise discord .ExtensionNotLoaded (name )
771
+
772
+ self ._remove_module_references (lib .__name__ )
773
+ self ._call_module_finalizers (lib , name )
774
+
775
+ def reload_extension (self , name : str , * , package : Optional [str ] = None ) -> None :
776
+ """Atomically reloads an extension.
777
+
778
+ This replaces the extension with the same extension, only refreshed. This is
779
+ equivalent to a :meth:`unload_extension` followed by a :meth:`load_extension`
780
+ except done in an atomic way. That is, if an operation fails mid-reload then
781
+ the bot will roll-back to the prior working state.
782
+
783
+ Parameters
784
+ ------------
785
+ name: :class:`str`
786
+ The extension name to reload. It must be dot separated like
787
+ regular Python imports if accessing a sub-module. e.g.
788
+ ``foo.test`` if you want to import ``foo/test.py``.
789
+ package: Optional[:class:`str`]
790
+ The package name to resolve relative imports with.
791
+ This is required when reloading an extension using a relative path, e.g ``.foo.test``.
792
+ Defaults to ``None``.
793
+
794
+ .. versionadded:: 1.7
795
+
796
+ Raises
797
+ -------
798
+ ExtensionNotLoaded
799
+ The extension was not loaded.
800
+ ExtensionNotFound
801
+ The extension could not be imported.
802
+ This is also raised if the name of the extension could not
803
+ be resolved using the provided ``package`` parameter.
804
+ NoEntryPointError
805
+ The extension does not have a setup function.
806
+ ExtensionFailed
807
+ The extension setup function had an execution error.
808
+ """
809
+
810
+ name = self ._resolve_name (name , package )
811
+ lib = self .__extensions .get (name )
812
+ if lib is None :
813
+ raise discord .ExtensionNotLoaded (name )
814
+
815
+ # get the previous module states from sys modules
816
+ modules = {
817
+ name : module
818
+ for name , module in sys .modules .items ()
819
+ if _is_submodule (lib .__name__ , name )
820
+ }
821
+
822
+ try :
823
+ # Unload and then load the module...
824
+ self ._remove_module_references (lib .__name__ )
825
+ self ._call_module_finalizers (lib , name )
826
+ self .load_extension (name )
827
+ except Exception :
828
+ # if the load failed, the remnants should have been
829
+ # cleaned from the load_extension function call
830
+ # so let's load it from our old compiled library.
831
+ lib .setup (self ) # type: ignore
832
+ self .__extensions [name ] = lib
833
+
834
+ # revert sys.modules back to normal and raise back to caller
835
+ sys .modules .update (modules )
836
+ raise
837
+
838
+ @property
839
+ def extensions (self ) -> Mapping [str , types .ModuleType ]:
840
+ """Mapping[:class:`str`, :class:`py:types.ModuleType`]: A read-only mapping of extension name to extension."""
841
+ return types .MappingProxyType (self .__extensions )
842
+
511
843
# help command stuff
512
844
513
845
@property
0 commit comments