I'm currently working on a CLI abstraction layer, which abstracts CLI programs as classes in Python. Such CLI programs offer a structured way to enable and configure CLI parameters. It helps checking for faulty inputs and generated properly escaped arguments (e.g. adding double quotes).
Note: The following example is using Git, while in my target application, it will be commercial tools, that don't offer any Python API or similar.
Basic Ideas:
- An abstraction of tool Git declares a
Git
class, which derives from classProgram
.
This parent class implements common methods to all programs. - CLI options are represented as nested class definitions on the
Git
class. - Nested classes are marked with a class-based decorator
CLIOption
derived fromAttribute
(see https://github.com/pyTooling/pyAttributes for more details) - CLI options can be enabled / modified via indexed syntax.
- An instance of
Git
is used to enabled / configure CLI parameters and helps to assemble a list of correctly encoded strings that can be used e.g. insubprocess.Popen(...)
tool = Git()
tool[tool.FlagVersion] = True
print(tool.ToArgumentList())
Some Python Code:
from pyAttributes import Attribute
class CLIOption(Attribute): ... # see pyAttributes for more details
class Argument:
_name: ClassVar[str]
def __init_subclass__(cls, *args, name: str = "", **kwargs):
super().__init_subclass__(*args, **kwargs)
cls._name = name
class FlagArgument(Argument): ...
class CommandArgument(Argument): ...
class Program:
__cliOptions__: Dict[Type[Argument], Optional[Argument]]
def __init_subclass__(cls, *args, **kwargs):
"""Hook into subclass creation to register all marked CLI options in ``__cliOptions__``.
super().__init_subclass__(*args, **kwargs)
# get all marked options and
cls.__cliOptions__ = {}
for option in CLIOption.GetClasses():
cls.__cliOptions__[option] = None
class Git(Program):
@CLIOption()
class FlagVersion(FlagArgument, name="--version"): ...
@CLIOption()
class FlagHelp(FlagArgument, name="--help"): ...
@CLIOption()
class CmdCommit(CommandArgument, name="commit"): ...
Observations:
- As @martineau pointed out in a comment, the CLIOption decorator has no access to the outer scope. So the scope can't be annotated to the nested classes.
- The nested classes are used because of some nice effects in Python not demonstrated here. Also to keep their scope local to a program. Imagine there might be multiple programs offering a
FlagVersion
flag. Some as-v
, but others as--version
.
Primary Questions:
- How can I check if class
FlagVersion
is a nested class of classGit
?
What I investigated so far:
- There is no helper function to achieve this goal like functions
isinstance(...)
orissubclass(...)
are offering. - While root-level classes have a
__module__
reference to the outer scope, nested classes have no "pointer" to the next outer scope. - Actually, nested classes have the same
__module__
values.
Which makes sense. - A class'
__qualname__
includes the names of parent classes.
Unfortunately this is a string likeGit.FlagVersion
So I see a possible "ugly" solution using __qualname__
and string operations to check if a class is nested and if it's nested in a certain outer scope.
Algorithm:
- Assemble fully qualified name from
__module__
and__qualname__
. - Check element by element from left to right for matches.
This gets even more complicated if one nested class is defined in a parent class and another nested class is defined in a derived class. Then I also need to look into MRO ... oOo
Secondary Questions:
- Is there a better way than using string operations?
- Shouldn't Pythons data model offer a better way to get this information?