Search code examples
pythoninner-classes

How to check if class X is a nested class in class Y?


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 class Program.
    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 from Attribute
    (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. in subprocess.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:

  1. How can I check if class FlagVersion is a nested class of class Git?

What I investigated so far:

  • There is no helper function to achieve this goal like functions isinstance(...) or issubclass(...) 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 like Git.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:

  1. Is there a better way than using string operations?
  2. Shouldn't Pythons data model offer a better way to get this information?

Solution

  • Following the proposed approaches by iterating __dict__ works quite good.

    So this was the first solution developed based on the given ideas:

    def isnestedclass(cls: Type, scope: Type) -> bool:
        for memberName in scope.__dict__:
            member = getattr(scope, memberName)
            if type(member) is type:
                if cls is member:
                    return True
    
        return False
    

    That solution doesn't work on members inherited from parent classes. So I extended it with searching through the inheritance graph via mro().

    This is my current and final solution for a isnestedclass helper function.

    def isnestedclass(cls: Type, scope: Type) -> bool:
        for mroClass in scope.mro():
            for memberName in mroClass.__dict__:
                member = getattr(mroClass, memberName)
                if type(member) is type:
                    if cls is member:
                        return True
    
        return False
    

    The function is available within the pyTooling package.