Mini Shell
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))
Zerion Mini Shell 1.0