169 lines
5.1 KiB
Python
169 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import collections
|
|
import inspect
|
|
from typing import Any, Iterator
|
|
|
|
from twisted.python.modules import PythonAttribute, PythonModule, getModule
|
|
|
|
from automat import MethodicalMachine
|
|
|
|
from ._typed import TypeMachine, InputProtocol, Core
|
|
|
|
|
|
def isOriginalLocation(attr: PythonAttribute | PythonModule) -> bool:
|
|
"""
|
|
Attempt to discover if this appearance of a PythonAttribute
|
|
representing a class refers to the module where that class was
|
|
defined.
|
|
"""
|
|
sourceModule = inspect.getmodule(attr.load())
|
|
if sourceModule is None:
|
|
return False
|
|
|
|
currentModule = attr
|
|
while not isinstance(currentModule, PythonModule):
|
|
currentModule = currentModule.onObject
|
|
|
|
return currentModule.name == sourceModule.__name__
|
|
|
|
|
|
def findMachinesViaWrapper(
|
|
within: PythonModule | PythonAttribute,
|
|
) -> Iterator[tuple[str, MethodicalMachine | TypeMachine[InputProtocol, Core]]]:
|
|
"""
|
|
Recursively yield L{MethodicalMachine}s and their FQPNs within a
|
|
L{PythonModule} or a L{twisted.python.modules.PythonAttribute}
|
|
wrapper object.
|
|
|
|
Note that L{PythonModule}s may refer to packages, as well.
|
|
|
|
The discovery heuristic considers L{MethodicalMachine} instances
|
|
that are module-level attributes or class-level attributes
|
|
accessible from module scope. Machines inside nested classes will
|
|
be discovered, but those returned from functions or methods will not be.
|
|
|
|
@type within: L{PythonModule} or L{twisted.python.modules.PythonAttribute}
|
|
@param within: Where to start the search.
|
|
|
|
@return: a generator which yields FQPN, L{MethodicalMachine} pairs.
|
|
"""
|
|
queue = collections.deque([within])
|
|
visited: set[
|
|
PythonModule
|
|
| PythonAttribute
|
|
| MethodicalMachine
|
|
| TypeMachine[InputProtocol, Core]
|
|
| type[Any]
|
|
] = set()
|
|
|
|
while queue:
|
|
attr = queue.pop()
|
|
value = attr.load()
|
|
if (
|
|
isinstance(value, MethodicalMachine) or isinstance(value, TypeMachine)
|
|
) and value not in visited:
|
|
visited.add(value)
|
|
yield attr.name, value
|
|
elif (
|
|
inspect.isclass(value) and isOriginalLocation(attr) and value not in visited
|
|
):
|
|
visited.add(value)
|
|
queue.extendleft(attr.iterAttributes())
|
|
elif isinstance(attr, PythonModule) and value not in visited:
|
|
visited.add(value)
|
|
queue.extendleft(attr.iterAttributes())
|
|
queue.extendleft(attr.iterModules())
|
|
|
|
|
|
class InvalidFQPN(Exception):
|
|
"""
|
|
The given FQPN was not a dot-separated list of Python objects.
|
|
"""
|
|
|
|
|
|
class NoModule(InvalidFQPN):
|
|
"""
|
|
A prefix of the FQPN was not an importable module or package.
|
|
"""
|
|
|
|
|
|
class NoObject(InvalidFQPN):
|
|
"""
|
|
A suffix of the FQPN was not an accessible object
|
|
"""
|
|
|
|
|
|
def wrapFQPN(fqpn: str) -> PythonModule | PythonAttribute:
|
|
"""
|
|
Given an FQPN, retrieve the object via the global Python module
|
|
namespace and wrap it with a L{PythonModule} or a
|
|
L{twisted.python.modules.PythonAttribute}.
|
|
"""
|
|
# largely cribbed from t.p.reflect.namedAny
|
|
|
|
if not fqpn:
|
|
raise InvalidFQPN("FQPN was empty")
|
|
|
|
components = collections.deque(fqpn.split("."))
|
|
|
|
if "" in components:
|
|
raise InvalidFQPN(
|
|
"name must be a string giving a '.'-separated list of Python "
|
|
"identifiers, not %r" % (fqpn,)
|
|
)
|
|
|
|
component = components.popleft()
|
|
try:
|
|
module = getModule(component)
|
|
except KeyError:
|
|
raise NoModule(component)
|
|
|
|
# find the bottom-most module
|
|
while components:
|
|
component = components.popleft()
|
|
try:
|
|
module = module[component]
|
|
except KeyError:
|
|
components.appendleft(component)
|
|
break
|
|
else:
|
|
module.load()
|
|
else:
|
|
return module
|
|
|
|
# find the bottom-most attribute
|
|
attribute = module
|
|
for component in components:
|
|
try:
|
|
attribute = next(
|
|
child
|
|
for child in attribute.iterAttributes()
|
|
if child.name.rsplit(".", 1)[-1] == component
|
|
)
|
|
except StopIteration:
|
|
raise NoObject("{}.{}".format(attribute.name, component))
|
|
|
|
return attribute
|
|
|
|
|
|
def findMachines(
|
|
fqpn: str,
|
|
) -> Iterator[tuple[str, MethodicalMachine | TypeMachine[InputProtocol, Core]]]:
|
|
"""
|
|
Recursively yield L{MethodicalMachine}s and their FQPNs in and under the a
|
|
Python object specified by an FQPN.
|
|
|
|
The discovery heuristic considers L{MethodicalMachine} instances that are
|
|
module-level attributes or class-level attributes accessible from module
|
|
scope. Machines inside nested classes will be discovered, but those
|
|
returned from functions or methods will not be.
|
|
|
|
@param fqpn: a fully-qualified Python identifier (i.e. the dotted
|
|
identifier of an object defined at module or class scope, including the
|
|
package and modele names); where to start the search.
|
|
|
|
@return: a generator which yields (C{FQPN}, L{MethodicalMachine}) pairs.
|
|
"""
|
|
return findMachinesViaWrapper(wrapFQPN(fqpn))
|