I generally feel that you should avoid code duplication, and that helper tools can often be very useful. However, module scraping is something that I feel is somewhat tricky. For all the time that you save in declaring things in the same module, and the cleanliness of doing so, in the most naive implementation you lose quite a lot of control.
For example, here you have each module with provides some features X, Y and Z, but most of the time you can’t logically separate a game into distinct logic modules. For example, although you can have your audio module, etc, typically these modules require some level of interaction with one another. In short, code modules tend to be organised and divided in a manner that does not directly map to logical category. The console command scraping assumes that your modules are logically divided. Imagine trying to add some kind of restriction on the kinds of commands that could be executed, or adding some overarching commands that aren’t exclusively audio or physics. These kinds of changes are common enough, and generally speaking, I advise not to restrict yourself to a particular design constraint until as late as possible (often, you can avoid this altogether).
Monster’s suggestion is that you sidestep the scraping code and have the modules register their API to a separate module. This is probably slightly better as you could more easily add more parameters to the registration system, but would still require that you import all the modules at least once before the console commands are used, in order for the commands to be available. With this constraint in mind, why not simply do this explicitly? You’re writing modules that offer an interface to other modules, why not simply define the console commands as clients of that interface? The commands don’t care how the API is implemented, just that they can call it, and as such, it is probably more sensible to move them out of the specifics of the implementation.
Here’s what I would do:
- Create a console factory. This produces a console object.
- Within the factory, register the available commands.
- Return the console object.
If you wanted to add support for permissions, just modify the interface. Rather than placing permissions code inside the command (Which makes it hard to predict if the command could be called successfully), you could use a decorator (which directly associates it with the function), function annotations (which aren’t a great usage of the API here) or, if you want to separate the what from the if, even as part of the registration API.
console.register_command(command_string, command, access_policy)
Here we separate the how (command string) from the what (command) and the if (access policy) which already adds support for access policies, command aliasing, mapping of available commands according to the policy etc.
You probably don’t need access policies.
But, it’s a fun thought experiment. To elaborate on my aversion to “fancy” Python - I wrote my original network library in a manner which heavily relied on metamagic. Great fun, but a real pain to manage. Import order mattered, certain classes returned instances of others when instantiated and when I came to add scene support, I added a context manager paradigm that was very fun, but in the end left my code looking like a desperate attempt to avoid explicit, predictability, which was far easier and nicer to use.
In short, if you come back in two months and want to immediately list where all of your console commands are, and check what you’ve defined, you have to touch several modules, and that’s a pain!
Just my thoughts