Paebbels
Paebbels

Reputation: 16249

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:

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:

Primary Questions:

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

What I investigated so far:

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:

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?

Upvotes: 0

Views: 97

Answers (2)

Paebbels
Paebbels

Reputation: 16249

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.

Upvotes: 0

mxp-xc
mxp-xc

Reputation: 504

Because I don’t understand English very well, I understand by translating is that you need to find out how to get the embedded class decorated by CLIOption in the subclass of Program (Git here). If so, the following methods may help you.

I read some codes of some pyAttributes

from pyAttributes import Attribute

class Program(object):
    __cliOptions__: Dict[Type[Argument], Optional[Argument]]

    def __init_subclass__(cls, *args, **kwargs):
        cls.__cliOptions__ = {}
        for obj in cls.__dict__.values():
            if hasattr(obj, Attribute.__AttributesMemberName__):
                print(obj)
        # for option in CLIOption.GetClasses():
        #     cls.__cliOptions__[option] = None


class Git(Program):
    a = 1
    b = 2

    @CLIOption()
    class FlagVersion(FlagArgument, name="--version"):
        ...

    @CLIOption()
    class FlagHelp(FlagArgument, name="--help"):
        ...

Of course, the above can’t work directly. Later I found that there was of course an error in the Attribute._AppendAttribute method, as follows, I modified it


class CLIOption(Attribute):
    ...  # see pyAttributes for more details

    @staticmethod
    def _AppendAttribute(func: Callable, attribute: 'Attribute') -> None:
        # inherit attributes and prepend myself or create a new attributes list
        if Attribute.__AttributesMemberName__ in func.__dict__:
            func.__dict__[Attribute.__AttributesMemberName__].insert(0, attribute)
        else:
            # The original func.__setattr__(Attribute.__AttributesMemberName__, [attribute]) has an error
            # Because __setattr__ of class FlagVersion is object.__setattr__

            setattr(func, Attribute.__AttributesMemberName__, [attribute])
            # or object.__setattr__(func, Attribute.__AttributesMemberName__, [attribute])

Upvotes: 1

Related Questions